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.
Files changed (62) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/VERSION +1 -0
  5. package/package.json +63 -0
  6. package/schemas/README.md +32 -0
  7. package/schemas/site.schema.json +5 -0
  8. package/schemas/theme.schema.json +5 -0
  9. package/schemas/v0/site.schema.json +172 -0
  10. package/schemas/v0/theme.schema.json +210 -0
  11. package/scripts/build-all.ts +121 -0
  12. package/scripts/build.ts +601 -0
  13. package/scripts/bundle.ts +781 -0
  14. package/scripts/dev.ts +777 -0
  15. package/scripts/generate-checksums.sh +78 -0
  16. package/scripts/release/export-release-key.sh +28 -0
  17. package/scripts/release/release-guard-tag-version.sh +79 -0
  18. package/scripts/release/sign-release-assets.sh +123 -0
  19. package/scripts/release/upload-release-assets.sh +76 -0
  20. package/scripts/release/upload-release-provenance.sh +52 -0
  21. package/scripts/release/verify-public-key.sh +48 -0
  22. package/scripts/release/verify-signatures.sh +117 -0
  23. package/scripts/version-sync.ts +82 -0
  24. package/src/__tests__/build.test.ts +240 -0
  25. package/src/__tests__/bundle.test.ts +786 -0
  26. package/src/__tests__/cli.test.ts +706 -0
  27. package/src/__tests__/crucible.test.ts +1043 -0
  28. package/src/__tests__/engine.test.ts +157 -0
  29. package/src/__tests__/init.test.ts +450 -0
  30. package/src/__tests__/pipeline.test.ts +1087 -0
  31. package/src/__tests__/productbook.test.ts +1206 -0
  32. package/src/__tests__/runbook.test.ts +974 -0
  33. package/src/__tests__/server-registry.test.ts +1251 -0
  34. package/src/__tests__/servicebook.test.ts +1248 -0
  35. package/src/__tests__/shared.test.ts +2005 -0
  36. package/src/__tests__/styles.test.ts +14 -0
  37. package/src/__tests__/theme-schema.test.ts +47 -0
  38. package/src/__tests__/theme.test.ts +554 -0
  39. package/src/cli.ts +582 -0
  40. package/src/commands/init.ts +92 -0
  41. package/src/commands/update.ts +444 -0
  42. package/src/engine.ts +20 -0
  43. package/src/logger.ts +15 -0
  44. package/src/migrations/0000_schema_versioning.ts +67 -0
  45. package/src/migrations/0001_server_port.ts +52 -0
  46. package/src/migrations/0002_brand_logo.ts +49 -0
  47. package/src/migrations/index.ts +26 -0
  48. package/src/migrations/schema.ts +24 -0
  49. package/src/server-registry.ts +405 -0
  50. package/src/shared.ts +1239 -0
  51. package/src/site/styles.css +931 -0
  52. package/src/site/template.html +193 -0
  53. package/src/templates/crucible.ts +1163 -0
  54. package/src/templates/driver.ts +876 -0
  55. package/src/templates/handbook.ts +339 -0
  56. package/src/templates/minimal.ts +139 -0
  57. package/src/templates/pipeline.ts +966 -0
  58. package/src/templates/productbook.ts +1032 -0
  59. package/src/templates/runbook.ts +829 -0
  60. package/src/templates/schema.ts +119 -0
  61. package/src/templates/servicebook.ts +1242 -0
  62. package/src/theme.ts +245 -0
package/src/cli.ts ADDED
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Kitfly CLI - Turn your writing into a website
4
+ *
5
+ * Minimal by design. One dependency. Understand in an afternoon.
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { readFileSync } from "node:fs";
10
+ import { arch, platform } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { loadSiteConfig } from "./shared.ts";
14
+
15
+ // Resolve paths relative to CLI location (works in binary too)
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const ROOT = join(__dirname, "..");
18
+
19
+ // Version: injected at compile time via --define, falls back to VERSION file
20
+ declare const __KITFLY_VERSION__: string | undefined;
21
+
22
+ function getVersion(): string {
23
+ if (typeof __KITFLY_VERSION__ !== "undefined") return __KITFLY_VERSION__;
24
+ try {
25
+ return readFileSync(join(ROOT, "VERSION"), "utf-8").trim();
26
+ } catch {
27
+ return "0.0.0";
28
+ }
29
+ }
30
+
31
+ // Get git info for extended version output
32
+ function getGitInfo(): { commit: string; branch: string; dirty: boolean } {
33
+ try {
34
+ const commit = execSync("git rev-parse --short HEAD", {
35
+ encoding: "utf-8",
36
+ stdio: ["pipe", "pipe", "pipe"],
37
+ }).trim();
38
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
39
+ encoding: "utf-8",
40
+ stdio: ["pipe", "pipe", "pipe"],
41
+ }).trim();
42
+ const status = execSync("git status --porcelain", {
43
+ encoding: "utf-8",
44
+ stdio: ["pipe", "pipe", "pipe"],
45
+ }).trim();
46
+ return { commit, branch, dirty: status.length > 0 };
47
+ } catch {
48
+ return { commit: "unknown", branch: "unknown", dirty: false };
49
+ }
50
+ }
51
+
52
+ // Print extended version info with provenance
53
+ function printVersionExtended(): void {
54
+ const git = getGitInfo();
55
+ const bunVersion = typeof Bun !== "undefined" ? Bun.version : "unknown";
56
+
57
+ console.log(`kitfly ${VERSION}`);
58
+ console.log(`Git commit: ${git.commit}`);
59
+ console.log(`Git branch: ${git.branch}`);
60
+ console.log(`Git status: ${git.dirty ? "dirty (uncommitted changes)" : "clean"}`);
61
+ console.log(`Bun: ${bunVersion}`);
62
+ console.log(`Platform: ${platform()}/${arch()}`);
63
+ }
64
+
65
+ const VERSION = getVersion();
66
+
67
+ const HELP = `
68
+ kitfly v${VERSION} - Turn your writing into a website
69
+
70
+ Usage:
71
+ kitfly dev [folder] Start dev server with hot reload
72
+ kitfly build [folder] Build static site to dist/
73
+ kitfly bundle [folder] Build single-file HTML bundle
74
+ kitfly init [name] Create new project from template
75
+ kitfly update [version] Update standalone site code
76
+ kitfly servers List running dev servers
77
+ kitfly stop <port|all> Stop dev server(s)
78
+ kitfly logs <port> View daemon server logs
79
+ kitfly version Show version (use 'version extended' for details)
80
+ kitfly help Show this help
81
+
82
+ Dev options:
83
+ --port <n> Server port [env: KITFLY_DEV_PORT] (default: 3333)
84
+ --host <h> Server host [env: KITFLY_DEV_HOST] (default: localhost)
85
+ --daemon, -d Run in background, return immediately
86
+ --json Output JSON (implies --daemon)
87
+ --no-open Don't open browser
88
+
89
+ Build/bundle options:
90
+ --out <dir> Output directory [env: KITFLY_BUILD_OUT] (default: dist)
91
+ --name <file> Bundle filename (default: bundle.html)
92
+ --no-raw Don't include raw markdown
93
+
94
+ Stop options:
95
+ --force Skip graceful shutdown, kill immediately
96
+
97
+ Logs options:
98
+ --follow, -f Follow log output (like tail -f)
99
+ --clean Remove log files for stopped servers
100
+
101
+ Update options:
102
+ --check Show current vs latest, no changes
103
+ --dry-run Show update plan, no changes
104
+ --force Overwrite modified managed files
105
+ --yes Non-interactive (assume yes)
106
+ --migrations-only Run config migrations only
107
+ --local Use local kitfly source (dev/offline)
108
+
109
+ Examples:
110
+ kitfly dev
111
+ kitfly dev ./my-docs --port 4000 --daemon
112
+ kitfly dev ./docs --json
113
+ kitfly servers
114
+ kitfly stop 4000
115
+ kitfly stop all
116
+ kitfly logs 3340
117
+ kitfly logs 3340 --follow
118
+ kitfly logs --clean
119
+ kitfly build ./docs --out ./public
120
+ kitfly init my-handbook
121
+ kitfly update --check
122
+
123
+ Documentation: https://kitfly.app
124
+ `;
125
+
126
+ // Simple arg parser
127
+ function parseArgs(args: string[]): {
128
+ positional: string[];
129
+ flags: Record<string, string | boolean>;
130
+ } {
131
+ const positional: string[] = [];
132
+ const flags: Record<string, string | boolean> = {};
133
+
134
+ for (let i = 0; i < args.length; i++) {
135
+ const arg = args[i];
136
+ if (arg.startsWith("--")) {
137
+ const key = arg.slice(2);
138
+ if (key.startsWith("no-")) {
139
+ flags[key.slice(3)] = false;
140
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
141
+ flags[key] = args[++i];
142
+ } else {
143
+ flags[key] = true;
144
+ }
145
+ } else if (arg.startsWith("-")) {
146
+ flags[arg.slice(1)] = true;
147
+ } else {
148
+ positional.push(arg);
149
+ }
150
+ }
151
+
152
+ return { positional, flags };
153
+ }
154
+
155
+ // Main
156
+ async function main() {
157
+ const [cmd, ...rest] = process.argv.slice(2);
158
+ const { positional, flags } = parseArgs(rest);
159
+
160
+ switch (cmd) {
161
+ case "dev":
162
+ case "serve": {
163
+ const folder = positional[0] || ".";
164
+ const portRaw = flags.port as string | undefined;
165
+
166
+ // Resolve absolute content root for registry (needed early for config loading)
167
+ const contentRoot = resolve(process.cwd(), folder);
168
+
169
+ // Resolve effective port following precedence: --port > env > site.yaml > default
170
+ let port: number;
171
+ if (portRaw) {
172
+ // Explicit --port flag always wins
173
+ port = parseInt(portRaw, 10);
174
+ // Validate port
175
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
176
+ console.error(`Error: Invalid port number\n`);
177
+ console.error(` "${portRaw}" is not a valid port number.`);
178
+ console.error(` Port must be a number between 1 and 65535.`);
179
+ process.exit(1);
180
+ }
181
+ } else {
182
+ // No explicit --port: resolve from env or site.yaml
183
+ const envPort = process.env.KITFLY_DEV_PORT;
184
+
185
+ // Try env var first
186
+ if (envPort) {
187
+ const parsed = parseInt(envPort, 10);
188
+ port = Number.isNaN(parsed) ? 3333 : parsed;
189
+ } else {
190
+ // Try site.yaml
191
+ try {
192
+ const siteConfig = await loadSiteConfig(contentRoot);
193
+ port = siteConfig?.server?.port ?? 3333;
194
+ } catch {
195
+ // site.yaml not found or invalid - use default
196
+ port = 3333;
197
+ }
198
+ }
199
+ }
200
+
201
+ const host = (flags.host as string) || "localhost";
202
+
203
+ // Warn if binding to all interfaces
204
+ if (host === "0.0.0.0" || host === "::") {
205
+ console.warn(`\x1b[33mWarning: Binding to all interfaces\x1b[0m`);
206
+ console.warn(` --host ${host} exposes the server to your network.`);
207
+ console.warn(` Use --host localhost for local-only access.\n`);
208
+ }
209
+
210
+ const open = flags.open !== false;
211
+ const daemon = flags.daemon === true || flags.d === true || flags.json === true;
212
+ const json = flags.json === true;
213
+
214
+ // Import registry for conflict checking
215
+ const {
216
+ checkPortConflict,
217
+ findPidOnPort,
218
+ findServerByPort,
219
+ registerServer,
220
+ getLogPath,
221
+ getKitflyHome,
222
+ } = await import("./server-registry.ts");
223
+
224
+ // Check for existing server on same port + content root
225
+ const existing = await findServerByPort(port);
226
+ if (existing && existing.contentRoot === contentRoot) {
227
+ // Same server already running - report and exit
228
+ if (json) {
229
+ console.log(
230
+ JSON.stringify({
231
+ status: "already_running",
232
+ pid: existing.pid,
233
+ port: existing.port,
234
+ url: `http://${existing.host === "0.0.0.0" ? "localhost" : existing.host}:${existing.port}`,
235
+ contentRoot: existing.contentRoot,
236
+ }),
237
+ );
238
+ } else {
239
+ console.log(`Server already running on port ${port} for ${contentRoot}`);
240
+ console.log(` PID: ${existing.pid}`);
241
+ console.log(
242
+ ` URL: http://${existing.host === "0.0.0.0" ? "localhost" : existing.host}:${existing.port}`,
243
+ );
244
+ }
245
+ process.exit(0);
246
+ }
247
+
248
+ // Check for port conflicts
249
+ const conflict = await checkPortConflict(port, contentRoot);
250
+ if (conflict) {
251
+ if (conflict.type === "kitfly") {
252
+ console.error(`Error: Port ${port} is already in use\n`);
253
+ console.error(` Another kitfly server is running:`);
254
+ console.error(` Content: ${conflict.contentRoot}`);
255
+ console.error(` PID: ${conflict.pid}`);
256
+ console.error(`\n Options:`);
257
+ console.error(` • Use a different port: kitfly dev ${folder} --port ${port + 1}`);
258
+ console.error(` • Stop the other server: kitfly stop ${port}`);
259
+ } else {
260
+ console.error(`Error: Port ${port} is in use by another process\n`);
261
+ console.error(` Process: ${conflict.processName || "unknown"} (PID ${conflict.pid})`);
262
+ console.error(`\n Choose a different port: kitfly dev ${folder} --port ${port + 1}`);
263
+ }
264
+ process.exit(1);
265
+ }
266
+
267
+ if (daemon) {
268
+ // Daemon mode: spawn detached process using shell redirection
269
+ const { mkdir, writeFile } = await import("node:fs/promises");
270
+ const logsDir = join(getKitflyHome(), "logs");
271
+ await mkdir(logsDir, { recursive: true });
272
+
273
+ const logPath = getLogPath(port);
274
+ const devScript = join(ROOT, "scripts/dev.ts");
275
+
276
+ // Truncate log file on each daemon start (log rotation)
277
+ await writeFile(logPath, "");
278
+
279
+ // Build command with shell redirection for logging
280
+ // Pass --log-format structured so dev.ts enables structured request logging
281
+ // Use nohup to prevent SIGHUP on terminal close
282
+ const shellCmd = `nohup bun run "${devScript}" "${folder}" --port ${port} --host "${host}" --no-open --log-format structured > "${logPath}" 2>&1 &`;
283
+
284
+ const proc = Bun.spawn(["sh", "-c", shellCmd], {
285
+ cwd: process.cwd(),
286
+ stdout: "ignore",
287
+ stderr: "ignore",
288
+ stdin: "ignore",
289
+ });
290
+
291
+ // Wait for shell to spawn the background process
292
+ await proc.exited;
293
+
294
+ // Give server a moment to start
295
+ await new Promise((resolve) => setTimeout(resolve, 1000));
296
+
297
+ // Find the actual server PID by checking what's listening on the port
298
+ const serverPid = findPidOnPort(port);
299
+
300
+ if (!serverPid) {
301
+ // Server didn't start - read log for error
302
+ const { readFile } = await import("node:fs/promises");
303
+ const logContent = await readFile(logPath, "utf-8").catch(() => "");
304
+ console.error("Error: Server failed to start");
305
+ if (logContent) {
306
+ console.error("\nLog output:");
307
+ console.error(logContent.slice(0, 500));
308
+ }
309
+ process.exit(1);
310
+ }
311
+
312
+ // Register server
313
+ await registerServer({
314
+ pid: serverPid,
315
+ port,
316
+ host,
317
+ contentRoot,
318
+ startTime: Date.now(),
319
+ kitflyVersion: VERSION,
320
+ daemonized: true,
321
+ });
322
+
323
+ const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
324
+
325
+ if (json) {
326
+ console.log(
327
+ JSON.stringify({
328
+ status: "started",
329
+ pid: serverPid,
330
+ port,
331
+ url,
332
+ contentRoot,
333
+ logFile: logPath,
334
+ }),
335
+ );
336
+ } else {
337
+ console.log(`Server started in background`);
338
+ console.log(` PID: ${serverPid}`);
339
+ console.log(` URL: ${url}`);
340
+ console.log(` Logs: ${logPath}`);
341
+ console.log(`\nTo stop: kitfly stop ${port}`);
342
+ }
343
+ } else {
344
+ // Foreground mode: run directly
345
+ const { dev } = await import("../scripts/dev.ts");
346
+ await dev({ folder, port, host, open });
347
+ }
348
+ break;
349
+ }
350
+
351
+ case "build": {
352
+ const folder = positional[0] || ".";
353
+ const out = (flags.out as string) || "dist";
354
+ const raw = flags.raw !== false; // --no-raw disables raw markdown
355
+ const { build } = await import("../scripts/build.ts");
356
+ await build({ folder, out, raw });
357
+ break;
358
+ }
359
+
360
+ case "bundle": {
361
+ const folder = positional[0] || ".";
362
+ const out = (flags.out as string) || "dist";
363
+ const name = (flags.name as string) || "bundle.html";
364
+ const raw = flags.raw !== false; // --no-raw disables raw markdown
365
+ const { bundleSite } = await import("../scripts/bundle.ts");
366
+ await bundleSite({ folder, out, name, raw });
367
+ break;
368
+ }
369
+
370
+ case "init": {
371
+ const name = positional[0];
372
+ if (!name) {
373
+ console.error("Error: Project name required\n");
374
+ console.error(
375
+ "Usage: kitfly init <name> [--template <type>] [--standalone] [--ai-assist] [--no-git]",
376
+ );
377
+ const { listTemplates } = await import("./templates/driver.ts");
378
+ const names = listTemplates()
379
+ .map((t) => t.id)
380
+ .join(", ");
381
+ console.error(`\nTemplates: ${names}`);
382
+ console.error("\nOptions:");
383
+ console.error(" --standalone Copy site code for self-contained operation");
384
+ console.error(" --ai-assist Add AI assistance instrumentation (AGENTS.md, roles)");
385
+ console.error(" --no-git Skip git initialization");
386
+ process.exit(1);
387
+ }
388
+ const { init } = await import("./commands/init.ts");
389
+ await init(name, {
390
+ template: (flags.template || flags.t) as string | undefined,
391
+ git: flags.git !== false && flags["no-git"] !== true,
392
+ standalone: flags.standalone === true,
393
+ aiAssist: flags["ai-assist"] === true || flags.aiAssist === true,
394
+ brand: flags.brand as string | undefined,
395
+ brandUrl: (flags["brand-url"] || flags.brandUrl) as string | undefined,
396
+ });
397
+ break;
398
+ }
399
+
400
+ case "update": {
401
+ const version = positional[0];
402
+ const { update } = await import("./commands/update.ts");
403
+ await update(version, {
404
+ check: flags.check === true,
405
+ dryRun: flags["dry-run"] === true || flags.dryRun === true,
406
+ force: flags.force === true,
407
+ yes: flags.yes === true,
408
+ migrationsOnly: flags["migrations-only"] === true || flags.migrationsOnly === true,
409
+ local: flags.local === true,
410
+ });
411
+ break;
412
+ }
413
+
414
+ case "servers":
415
+ case "ps": {
416
+ const { listServers, discoverOrphans } = await import("./server-registry.ts");
417
+ const servers = await listServers();
418
+ const json = flags.json === true;
419
+ const showAll = flags.all === true;
420
+
421
+ if (json) {
422
+ const orphans = showAll ? await discoverOrphans() : [];
423
+ console.log(JSON.stringify({ servers, orphans }));
424
+ } else if (servers.length === 0 && !showAll) {
425
+ console.log("No kitfly servers running");
426
+ } else {
427
+ if (servers.length > 0) {
428
+ console.log("PORT PID CONTENT ROOT");
429
+ console.log("─".repeat(60));
430
+ for (const s of servers) {
431
+ const portStr = String(s.port).padEnd(6);
432
+ const pidStr = String(s.pid).padEnd(8);
433
+ console.log(`${portStr} ${pidStr} ${s.contentRoot}`);
434
+ }
435
+ console.log(`\n${servers.length} server(s) running`);
436
+ } else {
437
+ console.log("No kitfly servers running");
438
+ }
439
+
440
+ if (showAll) {
441
+ const orphans = await discoverOrphans();
442
+ if (orphans.length > 0) {
443
+ console.log(`\nOrphaned kitfly processes (not in registry):`);
444
+ for (const o of orphans) {
445
+ console.log(` PID ${o.pid}: ${o.cmd}`);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ break;
451
+ }
452
+
453
+ case "stop": {
454
+ const target = positional[0];
455
+ if (!target) {
456
+ console.error("Error: Specify port number or 'all'\n");
457
+ console.error("Usage: kitfly stop <port|all>");
458
+ process.exit(1);
459
+ }
460
+
461
+ const { stopServer, stopAllServers } = await import("./server-registry.ts");
462
+ const force = flags.force === true;
463
+
464
+ if (target === "all") {
465
+ const result = await stopAllServers(force);
466
+ if (result.stopped === 0 && result.failed === 0 && result.orphans === 0) {
467
+ console.log("No servers to stop");
468
+ } else {
469
+ if (result.stopped > 0) {
470
+ console.log(`Stopped ${result.stopped} server(s)`);
471
+ }
472
+ if (result.failed > 0) {
473
+ console.log(`Failed to stop ${result.failed} server(s)`);
474
+ }
475
+ if (result.orphans > 0) {
476
+ console.log(`Stopped ${result.orphans} orphaned process(es)`);
477
+ }
478
+ }
479
+ } else {
480
+ const port = parseInt(target, 10);
481
+ if (Number.isNaN(port)) {
482
+ console.error(`Error: Invalid port number: ${target}`);
483
+ process.exit(1);
484
+ }
485
+ const result = await stopServer(port, force);
486
+ if (result.success) {
487
+ console.log(result.message);
488
+ } else {
489
+ console.error(`Error: ${result.message}`);
490
+ process.exit(1);
491
+ }
492
+ }
493
+ break;
494
+ }
495
+
496
+ case "logs": {
497
+ const { getLogPath, cleanLogs } = await import("./server-registry.ts");
498
+
499
+ if (flags.clean === true) {
500
+ const removed = await cleanLogs();
501
+ if (removed.length === 0) {
502
+ console.log("No stale log files to clean");
503
+ } else {
504
+ for (const f of removed) {
505
+ console.log(`Removed: ${f}`);
506
+ }
507
+ console.log(`\nCleaned ${removed.length} stale log file(s)`);
508
+ }
509
+ break;
510
+ }
511
+
512
+ const portStr = positional[0];
513
+ if (!portStr) {
514
+ console.error("Error: Specify port number or --clean\n");
515
+ console.error("Usage: kitfly logs <port> [--follow] | kitfly logs --clean");
516
+ process.exit(1);
517
+ }
518
+
519
+ const logPort = parseInt(portStr, 10);
520
+ if (Number.isNaN(logPort)) {
521
+ console.error(`Error: Invalid port number: ${portStr}`);
522
+ process.exit(1);
523
+ }
524
+
525
+ const logFile = getLogPath(logPort);
526
+ const follow = flags.follow === true || flags.f === true;
527
+
528
+ if (follow) {
529
+ // tail -f equivalent using Bun.spawn
530
+ const proc = Bun.spawn(["tail", "-f", logFile], {
531
+ stdout: "inherit",
532
+ stderr: "inherit",
533
+ });
534
+ await proc.exited;
535
+ } else {
536
+ const { readFile } = await import("node:fs/promises");
537
+ try {
538
+ const content = await readFile(logFile, "utf-8");
539
+ if (content.length === 0) {
540
+ console.log(`Log file is empty: ${logFile}`);
541
+ } else {
542
+ process.stdout.write(content);
543
+ }
544
+ } catch {
545
+ console.error(`No log file found for port ${logPort}`);
546
+ console.error(` Expected: ${logFile}`);
547
+ process.exit(1);
548
+ }
549
+ }
550
+ break;
551
+ }
552
+
553
+ case "version":
554
+ case "-v":
555
+ case "--version": {
556
+ const extended = positional.includes("extended") || flags.extended;
557
+ if (extended) {
558
+ printVersionExtended();
559
+ } else {
560
+ console.log(VERSION);
561
+ }
562
+ break;
563
+ }
564
+
565
+ case "help":
566
+ case "-h":
567
+ case "--help":
568
+ case undefined:
569
+ console.log(HELP);
570
+ break;
571
+
572
+ default:
573
+ console.error(`Unknown command: ${cmd}\n`);
574
+ console.log(HELP);
575
+ process.exit(1);
576
+ }
577
+ }
578
+
579
+ main().catch((err) => {
580
+ console.error(err.message || err);
581
+ process.exit(1);
582
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * kitfly init - Create a new site from template
3
+ *
4
+ * Usage:
5
+ * kitfly init <name> [--template <type>] [--no-git]
6
+ *
7
+ * Templates: minimal, handbook (more coming)
8
+ */
9
+
10
+ import { readdir, stat } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { getTemplate, listTemplates, runTemplate } from "../templates/driver.ts";
13
+ import type { InitOptions } from "../templates/schema.ts";
14
+
15
+ export interface InitFlags {
16
+ template?: string;
17
+ git?: boolean;
18
+ prompt?: boolean;
19
+ standalone?: boolean; // Copy site code for self-contained operation
20
+ aiAssist?: boolean; // Add AI assistance instrumentation
21
+ // Branding overrides
22
+ brand?: string;
23
+ brandUrl?: string;
24
+ }
25
+
26
+ export async function init(name: string, flags: InitFlags = {}) {
27
+ const dest = join(process.cwd(), name);
28
+
29
+ // Check if directory exists and is non-empty
30
+ try {
31
+ const stats = await stat(dest);
32
+ if (stats.isDirectory()) {
33
+ const contents = await readdir(dest);
34
+ if (contents.length > 0) {
35
+ console.error(`Error: Directory '${name}' exists and is not empty`);
36
+ process.exit(1);
37
+ }
38
+ // Directory exists but is empty - that's fine
39
+ console.log(`Using existing empty directory: ${name}/`);
40
+ }
41
+ } catch {
42
+ // Directory doesn't exist - that's fine, will be created
43
+ }
44
+
45
+ // Validate template
46
+ const templateId = flags.template || "minimal";
47
+ const template = getTemplate(templateId);
48
+
49
+ if (!template) {
50
+ console.error(`Error: Unknown template '${templateId}'`);
51
+ console.error(`\nAvailable templates:`);
52
+ for (const t of listTemplates()) {
53
+ console.error(` ${t.id.padEnd(12)} - ${t.description}`);
54
+ }
55
+ process.exit(1);
56
+ }
57
+
58
+ // Build options
59
+ const options: InitOptions = {
60
+ name,
61
+ template: templateId,
62
+ git: flags.git,
63
+ prompt: flags.prompt,
64
+ standalone: flags.standalone,
65
+ aiAssist: flags.aiAssist,
66
+ branding: {},
67
+ };
68
+
69
+ // Apply branding overrides from flags
70
+ const branding = options.branding ?? {};
71
+ if (flags.brand) {
72
+ branding.brandName = flags.brand;
73
+ branding.siteName = flags.brand;
74
+ }
75
+ if (flags.brandUrl) {
76
+ branding.brandUrl = flags.brandUrl;
77
+ }
78
+ options.branding = branding;
79
+
80
+ // Run the template
81
+ await runTemplate(options);
82
+ }
83
+
84
+ /**
85
+ * List available templates
86
+ */
87
+ export function listAvailableTemplates(): void {
88
+ console.log("Available templates:\n");
89
+ for (const t of listTemplates()) {
90
+ console.log(` ${t.id.padEnd(12)} - ${t.description}`);
91
+ }
92
+ }