create-stitchkit 0.2.0 → 0.3.1

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 (59) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/cli.js +7 -16
  3. package/examples/repository/_env.example.append +2 -0
  4. package/examples/repository/e2e/repository.spec.ts +76 -2
  5. package/examples/repository/packages/backend/src/domain/repository/github-cache.test.ts +8 -0
  6. package/examples/repository/packages/backend/src/domain/repository/github-cache.ts +2 -2
  7. package/examples/repository/packages/backend/src/surface.snapshot.json +58 -0
  8. package/examples/repository/packages/backend/src/surface.ts +7 -5
  9. package/examples/repository/packages/config/src/features.ts +1 -0
  10. package/examples/repository/packages/frontend/src/components/repository-summary.tsx +7 -3
  11. package/examples/repository/packages/frontend/src/lib/realtime/repository.ts +5 -6
  12. package/examples/repository/packages/shared/src/index.ts +3 -1
  13. package/examples/repository/packages/shared/src/realtime/repository.ts +10 -0
  14. package/examples/repository/packages/shared/src/schemas/repository.ts +4 -2
  15. package/examples/repository/scripts/runtime-smoke.ts +100 -19
  16. package/package.json +1 -1
  17. package/template/README.md +6 -10
  18. package/template/_env.example +1 -1
  19. package/template/bun.lock +4 -4
  20. package/template/docs/ADDING_A_FEATURE.md +3 -2
  21. package/template/e2e/starter.spec.ts +21 -13
  22. package/template/ecosystem.dev.config.cjs +0 -12
  23. package/template/package.json +5 -3
  24. package/template/packages/backend/src/cli.ts +1 -1
  25. package/template/packages/backend/src/index.ts +27 -24
  26. package/template/packages/backend/src/surface-manifest.test.ts +183 -8
  27. package/template/packages/backend/src/surface-manifest.ts +108 -5
  28. package/template/packages/backend/src/surface.snapshot.json +18 -0
  29. package/template/packages/backend/src/surface.ts +2 -1
  30. package/template/packages/backend/src/tools.ts +5 -2
  31. package/template/packages/backend/src/transport/errors.ts +1 -0
  32. package/template/packages/backend/src/transport/system-service.ts +8 -0
  33. package/template/packages/config/src/server.ts +1 -4
  34. package/template/packages/frontend/package.json +0 -1
  35. package/template/packages/frontend/src/lib/query-client.test.ts +22 -0
  36. package/template/packages/frontend/src/lib/query-client.ts +10 -2
  37. package/template/packages/frontend/tsconfig.json +7 -1
  38. package/template/packages/shared/package.json +0 -1
  39. package/template/packages/shared/src/contracts/system.ts +19 -0
  40. package/template/packages/shared/src/index.ts +2 -1
  41. package/template/packages/shared/src/schemas/system.ts +4 -0
  42. package/template/playwright.config.ts +2 -1
  43. package/template/scripts/check-authored.ts +20 -6
  44. package/template/scripts/dev.ts +19 -10
  45. package/template/scripts/local-env.test.ts +33 -0
  46. package/template/scripts/local-env.ts +16 -5
  47. package/template/scripts/runtime-smoke.ts +14 -4
  48. package/template/scripts/surface-conformance.ts +77 -16
  49. package/template/scripts/surface-snapshot.ts +21 -0
  50. package/template/scripts/tooling-env.ts +16 -3
  51. package/template/scripts/web-surface-smoke.ts +28 -0
  52. package/template/tsconfig.json +3 -1
  53. package/examples/repository/_env.append +0 -3
  54. package/examples/repository/packages/shared/src/events/repository.ts +0 -9
  55. package/template/_env +0 -9
  56. package/template/docs/LAN_HTTPS.md +0 -28
  57. package/template/packages/backend/src/transport/lan-onboarding.ts +0 -29
  58. package/template/scripts/dev-lan.test.ts +0 -58
  59. package/template/scripts/dev-lan.ts +0 -176
@@ -5,9 +5,11 @@
5
5
  "moduleResolution": "bundler",
6
6
  "strict": true,
7
7
  "skipLibCheck": true,
8
+ "types": ["bun", "node"],
8
9
  "allowImportingTsExtensions": true,
9
10
  "noEmit": true,
10
11
  "resolveJsonModule": true,
11
12
  "verbatimModuleSyntax": true
12
- }
13
+ },
14
+ "include": ["scripts/**/*.ts", "e2e/**/*.ts", "playwright.config.ts"]
13
15
  }
@@ -1,3 +0,0 @@
1
- GITHUB_REPOSITORY=max-listov/stitchkit
2
- GITHUB_CACHE_TTL_SECONDS=900
3
- # GITHUB_TOKEN=github_pat_...
@@ -1,9 +0,0 @@
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
- }
package/template/_env DELETED
@@ -1,9 +0,0 @@
1
- NODE_ENV=development
2
- DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter
3
- API_PORT=3211
4
- WEB_PORT=3210
5
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3211
6
- INTERNAL_API_URL=http://127.0.0.1:3211
7
- NEXT_PUBLIC_WEB_URL=http://127.0.0.1:3210
8
- LOG_FORMAT=pretty
9
- CORS_ORIGIN=*
@@ -1,28 +0,0 @@
1
- # Trusted LAN HTTPS development
2
-
3
- Use this optional mode to test microphone, camera, PWA and WebRTC behavior on a
4
- physical device connected to the same private network:
5
-
6
- ```bash
7
- # Install the maintained local-CA tool first:
8
- brew install mkcert
9
-
10
- bun run dev:lan
11
- # Multiple private interfaces:
12
- bun run dev:lan --host 192.168.1.20
13
- ```
14
-
15
- The launcher uses mkcert, stores leaf certificates under
16
- `~/.local/share/<app-slug>/lan-https`, and keeps the CA in mkcert's own CAROOT.
17
- No certificate or machine address is written into the project. When the chosen
18
- address changes, only the leaf certificate is renewed.
19
-
20
- The command prints the HTTPS web/API URLs and a development-only onboarding URL.
21
- Open that URL on the phone to download the **public** root certificate. On iOS,
22
- install the profile and enable full trust under Certificate Trust Settings. On
23
- Android, install it as a CA certificate; native development builds must opt into
24
- user-installed roots. The private CA key is never served.
25
-
26
- `bun run dev` remains plain localhost HTTP. Production never reads the LAN
27
- certificate variables or mounts the onboarding routes.
28
-
@@ -1,29 +0,0 @@
1
- import { env } from '@app/config';
2
- import type { RawRoute } from 'stitchkit/server';
3
-
4
- export function createLanOnboardingRoutes(): RawRoute[] {
5
- if (env.NODE_ENV !== 'development' || !env.DEV_HTTPS_CA) return [];
6
- const caPath = env.DEV_HTTPS_CA;
7
- return [
8
- {
9
- method: 'GET',
10
- path: '/__dev/lan-ca',
11
- handler: () =>
12
- new Response(Bun.file(caPath), {
13
- headers: {
14
- 'Content-Disposition': 'attachment; filename="stitchkit-lan-root-ca.pem"',
15
- 'Content-Type': 'application/x-pem-file',
16
- },
17
- }),
18
- },
19
- {
20
- method: 'GET',
21
- path: '/__dev/lan',
22
- handler: () =>
23
- new Response(
24
- `<!doctype html><meta name="viewport" content="width=device-width"><title>LAN HTTPS setup</title><main><h1>Trust this development CA</h1><p><a href="/__dev/lan-ca">Download the public root certificate</a>.</p><h2>iOS</h2><p>Install the downloaded profile, then enable full trust in Settings → General → About → Certificate Trust Settings.</p><h2>Android</h2><p>Install it as a CA certificate. Native development builds must explicitly allow user-installed roots.</p><p>No private key is exposed by this server.</p></main>`,
25
- { headers: { 'Content-Type': 'text/html; charset=utf-8' } },
26
- ),
27
- },
28
- ];
29
- }
@@ -1,58 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { developmentEnvironment } from './dev';
3
- import { privateLanAddresses, selectLanAddress } from './dev-lan';
4
-
5
- describe('LAN HTTPS address selection', () => {
6
- test('normal development clears every LAN-only process variable', () => {
7
- expect(developmentEnvironment()).toMatchObject({
8
- DEV_HTTPS_CA: '',
9
- DEV_HTTPS_CERT: '',
10
- DEV_HTTPS_KEY: '',
11
- NODE_EXTRA_CA_CERTS: '',
12
- });
13
- expect(developmentEnvironment({ DEV_HTTPS_CERT: '/cert.pem' }).DEV_HTTPS_CERT).toBe(
14
- '/cert.pem',
15
- );
16
- });
17
-
18
- test('keeps private IPv4 interfaces and ignores public/internal addresses', () => {
19
- expect(
20
- privateLanAddresses({
21
- eth0: [
22
- {
23
- address: '192.168.1.20',
24
- netmask: '',
25
- family: 'IPv4',
26
- mac: '',
27
- internal: false,
28
- cidr: null,
29
- },
30
- {
31
- address: '8.8.8.8',
32
- netmask: '',
33
- family: 'IPv4',
34
- mac: '',
35
- internal: false,
36
- cidr: null,
37
- },
38
- ],
39
- lo: [
40
- {
41
- address: '127.0.0.1',
42
- netmask: '',
43
- family: 'IPv4',
44
- mac: '',
45
- internal: true,
46
- cidr: null,
47
- },
48
- ],
49
- }),
50
- ).toEqual(['192.168.1.20']);
51
- });
52
-
53
- test('auto-selects one address and fails on ambiguity', () => {
54
- expect(selectLanAddress(['10.0.0.4'])).toBe('10.0.0.4');
55
- expect(() => selectLanAddress(['10.0.0.4', '192.168.1.20'])).toThrow('ambiguous');
56
- expect(() => selectLanAddress(['10.0.0.4'], '10.0.0.5')).toThrow('unavailable');
57
- });
58
- });
@@ -1,176 +0,0 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { homedir, networkInterfaces } from 'node:os';
3
- import { join } from 'node:path';
4
- import { z } from 'zod';
5
- import { appIdentity } from '../packages/config/src/identity';
6
- import { childEnvironment, env } from '../packages/config/src/server';
7
- import { runDevelopment } from './dev';
8
-
9
- export function privateLanAddresses(
10
- interfaces: ReturnType<typeof networkInterfaces>,
11
- ): string[] {
12
- const addresses = new Set<string>();
13
- for (const entries of Object.values(interfaces)) {
14
- for (const entry of entries ?? []) {
15
- if (entry.family !== 'IPv4' || entry.internal) continue;
16
- const [first, second] = entry.address.split('.').map(Number);
17
- if (
18
- first === 10 ||
19
- (first === 172 && second !== undefined && second >= 16 && second <= 31) ||
20
- (first === 192 && second === 168)
21
- ) {
22
- addresses.add(entry.address);
23
- }
24
- }
25
- }
26
- return [...addresses].sort();
27
- }
28
-
29
- export function selectLanAddress(addresses: readonly string[], requested?: string): string {
30
- if (requested) {
31
- if (!addresses.includes(requested)) {
32
- throw new Error(
33
- `Requested LAN address ${requested} is unavailable. Detected: ${addresses.join(', ') || '(none)'}`,
34
- );
35
- }
36
- return requested;
37
- }
38
- if (addresses.length !== 1) {
39
- throw new Error(
40
- `LAN address is ${addresses.length === 0 ? 'unavailable' : 'ambiguous'}. Use --host <address>. Detected: ${addresses.join(', ') || '(none)'}`,
41
- );
42
- }
43
- return addresses[0];
44
- }
45
-
46
- async function run(command: string[], environment?: Record<string, string>): Promise<string> {
47
- const child = Bun.spawn(command, {
48
- env: environment ? childEnvironment(environment) : undefined,
49
- stdout: 'pipe',
50
- stderr: 'inherit',
51
- });
52
- const output = await new Response(child.stdout).text();
53
- const exitCode = await child.exited;
54
- if (exitCode !== 0) throw new Error(`${command.join(' ')} failed with exit code ${exitCode}`);
55
- return output.trim();
56
- }
57
-
58
- function requestedHost(args: readonly string[]): string | undefined {
59
- const index = args.indexOf('--host');
60
- if (index === -1) return undefined;
61
- const value = args[index + 1];
62
- if (!value) throw new Error('--host requires an IPv4 address');
63
- return value;
64
- }
65
-
66
- async function ensureCertificates(host: string) {
67
- if (!Bun.which('mkcert')) {
68
- throw new Error(
69
- 'dev:lan requires mkcert. Install it with `brew install mkcert` (macOS/Linuxbrew) or the official prebuilt binary, then rerun.',
70
- );
71
- }
72
- const caRoot = await run(['mkcert', '-CAROOT']);
73
- const ca = join(caRoot, 'rootCA.pem');
74
- if (!(await Bun.file(ca).exists())) await run(['mkcert', '-install']);
75
-
76
- const directory = join(homedir(), '.local', 'share', appIdentity.slug, 'lan-https');
77
- const cert = join(directory, 'leaf.pem');
78
- const key = join(directory, 'leaf-key.pem');
79
- const marker = join(directory, 'host');
80
- await mkdir(directory, { recursive: true, mode: 0o700 });
81
- const previous = await readFile(marker, 'utf8').catch(() => '');
82
- if (
83
- previous.trim() !== host ||
84
- !(await Bun.file(cert).exists()) ||
85
- !(await Bun.file(key).exists())
86
- ) {
87
- await run([
88
- 'mkcert',
89
- '-cert-file',
90
- cert,
91
- '-key-file',
92
- key,
93
- host,
94
- 'localhost',
95
- '127.0.0.1',
96
- '::1',
97
- ]);
98
- await writeFile(marker, `${host}\n`, { mode: 0o600 });
99
- }
100
- return { ca, cert, key };
101
- }
102
-
103
- async function isPortOccupied(port: number): Promise<boolean> {
104
- return new Promise((resolve) => {
105
- let settled = false;
106
- const finish = (occupied: boolean, socket?: { end(): void }) => {
107
- if (settled) return;
108
- settled = true;
109
- resolve(occupied);
110
- socket?.end();
111
- };
112
- void Bun.connect({
113
- hostname: '127.0.0.1',
114
- port,
115
- socket: {
116
- open: (socket) => finish(true, socket),
117
- data: () => undefined,
118
- close: () => finish(false),
119
- error: () => finish(false),
120
- },
121
- }).catch(() => finish(false));
122
- });
123
- }
124
-
125
- async function assertDevelopmentPortsAvailable(): Promise<void> {
126
- const processList = await run(['pm2', 'jlist']);
127
- const onlineNames = new Set(
128
- z
129
- .array(
130
- z.object({
131
- name: z.string(),
132
- pm2_env: z.object({ status: z.string() }),
133
- }),
134
- )
135
- .parse(JSON.parse(processList))
136
- .filter((process) => process.pm2_env.status === 'online')
137
- .map((process) => process.name),
138
- );
139
- const expected = new Map([
140
- [env.WEB_PORT, `${appIdentity.slug}-frontend-dev`],
141
- [env.API_PORT, `${appIdentity.slug}-backend-dev`],
142
- ]);
143
- for (const [port, owner] of expected) {
144
- if ((await isPortOccupied(port)) && !onlineNames.has(owner)) {
145
- throw new Error(`Port ${port} is occupied by a process outside ${owner}`);
146
- }
147
- }
148
- }
149
-
150
- if (import.meta.main) {
151
- if (env.NODE_ENV !== 'development') {
152
- throw new Error('dev:lan is development-only');
153
- }
154
- const host = selectLanAddress(
155
- privateLanAddresses(networkInterfaces()),
156
- requestedHost(Bun.argv),
157
- );
158
- await assertDevelopmentPortsAvailable();
159
- const certificates = await ensureCertificates(host);
160
- const apiOrigin = `https://${host}:${env.API_PORT}`;
161
- const webOrigin = `https://${host}:${env.WEB_PORT}`;
162
- await runDevelopment({
163
- CORS_ORIGIN: webOrigin,
164
- DEV_HTTPS_CA: certificates.ca,
165
- DEV_HTTPS_CERT: certificates.cert,
166
- DEV_HTTPS_KEY: certificates.key,
167
- INTERNAL_API_URL: apiOrigin,
168
- NEXT_PUBLIC_API_URL: apiOrigin,
169
- NEXT_PUBLIC_WEB_URL: webOrigin,
170
- NODE_EXTRA_CA_CERTS: certificates.ca,
171
- });
172
- console.log(`${appIdentity.name} trusted LAN development is running`);
173
- console.log(`Web: ${webOrigin}/en`);
174
- console.log(`API: ${apiOrigin}`);
175
- console.log(`Device CA setup: ${apiOrigin}/__dev/lan`);
176
- }