octocms 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "../chunk-33PHLOAE.js";
9
9
 
10
10
  // package.json
11
- var version = "0.3.4";
11
+ var version = "0.3.5";
12
12
 
13
13
  // cli/index.ts
14
14
  var HELP = `
@@ -1 +1 @@
1
- {"version":3,"sources":["../../package.json","../../cli/index.ts"],"sourcesContent":["{\n \"name\": \"octocms\",\n \"version\": \"0.3.4\",\n \"description\": \"A file-based CMS for Next.js — schema-driven, Git-backed, no database\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"keywords\": [\n \"cms\",\n \"nextjs\",\n \"headless-cms\",\n \"git-cms\",\n \"content-management\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/octocms/octocms.git\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"require\": \"./dist/index.cjs\",\n \"import\": \"./dist/index.js\"\n },\n \"./query\": {\n \"types\": \"./dist/query.d.ts\",\n \"require\": \"./dist/query.cjs\",\n \"import\": \"./dist/query.js\"\n },\n \"./config\": {\n \"types\": \"./dist/config.d.ts\",\n \"require\": \"./dist/config.cjs\",\n \"import\": \"./dist/config.js\"\n },\n \"./defineConfig\": {\n \"types\": \"./dist/defineConfig.d.ts\",\n \"require\": \"./dist/defineConfig.cjs\",\n \"import\": \"./dist/defineConfig.js\"\n },\n \"./types\": {\n \"types\": \"./dist/types.d.ts\",\n \"require\": \"./dist/types.cjs\",\n \"import\": \"./dist/types.js\"\n },\n \"./withOctoCMS\": {\n \"types\": \"./dist/withOctoCMS.d.ts\",\n \"require\": \"./dist/withOctoCMS.cjs\",\n \"import\": \"./dist/withOctoCMS.js\"\n },\n \"./components/public\": {\n \"types\": \"./dist/components/public/index.d.ts\",\n \"require\": \"./dist/components/public/index.cjs\",\n \"import\": \"./dist/components/public/index.js\"\n },\n \"./admin/AdminApp\": \"./admin/AdminApp.tsx\",\n \"./admin/pages/AdminLayout\": \"./admin/pages/AdminLayout.tsx\",\n \"./globals.css\": \"./globals.css\",\n \"./docs/*\": \"./docs/*\"\n },\n \"bin\": {\n \"octocms\": \"dist/cli/index.js\"\n },\n \"files\": [\n \"dist\",\n \"docs\",\n \"globals.css\",\n \"admin\",\n \"components\",\n \"hooks\",\n \"lib\",\n \"index.ts\",\n \"query.ts\",\n \"config.ts\",\n \"defineConfig.ts\",\n \"types.ts\",\n \"withOctoCMS.ts\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"prepublishOnly\": \"npm run checks && npm run build\",\n \"lint\": \"oxlint .\",\n \"lint:fix\": \"oxlint --fix .\",\n \"fmt\": \"oxfmt .\",\n \"fmt:check\": \"oxfmt --check .\",\n \"test\": \"vitest\",\n \"test:run\": \"vitest run\",\n \"ts-check\": \"tsc --noEmit --incremental false\",\n \"checks\": \"npm run lint && npm run ts-check && npm run fmt:check && npm run test:run\",\n \"checks:fix\": \"npm run lint:fix && npm run fmt && npm run test:run\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\",\n \"react-dom\": \">=18\"\n },\n \"dependencies\": {\n \"@mdxeditor/editor\": \"^3.11.4\",\n \"@radix-ui/react-avatar\": \"^1.1.11\",\n \"@radix-ui/react-dialog\": \"^1.1.15\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n \"@radix-ui/react-label\": \"^2.1.8\",\n \"@radix-ui/react-select\": \"^2.2.6\",\n \"@radix-ui/react-slot\": \"^1.2.4\",\n \"@radix-ui/react-tabs\": \"^1.1.13\",\n \"@radix-ui/react-toast\": \"^1.2.15\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"glob\": \"^11.0.0\",\n \"lucide-react\": \"^1.7.0\",\n \"minisearch\": \"^7.2.0\",\n \"next-auth\": \"^4.24.13\",\n \"octokit\": \"^4.1.4\",\n \"react-markdown\": \"^10.1.0\",\n \"rehype-sanitize\": \"^6.0.0\",\n \"remark-gfm\": \"^4.0.1\",\n \"remark-mdx\": \"^3.1.1\",\n \"sharp\": \"^0.34.5\",\n \"slugify\": \"^1.6.9\",\n \"sonner\": \"^2.0.7\",\n \"tailwind-merge\": \"^3.5.0\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@testing-library/dom\": \"^10.4.1\",\n \"@testing-library/react\": \"^16.3.2\",\n \"@types/node\": \"^20\",\n \"@types/react\": \"^19.2.14\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react\": \"^6.0.1\",\n \"jsdom\": \"^29.0.1\",\n \"oxfmt\": \"^0.11.0\",\n \"oxlint\": \"^0.17.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"^5.9.3\",\n \"vite-tsconfig-paths\": \"^6.1.1\",\n \"vitest\": \"^4.1.2\"\n }\n}\n","#!/usr/bin/env node\n/* eslint-disable no-console */\n\n/**\n * OctoCMS CLI — command-line tools for managing an OctoCMS project.\n *\n * Commands:\n * init Initialize OctoCMS in a Next.js project\n * dev Start development server with config watching\n * types:gen Generate TypeScript types from next.config.ts\n * validate Validate all content entries against the schema\n * update Regenerate admin route files\n * agent-docs Inject AI agent doc links into AGENTS.md\n *\n * Usage:\n * octocms <command> [options]\n * octocms --help\n * octocms --version\n */\n\nimport { fmt, log } from './lib/logger';\nimport { resolveProjectRoot } from './lib/project';\nimport { version as VERSION } from '../package.json';\n\nconst HELP = `\n ${fmt.bold('OctoCMS CLI')} v${VERSION}\n\n Usage: octocms <command> [options]\n\n ${fmt.bold('Commands:')}\n init Initialize OctoCMS in a Next.js project\n dev Start development server with config watching\n types:gen Generate TypeScript types from next.config.ts\n validate Validate all content entries against the schema\n update Regenerate admin route files\n agent-docs Inject AI agent doc links into AGENTS.md\n\n ${fmt.bold('Options:')}\n --help Show this help message\n --version Show version number\n\n Run ${fmt.cyan('octocms <command> --help')} for command-specific help.\n`;\n\nconst COMMAND_HELP: Record<string, string> = {\n init: `\n ${fmt.bold('octocms init')} — Initialize OctoCMS in a Next.js project\n\n Creates next.config.ts, admin route files, demo content, and updates\n next.config.ts and tsconfig.json with required configuration.\n\n ${fmt.bold('Usage:')} octocms init [options]\n\n ${fmt.bold('Options:')}\n --yes, -y Accept all defaults (non-interactive)\n --help Show this help message\n`,\n dev: `\n ${fmt.bold('octocms dev')} — Start development server with config watching\n\n Spawns Next.js dev server and watches next.config.ts. When the config\n changes, types are automatically regenerated.\n\n ${fmt.bold('Usage:')} octocms dev [options]\n\n ${fmt.bold('Options:')}\n --port <n> Port number (default: 3000)\n --help Show this help message\n`,\n 'types:gen': `\n ${fmt.bold('octocms types:gen')} — Generate TypeScript types from next.config.ts\n\n Produces cms/__generated__/types.ts, enums.ts, content.d.ts, and index.ts.\n Validates the config before generating.\n\n ${fmt.bold('Usage:')} octocms types:gen\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n validate: `\n ${fmt.bold('octocms validate')} — Validate all content entries against the schema\n\n Reads every JSON file in cms/content/, validates structure, field types,\n required fields, select option values, reference targets, and companion files.\n\n ${fmt.bold('Usage:')} octocms validate\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n 'agent-docs': `\n ${fmt.bold('octocms agent-docs')} — Inject AI agent doc links into AGENTS.md\n\n Creates AGENTS.md if it doesn't exist, or appends the OctoCMS section\n if the file exists but doesn't reference the agent docs yet.\n\n ${fmt.bold('Usage:')} octocms agent-docs\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n update: `\n ${fmt.bold('octocms update')} — Regenerate admin route files\n\n Ensures src/app/cms/layout.tsx and the catch-all page re-export are\n up-to-date with the latest OctoCMS version.\n\n ${fmt.bold('Usage:')} octocms update\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n};\n\nfunction parseArgs(argv: string[]): { command: string | null; flags: Record<string, string | boolean> } {\n const args = argv.slice(2); // skip node + script path\n const flags: Record<string, string | boolean> = {};\n let command: string | null = null;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === '--help' || arg === '-h') {\n flags.help = true;\n } else if (arg === '--version' || arg === '-v') {\n flags.version = true;\n } else if (arg === '--yes' || arg === '-y') {\n flags.yes = true;\n } else if (arg === '--port') {\n flags.port = args[++i] ?? '';\n } else if (arg.startsWith('--port=')) {\n flags.port = arg.slice(7);\n } else if (!arg.startsWith('-') && !command) {\n // Handle compound commands as single tokens\n if (arg === 'types:gen' || arg === 'agent-docs') {\n command = arg;\n } else {\n command = arg;\n }\n }\n }\n\n return { command, flags };\n}\n\nasync function main(): Promise<void> {\n const { command, flags } = parseArgs(process.argv);\n\n if (flags.version) {\n console.log(VERSION);\n return;\n }\n\n if (!command || flags.help) {\n if (command && COMMAND_HELP[command]) {\n console.log(COMMAND_HELP[command]);\n } else {\n console.log(HELP);\n }\n return;\n }\n\n if (COMMAND_HELP[command] && flags.help) {\n console.log(COMMAND_HELP[command]);\n return;\n }\n\n try {\n switch (command) {\n case 'init': {\n // init doesn't need an existing project root — use cwd\n const { initCommand } = await import('./commands/init');\n await initCommand(process.cwd(), { yes: flags.yes === true });\n break;\n }\n case 'dev': {\n const projectRoot = resolveProjectRoot();\n const { devCommand } = await import('./commands/dev');\n const port = flags.port ? Number(flags.port) : undefined;\n await devCommand(projectRoot, { port });\n break;\n }\n case 'types:gen': {\n const projectRoot = resolveProjectRoot();\n const { typesGenCommand } = await import('./commands/typesGen');\n await typesGenCommand(projectRoot);\n break;\n }\n case 'validate': {\n const projectRoot = resolveProjectRoot();\n const { validateCommand } = await import('./commands/validate');\n await validateCommand(projectRoot);\n break;\n }\n case 'update': {\n const projectRoot = resolveProjectRoot();\n const { updateCommand } = await import('./commands/update');\n await updateCommand(projectRoot);\n break;\n }\n case 'agent-docs': {\n const projectRoot = resolveProjectRoot();\n const { agentDocsCommand } = await import('./commands/agentDocs');\n await agentDocsCommand(projectRoot);\n break;\n }\n default:\n log.error(`Unknown command: ${command}`);\n console.log(HELP);\n process.exitCode = 1;\n }\n } catch (e) {\n log.error((e as Error).message);\n process.exitCode = 1;\n }\n}\n\nmain();\n"],"mappings":";;;;;;;;;;AAEE,cAAW;;;ACsBb,IAAM,OAAO;AAAA,IACT,IAAI,KAAK,aAAa,CAAC,KAAK,OAAO;AAAA;AAAA;AAAA;AAAA,IAInC,IAAI,KAAK,WAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQrB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA,QAIhB,IAAI,KAAK,0BAA0B,CAAC;AAAA;AAG5C,IAAM,eAAuC;AAAA,EAC3C,MAAM;AAAA,IACJ,IAAI,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKxB,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA,EAItB,KAAK;AAAA,IACH,IAAI,KAAK,aAAa,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKvB,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA,EAItB,aAAa;AAAA,IACX,IAAI,KAAK,mBAAmB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAK7B,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA,EAGtB,UAAU;AAAA,IACR,IAAI,KAAK,kBAAkB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAK5B,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA,EAGtB,cAAc;AAAA,IACZ,IAAI,KAAK,oBAAoB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAK9B,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAAA,EAGtB,QAAQ;AAAA,IACN,IAAI,KAAK,gBAAgB,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAK1B,IAAI,KAAK,QAAQ,CAAC;AAAA;AAAA,IAElB,IAAI,KAAK,UAAU,CAAC;AAAA;AAAA;AAGxB;AAEA,SAAS,UAAU,MAAqF;AAnHxG;AAoHE,QAAM,OAAO,KAAK,MAAM,CAAC;AACzB,QAAM,QAA0C,CAAC;AACjD,MAAI,UAAyB;AAE7B,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAElB,QAAI,QAAQ,YAAY,QAAQ,MAAM;AACpC,YAAM,OAAO;AAAA,IACf,WAAW,QAAQ,eAAe,QAAQ,MAAM;AAC9C,YAAM,UAAU;AAAA,IAClB,WAAW,QAAQ,WAAW,QAAQ,MAAM;AAC1C,YAAM,MAAM;AAAA,IACd,WAAW,QAAQ,UAAU;AAC3B,YAAM,QAAO,UAAK,EAAE,CAAC,MAAR,YAAa;AAAA,IAC5B,WAAW,IAAI,WAAW,SAAS,GAAG;AACpC,YAAM,OAAO,IAAI,MAAM,CAAC;AAAA,IAC1B,WAAW,CAAC,IAAI,WAAW,GAAG,KAAK,CAAC,SAAS;AAE3C,UAAI,QAAQ,eAAe,QAAQ,cAAc;AAC/C,kBAAU;AAAA,MACZ,OAAO;AACL,kBAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAEA,eAAe,OAAsB;AACnC,QAAM,EAAE,SAAS,MAAM,IAAI,UAAU,QAAQ,IAAI;AAEjD,MAAI,MAAM,SAAS;AACjB,YAAQ,IAAI,OAAO;AACnB;AAAA,EACF;AAEA,MAAI,CAAC,WAAW,MAAM,MAAM;AAC1B,QAAI,WAAW,aAAa,OAAO,GAAG;AACpC,cAAQ,IAAI,aAAa,OAAO,CAAC;AAAA,IACnC,OAAO;AACL,cAAQ,IAAI,IAAI;AAAA,IAClB;AACA;AAAA,EACF;AAEA,MAAI,aAAa,OAAO,KAAK,MAAM,MAAM;AACvC,YAAQ,IAAI,aAAa,OAAO,CAAC;AACjC;AAAA,EACF;AAEA,MAAI;AACF,YAAQ,SAAS;AAAA,MACf,KAAK,QAAQ;AAEX,cAAM,EAAE,YAAY,IAAI,MAAM,OAAO,qBAAiB;AACtD,cAAM,YAAY,QAAQ,IAAI,GAAG,EAAE,KAAK,MAAM,QAAQ,KAAK,CAAC;AAC5D;AAAA,MACF;AAAA,MACA,KAAK,OAAO;AACV,cAAM,cAAc,mBAAmB;AACvC,cAAM,EAAE,WAAW,IAAI,MAAM,OAAO,oBAAgB;AACpD,cAAM,OAAO,MAAM,OAAO,OAAO,MAAM,IAAI,IAAI;AAC/C,cAAM,WAAW,aAAa,EAAE,KAAK,CAAC;AACtC;AAAA,MACF;AAAA,MACA,KAAK,aAAa;AAChB,cAAM,cAAc,mBAAmB;AACvC,cAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,yBAAqB;AAC9D,cAAM,gBAAgB,WAAW;AACjC;AAAA,MACF;AAAA,MACA,KAAK,YAAY;AACf,cAAM,cAAc,mBAAmB;AACvC,cAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,yBAAqB;AAC9D,cAAM,gBAAgB,WAAW;AACjC;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,cAAc,mBAAmB;AACvC,cAAM,EAAE,cAAc,IAAI,MAAM,OAAO,uBAAmB;AAC1D,cAAM,cAAc,WAAW;AAC/B;AAAA,MACF;AAAA,MACA,KAAK,cAAc;AACjB,cAAM,cAAc,mBAAmB;AACvC,cAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,0BAAsB;AAChE,cAAM,iBAAiB,WAAW;AAClC;AAAA,MACF;AAAA,MACA;AACE,YAAI,MAAM,oBAAoB,OAAO,EAAE;AACvC,gBAAQ,IAAI,IAAI;AAChB,gBAAQ,WAAW;AAAA,IACvB;AAAA,EACF,SAAS,GAAG;AACV,QAAI,MAAO,EAAY,OAAO;AAC9B,YAAQ,WAAW;AAAA,EACrB;AACF;AAEA,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../package.json","../../cli/index.ts"],"sourcesContent":["{\n \"name\": \"octocms\",\n \"version\": \"0.3.5\",\n \"description\": \"A file-based CMS for Next.js — schema-driven, Git-backed, no database\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"keywords\": [\n \"cms\",\n \"nextjs\",\n \"headless-cms\",\n \"git-cms\",\n \"content-management\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/octocms/octocms.git\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"require\": \"./dist/index.cjs\",\n \"import\": \"./dist/index.js\"\n },\n \"./query\": {\n \"types\": \"./dist/query.d.ts\",\n \"require\": \"./dist/query.cjs\",\n \"import\": \"./dist/query.js\"\n },\n \"./config\": {\n \"types\": \"./dist/config.d.ts\",\n \"require\": \"./dist/config.cjs\",\n \"import\": \"./dist/config.js\"\n },\n \"./defineConfig\": {\n \"types\": \"./dist/defineConfig.d.ts\",\n \"require\": \"./dist/defineConfig.cjs\",\n \"import\": \"./dist/defineConfig.js\"\n },\n \"./types\": {\n \"types\": \"./dist/types.d.ts\",\n \"require\": \"./dist/types.cjs\",\n \"import\": \"./dist/types.js\"\n },\n \"./withOctoCMS\": {\n \"types\": \"./dist/withOctoCMS.d.ts\",\n \"require\": \"./dist/withOctoCMS.cjs\",\n \"import\": \"./dist/withOctoCMS.js\"\n },\n \"./components/public\": {\n \"types\": \"./dist/components/public/index.d.ts\",\n \"require\": \"./dist/components/public/index.cjs\",\n \"import\": \"./dist/components/public/index.js\"\n },\n \"./admin/AdminApp\": \"./admin/AdminApp.tsx\",\n \"./admin/pages/AdminLayout\": \"./admin/pages/AdminLayout.tsx\",\n \"./globals.css\": \"./globals.css\",\n \"./docs/*\": \"./docs/*\"\n },\n \"bin\": {\n \"octocms\": \"dist/cli/index.js\"\n },\n \"files\": [\n \"dist\",\n \"docs\",\n \"globals.css\",\n \"admin\",\n \"components\",\n \"hooks\",\n \"lib\",\n \"utils\",\n \"index.ts\",\n \"query.ts\",\n \"config.ts\",\n \"defineConfig.ts\",\n \"github-public.ts\",\n \"types.ts\",\n \"withOctoCMS.ts\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"prepublishOnly\": \"npm run checks && npm run build\",\n \"lint\": \"oxlint .\",\n \"lint:fix\": \"oxlint --fix .\",\n \"fmt\": \"oxfmt .\",\n \"fmt:check\": \"oxfmt --check .\",\n \"test\": \"vitest\",\n \"test:run\": \"vitest run\",\n \"ts-check\": \"tsc --noEmit --incremental false\",\n \"checks\": \"npm run lint && npm run ts-check && npm run fmt:check && npm run test:run\",\n \"checks:fix\": \"npm run lint:fix && npm run fmt && npm run test:run\"\n },\n \"peerDependencies\": {\n \"next\": \">=15\",\n \"react\": \">=18\",\n \"react-dom\": \">=18\"\n },\n \"dependencies\": {\n \"@mdxeditor/editor\": \"^3.11.4\",\n \"@radix-ui/react-avatar\": \"^1.1.11\",\n \"@radix-ui/react-dialog\": \"^1.1.15\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n \"@radix-ui/react-label\": \"^2.1.8\",\n \"@radix-ui/react-select\": \"^2.2.6\",\n \"@radix-ui/react-slot\": \"^1.2.4\",\n \"@radix-ui/react-tabs\": \"^1.1.13\",\n \"@radix-ui/react-toast\": \"^1.2.15\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"glob\": \"^11.0.0\",\n \"lucide-react\": \"^1.7.0\",\n \"minisearch\": \"^7.2.0\",\n \"next-auth\": \"^4.24.13\",\n \"octokit\": \"^4.1.4\",\n \"react-markdown\": \"^10.1.0\",\n \"rehype-sanitize\": \"^6.0.0\",\n \"remark-gfm\": \"^4.0.1\",\n \"remark-mdx\": \"^3.1.1\",\n \"sharp\": \"^0.34.5\",\n \"slugify\": \"^1.6.9\",\n \"sonner\": \"^2.0.7\",\n \"tailwind-merge\": \"^3.5.0\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@testing-library/dom\": \"^10.4.1\",\n \"@testing-library/react\": \"^16.3.2\",\n \"@types/node\": \"^20\",\n \"@types/react\": \"^19.2.14\",\n \"@types/react-dom\": \"^19.2.3\",\n \"@vitejs/plugin-react\": \"^6.0.1\",\n \"jsdom\": \"^29.0.1\",\n \"oxfmt\": \"^0.11.0\",\n \"oxlint\": \"^0.17.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"^5.9.3\",\n \"vite-tsconfig-paths\": \"^6.1.1\",\n \"vitest\": \"^4.1.2\"\n }\n}\n","#!/usr/bin/env node\n/* eslint-disable no-console */\n\n/**\n * OctoCMS CLI — command-line tools for managing an OctoCMS project.\n *\n * Commands:\n * init Initialize OctoCMS in a Next.js project\n * dev Start development server with config watching\n * types:gen Generate TypeScript types from next.config.ts\n * validate Validate all content entries against the schema\n * update Regenerate admin route files\n * agent-docs Inject AI agent doc links into AGENTS.md\n *\n * Usage:\n * octocms <command> [options]\n * octocms --help\n * octocms --version\n */\n\nimport { fmt, log } from './lib/logger';\nimport { resolveProjectRoot } from './lib/project';\nimport { version as VERSION } from '../package.json';\n\nconst HELP = `\n ${fmt.bold('OctoCMS CLI')} v${VERSION}\n\n Usage: octocms <command> [options]\n\n ${fmt.bold('Commands:')}\n init Initialize OctoCMS in a Next.js project\n dev Start development server with config watching\n types:gen Generate TypeScript types from next.config.ts\n validate Validate all content entries against the schema\n update Regenerate admin route files\n agent-docs Inject AI agent doc links into AGENTS.md\n\n ${fmt.bold('Options:')}\n --help Show this help message\n --version Show version number\n\n Run ${fmt.cyan('octocms <command> --help')} for command-specific help.\n`;\n\nconst COMMAND_HELP: Record<string, string> = {\n init: `\n ${fmt.bold('octocms init')} — Initialize OctoCMS in a Next.js project\n\n Creates next.config.ts, admin route files, demo content, and updates\n next.config.ts and tsconfig.json with required configuration.\n\n ${fmt.bold('Usage:')} octocms init [options]\n\n ${fmt.bold('Options:')}\n --yes, -y Accept all defaults (non-interactive)\n --help Show this help message\n`,\n dev: `\n ${fmt.bold('octocms dev')} — Start development server with config watching\n\n Spawns Next.js dev server and watches next.config.ts. When the config\n changes, types are automatically regenerated.\n\n ${fmt.bold('Usage:')} octocms dev [options]\n\n ${fmt.bold('Options:')}\n --port <n> Port number (default: 3000)\n --help Show this help message\n`,\n 'types:gen': `\n ${fmt.bold('octocms types:gen')} — Generate TypeScript types from next.config.ts\n\n Produces cms/__generated__/types.ts, enums.ts, content.d.ts, and index.ts.\n Validates the config before generating.\n\n ${fmt.bold('Usage:')} octocms types:gen\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n validate: `\n ${fmt.bold('octocms validate')} — Validate all content entries against the schema\n\n Reads every JSON file in cms/content/, validates structure, field types,\n required fields, select option values, reference targets, and companion files.\n\n ${fmt.bold('Usage:')} octocms validate\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n 'agent-docs': `\n ${fmt.bold('octocms agent-docs')} — Inject AI agent doc links into AGENTS.md\n\n Creates AGENTS.md if it doesn't exist, or appends the OctoCMS section\n if the file exists but doesn't reference the agent docs yet.\n\n ${fmt.bold('Usage:')} octocms agent-docs\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n update: `\n ${fmt.bold('octocms update')} — Regenerate admin route files\n\n Ensures src/app/cms/layout.tsx and the catch-all page re-export are\n up-to-date with the latest OctoCMS version.\n\n ${fmt.bold('Usage:')} octocms update\n\n ${fmt.bold('Options:')}\n --help Show this help message\n`,\n};\n\nfunction parseArgs(argv: string[]): { command: string | null; flags: Record<string, string | boolean> } {\n const args = argv.slice(2); // skip node + script path\n const flags: Record<string, string | boolean> = {};\n let command: string | null = null;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n\n if (arg === '--help' || arg === '-h') {\n flags.help = true;\n } else if (arg === '--version' || arg === '-v') {\n flags.version = true;\n } else if (arg === '--yes' || arg === '-y') {\n flags.yes = true;\n } else if (arg === '--port') {\n flags.port = args[++i] ?? '';\n } else if (arg.startsWith('--port=')) {\n flags.port = arg.slice(7);\n } else if (!arg.startsWith('-') && !command) {\n // Handle compound commands as single tokens\n if (arg === 'types:gen' || arg === 'agent-docs') {\n command = arg;\n } else {\n command = arg;\n }\n }\n }\n\n return { command, flags };\n}\n\nasync function main(): Promise<void> {\n const { command, flags } = parseArgs(process.argv);\n\n if (flags.version) {\n console.log(VERSION);\n return;\n }\n\n if (!command || flags.help) {\n if (command && COMMAND_HELP[command]) {\n console.log(COMMAND_HELP[command]);\n } else {\n console.log(HELP);\n }\n return;\n }\n\n if (COMMAND_HELP[command] && flags.help) {\n console.log(COMMAND_HELP[command]);\n return;\n }\n\n try {\n switch (command) {\n case 'init': {\n // init doesn't need an existing project root — use cwd\n const { initCommand } = await import('./commands/init');\n await initCommand(process.cwd(), { yes: flags.yes === true });\n break;\n }\n case 'dev': {\n const projectRoot = resolveProjectRoot();\n const { devCommand } = await import('./commands/dev');\n const port = flags.port ? Number(flags.port) : undefined;\n await devCommand(projectRoot, { port });\n break;\n }\n case 'types:gen': {\n const projectRoot = resolveProjectRoot();\n const { typesGenCommand } = await import('./commands/typesGen');\n await typesGenCommand(projectRoot);\n break;\n }\n case 'validate': {\n const projectRoot = resolveProjectRoot();\n const { validateCommand } = await import('./commands/validate');\n await validateCommand(projectRoot);\n break;\n }\n case 'update': {\n const projectRoot = resolveProjectRoot();\n const { updateCommand } = await import('./commands/update');\n await updateCommand(projectRoot);\n break;\n }\n case 'agent-docs': {\n const projectRoot = resolveProjectRoot();\n const { agentDocsCommand } = await import('./commands/agentDocs');\n await agentDocsCommand(projectRoot);\n break;\n }\n default:\n log.error(`Unknown command: ${command}`);\n console.log(HELP);\n process.exitCode = 1;\n }\n } catch (e) {\n log.error((e as Error).message);\n process.exitCode = 1;\n }\n}\n\nmain();\n"],"mappings":"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","names":[]}
@@ -0,0 +1,160 @@
1
+ import { Octokit } from 'octokit';
2
+
3
+ import { getConfig } from './lib/configStore';
4
+ import { ContentSourceError, isContentSourceError, mapGitHubApiErrorToContentSource } from './lib/contentSourceError';
5
+
6
+ export { isProductionMode } from './lib/githubContentMode';
7
+
8
+ const getGitHubConfig = () => ({
9
+ owner: process.env.GITHUB_REPO_OWNER || '',
10
+ repo: process.env.GITHUB_REPO_NAME || '',
11
+ branch: getConfig().git.baseBranch,
12
+ });
13
+
14
+ export const assertGitHubConfig = () => {
15
+ const { owner, repo, branch } = getGitHubConfig();
16
+
17
+ if (!owner || !repo) {
18
+ throw new ContentSourceError(
19
+ 'github_config',
20
+ 'GitHub repository is not configured. Set environment variables GITHUB_REPO_OWNER and GITHUB_REPO_NAME.',
21
+ );
22
+ }
23
+
24
+ return { owner, repo, branch };
25
+ };
26
+
27
+ /**
28
+ * Create public-read Octokit clients.
29
+ *
30
+ * 1) Use CMS_GITHUB_TOKEN when available (private repos / higher rate limits).
31
+ * NOTE: Do NOT use GITHUB_TOKEN — Vercel overrides it with its own system token.
32
+ * 2) Also try unauthenticated reads (public repos) to avoid hard dependency.
33
+ */
34
+ export const getPublicOctokits = (): Octokit[] => {
35
+ const token = process.env.CMS_GITHUB_TOKEN?.trim();
36
+ const clients: Octokit[] = [];
37
+
38
+ if (token) {
39
+ clients.push(new Octokit({ auth: token }));
40
+ }
41
+
42
+ clients.push(new Octokit());
43
+
44
+ return clients;
45
+ };
46
+
47
+ /**
48
+ * When `config.git.publishedPointerBranch` is set, `cms/published.json` is read and written on
49
+ * that branch — use an unprotected branch so Publish avoids a protected base branch.
50
+ */
51
+ export const getPublishedPointerRef = (): string | undefined => {
52
+ const v = getConfig().git.publishedPointerBranch?.trim();
53
+ return v || undefined;
54
+ };
55
+
56
+ /**
57
+ * Read a file from GitHub using a static server token (no user session required).
58
+ * Used by public pages to fetch content in production.
59
+ */
60
+ export const readGitHubFilePublic = async (filePath: string, branch?: string): Promise<string | null> => {
61
+ const { owner, repo, branch: configBranch } = assertGitHubConfig();
62
+ const ref = branch ?? configBranch;
63
+ const clients = getPublicOctokits();
64
+
65
+ for (const octokit of clients) {
66
+ try {
67
+ const { data } = await octokit.rest.repos.getContent({
68
+ owner,
69
+ repo,
70
+ path: filePath,
71
+ ref,
72
+ });
73
+
74
+ if ('content' in data && data.type === 'file') {
75
+ return Buffer.from(data.content, 'base64').toString('utf-8');
76
+ }
77
+
78
+ return null;
79
+ } catch (error: any) {
80
+ if (error.status === 429) {
81
+ throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });
82
+ }
83
+
84
+ if (error.status === 401 || error.status === 403) {
85
+ // Try next client (e.g. unauthenticated) before giving up.
86
+ continue;
87
+ }
88
+
89
+ if (error.status === 404) {
90
+ // 404 can mean missing file, missing ref, or no repo access.
91
+ // Try next client if available before deciding.
92
+ continue;
93
+ }
94
+
95
+ throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });
96
+ }
97
+ }
98
+
99
+ // All clients failed — typical for private repos or invalid token permissions.
100
+ throw new ContentSourceError(
101
+ 'github_auth',
102
+ `Cannot read ${owner}/${repo} (ref: ${ref}) from GitHub. For private repositories, set CMS_GITHUB_TOKEN with Contents: Read access on this repository.`,
103
+ );
104
+ };
105
+
106
+ /**
107
+ * List files in a directory from the GitHub repo.
108
+ * Returns an array of file paths.
109
+ */
110
+ export const listGitHubFiles = async (dirPath: string, extension?: string, branch?: string): Promise<string[]> => {
111
+ const [octokit] = getPublicOctokits();
112
+ const { owner, repo, branch: configBranch } = assertGitHubConfig();
113
+ const ref = branch ?? configBranch;
114
+
115
+ try {
116
+ const { data } = await octokit.rest.repos.getContent({
117
+ owner,
118
+ repo,
119
+ path: dirPath,
120
+ ref,
121
+ });
122
+
123
+ if (!Array.isArray(data)) {
124
+ return [];
125
+ }
126
+
127
+ let files = data.filter((item) => item.type === 'file').map((item) => item.path);
128
+
129
+ if (extension) {
130
+ files = files.filter((f) => f.endsWith(extension));
131
+ }
132
+
133
+ return files;
134
+ } catch (error: any) {
135
+ if (error.status === 404) {
136
+ return [];
137
+ }
138
+ throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });
139
+ }
140
+ };
141
+
142
+ /**
143
+ * Read cms/published.json to determine which branch public pages read content from.
144
+ * Uses {@link getPublishedPointerRef} when set; otherwise reads from `config.git.baseBranch`.
145
+ * Falls back to base branch on missing file or invalid JSON.
146
+ */
147
+ export const getPublishedBranch = async (): Promise<string> => {
148
+ const { branch: configBranch } = assertGitHubConfig();
149
+ const pointerRef = getPublishedPointerRef();
150
+
151
+ try {
152
+ const content = await readGitHubFilePublic('cms/published.json', pointerRef);
153
+ if (!content) return configBranch;
154
+ const parsed = JSON.parse(content);
155
+ return typeof parsed.branch === 'string' ? parsed.branch : configBranch;
156
+ } catch (e) {
157
+ if (isContentSourceError(e)) throw e;
158
+ return configBranch;
159
+ }
160
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octocms",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "A file-based CMS for Next.js — schema-driven, Git-backed, no database",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -67,10 +67,12 @@
67
67
  "components",
68
68
  "hooks",
69
69
  "lib",
70
+ "utils",
70
71
  "index.ts",
71
72
  "query.ts",
72
73
  "config.ts",
73
74
  "defineConfig.ts",
75
+ "github-public.ts",
74
76
  "types.ts",
75
77
  "withOctoCMS.ts"
76
78
  ],
@@ -0,0 +1,30 @@
1
+ import { getConfig } from '../lib/configStore';
2
+
3
+ import { SelectedFile } from '../types';
4
+
5
+ export const parseFileName = (file: string): SelectedFile => {
6
+ const config = getConfig();
7
+ const nameWithoutFolder = file.replace(`${config.contentFolder}/`, '').replace('.json', '');
8
+ const split = nameWithoutFolder.split('/');
9
+ const type = split[0];
10
+ const id = split.pop() as string;
11
+
12
+ return { type, id, path: file };
13
+ };
14
+
15
+ export const parseMediaFileName = (file: string): SelectedFile => {
16
+ const config = getConfig();
17
+ let nameWithoutFolder = file.replace(`${config.mediaFolder}/`, '');
18
+ (config.mediaAllowedFormats || []).forEach((format) => {
19
+ nameWithoutFolder = nameWithoutFolder.replace(`.${format}`, '');
20
+ });
21
+ const split = nameWithoutFolder.split('/');
22
+ let type = split[0];
23
+ const id = split.pop() as string;
24
+
25
+ if (type === id) {
26
+ type = '/';
27
+ }
28
+
29
+ return { type, id, path: file };
30
+ };