minutework 0.1.0

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 (203) hide show
  1. package/EXTERNAL_ALPHA.md +74 -0
  2. package/README.md +57 -0
  3. package/assets/claude-local/CLAUDE.md.template +45 -0
  4. package/assets/claude-local/bundle.json +22 -0
  5. package/assets/claude-local/skills/README.md +6 -0
  6. package/assets/claude-local/skills/app-pack-authoring.md +8 -0
  7. package/assets/claude-local/skills/event-bus.md +8 -0
  8. package/assets/claude-local/skills/ontology-mapping.md +8 -0
  9. package/assets/claude-local/skills/openclaw-skill-importer.md +7 -0
  10. package/assets/claude-local/skills/schema-engine.md +8 -0
  11. package/assets/claude-local/skills/secrets-runtime-bridge.md +9 -0
  12. package/assets/claude-local/skills/sidecar-generation.md +9 -0
  13. package/assets/templates/fastapi-sidecar/.env.example +8 -0
  14. package/assets/templates/fastapi-sidecar/README.md +77 -0
  15. package/assets/templates/fastapi-sidecar/poetry.lock +757 -0
  16. package/assets/templates/fastapi-sidecar/pyproject.toml +42 -0
  17. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/__init__.py +3 -0
  18. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/auth.py +70 -0
  19. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/__init__.py +3 -0
  20. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/bridge/client.py +71 -0
  21. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/logging_utils.py +25 -0
  22. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/main.py +85 -0
  23. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/receipts.py +24 -0
  24. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/settings.py +41 -0
  25. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/template_validation.py +26 -0
  26. package/assets/templates/fastapi-sidecar/src/fastapi_sidecar/worker.py +33 -0
  27. package/assets/templates/fastapi-sidecar/template.json +43 -0
  28. package/assets/templates/fastapi-sidecar/template.schema.json +160 -0
  29. package/assets/templates/fastapi-sidecar/tests/conftest.py +36 -0
  30. package/assets/templates/fastapi-sidecar/tests/test_app.py +39 -0
  31. package/assets/templates/fastapi-sidecar/tests/test_auth.py +32 -0
  32. package/assets/templates/fastapi-sidecar/tests/test_bridge_client.py +31 -0
  33. package/assets/templates/fastapi-sidecar/tests/test_materialization.py +55 -0
  34. package/assets/templates/fastapi-sidecar/tests/test_template_contract.py +49 -0
  35. package/assets/templates/fastapi-sidecar/tests/test_worker.py +7 -0
  36. package/assets/templates/fastapi-sidecar/tools/template/validate_template.py +20 -0
  37. package/assets/templates/next-tenant-app/.env.example +8 -0
  38. package/assets/templates/next-tenant-app/.storybook/main.ts +19 -0
  39. package/assets/templates/next-tenant-app/.storybook/preview.tsx +38 -0
  40. package/assets/templates/next-tenant-app/README.md +115 -0
  41. package/assets/templates/next-tenant-app/components.json +21 -0
  42. package/assets/templates/next-tenant-app/eslint.config.mjs +41 -0
  43. package/assets/templates/next-tenant-app/next-env.d.ts +6 -0
  44. package/assets/templates/next-tenant-app/next.config.ts +8 -0
  45. package/assets/templates/next-tenant-app/package-lock.json +9682 -0
  46. package/assets/templates/next-tenant-app/package.json +59 -0
  47. package/assets/templates/next-tenant-app/pnpm-lock.yaml +6062 -0
  48. package/assets/templates/next-tenant-app/postcss.config.mjs +8 -0
  49. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +90 -0
  50. package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +78 -0
  51. package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +31 -0
  52. package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +16 -0
  53. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +79 -0
  54. package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +40 -0
  55. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +42 -0
  56. package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +29 -0
  57. package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +26 -0
  58. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +40 -0
  59. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +47 -0
  60. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +43 -0
  61. package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +45 -0
  62. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +83 -0
  63. package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +30 -0
  64. package/assets/templates/next-tenant-app/src/app/app/layout.tsx +20 -0
  65. package/assets/templates/next-tenant-app/src/app/app/page.test.ts +62 -0
  66. package/assets/templates/next-tenant-app/src/app/app/page.tsx +24 -0
  67. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +70 -0
  68. package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +57 -0
  69. package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +42 -0
  70. package/assets/templates/next-tenant-app/src/app/blog/page.tsx +37 -0
  71. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +70 -0
  72. package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +55 -0
  73. package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +42 -0
  74. package/assets/templates/next-tenant-app/src/app/docs/page.tsx +37 -0
  75. package/assets/templates/next-tenant-app/src/app/globals.css +70 -0
  76. package/assets/templates/next-tenant-app/src/app/layout.tsx +69 -0
  77. package/assets/templates/next-tenant-app/src/app/login/page.test.ts +55 -0
  78. package/assets/templates/next-tenant-app/src/app/login/page.tsx +33 -0
  79. package/assets/templates/next-tenant-app/src/app/page.test.ts +56 -0
  80. package/assets/templates/next-tenant-app/src/app/page.tsx +35 -0
  81. package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +55 -0
  82. package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +35 -0
  83. package/assets/templates/next-tenant-app/src/app/providers.tsx +25 -0
  84. package/assets/templates/next-tenant-app/src/app/robots.test.ts +20 -0
  85. package/assets/templates/next-tenant-app/src/app/robots.ts +18 -0
  86. package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +49 -0
  87. package/assets/templates/next-tenant-app/src/app/sitemap.ts +54 -0
  88. package/assets/templates/next-tenant-app/src/components/ui/button.tsx +59 -0
  89. package/assets/templates/next-tenant-app/src/components/ui/input.tsx +21 -0
  90. package/assets/templates/next-tenant-app/src/design-system/docs/governance.mdx +26 -0
  91. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.stories.tsx +48 -0
  92. package/assets/templates/next-tenant-app/src/design-system/patterns/panel-frame.tsx +26 -0
  93. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.stories.tsx +26 -0
  94. package/assets/templates/next-tenant-app/src/design-system/patterns/status-badge.tsx +35 -0
  95. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.stories.tsx +21 -0
  96. package/assets/templates/next-tenant-app/src/design-system/patterns/theme-mode-toggle.tsx +75 -0
  97. package/assets/templates/next-tenant-app/src/design-system/primitives/button.stories.tsx +37 -0
  98. package/assets/templates/next-tenant-app/src/design-system/primitives/button.ts +1 -0
  99. package/assets/templates/next-tenant-app/src/design-system/primitives/input.stories.tsx +26 -0
  100. package/assets/templates/next-tenant-app/src/design-system/primitives/input.ts +1 -0
  101. package/assets/templates/next-tenant-app/src/design-system/recipes/chrome.ts +28 -0
  102. package/assets/templates/next-tenant-app/src/design-system/tokens/foundation.css +31 -0
  103. package/assets/templates/next-tenant-app/src/design-system/tokens/index.css +3 -0
  104. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.json +85 -0
  105. package/assets/templates/next-tenant-app/src/design-system/tokens/manifest.ts +87 -0
  106. package/assets/templates/next-tenant-app/src/design-system/tokens/semantic.css +105 -0
  107. package/assets/templates/next-tenant-app/src/design-system/tokens/theme.css +59 -0
  108. package/assets/templates/next-tenant-app/src/design-system/tokens/tokens.stories.tsx +71 -0
  109. package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +198 -0
  110. package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +153 -0
  111. package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +342 -0
  112. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +66 -0
  113. package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +108 -0
  114. package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +111 -0
  115. package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +111 -0
  116. package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +624 -0
  117. package/assets/templates/next-tenant-app/src/lib/app-routes.test.ts +20 -0
  118. package/assets/templates/next-tenant-app/src/lib/app-routes.ts +59 -0
  119. package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +189 -0
  120. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +318 -0
  121. package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +232 -0
  122. package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +339 -0
  123. package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +5 -0
  124. package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +96 -0
  125. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +75 -0
  126. package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +25 -0
  127. package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +170 -0
  128. package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +661 -0
  129. package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +131 -0
  130. package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +34 -0
  131. package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +102 -0
  132. package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +87 -0
  133. package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +33 -0
  134. package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +108 -0
  135. package/assets/templates/next-tenant-app/src/lib/public-site.test.ts +20 -0
  136. package/assets/templates/next-tenant-app/src/lib/public-site.ts +49 -0
  137. package/assets/templates/next-tenant-app/src/lib/theme-config.ts +10 -0
  138. package/assets/templates/next-tenant-app/src/lib/theme.tsx +159 -0
  139. package/assets/templates/next-tenant-app/src/lib/utils.ts +6 -0
  140. package/assets/templates/next-tenant-app/template.json +27 -0
  141. package/assets/templates/next-tenant-app/template.schema.json +160 -0
  142. package/assets/templates/next-tenant-app/test/server-only-stub.ts +1 -0
  143. package/assets/templates/next-tenant-app/tools/design-system/build-token-manifest.mjs +3 -0
  144. package/assets/templates/next-tenant-app/tools/design-system/check-imports.mjs +9 -0
  145. package/assets/templates/next-tenant-app/tools/design-system/check-stories.mjs +9 -0
  146. package/assets/templates/next-tenant-app/tools/design-system/check-values.mjs +9 -0
  147. package/assets/templates/next-tenant-app/tools/design-system/checks.mjs +238 -0
  148. package/assets/templates/next-tenant-app/tools/design-system/eslint-plugin-design-system.mjs +184 -0
  149. package/assets/templates/next-tenant-app/tools/design-system/playwright.config.mjs +34 -0
  150. package/assets/templates/next-tenant-app/tools/design-system/run-checks.mjs +22 -0
  151. package/assets/templates/next-tenant-app/tools/design-system/shared.mjs +166 -0
  152. package/assets/templates/next-tenant-app/tools/design-system/visual.spec.ts +41 -0
  153. package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +39 -0
  154. package/assets/templates/next-tenant-app/tools/template/validate-template.mjs +45 -0
  155. package/assets/templates/next-tenant-app/tsconfig.json +42 -0
  156. package/assets/templates/next-tenant-app/vitest.config.ts +25 -0
  157. package/bin/minutework.js +40 -0
  158. package/dist/auth.d.ts +59 -0
  159. package/dist/auth.js +338 -0
  160. package/dist/auth.js.map +1 -0
  161. package/dist/browser.d.ts +1 -0
  162. package/dist/browser.js +26 -0
  163. package/dist/browser.js.map +1 -0
  164. package/dist/cli.d.ts +2 -0
  165. package/dist/cli.js +5 -0
  166. package/dist/cli.js.map +1 -0
  167. package/dist/compile.d.ts +20 -0
  168. package/dist/compile.js +121 -0
  169. package/dist/compile.js.map +1 -0
  170. package/dist/config.d.ts +25 -0
  171. package/dist/config.js +102 -0
  172. package/dist/config.js.map +1 -0
  173. package/dist/deploy-state.d.ts +35 -0
  174. package/dist/deploy-state.js +30 -0
  175. package/dist/deploy-state.js.map +1 -0
  176. package/dist/deploy.d.ts +22 -0
  177. package/dist/deploy.js +308 -0
  178. package/dist/deploy.js.map +1 -0
  179. package/dist/developer-client.d.ts +88 -0
  180. package/dist/developer-client.js +78 -0
  181. package/dist/developer-client.js.map +1 -0
  182. package/dist/index.d.ts +27 -0
  183. package/dist/index.js +290 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/init.d.ts +22 -0
  186. package/dist/init.js +421 -0
  187. package/dist/init.js.map +1 -0
  188. package/dist/launcher.d.ts +1 -0
  189. package/dist/launcher.js +50 -0
  190. package/dist/launcher.js.map +1 -0
  191. package/dist/paths.d.ts +12 -0
  192. package/dist/paths.js +33 -0
  193. package/dist/paths.js.map +1 -0
  194. package/dist/sandbox.d.ts +30 -0
  195. package/dist/sandbox.js +852 -0
  196. package/dist/sandbox.js.map +1 -0
  197. package/dist/state.d.ts +46 -0
  198. package/dist/state.js +82 -0
  199. package/dist/state.js.map +1 -0
  200. package/dist/tokens.d.ts +14 -0
  201. package/dist/tokens.js +293 -0
  202. package/dist/tokens.js.map +1 -0
  203. package/package.json +43 -0
@@ -0,0 +1,83 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const notFound = vi.fn(() => {
4
+ throw new Error("notFound");
5
+ });
6
+ const resolveAuthenticatedSession = vi.fn();
7
+
8
+ vi.mock("next/navigation", () => ({
9
+ notFound,
10
+ }));
11
+
12
+ vi.mock("@/features/shell/components/private-app-shell", () => ({
13
+ PrivateAppShell: () => null,
14
+ }));
15
+
16
+ vi.mock("@/lib/platform/auth.server", () => ({
17
+ resolveAuthenticatedSession,
18
+ }));
19
+
20
+ vi.mock("@/lib/platform/endpoints.server", () => ({
21
+ platformAuthEndpoints: {
22
+ operatorConsole: "http://127.0.0.1:8000/ops/login/",
23
+ },
24
+ }));
25
+
26
+ vi.mock("@/lib/platform/env.server", () => ({
27
+ env: {
28
+ MW_TEMPLATE_APP_NAME: "MinuteWork Combined Starter",
29
+ MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: false,
30
+ },
31
+ }));
32
+
33
+ describe("runtime commands example page", () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ vi.resetModules();
37
+ });
38
+
39
+ it("returns notFound before loading auth when the example is disabled", async () => {
40
+ const page = await import("./page");
41
+
42
+ await expect(page.default()).rejects.toThrow("notFound");
43
+ expect(notFound).toHaveBeenCalledTimes(1);
44
+ expect(resolveAuthenticatedSession).not.toHaveBeenCalled();
45
+ });
46
+
47
+ it("loads the private shell when the example is enabled", async () => {
48
+ vi.doMock("@/lib/platform/env.server", () => ({
49
+ env: {
50
+ MW_TEMPLATE_APP_NAME: "MinuteWork Combined Starter",
51
+ MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: true,
52
+ },
53
+ }));
54
+ resolveAuthenticatedSession.mockResolvedValue({
55
+ authenticated: true,
56
+ user: {
57
+ id: "user-1",
58
+ username: "demo-user",
59
+ email: "demo@example.com",
60
+ },
61
+ active_tenant_id: "tenant-1",
62
+ active_tenant: {
63
+ tenant_id: "tenant-1",
64
+ tenant_slug: "alpha",
65
+ tenant_name: "Alpha",
66
+ role: "admin",
67
+ },
68
+ memberships: [
69
+ {
70
+ tenant_id: "tenant-1",
71
+ tenant_slug: "alpha",
72
+ tenant_name: "Alpha",
73
+ role: "admin",
74
+ },
75
+ ],
76
+ });
77
+
78
+ const page = await import("./page");
79
+
80
+ await expect(page.default()).resolves.toBeDefined();
81
+ expect(resolveAuthenticatedSession).toHaveBeenCalledTimes(1);
82
+ });
83
+ });
@@ -0,0 +1,30 @@
1
+ import { notFound } from "next/navigation";
2
+
3
+ import { PrivateAppShell } from "@/features/shell/components/private-app-shell";
4
+ import { resolveAuthenticatedSession } from "@/lib/platform/auth.server";
5
+ import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
6
+ import { env } from "@/lib/platform/env.server";
7
+
8
+ export const metadata = {
9
+ title: "Runtime Commands Example",
10
+ description:
11
+ "Optional runtime command example inside the authenticated /app surface.",
12
+ };
13
+
14
+ export default async function RuntimeCommandsExamplePage() {
15
+ if (!env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE) {
16
+ notFound();
17
+ }
18
+
19
+ const session = await resolveAuthenticatedSession();
20
+
21
+ return (
22
+ <PrivateAppShell
23
+ initialSession={session}
24
+ appName={env.MW_TEMPLATE_APP_NAME}
25
+ operatorConsoleHref={platformAuthEndpoints.operatorConsole}
26
+ runtimeCommandExampleEnabled={env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE}
27
+ view="runtime-command-demo"
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,20 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { resolveAuthenticatedSession } from "@/lib/platform/auth.server";
5
+
6
+ export const metadata: Metadata = {
7
+ robots: {
8
+ index: false,
9
+ follow: false,
10
+ },
11
+ };
12
+
13
+ export default async function AuthenticatedAppLayout({
14
+ children,
15
+ }: {
16
+ children: ReactNode;
17
+ }) {
18
+ await resolveAuthenticatedSession();
19
+ return children;
20
+ }
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const resolveAuthenticatedSession = vi.fn();
4
+
5
+ vi.mock("@/features/shell/components/private-app-shell", () => ({
6
+ PrivateAppShell: () => null,
7
+ }));
8
+
9
+ vi.mock("@/lib/platform/auth.server", () => ({
10
+ resolveAuthenticatedSession,
11
+ }));
12
+
13
+ vi.mock("@/lib/platform/endpoints.server", () => ({
14
+ platformAuthEndpoints: {
15
+ operatorConsole: "http://127.0.0.1:8000/ops/login/",
16
+ },
17
+ }));
18
+
19
+ vi.mock("@/lib/platform/env.server", () => ({
20
+ env: {
21
+ MW_TEMPLATE_APP_NAME: "MinuteWork Combined Starter",
22
+ MW_ENABLE_RUNTIME_COMMAND_EXAMPLE: false,
23
+ },
24
+ }));
25
+
26
+ describe("authenticated app home page", () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ vi.resetModules();
30
+ });
31
+
32
+ it("renders the private shell after the auth guard resolves", async () => {
33
+ resolveAuthenticatedSession.mockResolvedValue({
34
+ authenticated: true,
35
+ user: {
36
+ id: "user-1",
37
+ username: "demo-user",
38
+ email: "demo@example.com",
39
+ },
40
+ active_tenant_id: "tenant-1",
41
+ active_tenant: {
42
+ tenant_id: "tenant-1",
43
+ tenant_slug: "alpha",
44
+ tenant_name: "Alpha",
45
+ role: "admin",
46
+ },
47
+ memberships: [
48
+ {
49
+ tenant_id: "tenant-1",
50
+ tenant_slug: "alpha",
51
+ tenant_name: "Alpha",
52
+ role: "admin",
53
+ },
54
+ ],
55
+ });
56
+
57
+ const page = await import("./page");
58
+
59
+ await expect(page.default()).resolves.toBeDefined();
60
+ expect(resolveAuthenticatedSession).toHaveBeenCalledTimes(1);
61
+ });
62
+ });
@@ -0,0 +1,24 @@
1
+ import { PrivateAppShell } from "@/features/shell/components/private-app-shell";
2
+ import { resolveAuthenticatedSession } from "@/lib/platform/auth.server";
3
+ import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
4
+ import { env } from "@/lib/platform/env.server";
5
+
6
+ export const metadata = {
7
+ title: "Workspace",
8
+ description:
9
+ "Authenticated workspace surface for the combined MinuteWork starter.",
10
+ };
11
+
12
+ export default async function AppHomePage() {
13
+ const session = await resolveAuthenticatedSession();
14
+
15
+ return (
16
+ <PrivateAppShell
17
+ initialSession={session}
18
+ appName={env.MW_TEMPLATE_APP_NAME}
19
+ operatorConsoleHref={platformAuthEndpoints.operatorConsole}
20
+ runtimeCommandExampleEnabled={env.MW_ENABLE_RUNTIME_COMMAND_EXAMPLE}
21
+ view="dashboard"
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,70 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const notFound = vi.fn(() => {
4
+ throw new Error("notFound");
5
+ });
6
+ const getEntry = vi.fn();
7
+ const getSiteConfig = vi.fn();
8
+ const listEntries = vi.fn();
9
+
10
+ vi.mock("next/navigation", () => ({
11
+ notFound,
12
+ }));
13
+
14
+ vi.mock("@/features/public-shell/components/content-article", () => ({
15
+ ContentArticle: () => null,
16
+ }));
17
+
18
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
19
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
20
+ }));
21
+
22
+ vi.mock("@/lib/content/adapter.server", () => ({
23
+ getEntry,
24
+ getSiteConfig,
25
+ listEntries,
26
+ }));
27
+
28
+ describe("blog article page", () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ vi.resetModules();
32
+ });
33
+
34
+ it("returns notFound for unknown blog slugs", async () => {
35
+ getSiteConfig.mockResolvedValue({
36
+ collections: {
37
+ blog: {
38
+ title: "Starter Blog",
39
+ description: "Blog",
40
+ },
41
+ },
42
+ siteName: "MinuteWork Combined Starter",
43
+ });
44
+ getEntry.mockResolvedValue(null);
45
+
46
+ const page = await import("./page");
47
+
48
+ await expect(
49
+ page.default({
50
+ params: Promise.resolve({ slug: "missing-entry" }),
51
+ }),
52
+ ).rejects.toThrow("notFound");
53
+ expect(notFound).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it("derives static params from published blog entries", async () => {
57
+ listEntries.mockResolvedValue([
58
+ { slug: ["public-site-api-default"] },
59
+ { slug: ["launch-note"] },
60
+ ]);
61
+
62
+ const page = await import("./page");
63
+
64
+ await expect(page.generateStaticParams()).resolves.toEqual([
65
+ { slug: "public-site-api-default" },
66
+ { slug: "launch-note" },
67
+ ]);
68
+ expect(page.dynamicParams).toBe(false);
69
+ });
70
+ });
@@ -0,0 +1,57 @@
1
+ import { notFound } from "next/navigation";
2
+
3
+ import { ContentArticle } from "@/features/public-shell/components/content-article";
4
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
+ import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
6
+ import { appRoutes } from "@/lib/app-routes";
7
+ import { buildPublicMetadata } from "@/lib/public-site";
8
+
9
+ type BlogArticlePageProps = {
10
+ params: Promise<{ slug: string }>;
11
+ };
12
+
13
+ export const dynamicParams = false;
14
+
15
+ export async function generateStaticParams() {
16
+ return (await listEntries("blog")).map((entry) => ({
17
+ slug: entry.slug[0],
18
+ }));
19
+ }
20
+
21
+ export async function generateMetadata({ params }: BlogArticlePageProps) {
22
+ const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
23
+ const entry = await getEntry("blog", [slug]);
24
+
25
+ if (!entry) {
26
+ return buildPublicMetadata({
27
+ title: siteConfig.collections.blog.title,
28
+ description: siteConfig.collections.blog.description,
29
+ path: appRoutes.blogIndex,
30
+ siteName: siteConfig.siteName,
31
+ });
32
+ }
33
+
34
+ return buildPublicMetadata({
35
+ title: entry.seo.title,
36
+ description: entry.seo.description,
37
+ path: appRoutes.blogPost(entry.slug),
38
+ siteName: siteConfig.siteName,
39
+ type: "article",
40
+ publishedTime: entry.publishedAt,
41
+ });
42
+ }
43
+
44
+ export default async function BlogArticlePage({ params }: BlogArticlePageProps) {
45
+ const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
46
+ const entry = await getEntry("blog", [slug]);
47
+
48
+ if (!entry) {
49
+ notFound();
50
+ }
51
+
52
+ return (
53
+ <PublicSiteShell activeHref={appRoutes.blogIndex} siteConfig={siteConfig}>
54
+ <ContentArticle entry={entry} />
55
+ </PublicSiteShell>
56
+ );
57
+ }
@@ -0,0 +1,42 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const getSiteConfig = vi.fn();
4
+ const listEntries = vi.fn();
5
+
6
+ vi.mock("@/features/public-shell/components/content-collection", () => ({
7
+ ContentCollection: () => null,
8
+ }));
9
+
10
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
11
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
12
+ }));
13
+
14
+ vi.mock("@/lib/content/adapter.server", () => ({
15
+ getSiteConfig,
16
+ listEntries,
17
+ }));
18
+
19
+ describe("blog index page", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ vi.resetModules();
23
+ });
24
+
25
+ it("renders the public blog collection", async () => {
26
+ getSiteConfig.mockResolvedValue({
27
+ collections: {
28
+ blog: {
29
+ eyebrow: "Blog",
30
+ title: "Starter Blog",
31
+ description: "Combined starter notes.",
32
+ },
33
+ },
34
+ });
35
+ listEntries.mockResolvedValue([]);
36
+
37
+ const page = await import("./page");
38
+
39
+ await expect(page.default()).resolves.toBeDefined();
40
+ expect(listEntries).toHaveBeenCalledWith("blog");
41
+ });
42
+ });
@@ -0,0 +1,37 @@
1
+ import { ContentCollection } from "@/features/public-shell/components/content-collection";
2
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
3
+ import { getSiteConfig, listEntries } from "@/lib/content/adapter.server";
4
+ import { appRoutes } from "@/lib/app-routes";
5
+ import { buildPublicMetadata } from "@/lib/public-site";
6
+
7
+ export async function generateMetadata() {
8
+ const siteConfig = await getSiteConfig();
9
+ const collection = siteConfig.collections.blog;
10
+
11
+ return buildPublicMetadata({
12
+ title: collection.title,
13
+ description: collection.description,
14
+ path: appRoutes.blogIndex,
15
+ siteName: siteConfig.siteName,
16
+ });
17
+ }
18
+
19
+ export default async function BlogIndexPage() {
20
+ const [siteConfig, entries] = await Promise.all([
21
+ getSiteConfig(),
22
+ listEntries("blog"),
23
+ ]);
24
+ const collection = siteConfig.collections.blog;
25
+
26
+ return (
27
+ <PublicSiteShell activeHref={appRoutes.blogIndex} siteConfig={siteConfig}>
28
+ <ContentCollection
29
+ description={collection.description}
30
+ entries={entries}
31
+ eyebrow={collection.eyebrow}
32
+ kind="blog"
33
+ title={collection.title}
34
+ />
35
+ </PublicSiteShell>
36
+ );
37
+ }
@@ -0,0 +1,70 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const notFound = vi.fn(() => {
4
+ throw new Error("notFound");
5
+ });
6
+ const getEntry = vi.fn();
7
+ const getSiteConfig = vi.fn();
8
+ const listEntries = vi.fn();
9
+
10
+ vi.mock("next/navigation", () => ({
11
+ notFound,
12
+ }));
13
+
14
+ vi.mock("@/features/public-shell/components/content-article", () => ({
15
+ ContentArticle: () => null,
16
+ }));
17
+
18
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
19
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
20
+ }));
21
+
22
+ vi.mock("@/lib/content/adapter.server", () => ({
23
+ getEntry,
24
+ getSiteConfig,
25
+ listEntries,
26
+ }));
27
+
28
+ describe("docs article page", () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ vi.resetModules();
32
+ });
33
+
34
+ it("returns notFound for unknown docs slugs", async () => {
35
+ getSiteConfig.mockResolvedValue({
36
+ collections: {
37
+ docs: {
38
+ title: "Starter Docs",
39
+ description: "Docs",
40
+ },
41
+ },
42
+ siteName: "MinuteWork Combined Starter",
43
+ });
44
+ getEntry.mockResolvedValue(null);
45
+
46
+ const page = await import("./page");
47
+
48
+ await expect(
49
+ page.default({
50
+ params: Promise.resolve({ slug: ["missing-entry"] }),
51
+ }),
52
+ ).rejects.toThrow("notFound");
53
+ expect(notFound).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it("derives static params from published docs entries", async () => {
57
+ listEntries.mockResolvedValue([
58
+ { slug: ["guides", "public-site-content"] },
59
+ { slug: ["reference", "api"] },
60
+ ]);
61
+
62
+ const page = await import("./page");
63
+
64
+ await expect(page.generateStaticParams()).resolves.toEqual([
65
+ { slug: ["guides", "public-site-content"] },
66
+ { slug: ["reference", "api"] },
67
+ ]);
68
+ expect(page.dynamicParams).toBe(false);
69
+ });
70
+ });
@@ -0,0 +1,55 @@
1
+ import { notFound } from "next/navigation";
2
+
3
+ import { ContentArticle } from "@/features/public-shell/components/content-article";
4
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
5
+ import { getEntry, getSiteConfig, listEntries } from "@/lib/content/adapter.server";
6
+ import { appRoutes } from "@/lib/app-routes";
7
+ import { buildPublicMetadata } from "@/lib/public-site";
8
+
9
+ type DocsArticlePageProps = {
10
+ params: Promise<{ slug: string[] }>;
11
+ };
12
+
13
+ export const dynamicParams = false;
14
+
15
+ export async function generateStaticParams() {
16
+ return (await listEntries("docs")).map((entry) => ({
17
+ slug: entry.slug,
18
+ }));
19
+ }
20
+
21
+ export async function generateMetadata({ params }: DocsArticlePageProps) {
22
+ const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
23
+ const entry = await getEntry("docs", slug);
24
+
25
+ if (!entry) {
26
+ return buildPublicMetadata({
27
+ title: siteConfig.collections.docs.title,
28
+ description: siteConfig.collections.docs.description,
29
+ path: appRoutes.docsIndex,
30
+ siteName: siteConfig.siteName,
31
+ });
32
+ }
33
+
34
+ return buildPublicMetadata({
35
+ title: entry.seo.title,
36
+ description: entry.seo.description,
37
+ path: appRoutes.docsPage(entry.slug),
38
+ siteName: siteConfig.siteName,
39
+ });
40
+ }
41
+
42
+ export default async function DocsArticlePage({ params }: DocsArticlePageProps) {
43
+ const [{ slug }, siteConfig] = await Promise.all([params, getSiteConfig()]);
44
+ const entry = await getEntry("docs", slug);
45
+
46
+ if (!entry) {
47
+ notFound();
48
+ }
49
+
50
+ return (
51
+ <PublicSiteShell activeHref={appRoutes.docsIndex} siteConfig={siteConfig}>
52
+ <ContentArticle entry={entry} />
53
+ </PublicSiteShell>
54
+ );
55
+ }
@@ -0,0 +1,42 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const getSiteConfig = vi.fn();
4
+ const listEntries = vi.fn();
5
+
6
+ vi.mock("@/features/public-shell/components/content-collection", () => ({
7
+ ContentCollection: () => null,
8
+ }));
9
+
10
+ vi.mock("@/features/public-shell/components/public-site-shell", () => ({
11
+ PublicSiteShell: ({ children }: { children: unknown }) => children,
12
+ }));
13
+
14
+ vi.mock("@/lib/content/adapter.server", () => ({
15
+ getSiteConfig,
16
+ listEntries,
17
+ }));
18
+
19
+ describe("docs index page", () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ vi.resetModules();
23
+ });
24
+
25
+ it("renders the public docs collection", async () => {
26
+ getSiteConfig.mockResolvedValue({
27
+ collections: {
28
+ docs: {
29
+ eyebrow: "Docs",
30
+ title: "Starter Docs",
31
+ description: "Route content through the adapter seam.",
32
+ },
33
+ },
34
+ });
35
+ listEntries.mockResolvedValue([]);
36
+
37
+ const page = await import("./page");
38
+
39
+ await expect(page.default()).resolves.toBeDefined();
40
+ expect(listEntries).toHaveBeenCalledWith("docs");
41
+ });
42
+ });
@@ -0,0 +1,37 @@
1
+ import { ContentCollection } from "@/features/public-shell/components/content-collection";
2
+ import { PublicSiteShell } from "@/features/public-shell/components/public-site-shell";
3
+ import { getSiteConfig, listEntries } from "@/lib/content/adapter.server";
4
+ import { appRoutes } from "@/lib/app-routes";
5
+ import { buildPublicMetadata } from "@/lib/public-site";
6
+
7
+ export async function generateMetadata() {
8
+ const siteConfig = await getSiteConfig();
9
+ const collection = siteConfig.collections.docs;
10
+
11
+ return buildPublicMetadata({
12
+ title: collection.title,
13
+ description: collection.description,
14
+ path: appRoutes.docsIndex,
15
+ siteName: siteConfig.siteName,
16
+ });
17
+ }
18
+
19
+ export default async function DocsIndexPage() {
20
+ const [siteConfig, entries] = await Promise.all([
21
+ getSiteConfig(),
22
+ listEntries("docs"),
23
+ ]);
24
+ const collection = siteConfig.collections.docs;
25
+
26
+ return (
27
+ <PublicSiteShell activeHref={appRoutes.docsIndex} siteConfig={siteConfig}>
28
+ <ContentCollection
29
+ description={collection.description}
30
+ entries={entries}
31
+ eyebrow={collection.eyebrow}
32
+ kind="docs"
33
+ title={collection.title}
34
+ />
35
+ </PublicSiteShell>
36
+ );
37
+ }