create-stitchkit 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 (174) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/dist/cli.js +230 -0
  5. package/package.json +50 -0
  6. package/template/README.md +69 -0
  7. package/template/_env +11 -0
  8. package/template/_env.example +12 -0
  9. package/template/_gitignore +10 -0
  10. package/template/biome.json +43 -0
  11. package/template/bun.lock +1372 -0
  12. package/template/e2e/starter.spec.ts +237 -0
  13. package/template/ecosystem.config.cjs +28 -0
  14. package/template/ecosystem.dev.config.cjs +29 -0
  15. package/template/package.json +55 -0
  16. package/template/packages/backend/package.json +31 -0
  17. package/template/packages/backend/src/cli.ts +12 -0
  18. package/template/packages/backend/src/domain/errors.ts +9 -0
  19. package/template/packages/backend/src/domain/repository/github-cache.test.ts +72 -0
  20. package/template/packages/backend/src/domain/repository/github-cache.ts +205 -0
  21. package/template/packages/backend/src/index.ts +56 -0
  22. package/template/packages/backend/src/lib/db.ts +7 -0
  23. package/template/packages/backend/src/surface.ts +13 -0
  24. package/template/packages/backend/src/tools.ts +21 -0
  25. package/template/packages/backend/src/transport/errors.ts +28 -0
  26. package/template/packages/backend/src/transport/repository-service.ts +18 -0
  27. package/template/packages/backend/tsconfig.json +8 -0
  28. package/template/packages/config/package.json +22 -0
  29. package/template/packages/config/src/server.ts +24 -0
  30. package/template/packages/config/tsconfig.json +5 -0
  31. package/template/packages/db/migrations/20260808000000_init/migration.sql +16 -0
  32. package/template/packages/db/migrations/20260808170000_repository_visibility/migration.sql +4 -0
  33. package/template/packages/db/migrations/migration_lock.toml +1 -0
  34. package/template/packages/db/package.json +29 -0
  35. package/template/packages/db/prisma.config.ts +8 -0
  36. package/template/packages/db/schema.prisma +31 -0
  37. package/template/packages/db/tsconfig.json +5 -0
  38. package/template/packages/frontend/messages/en.json +15 -0
  39. package/template/packages/frontend/messages/ru.json +15 -0
  40. package/template/packages/frontend/next.config.ts +18 -0
  41. package/template/packages/frontend/package.json +62 -0
  42. package/template/packages/frontend/postcss.config.mjs +1 -0
  43. package/template/packages/frontend/public/ASSETS.md +6 -0
  44. package/template/packages/frontend/public/favicon/mascot-stitch.png +0 -0
  45. package/template/packages/frontend/public/icons/biome.svg +1 -0
  46. package/template/packages/frontend/public/icons/bun.svg +19 -0
  47. package/template/packages/frontend/public/icons/next.svg +1 -0
  48. package/template/packages/frontend/public/icons/postgresql.svg +1 -0
  49. package/template/packages/frontend/public/icons/prisma.svg +6 -0
  50. package/template/packages/frontend/public/icons/react-query.svg +9 -0
  51. package/template/packages/frontend/public/icons/react.svg +1 -0
  52. package/template/packages/frontend/public/icons/tailwind.svg +12 -0
  53. package/template/packages/frontend/public/icons/tsconfig.svg +1 -0
  54. package/template/packages/frontend/public/icons/typescript.svg +7 -0
  55. package/template/packages/frontend/public/icons/zod.svg +1 -0
  56. package/template/packages/frontend/public/mascot-stitch.png +0 -0
  57. package/template/packages/frontend/public/theme-dark.svg +8 -0
  58. package/template/packages/frontend/public/theme-light.svg +8 -0
  59. package/template/packages/frontend/src/app/[locale]/error.tsx +19 -0
  60. package/template/packages/frontend/src/app/[locale]/layout.tsx +59 -0
  61. package/template/packages/frontend/src/app/[locale]/loading.tsx +14 -0
  62. package/template/packages/frontend/src/app/[locale]/page.tsx +44 -0
  63. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
  64. package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +40 -0
  65. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/account-showcase.tsx +112 -0
  66. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/admin-showcase.tsx +101 -0
  67. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-navigation.tsx +56 -0
  68. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/catalogue-shell.tsx +51 -0
  69. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/code-foundation.tsx +35 -0
  70. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/components-story.tsx +54 -0
  71. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/data-story.tsx +126 -0
  72. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/image-preloader.tsx +8 -0
  73. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/index.ts +3 -0
  74. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-sections.tsx +269 -0
  75. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +159 -0
  76. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/motion-story.tsx +78 -0
  77. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/overlays-story.tsx +134 -0
  78. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/payment-showcase.tsx +82 -0
  79. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/presentation-story.tsx +68 -0
  80. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/primitives-story.tsx +267 -0
  81. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/story-page.tsx +14 -0
  82. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story-client.tsx +208 -0
  83. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/theme-story.tsx +13 -0
  84. package/template/packages/frontend/src/app/[locale]/ui/layout.tsx +6 -0
  85. package/template/packages/frontend/src/app/[locale]/ui/page.tsx +10 -0
  86. package/template/packages/frontend/src/app/api/og/[locale]/[page]/route.tsx +53 -0
  87. package/template/packages/frontend/src/app/globals.css +210 -0
  88. package/template/packages/frontend/src/app/robots.ts +9 -0
  89. package/template/packages/frontend/src/app/sitemap.ts +22 -0
  90. package/template/packages/frontend/src/components/brand-mark.tsx +22 -0
  91. package/template/packages/frontend/src/components/repository-summary.tsx +107 -0
  92. package/template/packages/frontend/src/components/system-controls.tsx +92 -0
  93. package/template/packages/frontend/src/components/ui/adaptive-modal.tsx +382 -0
  94. package/template/packages/frontend/src/components/ui/animated-counter.tsx +80 -0
  95. package/template/packages/frontend/src/components/ui/avatar.tsx +46 -0
  96. package/template/packages/frontend/src/components/ui/badge.tsx +94 -0
  97. package/template/packages/frontend/src/components/ui/button-variants.ts +24 -0
  98. package/template/packages/frontend/src/components/ui/button.tsx +81 -0
  99. package/template/packages/frontend/src/components/ui/card-deck.tsx +73 -0
  100. package/template/packages/frontend/src/components/ui/card.tsx +81 -0
  101. package/template/packages/frontend/src/components/ui/charts/area-chart.tsx +167 -0
  102. package/template/packages/frontend/src/components/ui/charts/index.ts +7 -0
  103. package/template/packages/frontend/src/components/ui/checkbox.tsx +133 -0
  104. package/template/packages/frontend/src/components/ui/confirmation-modal.tsx +129 -0
  105. package/template/packages/frontend/src/components/ui/copyable-text.tsx +85 -0
  106. package/template/packages/frontend/src/components/ui/drawer.tsx +136 -0
  107. package/template/packages/frontend/src/components/ui/dropdown-menu.tsx +185 -0
  108. package/template/packages/frontend/src/components/ui/field.tsx +28 -0
  109. package/template/packages/frontend/src/components/ui/hand-drawn-icons.tsx +41 -0
  110. package/template/packages/frontend/src/components/ui/index.ts +36 -0
  111. package/template/packages/frontend/src/components/ui/input.tsx +24 -0
  112. package/template/packages/frontend/src/components/ui/label.tsx +18 -0
  113. package/template/packages/frontend/src/components/ui/otp-input.tsx +140 -0
  114. package/template/packages/frontend/src/components/ui/select.tsx +240 -0
  115. package/template/packages/frontend/src/components/ui/sidebar.tsx +343 -0
  116. package/template/packages/frontend/src/components/ui/skeleton.tsx +8 -0
  117. package/template/packages/frontend/src/components/ui/slider.tsx +31 -0
  118. package/template/packages/frontend/src/components/ui/slot-drum.tsx +50 -0
  119. package/template/packages/frontend/src/components/ui/spinner.tsx +30 -0
  120. package/template/packages/frontend/src/components/ui/switch.tsx +31 -0
  121. package/template/packages/frontend/src/components/ui/table.tsx +106 -0
  122. package/template/packages/frontend/src/components/ui/tabs.tsx +196 -0
  123. package/template/packages/frontend/src/components/ui/textarea.tsx +21 -0
  124. package/template/packages/frontend/src/components/ui/toaster.tsx +72 -0
  125. package/template/packages/frontend/src/components/ui/tooltip.tsx +31 -0
  126. package/template/packages/frontend/src/env.ts +21 -0
  127. package/template/packages/frontend/src/features/data-table/components/column-helpers.tsx +136 -0
  128. package/template/packages/frontend/src/features/data-table/components/data-table.tsx +303 -0
  129. package/template/packages/frontend/src/features/data-table/components/inline-stat.tsx +113 -0
  130. package/template/packages/frontend/src/features/data-table/components/sortable-header.tsx +33 -0
  131. package/template/packages/frontend/src/features/data-table/components/table-error.tsx +24 -0
  132. package/template/packages/frontend/src/features/data-table/components/table-filters.tsx +82 -0
  133. package/template/packages/frontend/src/features/data-table/hooks/useTableState.ts +111 -0
  134. package/template/packages/frontend/src/features/data-table/index.ts +26 -0
  135. package/template/packages/frontend/src/features/data-table/table-features.ts +26 -0
  136. package/template/packages/frontend/src/hooks/index.ts +3 -0
  137. package/template/packages/frontend/src/hooks/useInfiniteScroll.ts +67 -0
  138. package/template/packages/frontend/src/hooks/useIsTouchDevice.ts +25 -0
  139. package/template/packages/frontend/src/hooks/useMediaQuery.ts +30 -0
  140. package/template/packages/frontend/src/i18n/locales.ts +6 -0
  141. package/template/packages/frontend/src/i18n/navigation.ts +4 -0
  142. package/template/packages/frontend/src/i18n/request.ts +9 -0
  143. package/template/packages/frontend/src/i18n/routing.ts +8 -0
  144. package/template/packages/frontend/src/lib/api/client.ts +20 -0
  145. package/template/packages/frontend/src/lib/api/queries.ts +11 -0
  146. package/template/packages/frontend/src/lib/query-client.ts +20 -0
  147. package/template/packages/frontend/src/lib/realtime/repository.ts +21 -0
  148. package/template/packages/frontend/src/lib/seo/metadata.ts +50 -0
  149. package/template/packages/frontend/src/lib/seo/pages.test.ts +25 -0
  150. package/template/packages/frontend/src/lib/seo/pages.ts +107 -0
  151. package/template/packages/frontend/src/lib/utils/cn.ts +6 -0
  152. package/template/packages/frontend/src/lib/utils/format.ts +25 -0
  153. package/template/packages/frontend/src/lib/utils/index.ts +2 -0
  154. package/template/packages/frontend/src/providers/index.tsx +18 -0
  155. package/template/packages/frontend/src/providers/react-query.tsx +9 -0
  156. package/template/packages/frontend/src/providers/realtime.tsx +16 -0
  157. package/template/packages/frontend/src/proxy.ts +6 -0
  158. package/template/packages/frontend/src/theme/config.ts +20 -0
  159. package/template/packages/frontend/src/theme/transition.ts +54 -0
  160. package/template/packages/frontend/tsconfig.json +13 -0
  161. package/template/packages/shared/package.json +23 -0
  162. package/template/packages/shared/src/contracts/repository.ts +28 -0
  163. package/template/packages/shared/src/events/repository.ts +9 -0
  164. package/template/packages/shared/src/index.ts +3 -0
  165. package/template/packages/shared/src/schemas/repository.test.ts +36 -0
  166. package/template/packages/shared/src/schemas/repository.ts +28 -0
  167. package/template/packages/shared/tsconfig.json +5 -0
  168. package/template/playwright.config.ts +15 -0
  169. package/template/scripts/check-authored.ts +81 -0
  170. package/template/scripts/dev.ts +23 -0
  171. package/template/scripts/local-env.ts +12 -0
  172. package/template/scripts/runtime-smoke.ts +67 -0
  173. package/template/scripts/tooling-env.ts +15 -0
  174. package/template/tsconfig.json +13 -0
@@ -0,0 +1,28 @@
1
+ import { createContractFactory } from 'stitchkit';
2
+ import { RepositorySnapshotSchema } from '../schemas/repository';
3
+
4
+ export type AppScope = 'public';
5
+
6
+ const { defineContract } = createContractFactory<AppScope>();
7
+
8
+ export const repositoryContract = defineContract(
9
+ { prefix: 'repository', scope: 'public' },
10
+ {
11
+ read: {
12
+ method: 'GET',
13
+ path: '/',
14
+ desc: 'Read the server-cached repository snapshot',
15
+ output: RepositorySnapshotSchema,
16
+ expose: ['HTTP', 'MCP', 'AGENT', 'CLI'],
17
+ toolName: 'repository_read',
18
+ },
19
+ refresh: {
20
+ method: 'POST',
21
+ path: '/refresh',
22
+ desc: 'Refresh the repository snapshot from GitHub',
23
+ output: RepositorySnapshotSchema,
24
+ expose: ['HTTP', 'MCP', 'AGENT', 'CLI'],
25
+ toolName: 'repository_refresh',
26
+ },
27
+ },
28
+ );
@@ -0,0 +1,9 @@
1
+ import type { RepositorySnapshot } from '../schemas/repository';
2
+
3
+ export interface ServerToClientEvents {
4
+ 'repository:refreshed': (snapshot: RepositorySnapshot) => void;
5
+ }
6
+
7
+ export interface ClientToServerEvents {
8
+ 'repository:watch': () => void;
9
+ }
@@ -0,0 +1,3 @@
1
+ export * from './contracts/repository';
2
+ export * from './events/repository';
3
+ export * from './schemas/repository';
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { RepositorySnapshotSchema } from './repository';
3
+
4
+ describe('repository snapshot schema', () => {
5
+ test('accepts a bounded GitHub snapshot', () => {
6
+ const result = RepositorySnapshotSchema.safeParse({
7
+ fullName: 'owner/repository',
8
+ description: 'A repository',
9
+ htmlUrl: 'https://github.com/owner/repository',
10
+ language: 'TypeScript',
11
+ visibility: 'PUBLIC',
12
+ stars: 2,
13
+ forks: 1,
14
+ openIssues: 0,
15
+ commitCount: 42,
16
+ latestCommit: {
17
+ sha: 'abc123',
18
+ message: 'Ship the starter',
19
+ committedAt: '2026-08-08T00:00:00Z',
20
+ },
21
+ cache: {
22
+ state: 'fresh',
23
+ fetchedAt: '2026-08-08T00:00:00Z',
24
+ expiresAt: '2026-08-08T00:15:00Z',
25
+ },
26
+ });
27
+
28
+ expect(result.success).toBe(true);
29
+ });
30
+
31
+ test('rejects repository visibility outside the database enum', () => {
32
+ const result = RepositorySnapshotSchema.shape.visibility.safeParse('UNLISTED');
33
+
34
+ expect(result.success).toBe(false);
35
+ });
36
+ });
@@ -0,0 +1,28 @@
1
+ import { RepositoryVisibility } from '@app/db/enums';
2
+ import { z } from 'zod';
3
+
4
+ export const RepositorySnapshotSchema = z.object({
5
+ fullName: z.string().min(1),
6
+ description: z.string().nullable(),
7
+ htmlUrl: z.url(),
8
+ language: z.string().nullable(),
9
+ visibility: z.enum(RepositoryVisibility),
10
+ stars: z.number().int().nonnegative(),
11
+ forks: z.number().int().nonnegative(),
12
+ openIssues: z.number().int().nonnegative(),
13
+ commitCount: z.number().int().nonnegative(),
14
+ latestCommit: z
15
+ .object({
16
+ sha: z.string().min(1),
17
+ message: z.string().min(1),
18
+ committedAt: z.iso.datetime(),
19
+ })
20
+ .nullable(),
21
+ cache: z.object({
22
+ state: z.enum(['fresh', 'revalidated', 'stale']),
23
+ fetchedAt: z.iso.datetime(),
24
+ expiresAt: z.iso.datetime(),
25
+ }),
26
+ });
27
+
28
+ export type RepositorySnapshot = z.infer<typeof RepositorySnapshotSchema>;
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": { "lib": ["ESNext", "DOM"], "types": ["bun"] },
4
+ "include": ["src"]
5
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+ import { toolingEnv } from './scripts/tooling-env';
3
+
4
+ const baseURL = toolingEnv.PLAYWRIGHT_BASE_URL ?? toolingEnv.NEXT_PUBLIC_WEB_URL;
5
+
6
+ export default defineConfig({
7
+ testDir: './e2e',
8
+ retries: 0,
9
+ use: { baseURL, trace: 'retain-on-failure' },
10
+ projects: [
11
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
12
+ { name: 'webkit', use: { ...devices['Desktop Safari'] } },
13
+ { name: 'mobile-chromium', use: { ...devices['Pixel 7'] } },
14
+ ],
15
+ });
@@ -0,0 +1,81 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { extname, join } from 'node:path';
3
+ import { parseSync, Visitor } from 'oxc-parser';
4
+
5
+ const roots = ['packages', 'scripts'];
6
+ const processEnvMarker = ['process', 'env'].join('.');
7
+ const replacedThemePackage = ['next', 'themes'].join('-');
8
+ const generatedDirectories = new Set(['.git', '.next', 'dist', 'node_modules']);
9
+ const processEnvBoundaries = new Set([
10
+ 'packages/frontend/src/env.ts',
11
+ 'packages/config/src/server.ts',
12
+ 'scripts/tooling-env.ts',
13
+ ]);
14
+
15
+ function inspect(path: string, source: string): string[] {
16
+ const result = parseSync(path, source);
17
+ const failures = result.errors.map((error) => `${path}: parse error: ${error.message}`);
18
+ const visitor = new Visitor({
19
+ TSAsExpression(node) {
20
+ failures.push(`${path}:${node.loc?.start.line ?? 1}: type assertion`);
21
+ },
22
+ TSTypeAssertion(node) {
23
+ failures.push(`${path}:${node.loc?.start.line ?? 1}: type assertion`);
24
+ },
25
+ TSAnyKeyword(node) {
26
+ failures.push(`${path}:${node.loc?.start.line ?? 1}: explicit any`);
27
+ },
28
+ });
29
+ visitor.visit(result.program);
30
+ if (source.includes(processEnvMarker) && !processEnvBoundaries.has(path)) {
31
+ failures.push(`${path}: direct environment access outside a validated env boundary`);
32
+ }
33
+ if (source.includes(replacedThemePackage)) {
34
+ failures.push(`${path}: use @wrksz/themes as the single theme runtime`);
35
+ }
36
+ if (path.startsWith('packages/frontend/') || path.startsWith('packages/shared/')) {
37
+ for (const specifier of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
38
+ const dependency = specifier[1];
39
+ if (
40
+ dependency === '@app/db' ||
41
+ dependency?.startsWith('@prisma/') ||
42
+ (path.startsWith('packages/shared/') && dependency?.startsWith('node:')) ||
43
+ (path.startsWith('packages/shared/') && dependency === 'bun')
44
+ ) {
45
+ failures.push(
46
+ `${path}: server-only dependency ${dependency} crossed the browser boundary`,
47
+ );
48
+ }
49
+ }
50
+ }
51
+ return failures;
52
+ }
53
+
54
+ async function visitDirectory(directory: string): Promise<string[]> {
55
+ const failures: string[] = [];
56
+ const entries = await readdir(directory, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (entry.isDirectory() && generatedDirectories.has(entry.name)) continue;
59
+ const path = join(directory, entry.name);
60
+ if (path.includes('/src/generated')) continue;
61
+ if (entry.isDirectory()) {
62
+ failures.push(...(await visitDirectory(path)));
63
+ continue;
64
+ }
65
+ if (!['.ts', '.tsx'].includes(extname(path))) continue;
66
+ failures.push(...inspect(path, await readFile(path, 'utf8')));
67
+ }
68
+ return failures;
69
+ }
70
+
71
+ const failures = (await Promise.all(roots.map(visitDirectory))).flat();
72
+ const webPackage = await readFile('packages/frontend/package.json', 'utf8');
73
+ if (webPackage.includes(replacedThemePackage)) {
74
+ failures.push(
75
+ 'packages/frontend/package.json: use @wrksz/themes as the single theme runtime',
76
+ );
77
+ }
78
+ if (failures.length > 0) {
79
+ for (const failure of failures) console.error(failure);
80
+ process.exit(1);
81
+ }
@@ -0,0 +1,23 @@
1
+ import { resolve } from 'node:path';
2
+ import { ensureLocalEnvironment } from './local-env';
3
+
4
+ const root = resolve(import.meta.dir, '..');
5
+ async function run(command: string[]): Promise<void> {
6
+ const child = Bun.spawn(command, {
7
+ cwd: root,
8
+ stdin: 'inherit',
9
+ stdout: 'inherit',
10
+ stderr: 'inherit',
11
+ });
12
+ const exitCode = await child.exited;
13
+ if (exitCode !== 0) throw new Error(`${command.join(' ')} failed with exit code ${exitCode}`);
14
+ }
15
+
16
+ await ensureLocalEnvironment(root);
17
+
18
+ await run(['bun', 'run', 'db:setup']);
19
+ await run(['pm2', 'startOrReload', 'ecosystem.dev.config.cjs', '--update-env']);
20
+
21
+ console.log('Stitchkit Starter development processes are running');
22
+ console.log('Web: http://localhost:3210/en');
23
+ console.log('API: http://localhost:3211');
@@ -0,0 +1,12 @@
1
+ import { copyFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ export async function ensureLocalEnvironment(root: string): Promise<void> {
5
+ const destination = resolve(root, '.env');
6
+ if (await Bun.file(destination).exists()) return;
7
+ await copyFile(resolve(root, '_env'), destination);
8
+ }
9
+
10
+ if (import.meta.main) {
11
+ await ensureLocalEnvironment(resolve(import.meta.dir, '..'));
12
+ }
@@ -0,0 +1,67 @@
1
+ import { RepositorySnapshotSchema } from '@app/shared';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import { io } from 'socket.io-client';
5
+ import { z } from 'zod';
6
+ import { toolingEnv } from './tooling-env';
7
+
8
+ const apiOrigin = toolingEnv.NEXT_PUBLIC_API_URL;
9
+
10
+ async function json(path: string, init?: RequestInit): Promise<unknown> {
11
+ const response = await fetch(`${apiOrigin}${path}`, init);
12
+ if (!response.ok)
13
+ throw new Error(`${init?.method ?? 'GET'} ${path} returned ${response.status}`);
14
+ return response.json();
15
+ }
16
+
17
+ const socket = io(apiOrigin, { transports: ['websocket'] });
18
+ const refreshedEvent = new Promise<string>((resolve, reject) => {
19
+ const timeout = setTimeout(
20
+ () => reject(new Error('Socket.IO repository refresh event timed out')),
21
+ 5_000,
22
+ );
23
+ socket.once('repository:refreshed', (snapshot) => {
24
+ clearTimeout(timeout);
25
+ resolve(snapshot.fullName);
26
+ });
27
+ });
28
+
29
+ try {
30
+ z.object({ status: z.literal('ok') }).parse(await json('/health'));
31
+ const corsResponse = await fetch(`${apiOrigin}/api/repository/`, {
32
+ headers: { Origin: 'http://localhost:58302' },
33
+ });
34
+ if (corsResponse.headers.get('access-control-allow-origin') !== '*') {
35
+ throw new Error('Public API does not allow a forwarded browser origin');
36
+ }
37
+ const openApi = z
38
+ .object({ paths: z.record(z.string(), z.unknown()) })
39
+ .parse(await json('/openapi.json'));
40
+ if (!Object.keys(openApi.paths).some((path) => path.startsWith('/api/repository'))) {
41
+ throw new Error('OpenAPI repository path is missing');
42
+ }
43
+
44
+ const refreshed = RepositorySnapshotSchema.parse(
45
+ await json('/api/repository/refresh', { method: 'POST' }),
46
+ );
47
+ if ((await refreshedEvent) !== refreshed.fullName) {
48
+ throw new Error('Socket.IO repository identity differs');
49
+ }
50
+ const cached = RepositorySnapshotSchema.parse(await json('/api/repository/'));
51
+ if (cached.fullName !== refreshed.fullName) throw new Error('Repository cache read differs');
52
+
53
+ const client = new Client({ name: 'starter-lane', version: '1.0.0' });
54
+ const transport = new StreamableHTTPClientTransport(new URL(`${apiOrigin}/mcp`));
55
+ await client.connect(transport);
56
+ const tools = await client.listTools();
57
+ if (!tools.tools.some((tool) => tool.name === 'repository_read')) {
58
+ throw new Error('MCP repository tool is absent');
59
+ }
60
+ const result = await client.callTool({ name: 'repository_read', arguments: {} });
61
+ if (result.isError) throw new Error('MCP repository read failed');
62
+ await client.close();
63
+ } finally {
64
+ socket.close();
65
+ }
66
+
67
+ console.log('Runtime HTTP, OpenAPI, Socket.IO and MCP smoke passed');
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { config as dotenvConfig } from 'dotenv';
4
+ import { z } from 'zod';
5
+
6
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
7
+ dotenvConfig({ path: path.resolve(scriptDirectory, '..', '.env'), quiet: true });
8
+
9
+ const ToolingEnvSchema = z.object({
10
+ NEXT_PUBLIC_API_URL: z.url(),
11
+ NEXT_PUBLIC_WEB_URL: z.url(),
12
+ PLAYWRIGHT_BASE_URL: z.url().optional(),
13
+ });
14
+
15
+ export const toolingEnv = ToolingEnvSchema.parse(process.env);
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "allowImportingTsExtensions": true,
9
+ "noEmit": true,
10
+ "resolveJsonModule": true,
11
+ "verbatimModuleSyntax": true
12
+ }
13
+ }