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
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
|
+
}
|