create-stitchkit 0.2.0 → 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.
- package/CHANGELOG.md +60 -0
- package/dist/cli.js +7 -16
- package/examples/repository/e2e/repository.spec.ts +50 -0
- package/examples/repository/packages/backend/src/domain/repository/github-cache.test.ts +8 -0
- package/examples/repository/packages/backend/src/surface.snapshot.json +58 -0
- package/examples/repository/packages/backend/src/surface.ts +7 -5
- package/examples/repository/packages/frontend/src/components/repository-summary.tsx +5 -1
- package/examples/repository/packages/frontend/src/lib/realtime/repository.ts +5 -6
- package/examples/repository/packages/shared/src/index.ts +3 -1
- package/examples/repository/packages/shared/src/realtime/repository.ts +10 -0
- package/examples/repository/packages/shared/src/schemas/repository.ts +4 -2
- package/examples/repository/scripts/runtime-smoke.ts +96 -18
- package/package.json +1 -1
- package/template/README.md +6 -10
- package/template/_env.example +1 -1
- package/template/bun.lock +4 -4
- package/template/docs/ADDING_A_FEATURE.md +3 -2
- package/template/e2e/starter.spec.ts +17 -0
- package/template/ecosystem.dev.config.cjs +0 -12
- package/template/package.json +5 -3
- package/template/packages/backend/src/index.ts +1 -15
- package/template/packages/backend/src/surface-manifest.test.ts +183 -8
- package/template/packages/backend/src/surface-manifest.ts +108 -5
- package/template/packages/backend/src/surface.snapshot.json +18 -0
- package/template/packages/backend/src/surface.ts +2 -1
- package/template/packages/backend/src/tools.ts +4 -1
- package/template/packages/backend/src/transport/errors.ts +1 -0
- package/template/packages/backend/src/transport/system-service.ts +8 -0
- package/template/packages/config/src/server.ts +1 -4
- package/template/packages/frontend/package.json +0 -1
- package/template/packages/frontend/src/lib/query-client.test.ts +22 -0
- package/template/packages/frontend/src/lib/query-client.ts +10 -2
- package/template/packages/frontend/tsconfig.json +7 -1
- package/template/packages/shared/package.json +0 -1
- package/template/packages/shared/src/contracts/system.ts +19 -0
- package/template/packages/shared/src/index.ts +2 -1
- package/template/packages/shared/src/schemas/system.ts +4 -0
- package/template/playwright.config.ts +2 -1
- package/template/scripts/check-authored.ts +20 -6
- package/template/scripts/dev.ts +19 -10
- package/template/scripts/local-env.test.ts +33 -0
- package/template/scripts/local-env.ts +16 -5
- package/template/scripts/runtime-smoke.ts +12 -5
- package/template/scripts/surface-conformance.ts +76 -15
- package/template/scripts/surface-snapshot.ts +21 -0
- package/template/scripts/tooling-env.ts +16 -3
- package/template/tsconfig.json +3 -1
- package/examples/repository/_env.append +0 -3
- package/examples/repository/packages/shared/src/events/repository.ts +0 -9
- package/template/_env +0 -9
- package/template/docs/LAN_HTTPS.md +0 -28
- package/template/packages/backend/src/transport/lan-onboarding.ts +0 -29
- package/template/scripts/dev-lan.test.ts +0 -58
- package/template/scripts/dev-lan.ts +0 -176
|
@@ -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
|
-
}
|