create-stitchkit 0.1.1 → 0.3.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 (82) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +11 -4
  3. package/dist/cli.js +93 -15
  4. package/examples/repository/_env.example.append +4 -0
  5. package/examples/repository/e2e/repository.spec.ts +69 -0
  6. package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.test.ts +8 -0
  7. package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.ts +2 -1
  8. package/examples/repository/packages/backend/src/surface.snapshot.json +58 -0
  9. package/examples/repository/packages/backend/src/surface.ts +16 -0
  10. package/examples/repository/packages/config/src/features.ts +7 -0
  11. package/examples/repository/packages/db/schema.prisma +31 -0
  12. package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +45 -0
  13. package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
  14. package/{template → examples/repository}/packages/frontend/src/components/repository-summary.tsx +5 -1
  15. package/{template → examples/repository}/packages/frontend/src/lib/realtime/repository.ts +5 -6
  16. package/examples/repository/packages/frontend/src/providers/index.tsx +18 -0
  17. package/examples/repository/packages/shared/src/index.ts +5 -0
  18. package/examples/repository/packages/shared/src/realtime/repository.ts +10 -0
  19. package/{template → examples/repository}/packages/shared/src/schemas/repository.ts +4 -2
  20. package/examples/repository/scripts/runtime-smoke.ts +169 -0
  21. package/package.json +10 -1
  22. package/template/AGENTS.md +50 -0
  23. package/template/README.md +17 -8
  24. package/template/_env.example +1 -4
  25. package/template/app.config.json +9 -0
  26. package/template/bun.lock +6 -4
  27. package/template/docs/ADDING_A_FEATURE.md +102 -0
  28. package/template/e2e/starter.spec.ts +34 -22
  29. package/template/ecosystem.config.cjs +3 -2
  30. package/template/ecosystem.dev.config.cjs +7 -4
  31. package/template/package.json +6 -2
  32. package/template/packages/backend/src/cli.ts +2 -1
  33. package/template/packages/backend/src/index.ts +4 -3
  34. package/template/packages/backend/src/surface-manifest.test.ts +237 -0
  35. package/template/packages/backend/src/surface-manifest.ts +220 -0
  36. package/template/packages/backend/src/surface.snapshot.json +18 -0
  37. package/template/packages/backend/src/surface.ts +4 -9
  38. package/template/packages/backend/src/tools.ts +4 -1
  39. package/template/packages/backend/src/transport/errors.ts +1 -0
  40. package/template/packages/backend/src/transport/system-service.ts +8 -0
  41. package/template/packages/config/package.json +2 -1
  42. package/template/packages/config/src/features.ts +1 -0
  43. package/template/packages/config/src/identity.ts +18 -0
  44. package/template/packages/config/src/server.ts +10 -3
  45. package/template/packages/db/schema.prisma +0 -23
  46. package/template/packages/frontend/messages/en.json +0 -1
  47. package/template/packages/frontend/messages/ru.json +0 -1
  48. package/template/packages/frontend/package.json +1 -1
  49. package/template/packages/frontend/src/app/[locale]/page.tsx +8 -19
  50. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +3 -4
  51. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +3 -2
  52. package/template/packages/frontend/src/lib/query-client.test.ts +22 -0
  53. package/template/packages/frontend/src/lib/query-client.ts +10 -2
  54. package/template/packages/frontend/src/lib/seo/pages.ts +4 -5
  55. package/template/packages/frontend/src/providers/index.tsx +1 -4
  56. package/template/packages/frontend/src/theme/config.ts +2 -1
  57. package/template/packages/frontend/tsconfig.json +7 -1
  58. package/template/packages/shared/package.json +0 -1
  59. package/template/packages/shared/src/contracts/system.ts +19 -0
  60. package/template/packages/shared/src/index.ts +2 -3
  61. package/template/packages/shared/src/schemas/system.ts +4 -0
  62. package/template/playwright.config.ts +2 -1
  63. package/template/scripts/check-authored.ts +20 -6
  64. package/template/scripts/dev.ts +46 -7
  65. package/template/scripts/local-env.test.ts +33 -0
  66. package/template/scripts/local-env.ts +16 -5
  67. package/template/scripts/runtime-smoke.ts +22 -61
  68. package/template/scripts/surface-conformance.ts +166 -0
  69. package/template/scripts/surface-snapshot.ts +21 -0
  70. package/template/scripts/tooling-env.ts +16 -3
  71. package/template/tsconfig.json +3 -1
  72. package/template/_env +0 -11
  73. package/template/packages/shared/src/events/repository.ts +0 -9
  74. /package/{template → examples/repository}/packages/backend/src/domain/errors.ts +0 -0
  75. /package/{template → examples/repository}/packages/backend/src/transport/repository-service.ts +0 -0
  76. /package/{template → examples/repository}/packages/db/migrations/20260808000000_init/migration.sql +0 -0
  77. /package/{template → examples/repository}/packages/db/migrations/20260808170000_repository_visibility/migration.sql +0 -0
  78. /package/{template → examples/repository}/packages/frontend/src/lib/api/client.ts +0 -0
  79. /package/{template → examples/repository}/packages/frontend/src/lib/api/queries.ts +0 -0
  80. /package/{template → examples/repository}/packages/frontend/src/providers/realtime.tsx +0 -0
  81. /package/{template → examples/repository}/packages/shared/src/contracts/repository.ts +0 -0
  82. /package/{template → examples/repository}/packages/shared/src/schemas/repository.test.ts +0 -0
@@ -0,0 +1,220 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { HttpMethod } from 'stitchkit/contract';
3
+ import type { OpenApiDocument, ServiceDef } from 'stitchkit/server';
4
+ import { listToolNames } from 'stitchkit/tools';
5
+ import { z } from 'zod';
6
+
7
+ export interface SurfaceManifestOperation {
8
+ service: string;
9
+ action: string;
10
+ scope: string;
11
+ hasInput: boolean;
12
+ hasOutput: boolean;
13
+ /** Digest of the input JSON Schema — a TYPE change flips it, not just presence. */
14
+ inputShape: string | null;
15
+ /** Digest of the output JSON Schema. */
16
+ outputShape: string | null;
17
+ http: Array<{ method: HttpMethod; path: string }>;
18
+ tools: Partial<Record<'MCP' | 'AGENT' | 'CLI', string>>;
19
+ }
20
+
21
+ function schemaShape(schema: z.ZodType | undefined, io: 'input' | 'output'): string | null {
22
+ if (!schema) return null;
23
+ const document = z.toJSONSchema(schema, { io, unrepresentable: 'any' });
24
+ return createHash('sha256').update(JSON.stringify(document)).digest('hex').slice(0, 16);
25
+ }
26
+
27
+ function joinPath(...parts: Array<string | undefined>): string {
28
+ const joined = parts
29
+ .filter((part): part is string => Boolean(part))
30
+ .map((part) => part.replace(/^\/+|\/+$/g, ''))
31
+ .filter(Boolean)
32
+ .join('/');
33
+ return `/${joined}`;
34
+ }
35
+
36
+ function operationKey(service: string, action: string): string {
37
+ return `${service}\u0000${action}`;
38
+ }
39
+
40
+ function sorted(values: Iterable<string>): string[] {
41
+ return [...values].sort((left, right) => left.localeCompare(right));
42
+ }
43
+
44
+ function assertSameSet(
45
+ label: string,
46
+ expectedValues: Iterable<string>,
47
+ actualValues: Iterable<string>,
48
+ ) {
49
+ const expected = sorted(expectedValues);
50
+ const actual = sorted(actualValues);
51
+ if (
52
+ expected.length === actual.length &&
53
+ expected.every((value, index) => value === actual[index])
54
+ ) {
55
+ return;
56
+ }
57
+ throw new Error(
58
+ `${label} surface mismatch\nexpected: ${expected.join(', ') || '(empty)'}\nactual: ${actual.join(', ') || '(empty)'}`,
59
+ );
60
+ }
61
+
62
+ export function buildSurfaceManifest(
63
+ services: readonly ServiceDef[],
64
+ pathPrefix = '/api',
65
+ ): SurfaceManifestOperation[] {
66
+ const operations = new Map<string, SurfaceManifestOperation>();
67
+ for (const service of services) {
68
+ for (const method of Object.values(service.methods)) {
69
+ const key = operationKey(service.name, method.key);
70
+ if (operations.has(key)) {
71
+ throw new Error(`Duplicate operation identity ${service.name}.${method.key}`);
72
+ }
73
+ operations.set(key, {
74
+ service: service.name,
75
+ action: method.key,
76
+ scope: method.scope ?? service.scope,
77
+ hasInput: Boolean(method.inputSchema),
78
+ hasOutput: Boolean(method.outputSchema),
79
+ inputShape: schemaShape(method.inputSchema, 'input'),
80
+ outputShape: schemaShape(method.outputSchema, 'output'),
81
+ http:
82
+ !method.expose || method.expose.includes('HTTP')
83
+ ? [
84
+ {
85
+ method: method.method,
86
+ path: joinPath(pathPrefix, service.prefix, method.path).replace(
87
+ /:([^/]+)/g,
88
+ '{$1}',
89
+ ),
90
+ },
91
+ ]
92
+ : [],
93
+ tools: {},
94
+ });
95
+ }
96
+ }
97
+ for (const tool of listToolNames({ services })) {
98
+ const operation = operations.get(operationKey(tool.service, tool.method));
99
+ if (!operation) {
100
+ throw new Error(`Tool ${tool.name} has no contract identity`);
101
+ }
102
+ for (const transport of tool.transports) {
103
+ if (transport !== 'HTTP') operation.tools[transport] = tool.name;
104
+ }
105
+ }
106
+ return [...operations.values()].sort(
107
+ (left, right) =>
108
+ left.service.localeCompare(right.service) || left.action.localeCompare(right.action),
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Compare the LIVE manifest against the committed snapshot — the external
114
+ * anchor that a source-level change (an `expose` edit, a schema type change, a
115
+ * scope change) cannot move along with itself. Regenerate deliberately with
116
+ * `bun run surface:snapshot` and review the diff.
117
+ */
118
+ export function assertManifestMatchesSnapshot(
119
+ manifest: readonly SurfaceManifestOperation[],
120
+ snapshot: readonly SurfaceManifestOperation[],
121
+ ): void {
122
+ const actual = JSON.stringify(manifest, null, 2);
123
+ const expected = JSON.stringify(snapshot, null, 2);
124
+ if (actual !== expected) {
125
+ throw new Error(
126
+ `Declared surface diverged from the committed snapshot — if the change is intended, regenerate it with "bun run surface:snapshot" and review the diff.\nsnapshot: ${expected}\nactual: ${actual}`,
127
+ );
128
+ }
129
+ }
130
+
131
+ export function assertSurfaceConformance({
132
+ manifest,
133
+ openApi,
134
+ mcpToolNames,
135
+ agentToolNames,
136
+ cliToolNames,
137
+ metadata = 'require',
138
+ }: {
139
+ manifest: readonly SurfaceManifestOperation[];
140
+ openApi: Pick<OpenApiDocument, 'paths'>;
141
+ mcpToolNames: readonly string[];
142
+ agentToolNames: readonly string[];
143
+ cliToolNames: readonly string[];
144
+ /**
145
+ * `'require'` (default) — the OpenAPI document must carry `x-stitchkit-*`
146
+ * metadata and it is compared; `'ignore'` — an explicitly declared mode for
147
+ * standard documents. A silent skip is not an option.
148
+ */
149
+ metadata?: 'require' | 'ignore';
150
+ }): void {
151
+ const openApiOperations = Object.entries(openApi.paths).flatMap(([path, item]) =>
152
+ Object.entries(item)
153
+ .filter(([method]) => ['get', 'post', 'put', 'patch', 'delete', 'head'].includes(method))
154
+ .map(([method, operation]) => ({ method: method.toUpperCase(), path, operation })),
155
+ );
156
+ assertSameSet(
157
+ 'HTTP/OpenAPI',
158
+ manifest.flatMap((operation) =>
159
+ operation.http.map((entry) => `${entry.method}:${entry.path}`),
160
+ ),
161
+ openApiOperations.map(({ method, path }) => `${method}:${path}`),
162
+ );
163
+ const carriesContractMetadata = openApiOperations.some(({ operation }) =>
164
+ OpenApiOperationMetadataPresenceSchema.parse(operation),
165
+ );
166
+ if (metadata === 'require' && openApiOperations.length > 0 && !carriesContractMetadata) {
167
+ throw new Error(
168
+ 'OpenAPI document carries no x-stitchkit-* contract metadata — pass metadata: "ignore" only for a deliberately standard document',
169
+ );
170
+ }
171
+ if (metadata === 'require' && carriesContractMetadata) {
172
+ assertSameSet(
173
+ 'HTTP/OpenAPI contract metadata',
174
+ manifest.flatMap((operation) =>
175
+ operation.http.map(
176
+ (entry) =>
177
+ `${entry.method}:${entry.path}:${operation.scope}:${operation.hasInput}:${operation.hasOutput}`,
178
+ ),
179
+ ),
180
+ openApiOperations.map(({ method, path, operation }) => {
181
+ const metadata = OpenApiOperationMetadataSchema.parse(operation);
182
+ return `${method}:${path}:${metadata['x-stitchkit-scope']}:${metadata['x-stitchkit-has-input']}:${metadata['x-stitchkit-has-output']}`;
183
+ }),
184
+ );
185
+ }
186
+ assertSameSet(
187
+ 'MCP discovery',
188
+ manifest.flatMap((operation) => (operation.tools.MCP ? [operation.tools.MCP] : [])),
189
+ mcpToolNames,
190
+ );
191
+ assertSameSet(
192
+ 'AGENT mount',
193
+ manifest.flatMap((operation) => (operation.tools.AGENT ? [operation.tools.AGENT] : [])),
194
+ agentToolNames,
195
+ );
196
+ assertSameSet(
197
+ 'CLI manifest',
198
+ manifest.flatMap((operation) => (operation.tools.CLI ? [operation.tools.CLI] : [])),
199
+ cliToolNames,
200
+ );
201
+ }
202
+
203
+ const OpenApiOperationMetadataSchema = z.object({
204
+ 'x-stitchkit-scope': z.string(),
205
+ 'x-stitchkit-has-input': z.boolean(),
206
+ 'x-stitchkit-has-output': z.boolean(),
207
+ });
208
+
209
+ const OpenApiOperationMetadataPresenceSchema = z
210
+ .object({
211
+ 'x-stitchkit-scope': z.unknown().optional(),
212
+ 'x-stitchkit-has-input': z.unknown().optional(),
213
+ 'x-stitchkit-has-output': z.unknown().optional(),
214
+ })
215
+ .transform(
216
+ (metadata) =>
217
+ metadata['x-stitchkit-scope'] !== undefined ||
218
+ metadata['x-stitchkit-has-input'] !== undefined ||
219
+ metadata['x-stitchkit-has-output'] !== undefined,
220
+ );
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "service": "system",
4
+ "action": "status",
5
+ "scope": "public",
6
+ "hasInput": false,
7
+ "hasOutput": true,
8
+ "inputShape": null,
9
+ "outputShape": "58b078ade3ee6167",
10
+ "http": [
11
+ {
12
+ "method": "GET",
13
+ "path": "/api/system/status"
14
+ }
15
+ ],
16
+ "tools": {}
17
+ }
18
+ ]
@@ -1,13 +1,8 @@
1
- import type { ClientToServerEvents, ServerToClientEvents } from '@app/shared';
1
+ import { env } from '@app/config';
2
2
  import { createSocketIOServer } from 'stitchkit/server';
3
- import { createRepositoryService } from './transport/repository-service';
3
+ import { createSystemService } from './transport/system-service';
4
4
 
5
5
  export async function createSurface() {
6
- const socket = await createSocketIOServer<ServerToClientEvents, ClientToServerEvents>({
7
- cors: { origin: '*' },
8
- });
9
- const repositoryService = createRepositoryService((snapshot) =>
10
- socket.io.emit('repository:refreshed', snapshot),
11
- );
12
- return { socket, services: [repositoryService] };
6
+ const socket = await createSocketIOServer({ cors: { origin: env.CORS_ORIGIN } });
7
+ return { socket, services: [createSystemService()] };
13
8
  }
@@ -5,11 +5,14 @@ const { services, socket } = await createSurface();
5
5
 
6
6
  try {
7
7
  const surface = { services };
8
+ const toolNames = listToolNames(surface)
9
+ .filter((tool) => tool.transports.some((transport) => transport !== 'HTTP'))
10
+ .map((tool) => tool.name);
8
11
  console.log(
9
12
  JSON.stringify(
10
13
  {
11
14
  manifest: buildToolManifest({ services, transport: 'AGENT' }),
12
- names: listToolNames(surface),
15
+ names: toolNames,
13
16
  transports: summarizeTransports(surface),
14
17
  },
15
18
  null,
@@ -11,6 +11,7 @@ const codeMap: Record<StitchErrorCode, string> = {
11
11
  CONFLICT: 'conflict',
12
12
  RATE_LIMITED: 'rate_limited',
13
13
  INTERNAL_SERVER_ERROR: 'internal_error',
14
+ REALTIME_CONTRACT_VIOLATION: 'internal_error',
14
15
  };
15
16
 
16
17
  export const onError = createErrorHook({
@@ -0,0 +1,8 @@
1
+ import { type SystemStatus, systemContract } from '@app/shared';
2
+ import { implement } from 'stitchkit/server';
3
+
4
+ export function createSystemService() {
5
+ return implement(systemContract, {
6
+ status: (): SystemStatus => ({ status: 'ok' }),
7
+ });
8
+ }
@@ -4,7 +4,8 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/server.ts"
7
+ ".": "./src/server.ts",
8
+ "./identity": "./src/identity.ts"
8
9
  },
9
10
  "scripts": {
10
11
  "check": "bun x tsc --noEmit",
@@ -0,0 +1 @@
1
+ export const featureServerSchema = {};
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import source from '../../../app.config.json' with { type: 'json' };
3
+
4
+ export const ApplicationIdentitySchema = z.object({
5
+ slug: z
6
+ .string()
7
+ .min(1)
8
+ .max(64)
9
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
10
+ name: z.string().trim().min(1).max(80),
11
+ version: z.string().regex(/^\d+\.\d+\.\d+$/),
12
+ description: z.object({
13
+ en: z.string().trim().min(1),
14
+ ru: z.string().trim().min(1),
15
+ }),
16
+ });
17
+
18
+ export const appIdentity = ApplicationIdentitySchema.parse(source);
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { createEnv } from '@t3-oss/env-core';
3
3
  import { config } from 'dotenv';
4
4
  import { z } from 'zod';
5
+ import { featureServerSchema } from './features';
5
6
 
6
7
  config({ path: path.resolve(import.meta.dirname, '../../../.env'), quiet: true });
7
8
 
@@ -15,10 +16,16 @@ export const env = createEnv({
15
16
  INTERNAL_API_URL: z.url(),
16
17
  NEXT_PUBLIC_WEB_URL: z.url(),
17
18
  LOG_FORMAT: z.enum(['pretty', 'json']).default('pretty'),
18
- GITHUB_REPOSITORY: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/),
19
- GITHUB_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(900),
20
- GITHUB_TOKEN: z.string().min(1).optional(),
19
+ CORS_ORIGIN: z.url(),
20
+ ...featureServerSchema,
21
21
  },
22
22
  runtimeEnv: process.env,
23
23
  emptyStringAsUndefined: true,
24
24
  });
25
+
26
+ /** Preserve the host process environment while applying explicit child-process overrides. */
27
+ export function childEnvironment(
28
+ overrides: Record<string, string>,
29
+ ): Record<string, string | undefined> {
30
+ return { ...process.env, ...overrides };
31
+ }
@@ -6,26 +6,3 @@ generator client {
6
6
  datasource db {
7
7
  provider = "postgresql"
8
8
  }
9
-
10
- enum RepositoryVisibility {
11
- PUBLIC
12
- PRIVATE
13
- INTERNAL
14
- }
15
-
16
- model RepositorySnapshot {
17
- fullName String @id
18
- description String?
19
- htmlUrl String
20
- language String?
21
- visibility RepositoryVisibility @default(PUBLIC)
22
- stars Int
23
- forks Int
24
- openIssues Int
25
- commitCount Int
26
- latestCommitSha String?
27
- latestCommitMessage String?
28
- latestCommittedAt DateTime?
29
- fetchedAt DateTime
30
- expiresAt DateTime
31
- }
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "App": {
3
- "title": "Stitchkit Starter",
4
3
  "description": "A compact production base with one typed contract across HTTP, MCP, agents and the browser — ready for your product, not a demo domain.",
5
4
  "heroTitle": "Build the product, not the plumbing.",
6
5
  "ui": "UI system"
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "App": {
3
- "title": "Stitchkit Starter",
4
3
  "description": "Компактная production-база с единым типизированным контрактом для HTTP, MCP, агентов и браузера — готовая к вашему продукту, а не к демо-домену.",
5
4
  "heroTitle": "Стройте продукт, а не инфраструктурную рутину.",
6
5
  "ui": "UI-система"
@@ -11,7 +11,7 @@
11
11
  "test": "bun test --pass-with-no-tests"
12
12
  },
13
13
  "dependencies": {
14
- "@app/db": "workspace:*",
14
+ "@app/config": "workspace:*",
15
15
  "@app/shared": "workspace:*",
16
16
  "@radix-ui/react-alert-dialog": "^1.1.23",
17
17
  "@radix-ui/react-avatar": "^1.2.6",
@@ -1,10 +1,7 @@
1
- import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
1
+ import { appIdentity } from '@app/config/identity';
2
2
  import type { Metadata } from 'next';
3
3
  import { getTranslations } from 'next-intl/server';
4
4
  import { LocaleSchema } from '@/i18n/locales';
5
- import { createServerRepositoryApi } from '@/lib/api/client';
6
- import { useRepository } from '@/lib/api/queries';
7
- import { getQueryClient } from '@/lib/query-client';
8
5
  import { createPageMetadata } from '@/lib/seo/metadata';
9
6
  import { StarterPage } from './starter-page';
10
7
 
@@ -22,23 +19,15 @@ export async function generateMetadata({
22
19
  export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
23
20
  const { locale } = await params;
24
21
  const appLocale = LocaleSchema.parse(locale);
25
- const queryClient = getQueryClient();
26
- const api = createServerRepositoryApi();
27
22
  const t = await getTranslations('App');
28
- await queryClient.prefetchQuery({
29
- queryKey: useRepository.getKey(),
30
- queryFn: () => api.read(),
31
- });
32
23
 
33
24
  return (
34
- <HydrationBoundary state={dehydrate(queryClient)}>
35
- <StarterPage
36
- applicationName={t('title')}
37
- applicationDescription={t('description')}
38
- heroTitle={t('heroTitle')}
39
- catalogueLabel={t('ui')}
40
- locale={appLocale}
41
- />
42
- </HydrationBoundary>
25
+ <StarterPage
26
+ applicationName={appIdentity.name}
27
+ applicationDescription={appIdentity.description[appLocale]}
28
+ heroTitle={t('heroTitle')}
29
+ catalogueLabel={t('ui')}
30
+ locale={appLocale}
31
+ />
43
32
  );
44
33
  }
@@ -1,7 +1,6 @@
1
1
  import { IconArrowUpRight } from '@tabler/icons-react';
2
2
  import Image from 'next/image';
3
3
  import { BrandMark } from '@/components/brand-mark';
4
- import { RepositorySummary } from '@/components/repository-summary';
5
4
  import { LanguageSwitcher, ThemeToggle } from '@/components/system-controls';
6
5
  import { buttonVariants } from '@/components/ui';
7
6
  import type { AppLocale } from '@/i18n/locales';
@@ -140,9 +139,9 @@ export function StarterPage({
140
139
  </div>
141
140
  </div>
142
141
 
143
- <div className='mt-5 w-full'>
144
- <RepositorySummary />
145
- </div>
142
+ <p className='mt-5 text-sm text-muted-foreground'>
143
+ Add your first vertical feature from schema to transport and UI.
144
+ </p>
146
145
  </section>
147
146
  </div>
148
147
  </div>
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { appIdentity } from '@app/config/identity';
3
4
  import { IconArrowRight, IconBraces, IconDatabase, IconWorld } from '@tabler/icons-react';
4
5
  import type { ReactNode } from 'react';
5
6
  import { BrandMark } from '@/components/brand-mark';
@@ -36,7 +37,7 @@ export function LandingHeader({ actions }: { actions?: ReactNode }) {
36
37
  <header className='flex items-center justify-between border-b border-border py-4'>
37
38
  <div className='flex items-center gap-2 font-medium'>
38
39
  <BrandMark priority />
39
- Stitchkit Starter
40
+ {appIdentity.name}
40
41
  </div>
41
42
  <div className='flex items-center gap-2'>{actions}</div>
42
43
  </header>
@@ -152,7 +153,7 @@ export function PricingSection() {
152
153
  export function LandingFooter() {
153
154
  return (
154
155
  <footer className='flex flex-col gap-3 border-t border-border py-8 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between'>
155
- <span>Stitchkit Starter · Built with Stitchkit</span>
156
+ <span>{appIdentity.name} · Built with Stitchkit</span>
156
157
  <span>Contracts · UI · Realtime · Tools</span>
157
158
  </footer>
158
159
  );
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getQueryClient } from './query-client';
3
+
4
+ describe('query dehydration policy', () => {
5
+ test('includes successful prefetched data and pending streamed queries', () => {
6
+ const queryClient = getQueryClient();
7
+ queryClient.setQueryData(['prefetched-project'], { id: 'project-1' });
8
+ void queryClient.prefetchQuery({
9
+ queryKey: ['pending-project'],
10
+ queryFn: () => new Promise(() => undefined),
11
+ });
12
+
13
+ const shouldDehydrate = queryClient.getDefaultOptions().dehydrate?.shouldDehydrateQuery;
14
+ if (!shouldDehydrate) throw new Error('dehydration policy is required');
15
+ const successful = queryClient.getQueryCache().find({ queryKey: ['prefetched-project'] });
16
+ const pending = queryClient.getQueryCache().find({ queryKey: ['pending-project'] });
17
+ if (!successful || !pending) throw new Error('test queries were not created');
18
+
19
+ expect(shouldDehydrate(successful)).toBe(true);
20
+ expect(shouldDehydrate(pending)).toBe(true);
21
+ });
22
+ });
@@ -1,11 +1,19 @@
1
- import { isServer, QueryClient } from '@tanstack/react-query';
1
+ import { defaultShouldDehydrateQuery, isServer, QueryClient } from '@tanstack/react-query';
2
2
  import { cache } from 'react';
3
3
 
4
4
  function createQueryClient(): QueryClient {
5
5
  return new QueryClient({
6
6
  defaultOptions: {
7
7
  queries: { staleTime: 30_000, retry: 1 },
8
- dehydrate: { shouldDehydrateQuery: (query) => query.state.status === 'pending' },
8
+ dehydrate: {
9
+ // `pending` queries dehydrate too: a server component may kick off a
10
+ // prefetch without awaiting it, and streaming SSR hands the in-flight
11
+ // promise to the client, which resumes it instead of refetching. The
12
+ // default predicate would drop exactly those queries and reintroduce
13
+ // the client-side loading flash.
14
+ shouldDehydrateQuery: (query) =>
15
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
16
+ },
9
17
  },
10
18
  });
11
19
  }
@@ -1,7 +1,8 @@
1
+ import { appIdentity } from '@app/config/identity';
1
2
  import { z } from 'zod';
2
3
  import type { AppLocale } from '@/i18n/locales';
3
4
 
4
- export const SITE_NAME = 'Stitchkit Starter';
5
+ export const SITE_NAME = appIdentity.name;
5
6
  export const StoryIdSchema = z.enum(['components', 'themes', 'blocks']);
6
7
  export type StoryId = z.infer<typeof StoryIdSchema>;
7
8
 
@@ -42,14 +43,12 @@ const seoPages: Record<SeoPageId, SeoPageDefinition> = {
42
43
  copy: {
43
44
  en: {
44
45
  title: 'UI components',
45
- description:
46
- 'Explore accessible actions, forms, feedback, overlays, data displays and motion components included in the Stitchkit starter.',
46
+ description: `Explore accessible actions, forms, feedback, overlays, data displays and motion components included in ${SITE_NAME}.`,
47
47
  eyebrow: 'UI system',
48
48
  },
49
49
  ru: {
50
50
  title: 'UI-компоненты',
51
- description:
52
- 'Изучите доступные действия, формы, обратную связь, оверлеи, представление данных и анимации в Stitchkit Starter.',
51
+ description: `Изучите доступные действия, формы, обратную связь, оверлеи, представление данных и анимации в ${SITE_NAME}.`,
53
52
  eyebrow: 'UI-система',
54
53
  },
55
54
  },
@@ -4,14 +4,11 @@ import { TooltipProvider } from '@radix-ui/react-tooltip';
4
4
  import type { ReactNode } from 'react';
5
5
  import { Toaster } from '@/components/ui/toaster';
6
6
  import { ReactQueryProvider } from './react-query';
7
- import { RealtimeProvider } from './realtime';
8
7
 
9
8
  export function Providers({ children }: { children: ReactNode }) {
10
9
  return (
11
10
  <ReactQueryProvider>
12
- <RealtimeProvider>
13
- <TooltipProvider delayDuration={250}>{children}</TooltipProvider>
14
- </RealtimeProvider>
11
+ <TooltipProvider delayDuration={250}>{children}</TooltipProvider>
15
12
  <Toaster />
16
13
  </ReactQueryProvider>
17
14
  );
@@ -1,9 +1,10 @@
1
+ import { appIdentity } from '@app/config/identity';
1
2
  import type { ThemeProviderProps } from '@wrksz/themes/next';
2
3
 
3
4
  export type AppTheme = 'light' | 'dark';
4
5
 
5
6
  export const appThemes: readonly [AppTheme, AppTheme] = ['light', 'dark'];
6
- export const themeStorageKey = 'stitchkit-starter-theme';
7
+ export const themeStorageKey = `${appIdentity.slug}-theme`;
7
8
 
8
9
  export const themeProviderConfig = {
9
10
  themes: appThemes,
@@ -8,6 +8,12 @@
8
8
  "plugins": [{ "name": "next" }],
9
9
  "paths": { "@/*": ["./src/*"] }
10
10
  },
11
- "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", ".next/types/**/*.ts"],
11
+ "include": [
12
+ "next-env.d.ts",
13
+ "next.config.ts",
14
+ "src/**/*.ts",
15
+ "src/**/*.tsx",
16
+ ".next/types/**/*.ts"
17
+ ],
12
18
  "exclude": ["node_modules"]
13
19
  }
@@ -12,7 +12,6 @@
12
12
  "test": "bun test --pass-with-no-tests"
13
13
  },
14
14
  "dependencies": {
15
- "@app/db": "workspace:*",
16
15
  "stitchkit": "catalog:",
17
16
  "zod": "^4.4.3"
18
17
  },
@@ -0,0 +1,19 @@
1
+ import { createContractFactory } from 'stitchkit';
2
+ import { SystemStatusSchema } from '../schemas/system';
3
+
4
+ const { defineContract } = createContractFactory<'public'>({
5
+ toolExposure: 'explicit',
6
+ });
7
+
8
+ export const systemContract = defineContract(
9
+ { prefix: 'system', scope: 'public' },
10
+ {
11
+ status: {
12
+ method: 'GET',
13
+ path: '/status',
14
+ desc: 'Read application readiness',
15
+ output: SystemStatusSchema,
16
+ expose: ['HTTP'],
17
+ },
18
+ },
19
+ );
@@ -1,3 +1,2 @@
1
- export * from './contracts/repository';
2
- export * from './events/repository';
3
- export * from './schemas/repository';
1
+ export * from './contracts/system';
2
+ export * from './schemas/system';
@@ -0,0 +1,4 @@
1
+ import { z } from 'zod';
2
+
3
+ export const SystemStatusSchema = z.object({ status: z.literal('ok') });
4
+ export type SystemStatus = z.infer<typeof SystemStatusSchema>;