wiki_ghostguild/scripts/import-curriculum.js
Jennie Robinson Faber 136ee2442b Add Coop Foundations Curriculum content and import script
9 session pages and 10 PS Guide markdown files for the Baby Ghosts
cooperative foundations curriculum. Import script creates documents in
Outline wiki with cross-links between paired session/PS Guide pages.
2026-03-09 14:33:06 +00:00

297 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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=<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);
});