octocms 0.4.9 → 0.4.10

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.
Files changed (66) hide show
  1. package/dist/admin/AdminApp.d.ts +2 -2
  2. package/dist/admin/AdminApp.d.ts.map +1 -1
  3. package/dist/admin/AdminApp.js +1 -2
  4. package/dist/admin/AdminApp.js.map +1 -1
  5. package/dist/admin/actions/git.d.ts +17 -0
  6. package/dist/admin/actions/git.d.ts.map +1 -1
  7. package/dist/admin/actions/git.js +11 -0
  8. package/dist/admin/actions/git.js.map +1 -1
  9. package/dist/admin/github.d.ts +16 -0
  10. package/dist/admin/github.d.ts.map +1 -1
  11. package/dist/admin/github.js +27 -0
  12. package/dist/admin/github.js.map +1 -1
  13. package/dist/admin/query/hooks/useRecentCMSPullRequests.d.ts +7 -0
  14. package/dist/admin/query/hooks/useRecentCMSPullRequests.d.ts.map +1 -0
  15. package/dist/admin/query/hooks/useRecentCMSPullRequests.js +16 -0
  16. package/dist/admin/query/hooks/useRecentCMSPullRequests.js.map +1 -0
  17. package/dist/admin/query/keys.d.ts +1 -0
  18. package/dist/admin/query/keys.d.ts.map +1 -1
  19. package/dist/admin/query/keys.js +2 -1
  20. package/dist/admin/query/keys.js.map +1 -1
  21. package/dist/agent/index.cjs +28 -1
  22. package/dist/agent/index.cjs.map +1 -1
  23. package/dist/chunk-5MC45AYI.js +7 -0
  24. package/dist/chunk-5MC45AYI.js.map +1 -0
  25. package/dist/cli/index.js +3 -3
  26. package/dist/components/Dashboard/DashboardContent.d.ts.map +1 -1
  27. package/dist/components/Dashboard/DashboardContent.js +30 -9
  28. package/dist/components/Dashboard/DashboardContent.js.map +1 -1
  29. package/dist/components/Dashboard/RecentPullRequests.d.ts +7 -0
  30. package/dist/components/Dashboard/RecentPullRequests.d.ts.map +1 -0
  31. package/dist/components/Dashboard/RecentPullRequests.js +96 -0
  32. package/dist/components/Dashboard/RecentPullRequests.js.map +1 -0
  33. package/dist/components/Dashboard/skeletons/{DashboardPageChromeSkeleton.d.ts → ContentPageChromeSkeleton.d.ts} +3 -3
  34. package/dist/components/Dashboard/skeletons/ContentPageChromeSkeleton.d.ts.map +1 -0
  35. package/dist/components/Dashboard/skeletons/{DashboardPageChromeSkeleton.js → ContentPageChromeSkeleton.js} +3 -3
  36. package/dist/components/Dashboard/skeletons/ContentPageChromeSkeleton.js.map +1 -0
  37. package/dist/components/Layout/TopHeader.d.ts.map +1 -1
  38. package/dist/components/Layout/TopHeader.js +2 -3
  39. package/dist/components/Layout/TopHeader.js.map +1 -1
  40. package/dist/components/skeletons/MainSlotSkeleton.js +3 -3
  41. package/dist/components/skeletons/MainSlotSkeleton.js.map +1 -1
  42. package/dist/{embeddingsGen-NZQ2RXXP.js → embeddingsGen-7MXSZQ43.js} +2 -2
  43. package/dist/{github-RGFCTMMT.js → github-7HIP6RW3.js} +28 -1
  44. package/dist/github-7HIP6RW3.js.map +1 -0
  45. package/dist/globals.css +25 -3
  46. package/dist/index.cjs +47 -19
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/{init-W645ZB34.js → init-LMBF6CMC.js} +2 -2
  49. package/dist/query.cjs +47 -19
  50. package/dist/query.cjs.map +1 -1
  51. package/dist/query.d.ts.map +1 -1
  52. package/dist/query.js +47 -19
  53. package/dist/query.js.map +1 -1
  54. package/globals.css +25 -3
  55. package/package.json +1 -1
  56. package/dist/admin/pages/DashboardPage.d.ts +0 -2
  57. package/dist/admin/pages/DashboardPage.d.ts.map +0 -1
  58. package/dist/admin/pages/DashboardPage.js +0 -18
  59. package/dist/admin/pages/DashboardPage.js.map +0 -1
  60. package/dist/chunk-GL7FGEA4.js +0 -7
  61. package/dist/chunk-GL7FGEA4.js.map +0 -1
  62. package/dist/components/Dashboard/skeletons/DashboardPageChromeSkeleton.d.ts.map +0 -1
  63. package/dist/components/Dashboard/skeletons/DashboardPageChromeSkeleton.js.map +0 -1
  64. package/dist/github-RGFCTMMT.js.map +0 -1
  65. /package/dist/{embeddingsGen-NZQ2RXXP.js.map → embeddingsGen-7MXSZQ43.js.map} +0 -0
  66. /package/dist/{init-W645ZB34.js.map → init-LMBF6CMC.js.map} +0 -0
@@ -8,8 +8,8 @@ type AdminAppProps = {
8
8
  * file in the user app that re-exports this component as the default.
9
9
  *
10
10
  * Route segments map to admin pages:
11
- * /cms → DashboardPage (empty home)
12
- * /cms/content → ContentPage (all entries)
11
+ * /cms → ContentPage (admin home — content list)
12
+ * /cms/content → ContentPage (alias for /cms — kept for legacy links)
13
13
  * /cms/content/<type> → CollectionPage
14
14
  * /cms/content/<type>/<id> → EntryPage
15
15
  * /cms/chat → ChatPage (gated on `isAgentEnabled(agentConfig)`)
@@ -1 +1 @@
1
- {"version":3,"file":"AdminApp.d.ts","sourceRoot":"","sources":["../../admin/AdminApp.tsx"],"names":[],"mappings":"AAYA,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,aAAa,oDAyCvD"}
1
+ {"version":3,"file":"AdminApp.d.ts","sourceRoot":"","sources":["../../admin/AdminApp.tsx"],"names":[],"mappings":"AAWA,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,aAAa,oDAyCvD"}
@@ -6,7 +6,6 @@ import { CollectionPage } from "./pages/CollectionPage";
6
6
  import { ContentModelPage } from "./pages/ContentModelPage";
7
7
  import { ContentPage } from "./pages/ContentPage";
8
8
  import { ContentTypePage } from "./pages/ContentTypePage";
9
- import { DashboardPage } from "./pages/DashboardPage";
10
9
  import { EntryPage } from "./pages/EntryPage";
11
10
  import { MediaAssetPage } from "./pages/MediaAssetPage";
12
11
  import { MediaPage } from "./pages/MediaPage";
@@ -14,7 +13,7 @@ async function AdminApp({ params }) {
14
13
  const { path } = await params;
15
14
  const segments = path != null ? path : [];
16
15
  if (segments.length === 0) {
17
- return /* @__PURE__ */ jsx(DashboardPage, {});
16
+ return /* @__PURE__ */ jsx(ContentPage, {});
18
17
  }
19
18
  if (segments[0] === "chat") {
20
19
  return /* @__PURE__ */ jsx(ChatPage, {});
@@ -1 +1 @@
1
- {"version":3,"sources":["../../admin/AdminApp.tsx"],"sourcesContent":["import { DashboardContentSkeleton } from '../components/Dashboard/DashboardContent.skeleton';\n\nimport { ChatPage } from './pages/ChatPage';\nimport { CollectionPage } from './pages/CollectionPage';\nimport { ContentModelPage } from './pages/ContentModelPage';\nimport { ContentPage } from './pages/ContentPage';\nimport { ContentTypePage } from './pages/ContentTypePage';\nimport { DashboardPage } from './pages/DashboardPage';\nimport { EntryPage } from './pages/EntryPage';\nimport { MediaAssetPage } from './pages/MediaAssetPage';\nimport { MediaPage } from './pages/MediaPage';\n\ntype AdminAppProps = {\n params: Promise<{ path?: string[] }>;\n};\n\n/**\n * Catch-all admin router. Mounted via a single `src/app/cms/[[...path]]/page.tsx`\n * file in the user app that re-exports this component as the default.\n *\n * Route segments map to admin pages:\n * /cms → DashboardPage (empty home)\n * /cms/content → ContentPage (all entries)\n * /cms/content/<type> → CollectionPage\n * /cms/content/<type>/<id> → EntryPage\n * /cms/chat → ChatPage (gated on `isAgentEnabled(agentConfig)`)\n * /cms/media → MediaPage (library — grid + folders)\n * /cms/media/<id> → MediaAssetPage (full-page asset editor)\n * /cms/model → ContentModelPage\n * /cms/model/<type> → ContentTypePage\n *\n * **No `Suspense` in the dispatcher** — `await params` runs in this async RSC; Next.js\n * keeps the previous segment visible during navigation. Thin server shells (`*Page`)\n * hand off to client components that load via TanStack Query and render their own\n * block-level skeletons (`LeftPanelSkeleton`, `ContentTableSkeleton`, etc.).\n */\nexport async function AdminApp({ params }: AdminAppProps) {\n const { path } = await params;\n const segments = path ?? [];\n\n if (segments.length === 0) {\n return <DashboardPage />;\n }\n\n if (segments[0] === 'chat') {\n return <ChatPage />;\n }\n\n if (segments[0] === 'media') {\n if (segments.length === 1) {\n return <MediaPage />;\n }\n const id = segments[1];\n return <MediaAssetPage id={id} key={id} />;\n }\n\n if (segments[0] === 'model') {\n if (segments.length === 1) {\n return <ContentModelPage />;\n }\n const [, type] = segments;\n return <ContentTypePage type={type} key={type} />;\n }\n\n if (segments[0] === 'content') {\n if (segments.length === 1) {\n return <ContentPage />;\n }\n if (segments.length === 2) {\n const [, type] = segments;\n return <CollectionPage params={Promise.resolve({ type })} key={type} />;\n }\n const [, type, id] = segments;\n return <EntryPage params={Promise.resolve({ type, id })} key={`${type}/${id}`} />;\n }\n\n return <DashboardContentSkeleton />;\n}\n"],"mappings":";AAyCW;AAzCX,SAAS,gCAAgC;AAEzC,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB;AA0B1B,eAAsB,SAAS,EAAE,OAAO,GAAkB;AACxD,QAAM,EAAE,KAAK,IAAI,MAAM;AACvB,QAAM,WAAW,sBAAQ,CAAC;AAE1B,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,oBAAC,iBAAc;AAAA,EACxB;AAEA,MAAI,SAAS,CAAC,MAAM,QAAQ;AAC1B,WAAO,oBAAC,YAAS;AAAA,EACnB;AAEA,MAAI,SAAS,CAAC,MAAM,SAAS;AAC3B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,aAAU;AAAA,IACpB;AACA,UAAM,KAAK,SAAS,CAAC;AACrB,WAAO,oBAAC,kBAAe,MAAa,EAAI;AAAA,EAC1C;AAEA,MAAI,SAAS,CAAC,MAAM,SAAS;AAC3B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,oBAAiB;AAAA,IAC3B;AACA,UAAM,CAAC,EAAE,IAAI,IAAI;AACjB,WAAO,oBAAC,mBAAgB,QAAiB,IAAM;AAAA,EACjD;AAEA,MAAI,SAAS,CAAC,MAAM,WAAW;AAC7B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,eAAY;AAAA,IACtB;AACA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,CAAC,EAAEA,KAAI,IAAI;AACjB,aAAO,oBAAC,kBAAe,QAAQ,QAAQ,QAAQ,EAAE,MAAAA,MAAK,CAAC,KAAQA,KAAM;AAAA,IACvE;AACA,UAAM,CAAC,EAAE,MAAM,EAAE,IAAI;AACrB,WAAO,oBAAC,aAAU,QAAQ,QAAQ,QAAQ,EAAE,MAAM,GAAG,CAAC,KAAQ,GAAG,IAAI,IAAI,EAAE,EAAI;AAAA,EACjF;AAEA,SAAO,oBAAC,4BAAyB;AACnC;","names":["type"]}
1
+ {"version":3,"sources":["../../admin/AdminApp.tsx"],"sourcesContent":["import { DashboardContentSkeleton } from '../components/Dashboard/DashboardContent.skeleton';\n\nimport { ChatPage } from './pages/ChatPage';\nimport { CollectionPage } from './pages/CollectionPage';\nimport { ContentModelPage } from './pages/ContentModelPage';\nimport { ContentPage } from './pages/ContentPage';\nimport { ContentTypePage } from './pages/ContentTypePage';\nimport { EntryPage } from './pages/EntryPage';\nimport { MediaAssetPage } from './pages/MediaAssetPage';\nimport { MediaPage } from './pages/MediaPage';\n\ntype AdminAppProps = {\n params: Promise<{ path?: string[] }>;\n};\n\n/**\n * Catch-all admin router. Mounted via a single `src/app/cms/[[...path]]/page.tsx`\n * file in the user app that re-exports this component as the default.\n *\n * Route segments map to admin pages:\n * /cms → ContentPage (admin home — content list)\n * /cms/content → ContentPage (alias for /cms — kept for legacy links)\n * /cms/content/<type> → CollectionPage\n * /cms/content/<type>/<id> → EntryPage\n * /cms/chat → ChatPage (gated on `isAgentEnabled(agentConfig)`)\n * /cms/media → MediaPage (library — grid + folders)\n * /cms/media/<id> → MediaAssetPage (full-page asset editor)\n * /cms/model → ContentModelPage\n * /cms/model/<type> → ContentTypePage\n *\n * **No `Suspense` in the dispatcher** — `await params` runs in this async RSC; Next.js\n * keeps the previous segment visible during navigation. Thin server shells (`*Page`)\n * hand off to client components that load via TanStack Query and render their own\n * block-level skeletons (`LeftPanelSkeleton`, `ContentTableSkeleton`, etc.).\n */\nexport async function AdminApp({ params }: AdminAppProps) {\n const { path } = await params;\n const segments = path ?? [];\n\n if (segments.length === 0) {\n return <ContentPage />;\n }\n\n if (segments[0] === 'chat') {\n return <ChatPage />;\n }\n\n if (segments[0] === 'media') {\n if (segments.length === 1) {\n return <MediaPage />;\n }\n const id = segments[1];\n return <MediaAssetPage id={id} key={id} />;\n }\n\n if (segments[0] === 'model') {\n if (segments.length === 1) {\n return <ContentModelPage />;\n }\n const [, type] = segments;\n return <ContentTypePage type={type} key={type} />;\n }\n\n if (segments[0] === 'content') {\n if (segments.length === 1) {\n return <ContentPage />;\n }\n if (segments.length === 2) {\n const [, type] = segments;\n return <CollectionPage params={Promise.resolve({ type })} key={type} />;\n }\n const [, type, id] = segments;\n return <EntryPage params={Promise.resolve({ type, id })} key={`${type}/${id}`} />;\n }\n\n return <DashboardContentSkeleton />;\n}\n"],"mappings":";AAwCW;AAxCX,SAAS,gCAAgC;AAEzC,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB;AA0B1B,eAAsB,SAAS,EAAE,OAAO,GAAkB;AACxD,QAAM,EAAE,KAAK,IAAI,MAAM;AACvB,QAAM,WAAW,sBAAQ,CAAC;AAE1B,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,oBAAC,eAAY;AAAA,EACtB;AAEA,MAAI,SAAS,CAAC,MAAM,QAAQ;AAC1B,WAAO,oBAAC,YAAS;AAAA,EACnB;AAEA,MAAI,SAAS,CAAC,MAAM,SAAS;AAC3B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,aAAU;AAAA,IACpB;AACA,UAAM,KAAK,SAAS,CAAC;AACrB,WAAO,oBAAC,kBAAe,MAAa,EAAI;AAAA,EAC1C;AAEA,MAAI,SAAS,CAAC,MAAM,SAAS;AAC3B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,oBAAiB;AAAA,IAC3B;AACA,UAAM,CAAC,EAAE,IAAI,IAAI;AACjB,WAAO,oBAAC,mBAAgB,QAAiB,IAAM;AAAA,EACjD;AAEA,MAAI,SAAS,CAAC,MAAM,WAAW;AAC7B,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,oBAAC,eAAY;AAAA,IACtB;AACA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,CAAC,EAAEA,KAAI,IAAI;AACjB,aAAO,oBAAC,kBAAe,QAAQ,QAAQ,QAAQ,EAAE,MAAAA,MAAK,CAAC,KAAQA,KAAM;AAAA,IACvE;AACA,UAAM,CAAC,EAAE,MAAM,EAAE,IAAI;AACrB,WAAO,oBAAC,aAAU,QAAQ,QAAQ,QAAQ,EAAE,MAAM,GAAG,CAAC,KAAQ,GAAG,IAAI,IAAI,EAAE,EAAI;AAAA,EACjF;AAEA,SAAO,oBAAC,4BAAyB;AACnC;","names":["type"]}
@@ -1,4 +1,5 @@
1
1
  import './registerConfig';
2
+ import { type RecentCMSPullRequestState } from '../github';
2
3
  import type { EntryCommitHistory } from '../../types';
3
4
  import { type ActionResult, type CreateBranchInput, type CreateBranchResult, type CreatePRResult } from './utils';
4
5
  export declare const pushToGit: () => Promise<ActionResult>;
@@ -56,4 +57,20 @@ export declare const publishBranch: (branchName: string) => Promise<ActionResult
56
57
  * shape so the UI never throws.
57
58
  */
58
59
  export declare const getEntryCommits: (filePath: string) => Promise<EntryCommitHistory>;
60
+ export type RecentCMSPullRequest = {
61
+ branch: string;
62
+ prUrl: string;
63
+ prNumber: number;
64
+ title: string;
65
+ state: RecentCMSPullRequestState;
66
+ updatedAt: string;
67
+ authorLogin: string | null;
68
+ authorAvatarUrl: string | null;
69
+ };
70
+ /**
71
+ * Most-recently-updated PRs tagged with the `cms-update` label, across all
72
+ * states (open / merged / closed). Powers the dashboard "Recent pull requests"
73
+ * card. Returns `[]` in dev or on error.
74
+ */
75
+ export declare const getRecentCMSPullRequests: (limit?: number) => Promise<RecentCMSPullRequest[]>;
59
76
  //# sourceMappingURL=git.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../admin/actions/git.ts"],"names":[],"mappings":"AAEA,OAAO,kBAAkB,CAAC;AAsB1B,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQnE,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAQjB,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,YAAY,CA2BtD,CAAC;AAEF,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,cAAc,CAYvD,CAAC;AAEF,eAAO,MAAM,eAAe,wBAE3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAoBhD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,OAAO,CAGvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,OAAO,iBAAiB,KAAG,OAAO,CAAC,kBAAkB,CAoEvF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAUtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAGhD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,SAAS,EAAE,CAsC3D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,YAAY,CAyB5E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,kBAAkB,CAkDlF,CAAC"}
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../admin/actions/git.ts"],"names":[],"mappings":"AAEA,OAAO,kBAAkB,CAAC;AAQ1B,OAAO,EAcL,KAAK,yBAAyB,EAC/B,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQnE,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAQjB,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,YAAY,CA2BtD,CAAC;AAEF,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,cAAc,CAYvD,CAAC;AAEF,eAAO,MAAM,eAAe,wBAE3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAoBhD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,OAAO,CAGvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAU,OAAO,iBAAiB,KAAG,OAAO,CAAC,kBAAkB,CAoEvF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAUtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAGhD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,SAAS,EAAE,CAsC3D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,YAAY,CAyB5E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,kBAAkB,CAkDlF,CAAC;AAMF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,yBAAyB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAU,cAAS,KAAG,OAAO,CAAC,oBAAoB,EAAE,CASxF,CAAC"}
@@ -17,6 +17,7 @@ import {
17
17
  getPublishedPointerRef,
18
18
  isProductionMode,
19
19
  listGitHubCMSPullRequests,
20
+ listGitHubRecentCMSPullRequests,
20
21
  markPRReadyForReview,
21
22
  resolveContentBranch,
22
23
  saveGitHubFile
@@ -269,6 +270,15 @@ const getEntryCommits = async (filePath) => {
269
270
  return empty;
270
271
  }
271
272
  };
273
+ const getRecentCMSPullRequests = async (limit = 5) => {
274
+ if (!isProductionMode()) return [];
275
+ try {
276
+ return await listGitHubRecentCMSPullRequests(limit);
277
+ } catch (e) {
278
+ logCmsServerError({ operation: "getRecentCMSPullRequests", message: getErrorMessage(e) });
279
+ return [];
280
+ }
281
+ };
272
282
  export {
273
283
  clearBranch,
274
284
  createBranch,
@@ -276,6 +286,7 @@ export {
276
286
  getBranch,
277
287
  getEntryCommits,
278
288
  getIsProduction,
289
+ getRecentCMSPullRequests,
279
290
  hasActiveBranch,
280
291
  listCMSBranches,
281
292
  publishBranch,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../admin/actions/git.ts"],"sourcesContent":["'use server';\n\nimport './registerConfig';\n\nimport { execFile } from 'node:child_process';\n\nimport { cookies } from 'next/headers';\n\nimport { getConfig } from '../../lib/configStore';\n\nimport {\n assertGitHubConfig,\n createGitHubBranch,\n createGitHubCMSPullRequest,\n createGitHubPullRequest,\n getGitHubFile,\n getPublicOctokits,\n getPublishedPointerRef,\n isProductionMode,\n listGitHubCMSPullRequests,\n markPRReadyForReview,\n resolveContentBranch,\n saveGitHubFile,\n} from '../github';\nimport type { EntryCommit, EntryCommitHistory } from '../../types';\nimport {\n BRANCH_HISTORY_FILE_PATH,\n parseBranchHistoryFile,\n serializeBranchHistoryFile,\n upsertBranchWorkspace,\n} from '../../lib/branchHistory';\nimport { logCmsServerError } from '../../lib/cmsServerLog';\nimport {\n actionErr,\n actionOk,\n getErrorMessage,\n type ActionResult,\n type CreateBranchInput,\n type CreateBranchResult,\n type CreatePRResult,\n} from './utils';\nimport { getPointerFilePath, serializePointerPayload } from '../../lib/contentBranch';\nimport { warmBranch } from '../store/contentStore';\nimport { buildJsons } from './build';\nimport { waitForPublicReadConsistency } from './files';\n\nconst CMS_BRANCH_COOKIE = 'cms-active-branch';\n\nexport const pushToGit = async (): Promise<ActionResult> => {\n if (isProductionMode()) {\n // In production, commits are made via GitHub API on each save — no push needed\n return actionOk();\n }\n\n try {\n const cookieStore = await cookies();\n const activeBranch = cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n const pushCmd = activeBranch\n ? `git commit -am CommitFromCMS && git push origin HEAD:${activeBranch}`\n : 'git commit -am CommitFromCMS && git push';\n\n await new Promise<void>((resolve, reject) => {\n execFile('sh', ['-c', pushCmd], (error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n });\n\n return actionOk();\n } catch (e) {\n return actionErr(new Error(`Failed git push: ${getErrorMessage(e)}`));\n }\n};\n\nexport const createPR = async (): Promise<CreatePRResult> => {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const title = `CMS content update (${new Date().toISOString().split('T')[0]})`;\n const body = `Content changes from the CMS editor.\\n\\nSource branch: \\`${baseBranch}\\``;\n\n try {\n const { url, number } = await createGitHubPullRequest(title, body, baseBranch);\n return { success: true, url, number };\n } catch (e) {\n return actionErr(e);\n }\n};\n\nexport const getIsProduction = async () => {\n return isProductionMode();\n};\n\n/**\n * Get the currently active CMS branch.\n * In dev: falls back to the local git branch name.\n * In prod: falls back to `config.git.baseBranch`.\n */\nexport const getBranch = async (): Promise<string> => {\n const cookieStore = await cookies();\n const activeBranch = cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n\n if (activeBranch) return activeBranch;\n\n if (!isProductionMode()) {\n try {\n return await new Promise<string>((resolve, reject) => {\n execFile('git', ['branch', '--show-current'], (error, stdout) => {\n if (error) reject(error);\n else resolve(stdout.trim());\n });\n });\n } catch (_e) {\n // fall through to config.git.baseBranch\n }\n }\n\n return getConfig().git.baseBranch;\n};\n\n/**\n * Returns true if a CMS feature branch is currently active (cookie is set).\n */\nexport const hasActiveBranch = async (): Promise<boolean> => {\n const cookieStore = await cookies();\n return !!cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n};\n\n/**\n * Create a GitHub branch from `config.git.baseBranch`, commit `cms/branch-history.json` so the\n * branch has at least one commit, open a draft PR with the `cms-update` label (best effort),\n * and set the active CMS branch cookie.\n */\nexport const createBranch = async (input: CreateBranchInput): Promise<CreateBranchResult> => {\n const config = getConfig();\n const branchName = input.branchName.trim();\n const workspaceTitle = input.title.trim();\n const description = input.description?.trim();\n\n if (!branchName) {\n return { success: false, error: 'Branch name is required.' };\n }\n\n if (!workspaceTitle) {\n return { success: false, error: 'Workspace title is required.' };\n }\n\n const baseBranch = config.git.baseBranch;\n\n try {\n await createGitHubBranch(branchName);\n\n const historyRaw = await getGitHubFile(BRANCH_HISTORY_FILE_PATH, baseBranch);\n const historyDoc = parseBranchHistoryFile(historyRaw?.content ?? '');\n const merged = upsertBranchWorkspace(historyDoc, branchName, {\n title: workspaceTitle,\n ...(description ? { description } : {}),\n createdAt: new Date().toISOString(),\n });\n const historyContent = serializeBranchHistoryFile(merged);\n\n await saveGitHubFile(BRANCH_HISTORY_FILE_PATH, historyContent, 'CMS: add branch workspace metadata', branchName);\n } catch (e) {\n const msg = getErrorMessage(e);\n logCmsServerError({\n operation: 'createBranch.metadata',\n branch: branchName,\n message: msg,\n });\n return { success: false, error: msg };\n }\n\n const prBodyLines = [\n `Content changes from the CMS editor.`,\n '',\n `Branch: \\`${branchName}\\``,\n ...(description ? ['', description] : []),\n ];\n const prBody = prBodyLines.join('\\n');\n\n let prUrl = '';\n let prWarning: string | undefined;\n\n try {\n const pr = await createGitHubCMSPullRequest(branchName, workspaceTitle, prBody, baseBranch);\n prUrl = pr.url;\n } catch (e) {\n prWarning = getErrorMessage(e);\n }\n\n const cookieStore = await cookies();\n cookieStore.set(CMS_BRANCH_COOKIE, branchName, { path: '/' });\n\n // Pre-populate the content store for the new branch\n if (isProductionMode()) {\n warmBranch(branchName).catch(() => {\n /* best-effort */\n });\n }\n\n return prWarning ? { success: true, prUrl: '', prWarning } : { success: true, prUrl };\n};\n\n/**\n * Switch the active CMS branch to an existing branch (by PR).\n */\nexport const setActiveBranch = async (branchName: string): Promise<void> => {\n const cookieStore = await cookies();\n cookieStore.set(CMS_BRANCH_COOKIE, branchName, { path: '/' });\n\n // Pre-populate the content store so the first read after switching is instant\n if (isProductionMode()) {\n warmBranch(branchName).catch(() => {\n /* best-effort — first read will trigger a blocking fetch if this fails */\n });\n }\n};\n\n/**\n * Clear the active CMS branch, reverting listing/reads to `config.git.baseBranch`.\n */\nexport const clearBranch = async (): Promise<void> => {\n const cookieStore = await cookies();\n cookieStore.delete(CMS_BRANCH_COOKIE);\n};\n\nexport type CMSBranch = {\n branch: string;\n prUrl: string;\n prNumber: number;\n title: string;\n isPublished: boolean;\n};\n\n/**\n * List CMS branches for the header: base branch plus **open** PRs labelled `cms-update`\n * (see `listGitHubCMSPullRequests`). Remote `cms/*` refs without an open labelled PR are omitted.\n */\nexport const listCMSBranches = async (): Promise<CMSBranch[]> => {\n try {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const pointerBranch = config.git.publishedPointerBranch?.trim();\n\n const [publishedBranch, prList] = await Promise.all([resolveContentBranch(), listGitHubCMSPullRequests()]);\n\n const base: CMSBranch = {\n branch: baseBranch,\n prUrl: '',\n prNumber: 0,\n title: baseBranch,\n isPublished: publishedBranch === baseBranch,\n };\n\n const features: CMSBranch[] = [];\n\n for (const pr of prList) {\n const branch = pr.branch.trim();\n if (!branch || branch === baseBranch) continue;\n if (pointerBranch && branch === pointerBranch) continue;\n if (!branch.startsWith('cms/')) continue;\n features.push({\n branch,\n prUrl: pr.prUrl,\n prNumber: pr.prNumber,\n title: pr.title.trim() || branch,\n isPublished: branch === publishedBranch,\n });\n }\n\n features.sort((a, b) => b.prNumber - a.prNumber);\n\n return [base, ...features];\n } catch (_e) {\n return [];\n }\n};\n\n/**\n * Publish a branch by writing `{ \"branch\": \"<name>\", \"buildId\": \"<id>\" }` into the per-build\n * pointer file (see {@link getPointerFilePath}) on the pointer branch when\n * `config.git.publishedPointerBranch` is set, otherwise on the base branch, invalidating all\n * public caches, and marking the associated PR as ready for review.\n *\n * Public pages will serve content from the published branch on the next request.\n */\nexport const publishBranch = async (branchName: string): Promise<ActionResult> => {\n try {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const pointerRef = getPublishedPointerRef();\n const targetBranch = pointerRef ?? baseBranch;\n const pointerPath = getPointerFilePath();\n const content = serializePointerPayload(branchName);\n\n await saveGitHubFile(pointerPath, content, `Publish branch ${branchName} (${pointerPath})`, targetBranch);\n await waitForPublicReadConsistency(pointerPath, content, targetBranch);\n await buildJsons('');\n\n if (branchName !== baseBranch) {\n try {\n await markPRReadyForReview(branchName);\n } catch (_e) {\n // PR marking is best-effort — don't fail publish if it errors\n }\n }\n\n return actionOk();\n } catch (e) {\n return actionErr(new Error(`Failed to publish branch: ${getErrorMessage(e)}`));\n }\n};\n\n/**\n * Fetch the last 5 commits that touched the given entry file for display in the\n * editor History panel. Production-only (dev mode writes to the local FS so there\n * is no GitHub history to show). Best-effort: any failure resolves to an empty\n * shape so the UI never throws.\n */\nexport const getEntryCommits = async (filePath: string): Promise<EntryCommitHistory> => {\n const empty: EntryCommitHistory = { commits: [], seeAllUrl: '' };\n\n if (!isProductionMode()) {\n return empty;\n }\n if (!filePath) {\n return empty;\n }\n\n try {\n const { owner, repo } = assertGitHubConfig();\n const [octokit] = getPublicOctokits();\n const baseBranch = getConfig().git.baseBranch;\n\n const { data } = await octokit.rest.repos.listCommits({\n owner,\n repo,\n path: filePath,\n per_page: 5,\n });\n\n const commits: EntryCommit[] = data.slice(0, 5).map((c) => {\n const gitAuthor = c.commit.author;\n const committer = c.commit.committer;\n const ghAuthor = c.author;\n\n const message = (c.commit.message || '').split('\\n', 1)[0];\n const login = ghAuthor?.login ?? null;\n const name = gitAuthor?.name || ghAuthor?.login || 'unknown';\n const avatarUrl = ghAuthor?.avatar_url ?? null;\n const committedAt = gitAuthor?.date || committer?.date || '';\n\n return {\n sha: c.sha,\n shortSha: c.sha.slice(0, 7),\n message,\n author: { login, name, avatarUrl },\n committedAt,\n url: c.html_url,\n };\n });\n\n const seeAllUrl = `https://github.com/${owner}/${repo}/commits/${encodeURIComponent(baseBranch)}/${encodeURI(filePath)}`;\n\n return { commits, seeAllUrl };\n } catch (e) {\n logCmsServerError({ operation: 'getEntryCommits', message: getErrorMessage(e) });\n return empty;\n }\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../admin/actions/git.ts"],"sourcesContent":["'use server';\n\nimport './registerConfig';\n\nimport { execFile } from 'node:child_process';\n\nimport { cookies } from 'next/headers';\n\nimport { getConfig } from '../../lib/configStore';\n\nimport {\n assertGitHubConfig,\n createGitHubBranch,\n createGitHubCMSPullRequest,\n createGitHubPullRequest,\n getGitHubFile,\n getPublicOctokits,\n getPublishedPointerRef,\n isProductionMode,\n listGitHubCMSPullRequests,\n listGitHubRecentCMSPullRequests,\n markPRReadyForReview,\n resolveContentBranch,\n saveGitHubFile,\n type RecentCMSPullRequestState,\n} from '../github';\nimport type { EntryCommit, EntryCommitHistory } from '../../types';\nimport {\n BRANCH_HISTORY_FILE_PATH,\n parseBranchHistoryFile,\n serializeBranchHistoryFile,\n upsertBranchWorkspace,\n} from '../../lib/branchHistory';\nimport { logCmsServerError } from '../../lib/cmsServerLog';\nimport {\n actionErr,\n actionOk,\n getErrorMessage,\n type ActionResult,\n type CreateBranchInput,\n type CreateBranchResult,\n type CreatePRResult,\n} from './utils';\nimport { getPointerFilePath, serializePointerPayload } from '../../lib/contentBranch';\nimport { warmBranch } from '../store/contentStore';\nimport { buildJsons } from './build';\nimport { waitForPublicReadConsistency } from './files';\n\nconst CMS_BRANCH_COOKIE = 'cms-active-branch';\n\nexport const pushToGit = async (): Promise<ActionResult> => {\n if (isProductionMode()) {\n // In production, commits are made via GitHub API on each save — no push needed\n return actionOk();\n }\n\n try {\n const cookieStore = await cookies();\n const activeBranch = cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n const pushCmd = activeBranch\n ? `git commit -am CommitFromCMS && git push origin HEAD:${activeBranch}`\n : 'git commit -am CommitFromCMS && git push';\n\n await new Promise<void>((resolve, reject) => {\n execFile('sh', ['-c', pushCmd], (error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n });\n\n return actionOk();\n } catch (e) {\n return actionErr(new Error(`Failed git push: ${getErrorMessage(e)}`));\n }\n};\n\nexport const createPR = async (): Promise<CreatePRResult> => {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const title = `CMS content update (${new Date().toISOString().split('T')[0]})`;\n const body = `Content changes from the CMS editor.\\n\\nSource branch: \\`${baseBranch}\\``;\n\n try {\n const { url, number } = await createGitHubPullRequest(title, body, baseBranch);\n return { success: true, url, number };\n } catch (e) {\n return actionErr(e);\n }\n};\n\nexport const getIsProduction = async () => {\n return isProductionMode();\n};\n\n/**\n * Get the currently active CMS branch.\n * In dev: falls back to the local git branch name.\n * In prod: falls back to `config.git.baseBranch`.\n */\nexport const getBranch = async (): Promise<string> => {\n const cookieStore = await cookies();\n const activeBranch = cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n\n if (activeBranch) return activeBranch;\n\n if (!isProductionMode()) {\n try {\n return await new Promise<string>((resolve, reject) => {\n execFile('git', ['branch', '--show-current'], (error, stdout) => {\n if (error) reject(error);\n else resolve(stdout.trim());\n });\n });\n } catch (_e) {\n // fall through to config.git.baseBranch\n }\n }\n\n return getConfig().git.baseBranch;\n};\n\n/**\n * Returns true if a CMS feature branch is currently active (cookie is set).\n */\nexport const hasActiveBranch = async (): Promise<boolean> => {\n const cookieStore = await cookies();\n return !!cookieStore.get(CMS_BRANCH_COOKIE)?.value;\n};\n\n/**\n * Create a GitHub branch from `config.git.baseBranch`, commit `cms/branch-history.json` so the\n * branch has at least one commit, open a draft PR with the `cms-update` label (best effort),\n * and set the active CMS branch cookie.\n */\nexport const createBranch = async (input: CreateBranchInput): Promise<CreateBranchResult> => {\n const config = getConfig();\n const branchName = input.branchName.trim();\n const workspaceTitle = input.title.trim();\n const description = input.description?.trim();\n\n if (!branchName) {\n return { success: false, error: 'Branch name is required.' };\n }\n\n if (!workspaceTitle) {\n return { success: false, error: 'Workspace title is required.' };\n }\n\n const baseBranch = config.git.baseBranch;\n\n try {\n await createGitHubBranch(branchName);\n\n const historyRaw = await getGitHubFile(BRANCH_HISTORY_FILE_PATH, baseBranch);\n const historyDoc = parseBranchHistoryFile(historyRaw?.content ?? '');\n const merged = upsertBranchWorkspace(historyDoc, branchName, {\n title: workspaceTitle,\n ...(description ? { description } : {}),\n createdAt: new Date().toISOString(),\n });\n const historyContent = serializeBranchHistoryFile(merged);\n\n await saveGitHubFile(BRANCH_HISTORY_FILE_PATH, historyContent, 'CMS: add branch workspace metadata', branchName);\n } catch (e) {\n const msg = getErrorMessage(e);\n logCmsServerError({\n operation: 'createBranch.metadata',\n branch: branchName,\n message: msg,\n });\n return { success: false, error: msg };\n }\n\n const prBodyLines = [\n `Content changes from the CMS editor.`,\n '',\n `Branch: \\`${branchName}\\``,\n ...(description ? ['', description] : []),\n ];\n const prBody = prBodyLines.join('\\n');\n\n let prUrl = '';\n let prWarning: string | undefined;\n\n try {\n const pr = await createGitHubCMSPullRequest(branchName, workspaceTitle, prBody, baseBranch);\n prUrl = pr.url;\n } catch (e) {\n prWarning = getErrorMessage(e);\n }\n\n const cookieStore = await cookies();\n cookieStore.set(CMS_BRANCH_COOKIE, branchName, { path: '/' });\n\n // Pre-populate the content store for the new branch\n if (isProductionMode()) {\n warmBranch(branchName).catch(() => {\n /* best-effort */\n });\n }\n\n return prWarning ? { success: true, prUrl: '', prWarning } : { success: true, prUrl };\n};\n\n/**\n * Switch the active CMS branch to an existing branch (by PR).\n */\nexport const setActiveBranch = async (branchName: string): Promise<void> => {\n const cookieStore = await cookies();\n cookieStore.set(CMS_BRANCH_COOKIE, branchName, { path: '/' });\n\n // Pre-populate the content store so the first read after switching is instant\n if (isProductionMode()) {\n warmBranch(branchName).catch(() => {\n /* best-effort — first read will trigger a blocking fetch if this fails */\n });\n }\n};\n\n/**\n * Clear the active CMS branch, reverting listing/reads to `config.git.baseBranch`.\n */\nexport const clearBranch = async (): Promise<void> => {\n const cookieStore = await cookies();\n cookieStore.delete(CMS_BRANCH_COOKIE);\n};\n\nexport type CMSBranch = {\n branch: string;\n prUrl: string;\n prNumber: number;\n title: string;\n isPublished: boolean;\n};\n\n/**\n * List CMS branches for the header: base branch plus **open** PRs labelled `cms-update`\n * (see `listGitHubCMSPullRequests`). Remote `cms/*` refs without an open labelled PR are omitted.\n */\nexport const listCMSBranches = async (): Promise<CMSBranch[]> => {\n try {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const pointerBranch = config.git.publishedPointerBranch?.trim();\n\n const [publishedBranch, prList] = await Promise.all([resolveContentBranch(), listGitHubCMSPullRequests()]);\n\n const base: CMSBranch = {\n branch: baseBranch,\n prUrl: '',\n prNumber: 0,\n title: baseBranch,\n isPublished: publishedBranch === baseBranch,\n };\n\n const features: CMSBranch[] = [];\n\n for (const pr of prList) {\n const branch = pr.branch.trim();\n if (!branch || branch === baseBranch) continue;\n if (pointerBranch && branch === pointerBranch) continue;\n if (!branch.startsWith('cms/')) continue;\n features.push({\n branch,\n prUrl: pr.prUrl,\n prNumber: pr.prNumber,\n title: pr.title.trim() || branch,\n isPublished: branch === publishedBranch,\n });\n }\n\n features.sort((a, b) => b.prNumber - a.prNumber);\n\n return [base, ...features];\n } catch (_e) {\n return [];\n }\n};\n\n/**\n * Publish a branch by writing `{ \"branch\": \"<name>\", \"buildId\": \"<id>\" }` into the per-build\n * pointer file (see {@link getPointerFilePath}) on the pointer branch when\n * `config.git.publishedPointerBranch` is set, otherwise on the base branch, invalidating all\n * public caches, and marking the associated PR as ready for review.\n *\n * Public pages will serve content from the published branch on the next request.\n */\nexport const publishBranch = async (branchName: string): Promise<ActionResult> => {\n try {\n const config = getConfig();\n const baseBranch = config.git.baseBranch;\n const pointerRef = getPublishedPointerRef();\n const targetBranch = pointerRef ?? baseBranch;\n const pointerPath = getPointerFilePath();\n const content = serializePointerPayload(branchName);\n\n await saveGitHubFile(pointerPath, content, `Publish branch ${branchName} (${pointerPath})`, targetBranch);\n await waitForPublicReadConsistency(pointerPath, content, targetBranch);\n await buildJsons('');\n\n if (branchName !== baseBranch) {\n try {\n await markPRReadyForReview(branchName);\n } catch (_e) {\n // PR marking is best-effort — don't fail publish if it errors\n }\n }\n\n return actionOk();\n } catch (e) {\n return actionErr(new Error(`Failed to publish branch: ${getErrorMessage(e)}`));\n }\n};\n\n/**\n * Fetch the last 5 commits that touched the given entry file for display in the\n * editor History panel. Production-only (dev mode writes to the local FS so there\n * is no GitHub history to show). Best-effort: any failure resolves to an empty\n * shape so the UI never throws.\n */\nexport const getEntryCommits = async (filePath: string): Promise<EntryCommitHistory> => {\n const empty: EntryCommitHistory = { commits: [], seeAllUrl: '' };\n\n if (!isProductionMode()) {\n return empty;\n }\n if (!filePath) {\n return empty;\n }\n\n try {\n const { owner, repo } = assertGitHubConfig();\n const [octokit] = getPublicOctokits();\n const baseBranch = getConfig().git.baseBranch;\n\n const { data } = await octokit.rest.repos.listCommits({\n owner,\n repo,\n path: filePath,\n per_page: 5,\n });\n\n const commits: EntryCommit[] = data.slice(0, 5).map((c) => {\n const gitAuthor = c.commit.author;\n const committer = c.commit.committer;\n const ghAuthor = c.author;\n\n const message = (c.commit.message || '').split('\\n', 1)[0];\n const login = ghAuthor?.login ?? null;\n const name = gitAuthor?.name || ghAuthor?.login || 'unknown';\n const avatarUrl = ghAuthor?.avatar_url ?? null;\n const committedAt = gitAuthor?.date || committer?.date || '';\n\n return {\n sha: c.sha,\n shortSha: c.sha.slice(0, 7),\n message,\n author: { login, name, avatarUrl },\n committedAt,\n url: c.html_url,\n };\n });\n\n const seeAllUrl = `https://github.com/${owner}/${repo}/commits/${encodeURIComponent(baseBranch)}/${encodeURI(filePath)}`;\n\n return { commits, seeAllUrl };\n } catch (e) {\n logCmsServerError({ operation: 'getEntryCommits', message: getErrorMessage(e) });\n return empty;\n }\n};\n\n// ---------------------------------------------------------------------------\n// Dashboard reads (production-only, best-effort)\n// ---------------------------------------------------------------------------\n\nexport type RecentCMSPullRequest = {\n branch: string;\n prUrl: string;\n prNumber: number;\n title: string;\n state: RecentCMSPullRequestState;\n updatedAt: string;\n authorLogin: string | null;\n authorAvatarUrl: string | null;\n};\n\n/**\n * Most-recently-updated PRs tagged with the `cms-update` label, across all\n * states (open / merged / closed). Powers the dashboard \"Recent pull requests\"\n * card. Returns `[]` in dev or on error.\n */\nexport const getRecentCMSPullRequests = async (limit = 5): Promise<RecentCMSPullRequest[]> => {\n if (!isProductionMode()) return [];\n\n try {\n return await listGitHubRecentCMSPullRequests(limit);\n } catch (e) {\n logCmsServerError({ operation: 'getRecentCMSPullRequests', message: getErrorMessage(e) });\n return [];\n }\n};\n"],"mappings":"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","names":[]}
@@ -84,6 +84,22 @@ export declare const listGitHubCMSPullRequests: () => Promise<{
84
84
  prNumber: number;
85
85
  title: string;
86
86
  }[]>;
87
+ export type RecentCMSPullRequestState = 'open' | 'merged' | 'closed';
88
+ /**
89
+ * List the most-recently-updated PRs tagged with the `cms-update` label,
90
+ * across all states (open / merged / closed). Used by the dashboard
91
+ * "Recent pull requests" card.
92
+ */
93
+ export declare const listGitHubRecentCMSPullRequests: (limit?: number) => Promise<{
94
+ branch: string;
95
+ prUrl: string;
96
+ prNumber: number;
97
+ title: string;
98
+ state: RecentCMSPullRequestState;
99
+ updatedAt: string;
100
+ authorLogin: string | null;
101
+ authorAvatarUrl: string | null;
102
+ }[]>;
87
103
  /**
88
104
  * Create a pull request from the configured branch to the target branch.
89
105
  */
@@ -1 +1 @@
1
- {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../admin/github.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../admin/github.ts"],"names":[],"mappings":"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"}
@@ -381,6 +381,32 @@ const listGitHubCMSPullRequests = async () => {
381
381
  title: pr.title
382
382
  }));
383
383
  };
384
+ const listGitHubRecentCMSPullRequests = async (limit = 5) => {
385
+ const octokit = await getWriteOctokit();
386
+ const { owner, repo } = assertGitHubConfig();
387
+ const { data: prs } = await octokit.rest.pulls.list({
388
+ owner,
389
+ repo,
390
+ state: "all",
391
+ sort: "updated",
392
+ direction: "desc",
393
+ per_page: 50
394
+ });
395
+ return prs.filter((pr) => pr.labels.some((l) => l.name === "cms-update")).slice(0, Math.max(1, limit)).map((pr) => {
396
+ var _a, _b, _c, _d;
397
+ const state = pr.merged_at ? "merged" : pr.state === "open" ? "open" : "closed";
398
+ return {
399
+ branch: pr.head.ref,
400
+ prUrl: pr.html_url,
401
+ prNumber: pr.number,
402
+ title: pr.title,
403
+ state,
404
+ updatedAt: pr.updated_at,
405
+ authorLogin: (_b = (_a = pr.user) == null ? void 0 : _a.login) != null ? _b : null,
406
+ authorAvatarUrl: (_d = (_c = pr.user) == null ? void 0 : _c.avatar_url) != null ? _d : null
407
+ };
408
+ });
409
+ };
384
410
  const createGitHubPullRequest = async (title, body, targetBranch = "main") => {
385
411
  const octokit = await getWriteOctokit();
386
412
  const { owner, repo, branch } = assertGitHubConfig();
@@ -443,6 +469,7 @@ export {
443
469
  listGitHubCMSPullRequests,
444
470
  listGitHubFiles,
445
471
  listGitHubFilesRecursive,
472
+ listGitHubRecentCMSPullRequests,
446
473
  markPRReadyForReview,
447
474
  mergePullRequest,
448
475
  readGitHubBinaryFilePublic,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../admin/github.ts"],"sourcesContent":["import { Octokit } from 'octokit';\nimport { getServerSession } from 'next-auth';\n\nimport { logCmsServerError } from '../lib/cmsServerLog';\nimport { mapGitHubApiErrorToContentSource } from '../lib/contentSourceError';\n\nimport { authOptions } from './auth';\n\nimport {\n assertGitHubConfig,\n getPublicOctokits,\n getPublishedPointerRef,\n readGitHubFilePublic,\n listGitHubFiles,\n listGitHubFilesRecursive,\n listGitHubBranchRefsByPrefix,\n resolveContentBranch,\n isProductionMode,\n} from '../github-public';\n\nexport {\n assertGitHubConfig,\n getPublicOctokits,\n getPublishedPointerRef,\n readGitHubFilePublic,\n listGitHubFiles,\n listGitHubFilesRecursive,\n listGitHubBranchRefsByPrefix,\n resolveContentBranch,\n isProductionMode,\n};\n\nconst getErrorMessage = (error: unknown): string => {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n};\n\nconst formatGitHubApiError = (error: any): string => {\n const status = error?.status;\n const message = error?.response?.data?.message || error?.message || getErrorMessage(error);\n\n if (status) {\n return `${status} ${message}`;\n }\n\n return message;\n};\n\nconst getDefaultBranchRef = async (octokit: Octokit, owner: string, repo: string): Promise<string> => {\n const { data: repoData } = await octokit.rest.repos.get({ owner, repo });\n const defaultBranch = repoData.default_branch || 'main';\n const { data: defaultRef } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${defaultBranch}`,\n });\n\n return defaultRef.object.sha;\n};\n\nconst ensureBranchExists = async (octokit: Octokit, owner: string, repo: string, branch: string) => {\n try {\n await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${branch}`,\n });\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n\n const baseSha = await getDefaultBranchRef(octokit, owner, repo);\n\n await octokit.rest.git.createRef({\n owner,\n repo,\n ref: `refs/heads/${branch}`,\n sha: baseSha,\n });\n }\n};\n\nconst getOctokit = async () => {\n const session = await getServerSession(authOptions);\n const accessToken = (session as any)?.accessToken;\n\n if (!accessToken) {\n throw new Error('No GitHub access token found. Please sign in again.');\n }\n\n return new Octokit({ auth: accessToken });\n};\n\n/**\n * Get an Octokit client suitable for write operations (save, delete, create PR).\n * Prefers CMS_GITHUB_TOKEN so it works even when the GitHub App is not installed\n * on the target repo. Falls back to the user session token.\n */\nconst getWriteOctokit = async (): Promise<Octokit> => {\n const token = process.env.CMS_GITHUB_TOKEN?.trim();\n\n if (token) {\n return new Octokit({ auth: token });\n }\n\n return getOctokit();\n};\n\n/**\n * Read a binary file from GitHub using a static server token (no user session required).\n * If `branch` is provided, reads from that branch directly (used by the media route\n * so editors see uploads on their active feature branch before publishing).\n * Otherwise uses {@link resolveContentBranch} so assets match the same ref as\n * `query()` / public JSON (not only `git.baseBranch`).\n * Returns a Buffer, or null if the file does not exist.\n */\nexport const readGitHubBinaryFilePublic = async (filePath: string, branch?: string): Promise<Buffer | null> => {\n const { owner, repo } = assertGitHubConfig();\n const ref = branch ?? (await resolveContentBranch());\n const clients = getPublicOctokits();\n\n for (const octokit of clients) {\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filePath,\n ref,\n });\n\n if ('content' in data && data.type === 'file') {\n return Buffer.from(data.content.replace(/\\n/g, ''), 'base64');\n }\n\n return null;\n } catch (error: any) {\n if (error.status === 429) {\n throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });\n }\n\n if (error.status === 401 || error.status === 403 || error.status === 404) {\n continue;\n }\n\n throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });\n }\n }\n\n return null;\n};\n\n/**\n * Get a file's content and SHA from the GitHub repo.\n * Returns { content, sha } or null if the file doesn't exist.\n */\nexport const getGitHubFile = async (\n filePath: string,\n branch?: string,\n): Promise<{ content: string; sha: string } | null> => {\n const [octokit] = getPublicOctokits();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const ref = branch || configBranch;\n\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filePath,\n ref,\n });\n\n if ('content' in data && data.type === 'file') {\n const content = Buffer.from(data.content, 'base64').toString('utf-8');\n return { content, sha: data.sha };\n }\n\n return null;\n } catch (error: any) {\n if (error.status === 404) {\n return null;\n }\n throw error;\n }\n};\n\n/**\n * Create or update a file in the GitHub repo via a commit.\n *\n * @param existingSha Optional SHA of the existing file blob. When provided from\n * the content store, skips the pre-read API call (saves 1 request per write).\n */\nexport const saveGitHubFile = async (\n filePath: string,\n content: string,\n message: string,\n branch?: string,\n existingSha?: string,\n) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n // Get existing file SHA if it exists (required for updates) — skip when caller provides it\n const existing = existingSha ? { sha: existingSha } : await getGitHubFile(filePath, targetBranch);\n\n try {\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filePath,\n message,\n content: Buffer.from(content).toString('base64'),\n branch: targetBranch,\n ...(existing ? { sha: existing.sha } : {}),\n });\n } catch (error: any) {\n if (error.status === 403) {\n throw new Error(\n 'GitHub token is missing repository write permissions. Ensure the app is installed on the repo and has Contents: Read and write permission.',\n );\n }\n\n if (error.status === 409) {\n throw new Error(\n 'GitHub rejected the commit due to branch protection or write conflicts. Use a writable branch (for example cms/edits) or relax direct-push protection on main.',\n );\n }\n\n if (error.status === 422) {\n throw new Error(`GitHub validation failed while saving content: ${formatGitHubApiError(error)}`);\n }\n\n throw new Error(`GitHub save failed: ${formatGitHubApiError(error)}`);\n }\n};\n\n/**\n * Create or update a binary file in the GitHub repo via a commit.\n * Unlike saveGitHubFile, this accepts a Buffer directly to avoid encoding corruption.\n */\nexport const saveGitHubBinaryFile = async (filePath: string, buffer: Buffer, message: string, branch?: string) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n const existing = await getGitHubFile(filePath, targetBranch);\n\n try {\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filePath,\n message,\n content: buffer.toString('base64'),\n branch: targetBranch,\n ...(existing ? { sha: existing.sha } : {}),\n });\n } catch (error: any) {\n if (error.status === 403) {\n throw new Error(\n 'GitHub token is missing repository write permissions. Ensure the app is installed on the repo and has Contents: Read and write permission.',\n );\n }\n\n if (error.status === 409) {\n throw new Error(\n 'GitHub rejected the commit due to branch protection or write conflicts. Use a writable branch (for example cms/edits) or relax direct-push protection on main.',\n );\n }\n\n if (error.status === 422) {\n throw new Error(`GitHub validation failed while saving content: ${formatGitHubApiError(error)}`);\n }\n\n throw new Error(`GitHub save failed: ${formatGitHubApiError(error)}`);\n }\n};\n\n/**\n * A single file change in a multi-file commit. `delete` removes the path\n * from the tree; `upsert-text` and `upsert-binary` create or update it.\n */\nexport type GitHubBatchChange =\n | { kind: 'upsert-text'; path: string; content: string }\n | { kind: 'upsert-binary'; path: string; content: Buffer }\n | { kind: 'delete'; path: string };\n\n/**\n * Commit multiple file changes atomically as a single GitHub commit on\n * `branch`. Used by the visual schema editor to write `cms/schema.json`\n * plus regenerated artifacts plus migrated entry files in one commit.\n *\n * Implementation: builds a git tree off the branch's current HEAD, creates a\n * single commit, then fast-forwards the branch. If `branch` does not exist,\n * it is created from `git.baseBranch` first (mirrors `ensureBranchExists`).\n *\n * Throws if all `changes` are no-ops (e.g. all deletes target missing paths)\n * — GitHub rejects empty trees.\n */\nexport const commitMultipleFilesToGitHub = async (\n changes: readonly GitHubBatchChange[],\n message: string,\n branch?: string,\n): Promise<{ sha: string }> => {\n if (changes.length === 0) {\n throw new Error('commitMultipleFilesToGitHub: no changes provided');\n }\n\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n const { data: ref } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${targetBranch}`,\n });\n const headSha = ref.object.sha;\n\n const { data: headCommit } = await octokit.rest.git.getCommit({\n owner,\n repo,\n commit_sha: headSha,\n });\n const baseTreeSha = headCommit.tree.sha;\n\n const tree: {\n path: string;\n mode: '100644';\n type: 'blob';\n sha?: string | null;\n content?: string;\n }[] = [];\n\n for (const change of changes) {\n if (change.kind === 'delete') {\n tree.push({ path: change.path, mode: '100644', type: 'blob', sha: null });\n continue;\n }\n if (change.kind === 'upsert-text') {\n // Push content directly. GitHub will create a blob server-side.\n tree.push({ path: change.path, mode: '100644', type: 'blob', content: change.content });\n continue;\n }\n // Binary: must upload as a blob first, then reference its SHA.\n const { data: blob } = await octokit.rest.git.createBlob({\n owner,\n repo,\n content: change.content.toString('base64'),\n encoding: 'base64',\n });\n tree.push({ path: change.path, mode: '100644', type: 'blob', sha: blob.sha });\n }\n\n let newTreeSha: string;\n try {\n const { data: newTree } = await octokit.rest.git.createTree({\n owner,\n repo,\n base_tree: baseTreeSha,\n tree,\n });\n newTreeSha = newTree.sha;\n } catch (error: any) {\n throw new Error(`GitHub createTree failed: ${formatGitHubApiError(error)}`);\n }\n\n if (newTreeSha === baseTreeSha) {\n throw new Error('commitMultipleFilesToGitHub: changes produced no diff (every file was already up to date)');\n }\n\n const { data: newCommit } = await octokit.rest.git.createCommit({\n owner,\n repo,\n message,\n tree: newTreeSha,\n parents: [headSha],\n });\n\n try {\n await octokit.rest.git.updateRef({\n owner,\n repo,\n ref: `heads/${targetBranch}`,\n sha: newCommit.sha,\n });\n } catch (error: any) {\n if (error.status === 422) {\n throw new Error(\n `Branch \"${targetBranch}\" moved while committing. Retry the schema save: ${formatGitHubApiError(error)}`,\n );\n }\n throw new Error(`GitHub updateRef failed: ${formatGitHubApiError(error)}`);\n }\n\n return { sha: newCommit.sha };\n};\n\n/**\n * Delete a file from the GitHub repo via a commit.\n */\nexport const deleteGitHubFile = async (filePath: string, message: string, branch?: string) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n const existing = await getGitHubFile(filePath, targetBranch);\n\n if (!existing) {\n return; // File doesn't exist, nothing to delete\n }\n\n await octokit.rest.repos.deleteFile({\n owner,\n repo,\n path: filePath,\n message,\n sha: existing.sha,\n branch: targetBranch,\n });\n};\n\n/**\n * Create a new branch in the GitHub repo from `config.git.baseBranch`.\n */\nexport const createGitHubBranch = async (branchName: string): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: baseBranch } = assertGitHubConfig();\n\n const { data: baseRef } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${baseBranch}`,\n });\n\n await octokit.rest.git.createRef({\n owner,\n repo,\n ref: `refs/heads/${branchName}`,\n sha: baseRef.object.sha,\n });\n};\n\nconst ensureLabelExists = async (octokit: Octokit, owner: string, repo: string, name: string, color: string) => {\n try {\n await octokit.rest.issues.getLabel({ owner, repo, name });\n } catch (error: any) {\n if (error.status === 404) {\n await octokit.rest.issues.createLabel({\n owner,\n repo,\n name,\n color,\n description: 'Content updates from the CMS editor',\n });\n } else {\n throw error;\n }\n }\n};\n\n/**\n * Create a draft pull request for a CMS branch, adding the 'cms-update' label.\n */\nexport const createGitHubCMSPullRequest = async (\n headBranch: string,\n title: string,\n body: string,\n targetBranch: string,\n): Promise<{ url: string; number: number }> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n let data: { html_url: string; number: number };\n\n try {\n const res = await octokit.rest.pulls.create({\n owner,\n repo,\n title,\n body,\n head: headBranch,\n base: targetBranch,\n draft: true,\n });\n data = res.data;\n } catch (error: any) {\n const detail = formatGitHubApiError(error);\n logCmsServerError({\n operation: 'pulls.create',\n branch: headBranch,\n status: error?.status,\n message: detail,\n });\n\n if (error?.status === 422) {\n const lower = detail.toLowerCase();\n if (lower.includes('no commits between')) {\n throw new Error(\n 'Could not open draft pull request: GitHub reports no new commits on this branch versus the base branch. Save branch metadata should have created a commit — please try again or open a pull request manually on GitHub.',\n );\n }\n\n throw new Error(`Could not open draft pull request: ${detail}`);\n }\n\n throw new Error(`Could not open draft pull request: ${detail}`);\n }\n\n try {\n await ensureLabelExists(octokit, owner, repo, 'cms-update', '0075ca');\n await octokit.rest.issues.addLabels({\n owner,\n repo,\n issue_number: data.number,\n labels: ['cms-update'],\n });\n } catch (_e) {\n // Label is cosmetic — don't fail branch creation if labelling errors\n }\n\n return { url: data.html_url, number: data.number };\n};\n\n/**\n * List all open PRs tagged with the 'cms-update' label.\n */\nexport const listGitHubCMSPullRequests = async (): Promise<\n { branch: string; prUrl: string; prNumber: number; title: string }[]\n> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n const { data: prs } = await octokit.rest.pulls.list({\n owner,\n repo,\n state: 'open',\n per_page: 100,\n });\n\n return prs\n .filter((pr) => pr.labels.some((l) => l.name === 'cms-update'))\n .map((pr) => ({\n branch: pr.head.ref,\n prUrl: pr.html_url,\n prNumber: pr.number,\n title: pr.title,\n }));\n};\n\n/**\n * Create a pull request from the configured branch to the target branch.\n */\nexport const createGitHubPullRequest = async (\n title: string,\n body: string,\n targetBranch: string = 'main',\n): Promise<{ url: string; number: number }> => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch } = assertGitHubConfig();\n\n if (branch === targetBranch) {\n throw new Error(`Cannot create PR: source branch \"${branch}\" is the same as target branch \"${targetBranch}\"`);\n }\n\n const { data } = await octokit.rest.pulls.create({\n owner,\n repo,\n title,\n body,\n head: branch,\n base: targetBranch,\n });\n\n return { url: data.html_url, number: data.number };\n};\n\n/**\n * Merge a pull request via squash.\n */\nexport const mergePullRequest = async (prNumber: number): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n await octokit.rest.pulls.merge({\n owner,\n repo,\n pull_number: prNumber,\n merge_method: 'squash',\n });\n};\n\n/**\n * Convert a draft pull request for a CMS branch to \"Ready for Review\".\n * No-op if no open PR exists for the branch or if it is already non-draft.\n */\nexport const markPRReadyForReview = async (branchName: string): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n const { data: prs } = await octokit.rest.pulls.list({\n owner,\n repo,\n head: `${owner}:${branchName}`,\n state: 'open',\n per_page: 1,\n });\n\n const pr = prs[0];\n if (!pr || !pr.draft) return;\n\n await octokit.graphql(\n `mutation MarkReadyForReview($pullRequestId: ID!) {\n markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {\n pullRequest { isDraft }\n }\n }`,\n { pullRequestId: pr.node_id },\n );\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../admin/github.ts"],"sourcesContent":["import { Octokit } from 'octokit';\nimport { getServerSession } from 'next-auth';\n\nimport { logCmsServerError } from '../lib/cmsServerLog';\nimport { mapGitHubApiErrorToContentSource } from '../lib/contentSourceError';\n\nimport { authOptions } from './auth';\n\nimport {\n assertGitHubConfig,\n getPublicOctokits,\n getPublishedPointerRef,\n readGitHubFilePublic,\n listGitHubFiles,\n listGitHubFilesRecursive,\n listGitHubBranchRefsByPrefix,\n resolveContentBranch,\n isProductionMode,\n} from '../github-public';\n\nexport {\n assertGitHubConfig,\n getPublicOctokits,\n getPublishedPointerRef,\n readGitHubFilePublic,\n listGitHubFiles,\n listGitHubFilesRecursive,\n listGitHubBranchRefsByPrefix,\n resolveContentBranch,\n isProductionMode,\n};\n\nconst getErrorMessage = (error: unknown): string => {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n};\n\nconst formatGitHubApiError = (error: any): string => {\n const status = error?.status;\n const message = error?.response?.data?.message || error?.message || getErrorMessage(error);\n\n if (status) {\n return `${status} ${message}`;\n }\n\n return message;\n};\n\nconst getDefaultBranchRef = async (octokit: Octokit, owner: string, repo: string): Promise<string> => {\n const { data: repoData } = await octokit.rest.repos.get({ owner, repo });\n const defaultBranch = repoData.default_branch || 'main';\n const { data: defaultRef } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${defaultBranch}`,\n });\n\n return defaultRef.object.sha;\n};\n\nconst ensureBranchExists = async (octokit: Octokit, owner: string, repo: string, branch: string) => {\n try {\n await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${branch}`,\n });\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n\n const baseSha = await getDefaultBranchRef(octokit, owner, repo);\n\n await octokit.rest.git.createRef({\n owner,\n repo,\n ref: `refs/heads/${branch}`,\n sha: baseSha,\n });\n }\n};\n\nconst getOctokit = async () => {\n const session = await getServerSession(authOptions);\n const accessToken = (session as any)?.accessToken;\n\n if (!accessToken) {\n throw new Error('No GitHub access token found. Please sign in again.');\n }\n\n return new Octokit({ auth: accessToken });\n};\n\n/**\n * Get an Octokit client suitable for write operations (save, delete, create PR).\n * Prefers CMS_GITHUB_TOKEN so it works even when the GitHub App is not installed\n * on the target repo. Falls back to the user session token.\n */\nconst getWriteOctokit = async (): Promise<Octokit> => {\n const token = process.env.CMS_GITHUB_TOKEN?.trim();\n\n if (token) {\n return new Octokit({ auth: token });\n }\n\n return getOctokit();\n};\n\n/**\n * Read a binary file from GitHub using a static server token (no user session required).\n * If `branch` is provided, reads from that branch directly (used by the media route\n * so editors see uploads on their active feature branch before publishing).\n * Otherwise uses {@link resolveContentBranch} so assets match the same ref as\n * `query()` / public JSON (not only `git.baseBranch`).\n * Returns a Buffer, or null if the file does not exist.\n */\nexport const readGitHubBinaryFilePublic = async (filePath: string, branch?: string): Promise<Buffer | null> => {\n const { owner, repo } = assertGitHubConfig();\n const ref = branch ?? (await resolveContentBranch());\n const clients = getPublicOctokits();\n\n for (const octokit of clients) {\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filePath,\n ref,\n });\n\n if ('content' in data && data.type === 'file') {\n return Buffer.from(data.content.replace(/\\n/g, ''), 'base64');\n }\n\n return null;\n } catch (error: any) {\n if (error.status === 429) {\n throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });\n }\n\n if (error.status === 401 || error.status === 403 || error.status === 404) {\n continue;\n }\n\n throw mapGitHubApiErrorToContentSource(error, { owner, repo, ref });\n }\n }\n\n return null;\n};\n\n/**\n * Get a file's content and SHA from the GitHub repo.\n * Returns { content, sha } or null if the file doesn't exist.\n */\nexport const getGitHubFile = async (\n filePath: string,\n branch?: string,\n): Promise<{ content: string; sha: string } | null> => {\n const [octokit] = getPublicOctokits();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const ref = branch || configBranch;\n\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filePath,\n ref,\n });\n\n if ('content' in data && data.type === 'file') {\n const content = Buffer.from(data.content, 'base64').toString('utf-8');\n return { content, sha: data.sha };\n }\n\n return null;\n } catch (error: any) {\n if (error.status === 404) {\n return null;\n }\n throw error;\n }\n};\n\n/**\n * Create or update a file in the GitHub repo via a commit.\n *\n * @param existingSha Optional SHA of the existing file blob. When provided from\n * the content store, skips the pre-read API call (saves 1 request per write).\n */\nexport const saveGitHubFile = async (\n filePath: string,\n content: string,\n message: string,\n branch?: string,\n existingSha?: string,\n) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n // Get existing file SHA if it exists (required for updates) — skip when caller provides it\n const existing = existingSha ? { sha: existingSha } : await getGitHubFile(filePath, targetBranch);\n\n try {\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filePath,\n message,\n content: Buffer.from(content).toString('base64'),\n branch: targetBranch,\n ...(existing ? { sha: existing.sha } : {}),\n });\n } catch (error: any) {\n if (error.status === 403) {\n throw new Error(\n 'GitHub token is missing repository write permissions. Ensure the app is installed on the repo and has Contents: Read and write permission.',\n );\n }\n\n if (error.status === 409) {\n throw new Error(\n 'GitHub rejected the commit due to branch protection or write conflicts. Use a writable branch (for example cms/edits) or relax direct-push protection on main.',\n );\n }\n\n if (error.status === 422) {\n throw new Error(`GitHub validation failed while saving content: ${formatGitHubApiError(error)}`);\n }\n\n throw new Error(`GitHub save failed: ${formatGitHubApiError(error)}`);\n }\n};\n\n/**\n * Create or update a binary file in the GitHub repo via a commit.\n * Unlike saveGitHubFile, this accepts a Buffer directly to avoid encoding corruption.\n */\nexport const saveGitHubBinaryFile = async (filePath: string, buffer: Buffer, message: string, branch?: string) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n const existing = await getGitHubFile(filePath, targetBranch);\n\n try {\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filePath,\n message,\n content: buffer.toString('base64'),\n branch: targetBranch,\n ...(existing ? { sha: existing.sha } : {}),\n });\n } catch (error: any) {\n if (error.status === 403) {\n throw new Error(\n 'GitHub token is missing repository write permissions. Ensure the app is installed on the repo and has Contents: Read and write permission.',\n );\n }\n\n if (error.status === 409) {\n throw new Error(\n 'GitHub rejected the commit due to branch protection or write conflicts. Use a writable branch (for example cms/edits) or relax direct-push protection on main.',\n );\n }\n\n if (error.status === 422) {\n throw new Error(`GitHub validation failed while saving content: ${formatGitHubApiError(error)}`);\n }\n\n throw new Error(`GitHub save failed: ${formatGitHubApiError(error)}`);\n }\n};\n\n/**\n * A single file change in a multi-file commit. `delete` removes the path\n * from the tree; `upsert-text` and `upsert-binary` create or update it.\n */\nexport type GitHubBatchChange =\n | { kind: 'upsert-text'; path: string; content: string }\n | { kind: 'upsert-binary'; path: string; content: Buffer }\n | { kind: 'delete'; path: string };\n\n/**\n * Commit multiple file changes atomically as a single GitHub commit on\n * `branch`. Used by the visual schema editor to write `cms/schema.json`\n * plus regenerated artifacts plus migrated entry files in one commit.\n *\n * Implementation: builds a git tree off the branch's current HEAD, creates a\n * single commit, then fast-forwards the branch. If `branch` does not exist,\n * it is created from `git.baseBranch` first (mirrors `ensureBranchExists`).\n *\n * Throws if all `changes` are no-ops (e.g. all deletes target missing paths)\n * — GitHub rejects empty trees.\n */\nexport const commitMultipleFilesToGitHub = async (\n changes: readonly GitHubBatchChange[],\n message: string,\n branch?: string,\n): Promise<{ sha: string }> => {\n if (changes.length === 0) {\n throw new Error('commitMultipleFilesToGitHub: no changes provided');\n }\n\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n await ensureBranchExists(octokit, owner, repo, targetBranch);\n\n const { data: ref } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${targetBranch}`,\n });\n const headSha = ref.object.sha;\n\n const { data: headCommit } = await octokit.rest.git.getCommit({\n owner,\n repo,\n commit_sha: headSha,\n });\n const baseTreeSha = headCommit.tree.sha;\n\n const tree: {\n path: string;\n mode: '100644';\n type: 'blob';\n sha?: string | null;\n content?: string;\n }[] = [];\n\n for (const change of changes) {\n if (change.kind === 'delete') {\n tree.push({ path: change.path, mode: '100644', type: 'blob', sha: null });\n continue;\n }\n if (change.kind === 'upsert-text') {\n // Push content directly. GitHub will create a blob server-side.\n tree.push({ path: change.path, mode: '100644', type: 'blob', content: change.content });\n continue;\n }\n // Binary: must upload as a blob first, then reference its SHA.\n const { data: blob } = await octokit.rest.git.createBlob({\n owner,\n repo,\n content: change.content.toString('base64'),\n encoding: 'base64',\n });\n tree.push({ path: change.path, mode: '100644', type: 'blob', sha: blob.sha });\n }\n\n let newTreeSha: string;\n try {\n const { data: newTree } = await octokit.rest.git.createTree({\n owner,\n repo,\n base_tree: baseTreeSha,\n tree,\n });\n newTreeSha = newTree.sha;\n } catch (error: any) {\n throw new Error(`GitHub createTree failed: ${formatGitHubApiError(error)}`);\n }\n\n if (newTreeSha === baseTreeSha) {\n throw new Error('commitMultipleFilesToGitHub: changes produced no diff (every file was already up to date)');\n }\n\n const { data: newCommit } = await octokit.rest.git.createCommit({\n owner,\n repo,\n message,\n tree: newTreeSha,\n parents: [headSha],\n });\n\n try {\n await octokit.rest.git.updateRef({\n owner,\n repo,\n ref: `heads/${targetBranch}`,\n sha: newCommit.sha,\n });\n } catch (error: any) {\n if (error.status === 422) {\n throw new Error(\n `Branch \"${targetBranch}\" moved while committing. Retry the schema save: ${formatGitHubApiError(error)}`,\n );\n }\n throw new Error(`GitHub updateRef failed: ${formatGitHubApiError(error)}`);\n }\n\n return { sha: newCommit.sha };\n};\n\n/**\n * Delete a file from the GitHub repo via a commit.\n */\nexport const deleteGitHubFile = async (filePath: string, message: string, branch?: string) => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: configBranch } = assertGitHubConfig();\n const targetBranch = branch || configBranch;\n\n const existing = await getGitHubFile(filePath, targetBranch);\n\n if (!existing) {\n return; // File doesn't exist, nothing to delete\n }\n\n await octokit.rest.repos.deleteFile({\n owner,\n repo,\n path: filePath,\n message,\n sha: existing.sha,\n branch: targetBranch,\n });\n};\n\n/**\n * Create a new branch in the GitHub repo from `config.git.baseBranch`.\n */\nexport const createGitHubBranch = async (branchName: string): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch: baseBranch } = assertGitHubConfig();\n\n const { data: baseRef } = await octokit.rest.git.getRef({\n owner,\n repo,\n ref: `heads/${baseBranch}`,\n });\n\n await octokit.rest.git.createRef({\n owner,\n repo,\n ref: `refs/heads/${branchName}`,\n sha: baseRef.object.sha,\n });\n};\n\nconst ensureLabelExists = async (octokit: Octokit, owner: string, repo: string, name: string, color: string) => {\n try {\n await octokit.rest.issues.getLabel({ owner, repo, name });\n } catch (error: any) {\n if (error.status === 404) {\n await octokit.rest.issues.createLabel({\n owner,\n repo,\n name,\n color,\n description: 'Content updates from the CMS editor',\n });\n } else {\n throw error;\n }\n }\n};\n\n/**\n * Create a draft pull request for a CMS branch, adding the 'cms-update' label.\n */\nexport const createGitHubCMSPullRequest = async (\n headBranch: string,\n title: string,\n body: string,\n targetBranch: string,\n): Promise<{ url: string; number: number }> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n let data: { html_url: string; number: number };\n\n try {\n const res = await octokit.rest.pulls.create({\n owner,\n repo,\n title,\n body,\n head: headBranch,\n base: targetBranch,\n draft: true,\n });\n data = res.data;\n } catch (error: any) {\n const detail = formatGitHubApiError(error);\n logCmsServerError({\n operation: 'pulls.create',\n branch: headBranch,\n status: error?.status,\n message: detail,\n });\n\n if (error?.status === 422) {\n const lower = detail.toLowerCase();\n if (lower.includes('no commits between')) {\n throw new Error(\n 'Could not open draft pull request: GitHub reports no new commits on this branch versus the base branch. Save branch metadata should have created a commit — please try again or open a pull request manually on GitHub.',\n );\n }\n\n throw new Error(`Could not open draft pull request: ${detail}`);\n }\n\n throw new Error(`Could not open draft pull request: ${detail}`);\n }\n\n try {\n await ensureLabelExists(octokit, owner, repo, 'cms-update', '0075ca');\n await octokit.rest.issues.addLabels({\n owner,\n repo,\n issue_number: data.number,\n labels: ['cms-update'],\n });\n } catch (_e) {\n // Label is cosmetic — don't fail branch creation if labelling errors\n }\n\n return { url: data.html_url, number: data.number };\n};\n\n/**\n * List all open PRs tagged with the 'cms-update' label.\n */\nexport const listGitHubCMSPullRequests = async (): Promise<\n { branch: string; prUrl: string; prNumber: number; title: string }[]\n> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n const { data: prs } = await octokit.rest.pulls.list({\n owner,\n repo,\n state: 'open',\n per_page: 100,\n });\n\n return prs\n .filter((pr) => pr.labels.some((l) => l.name === 'cms-update'))\n .map((pr) => ({\n branch: pr.head.ref,\n prUrl: pr.html_url,\n prNumber: pr.number,\n title: pr.title,\n }));\n};\n\nexport type RecentCMSPullRequestState = 'open' | 'merged' | 'closed';\n\n/**\n * List the most-recently-updated PRs tagged with the `cms-update` label,\n * across all states (open / merged / closed). Used by the dashboard\n * \"Recent pull requests\" card.\n */\nexport const listGitHubRecentCMSPullRequests = async (\n limit = 5,\n): Promise<\n {\n branch: string;\n prUrl: string;\n prNumber: number;\n title: string;\n state: RecentCMSPullRequestState;\n updatedAt: string;\n authorLogin: string | null;\n authorAvatarUrl: string | null;\n }[]\n> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n const { data: prs } = await octokit.rest.pulls.list({\n owner,\n repo,\n state: 'all',\n sort: 'updated',\n direction: 'desc',\n per_page: 50,\n });\n\n return prs\n .filter((pr) => pr.labels.some((l) => l.name === 'cms-update'))\n .slice(0, Math.max(1, limit))\n .map((pr) => {\n const state: RecentCMSPullRequestState = pr.merged_at ? 'merged' : pr.state === 'open' ? 'open' : 'closed';\n return {\n branch: pr.head.ref,\n prUrl: pr.html_url,\n prNumber: pr.number,\n title: pr.title,\n state,\n updatedAt: pr.updated_at,\n authorLogin: pr.user?.login ?? null,\n authorAvatarUrl: pr.user?.avatar_url ?? null,\n };\n });\n};\n\n/**\n * Create a pull request from the configured branch to the target branch.\n */\nexport const createGitHubPullRequest = async (\n title: string,\n body: string,\n targetBranch: string = 'main',\n): Promise<{ url: string; number: number }> => {\n const octokit = await getWriteOctokit();\n const { owner, repo, branch } = assertGitHubConfig();\n\n if (branch === targetBranch) {\n throw new Error(`Cannot create PR: source branch \"${branch}\" is the same as target branch \"${targetBranch}\"`);\n }\n\n const { data } = await octokit.rest.pulls.create({\n owner,\n repo,\n title,\n body,\n head: branch,\n base: targetBranch,\n });\n\n return { url: data.html_url, number: data.number };\n};\n\n/**\n * Merge a pull request via squash.\n */\nexport const mergePullRequest = async (prNumber: number): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n await octokit.rest.pulls.merge({\n owner,\n repo,\n pull_number: prNumber,\n merge_method: 'squash',\n });\n};\n\n/**\n * Convert a draft pull request for a CMS branch to \"Ready for Review\".\n * No-op if no open PR exists for the branch or if it is already non-draft.\n */\nexport const markPRReadyForReview = async (branchName: string): Promise<void> => {\n const octokit = await getWriteOctokit();\n const { owner, repo } = assertGitHubConfig();\n\n const { data: prs } = await octokit.rest.pulls.list({\n owner,\n repo,\n head: `${owner}:${branchName}`,\n state: 'open',\n per_page: 1,\n });\n\n const pr = prs[0];\n if (!pr || !pr.draft) return;\n\n await octokit.graphql(\n `mutation MarkReadyForReview($pullRequestId: ID!) {\n markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {\n pullRequest { isDraft }\n }\n }`,\n { pullRequestId: pr.node_id },\n );\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Most-recently-updated PRs tagged `cms-update`, across all states. Powers the
3
+ * dashboard "Recent pull requests" card. Tier `realtime` — PR state is
4
+ * mutated outside the admin UI (review, merge, close).
5
+ */
6
+ export declare function useRecentCMSPullRequests(limit?: number): import("@tanstack/react-query").UseQueryResult<import("../../actions").RecentCMSPullRequest[], Error>;
7
+ //# sourceMappingURL=useRecentCMSPullRequests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRecentCMSPullRequests.d.ts","sourceRoot":"","sources":["../../../../admin/query/hooks/useRecentCMSPullRequests.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,SAAI,yGAMjD"}
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import "../../../chunk-B5LE2OEC.js";
3
+ import { getRecentCMSPullRequests } from "../../actions/git";
4
+ import { queryKeys } from "../keys";
5
+ import { useAdminQuery } from "../useAdminQuery";
6
+ function useRecentCMSPullRequests(limit = 5) {
7
+ return useAdminQuery({
8
+ queryKey: queryKeys.git.recentCMSPRs(limit),
9
+ queryFn: () => getRecentCMSPullRequests(limit),
10
+ tier: "realtime"
11
+ });
12
+ }
13
+ export {
14
+ useRecentCMSPullRequests
15
+ };
16
+ //# sourceMappingURL=useRecentCMSPullRequests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../admin/query/hooks/useRecentCMSPullRequests.ts"],"sourcesContent":["'use client';\n\nimport { getRecentCMSPullRequests } from '../../actions/git';\nimport { queryKeys } from '../keys';\nimport { useAdminQuery } from '../useAdminQuery';\n\n/**\n * Most-recently-updated PRs tagged `cms-update`, across all states. Powers the\n * dashboard \"Recent pull requests\" card. Tier `realtime` — PR state is\n * mutated outside the admin UI (review, merge, close).\n */\nexport function useRecentCMSPullRequests(limit = 5) {\n return useAdminQuery({\n queryKey: queryKeys.git.recentCMSPRs(limit),\n queryFn: () => getRecentCMSPullRequests(limit),\n tier: 'realtime',\n });\n}\n"],"mappings":";;AAEA,SAAS,gCAAgC;AACzC,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAOvB,SAAS,yBAAyB,QAAQ,GAAG;AAClD,SAAO,cAAc;AAAA,IACnB,UAAU,UAAU,IAAI,aAAa,KAAK;AAAA,IAC1C,SAAS,MAAM,yBAAyB,KAAK;AAAA,IAC7C,MAAM;AAAA,EACR,CAAC;AACH;","names":[]}
@@ -31,6 +31,7 @@ export declare const queryKeys: {
31
31
  readonly hasActive: () => readonly ["git", "hasActive"];
32
32
  readonly branches: () => readonly ["git", "branches"];
33
33
  readonly isProduction: () => readonly ["git", "isProduction"];
34
+ readonly recentCMSPRs: (limit?: number) => readonly ["git", "recentCMSPRs", number];
34
35
  };
35
36
  readonly agent: {
36
37
  readonly all: readonly ["agent"];
@@ -1 +1 @@
1
- {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../admin/query/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;qCAGE,MAAM;oCACP,MAAM;qCACL,MAAM;2CACA,MAAM;kCACf,MAAM;;;;;6BAKX,MAAM;;;;;sCAKG,MAAM;;;;;;;;;;;;;CAarB,CAAC"}
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../admin/query/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;qCAGE,MAAM;oCACP,MAAM;qCACL,MAAM;2CACA,MAAM;kCACf,MAAM;;;;;6BAKX,MAAM;;;;;sCAKG,MAAM;;;;;;;;wCAQJ,MAAM;;;;;;CAMvB,CAAC"}
@@ -23,7 +23,8 @@ const queryKeys = {
23
23
  branch: () => ["git", "branch"],
24
24
  hasActive: () => ["git", "hasActive"],
25
25
  branches: () => ["git", "branches"],
26
- isProduction: () => ["git", "isProduction"]
26
+ isProduction: () => ["git", "isProduction"],
27
+ recentCMSPRs: (limit) => ["git", "recentCMSPRs", limit != null ? limit : 5]
27
28
  },
28
29
  agent: {
29
30
  all: ["agent"],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../admin/query/keys.ts"],"sourcesContent":["/**\n * Centralized query-key registry.\n *\n * Keys are tuples; the first element is the **domain** so a coarse\n * `invalidateQueries({ queryKey: ['entries'] })` covers every entries-related\n * query at once. Add new keys here rather than ad-hoc inline tuples — the\n * `invalidateAfterMutation` helper in `./invalidate.ts` reads this object.\n */\nexport const queryKeys = {\n entries: {\n all: ['entries'] as const,\n list: (collection?: string) => ['entries', 'list', collection ?? '*'] as const,\n detail: (filePath: string) => ['entries', 'detail', filePath] as const,\n commits: (filePath: string) => ['entries', 'commits', filePath] as const,\n backlinks: (referenceKey: string) => ['entries', 'backlinks', referenceKey] as const,\n diff: (filePath: string) => ['entries', 'diff', filePath] as const,\n },\n media: {\n all: ['media'] as const,\n list: () => ['media', 'list'] as const,\n asset: (id: string) => ['media', 'asset', id] as const,\n },\n schema: {\n all: ['schema'] as const,\n current: () => ['schema', 'current'] as const,\n impact: (proposalId: string) => ['schema', 'impact', proposalId] as const,\n },\n git: {\n all: ['git'] as const,\n branch: () => ['git', 'branch'] as const,\n hasActive: () => ['git', 'hasActive'] as const,\n branches: () => ['git', 'branches'] as const,\n isProduction: () => ['git', 'isProduction'] as const,\n },\n agent: {\n all: ['agent'] as const,\n status: () => ['agent', 'status'] as const,\n },\n} as const;\n"],"mappings":";AAQO,MAAM,YAAY;AAAA,EACvB,SAAS;AAAA,IACP,KAAK,CAAC,SAAS;AAAA,IACf,MAAM,CAAC,eAAwB,CAAC,WAAW,QAAQ,kCAAc,GAAG;AAAA,IACpE,QAAQ,CAAC,aAAqB,CAAC,WAAW,UAAU,QAAQ;AAAA,IAC5D,SAAS,CAAC,aAAqB,CAAC,WAAW,WAAW,QAAQ;AAAA,IAC9D,WAAW,CAAC,iBAAyB,CAAC,WAAW,aAAa,YAAY;AAAA,IAC1E,MAAM,CAAC,aAAqB,CAAC,WAAW,QAAQ,QAAQ;AAAA,EAC1D;AAAA,EACA,OAAO;AAAA,IACL,KAAK,CAAC,OAAO;AAAA,IACb,MAAM,MAAM,CAAC,SAAS,MAAM;AAAA,IAC5B,OAAO,CAAC,OAAe,CAAC,SAAS,SAAS,EAAE;AAAA,EAC9C;AAAA,EACA,QAAQ;AAAA,IACN,KAAK,CAAC,QAAQ;AAAA,IACd,SAAS,MAAM,CAAC,UAAU,SAAS;AAAA,IACnC,QAAQ,CAAC,eAAuB,CAAC,UAAU,UAAU,UAAU;AAAA,EACjE;AAAA,EACA,KAAK;AAAA,IACH,KAAK,CAAC,KAAK;AAAA,IACX,QAAQ,MAAM,CAAC,OAAO,QAAQ;AAAA,IAC9B,WAAW,MAAM,CAAC,OAAO,WAAW;AAAA,IACpC,UAAU,MAAM,CAAC,OAAO,UAAU;AAAA,IAClC,cAAc,MAAM,CAAC,OAAO,cAAc;AAAA,EAC5C;AAAA,EACA,OAAO;AAAA,IACL,KAAK,CAAC,OAAO;AAAA,IACb,QAAQ,MAAM,CAAC,SAAS,QAAQ;AAAA,EAClC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../admin/query/keys.ts"],"sourcesContent":["/**\n * Centralized query-key registry.\n *\n * Keys are tuples; the first element is the **domain** so a coarse\n * `invalidateQueries({ queryKey: ['entries'] })` covers every entries-related\n * query at once. Add new keys here rather than ad-hoc inline tuples — the\n * `invalidateAfterMutation` helper in `./invalidate.ts` reads this object.\n */\nexport const queryKeys = {\n entries: {\n all: ['entries'] as const,\n list: (collection?: string) => ['entries', 'list', collection ?? '*'] as const,\n detail: (filePath: string) => ['entries', 'detail', filePath] as const,\n commits: (filePath: string) => ['entries', 'commits', filePath] as const,\n backlinks: (referenceKey: string) => ['entries', 'backlinks', referenceKey] as const,\n diff: (filePath: string) => ['entries', 'diff', filePath] as const,\n },\n media: {\n all: ['media'] as const,\n list: () => ['media', 'list'] as const,\n asset: (id: string) => ['media', 'asset', id] as const,\n },\n schema: {\n all: ['schema'] as const,\n current: () => ['schema', 'current'] as const,\n impact: (proposalId: string) => ['schema', 'impact', proposalId] as const,\n },\n git: {\n all: ['git'] as const,\n branch: () => ['git', 'branch'] as const,\n hasActive: () => ['git', 'hasActive'] as const,\n branches: () => ['git', 'branches'] as const,\n isProduction: () => ['git', 'isProduction'] as const,\n recentCMSPRs: (limit?: number) => ['git', 'recentCMSPRs', limit ?? 5] as const,\n },\n agent: {\n all: ['agent'] as const,\n status: () => ['agent', 'status'] as const,\n },\n} as const;\n"],"mappings":";AAQO,MAAM,YAAY;AAAA,EACvB,SAAS;AAAA,IACP,KAAK,CAAC,SAAS;AAAA,IACf,MAAM,CAAC,eAAwB,CAAC,WAAW,QAAQ,kCAAc,GAAG;AAAA,IACpE,QAAQ,CAAC,aAAqB,CAAC,WAAW,UAAU,QAAQ;AAAA,IAC5D,SAAS,CAAC,aAAqB,CAAC,WAAW,WAAW,QAAQ;AAAA,IAC9D,WAAW,CAAC,iBAAyB,CAAC,WAAW,aAAa,YAAY;AAAA,IAC1E,MAAM,CAAC,aAAqB,CAAC,WAAW,QAAQ,QAAQ;AAAA,EAC1D;AAAA,EACA,OAAO;AAAA,IACL,KAAK,CAAC,OAAO;AAAA,IACb,MAAM,MAAM,CAAC,SAAS,MAAM;AAAA,IAC5B,OAAO,CAAC,OAAe,CAAC,SAAS,SAAS,EAAE;AAAA,EAC9C;AAAA,EACA,QAAQ;AAAA,IACN,KAAK,CAAC,QAAQ;AAAA,IACd,SAAS,MAAM,CAAC,UAAU,SAAS;AAAA,IACnC,QAAQ,CAAC,eAAuB,CAAC,UAAU,UAAU,UAAU;AAAA,EACjE;AAAA,EACA,KAAK;AAAA,IACH,KAAK,CAAC,KAAK;AAAA,IACX,QAAQ,MAAM,CAAC,OAAO,QAAQ;AAAA,IAC9B,WAAW,MAAM,CAAC,OAAO,WAAW;AAAA,IACpC,UAAU,MAAM,CAAC,OAAO,UAAU;AAAA,IAClC,cAAc,MAAM,CAAC,OAAO,cAAc;AAAA,IAC1C,cAAc,CAAC,UAAmB,CAAC,OAAO,gBAAgB,wBAAS,CAAC;AAAA,EACtE;AAAA,EACA,OAAO;AAAA,IACL,KAAK,CAAC,OAAO;AAAA,IACb,QAAQ,MAAM,CAAC,SAAS,QAAQ;AAAA,EAClC;AACF;","names":[]}
@@ -836,6 +836,7 @@ __export(github_exports, {
836
836
  listGitHubCMSPullRequests: () => listGitHubCMSPullRequests,
837
837
  listGitHubFiles: () => listGitHubFiles,
838
838
  listGitHubFilesRecursive: () => listGitHubFilesRecursive,
839
+ listGitHubRecentCMSPullRequests: () => listGitHubRecentCMSPullRequests,
839
840
  markPRReadyForReview: () => markPRReadyForReview,
840
841
  mergePullRequest: () => mergePullRequest,
841
842
  readGitHubBinaryFilePublic: () => readGitHubBinaryFilePublic,
@@ -844,7 +845,7 @@ __export(github_exports, {
844
845
  saveGitHubBinaryFile: () => saveGitHubBinaryFile,
845
846
  saveGitHubFile: () => saveGitHubFile
846
847
  });
847
- var import_octokit2, import_next_auth, getErrorMessage, formatGitHubApiError, getDefaultBranchRef, ensureBranchExists, getOctokit, getWriteOctokit, readGitHubBinaryFilePublic, getGitHubFile, saveGitHubFile, saveGitHubBinaryFile, commitMultipleFilesToGitHub, deleteGitHubFile, createGitHubBranch, ensureLabelExists, createGitHubCMSPullRequest, listGitHubCMSPullRequests, createGitHubPullRequest, mergePullRequest, markPRReadyForReview;
848
+ var import_octokit2, import_next_auth, getErrorMessage, formatGitHubApiError, getDefaultBranchRef, ensureBranchExists, getOctokit, getWriteOctokit, readGitHubBinaryFilePublic, getGitHubFile, saveGitHubFile, saveGitHubBinaryFile, commitMultipleFilesToGitHub, deleteGitHubFile, createGitHubBranch, ensureLabelExists, createGitHubCMSPullRequest, listGitHubCMSPullRequests, listGitHubRecentCMSPullRequests, createGitHubPullRequest, mergePullRequest, markPRReadyForReview;
848
849
  var init_github = __esm({
849
850
  "admin/github.ts"() {
850
851
  "use strict";
@@ -1218,6 +1219,32 @@ var init_github = __esm({
1218
1219
  title: pr.title
1219
1220
  }));
1220
1221
  };
1222
+ listGitHubRecentCMSPullRequests = async (limit = 5) => {
1223
+ const octokit = await getWriteOctokit();
1224
+ const { owner, repo } = assertGitHubConfig();
1225
+ const { data: prs } = await octokit.rest.pulls.list({
1226
+ owner,
1227
+ repo,
1228
+ state: "all",
1229
+ sort: "updated",
1230
+ direction: "desc",
1231
+ per_page: 50
1232
+ });
1233
+ return prs.filter((pr) => pr.labels.some((l) => l.name === "cms-update")).slice(0, Math.max(1, limit)).map((pr) => {
1234
+ var _a, _b, _c, _d;
1235
+ const state2 = pr.merged_at ? "merged" : pr.state === "open" ? "open" : "closed";
1236
+ return {
1237
+ branch: pr.head.ref,
1238
+ prUrl: pr.html_url,
1239
+ prNumber: pr.number,
1240
+ title: pr.title,
1241
+ state: state2,
1242
+ updatedAt: pr.updated_at,
1243
+ authorLogin: (_b = (_a = pr.user) == null ? void 0 : _a.login) != null ? _b : null,
1244
+ authorAvatarUrl: (_d = (_c = pr.user) == null ? void 0 : _c.avatar_url) != null ? _d : null
1245
+ };
1246
+ });
1247
+ };
1221
1248
  createGitHubPullRequest = async (title, body, targetBranch = "main") => {
1222
1249
  const octokit = await getWriteOctokit();
1223
1250
  const { owner, repo, branch } = assertGitHubConfig();