storebound/cli/lib/frontmatter.ts
CRT c99febc45b Review replies: storebound reviews list | draft | send, review-replies skill
App Store customerReviews / customerReviewResponses and Google Play reviews.list / reviews.reply. draft writes one
Markdown file per unanswered review to .storebound/replies (front matter approved: false, never overwritten); send
only sends approved, unsent drafts that pass the checks (Play 350 characters, no incentives or rating requests,
personal data / link / tone warnings) after a confirmation, and marks them sent. MCP: reviews_list, reviews_draft;
send is never exposed.
2026-10-10 23:28:10 +08:00

34 lines
1.6 KiB
TypeScript

// Minimal front matter for the local Markdown drafts storebound writes (review replies): "---", one `key: value` per
// line (strings, numbers, true / false, null; strings JSON-quoted when needed), "---", then the body. Pure.
export type FmValue = string | number | boolean | null;
export function parseFrontMatter(text: string): { data: Record<string, FmValue>; body: string } {
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
if (!m) return { data: {}, body: text };
const data: Record<string, FmValue> = {};
for (const line of m[1]!.split(/\r?\n/)) {
const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
if (!kv) continue;
data[kv[1]!] = scalar(kv[2]!.trim());
}
return { data, body: text.slice(m[0].length) };
}
function scalar(v: string): FmValue {
if (v === 'true') return true;
if (v === 'false') return false;
if (v === 'null' || v === '~' || v === '') return null;
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
if (v.startsWith('"')) { try { return JSON.parse(v) as string; } catch { return v; } }
if (v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
return v;
}
const plain = (s: string) => /^[\w./@+-][\w ./@:+-]*$/.test(s) && !/^(true|false|null|~|-?\d+(\.\d+)?)$/.test(s) && !/ $/.test(s);
export function stringifyFrontMatter(data: Record<string, FmValue | undefined>, body: string): string {
const lines = Object.entries(data).filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}: ${v === null ? 'null' : typeof v === 'string' ? (plain(v) ? v : JSON.stringify(v)) : String(v)}`);
return `---\n${lines.join('\n')}\n---\n${body.startsWith('\n') ? body : '\n' + body}`;
}