diff --git a/.env.example b/.env.example index cf455ff..cfc57a2 100644 --- a/.env.example +++ b/.env.example @@ -17,9 +17,12 @@ NODE_ENV=development # Public gitsheets data repo # --------------------------------------------------------------------------- -# Absolute path (or relative from repo root) to the codeforphilly-data working tree. -# Clone https://github.com/CodeForPhilly/codeforphilly-data-snapshot as a sibling: -# git clone https://github.com/CodeForPhilly/codeforphilly-data-snapshot ../codeforphilly-data +# Path to the codeforphilly-data working tree. Relative paths are resolved +# from the API workspace dir (`apps/api/`) — prefer absolute paths in dev. +# Bootstrap a fresh dev data repo with the sheet configs the API needs: +# npm run -w apps/api script:setup-dev-data +# (Uses this CFP_DATA_REPO_PATH; creates the directory + git repo + .gitsheets/.toml.) +# In production, point at a clone of https://github.com/CodeForPhilly/codeforphilly-data. CFP_DATA_REPO_PATH=../codeforphilly-data # Git remote URL to push public data commits to. Optional in dev; required in prod. diff --git a/.gitignore b/.gitignore index 63587d1..7ae6cb1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,14 @@ build/ # it ships in the code repo so contributors can load a fake-data baseline. private-storage/ +# Dev data repo created by `npm run -w apps/api script:setup-dev-data`. +# Resolved relative to apps/api/ per .env.example's CFP_DATA_REPO_PATH default. +apps/codeforphilly-data/ +codeforphilly-data/ + +# Agent worktrees (claude-code background agents create these here). +.claude/worktrees/ + # Editor / OS .DS_Store .vscode/* diff --git a/apps/api/package.json b/apps/api/package.json index 58ea467..2797f88 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,12 +5,13 @@ "type": "module", "main": "dist/index.js", "scripts": { - "dev": "tsx watch src/index.ts", + "dev": "tsx watch --env-file=../../.env src/index.ts", "build": "tsc -p tsconfig.json", "start": "node dist/index.js", "type-check": "tsc -p tsconfig.json --noEmit", "test": "vitest run", - "script:scrub-data": "tsx scripts/scrub-data.ts" + "script:scrub-data": "tsx scripts/scrub-data.ts", + "script:setup-dev-data": "tsx scripts/setup-dev-data.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1048.0", diff --git a/apps/api/scripts/setup-dev-data.ts b/apps/api/scripts/setup-dev-data.ts new file mode 100644 index 0000000..6b4020d --- /dev/null +++ b/apps/api/scripts/setup-dev-data.ts @@ -0,0 +1,105 @@ +// Seed a local development data repo with the minimal sheet configs the API +// needs to boot. Run once after `cp .env.example .env`: +// +// npm run -w apps/api script:setup-dev-data [-- ] +// +// `` defaults to the value of `CFP_DATA_REPO_PATH` in the env, or +// `../codeforphilly-data` if unset. +// +// What this does: +// 1. Creates the directory if missing +// 2. `git init -b main` if not already a repo +// 3. Writes one `.gitsheets/.toml` per declared sheet (minimal +// `[gitsheet]` block — root + a placeholder path template) +// 4. Commits as `initial: dev data repo configs` +// +// The path templates here are dev-stub defaults, not authoritative. The real +// templates are authored alongside the production data repo. For an empty +// dev repo with no records, the path template doesn't matter at boot — only +// the existence of `.gitsheets/.toml` does. + +import { execFile } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; +import { promisify } from 'node:util'; + +const exec = promisify(execFile); + +const SHEET_CONFIGS: ReadonlyArray<{ name: string; root: string; path: string }> = [ + { name: 'people', root: 'people', path: '${{ slug }}' }, + { name: 'projects', root: 'projects', path: '${{ slug }}' }, + { name: 'project-memberships', root: 'project-memberships', path: '${{ projectSlug }}/${{ personSlug }}' }, + { name: 'project-updates', root: 'project-updates', path: '${{ projectSlug }}/${{ number }}' }, + { name: 'project-buzz', root: 'project-buzz', path: '${{ projectSlug }}/${{ slug }}' }, + { name: 'help-wanted-roles', root: 'help-wanted-roles', path: '${{ projectSlug }}/${{ slug }}' }, + { name: 'help-wanted-interest', root: 'help-wanted-interest', path: '${{ roleId }}/${{ personId }}' }, + { name: 'tags', root: 'tags', path: '${{ namespace }}/${{ slug }}' }, + { name: 'tag-assignments', root: 'tag-assignments', path: '${{ taggableType }}/${{ taggableId }}/${{ tagId }}' }, + { name: 'slug-history', root: 'slug-history', path: '${{ entityType }}/${{ slug }}' }, + { name: 'revocations', root: 'revocations', path: '${{ jti }}' }, +]; + +function configToml(root: string, path: string): string { + return `[gitsheet]\nroot = '${root}'\npath = '${path}'\n`; +} + +async function main(): Promise { + const argPath = process.argv[2]; + const envPath = process.env['CFP_DATA_REPO_PATH']; + const targetPath = resolve(argPath ?? envPath ?? '../codeforphilly-data'); + + console.log(`[setup-dev-data] target: ${targetPath}`); + + if (!existsSync(targetPath)) { + await mkdir(targetPath, { recursive: true }); + console.log(`[setup-dev-data] created directory`); + } + + const git = async (...args: string[]): Promise => { + const { stdout } = await exec('git', args, { cwd: targetPath }); + return stdout; + }; + + if (!existsSync(join(targetPath, '.git'))) { + await git('init', '-b', 'main'); + await git('config', 'user.email', 'dev@local'); + await git('config', 'user.name', 'Dev'); + await git('commit', '--allow-empty', '-m', 'initial'); + console.log(`[setup-dev-data] initialized git repo`); + } + + const gitsheetsDir = join(targetPath, '.gitsheets'); + await mkdir(gitsheetsDir, { recursive: true }); + + let wroteAny = false; + for (const { name, root, path } of SHEET_CONFIGS) { + const configPath = join(gitsheetsDir, `${name}.toml`); + if (existsSync(configPath)) { + console.log(`[setup-dev-data] skip ${name}.toml (already exists)`); + continue; + } + await writeFile(configPath, configToml(root, path), 'utf8'); + wroteAny = true; + console.log(`[setup-dev-data] wrote ${name}.toml`); + } + + if (!wroteAny) { + console.log(`[setup-dev-data] all sheet configs already present — nothing to commit`); + return; + } + + await git('add', '.gitsheets'); + const { stdout: porcelain } = await exec('git', ['status', '--porcelain'], { cwd: targetPath }); + if (porcelain.trim() === '') { + console.log(`[setup-dev-data] nothing to commit (configs were unchanged)`); + return; + } + await git('commit', '-m', 'initial: dev data repo configs'); + console.log(`[setup-dev-data] committed sheet configs`); +} + +main().catch((err: unknown) => { + console.error(`[setup-dev-data] failed:`, err); + process.exit(1); +});