create-stitchkit 0.1.1 → 0.2.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 (68) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +11 -4
  3. package/dist/cli.js +99 -12
  4. package/examples/repository/_env.append +3 -0
  5. package/examples/repository/_env.example.append +4 -0
  6. package/examples/repository/e2e/repository.spec.ts +19 -0
  7. package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.ts +2 -1
  8. package/examples/repository/packages/backend/src/surface.ts +14 -0
  9. package/examples/repository/packages/config/src/features.ts +7 -0
  10. package/examples/repository/packages/db/schema.prisma +31 -0
  11. package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +45 -0
  12. package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +151 -0
  13. package/examples/repository/packages/frontend/src/providers/index.tsx +18 -0
  14. package/examples/repository/packages/shared/src/index.ts +3 -0
  15. package/examples/repository/scripts/runtime-smoke.ts +91 -0
  16. package/package.json +10 -1
  17. package/template/AGENTS.md +50 -0
  18. package/template/README.md +18 -5
  19. package/template/_env +1 -3
  20. package/template/_env.example +1 -4
  21. package/template/app.config.json +9 -0
  22. package/template/bun.lock +4 -2
  23. package/template/docs/ADDING_A_FEATURE.md +101 -0
  24. package/template/docs/LAN_HTTPS.md +28 -0
  25. package/template/e2e/starter.spec.ts +17 -22
  26. package/template/ecosystem.config.cjs +3 -2
  27. package/template/ecosystem.dev.config.cjs +19 -4
  28. package/template/package.json +3 -1
  29. package/template/packages/backend/src/cli.ts +2 -1
  30. package/template/packages/backend/src/index.ts +19 -4
  31. package/template/packages/backend/src/surface-manifest.test.ts +62 -0
  32. package/template/packages/backend/src/surface-manifest.ts +117 -0
  33. package/template/packages/backend/src/surface.ts +3 -9
  34. package/template/packages/backend/src/transport/lan-onboarding.ts +29 -0
  35. package/template/packages/config/package.json +2 -1
  36. package/template/packages/config/src/features.ts +1 -0
  37. package/template/packages/config/src/identity.ts +18 -0
  38. package/template/packages/config/src/server.ts +13 -3
  39. package/template/packages/db/schema.prisma +0 -23
  40. package/template/packages/frontend/messages/en.json +0 -1
  41. package/template/packages/frontend/messages/ru.json +0 -1
  42. package/template/packages/frontend/package.json +1 -0
  43. package/template/packages/frontend/src/app/[locale]/page.tsx +8 -19
  44. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +3 -4
  45. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +3 -2
  46. package/template/packages/frontend/src/lib/seo/pages.ts +4 -5
  47. package/template/packages/frontend/src/providers/index.tsx +1 -4
  48. package/template/packages/frontend/src/theme/config.ts +2 -1
  49. package/template/packages/shared/src/index.ts +1 -3
  50. package/template/scripts/dev-lan.test.ts +58 -0
  51. package/template/scripts/dev-lan.ts +176 -0
  52. package/template/scripts/dev.ts +37 -7
  53. package/template/scripts/runtime-smoke.ts +13 -59
  54. package/template/scripts/surface-conformance.ts +105 -0
  55. /package/{template → examples/repository}/packages/backend/src/domain/errors.ts +0 -0
  56. /package/{template → examples/repository}/packages/backend/src/domain/repository/github-cache.test.ts +0 -0
  57. /package/{template → examples/repository}/packages/backend/src/transport/repository-service.ts +0 -0
  58. /package/{template → examples/repository}/packages/db/migrations/20260808000000_init/migration.sql +0 -0
  59. /package/{template → examples/repository}/packages/db/migrations/20260808170000_repository_visibility/migration.sql +0 -0
  60. /package/{template → examples/repository}/packages/frontend/src/components/repository-summary.tsx +0 -0
  61. /package/{template → examples/repository}/packages/frontend/src/lib/api/client.ts +0 -0
  62. /package/{template → examples/repository}/packages/frontend/src/lib/api/queries.ts +0 -0
  63. /package/{template → examples/repository}/packages/frontend/src/lib/realtime/repository.ts +0 -0
  64. /package/{template → examples/repository}/packages/frontend/src/providers/realtime.tsx +0 -0
  65. /package/{template → examples/repository}/packages/shared/src/contracts/repository.ts +0 -0
  66. /package/{template → examples/repository}/packages/shared/src/events/repository.ts +0 -0
  67. /package/{template → examples/repository}/packages/shared/src/schemas/repository.test.ts +0 -0
  68. /package/{template → examples/repository}/packages/shared/src/schemas/repository.ts +0 -0
@@ -1,10 +1,13 @@
1
1
  import { resolve } from 'node:path';
2
+ import { appIdentity } from '../packages/config/src/identity';
3
+ import { childEnvironment, env } from '../packages/config/src/server';
2
4
  import { ensureLocalEnvironment } from './local-env';
3
5
 
4
6
  const root = resolve(import.meta.dir, '..');
5
- async function run(command: string[]): Promise<void> {
7
+ async function run(command: string[], environment?: Record<string, string>): Promise<void> {
6
8
  const child = Bun.spawn(command, {
7
9
  cwd: root,
10
+ env: environment ? childEnvironment(environment) : undefined,
8
11
  stdin: 'inherit',
9
12
  stdout: 'inherit',
10
13
  stderr: 'inherit',
@@ -13,11 +16,38 @@ async function run(command: string[]): Promise<void> {
13
16
  if (exitCode !== 0) throw new Error(`${command.join(' ')} failed with exit code ${exitCode}`);
14
17
  }
15
18
 
16
- await ensureLocalEnvironment(root);
19
+ export async function runDevelopment(environment?: Record<string, string>): Promise<void> {
20
+ await ensureLocalEnvironment(root);
21
+ const environmentForRun = developmentEnvironment(environment);
22
+ await run(['bun', 'run', 'db:setup'], environmentForRun);
23
+ await run(
24
+ ['pm2', 'startOrReload', 'ecosystem.dev.config.cjs', '--update-env'],
25
+ environmentForRun,
26
+ );
27
+ }
28
+
29
+ export function developmentEnvironment(
30
+ overrides: Record<string, string> = {},
31
+ ): Record<string, string> {
32
+ return {
33
+ API_PORT: String(env.API_PORT),
34
+ WEB_PORT: String(env.WEB_PORT),
35
+ CORS_ORIGIN: env.CORS_ORIGIN,
36
+ NEXT_PUBLIC_API_URL: env.NEXT_PUBLIC_API_URL,
37
+ NEXT_PUBLIC_WEB_URL: env.NEXT_PUBLIC_WEB_URL,
38
+ INTERNAL_API_URL: env.INTERNAL_API_URL,
39
+ DEV_HTTPS_CERT: '',
40
+ DEV_HTTPS_KEY: '',
41
+ DEV_HTTPS_CA: '',
42
+ NODE_EXTRA_CA_CERTS: '',
43
+ ...overrides,
44
+ };
45
+ }
17
46
 
18
- await run(['bun', 'run', 'db:setup']);
19
- await run(['pm2', 'startOrReload', 'ecosystem.dev.config.cjs', '--update-env']);
47
+ if (import.meta.main) {
48
+ await runDevelopment();
20
49
 
21
- console.log('Stitchkit Starter development processes are running');
22
- console.log('Web: http://localhost:3210/en');
23
- console.log('API: http://localhost:3211');
50
+ console.log(`${appIdentity.name} development processes are running`);
51
+ console.log('Web: http://localhost:3210/en');
52
+ console.log('API: http://localhost:3211');
53
+ }
@@ -1,69 +1,23 @@
1
- import { RepositorySnapshotSchema } from '@app/shared';
2
- import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
3
- import { io } from 'socket.io-client';
4
1
  import { z } from 'zod';
2
+ import { runSurfaceConformance } from './surface-conformance';
5
3
  import { toolingEnv } from './tooling-env';
6
4
 
7
5
  const apiOrigin = toolingEnv.NEXT_PUBLIC_API_URL;
8
6
 
9
- async function json(path: string, init?: RequestInit): Promise<unknown> {
10
- const response = await fetch(`${apiOrigin}${path}`, init);
11
- if (!response.ok)
12
- throw new Error(`${init?.method ?? 'GET'} ${path} returned ${response.status}`);
7
+ async function json(path: string): Promise<unknown> {
8
+ const response = await fetch(`${apiOrigin}${path}`);
9
+ if (!response.ok) throw new Error(`GET ${path} returned ${response.status}`);
13
10
  return response.json();
14
11
  }
15
12
 
16
- const socket = io(apiOrigin, { transports: ['websocket'] });
17
- const refreshedEvent = new Promise<string>((resolve, reject) => {
18
- const timeout = setTimeout(
19
- () => reject(new Error('Socket.IO repository refresh event timed out')),
20
- 5_000,
21
- );
22
- socket.once('repository:refreshed', (snapshot) => {
23
- clearTimeout(timeout);
24
- resolve(snapshot.fullName);
25
- });
26
- });
27
-
28
- try {
29
- z.object({ status: z.literal('ok') }).parse(await json('/health'));
30
- const corsResponse = await fetch(`${apiOrigin}/api/repository/`, {
31
- headers: { Origin: 'http://localhost:58302' },
32
- });
33
- if (corsResponse.headers.get('access-control-allow-origin') !== '*') {
34
- throw new Error('Public API does not allow a forwarded browser origin');
35
- }
36
- const openApi = z
37
- .object({ paths: z.record(z.string(), z.unknown()) })
38
- .parse(await json('/openapi.json'));
39
- if (!Object.keys(openApi.paths).some((path) => path.startsWith('/api/repository'))) {
40
- throw new Error('OpenAPI repository path is missing');
41
- }
42
-
43
- const refreshed = RepositorySnapshotSchema.parse(
44
- await json('/api/repository/refresh', { method: 'POST' }),
45
- );
46
- if ((await refreshedEvent) !== refreshed.fullName) {
47
- throw new Error('Socket.IO repository identity differs');
48
- }
49
- const cached = RepositorySnapshotSchema.parse(await json('/api/repository/'));
50
- if (cached.fullName !== refreshed.fullName) throw new Error('Repository cache read differs');
51
-
52
- const client = new Client(
53
- { name: 'starter-lane', version: '1.0.0' },
54
- { versionNegotiation: { mode: { pin: '2026-07-28' } } },
55
- );
56
- const transport = new StreamableHTTPClientTransport(new URL(`${apiOrigin}/mcp`));
57
- await client.connect(transport);
58
- const tools = await client.listTools();
59
- if (!tools.tools.some((tool) => tool.name === 'repository_read')) {
60
- throw new Error('MCP repository tool is absent');
61
- }
62
- const result = await client.callTool({ name: 'repository_read', arguments: {} });
63
- if (result.isError) throw new Error('MCP repository read failed');
64
- await client.close();
65
- } finally {
66
- socket.close();
13
+ z.object({ status: z.literal('ok') }).parse(await json('/health'));
14
+ const openApi = z
15
+ .object({ paths: z.record(z.string(), z.unknown()) })
16
+ .parse(await json('/openapi.json'));
17
+ if (Object.keys(openApi.paths).some((path) => path.startsWith('/api/'))) {
18
+ throw new Error('Blank starter unexpectedly publishes application contracts');
67
19
  }
68
20
 
69
- console.log('Runtime HTTP, OpenAPI, Socket.IO and MCP smoke passed');
21
+ await runSurfaceConformance({ apiOrigin });
22
+
23
+ console.log('Blank runtime HTTP, OpenAPI and MCP smoke passed');
@@ -0,0 +1,105 @@
1
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
2
+ import { z } from 'zod';
3
+ import { createSurface } from '../packages/backend/src/surface';
4
+ import {
5
+ assertSurfaceConformance,
6
+ buildSurfaceManifest,
7
+ type SurfaceManifestOperation,
8
+ } from '../packages/backend/src/surface-manifest';
9
+
10
+ export interface SurfaceProbe {
11
+ name: string;
12
+ run: () => Promise<void>;
13
+ }
14
+
15
+ export function defineSurfaceProbe<TInput, TOutput>({
16
+ name,
17
+ input,
18
+ fixture,
19
+ output,
20
+ run,
21
+ }: {
22
+ name: string;
23
+ input: z.ZodType<TInput>;
24
+ fixture: unknown;
25
+ output?: z.ZodType<TOutput>;
26
+ run: (input: TInput) => Promise<TOutput>;
27
+ }): SurfaceProbe {
28
+ return {
29
+ name,
30
+ run: async () => {
31
+ const result = await run(input.parse(fixture));
32
+ output?.parse(result);
33
+ },
34
+ };
35
+ }
36
+
37
+ interface SurfaceConformanceOptions {
38
+ apiOrigin: string;
39
+ probes?: readonly SurfaceProbe[];
40
+ }
41
+
42
+ async function readJson(url: string): Promise<unknown> {
43
+ const response = await fetch(url);
44
+ if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
45
+ return response.json();
46
+ }
47
+
48
+ async function discoverMcpTools(
49
+ apiOrigin: string,
50
+ manifest: readonly SurfaceManifestOperation[],
51
+ ): Promise<string[]> {
52
+ const expectedCount = manifest.filter((operation) => operation.tools.MCP).length;
53
+ const client = new Client(
54
+ { name: 'surface-conformance', version: '1.0.0' },
55
+ { versionNegotiation: { mode: { pin: '2026-07-28' } } },
56
+ );
57
+ const transport = new StreamableHTTPClientTransport(new URL(`${apiOrigin}/mcp`));
58
+ try {
59
+ await client.connect(transport);
60
+ try {
61
+ return (await client.listTools()).tools.map((tool) => tool.name);
62
+ } catch (error) {
63
+ if (
64
+ expectedCount === 0 &&
65
+ error instanceof Error &&
66
+ error.message.includes('not supported by the negotiated protocol version')
67
+ ) {
68
+ return [];
69
+ }
70
+ throw error;
71
+ }
72
+ } finally {
73
+ await client.close();
74
+ }
75
+ }
76
+
77
+ export async function runSurfaceConformance({
78
+ apiOrigin,
79
+ probes = [],
80
+ }: SurfaceConformanceOptions): Promise<void> {
81
+ const { services, socket } = await createSurface();
82
+ try {
83
+ const manifest = buildSurfaceManifest(services);
84
+ const openApi = await readJson(`${apiOrigin}/openapi.json`);
85
+ const mcpToolNames = await discoverMcpTools(apiOrigin, manifest);
86
+ assertSurfaceConformance({
87
+ manifest,
88
+ openApi: OpenApiSnapshotSchema.parse(openApi),
89
+ mcpToolNames,
90
+ });
91
+ for (const probe of probes) {
92
+ try {
93
+ await probe.run();
94
+ } catch (cause) {
95
+ throw new Error(`Surface probe "${probe.name}" failed`, { cause });
96
+ }
97
+ }
98
+ } finally {
99
+ await socket.io.close();
100
+ }
101
+ }
102
+
103
+ const OpenApiSnapshotSchema = z.object({
104
+ paths: z.record(z.string(), z.record(z.string(), z.unknown())),
105
+ });