kitfly 0.1.2
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/LICENSE +21 -0
- package/README.md +136 -0
- package/VERSION +1 -0
- package/package.json +63 -0
- package/schemas/README.md +32 -0
- package/schemas/site.schema.json +5 -0
- package/schemas/theme.schema.json +5 -0
- package/schemas/v0/site.schema.json +172 -0
- package/schemas/v0/theme.schema.json +210 -0
- package/scripts/build-all.ts +121 -0
- package/scripts/build.ts +601 -0
- package/scripts/bundle.ts +781 -0
- package/scripts/dev.ts +777 -0
- package/scripts/generate-checksums.sh +78 -0
- package/scripts/release/export-release-key.sh +28 -0
- package/scripts/release/release-guard-tag-version.sh +79 -0
- package/scripts/release/sign-release-assets.sh +123 -0
- package/scripts/release/upload-release-assets.sh +76 -0
- package/scripts/release/upload-release-provenance.sh +52 -0
- package/scripts/release/verify-public-key.sh +48 -0
- package/scripts/release/verify-signatures.sh +117 -0
- package/scripts/version-sync.ts +82 -0
- package/src/__tests__/build.test.ts +240 -0
- package/src/__tests__/bundle.test.ts +786 -0
- package/src/__tests__/cli.test.ts +706 -0
- package/src/__tests__/crucible.test.ts +1043 -0
- package/src/__tests__/engine.test.ts +157 -0
- package/src/__tests__/init.test.ts +450 -0
- package/src/__tests__/pipeline.test.ts +1087 -0
- package/src/__tests__/productbook.test.ts +1206 -0
- package/src/__tests__/runbook.test.ts +974 -0
- package/src/__tests__/server-registry.test.ts +1251 -0
- package/src/__tests__/servicebook.test.ts +1248 -0
- package/src/__tests__/shared.test.ts +2005 -0
- package/src/__tests__/styles.test.ts +14 -0
- package/src/__tests__/theme-schema.test.ts +47 -0
- package/src/__tests__/theme.test.ts +554 -0
- package/src/cli.ts +582 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/update.ts +444 -0
- package/src/engine.ts +20 -0
- package/src/logger.ts +15 -0
- package/src/migrations/0000_schema_versioning.ts +67 -0
- package/src/migrations/0001_server_port.ts +52 -0
- package/src/migrations/0002_brand_logo.ts +49 -0
- package/src/migrations/index.ts +26 -0
- package/src/migrations/schema.ts +24 -0
- package/src/server-registry.ts +405 -0
- package/src/shared.ts +1239 -0
- package/src/site/styles.css +931 -0
- package/src/site/template.html +193 -0
- package/src/templates/crucible.ts +1163 -0
- package/src/templates/driver.ts +876 -0
- package/src/templates/handbook.ts +339 -0
- package/src/templates/minimal.ts +139 -0
- package/src/templates/pipeline.ts +966 -0
- package/src/templates/productbook.ts +1032 -0
- package/src/templates/runbook.ts +829 -0
- package/src/templates/schema.ts +119 -0
- package/src/templates/servicebook.ts +1242 -0
- package/src/theme.ts +245 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Registry - Track running kitfly dev servers
|
|
3
|
+
*
|
|
4
|
+
* Registry location: ~/.kitfly/servers.json
|
|
5
|
+
*
|
|
6
|
+
* Used by:
|
|
7
|
+
* - kitfly dev --daemon (register new server)
|
|
8
|
+
* - kitfly servers (list running)
|
|
9
|
+
* - kitfly stop (terminate server)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface ServerEntry {
|
|
21
|
+
pid: number;
|
|
22
|
+
port: number;
|
|
23
|
+
host: string;
|
|
24
|
+
contentRoot: string;
|
|
25
|
+
startTime: number;
|
|
26
|
+
kitflyVersion: string;
|
|
27
|
+
daemonized: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ServerRegistry {
|
|
31
|
+
version: 1;
|
|
32
|
+
servers: ServerEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Paths
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const KITFLY_HOME = join(homedir(), ".kitfly");
|
|
40
|
+
const REGISTRY_PATH = join(KITFLY_HOME, "servers.json");
|
|
41
|
+
const LOGS_DIR = join(KITFLY_HOME, "logs");
|
|
42
|
+
|
|
43
|
+
export function getKitflyHome(): string {
|
|
44
|
+
return KITFLY_HOME;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getLogsDir(): string {
|
|
48
|
+
return LOGS_DIR;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getLogPath(port: number): string {
|
|
52
|
+
return join(LOGS_DIR, `${port}.log`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Registry I/O
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
async function ensureKitflyHome(): Promise<void> {
|
|
60
|
+
await mkdir(KITFLY_HOME, { recursive: true });
|
|
61
|
+
await mkdir(LOGS_DIR, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readRegistry(): Promise<ServerRegistry> {
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(REGISTRY_PATH, "utf-8");
|
|
67
|
+
return JSON.parse(content) as ServerRegistry;
|
|
68
|
+
} catch {
|
|
69
|
+
return { version: 1, servers: [] };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function writeRegistry(registry: ServerRegistry): Promise<void> {
|
|
74
|
+
await ensureKitflyHome();
|
|
75
|
+
await writeFile(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Process utilities (via sysprims)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
import { listeningPorts, processList, procGet } from "@3leaps/sysprims";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find PID listening on a port.
|
|
86
|
+
* When multiple processes bind the same port (e.g., nohup shell + bun child),
|
|
87
|
+
* prefer the bun process over shell wrappers.
|
|
88
|
+
*/
|
|
89
|
+
export function findPidOnPort(port: number): number | null {
|
|
90
|
+
const result = listeningPorts({ local_port: port });
|
|
91
|
+
if (result.bindings.length === 0) return null;
|
|
92
|
+
if (result.bindings.length === 1) return result.bindings[0].pid ?? null;
|
|
93
|
+
|
|
94
|
+
// Multiple bindings — prefer the bun process over shell wrapper
|
|
95
|
+
for (const binding of result.bindings) {
|
|
96
|
+
if (binding.process?.name.includes("bun")) {
|
|
97
|
+
return binding.pid ?? null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Fallback: last binding (child is usually listed after parent)
|
|
101
|
+
return result.bindings[result.bindings.length - 1].pid ?? null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get process info
|
|
106
|
+
*/
|
|
107
|
+
function getProcessInfo(pid: number): { name: string } | null {
|
|
108
|
+
const proc = procGet(pid);
|
|
109
|
+
return proc ? { name: proc.name } : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Process validation
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async function isProcessAlive(entry: ServerEntry): Promise<boolean> {
|
|
117
|
+
// Never signal pid <= 0 — POSIX kill(0) signals the process group,
|
|
118
|
+
// kill(-1) signals every process owned by the user
|
|
119
|
+
if (entry.pid <= 0) return false;
|
|
120
|
+
try {
|
|
121
|
+
process.kill(entry.pid, 0);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function isPortBoundByProcess(port: number, pid: number): Promise<boolean> {
|
|
129
|
+
const actualPid = await findPidOnPort(port);
|
|
130
|
+
if (actualPid === null) return false;
|
|
131
|
+
return actualPid === pid;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Registry operations
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Clean stale entries from registry
|
|
140
|
+
*/
|
|
141
|
+
export async function cleanRegistry(): Promise<ServerEntry[]> {
|
|
142
|
+
const registry = await readRegistry();
|
|
143
|
+
const alive: ServerEntry[] = [];
|
|
144
|
+
const removed: ServerEntry[] = [];
|
|
145
|
+
|
|
146
|
+
for (const entry of registry.servers) {
|
|
147
|
+
const processAlive = await isProcessAlive(entry);
|
|
148
|
+
const portBound = processAlive && (await isPortBoundByProcess(entry.port, entry.pid));
|
|
149
|
+
|
|
150
|
+
if (processAlive && portBound) {
|
|
151
|
+
alive.push(entry);
|
|
152
|
+
} else {
|
|
153
|
+
removed.push(entry);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (removed.length > 0) {
|
|
158
|
+
await writeRegistry({ version: 1, servers: alive });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return alive;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all registered servers (after cleaning stale entries)
|
|
166
|
+
*/
|
|
167
|
+
export async function listServers(): Promise<ServerEntry[]> {
|
|
168
|
+
return cleanRegistry();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find server by port
|
|
173
|
+
*/
|
|
174
|
+
export async function findServerByPort(port: number): Promise<ServerEntry | null> {
|
|
175
|
+
const servers = await listServers();
|
|
176
|
+
return servers.find((s) => s.port === port) ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Find server by content root
|
|
181
|
+
*/
|
|
182
|
+
export async function findServerByContentRoot(contentRoot: string): Promise<ServerEntry | null> {
|
|
183
|
+
const servers = await listServers();
|
|
184
|
+
return servers.find((s) => s.contentRoot === contentRoot) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Register a new server
|
|
189
|
+
*/
|
|
190
|
+
export async function registerServer(entry: ServerEntry): Promise<void> {
|
|
191
|
+
const servers = await listServers();
|
|
192
|
+
// Remove any existing entry for same port (shouldn't happen, but be safe)
|
|
193
|
+
const filtered = servers.filter((s) => s.port !== entry.port);
|
|
194
|
+
filtered.push(entry);
|
|
195
|
+
await writeRegistry({ version: 1, servers: filtered });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Unregister a server by port
|
|
200
|
+
*/
|
|
201
|
+
export async function unregisterServer(port: number): Promise<boolean> {
|
|
202
|
+
const registry = await readRegistry();
|
|
203
|
+
const before = registry.servers.length;
|
|
204
|
+
registry.servers = registry.servers.filter((s) => s.port !== port);
|
|
205
|
+
if (registry.servers.length < before) {
|
|
206
|
+
await writeRegistry(registry);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Port conflict detection
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export interface PortConflict {
|
|
217
|
+
type: "kitfly" | "other";
|
|
218
|
+
port: number;
|
|
219
|
+
pid: number;
|
|
220
|
+
processName?: string;
|
|
221
|
+
contentRoot?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if a port is available or identify what's using it
|
|
226
|
+
*/
|
|
227
|
+
export async function checkPortConflict(
|
|
228
|
+
port: number,
|
|
229
|
+
contentRoot: string,
|
|
230
|
+
): Promise<PortConflict | null> {
|
|
231
|
+
// First check our registry
|
|
232
|
+
const existingServer = await findServerByPort(port);
|
|
233
|
+
if (existingServer) {
|
|
234
|
+
// Same content root = can reuse
|
|
235
|
+
if (existingServer.contentRoot === contentRoot) {
|
|
236
|
+
return null; // No conflict - same server
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
type: "kitfly",
|
|
240
|
+
port,
|
|
241
|
+
pid: existingServer.pid,
|
|
242
|
+
contentRoot: existingServer.contentRoot,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if port is in use by another process using lsof
|
|
247
|
+
const pid = await findPidOnPort(port);
|
|
248
|
+
if (pid !== null) {
|
|
249
|
+
const proc = await getProcessInfo(pid);
|
|
250
|
+
return {
|
|
251
|
+
type: "other",
|
|
252
|
+
port,
|
|
253
|
+
pid,
|
|
254
|
+
processName: proc?.name,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Server lifecycle
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Stop a server by port
|
|
267
|
+
*/
|
|
268
|
+
export async function stopServer(
|
|
269
|
+
port: number,
|
|
270
|
+
force = false,
|
|
271
|
+
): Promise<{ success: boolean; message: string }> {
|
|
272
|
+
const server = await findServerByPort(port);
|
|
273
|
+
if (!server) {
|
|
274
|
+
return { success: false, message: `No server running on port ${port}` };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Never signal pid <= 0 — remove the bad entry instead
|
|
278
|
+
if (server.pid <= 0) {
|
|
279
|
+
await unregisterServer(port);
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
message: `Removed invalid registry entry for port ${port} (pid: ${server.pid})`,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Send signal
|
|
288
|
+
process.kill(server.pid, force ? "SIGKILL" : "SIGTERM");
|
|
289
|
+
|
|
290
|
+
// If graceful, wait a bit then force if still running
|
|
291
|
+
if (!force) {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
293
|
+
try {
|
|
294
|
+
process.kill(server.pid, 0); // Check if still alive
|
|
295
|
+
// Still alive - wait more
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
297
|
+
try {
|
|
298
|
+
process.kill(server.pid, 0);
|
|
299
|
+
// Still alive after 3s - force kill
|
|
300
|
+
process.kill(server.pid, "SIGKILL");
|
|
301
|
+
} catch {
|
|
302
|
+
// Dead now
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Dead
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
// ESRCH means process doesn't exist - that's fine
|
|
310
|
+
if ((err as NodeJS.ErrnoException).code !== "ESRCH") {
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
message: `Failed to stop server (PID ${server.pid}): ${err}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await unregisterServer(port);
|
|
319
|
+
return { success: true, message: `Stopped server on port ${port}` };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Discover kitfly dev processes not tracked in the registry.
|
|
324
|
+
* Finds bun processes whose cmdline includes "scripts/dev.ts".
|
|
325
|
+
*/
|
|
326
|
+
export async function discoverOrphans(): Promise<Array<{ pid: number; cmd: string }>> {
|
|
327
|
+
const snapshot = processList({ name_contains: "bun" });
|
|
328
|
+
const registered = await listServers();
|
|
329
|
+
const registeredPids = new Set(registered.map((s) => s.pid));
|
|
330
|
+
|
|
331
|
+
return snapshot.processes
|
|
332
|
+
.filter((p) => {
|
|
333
|
+
if (registeredPids.has(p.pid)) return false;
|
|
334
|
+
return p.cmdline.some((arg) => arg.includes("scripts/dev.ts"));
|
|
335
|
+
})
|
|
336
|
+
.map((p) => ({ pid: p.pid, cmd: p.cmdline.join(" ") }));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Stop all servers, then sweep for orphaned kitfly processes
|
|
341
|
+
*/
|
|
342
|
+
export async function stopAllServers(
|
|
343
|
+
force = false,
|
|
344
|
+
): Promise<{ stopped: number; failed: number; orphans: number }> {
|
|
345
|
+
const servers = await listServers();
|
|
346
|
+
let stopped = 0;
|
|
347
|
+
let failed = 0;
|
|
348
|
+
|
|
349
|
+
for (const server of servers) {
|
|
350
|
+
const result = await stopServer(server.port, force);
|
|
351
|
+
if (result.success) {
|
|
352
|
+
stopped++;
|
|
353
|
+
} else {
|
|
354
|
+
failed++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Sweep for orphaned kitfly processes not in the registry
|
|
359
|
+
let orphans = 0;
|
|
360
|
+
const orphanList = await discoverOrphans();
|
|
361
|
+
for (const orphan of orphanList) {
|
|
362
|
+
try {
|
|
363
|
+
process.kill(orphan.pid, force ? "SIGKILL" : "SIGTERM");
|
|
364
|
+
orphans++;
|
|
365
|
+
} catch {
|
|
366
|
+
// Already dead or permission denied
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { stopped, failed, orphans };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Log cleanup
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Remove log files for ports not in the server registry.
|
|
379
|
+
* Returns list of removed log file paths.
|
|
380
|
+
*/
|
|
381
|
+
export async function cleanLogs(): Promise<string[]> {
|
|
382
|
+
const servers = await listServers();
|
|
383
|
+
const activePorts = new Set(servers.map((s) => s.port));
|
|
384
|
+
const removed: string[] = [];
|
|
385
|
+
|
|
386
|
+
let entries: string[];
|
|
387
|
+
try {
|
|
388
|
+
entries = await readdir(LOGS_DIR);
|
|
389
|
+
} catch {
|
|
390
|
+
return removed; // No logs dir — nothing to clean
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const entry of entries) {
|
|
394
|
+
const match = entry.match(/^(\d+)\.log$/);
|
|
395
|
+
if (!match) continue;
|
|
396
|
+
const port = parseInt(match[1], 10);
|
|
397
|
+
if (!activePorts.has(port)) {
|
|
398
|
+
const logPath = join(LOGS_DIR, entry);
|
|
399
|
+
await unlink(logPath).catch(() => {});
|
|
400
|
+
removed.push(logPath);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return removed;
|
|
405
|
+
}
|