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.
This commit is contained in:
parent
dd143b20fc
commit
136ee2442b
21 changed files with 4048 additions and 0 deletions
297
scripts/import-curriculum.js
Normal file
297
scripts/import-curriculum.js
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
#!/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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue