#!/usr/bin/env node /** * Import Baby Ghosts Coop Foundations Curriculum into Outline wiki. * * - 9 session pages → top-level in "Cooperative Foundations" collection * - 10 PS Guide pages → nested under "Peer Support Playbook" parent doc * - Cross-links added between paired session/PS Guide pages * * Usage: * OUTLINE_URL=https://wiki.ghostguild.org OUTLINE_API_TOKEN= node scripts/import-curriculum.js */ import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const OUTLINE_URL = process.env.OUTLINE_URL; const OUTLINE_API_TOKEN = process.env.OUTLINE_API_TOKEN; const RATE_LIMIT_MS = 200; const SESSIONS_DIR = path.resolve(__dirname, "../content/curriculum/Sessions"); const PS_GUIDES_DIR = path.resolve(__dirname, "../content/curriculum/PS Guides"); if (!OUTLINE_URL || !OUTLINE_API_TOKEN) { console.error("Error: OUTLINE_URL and OUTLINE_API_TOKEN env vars are required."); process.exit(1); } // --- API helper (same pattern as migrate-content.js) --- async function outlineApi(endpoint, body) { const res = await fetch(`${OUTLINE_URL}/api/${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${OUTLINE_API_TOKEN}`, }, body: JSON.stringify(body), }); if (!res.ok) { const text = await res.text(); throw new Error(`API ${endpoint} failed (${res.status}): ${text}`); } return res.json(); } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // --- Session files in order --- const SESSION_FILES = [ "Session 0 – Kickoff + Onboarding.md", "Session 1 – Coop Principles and Power.md", "Session 2 – Shared Purpose and Alignment.md", "Session 3 – Actionable Values and Impact.md", "Session 4 – Decision-Making in Practice.md", "Session 5 – Coop Structures and Governance.md", "Session 6 – Equitable Economics.md", "Session 7 – Conflict Resolution and Collective Care.md", "Session 8 – Self-Evaluation and Pathways.md", ]; // --- PS Guide files with explicit titles --- const PS_GUIDE_FILES = [ { file: "pre-program-onboarding-and-prep.md", title: "Pre-program: Onboarding and Prep", sessionIndex: null, // no cross-link pair }, { file: "0-kickoff-onboarding.md", title: "PS Guide: Session 0 — Kickoff + Onboarding", sessionIndex: 0, }, { file: "1-coop-principles-power.md", title: "PS Guide: Session 1 — Coop Principles and Power", sessionIndex: 1, }, { file: "2-shared-purpose-alignment.md", title: "PS Guide: Session 2 — Shared Purpose and Alignment", sessionIndex: 2, }, { file: "3-actionable-values-impact.md", title: "PS Guide: Session 3 — Actionable Values and Impact", sessionIndex: 3, }, { file: "4-decision-making-in-practice.md", title: "PS Guide: Session 4 — Decision-Making in Practice", sessionIndex: 4, }, { file: "5-coop-structures-governance.md", title: "PS Guide: Session 5 — Coop Structures and Governance", sessionIndex: 5, }, { file: "6-equitable-economics.md", title: "PS Guide: Session 6 — Equitable Economics", sessionIndex: 6, }, { file: "7-conflict-resolution-collective-care.md", title: "PS Guide: Session 7 — Conflict Resolution and Collective Care", sessionIndex: 7, }, { file: "8-self-evaluation-pathways.md", title: "PS Guide: Session 8 — Self-Evaluation and Pathways", sessionIndex: 8, }, ]; // --- Content helpers --- /** Strip the H1 line from markdown content. */ function stripH1(text) { return text.replace(/^# .+\n+/, ""); } /** * Remove PS reference lines that appear near the top of session pages. * Handles both italic and non-italic variants: * *Peer Supports: See **PS Guide: Session N** for pre-session tasks.* * Peer Supports: See **PS Guide: Session N** for pre-session tasks. * Also removes an empty "## Pre-session" heading left behind. */ function stripPsReference(text) { // Remove the PS reference line itself let result = text.replace( /^\*?Peer Supports: See \*\*PS Guide: Session \d\*\* for pre-session tasks\.\*?\n+/m, "" ); // Remove empty "## Pre-session" section (heading followed only by --- or whitespace) result = result.replace(/^## Pre-session\n+(?=---)/m, ""); return result; } // --- Main --- async function main() { // ============================================================ // Phase 1: Find existing collection and parent document // ============================================================ console.log("Phase 1: Finding collection and parent document...\n"); const { data: collections } = await outlineApi("collections.list", { limit: 100 }); const collection = collections.find( (c) => c.name.toLowerCase() === "cooperative foundations" ); if (!collection) { console.error('Error: Collection "Cooperative Foundations" not found.'); process.exit(1); } console.log(` Collection: ${collection.name} (${collection.id})`); await delay(RATE_LIMIT_MS); const { data: searchResults } = await outlineApi("documents.search", { query: "Peer Support Playbook", collectionId: collection.id, limit: 5, }); const parentDoc = searchResults.find( (r) => r.document.title === "Peer Support Playbook" ); if (!parentDoc) { console.error('Error: Document "Peer Support Playbook" not found in collection.'); process.exit(1); } const parentDocId = parentDoc.document.id; console.log(` Parent doc: Peer Support Playbook (${parentDocId})\n`); await delay(RATE_LIMIT_MS); // ============================================================ // Phase 2: Import 9 session pages // ============================================================ console.log("Phase 2: Importing session pages...\n"); const sessions = []; // { id, slug, title } for (const file of SESSION_FILES) { const raw = await fs.readFile(path.join(SESSIONS_DIR, file), "utf-8"); // Extract H1 as title const h1Match = raw.match(/^# (.+)$/m); const title = h1Match ? h1Match[1] : path.basename(file, ".md"); // Strip H1 and PS reference from body let body = stripH1(raw); body = stripPsReference(body); const { data: doc } = await outlineApi("documents.create", { title, text: body.trim(), collectionId: collection.id, publish: true, }); sessions.push({ id: doc.id, slug: doc.url.split("/doc/")[1], title }); console.log(` ✓ ${title}`); await delay(RATE_LIMIT_MS); } // ============================================================ // Phase 3: Import 10 PS Guide pages // ============================================================ console.log("\nPhase 3: Importing PS Guide pages...\n"); const psGuides = []; // { id, slug, title, sessionIndex } for (const entry of PS_GUIDE_FILES) { const raw = await fs.readFile(path.join(PS_GUIDES_DIR, entry.file), "utf-8"); // Strip H1 from body const body = stripH1(raw); const { data: doc } = await outlineApi("documents.create", { title: entry.title, text: body.trim(), collectionId: collection.id, parentDocumentId: parentDocId, publish: true, }); psGuides.push({ id: doc.id, slug: doc.url.split("/doc/")[1], title: entry.title, sessionIndex: entry.sessionIndex, }); console.log(` ✓ ${entry.title}`); await delay(RATE_LIMIT_MS); } // ============================================================ // Phase 4: Add cross-links // ============================================================ console.log("\nPhase 4: Adding cross-links...\n"); for (const psGuide of psGuides) { if (psGuide.sessionIndex === null) continue; // skip pre-program const session = sessions[psGuide.sessionIndex]; if (!session) { console.error(` ✗ No session found for index ${psGuide.sessionIndex}`); continue; } // Update session page: prepend PS Guide link const { data: sessionDoc } = await outlineApi("documents.info", { id: session.id }); await delay(RATE_LIMIT_MS); const sessionCrossLink = `> **Peer Supports:** See [${psGuide.title}](/doc/${psGuide.slug}) for your role during session and this week's studio support meeting.\n\n`; await outlineApi("documents.update", { id: session.id, text: sessionCrossLink + sessionDoc.text, }); console.log(` ✓ ${session.title} → ${psGuide.title}`); await delay(RATE_LIMIT_MS); // Update PS Guide page: prepend session link const { data: psDoc } = await outlineApi("documents.info", { id: psGuide.id }); await delay(RATE_LIMIT_MS); const psCrossLink = `> **Session content:** See [${session.title}](/doc/${session.slug}) for the full curriculum.\n\n`; await outlineApi("documents.update", { id: psGuide.id, text: psCrossLink + psDoc.text, }); console.log(` ✓ ${psGuide.title} → ${session.title}`); await delay(RATE_LIMIT_MS); } console.log(`\nDone. ${sessions.length} sessions + ${psGuides.length} PS Guides imported.`); } main().catch((err) => { console.error("Import failed:", err); process.exit(1); });