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,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Driver - Base Operations
|
|
3
|
+
*
|
|
4
|
+
* Provides common operations for template generation:
|
|
5
|
+
* - Directory creation
|
|
6
|
+
* - File writing with template expansion
|
|
7
|
+
* - Git initialization
|
|
8
|
+
* - Standalone mode (copy site code)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { crucible } from "./crucible.ts";
|
|
15
|
+
import { handbook } from "./handbook.ts";
|
|
16
|
+
import { minimal } from "./minimal.ts";
|
|
17
|
+
import { pipeline } from "./pipeline.ts";
|
|
18
|
+
import { productbook } from "./productbook.ts";
|
|
19
|
+
import { runbook } from "./runbook.ts";
|
|
20
|
+
import type {
|
|
21
|
+
BrandingConfig,
|
|
22
|
+
InitOptions,
|
|
23
|
+
SiteManifest,
|
|
24
|
+
StandaloneProvenance,
|
|
25
|
+
TemplateContext,
|
|
26
|
+
TemplateDef,
|
|
27
|
+
TemplateFile,
|
|
28
|
+
TemplateRegistry,
|
|
29
|
+
} from "./schema.ts";
|
|
30
|
+
import { servicebook } from "./servicebook.ts";
|
|
31
|
+
|
|
32
|
+
// Resolve kitfly root
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const KITFLY_ROOT = join(__dirname, "../..");
|
|
35
|
+
|
|
36
|
+
// -----------------------------------------------------------------------------
|
|
37
|
+
// Template Registry
|
|
38
|
+
// -----------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const templates: TemplateRegistry = new Map();
|
|
41
|
+
|
|
42
|
+
export function registerTemplate(template: TemplateDef): void {
|
|
43
|
+
templates.set(template.id, template);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getTemplate(id: string): TemplateDef | undefined {
|
|
47
|
+
return templates.get(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function listTemplates(): TemplateDef[] {
|
|
51
|
+
return Array.from(templates.values());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Register built-in templates
|
|
55
|
+
registerTemplate(minimal);
|
|
56
|
+
registerTemplate(handbook);
|
|
57
|
+
registerTemplate(pipeline);
|
|
58
|
+
registerTemplate(productbook);
|
|
59
|
+
registerTemplate(runbook);
|
|
60
|
+
registerTemplate(servicebook);
|
|
61
|
+
registerTemplate(crucible);
|
|
62
|
+
|
|
63
|
+
// -----------------------------------------------------------------------------
|
|
64
|
+
// File Operations
|
|
65
|
+
// -----------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export async function ensureDir(path: string): Promise<void> {
|
|
68
|
+
await mkdir(path, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function writeTemplateFile(
|
|
72
|
+
root: string,
|
|
73
|
+
file: TemplateFile,
|
|
74
|
+
ctx: TemplateContext,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const filePath = join(root, file.path);
|
|
77
|
+
await ensureDir(dirname(filePath));
|
|
78
|
+
|
|
79
|
+
const content = typeof file.content === "function" ? file.content(ctx) : file.content;
|
|
80
|
+
|
|
81
|
+
await writeFile(filePath, content, "utf-8");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// -----------------------------------------------------------------------------
|
|
85
|
+
// Git Operations
|
|
86
|
+
// -----------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
export async function initGit(root: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
const proc = Bun.spawn(["git", "init"], {
|
|
91
|
+
cwd: root,
|
|
92
|
+
stdout: "pipe",
|
|
93
|
+
stderr: "pipe",
|
|
94
|
+
});
|
|
95
|
+
await proc.exited;
|
|
96
|
+
return proc.exitCode === 0;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function gitCommit(root: string, message: string): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
const add = Bun.spawn(["git", "add", "-A"], { cwd: root, stdout: "ignore", stderr: "ignore" });
|
|
105
|
+
await add.exited;
|
|
106
|
+
|
|
107
|
+
const commit = Bun.spawn(["git", "commit", "-m", message], {
|
|
108
|
+
cwd: root,
|
|
109
|
+
stdout: "ignore",
|
|
110
|
+
stderr: "ignore",
|
|
111
|
+
});
|
|
112
|
+
await commit.exited;
|
|
113
|
+
return commit.exitCode === 0;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// -----------------------------------------------------------------------------
|
|
120
|
+
// Branding Defaults
|
|
121
|
+
// -----------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export function defaultBranding(name: string): BrandingConfig {
|
|
124
|
+
const titleCase = name
|
|
125
|
+
.split(/[-_]/)
|
|
126
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
127
|
+
.join(" ");
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
siteName: titleCase,
|
|
131
|
+
brandName: titleCase,
|
|
132
|
+
brandUrl: "/",
|
|
133
|
+
primaryColor: "#2563eb",
|
|
134
|
+
footerText: `© ${new Date().getFullYear()} ${titleCase}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// -----------------------------------------------------------------------------
|
|
139
|
+
// Standalone Mode - Files to Copy
|
|
140
|
+
// -----------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
const STANDALONE_FILES = [
|
|
143
|
+
// Core scripts
|
|
144
|
+
"scripts/dev.ts",
|
|
145
|
+
"scripts/build.ts",
|
|
146
|
+
"scripts/bundle.ts",
|
|
147
|
+
// Shared code
|
|
148
|
+
"src/shared.ts",
|
|
149
|
+
"src/engine.ts",
|
|
150
|
+
"src/theme.ts",
|
|
151
|
+
// Schemas (editor validation + migrations)
|
|
152
|
+
"schemas/README.md",
|
|
153
|
+
"schemas/site.schema.json",
|
|
154
|
+
"schemas/theme.schema.json",
|
|
155
|
+
"schemas/v0/site.schema.json",
|
|
156
|
+
"schemas/v0/theme.schema.json",
|
|
157
|
+
// Site templates and assets
|
|
158
|
+
"src/site/template.html",
|
|
159
|
+
"src/site/styles.css",
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Directories to copy entirely
|
|
163
|
+
const STANDALONE_DIRS = ["assets"];
|
|
164
|
+
|
|
165
|
+
function toArrayBuffer(data: Uint8Array): ArrayBuffer {
|
|
166
|
+
// Normalize to a standalone ArrayBuffer to satisfy SubtleCrypto typing.
|
|
167
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function hashBytes(data: Uint8Array): Promise<string> {
|
|
171
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", toArrayBuffer(data));
|
|
172
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
173
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function hashContent(content: string): Promise<string> {
|
|
177
|
+
return hashBytes(new TextEncoder().encode(content));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function getKitflyVersion(): Promise<string> {
|
|
181
|
+
try {
|
|
182
|
+
const versionFile = join(KITFLY_ROOT, "VERSION");
|
|
183
|
+
return (await readFile(versionFile, "utf-8")).trim();
|
|
184
|
+
} catch {
|
|
185
|
+
return "0.0.0";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function copyStandaloneFiles(
|
|
190
|
+
root: string,
|
|
191
|
+
ctx: TemplateContext,
|
|
192
|
+
): Promise<StandaloneProvenance> {
|
|
193
|
+
const provenance: StandaloneProvenance = {
|
|
194
|
+
kitflyVersion: await getKitflyVersion(),
|
|
195
|
+
createdAt: new Date().toISOString(),
|
|
196
|
+
template: ctx.template.id,
|
|
197
|
+
files: [],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Copy individual files
|
|
201
|
+
for (const relPath of STANDALONE_FILES) {
|
|
202
|
+
const srcPath = join(KITFLY_ROOT, relPath);
|
|
203
|
+
const destPath = join(root, relPath);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const content = await readFile(srcPath, "utf-8");
|
|
207
|
+
await ensureDir(dirname(destPath));
|
|
208
|
+
await writeFile(destPath, content, "utf-8");
|
|
209
|
+
const localHash = await hashContent(content);
|
|
210
|
+
|
|
211
|
+
provenance.files.push({
|
|
212
|
+
path: relPath,
|
|
213
|
+
sourceHash: localHash,
|
|
214
|
+
localHash,
|
|
215
|
+
modified: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
console.log(` + ${relPath} (standalone)`);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.warn(` ! Could not copy ${relPath}: ${e}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Copy asset directories
|
|
225
|
+
for (const dirName of STANDALONE_DIRS) {
|
|
226
|
+
const srcDir = join(KITFLY_ROOT, dirName);
|
|
227
|
+
const destDir = join(root, dirName);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await copyDir(srcDir, destDir, provenance);
|
|
231
|
+
} catch {
|
|
232
|
+
// Directory may not exist, that's ok
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return provenance;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function copyDir(src: string, dest: string, provenance: StandaloneProvenance): Promise<void> {
|
|
240
|
+
const { readdir } = await import("node:fs/promises");
|
|
241
|
+
|
|
242
|
+
await ensureDir(dest);
|
|
243
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
244
|
+
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const srcPath = join(src, entry.name);
|
|
247
|
+
const destPath = join(dest, entry.name);
|
|
248
|
+
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
await copyDir(srcPath, destPath, provenance);
|
|
251
|
+
} else {
|
|
252
|
+
const content = await readFile(srcPath);
|
|
253
|
+
await writeFile(destPath, content);
|
|
254
|
+
|
|
255
|
+
// Get relative path from KITFLY_ROOT for provenance
|
|
256
|
+
const relPath = srcPath.replace(`${KITFLY_ROOT}/`, "");
|
|
257
|
+
const bytes = new Uint8Array(content);
|
|
258
|
+
const localHash = await hashBytes(bytes);
|
|
259
|
+
provenance.files.push({
|
|
260
|
+
path: relPath,
|
|
261
|
+
sourceHash: localHash,
|
|
262
|
+
localHash,
|
|
263
|
+
modified: false,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function ensureYamlSchemaComment(content: string, schemaPath: string): string {
|
|
270
|
+
const lines = content.split("\n");
|
|
271
|
+
const desired = `# yaml-language-server: $schema=${schemaPath}`;
|
|
272
|
+
if (lines[0]?.startsWith("# yaml-language-server:")) {
|
|
273
|
+
lines[0] = desired;
|
|
274
|
+
return lines.join("\n");
|
|
275
|
+
}
|
|
276
|
+
return `${desired}\n${content}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function ensureSiteSchemaVersion(content: string, version: string): string {
|
|
280
|
+
if (/^schemaVersion\s*:/m.test(content)) return content;
|
|
281
|
+
const lines = content.split("\n");
|
|
282
|
+
const insertLine = `schemaVersion: "${version}"`;
|
|
283
|
+
if (lines[0]?.startsWith("# yaml-language-server:")) {
|
|
284
|
+
lines.splice(1, 0, insertLine);
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
return `${insertLine}\n${content}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function stampSchemaVersion(root: string, version: string): Promise<void> {
|
|
291
|
+
const sitePath = join(root, "site.yaml");
|
|
292
|
+
try {
|
|
293
|
+
let content = await readFile(sitePath, "utf-8");
|
|
294
|
+
content = ensureYamlSchemaComment(content, "./schemas/v0/site.schema.json");
|
|
295
|
+
content = ensureSiteSchemaVersion(content, version);
|
|
296
|
+
await writeFile(sitePath, content, "utf-8");
|
|
297
|
+
} catch {
|
|
298
|
+
// site.yaml may not exist; ignore
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const themePath = join(root, "theme.yaml");
|
|
302
|
+
try {
|
|
303
|
+
let content = await readFile(themePath, "utf-8");
|
|
304
|
+
content = ensureYamlSchemaComment(content, "./schemas/v0/theme.schema.json");
|
|
305
|
+
await writeFile(themePath, content, "utf-8");
|
|
306
|
+
} catch {
|
|
307
|
+
// theme.yaml may not exist; ignore
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function generateStandalonePackageJson(ctx: TemplateContext): string {
|
|
312
|
+
return JSON.stringify(
|
|
313
|
+
{
|
|
314
|
+
name: ctx.name,
|
|
315
|
+
version: "0.1.0",
|
|
316
|
+
type: "module",
|
|
317
|
+
description: ctx.branding.siteName,
|
|
318
|
+
scripts: {
|
|
319
|
+
dev: "bun run scripts/dev.ts",
|
|
320
|
+
build: "bun run scripts/build.ts",
|
|
321
|
+
bundle: "bun run scripts/bundle.ts",
|
|
322
|
+
},
|
|
323
|
+
dependencies: {
|
|
324
|
+
marked: "^15.0.0",
|
|
325
|
+
},
|
|
326
|
+
devDependencies: {
|
|
327
|
+
"@types/bun": "^1.2.0",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
null,
|
|
331
|
+
2,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function generateStandaloneReadme(ctx: TemplateContext): string {
|
|
336
|
+
return `# ${ctx.branding.siteName}
|
|
337
|
+
|
|
338
|
+
Documentation site built with [Kitfly](https://github.com/3leaps/kitfly) (standalone mode).
|
|
339
|
+
|
|
340
|
+
## Prerequisites
|
|
341
|
+
|
|
342
|
+
### Install Bun
|
|
343
|
+
|
|
344
|
+
This site uses [Bun](https://bun.sh) as its JavaScript runtime.
|
|
345
|
+
|
|
346
|
+
**macOS/Linux:**
|
|
347
|
+
\`\`\`bash
|
|
348
|
+
curl -fsSL https://bun.sh/install | bash
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
**Windows:**
|
|
352
|
+
\`\`\`powershell
|
|
353
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
354
|
+
\`\`\`
|
|
355
|
+
|
|
356
|
+
For other installation methods, see: https://bun.sh/docs/installation
|
|
357
|
+
|
|
358
|
+
### Install Dependencies
|
|
359
|
+
|
|
360
|
+
\`\`\`bash
|
|
361
|
+
bun install
|
|
362
|
+
\`\`\`
|
|
363
|
+
|
|
364
|
+
## Development
|
|
365
|
+
|
|
366
|
+
\`\`\`bash
|
|
367
|
+
# Preview locally with hot reload
|
|
368
|
+
bun run dev
|
|
369
|
+
|
|
370
|
+
# Build static site to dist/
|
|
371
|
+
bun run build
|
|
372
|
+
|
|
373
|
+
# Create offline bundle (single HTML file)
|
|
374
|
+
bun run bundle
|
|
375
|
+
\`\`\`
|
|
376
|
+
|
|
377
|
+
## Structure
|
|
378
|
+
|
|
379
|
+
\`\`\`
|
|
380
|
+
${ctx.name}/
|
|
381
|
+
├── site.yaml # Site configuration
|
|
382
|
+
├── theme.yaml # Theme customization (optional)
|
|
383
|
+
├── index.md # Home page
|
|
384
|
+
├── content/ # Documentation content
|
|
385
|
+
├── assets/ # Static assets (images, brand files)
|
|
386
|
+
├── scripts/ # Build scripts (standalone)
|
|
387
|
+
│ ├── dev.ts # Development server
|
|
388
|
+
│ ├── build.ts # Static site generator
|
|
389
|
+
│ └── bundle.ts # Single-file bundler
|
|
390
|
+
└── src/ # Site engine (standalone)
|
|
391
|
+
├── shared.ts # Shared utilities
|
|
392
|
+
├── engine.ts # Engine paths
|
|
393
|
+
├── theme.ts # Theme system
|
|
394
|
+
└── site/ # HTML template & styles
|
|
395
|
+
\`\`\`
|
|
396
|
+
|
|
397
|
+
## Customization
|
|
398
|
+
|
|
399
|
+
- Edit \`site.yaml\` to configure sections and branding
|
|
400
|
+
- Add a \`theme.yaml\` for color/typography customization
|
|
401
|
+
- Replace files in \`assets/brand/\` with your logo and favicon
|
|
402
|
+
|
|
403
|
+
## Standalone Mode
|
|
404
|
+
|
|
405
|
+
This site was created with \`--standalone\` flag, meaning all build tooling is included.
|
|
406
|
+
No external kitfly installation is required.
|
|
407
|
+
|
|
408
|
+
See \`.kitfly/provenance.json\` for version tracking.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
© ${ctx.year} ${ctx.branding.brandName}
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function generateDependentReadme(ctx: TemplateContext): string {
|
|
416
|
+
return `# ${ctx.branding.siteName}
|
|
417
|
+
|
|
418
|
+
Documentation site built with [Kitfly](https://github.com/3leaps/kitfly).
|
|
419
|
+
|
|
420
|
+
## Prerequisites
|
|
421
|
+
|
|
422
|
+
### Install Bun
|
|
423
|
+
|
|
424
|
+
This site uses [Bun](https://bun.sh) as its JavaScript runtime.
|
|
425
|
+
|
|
426
|
+
**macOS/Linux:**
|
|
427
|
+
\`\`\`bash
|
|
428
|
+
curl -fsSL https://bun.sh/install | bash
|
|
429
|
+
\`\`\`
|
|
430
|
+
|
|
431
|
+
**Windows:**
|
|
432
|
+
\`\`\`powershell
|
|
433
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
434
|
+
\`\`\`
|
|
435
|
+
|
|
436
|
+
### Install Kitfly
|
|
437
|
+
|
|
438
|
+
\`\`\`bash
|
|
439
|
+
# Via npm/bun
|
|
440
|
+
bun install -g kitfly
|
|
441
|
+
|
|
442
|
+
# Or clone and link
|
|
443
|
+
git clone https://github.com/3leaps/kitfly.git
|
|
444
|
+
cd kitfly && bun link
|
|
445
|
+
\`\`\`
|
|
446
|
+
|
|
447
|
+
## Development
|
|
448
|
+
|
|
449
|
+
\`\`\`bash
|
|
450
|
+
# Preview locally with hot reload
|
|
451
|
+
kitfly dev
|
|
452
|
+
|
|
453
|
+
# Build static site to dist/
|
|
454
|
+
kitfly build
|
|
455
|
+
|
|
456
|
+
# Create offline bundle (single HTML file)
|
|
457
|
+
kitfly bundle
|
|
458
|
+
\`\`\`
|
|
459
|
+
|
|
460
|
+
## Structure
|
|
461
|
+
|
|
462
|
+
\`\`\`
|
|
463
|
+
${ctx.name}/
|
|
464
|
+
├── site.yaml # Site configuration
|
|
465
|
+
├── index.md # Home page
|
|
466
|
+
├── content/ # Documentation content
|
|
467
|
+
└── assets/brand/ # Logo, favicon, etc.
|
|
468
|
+
\`\`\`
|
|
469
|
+
|
|
470
|
+
## Customization
|
|
471
|
+
|
|
472
|
+
- Edit \`site.yaml\` to configure sections and branding
|
|
473
|
+
- Add a \`theme.yaml\` for color/typography customization
|
|
474
|
+
- Replace \`assets/brand/\` files with your logo
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
© ${ctx.year} ${ctx.branding.brandName}
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// -----------------------------------------------------------------------------
|
|
482
|
+
// AI Assistance Instrumentation
|
|
483
|
+
// -----------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
function generateAgentsMd(ctx: TemplateContext): string {
|
|
486
|
+
const templateType = ctx.template.id;
|
|
487
|
+
const isOperational = templateType === "runbook" || templateType === "pipeline";
|
|
488
|
+
const isPipeline = templateType === "pipeline";
|
|
489
|
+
const isProductbook = templateType === "productbook";
|
|
490
|
+
const isServicebook = templateType === "servicebook";
|
|
491
|
+
const isCrucible = templateType === "crucible";
|
|
492
|
+
|
|
493
|
+
const typeLabel = isCrucible
|
|
494
|
+
? "information architecture SSOT"
|
|
495
|
+
: isServicebook
|
|
496
|
+
? "professional services catalog"
|
|
497
|
+
: isProductbook
|
|
498
|
+
? "product and domain documentation site"
|
|
499
|
+
: isPipeline
|
|
500
|
+
? "pipeline operations site"
|
|
501
|
+
: templateType === "runbook"
|
|
502
|
+
? "runbook"
|
|
503
|
+
: "documentation site";
|
|
504
|
+
|
|
505
|
+
return `# ${ctx.branding.siteName} - AI Agent Guide
|
|
506
|
+
|
|
507
|
+
## Overview
|
|
508
|
+
|
|
509
|
+
This ${typeLabel} is instrumented for AI assistance. AI coding assistants can help maintain and extend this documentation.
|
|
510
|
+
|
|
511
|
+
## Site Structure
|
|
512
|
+
|
|
513
|
+
See \`CUSTOMIZING.md\` for full details on:
|
|
514
|
+
- Adding new pages and sections
|
|
515
|
+
- Configuration options
|
|
516
|
+
- Linking conventions
|
|
517
|
+
- Brand assets
|
|
518
|
+
|
|
519
|
+
## Content Guidelines
|
|
520
|
+
|
|
521
|
+
${
|
|
522
|
+
isCrucible
|
|
523
|
+
? `### Specs
|
|
524
|
+
- Use **RFC 2119 language** (MUST, SHOULD, MAY) for requirements
|
|
525
|
+
- Include **version and status** in every specification
|
|
526
|
+
- Provide **compliant and non-compliant examples**
|
|
527
|
+
- State scope explicitly — what is and isn't covered
|
|
528
|
+
|
|
529
|
+
### Schemas
|
|
530
|
+
- Document **purpose, fields, and examples** for each schema
|
|
531
|
+
- Link to the raw \`.schema.json\` file in \`schemas/\`
|
|
532
|
+
- Note **breaking change policy** and version compatibility
|
|
533
|
+
- Include sample valid documents
|
|
534
|
+
|
|
535
|
+
### Config
|
|
536
|
+
- Document **valid values and defaults** for every config entry
|
|
537
|
+
- Explain what each config controls and who consumes it
|
|
538
|
+
- Link to the validating schema (if any)
|
|
539
|
+
|
|
540
|
+
### Policies
|
|
541
|
+
- Be prescriptive — use **MUST/SHOULD/MAY** language
|
|
542
|
+
- Include **rationale** for each rule
|
|
543
|
+
- Define **consequences** for non-compliance
|
|
544
|
+
- State review and approval requirements`
|
|
545
|
+
: isServicebook
|
|
546
|
+
? `### Offerings
|
|
547
|
+
- State the **client problem** before describing the service
|
|
548
|
+
- Define **deliverables** with format, content, and audience
|
|
549
|
+
- Include **scoping criteria** so engagements are sized consistently
|
|
550
|
+
- Link to relevant methodology phases and delivery templates
|
|
551
|
+
|
|
552
|
+
### Methodology
|
|
553
|
+
- Document phases with **inputs, activities, and outputs**
|
|
554
|
+
- Tools should include purpose, capabilities, and when they're used
|
|
555
|
+
- Frameworks need clear **scoring criteria and limitations**
|
|
556
|
+
- Record methodology changes as decision records (MDRs)
|
|
557
|
+
|
|
558
|
+
### Delivery
|
|
559
|
+
- Engagement lifecycle needs **quality gates** between each stage
|
|
560
|
+
- Quality gates need **specific, checkable criteria**
|
|
561
|
+
- Deliverable templates should be ready to use, not just described
|
|
562
|
+
- Document onboarding as a checklist with clear prerequisites
|
|
563
|
+
|
|
564
|
+
### Case Studies
|
|
565
|
+
- Always **anonymize** unless client has approved attribution
|
|
566
|
+
- **Quantify outcomes** where possible
|
|
567
|
+
- Include **lessons learned** — what you'd do differently
|
|
568
|
+
- Link to relevant verticals and methodology`
|
|
569
|
+
: isProductbook
|
|
570
|
+
? `### Product
|
|
571
|
+
- State the **user problem** before the solution
|
|
572
|
+
- Include **acceptance criteria** and **success metrics**
|
|
573
|
+
- Link to related domain context and specs
|
|
574
|
+
|
|
575
|
+
### Domain
|
|
576
|
+
- Write for someone **new to the business** — explain why, not just what
|
|
577
|
+
- Be precise with terminology — the **data dictionary is canonical**
|
|
578
|
+
- Document **edge cases and variations**, not just happy paths
|
|
579
|
+
- Business processes need: triggers, steps, actors, systems, business rules
|
|
580
|
+
|
|
581
|
+
### Planning
|
|
582
|
+
- **Decisions**: capture context and alternatives, not just the choice
|
|
583
|
+
- **Specs**: problem first, solution second, acceptance criteria always
|
|
584
|
+
- **Research**: state methodology and limitations
|
|
585
|
+
|
|
586
|
+
### Guides
|
|
587
|
+
- Write for the audience (team member vs end user)
|
|
588
|
+
- Start with what they need to know first`
|
|
589
|
+
: isPipeline
|
|
590
|
+
? `### Pipeline Stages
|
|
591
|
+
- Start with **Objective** (what this stage accomplishes)
|
|
592
|
+
- List **Prerequisites** as checkboxes
|
|
593
|
+
- Number **Steps** explicitly with verification
|
|
594
|
+
- Include expected outputs and verification commands
|
|
595
|
+
|
|
596
|
+
### Sources & Destinations
|
|
597
|
+
- Document connection details, auth profiles, data formats
|
|
598
|
+
- Keep index tables in \`index.md\` up to date
|
|
599
|
+
- Include schema or path structure documentation
|
|
600
|
+
|
|
601
|
+
### Troubleshooting
|
|
602
|
+
- Lead with **Symptoms** (what user observes)
|
|
603
|
+
- List **Possible Causes**
|
|
604
|
+
- Provide step-by-step **Resolution**
|
|
605
|
+
- Include **Prevention** guidance
|
|
606
|
+
|
|
607
|
+
### Checklists
|
|
608
|
+
- Use checkbox format: \`- [ ] Item\`
|
|
609
|
+
- Group by phase or category
|
|
610
|
+
- Include Go/No-Go decision point`
|
|
611
|
+
: isOperational
|
|
612
|
+
? `### Procedures
|
|
613
|
+
- Start with **Objective** (what this accomplishes)
|
|
614
|
+
- List **Prerequisites** as checkboxes
|
|
615
|
+
- Number **Steps** explicitly with verification
|
|
616
|
+
- Include expected outputs
|
|
617
|
+
- Always provide **Rollback** section
|
|
618
|
+
|
|
619
|
+
### Troubleshooting
|
|
620
|
+
- Lead with **Symptoms** (what user observes)
|
|
621
|
+
- List **Possible Causes**
|
|
622
|
+
- Provide step-by-step **Resolution**
|
|
623
|
+
- Include **Escalation** path
|
|
624
|
+
|
|
625
|
+
### Checklists
|
|
626
|
+
- Use checkbox format: \`- [ ] Item\`
|
|
627
|
+
- Group by phase or category
|
|
628
|
+
- Include Go/No-Go decision point`
|
|
629
|
+
: `### General Guidelines
|
|
630
|
+
- Use clear, descriptive headings
|
|
631
|
+
- Include code examples where helpful
|
|
632
|
+
- Link related content using relative paths
|
|
633
|
+
- Mark placeholder content with \`<!-- ← CUSTOMIZE -->\``
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
## AI Assistant Instructions
|
|
637
|
+
|
|
638
|
+
When working on this site:
|
|
639
|
+
|
|
640
|
+
1. **Read \`CUSTOMIZING.md\` first** - Understand the structure before making changes
|
|
641
|
+
2. **Follow existing patterns** - Match the style of existing content
|
|
642
|
+
3. **Check \`.kitfly/manifest.json\`** - Know which template was used
|
|
643
|
+
4. **Respect limitations** - Content must be inside this folder; link external resources via URL
|
|
644
|
+
5. **Preserve frontmatter** - Every markdown file needs title and description
|
|
645
|
+
|
|
646
|
+
## Roles
|
|
647
|
+
|
|
648
|
+
${
|
|
649
|
+
isCrucible
|
|
650
|
+
? `Recommended roles for crucible maintenance:
|
|
651
|
+
- \`infoarch\` - Information architecture, standards structure, consistency
|
|
652
|
+
- \`devlead\` - Schema design, config catalogs, technical standards
|
|
653
|
+
- \`advisor\` - Governance, policy design, strategic decisions
|
|
654
|
+
- \`analyst\` - Research, gap analysis, ecosystem assessment
|
|
655
|
+
- \`qa\` - Validation, compliance checking, review`
|
|
656
|
+
: isServicebook
|
|
657
|
+
? `Recommended roles for servicebook maintenance:
|
|
658
|
+
- \`prodstrat\` - Service strategy, offering design, market positioning
|
|
659
|
+
- \`advisor\` - Client engagement, industry expertise, methodology
|
|
660
|
+
- \`analyst\` - Research, frameworks, case study analysis
|
|
661
|
+
- \`devlead\` - Delivery operations, tooling, quality processes
|
|
662
|
+
- \`infoarch\` - Documentation structure, consistency`
|
|
663
|
+
: isProductbook
|
|
664
|
+
? `Recommended roles for productbook maintenance:
|
|
665
|
+
- \`prodstrat\` - Product direction, roadmap, feature prioritization
|
|
666
|
+
- \`advisor\` - Domain knowledge, business context, strategic decisions
|
|
667
|
+
- \`analyst\` - Research, data modeling, business process analysis
|
|
668
|
+
- \`devlead\` - Architecture, operations, implementation
|
|
669
|
+
- \`infoarch\` - Documentation structure, consistency`
|
|
670
|
+
: isOperational
|
|
671
|
+
? `Recommended roles for ${isPipeline ? "pipeline operations" : "runbook"} maintenance:
|
|
672
|
+
- \`devlead\` - Implementation, fixing procedures
|
|
673
|
+
- \`infoarch\` - Documentation structure, organization
|
|
674
|
+
- \`qa\` - Testing, validation, checklists`
|
|
675
|
+
: `Recommended roles for documentation:
|
|
676
|
+
- \`devlead\` - Technical content, code examples
|
|
677
|
+
- \`infoarch\` - Structure, navigation, organization
|
|
678
|
+
- \`prodmktg\` - Messaging, user-facing content`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
See \`config/agentic/roles/\` for role definitions (if present).
|
|
682
|
+
|
|
683
|
+
## Quick Reference
|
|
684
|
+
|
|
685
|
+
| Task | Location |
|
|
686
|
+
|------|----------|
|
|
687
|
+
| Site config | \`site.yaml\` |
|
|
688
|
+
| Theme/styling | \`theme.yaml\` (create if needed) |
|
|
689
|
+
| Add content | \`content/<section>/\` |
|
|
690
|
+
| Brand assets | \`assets/brand/\` |
|
|
691
|
+
| AI instructions | This file (\`AGENTS.md\`) |
|
|
692
|
+
| Customization guide | \`CUSTOMIZING.md\` |
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
*Generated by kitfly from ${templateType} template*
|
|
697
|
+
`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function addAiAssistInstrumentation(root: string, ctx: TemplateContext): Promise<void> {
|
|
701
|
+
// Generate AGENTS.md
|
|
702
|
+
const agentsMd = generateAgentsMd(ctx);
|
|
703
|
+
await writeFile(join(root, "AGENTS.md"), agentsMd, "utf-8");
|
|
704
|
+
console.log(` + AGENTS.md (AI assistance)`);
|
|
705
|
+
|
|
706
|
+
// Copy relevant roles from kitfly's config
|
|
707
|
+
const rolesSource = join(KITFLY_ROOT, "config/agentic/roles");
|
|
708
|
+
const rolesTarget = join(root, "config/agentic/roles");
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const { readdir } = await import("node:fs/promises");
|
|
712
|
+
const entries = await readdir(rolesSource);
|
|
713
|
+
|
|
714
|
+
// Select roles relevant to documentation work
|
|
715
|
+
const relevantRoles = ["devlead.yaml", "infoarch.yaml", "qa.yaml", "README.md"];
|
|
716
|
+
if (ctx.template.id === "crucible") {
|
|
717
|
+
relevantRoles.push("advisor.yaml", "analyst.yaml");
|
|
718
|
+
} else if (ctx.template.id === "productbook" || ctx.template.id === "servicebook") {
|
|
719
|
+
relevantRoles.push("prodstrat.yaml", "advisor.yaml", "analyst.yaml");
|
|
720
|
+
} else if (ctx.template.id !== "runbook" && ctx.template.id !== "pipeline") {
|
|
721
|
+
relevantRoles.push("prodmktg.yaml");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
await ensureDir(rolesTarget);
|
|
725
|
+
|
|
726
|
+
for (const entry of entries) {
|
|
727
|
+
if (relevantRoles.includes(entry)) {
|
|
728
|
+
const srcPath = join(rolesSource, entry);
|
|
729
|
+
const destPath = join(rolesTarget, entry);
|
|
730
|
+
const content = await readFile(srcPath, "utf-8");
|
|
731
|
+
await writeFile(destPath, content, "utf-8");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
console.log(` + config/agentic/roles/ (${relevantRoles.length} files)`);
|
|
735
|
+
} catch {
|
|
736
|
+
// Roles directory not found - skip silently
|
|
737
|
+
console.log(` ! config/agentic/roles/ not copied (source not found)`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// -----------------------------------------------------------------------------
|
|
742
|
+
// Template Execution
|
|
743
|
+
// -----------------------------------------------------------------------------
|
|
744
|
+
|
|
745
|
+
export async function runTemplate(options: InitOptions): Promise<void> {
|
|
746
|
+
const template = getTemplate(options.template);
|
|
747
|
+
if (!template) {
|
|
748
|
+
throw new Error(`Unknown template: ${options.template}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Resolve inheritance chain
|
|
752
|
+
const chain = resolveTemplateChain(template);
|
|
753
|
+
|
|
754
|
+
// Build context
|
|
755
|
+
const branding: BrandingConfig = {
|
|
756
|
+
...defaultBranding(options.name),
|
|
757
|
+
...options.branding,
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const ctx: TemplateContext = {
|
|
761
|
+
name: options.name,
|
|
762
|
+
branding,
|
|
763
|
+
template,
|
|
764
|
+
year: new Date().getFullYear(),
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
const root = join(process.cwd(), options.name);
|
|
768
|
+
const modeLabel = options.standalone ? "standalone" : "standard";
|
|
769
|
+
|
|
770
|
+
console.log(`Creating ${template.name} site (${modeLabel}): ${options.name}/\n`);
|
|
771
|
+
|
|
772
|
+
// Create root directory
|
|
773
|
+
await ensureDir(root);
|
|
774
|
+
|
|
775
|
+
// Create sections from all templates in chain
|
|
776
|
+
for (const tpl of chain) {
|
|
777
|
+
for (const section of tpl.sections) {
|
|
778
|
+
const sectionPath = join(root, section.path);
|
|
779
|
+
await ensureDir(sectionPath);
|
|
780
|
+
console.log(` + ${section.path}/`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Write files from all templates in chain (except README which we handle specially)
|
|
785
|
+
for (const tpl of chain) {
|
|
786
|
+
for (const file of tpl.files) {
|
|
787
|
+
if (file.path === "README.md") continue; // Skip, we generate mode-specific README
|
|
788
|
+
await writeTemplateFile(root, file, ctx);
|
|
789
|
+
console.log(` + ${file.path}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Generate mode-specific README
|
|
794
|
+
const readme = options.standalone ? generateStandaloneReadme(ctx) : generateDependentReadme(ctx);
|
|
795
|
+
await writeFile(join(root, "README.md"), readme, "utf-8");
|
|
796
|
+
console.log(` + README.md`);
|
|
797
|
+
|
|
798
|
+
// Create .kitfly/ metadata folder with manifest (for all sites)
|
|
799
|
+
const kitflyVersion = await getKitflyVersion();
|
|
800
|
+
const manifest: SiteManifest = {
|
|
801
|
+
template: template.id,
|
|
802
|
+
templateVersion: template.version,
|
|
803
|
+
created: new Date().toISOString(),
|
|
804
|
+
kitflyVersion,
|
|
805
|
+
standalone: options.standalone ?? false,
|
|
806
|
+
schemaVersion: kitflyVersion,
|
|
807
|
+
};
|
|
808
|
+
await ensureDir(join(root, ".kitfly"));
|
|
809
|
+
await writeFile(join(root, ".kitfly/manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
810
|
+
console.log(` + .kitfly/manifest.json`);
|
|
811
|
+
|
|
812
|
+
// Handle standalone mode
|
|
813
|
+
if (options.standalone) {
|
|
814
|
+
// Copy site code
|
|
815
|
+
const provenance = await copyStandaloneFiles(root, ctx);
|
|
816
|
+
await stampSchemaVersion(root, kitflyVersion);
|
|
817
|
+
|
|
818
|
+
// Write standalone package.json
|
|
819
|
+
const packageJson = generateStandalonePackageJson(ctx);
|
|
820
|
+
await writeFile(join(root, "package.json"), packageJson, "utf-8");
|
|
821
|
+
console.log(` + package.json (standalone)`);
|
|
822
|
+
|
|
823
|
+
// Write provenance
|
|
824
|
+
await writeFile(
|
|
825
|
+
join(root, ".kitfly/provenance.json"),
|
|
826
|
+
JSON.stringify(provenance, null, 2),
|
|
827
|
+
"utf-8",
|
|
828
|
+
);
|
|
829
|
+
console.log(` + .kitfly/provenance.json`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Add AI assistance instrumentation if requested
|
|
833
|
+
if (options.aiAssist) {
|
|
834
|
+
await addAiAssistInstrumentation(root, ctx);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Initialize git if requested
|
|
838
|
+
if (options.git !== false) {
|
|
839
|
+
const gitOk = await initGit(root);
|
|
840
|
+
if (gitOk) {
|
|
841
|
+
await gitCommit(root, "Initial commit from kitfly init");
|
|
842
|
+
console.log(` + .git/ (initialized)`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
console.log(`\n✓ Site created at ${options.name}/`);
|
|
847
|
+
console.log(`\nNext steps:`);
|
|
848
|
+
console.log(` cd ${options.name}`);
|
|
849
|
+
|
|
850
|
+
if (options.standalone) {
|
|
851
|
+
console.log(` bun install`);
|
|
852
|
+
console.log(` bun run dev`);
|
|
853
|
+
} else {
|
|
854
|
+
console.log(` kitfly dev`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// -----------------------------------------------------------------------------
|
|
859
|
+
// Template Inheritance
|
|
860
|
+
// -----------------------------------------------------------------------------
|
|
861
|
+
|
|
862
|
+
function resolveTemplateChain(template: TemplateDef): TemplateDef[] {
|
|
863
|
+
const chain: TemplateDef[] = [];
|
|
864
|
+
let current: TemplateDef | undefined = template;
|
|
865
|
+
|
|
866
|
+
while (current) {
|
|
867
|
+
chain.unshift(current); // Add to front (base first)
|
|
868
|
+
if (current.extends) {
|
|
869
|
+
current = getTemplate(current.extends);
|
|
870
|
+
} else {
|
|
871
|
+
current = undefined;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return chain;
|
|
876
|
+
}
|