import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { type UniDeskConfig, repoRoot } from "./config"; import { coreInternalFetch } from "./microservices"; type DecisionRecordType = "meeting" | "decision" | "goal" | "external_goal" | "internal_goal" | "blocker" | "debt" | "experiment"; type RequirementRecordType = Exclude; type DecisionRecordLevel = "G0" | "G1" | "G2" | "G3" | "P0" | "P1" | "P2" | "P3" | "none"; type DecisionRecordStatus = "active" | "blocked" | "parked" | "done"; type DecisionDocumentType = "DCSN" | "GOAL" | "PLAN" | "RPRT" | "ACTN" | "ISSU" | "RETR" | "RQST" | "RESP" | "MINS"; type DecisionDocumentPriority = "P0" | "P1" | "P2" | "P3"; const serviceId = "decision-center"; const typeValues = new Set(["meeting", "decision", "goal", "external_goal", "internal_goal", "blocker", "debt", "experiment"]); const requirementTypeValues = new Set(["decision", "goal", "external_goal", "internal_goal", "blocker", "debt", "experiment"]); const levelValues = new Set(["G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "none"]); const statusValues = new Set(["active", "blocked", "parked", "done"]); const documentTypeValues = new Set(["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"]); const documentPriorityValues = new Set(["P0", "P1", "P2", "P3"]); function optionValue(args: string[], names: string[]): string | undefined { for (const name of names) { const index = args.indexOf(name); if (index === -1) continue; const raw = args[index + 1]; if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${name} requires a non-empty value`); return raw; } return undefined; } function optionValues(args: string[], names: string[]): string[] { const values: string[] = []; for (let index = 0; index < args.length; index += 1) { if (!names.includes(args[index] ?? "")) continue; const raw = args[index + 1]; if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${args[index]} requires a non-empty value`); values.push(raw); index += 1; } return values; } function positionalArgs(args: string[]): string[] { const positions: string[] = []; for (let index = 0; index < args.length; index += 1) { const value = args[index] ?? ""; if (value.startsWith("--")) { index += 1; continue; } positions.push(value); } return positions; } function parseType(raw: string | undefined, fallback: DecisionRecordType): DecisionRecordType { const value = raw || fallback; if (!typeValues.has(value as DecisionRecordType)) throw new Error(`--type must be one of: ${Array.from(typeValues).join(", ")}`); 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(", ")}`); return value as DecisionRecordLevel; } function parseStatus(raw: string | undefined, fallback: DecisionRecordStatus): DecisionRecordStatus { const value = raw || fallback; if (!statusValues.has(value as DecisionRecordStatus)) throw new Error(`--status must be one of: ${Array.from(statusValues).join(", ")}`); return value as DecisionRecordStatus; } function parseDocumentNo(raw: string | undefined): string | undefined { if (raw === undefined) return undefined; const value = raw.trim().toUpperCase(); if (!/^DC-(DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS)-P[0-3]-\d{4}-\d{1,9}$/u.test(value)) { throw new Error("--doc-no must match DC----, for example DC-GOAL-P0-2026-001"); } const [prefix, docType, docPriority, docYear, docSeq] = value.split("-"); return `${prefix}-${docType}-${docPriority}-${docYear}-${String(Number(docSeq)).padStart(3, "0")}`; } function parseDocumentType(raw: string | undefined): DecisionDocumentType | undefined { if (raw === undefined) return undefined; const value = raw.toUpperCase(); if (!documentTypeValues.has(value as DecisionDocumentType)) throw new Error(`--doc-type must be one of: ${Array.from(documentTypeValues).join(", ")}`); return value as DecisionDocumentType; } function parseDocumentPriority(raw: string | undefined): DecisionDocumentPriority | undefined { if (raw === undefined) return undefined; const value = raw.toUpperCase(); if (!documentPriorityValues.has(value as DecisionDocumentPriority)) throw new Error(`--doc-priority must be one of: ${Array.from(documentPriorityValues).join(", ")}`); return value as DecisionDocumentPriority; } function parseDocumentYear(raw: string | undefined): number | undefined { if (raw === undefined) return undefined; const value = Number(raw); if (!/^\d{4}$/u.test(raw) || !Number.isInteger(value) || value < 1970 || value > 2100) throw new Error("--doc-year must be a four-digit year between 1970 and 2100"); return value; } function parseDocumentSeq(raw: string | undefined): number | undefined { if (raw === undefined) return undefined; const value = Number(raw); if (!/^\d+$/u.test(raw) || !Number.isInteger(value) || value <= 0) throw new Error("--doc-seq must be a positive integer"); return value; } function addDocumentPayloadFields(payload: Record, args: string[]): void { const docNo = parseDocumentNo(optionValue(args, ["--doc-no", "--docNo", "--document-no", "--documentNo"])); const docType = parseDocumentType(optionValue(args, ["--doc-type", "--docType"])); const docPriority = parseDocumentPriority(optionValue(args, ["--doc-priority", "--docPriority"])); const docYear = parseDocumentYear(optionValue(args, ["--doc-year", "--docYear", "--year"])); const docSeq = parseDocumentSeq(optionValue(args, ["--doc-seq", "--docSeq"])); const signer = optionValue(args, ["--signer"]); const issuedAt = optionValue(args, ["--issued-at", "--issuedAt"]); const effectiveScope = optionValue(args, ["--effective-scope", "--effectiveScope"]); const supersedes = splitList(optionValues(args, ["--supersedes"])); const supersededBy = splitList(optionValues(args, ["--superseded-by", "--supersededBy"])); if (docNo !== undefined) payload.docNo = docNo; if (docType !== undefined) payload.docType = docType; if (docPriority !== undefined) payload.docPriority = docPriority; if (docYear !== undefined) payload.docYear = docYear; if (docSeq !== undefined) payload.docSeq = docSeq; if (signer !== undefined) payload.signer = signer; if (issuedAt !== undefined) payload.issuedAt = issuedAt; if (effectiveScope !== undefined) payload.effectiveScope = effectiveScope; if (supersedes.length > 0) payload.supersedes = supersedes; if (supersededBy.length > 0) payload.supersededBy = supersededBy; } function splitList(values: string[]): string[] { return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))]; } function readMarkdownFile(path: string): { absolutePath: string; markdown: string } { const absolutePath = resolve(repoRoot, path); const markdown = readFileSync(absolutePath, "utf8"); if (markdown.trim().length === 0) throw new Error(`markdown file is empty: ${absolutePath}`); if (markdown.length > 4_500_000) throw new Error(`markdown file is too large: ${absolutePath}`); return { absolutePath, markdown }; } function optionalBodyFromArgs(args: string[], command: string): { body: string | undefined; bodySource: Record } { 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 } }; } return { body: undefined, bodySource: { kind: "none" } }; } function bodyFromArgs(args: string[], command: string): { body: string; bodySource: Record } { const { body, bodySource } = optionalBodyFromArgs(args, command); if (body !== undefined) return { body, bodySource }; 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); } async function decisionProxyAsync( fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise, path: string, init?: { method?: string; body?: unknown }, ): Promise { return await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init); } function unwrapProxyResponse(response: unknown): unknown { const record = typeof response === "object" && response !== null && !Array.isArray(response) ? response as Record : {}; if (record.ok !== true) return response; const body = record.body; return { upstream: { ok: record.ok, status: record.status }, body }; } function uploadMeeting(args: string[]): unknown { const file = positionalArgs(args)[0]; if (!file) throw new Error("decision upload requires markdown file"); const { absolutePath, markdown } = readMarkdownFile(file); const type = parseType(optionValue(args, ["--type"]), "meeting"); const payload = { markdown, title: optionValue(args, ["--title"]), type, level: parseLevel(optionValue(args, ["--level", "--priority"]), "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"])), source: optionValue(args, ["--source"]), sourceSession: optionValue(args, ["--source-session", "--sourceSession"]), issueId: optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]), taskId: optionValue(args, ["--task-id", "--taskId"]), commitId: optionValue(args, ["--commit-id", "--commitId"]), }; addDocumentPayloadFields(payload, args); const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records"; const body = type === "meeting" ? payload : { ...payload, body: markdown }; return { file: absolutePath, result: unwrapProxyResponse(decisionProxy(endpoint, { method: "POST", body })) }; } async function uploadMeetingAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { const file = positionalArgs(args)[0]; if (!file) throw new Error("decision upload requires markdown file"); const { absolutePath, markdown } = readMarkdownFile(file); const type = parseType(optionValue(args, ["--type"]), "meeting"); const payload = { markdown, title: optionValue(args, ["--title"]), type, level: parseLevel(optionValue(args, ["--level", "--priority"]), "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"])), source: optionValue(args, ["--source"]), sourceSession: optionValue(args, ["--source-session", "--sourceSession"]), issueId: optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]), taskId: optionValue(args, ["--task-id", "--taskId"]), commitId: optionValue(args, ["--commit-id", "--commitId"]), }; addDocumentPayloadFields(payload, args); const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records"; const body = type === "meeting" ? payload : { ...payload, body: markdown }; return { file: absolutePath, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, endpoint, { method: "POST", body })) }; } function summarizeDiaryImportResult(result: unknown, includeEntries: boolean): unknown { const body = typeof result === "object" && result !== null && !Array.isArray(result) ? result as Record : {}; const entries = Array.isArray(body.entries) ? body.entries : []; const summaryEntries = entries.map((item) => { const record = typeof item === "object" && item !== null && !Array.isArray(item) ? item as Record : {}; return { id: record.id, date: record.date, month: record.month, title: record.title, markdownPath: record.markdownPath, summary: record.summary, sourceFile: record.sourceFile, updatedAt: record.updatedAt, }; }); return { ...body, entries: includeEntries ? summaryEntries : summaryEntries.slice(0, 20), entriesOmitted: includeEntries ? 0 : Math.max(0, summaryEntries.length - 20), outputPolicy: includeEntries ? { bounded: false, entries: summaryEntries.length } : { bounded: true, entriesShown: Math.min(summaryEntries.length, 20), fullCommand: "Re-run with --include-entries to show every imported day summary." }, }; } function diaryImportPayload(args: string[]): { absolutePath: string; payload: Record } { const file = positionalArgs(args)[0]; if (!file) throw new Error("decision diary import requires markdown file"); const { absolutePath, markdown } = readMarkdownFile(file); return { absolutePath, payload: { markdown, sourceFile: optionValue(args, ["--source-file", "--source-path", "--source"]) ?? absolutePath, tags: splitList(optionValues(args, ["--tag", "--tags"])), }, }; } function importDiary(args: string[]): unknown { const { absolutePath, payload } = diaryImportPayload(args); const result = unwrapProxyResponse(decisionProxy("/api/diary/import", { method: "POST", body: payload })); const body = typeof result === "object" && result !== null && !Array.isArray(result) && "body" in result ? (result as { body?: unknown }).body : result; return { file: absolutePath, result: { ...(typeof result === "object" && result !== null && !Array.isArray(result) && "upstream" in result ? { upstream: (result as { upstream?: unknown }).upstream } : {}), body: summarizeDiaryImportResult(body, args.includes("--include-entries")) } }; } async function importDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { const { absolutePath, payload } = diaryImportPayload(args); const result = unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/import", { method: "POST", body: payload })); const body = typeof result === "object" && result !== null && !Array.isArray(result) && "body" in result ? (result as { body?: unknown }).body : result; return { file: absolutePath, result: { ...(typeof result === "object" && result !== null && !Array.isArray(result) && "upstream" in result ? { upstream: (result as { upstream?: unknown }).upstream } : {}), body: summarizeDiaryImportResult(body, args.includes("--include-entries")) } }; } function listRecords(args: string[]): unknown { 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): Promise { 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", "--priority"]); const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]); const source = optionValue(args, ["--source"]); const issueId = optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]); const tag = optionValue(args, ["--tag", "--tags"]); const queryText = optionValue(args, ["--query", "--q"]); const limit = optionValue(args, ["--limit"]); const docNo = parseDocumentNo(optionValue(args, ["--doc-no", "--docNo", "--document-no", "--documentNo"])); const docType = parseDocumentType(optionValue(args, ["--doc-type", "--docType"])); const docPriority = parseDocumentPriority(optionValue(args, ["--doc-priority", "--docPriority"])); const docYear = parseDocumentYear(optionValue(args, ["--doc-year", "--docYear", "--year"])); if (docNo !== undefined) params.set("docNo", docNo); if (docType !== undefined) params.set("docType", docType); if (docPriority !== undefined) params.set("docPriority", docPriority); if (docYear !== undefined) params.set("docYear", String(docYear)); 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 (source !== undefined) params.set("source", source); if (issueId !== undefined) params.set("issueId", issueId); if (tag !== undefined) params.set("tag", tag); if (queryText !== undefined) params.set("q", queryText); if (limit !== undefined) params.set("limit", limit); if (args.includes("--include-body")) params.set("includeBody", "true"); return params.toString(); } function diaryQuery(args: string[]): string { const params = new URLSearchParams(); const month = optionValue(args, ["--month"]); const from = optionValue(args, ["--from"]); const to = optionValue(args, ["--to"]); const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]); const limit = optionValue(args, ["--limit"]); if (month !== undefined) params.set("month", month); if (from !== undefined) params.set("from", from); if (to !== undefined) params.set("to", to); if (sourceFile !== undefined) params.set("sourceFile", sourceFile); if (limit !== undefined) params.set("limit", limit); if (args.includes("--include-body")) params.set("includeBody", "true"); const query = params.toString(); return query ? `?${query}` : ""; } function listDiary(args: string[]): unknown { return unwrapProxyResponse(decisionProxy(`/api/diary/entries${diaryQuery(args)}`)); } async function listDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries${diaryQuery(args)}`)); } function diaryHistory(args: string[]): unknown { return unwrapProxyResponse(decisionProxy(`/api/diary/history${diaryQuery(args)}`)); } async function diaryHistoryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/history${diaryQuery(args)}`)); } function listDiaryMonths(): unknown { return unwrapProxyResponse(decisionProxy("/api/diary/months")); } async function listDiaryMonthsAsync(fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/months")); } function diaryShowQuery(args: string[]): string { const params = new URLSearchParams(); const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]); if (sourceFile !== undefined) params.set("sourceFile", sourceFile); const query = params.toString(); return query ? `?${query}` : ""; } function showDiary(key: string | undefined, args: string[] = []): unknown { if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date"); return unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`)); } async function showDiaryAsync(key: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise, args: string[] = []): Promise { if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date"); return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`)); } function todayDiary(): unknown { return unwrapProxyResponse(decisionProxy("/api/diary/today")); } async function todayDiaryAsync(fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/today")); } function diaryEditPayload(args: string[], command: string): { key: string; payload: Record; bodySource: Record } { const key = positionalArgs(args)[0]; if (!key) throw new Error(`${command} requires entry id or YYYY-MM-DD date`); const { body, bodySource } = optionalBodyFromArgs(args, command); const payload: Record = {}; const title = optionValue(args, ["--title"]); const summary = optionValue(args, ["--summary"]); const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]); if (body !== undefined) payload.body = body; if (title !== undefined) payload.title = title; if (summary !== undefined) payload.summary = summary; if (sourceFile !== undefined) payload.sourceFile = sourceFile; const tags = splitList(optionValues(args, ["--tag", "--tags"])); if (tags.length > 0) payload.tags = tags; if (Object.keys(payload).length === 0) throw new Error(`${command} requires --body text, --body-file path, --summary, --title, --source-file, or --tag`); 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): Promise { 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 editTodayDiary(args: string[]): unknown { const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit"); const payload: Record = {}; const title = optionValue(args, ["--title"]); const summary = optionValue(args, ["--summary"]); const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]); if (body !== undefined) payload.body = body; if (title !== undefined) payload.title = title; if (summary !== undefined) payload.summary = summary; if (sourceFile !== undefined) payload.sourceFile = sourceFile; const tags = splitList(optionValues(args, ["--tag", "--tags"])); if (tags.length > 0) payload.tags = tags; if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag"); return { bodySource, result: unwrapProxyResponse(decisionProxy("/api/diary/today", { method: "PUT", body: payload })) }; } async function editTodayDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit"); const payload: Record = {}; const title = optionValue(args, ["--title"]); const summary = optionValue(args, ["--summary"]); const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]); if (body !== undefined) payload.body = body; if (title !== undefined) payload.title = title; if (summary !== undefined) payload.summary = summary; if (sourceFile !== undefined) payload.sourceFile = sourceFile; const tags = splitList(optionValues(args, ["--tag", "--tags"])); if (tags.length > 0) payload.tags = tags; if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag"); return { bodySource, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/today", { 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)}`)); } async function showRecordAsync(id: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { if (!id) throw new Error("decision show requires record id"); return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records/${encodeURIComponent(id)}`)); } function requirementPayload(args: string[], command: string, options: { partial?: boolean; includeId?: boolean } = {}): Record { 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; const payload: Record = {}; const id = optionValue(args, ["--id"]); const type = optionValue(args, ["--type"]); const level = optionValue(args, ["--level", "--priority"]); const status = optionValue(args, ["--status"]); const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]); const source = optionValue(args, ["--source"]); const sourceSession = optionValue(args, ["--source-session", "--sourceSession"]); const issueId = optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]); const taskId = optionValue(args, ["--task-id", "--taskId"]); const commitId = optionValue(args, ["--commit-id", "--commitId"]); const tags = splitList(optionValues(args, ["--tag", "--tags"])); const evidenceLinks = splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])); if (options.includeId !== false && id !== undefined) payload.id = id; if (title !== undefined) payload.title = title; if (body !== undefined) payload.body = body; if (type !== undefined || options.partial !== true) payload.type = parseRequirementType(type, "external_goal"); if (level !== undefined || options.partial !== true) payload.level = parseLevel(level, "none"); if (status !== undefined || options.partial !== true) payload.status = parseStatus(status, "active"); if (linkedGoalId !== undefined) payload.linkedGoalId = linkedGoalId; if (tags.length > 0 || options.partial !== true) payload.tags = tags; if (evidenceLinks.length > 0 || options.partial !== true) payload.evidenceLinks = evidenceLinks; if (source !== undefined) payload.source = source; if (sourceSession !== undefined) payload.sourceSession = sourceSession; if (issueId !== undefined) payload.issueId = issueId; if (taskId !== undefined) payload.taskId = taskId; if (commitId !== undefined) payload.commitId = commitId; addDocumentPayloadFields(payload, args); if (Object.keys(payload).length === 0) throw new Error(`${command} requires at least one field to write`); if (options.partial !== true && title === undefined && body === undefined) throw new Error(`${command} requires --title or --body-file/--body`); return payload; } function createRequirement(args: string[]): unknown { return unwrapProxyResponse(decisionProxy("/api/requirements", { method: "POST", body: requirementPayload(args, "decision requirement create") })); } async function createRequirementAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/requirements", { method: "POST", body: requirementPayload(args, "decision requirement create") })); } 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): Promise { return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/requirements", { method: "PUT", body: requirementPayload(args, "decision requirement upsert") })); } function showRequirement(id: string | undefined): unknown { if (!id) throw new Error("decision requirement show requires record id or docNo"); return unwrapProxyResponse(decisionProxy(`/api/requirements/${encodeURIComponent(id)}`)); } async function showRequirementAsync(id: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { if (!id) throw new Error("decision requirement show requires record id or docNo"); return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements/${encodeURIComponent(id)}`)); } function updateRequirement(id: string | undefined, args: string[]): unknown { if (!id) throw new Error("decision requirement update requires record id or docNo"); return unwrapProxyResponse(decisionProxy(`/api/requirements/${encodeURIComponent(id)}`, { method: "PUT", body: requirementPayload(args, "decision requirement update", { partial: true, includeId: false }) })); } async function updateRequirementAsync(id: string | undefined, args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise): Promise { if (!id) throw new Error("decision requirement update requires record id or docNo"); return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements/${encodeURIComponent(id)}`, { method: "PUT", body: requirementPayload(args, "decision requirement update", { partial: true, includeId: false }) })); } export async function runDecisionCenterCommand(_config: UniDeskConfig, args: string[]): Promise { const [action = "list", id] = args; if (action === "diary") { const [diaryAction = "list", diaryId] = args.slice(1); if (diaryAction === "import") return importDiary(args.slice(2)); if (diaryAction === "list") return listDiary(args.slice(2)); if (diaryAction === "history") return diaryHistory(args.slice(2)); if (diaryAction === "months") return listDiaryMonths(); if (diaryAction === "today") return args.includes("--edit") ? editTodayDiary(args.slice(2).filter((arg) => arg !== "--edit")) : todayDiary(); if (diaryAction === "show") return showDiary(diaryId, args.slice(3)); if (diaryAction === "edit" || diaryAction === "upsert") return editDiary(args.slice(2)); throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert"); } if (action === "requirement" || action === "requirements") { const [requirementAction = "list", requirementId] = args.slice(1); if (requirementAction === "list") { const query = recordQuery(args.slice(2), { requirementOnly: true }); return unwrapProxyResponse(decisionProxy(`/api/requirements${query ? `?${query}` : ""}`)); } if (requirementAction === "create") return createRequirement(args.slice(2)); if (requirementAction === "upsert") return upsertRequirement(args.slice(2)); if (requirementAction === "show") return showRequirement(requirementId); if (requirementAction === "update") return updateRequirement(requirementId, args.slice(3)); throw new Error("decision requirement command must be one of: list, create, show, update, upsert"); } if (action === "upload") return uploadMeeting(args.slice(1)); if (action === "list") return listRecords(args.slice(1)); if (action === "show") return showRecord(id); if (action === "health") return unwrapProxyResponse(coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/health`)); throw new Error("decision command must be one of: upload, list, show, health, requirement, diary"); } export async function runDecisionCenterCommandAsync( _config: UniDeskConfig, args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise, ): Promise { const [action = "list", id] = args; if (action === "diary") { const [diaryAction = "list", diaryId] = args.slice(1); if (diaryAction === "import") return importDiaryAsync(args.slice(2), fetcher); if (diaryAction === "list") return listDiaryAsync(args.slice(2), fetcher); if (diaryAction === "history") return diaryHistoryAsync(args.slice(2), fetcher); if (diaryAction === "months") return listDiaryMonthsAsync(fetcher); if (diaryAction === "today") return args.includes("--edit") ? editTodayDiaryAsync(args.slice(2).filter((arg) => arg !== "--edit"), fetcher) : todayDiaryAsync(fetcher); if (diaryAction === "show") return showDiaryAsync(diaryId, fetcher, args.slice(3)); if (diaryAction === "edit" || diaryAction === "upsert") return editDiaryAsync(args.slice(2), fetcher); throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert"); } if (action === "requirement" || action === "requirements") { const [requirementAction = "list", requirementId] = 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 === "create") return createRequirementAsync(args.slice(2), fetcher); if (requirementAction === "upsert") return upsertRequirementAsync(args.slice(2), fetcher); if (requirementAction === "show") return showRequirementAsync(requirementId, fetcher); if (requirementAction === "update") return updateRequirementAsync(requirementId, args.slice(3), fetcher); throw new Error("decision requirement command must be one of: list, create, show, update, upsert"); } if (action === "upload") return uploadMeetingAsync(args.slice(1), fetcher); if (action === "list") return listRecordsAsync(args.slice(1), fetcher); if (action === "show") return showRecordAsync(id, fetcher); if (action === "health") return unwrapProxyResponse(await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/health`)); throw new Error("decision command must be one of: upload, list, show, health, requirement, diary"); }