beacon-docs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1493 @@
1
+ #!/usr/bin/env node
2
+ import fs4 from 'fs-extra';
3
+ import path2 from 'path';
4
+ import ejs from 'ejs';
5
+ import { fileURLToPath } from 'url';
6
+ import fg from 'fast-glob';
7
+ import matter from 'gray-matter';
8
+ import { cac } from 'cac';
9
+ import * as p from '@clack/prompts';
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ function configPath(projectRoot) {
21
+ return path2.join(projectRoot, "docs", "_meta", "beacon.config.json");
22
+ }
23
+ async function readConfig(projectRoot) {
24
+ const p2 = configPath(projectRoot);
25
+ if (!await fs4.pathExists(p2)) {
26
+ throw new Error(
27
+ `Could not find docs/_meta/beacon.config.json at ${projectRoot}. Run \`beacon init\` first.`
28
+ );
29
+ }
30
+ return await fs4.readJson(p2);
31
+ }
32
+ async function writeConfig(projectRoot, config) {
33
+ const p2 = configPath(projectRoot);
34
+ await fs4.ensureDir(path2.dirname(p2));
35
+ const json = JSON.stringify(config, null, 2) + "\n";
36
+ await fs4.writeFile(p2, json, "utf8");
37
+ }
38
+ var init_config = __esm({
39
+ "src/core/config.ts"() {
40
+ }
41
+ });
42
+
43
+ // src/core/categories.ts
44
+ function metaFor(category) {
45
+ return CATEGORY_META[category];
46
+ }
47
+ function isArchivable(category) {
48
+ return CATEGORY_META[category].archivable;
49
+ }
50
+ var CATEGORY_META;
51
+ var init_categories = __esm({
52
+ "src/core/categories.ts"() {
53
+ CATEGORY_META = {
54
+ reference: {
55
+ category: "reference",
56
+ location: "reference",
57
+ suffix: ".pattern.md",
58
+ archivable: false,
59
+ datePrefix: false
60
+ },
61
+ architecture: {
62
+ category: "architecture",
63
+ location: "architecture",
64
+ suffix: ".architecture.md",
65
+ archivable: false,
66
+ datePrefix: false
67
+ },
68
+ adr: {
69
+ category: "adr",
70
+ location: "adr",
71
+ suffix: ".md",
72
+ archivable: false,
73
+ datePrefix: false,
74
+ numberedPrefix: "ADR-"
75
+ },
76
+ plans: {
77
+ category: "plans",
78
+ location: "plans",
79
+ suffix: ".plan.md",
80
+ archivable: true,
81
+ datePrefix: false
82
+ },
83
+ backlog: {
84
+ category: "backlog",
85
+ location: "backlog",
86
+ suffix: ".todo.md",
87
+ archivable: false,
88
+ datePrefix: false
89
+ },
90
+ evaluations: {
91
+ category: "evaluations",
92
+ location: "evaluations",
93
+ suffix: ".eval.md",
94
+ archivable: false,
95
+ datePrefix: true
96
+ },
97
+ compliance: {
98
+ category: "compliance",
99
+ location: "compliance",
100
+ suffix: ".md",
101
+ archivable: false,
102
+ datePrefix: false
103
+ },
104
+ business: {
105
+ category: "business",
106
+ location: "business",
107
+ suffix: ".business.md",
108
+ archivable: false,
109
+ datePrefix: false
110
+ },
111
+ modules: {
112
+ category: "modules",
113
+ location: "modules",
114
+ suffix: ".module.md",
115
+ archivable: false,
116
+ datePrefix: false
117
+ },
118
+ integrations: {
119
+ category: "integrations",
120
+ location: "integrations",
121
+ suffix: ".guide.md",
122
+ archivable: false,
123
+ datePrefix: false
124
+ },
125
+ operations: {
126
+ category: "operations",
127
+ location: "operations",
128
+ suffix: ".guide.md",
129
+ archivable: false,
130
+ datePrefix: false
131
+ },
132
+ roadmaps: {
133
+ category: "roadmaps",
134
+ location: "roadmaps",
135
+ suffix: ".roadmap.md",
136
+ archivable: true,
137
+ datePrefix: false
138
+ }
139
+ };
140
+ }
141
+ });
142
+ function docsDir(root) {
143
+ return path2.join(root, "docs");
144
+ }
145
+ function metaDir(root) {
146
+ return path2.join(docsDir(root), "_meta");
147
+ }
148
+ function conventionPath(root) {
149
+ return path2.join(metaDir(root), "convention.md");
150
+ }
151
+ function categoryDir(root, category) {
152
+ return path2.join(docsDir(root), category);
153
+ }
154
+ function archiveDir(root, category) {
155
+ return path2.join(categoryDir(root, category), "_archive");
156
+ }
157
+ function rootFile(root, name) {
158
+ return path2.join(root, name);
159
+ }
160
+ var init_paths = __esm({
161
+ "src/core/paths.ts"() {
162
+ }
163
+ });
164
+ function resolveTemplateDir() {
165
+ const here = path2.dirname(fileURLToPath(import.meta.url));
166
+ const candidate = path2.join(here, "templates");
167
+ if (fs4.existsSync(candidate)) return candidate;
168
+ return path2.join(here, "..", "templates");
169
+ }
170
+ function renderMasterReadme(config) {
171
+ const tpl = fs4.readFileSync(path2.join(TEMPLATE_DIR, "readme-master.md.ejs"), "utf8");
172
+ const has = (c) => config.categories.includes(c);
173
+ return ejs.render(tpl, { projectType: config.projectType, has }, { rmWhitespace: false });
174
+ }
175
+ function renderCategoryReadme(category) {
176
+ const tpl = fs4.readFileSync(path2.join(TEMPLATE_DIR, "readme-category.md.ejs"), "utf8");
177
+ const m = metaFor(category);
178
+ const d = CATEGORY_DESCRIPTIONS[category];
179
+ return ejs.render(tpl, {
180
+ title: d.title,
181
+ description: d.description,
182
+ suffix: m.suffix,
183
+ numberedPrefix: m.numberedPrefix,
184
+ datePrefix: m.datePrefix,
185
+ archivable: m.archivable
186
+ });
187
+ }
188
+ var TEMPLATE_DIR, CATEGORY_DESCRIPTIONS;
189
+ var init_readme = __esm({
190
+ "src/generators/readme.ts"() {
191
+ init_categories();
192
+ TEMPLATE_DIR = resolveTemplateDir();
193
+ CATEGORY_DESCRIPTIONS = {
194
+ reference: { title: "reference/", description: "Replicable technical patterns \u2014 *how* something is done." },
195
+ architecture: { title: "architecture/", description: "System structure and layering decisions." },
196
+ adr: { title: "adr/", description: "Architecture Decision Records \u2014 *why* a choice was made." },
197
+ plans: { title: "plans/", description: "Active work with checked TODOs." },
198
+ backlog: { title: "backlog/", description: "Future work items not yet planned." },
199
+ evaluations: { title: "evaluations/", description: "Date-prefixed audits and state snapshots." },
200
+ compliance: { title: "compliance/", description: "Regulatory and normative documents." },
201
+ business: { title: "business/", description: "Business model, pricing, product strategy." },
202
+ modules: { title: "modules/", description: "Domain modules and their functional behavior." },
203
+ integrations: { title: "integrations/", description: "External service setup and configuration." },
204
+ operations: { title: "operations/", description: "Runbooks, deploy guides, troubleshooting." },
205
+ roadmaps: { title: "roadmaps/", description: "Multi-sprint planning (longer than `plans/`)." }
206
+ };
207
+ }
208
+ });
209
+
210
+ // src/generators/_header.ts
211
+ var HEADER;
212
+ var init_header = __esm({
213
+ "src/generators/_header.ts"() {
214
+ HEADER = "<!-- generated by beacon \u2014 do not edit. Edit docs/_meta/convention.md and run `beacon sync`. -->";
215
+ }
216
+ });
217
+
218
+ // src/generators/ai-rules.ts
219
+ function buildUniversalRules() {
220
+ return [
221
+ "## Universal rules",
222
+ "",
223
+ "1. **One doc = one category.** Never duplicate across folders.",
224
+ "2. **Status via folder, never filename.** Closed plan \u2192 `_archive/`, never `*_DONE.md` or `*_v2.md`.",
225
+ "3. **kebab-case for all filenames.** No camelCase, no snake_case, no spaces.",
226
+ "4. **README required in every category folder.**",
227
+ "5. **ADRs are append-only.** Supersede with a new ADR; never edit an accepted one.",
228
+ "6. **Evaluations are immutable snapshots.** To revise, create a new dated file.",
229
+ "7. **Plans archive to `_archive/` when done.** Move them, never rename.",
230
+ "8. **Generated AI files (this one) must match `docs/_meta/convention.md`.** Run `beacon sync` after editing the convention.",
231
+ "9. **Don't create folders outside the convention.** Run `beacon enable <addon>` or propose an ADR."
232
+ ].join("\n");
233
+ }
234
+ function buildProjectSpecificRules(config) {
235
+ const lines = ["## Project-specific rules", ""];
236
+ let any = false;
237
+ for (const c of config.categories) {
238
+ if (PROJECT_SPECIFIC_RULES[c]) {
239
+ lines.push(PROJECT_SPECIFIC_RULES[c]);
240
+ any = true;
241
+ }
242
+ }
243
+ if (!any) {
244
+ lines.push("- (none for this project)");
245
+ }
246
+ return lines.join("\n");
247
+ }
248
+ function buildDecisionTable(config) {
249
+ const lines = [
250
+ "## Where does X go?",
251
+ "",
252
+ "| Question | Folder |",
253
+ "|---|---|"
254
+ ];
255
+ for (const c of config.categories) {
256
+ if (DECISION_ROWS[c]) lines.push(DECISION_ROWS[c]);
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ var PROJECT_SPECIFIC_RULES, DECISION_ROWS;
261
+ var init_ai_rules = __esm({
262
+ "src/generators/ai-rules.ts"() {
263
+ init_categories();
264
+ PROJECT_SPECIFIC_RULES = {
265
+ compliance: "- **`compliance/`** \u2014 regulatory docs only. Link from related modules/architecture; never duplicate.",
266
+ modules: "- **`modules/`** \u2014 domain logic per module. Technical patterns extracted from a module go in `reference/`, not here.",
267
+ integrations: "- **`integrations/*.guide.md`** \u2014 setup for external services (Stripe, email, etc.). Code patterns for *using* them go in `reference/`.",
268
+ business: "- **`business/*.business.md`** \u2014 strategy, pricing, product model. No technical content.",
269
+ operations: "- **`operations/*.guide.md`** \u2014 deploy guides, admin runbooks, troubleshooting. Post-mortems go in `evaluations/`.",
270
+ roadmaps: "- **`roadmaps/*.roadmap.md`** \u2014 multi-sprint planning. Individual sprint work goes in `plans/`."
271
+ };
272
+ DECISION_ROWS = {
273
+ reference: '| "How is X done technically?" | `reference/` |',
274
+ architecture: '| "How is the system structured?" | `architecture/` |',
275
+ adr: '| "Why was X decided?" | `adr/` |',
276
+ modules: '| "What does module X do?" | `modules/` |',
277
+ compliance: '| "Does X comply with regulation Y?" | `compliance/` |',
278
+ integrations: '| "How do I set up service X?" | `integrations/` |',
279
+ operations: '| "How do I deploy/administer X?" | `operations/` |',
280
+ business: '| "What is the business model?" | `business/` |',
281
+ plans: '| "What is being built right now?" | `plans/` |',
282
+ roadmaps: '| "What is planned for next quarter?" | `roadmaps/` |',
283
+ backlog: '| "What is on the future backlog?" | `backlog/` |',
284
+ evaluations: '| "What was the state at date X?" | `evaluations/` |'
285
+ };
286
+ }
287
+ });
288
+
289
+ // src/generators/claude.ts
290
+ function renderClaudeMd(config) {
291
+ return [
292
+ HEADER,
293
+ "",
294
+ "# Documentation Convention",
295
+ "",
296
+ "> Project type: **" + config.projectType + "**. Full convention: [`docs/_meta/convention.md`](docs/_meta/convention.md).",
297
+ "",
298
+ buildUniversalRules(),
299
+ "",
300
+ buildProjectSpecificRules(config),
301
+ "",
302
+ buildDecisionTable(config),
303
+ ""
304
+ ].join("\n");
305
+ }
306
+ var init_claude = __esm({
307
+ "src/generators/claude.ts"() {
308
+ init_header();
309
+ init_ai_rules();
310
+ }
311
+ });
312
+
313
+ // src/generators/agents.ts
314
+ function renderAgentsMd(config) {
315
+ return [
316
+ HEADER,
317
+ "",
318
+ "# AGENTS.md \u2014 Documentation Convention",
319
+ "",
320
+ "> Read by OpenAI Codex / GitHub Copilot. Full convention: [`docs/_meta/convention.md`](docs/_meta/convention.md).",
321
+ "",
322
+ buildUniversalRules(),
323
+ "",
324
+ buildProjectSpecificRules(config),
325
+ "",
326
+ buildDecisionTable(config),
327
+ ""
328
+ ].join("\n");
329
+ }
330
+ var init_agents = __esm({
331
+ "src/generators/agents.ts"() {
332
+ init_header();
333
+ init_ai_rules();
334
+ }
335
+ });
336
+
337
+ // src/generators/gemini.ts
338
+ function renderGeminiMd(config) {
339
+ return [
340
+ HEADER,
341
+ "",
342
+ "# GEMINI.md \u2014 Documentation Convention",
343
+ "",
344
+ "> Read by Gemini CLI. Full convention: [`docs/_meta/convention.md`](docs/_meta/convention.md).",
345
+ "",
346
+ buildUniversalRules(),
347
+ "",
348
+ buildProjectSpecificRules(config),
349
+ "",
350
+ buildDecisionTable(config),
351
+ ""
352
+ ].join("\n");
353
+ }
354
+ var init_gemini = __esm({
355
+ "src/generators/gemini.ts"() {
356
+ init_header();
357
+ init_ai_rules();
358
+ }
359
+ });
360
+
361
+ // src/generators/cursor.ts
362
+ function renderCursorRules(config) {
363
+ return [
364
+ HEADER,
365
+ "",
366
+ buildUniversalRules(),
367
+ "",
368
+ buildProjectSpecificRules(config),
369
+ ""
370
+ ].join("\n");
371
+ }
372
+ function renderCursorMdc(config) {
373
+ const front = [
374
+ "---",
375
+ "description: Beacon documentation convention \u2014 where docs go and how they're named.",
376
+ 'globs: ["docs/**/*.md"]',
377
+ "alwaysApply: true",
378
+ "---"
379
+ ].join("\n");
380
+ return [
381
+ front,
382
+ "",
383
+ HEADER,
384
+ "",
385
+ buildUniversalRules(),
386
+ "",
387
+ buildProjectSpecificRules(config),
388
+ ""
389
+ ].join("\n");
390
+ }
391
+ var init_cursor = __esm({
392
+ "src/generators/cursor.ts"() {
393
+ init_header();
394
+ init_ai_rules();
395
+ }
396
+ });
397
+
398
+ // src/commands/sync.ts
399
+ var sync_exports = {};
400
+ __export(sync_exports, {
401
+ runSync: () => runSync
402
+ });
403
+ async function runSync(opts) {
404
+ const config = await readConfig(opts.root);
405
+ if (config.agents.includes("claude")) {
406
+ await fs4.writeFile(rootFile(opts.root, "CLAUDE.md"), renderClaudeMd(config), "utf8");
407
+ }
408
+ if (config.agents.includes("codex")) {
409
+ await fs4.writeFile(rootFile(opts.root, "AGENTS.md"), renderAgentsMd(config), "utf8");
410
+ }
411
+ if (config.agents.includes("gemini")) {
412
+ await fs4.writeFile(rootFile(opts.root, "GEMINI.md"), renderGeminiMd(config), "utf8");
413
+ }
414
+ if (config.agents.includes("cursor")) {
415
+ await fs4.writeFile(rootFile(opts.root, ".cursorrules"), renderCursorRules(config), "utf8");
416
+ const mdcDir = path2.join(opts.root, ".cursor", "rules");
417
+ await fs4.ensureDir(mdcDir);
418
+ await fs4.writeFile(path2.join(mdcDir, "beacon.mdc"), renderCursorMdc(config), "utf8");
419
+ }
420
+ }
421
+ var init_sync = __esm({
422
+ "src/commands/sync.ts"() {
423
+ init_config();
424
+ init_paths();
425
+ init_claude();
426
+ init_agents();
427
+ init_gemini();
428
+ init_cursor();
429
+ }
430
+ });
431
+ async function nextAdrNumber(root) {
432
+ const dir = categoryDir(root, "adr");
433
+ if (!await fs4.pathExists(dir)) return "001";
434
+ const files = await fs4.readdir(dir);
435
+ let max = 0;
436
+ for (const f of files) {
437
+ const m = ADR_FILE.exec(f);
438
+ if (m) {
439
+ const n = parseInt(m[1], 10);
440
+ if (n > max) max = n;
441
+ }
442
+ }
443
+ return String(max + 1).padStart(3, "0");
444
+ }
445
+ var ADR_FILE;
446
+ var init_adr_numbering = __esm({
447
+ "src/core/adr-numbering.ts"() {
448
+ init_paths();
449
+ ADR_FILE = /^ADR-(\d{3})-.+\.md$/;
450
+ }
451
+ });
452
+ function resolveTemplateDir3() {
453
+ const here = path2.dirname(fileURLToPath(import.meta.url));
454
+ const candidate = path2.join(here, "templates", "frontmatter");
455
+ if (fs4.existsSync(candidate)) return candidate;
456
+ return path2.join(here, "..", "templates", "frontmatter");
457
+ }
458
+ function renderDoc(input) {
459
+ const tplPath = path2.join(TEMPLATE_DIR3, `${input.type}.md.ejs`);
460
+ const tpl = fs4.readFileSync(tplPath, "utf8");
461
+ return ejs.render(tpl, input);
462
+ }
463
+ var TEMPLATE_DIR3;
464
+ var init_doc = __esm({
465
+ "src/generators/doc.ts"() {
466
+ TEMPLATE_DIR3 = resolveTemplateDir3();
467
+ }
468
+ });
469
+
470
+ // src/commands/new.ts
471
+ var new_exports = {};
472
+ __export(new_exports, {
473
+ runNew: () => runNew
474
+ });
475
+ async function runNew(input) {
476
+ if (!KEBAB.test(input.slug)) {
477
+ throw new Error(`Slug must be kebab-case: "${input.slug}".`);
478
+ }
479
+ const config = await readConfig(input.root);
480
+ const category = input.type === "guide" && input.category ? input.category : DOC_TYPE_TO_CATEGORY[input.type];
481
+ if (!config.categories.includes(category)) {
482
+ throw new Error(
483
+ `Category "${category}" is not enabled. Run \`beacon enable ${category}\` first.`
484
+ );
485
+ }
486
+ const meta = metaFor(category);
487
+ const dir = categoryDir(input.root, category);
488
+ let filename;
489
+ let number;
490
+ if (meta.numberedPrefix) {
491
+ number = await nextAdrNumber(input.root);
492
+ filename = `${meta.numberedPrefix}${number}-${input.slug}.md`;
493
+ } else if (meta.datePrefix) {
494
+ filename = `${input.today}-${input.slug}${meta.suffix}`;
495
+ } else {
496
+ filename = `${input.slug}${meta.suffix}`;
497
+ }
498
+ const filePath = path2.join(dir, filename);
499
+ if (await fs4.pathExists(filePath)) {
500
+ throw new Error(`File already exists: ${filePath}`);
501
+ }
502
+ const content = renderDoc({
503
+ type: input.type,
504
+ title: input.slug,
505
+ today: input.today,
506
+ number
507
+ });
508
+ await fs4.writeFile(filePath, content, "utf8");
509
+ return filePath;
510
+ }
511
+ var DOC_TYPE_TO_CATEGORY, KEBAB;
512
+ var init_new = __esm({
513
+ "src/commands/new.ts"() {
514
+ init_config();
515
+ init_categories();
516
+ init_paths();
517
+ init_adr_numbering();
518
+ init_doc();
519
+ DOC_TYPE_TO_CATEGORY = {
520
+ plan: "plans",
521
+ adr: "adr",
522
+ pattern: "reference",
523
+ eval: "evaluations",
524
+ architecture: "architecture",
525
+ module: "modules",
526
+ guide: "integrations",
527
+ // ambiguous — caller can override via `category` field
528
+ roadmap: "roadmaps",
529
+ todo: "backlog",
530
+ business: "business",
531
+ compliance: "compliance"
532
+ };
533
+ KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
534
+ }
535
+ });
536
+
537
+ // src/commands/archive.ts
538
+ var archive_exports = {};
539
+ __export(archive_exports, {
540
+ runArchive: () => runArchive
541
+ });
542
+ async function runArchive(input) {
543
+ const category = TYPE_TO_CATEGORY[input.type];
544
+ if (!category) {
545
+ throw new Error(`Type "${input.type}" cannot be archived. Only plan and roadmap support archiving.`);
546
+ }
547
+ const meta = metaFor(category);
548
+ if (!meta.archivable) {
549
+ throw new Error(`Category "${category}" cannot be archived.`);
550
+ }
551
+ const filename = `${input.slug}${meta.suffix}`;
552
+ const source = path2.join(categoryDir(input.root, category), filename);
553
+ if (!await fs4.pathExists(source)) {
554
+ throw new Error(`File not found: ${source}`);
555
+ }
556
+ const warnings = [];
557
+ const body = await fs4.readFile(source, "utf8");
558
+ if (/- \[ \]/.test(body)) {
559
+ warnings.push("Unchecked TODOs remain in the document.");
560
+ if (!input.force) {
561
+ throw new Error(
562
+ "Document has unchecked TODOs. Re-run with --force to archive anyway."
563
+ );
564
+ }
565
+ }
566
+ const dest = path2.join(archiveDir(input.root, category), filename);
567
+ await fs4.ensureDir(path2.dirname(dest));
568
+ await fs4.move(source, dest, { overwrite: false });
569
+ return { source, destination: dest, warnings };
570
+ }
571
+ var TYPE_TO_CATEGORY;
572
+ var init_archive = __esm({
573
+ "src/commands/archive.ts"() {
574
+ init_categories();
575
+ init_paths();
576
+ TYPE_TO_CATEGORY = {
577
+ plan: "plans",
578
+ roadmap: "roadmaps"
579
+ };
580
+ }
581
+ });
582
+
583
+ // src/commands/toggle.ts
584
+ var toggle_exports = {};
585
+ __export(toggle_exports, {
586
+ runDisable: () => runDisable,
587
+ runEnable: () => runEnable
588
+ });
589
+ async function runEnable(opts) {
590
+ const cfg = await readConfig(opts.root);
591
+ const addon = opts.addon;
592
+ if (!cfg.categories.includes(addon)) {
593
+ cfg.categories.push(addon);
594
+ await writeConfig(opts.root, cfg);
595
+ }
596
+ const dir = categoryDir(opts.root, addon);
597
+ await fs4.ensureDir(dir);
598
+ const readme = path2.join(dir, "README.md");
599
+ if (!await fs4.pathExists(readme)) {
600
+ await fs4.writeFile(readme, renderCategoryReadme(addon), "utf8");
601
+ }
602
+ if (isArchivable(addon)) {
603
+ await fs4.ensureDir(archiveDir(opts.root, addon));
604
+ }
605
+ await runSync({ root: opts.root });
606
+ }
607
+ async function runDisable(opts) {
608
+ const cfg = await readConfig(opts.root);
609
+ const dir = categoryDir(opts.root, opts.addon);
610
+ if (await fs4.pathExists(dir)) {
611
+ const entries = await fs4.readdir(dir);
612
+ const hasContent = entries.some(
613
+ (e) => e !== "README.md" && e !== "_archive"
614
+ );
615
+ if (hasContent && !opts.force) {
616
+ throw new Error(
617
+ `Folder ${dir} contains documents. Re-run with --force to disable anyway (files are kept on disk).`
618
+ );
619
+ }
620
+ }
621
+ cfg.categories = cfg.categories.filter((c) => c !== opts.addon);
622
+ await writeConfig(opts.root, cfg);
623
+ await runSync({ root: opts.root });
624
+ }
625
+ var init_toggle = __esm({
626
+ "src/commands/toggle.ts"() {
627
+ init_config();
628
+ init_paths();
629
+ init_categories();
630
+ init_readme();
631
+ init_sync();
632
+ }
633
+ });
634
+ async function walkDocs(root) {
635
+ const base = docsDir(root);
636
+ const matches = await fg(["**/*.md"], {
637
+ cwd: base,
638
+ dot: false,
639
+ onlyFiles: true
640
+ });
641
+ return matches.map((rel) => {
642
+ const segments = rel.split("/");
643
+ const category = segments[0] === "_meta" ? "_meta" : segments[0];
644
+ return {
645
+ absolutePath: path2.join(base, rel),
646
+ relativePath: rel,
647
+ // relative to docs/, matches test expectation
648
+ category,
649
+ basename: path2.basename(rel),
650
+ isReadme: path2.basename(rel) === "README.md",
651
+ isArchived: segments.includes("_archive")
652
+ };
653
+ });
654
+ }
655
+ var init_walker = __esm({
656
+ "src/linter/walker.ts"() {
657
+ init_paths();
658
+ }
659
+ });
660
+
661
+ // src/linter/index.ts
662
+ async function runLint(opts) {
663
+ const config = await readConfig(opts.root);
664
+ const files = await walkDocs(opts.root);
665
+ const ctx = { root: opts.root, config, files };
666
+ const findings = [];
667
+ for (const rule12 of opts.rules) {
668
+ const result = await rule12.check(ctx);
669
+ findings.push(...result);
670
+ }
671
+ const counts = { error: 0, warning: 0, suggestion: 0 };
672
+ for (const f of findings) counts[f.severity]++;
673
+ return {
674
+ findings,
675
+ errorCount: counts.error,
676
+ warningCount: counts.warning,
677
+ suggestionCount: counts.suggestion
678
+ };
679
+ }
680
+ var init_linter = __esm({
681
+ "src/linter/index.ts"() {
682
+ init_config();
683
+ init_walker();
684
+ }
685
+ });
686
+
687
+ // src/linter/reporter.ts
688
+ function formatText(findings) {
689
+ const lines = [];
690
+ const groups = { error: [], warning: [], suggestion: [] };
691
+ for (const f of findings) groups[f.severity].push(f);
692
+ function block(title, items) {
693
+ lines.push(`${title} (${items.length})`);
694
+ for (const f of items) {
695
+ const loc = f.file ? ` ${f.file}` : "";
696
+ lines.push(` [${f.rule}]${loc} \u2014 ${f.message}`);
697
+ }
698
+ lines.push("");
699
+ }
700
+ block("Errors", groups.error);
701
+ block("Warnings", groups.warning);
702
+ block("Suggestions", groups.suggestion);
703
+ return lines.join("\n");
704
+ }
705
+ function formatJson(findings) {
706
+ return JSON.stringify(findings, null, 2);
707
+ }
708
+ var init_reporter = __esm({
709
+ "src/linter/reporter.ts"() {
710
+ }
711
+ });
712
+
713
+ // src/linter/rules/suffix-location.ts
714
+ var SUFFIX_TO_CATEGORY, rule;
715
+ var init_suffix_location = __esm({
716
+ "src/linter/rules/suffix-location.ts"() {
717
+ init_categories();
718
+ SUFFIX_TO_CATEGORY = {};
719
+ for (const [, m] of Object.entries(CATEGORY_META)) {
720
+ if (m.suffix !== ".md") SUFFIX_TO_CATEGORY[m.suffix] = m.location;
721
+ }
722
+ rule = {
723
+ name: "suffix-location",
724
+ severity: "error",
725
+ check(ctx) {
726
+ const findings = [];
727
+ for (const f of ctx.files) {
728
+ if (f.isReadme) continue;
729
+ for (const [suffix, expectedDir] of Object.entries(SUFFIX_TO_CATEGORY)) {
730
+ if (f.basename.endsWith(suffix) && f.category !== expectedDir) {
731
+ findings.push({
732
+ severity: "error",
733
+ rule: "suffix-location",
734
+ file: f.relativePath,
735
+ message: `File with suffix "${suffix}" must live in docs/${expectedDir}/ (found in docs/${f.category}/).`
736
+ });
737
+ }
738
+ }
739
+ }
740
+ return findings;
741
+ }
742
+ };
743
+ }
744
+ });
745
+
746
+ // src/linter/rules/kebab-case.ts
747
+ var KEBAB_OR_ADR, rule2;
748
+ var init_kebab_case = __esm({
749
+ "src/linter/rules/kebab-case.ts"() {
750
+ KEBAB_OR_ADR = /^(ADR-\d{3}-)?[a-z0-9]+(-[a-z0-9]+)*(\.[a-z]+)*\.md$/;
751
+ rule2 = {
752
+ name: "kebab-case",
753
+ severity: "error",
754
+ check(ctx) {
755
+ const findings = [];
756
+ for (const f of ctx.files) {
757
+ if (f.isReadme) continue;
758
+ if (!KEBAB_OR_ADR.test(f.basename)) {
759
+ findings.push({
760
+ severity: "error",
761
+ rule: "kebab-case",
762
+ file: f.relativePath,
763
+ message: `Filename "${f.basename}" must be kebab-case (lowercase, hyphen-separated).`
764
+ });
765
+ }
766
+ }
767
+ return findings;
768
+ }
769
+ };
770
+ }
771
+ });
772
+
773
+ // src/linter/rules/eval-date-prefix.ts
774
+ var DATE_PREFIX, rule3;
775
+ var init_eval_date_prefix = __esm({
776
+ "src/linter/rules/eval-date-prefix.ts"() {
777
+ DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-.+\.eval\.md$/;
778
+ rule3 = {
779
+ name: "eval-date-prefix",
780
+ severity: "error",
781
+ check(ctx) {
782
+ const findings = [];
783
+ for (const f of ctx.files) {
784
+ if (!f.basename.endsWith(".eval.md")) continue;
785
+ if (!DATE_PREFIX.test(f.basename)) {
786
+ findings.push({
787
+ severity: "error",
788
+ rule: "eval-date-prefix",
789
+ file: f.relativePath,
790
+ message: `Eval file "${f.basename}" must start with YYYY-MM-DD-.`
791
+ });
792
+ }
793
+ }
794
+ return findings;
795
+ }
796
+ };
797
+ }
798
+ });
799
+
800
+ // src/linter/rules/readme-present.ts
801
+ var rule4;
802
+ var init_readme_present = __esm({
803
+ "src/linter/rules/readme-present.ts"() {
804
+ rule4 = {
805
+ name: "readme-present",
806
+ severity: "error",
807
+ check(ctx) {
808
+ const present = /* @__PURE__ */ new Set();
809
+ for (const f of ctx.files) {
810
+ if (f.isReadme && !f.isArchived) present.add(f.category);
811
+ }
812
+ const findings = [];
813
+ for (const c of ctx.config.categories) {
814
+ if (!present.has(c)) {
815
+ findings.push({
816
+ severity: "error",
817
+ rule: "readme-present",
818
+ message: `docs/${c}/README.md is required.`
819
+ });
820
+ }
821
+ }
822
+ return findings;
823
+ }
824
+ };
825
+ }
826
+ });
827
+ var rule5;
828
+ var init_ai_files_sync = __esm({
829
+ "src/linter/rules/ai-files-sync.ts"() {
830
+ init_claude();
831
+ init_agents();
832
+ init_gemini();
833
+ init_cursor();
834
+ rule5 = {
835
+ name: "ai-files-sync",
836
+ severity: "error",
837
+ async check(ctx) {
838
+ const findings = [];
839
+ const checks = [
840
+ { agent: "claude", filename: "CLAUDE.md", render: () => renderClaudeMd(ctx.config) },
841
+ { agent: "codex", filename: "AGENTS.md", render: () => renderAgentsMd(ctx.config) },
842
+ { agent: "gemini", filename: "GEMINI.md", render: () => renderGeminiMd(ctx.config) },
843
+ { agent: "cursor", filename: ".cursorrules", render: () => renderCursorRules(ctx.config) },
844
+ { agent: "cursor", filename: ".cursor/rules/beacon.mdc", render: () => renderCursorMdc(ctx.config) }
845
+ ];
846
+ for (const c of checks) {
847
+ if (!ctx.config.agents.includes(c.agent)) continue;
848
+ const file = path2.join(ctx.root, c.filename);
849
+ if (!await fs4.pathExists(file)) {
850
+ findings.push({
851
+ severity: "error",
852
+ rule: "ai-files-sync",
853
+ file: c.filename,
854
+ message: `${c.filename} missing. Run \`beacon sync\`.`
855
+ });
856
+ continue;
857
+ }
858
+ const actual = await fs4.readFile(file, "utf8");
859
+ const expected = c.render();
860
+ if (actual !== expected) {
861
+ findings.push({
862
+ severity: "error",
863
+ rule: "ai-files-sync",
864
+ file: c.filename,
865
+ message: `${c.filename} is out of sync with docs/_meta/convention.md. Run \`beacon sync\`.`
866
+ });
867
+ }
868
+ }
869
+ return findings;
870
+ }
871
+ };
872
+ }
873
+ });
874
+ var rule6;
875
+ var init_duplicate_titles = __esm({
876
+ "src/linter/rules/duplicate-titles.ts"() {
877
+ rule6 = {
878
+ name: "duplicate-titles",
879
+ severity: "warning",
880
+ async check(ctx) {
881
+ const titleMap = /* @__PURE__ */ new Map();
882
+ for (const f of ctx.files) {
883
+ if (f.isReadme || f.isArchived) continue;
884
+ const content = await fs4.readFile(f.absolutePath, "utf8");
885
+ const m = /^#\s+(.+)$/m.exec(content);
886
+ if (!m) continue;
887
+ const title = m[1].trim();
888
+ const arr = titleMap.get(title) ?? [];
889
+ arr.push(f.relativePath);
890
+ titleMap.set(title, arr);
891
+ }
892
+ const findings = [];
893
+ for (const [title, files] of titleMap) {
894
+ if (files.length > 1) {
895
+ findings.push({
896
+ severity: "warning",
897
+ rule: "duplicate-titles",
898
+ message: `Title "${title}" appears in: ${files.join(", ")}`
899
+ });
900
+ }
901
+ }
902
+ return findings;
903
+ }
904
+ };
905
+ }
906
+ });
907
+ var LIMIT, rule7;
908
+ var init_long_files = __esm({
909
+ "src/linter/rules/long-files.ts"() {
910
+ LIMIT = 1e3;
911
+ rule7 = {
912
+ name: "long-files",
913
+ severity: "warning",
914
+ async check(ctx) {
915
+ const findings = [];
916
+ for (const f of ctx.files) {
917
+ const content = await fs4.readFile(f.absolutePath, "utf8");
918
+ const lines = content.split("\n").length;
919
+ if (lines > LIMIT) {
920
+ findings.push({
921
+ severity: "warning",
922
+ rule: "long-files",
923
+ file: f.relativePath,
924
+ message: `File has ${lines} lines (>${LIMIT}). Consider splitting.`
925
+ });
926
+ }
927
+ }
928
+ return findings;
929
+ }
930
+ };
931
+ }
932
+ });
933
+
934
+ // src/linter/rules/folder-size.ts
935
+ var LIMIT2, rule8;
936
+ var init_folder_size = __esm({
937
+ "src/linter/rules/folder-size.ts"() {
938
+ LIMIT2 = 30;
939
+ rule8 = {
940
+ name: "folder-size",
941
+ severity: "warning",
942
+ check(ctx) {
943
+ const counts = /* @__PURE__ */ new Map();
944
+ for (const f of ctx.files) {
945
+ if (f.isReadme || f.isArchived) continue;
946
+ counts.set(f.category, (counts.get(f.category) ?? 0) + 1);
947
+ }
948
+ const findings = [];
949
+ for (const [cat, count] of counts) {
950
+ if (count > LIMIT2) {
951
+ findings.push({
952
+ severity: "warning",
953
+ rule: "folder-size",
954
+ message: `docs/${cat}/ has ${count} files (>${LIMIT2}). Consider organizing into subfolders.`
955
+ });
956
+ }
957
+ }
958
+ return findings;
959
+ }
960
+ };
961
+ }
962
+ });
963
+
964
+ // src/linter/rules/adr-numbering.ts
965
+ var ADR_FILE2, rule9;
966
+ var init_adr_numbering2 = __esm({
967
+ "src/linter/rules/adr-numbering.ts"() {
968
+ ADR_FILE2 = /^ADR-(\d{3})-.+\.md$/;
969
+ rule9 = {
970
+ name: "adr-numbering",
971
+ severity: "warning",
972
+ check(ctx) {
973
+ const numbers = [];
974
+ for (const f of ctx.files) {
975
+ if (f.category !== "adr") continue;
976
+ const m = ADR_FILE2.exec(f.basename);
977
+ if (m) numbers.push(parseInt(m[1], 10));
978
+ }
979
+ numbers.sort((a, b) => a - b);
980
+ const findings = [];
981
+ for (let i = 1; i < numbers.length; i++) {
982
+ const prev = numbers[i - 1];
983
+ const cur = numbers[i];
984
+ if (cur > prev + 1) {
985
+ for (let n = prev + 1; n < cur; n++) {
986
+ findings.push({
987
+ severity: "warning",
988
+ rule: "adr-numbering",
989
+ message: `ADR-${String(n).padStart(3, "0")} is missing. Intentional gaps (rejected/superseded) are OK; otherwise renumber.`
990
+ });
991
+ }
992
+ }
993
+ }
994
+ return findings;
995
+ }
996
+ };
997
+ }
998
+ });
999
+ var STALE_MS, rule10;
1000
+ var init_stale_plans = __esm({
1001
+ "src/linter/rules/stale-plans.ts"() {
1002
+ STALE_MS = 30 * 24 * 60 * 60 * 1e3;
1003
+ rule10 = {
1004
+ name: "stale-plans",
1005
+ severity: "suggestion",
1006
+ async check(ctx) {
1007
+ const findings = [];
1008
+ const now = Date.now();
1009
+ for (const f of ctx.files) {
1010
+ if (f.category !== "plans" || f.isArchived || f.isReadme) continue;
1011
+ const stat = await fs4.stat(f.absolutePath);
1012
+ if (now - stat.mtimeMs > STALE_MS) {
1013
+ findings.push({
1014
+ severity: "suggestion",
1015
+ rule: "stale-plans",
1016
+ file: f.relativePath,
1017
+ message: `Plan not modified in >30 days. If complete, archive it.`
1018
+ });
1019
+ }
1020
+ }
1021
+ return findings;
1022
+ }
1023
+ };
1024
+ }
1025
+ });
1026
+ var rule11;
1027
+ var init_adr_status = __esm({
1028
+ "src/linter/rules/adr-status.ts"() {
1029
+ rule11 = {
1030
+ name: "adr-status",
1031
+ severity: "suggestion",
1032
+ async check(ctx) {
1033
+ const findings = [];
1034
+ for (const f of ctx.files) {
1035
+ if (f.category !== "adr" || f.isReadme) continue;
1036
+ const content = await fs4.readFile(f.absolutePath, "utf8");
1037
+ const fm = matter(content).data;
1038
+ if (!fm.status) {
1039
+ findings.push({
1040
+ severity: "suggestion",
1041
+ rule: "adr-status",
1042
+ file: f.relativePath,
1043
+ message: "ADR missing `status:` frontmatter (proposed/accepted/superseded/rejected)."
1044
+ });
1045
+ }
1046
+ }
1047
+ return findings;
1048
+ }
1049
+ };
1050
+ }
1051
+ });
1052
+
1053
+ // src/commands/lint.ts
1054
+ var lint_exports = {};
1055
+ __export(lint_exports, {
1056
+ runLintCommand: () => runLintCommand
1057
+ });
1058
+ async function runLintCommand(opts) {
1059
+ const result = await runLint({ root: opts.root, rules: RULES });
1060
+ let exitCode = 0;
1061
+ if (result.errorCount > 0) exitCode = 1;
1062
+ if (opts.strict && result.warningCount > 0) exitCode = 1;
1063
+ const output = opts.json ? formatJson(result.findings) : formatText(result.findings);
1064
+ return { exitCode, output };
1065
+ }
1066
+ var RULES;
1067
+ var init_lint = __esm({
1068
+ "src/commands/lint.ts"() {
1069
+ init_linter();
1070
+ init_reporter();
1071
+ init_suffix_location();
1072
+ init_kebab_case();
1073
+ init_eval_date_prefix();
1074
+ init_readme_present();
1075
+ init_ai_files_sync();
1076
+ init_duplicate_titles();
1077
+ init_long_files();
1078
+ init_folder_size();
1079
+ init_adr_numbering2();
1080
+ init_stale_plans();
1081
+ init_adr_status();
1082
+ RULES = [
1083
+ rule,
1084
+ rule2,
1085
+ rule3,
1086
+ rule4,
1087
+ rule5,
1088
+ rule6,
1089
+ rule7,
1090
+ rule8,
1091
+ rule9,
1092
+ rule10,
1093
+ rule11
1094
+ ];
1095
+ }
1096
+ });
1097
+
1098
+ // src/core/project-types.ts
1099
+ var PROJECT_TYPES = [
1100
+ "web-app",
1101
+ "backend-service",
1102
+ "library",
1103
+ "cli-tool",
1104
+ "mobile-app",
1105
+ "monorepo",
1106
+ "custom"
1107
+ ];
1108
+ var CORE_CATEGORIES = [
1109
+ "reference",
1110
+ "architecture",
1111
+ "adr",
1112
+ "plans",
1113
+ "backlog",
1114
+ "evaluations"
1115
+ ];
1116
+ var ADDON_CATEGORIES = [
1117
+ "compliance",
1118
+ "business",
1119
+ "modules",
1120
+ "integrations",
1121
+ "operations",
1122
+ "roadmaps"
1123
+ ];
1124
+ var DEFAULTS_BY_TYPE = {
1125
+ "web-app": [
1126
+ ...CORE_CATEGORIES,
1127
+ "business",
1128
+ "integrations",
1129
+ "operations",
1130
+ "roadmaps"
1131
+ ],
1132
+ "backend-service": [
1133
+ ...CORE_CATEGORIES,
1134
+ "integrations",
1135
+ "operations",
1136
+ "roadmaps"
1137
+ ],
1138
+ library: [...CORE_CATEGORIES],
1139
+ "cli-tool": [...CORE_CATEGORIES, "operations"],
1140
+ "mobile-app": [
1141
+ ...CORE_CATEGORIES,
1142
+ "business",
1143
+ "integrations",
1144
+ "operations",
1145
+ "roadmaps"
1146
+ ],
1147
+ monorepo: [
1148
+ ...CORE_CATEGORIES,
1149
+ "modules",
1150
+ "integrations",
1151
+ "operations",
1152
+ "roadmaps"
1153
+ ],
1154
+ custom: []
1155
+ };
1156
+ function defaultCategoriesFor(type) {
1157
+ return [...DEFAULTS_BY_TYPE[type]];
1158
+ }
1159
+ function isValidProjectType(value) {
1160
+ return PROJECT_TYPES.includes(value);
1161
+ }
1162
+
1163
+ // src/generators/scaffold.ts
1164
+ init_config();
1165
+ init_categories();
1166
+ init_paths();
1167
+ init_readme();
1168
+
1169
+ // src/generators/convention.ts
1170
+ init_categories();
1171
+ function resolveTemplateDir2() {
1172
+ const here = path2.dirname(fileURLToPath(import.meta.url));
1173
+ const candidate = path2.join(here, "templates");
1174
+ if (fs4.existsSync(candidate)) return candidate;
1175
+ return path2.join(here, "..", "templates");
1176
+ }
1177
+ var TEMPLATE_DIR2 = resolveTemplateDir2();
1178
+ var CATEGORY_DESCRIPTIONS2 = {
1179
+ reference: "Replicable technical patterns.",
1180
+ architecture: "System structure and layering decisions.",
1181
+ adr: "Architecture Decision Records.",
1182
+ plans: "Active work with TODOs.",
1183
+ backlog: "Future items waiting to be sprinted.",
1184
+ evaluations: "Date-prefixed audits and snapshots.",
1185
+ compliance: "Regulatory and normative docs.",
1186
+ business: "Business model and product strategy.",
1187
+ modules: "Domain modules and their behavior.",
1188
+ integrations: "External service setup.",
1189
+ operations: "Runbooks and deploy guides.",
1190
+ roadmaps: "Multi-sprint planning."
1191
+ };
1192
+ function renderConvention(config) {
1193
+ const tpl = fs4.readFileSync(path2.join(TEMPLATE_DIR2, "convention.md.ejs"), "utf8");
1194
+ const suffixes = {};
1195
+ const suffixNotes = {};
1196
+ for (const c of config.categories) {
1197
+ suffixes[c] = CATEGORY_META[c].suffix;
1198
+ if (CATEGORY_META[c].numberedPrefix) suffixNotes[c] = `Auto-numbered (${CATEGORY_META[c].numberedPrefix}NNN-).`;
1199
+ if (CATEGORY_META[c].datePrefix) suffixNotes[c] = "Requires `YYYY-MM-DD-` prefix.";
1200
+ }
1201
+ return ejs.render(tpl, {
1202
+ projectType: config.projectType,
1203
+ categories: config.categories,
1204
+ descriptions: CATEGORY_DESCRIPTIONS2,
1205
+ suffixes,
1206
+ suffixNotes
1207
+ });
1208
+ }
1209
+
1210
+ // src/generators/scaffold.ts
1211
+ async function scaffoldStructure(root, config) {
1212
+ await fs4.ensureDir(metaDir(root));
1213
+ for (const category of config.categories) {
1214
+ const dir = categoryDir(root, category);
1215
+ await fs4.ensureDir(dir);
1216
+ if (isArchivable(category)) {
1217
+ await fs4.ensureDir(archiveDir(root, category));
1218
+ }
1219
+ const readmePath = path2.join(dir, "README.md");
1220
+ if (!await fs4.pathExists(readmePath)) {
1221
+ await fs4.writeFile(readmePath, renderCategoryReadme(category), "utf8");
1222
+ }
1223
+ }
1224
+ const masterReadmePath = path2.join(docsDir(root), "README.md");
1225
+ if (!await fs4.pathExists(masterReadmePath)) {
1226
+ await fs4.writeFile(masterReadmePath, renderMasterReadme(config), "utf8");
1227
+ }
1228
+ const convention = conventionPath(root);
1229
+ if (!await fs4.pathExists(convention)) {
1230
+ await fs4.writeFile(convention, renderConvention(config), "utf8");
1231
+ }
1232
+ await writeConfig(root, config);
1233
+ }
1234
+ async function addDocsLintScript(projectRoot) {
1235
+ const p2 = path2.join(projectRoot, "package.json");
1236
+ if (!await fs4.pathExists(p2)) return;
1237
+ const pkg = await fs4.readJson(p2);
1238
+ pkg.scripts ??= {};
1239
+ if (!pkg.scripts["docs:lint"]) {
1240
+ pkg.scripts["docs:lint"] = "beacon lint";
1241
+ await fs4.writeFile(p2, JSON.stringify(pkg, null, 2) + "\n", "utf8");
1242
+ }
1243
+ }
1244
+ var AGENT_FILES = [
1245
+ "CLAUDE.md",
1246
+ "AGENTS.md",
1247
+ "GEMINI.md",
1248
+ ".cursorrules"
1249
+ ];
1250
+ var INTEGRATIONS_HINTS = [
1251
+ "stripe",
1252
+ "@stripe/stripe-js",
1253
+ "mailgun-js",
1254
+ "twilio",
1255
+ "@sendgrid/mail",
1256
+ "resend"
1257
+ ];
1258
+ var DATABASE_HINTS = [
1259
+ "@prisma/client",
1260
+ "prisma",
1261
+ "drizzle-orm",
1262
+ "mongoose",
1263
+ "typeorm",
1264
+ "knex",
1265
+ "sequelize"
1266
+ ];
1267
+ async function detectContext(root) {
1268
+ const pkgPath = path2.join(root, "package.json");
1269
+ const tsconfigPath = path2.join(root, "tsconfig.json");
1270
+ const gitPath = path2.join(root, ".git");
1271
+ const hasPackageJson = await fs4.pathExists(pkgPath);
1272
+ const hasTsconfig = await fs4.pathExists(tsconfigPath);
1273
+ const hasGit = await fs4.pathExists(gitPath);
1274
+ let dependencies = [];
1275
+ if (hasPackageJson) {
1276
+ const pkg = await fs4.readJson(pkgPath);
1277
+ dependencies = [
1278
+ ...Object.keys(pkg.dependencies ?? {}),
1279
+ ...Object.keys(pkg.devDependencies ?? {})
1280
+ ];
1281
+ }
1282
+ const existingAgentFiles = [];
1283
+ for (const f of AGENT_FILES) {
1284
+ if (await fs4.pathExists(path2.join(root, f))) {
1285
+ existingAgentFiles.push(f);
1286
+ }
1287
+ }
1288
+ const suggestedAddons = [];
1289
+ if (dependencies.some((d) => INTEGRATIONS_HINTS.includes(d))) {
1290
+ suggestedAddons.push("integrations");
1291
+ }
1292
+ const suggestedSubfolders = [];
1293
+ if (dependencies.some((d) => DATABASE_HINTS.includes(d))) {
1294
+ suggestedSubfolders.push("reference/database");
1295
+ }
1296
+ return {
1297
+ hasPackageJson,
1298
+ hasTsconfig,
1299
+ hasGit,
1300
+ dependencies,
1301
+ existingAgentFiles,
1302
+ suggestedAddons,
1303
+ suggestedSubfolders
1304
+ };
1305
+ }
1306
+
1307
+ // src/commands/init.ts
1308
+ var ALL_CATEGORIES = /* @__PURE__ */ new Set([...CORE_CATEGORIES, ...ADDON_CATEGORIES]);
1309
+ async function runInit(options) {
1310
+ if (!isValidProjectType(options.type)) {
1311
+ throw new Error(
1312
+ `Unknown project type: "${options.type}". Valid types: ${PROJECT_TYPES.join(", ")}.`
1313
+ );
1314
+ }
1315
+ for (const c of [...options.with, ...options.without]) {
1316
+ if (!ALL_CATEGORIES.has(c)) {
1317
+ throw new Error(
1318
+ `Unknown category: "${c}". Valid categories: ${[...ALL_CATEGORIES].join(", ")}.`
1319
+ );
1320
+ }
1321
+ }
1322
+ const defaults = defaultCategoriesFor(options.type);
1323
+ const finalSet = new Set(defaults);
1324
+ for (const c of options.with) finalSet.add(c);
1325
+ for (const c of options.without) finalSet.delete(c);
1326
+ const config = {
1327
+ version: "1.0",
1328
+ projectType: options.type,
1329
+ categories: [...finalSet],
1330
+ agents: options.agents,
1331
+ language: options.language
1332
+ };
1333
+ await scaffoldStructure(options.root, config);
1334
+ await addDocsLintScript(options.root);
1335
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
1336
+ await runSync2({ root: options.root });
1337
+ return config;
1338
+ }
1339
+ async function runInitInteractive(opts) {
1340
+ p.intro("Beacon \u2014 initialize docs convention");
1341
+ const ctx = await detectContext(opts.root);
1342
+ const type = await p.select({
1343
+ message: "Project type?",
1344
+ options: [
1345
+ { value: "web-app", label: "Web Application (full-stack, SaaS)" },
1346
+ { value: "backend-service", label: "Backend Service / API" },
1347
+ { value: "library", label: "Library / SDK / Package" },
1348
+ { value: "cli-tool", label: "CLI Tool" },
1349
+ { value: "mobile-app", label: "Mobile App" },
1350
+ { value: "monorepo", label: "Monorepo / Workspace" },
1351
+ { value: "custom", label: "Custom (no defaults)" }
1352
+ ]
1353
+ });
1354
+ if (p.isCancel(type)) {
1355
+ p.cancel("Cancelled.");
1356
+ process.exit(0);
1357
+ }
1358
+ const defaults = defaultCategoriesFor(type);
1359
+ const categoryOptions = [...CORE_CATEGORIES, ...ADDON_CATEGORIES].map((c) => ({
1360
+ value: c,
1361
+ label: c,
1362
+ hint: ctx.suggestedAddons.includes(c) ? "suggested by detection" : void 0
1363
+ }));
1364
+ const categories = await p.multiselect({
1365
+ message: "Which categories to enable?",
1366
+ options: categoryOptions,
1367
+ initialValues: defaults,
1368
+ required: false
1369
+ });
1370
+ if (p.isCancel(categories)) {
1371
+ p.cancel("Cancelled.");
1372
+ process.exit(0);
1373
+ }
1374
+ const agents = await p.multiselect({
1375
+ message: "Which AI agents do you use?",
1376
+ options: [
1377
+ { value: "claude", label: "Claude Code" },
1378
+ { value: "cursor", label: "Cursor" },
1379
+ { value: "codex", label: "Codex / Copilot (AGENTS.md)" },
1380
+ { value: "gemini", label: "Gemini CLI" }
1381
+ ],
1382
+ initialValues: ["claude", "cursor"],
1383
+ required: true
1384
+ });
1385
+ if (p.isCancel(agents)) {
1386
+ p.cancel("Cancelled.");
1387
+ process.exit(0);
1388
+ }
1389
+ if (ctx.existingAgentFiles.length > 0) {
1390
+ const action = await p.select({
1391
+ message: `Existing files found (${ctx.existingAgentFiles.join(", ")}). What do you want to do?`,
1392
+ options: [
1393
+ { value: "merge", label: "Merge \u2014 keep existing content, append Beacon section" },
1394
+ { value: "replace", label: "Replace \u2014 overwrite with Beacon content" },
1395
+ { value: "skip", label: "Skip \u2014 leave them untouched" }
1396
+ ]
1397
+ });
1398
+ if (p.isCancel(action)) {
1399
+ p.cancel("Cancelled.");
1400
+ process.exit(0);
1401
+ }
1402
+ }
1403
+ const config = await runInit({
1404
+ root: opts.root,
1405
+ type,
1406
+ with: categories.filter((c) => !defaults.includes(c)),
1407
+ without: defaults.filter((c) => !categories.includes(c)),
1408
+ agents,
1409
+ language: "en"});
1410
+ p.outro(`Beacon docs scaffolded at ${opts.root}/docs/`);
1411
+ return config;
1412
+ }
1413
+
1414
+ // src/cli.ts
1415
+ var cli = cac("beacon");
1416
+ cli.command("init", "Initialize Beacon docs convention in this project").option("--yes", "Run non-interactively").option("--type <type>", "Project type (web-app, backend-service, library, cli-tool, mobile-app, monorepo, custom)").option("--with <categories>", "Categories to add beyond defaults (comma-separated)").option("--without <categories>", "Categories to remove from defaults (comma-separated)").option("--agents <agents>", "AI agents to support (comma-separated: claude,cursor,codex,gemini)").option("--language <lang>", "Docs language", { default: "en" }).action(async (opts) => {
1417
+ const root = process.cwd();
1418
+ if (opts.yes) {
1419
+ if (!opts.type) {
1420
+ console.error("Error: --type is required when using --yes.");
1421
+ process.exit(1);
1422
+ }
1423
+ await runInit({
1424
+ root,
1425
+ type: opts.type,
1426
+ with: splitList(opts.with),
1427
+ without: splitList(opts.without),
1428
+ agents: splitList(opts.agents).length ? splitList(opts.agents) : ["claude", "cursor"],
1429
+ language: opts.language ?? "en"});
1430
+ } else {
1431
+ await runInitInteractive({ root });
1432
+ }
1433
+ });
1434
+ cli.command("sync", "Regenerate AI rule files from docs/_meta/convention.md").action(async () => {
1435
+ const { runSync: runSync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
1436
+ await runSync2({ root: process.cwd() });
1437
+ console.log("\u2714 AI rule files regenerated.");
1438
+ });
1439
+ cli.command("new <type> <slug>", "Create a new doc with correct location and naming").option("--category <cat>", "Disambiguate for `guide` (integrations|operations)").action(async (type, slug, opts) => {
1440
+ const { runNew: runNew2 } = await Promise.resolve().then(() => (init_new(), new_exports));
1441
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1442
+ const file = await runNew2({
1443
+ root: process.cwd(),
1444
+ type,
1445
+ slug,
1446
+ today,
1447
+ category: opts.category
1448
+ });
1449
+ console.log(`\u2714 Created ${path2.relative(process.cwd(), file)}`);
1450
+ });
1451
+ cli.command("archive <type> <slug>", "Move a completed plan or roadmap to _archive/").option("--force", "Archive even if unchecked TODOs remain").action(async (type, slug, opts) => {
1452
+ const { runArchive: runArchive2 } = await Promise.resolve().then(() => (init_archive(), archive_exports));
1453
+ const result = await runArchive2({
1454
+ root: process.cwd(),
1455
+ type,
1456
+ slug,
1457
+ force: !!opts.force
1458
+ });
1459
+ if (result.warnings.length) {
1460
+ for (const w of result.warnings) console.warn(`\u26A0 ${w}`);
1461
+ }
1462
+ console.log(`\u2714 Archived to ${path2.relative(process.cwd(), result.destination)}`);
1463
+ });
1464
+ cli.command("enable <addon>", "Enable an add-on category").action(async (addon) => {
1465
+ const { runEnable: runEnable2 } = await Promise.resolve().then(() => (init_toggle(), toggle_exports));
1466
+ await runEnable2({ root: process.cwd(), addon });
1467
+ console.log(`\u2714 Enabled ${addon}.`);
1468
+ });
1469
+ cli.command("disable <addon>", "Disable an add-on category").option("--force", "Disable even if the folder has documents").action(async (addon, opts) => {
1470
+ const { runDisable: runDisable2 } = await Promise.resolve().then(() => (init_toggle(), toggle_exports));
1471
+ await runDisable2({ root: process.cwd(), addon, force: !!opts.force });
1472
+ console.log(`\u2714 Disabled ${addon}.`);
1473
+ });
1474
+ cli.command("lint", "Validate the docs tree against the convention").option("--strict", "Escalate warnings to errors").option("--json", "Emit JSON output").action(async (opts) => {
1475
+ const { runLintCommand: runLintCommand2 } = await Promise.resolve().then(() => (init_lint(), lint_exports));
1476
+ const result = await runLintCommand2({
1477
+ root: process.cwd(),
1478
+ strict: !!opts.strict,
1479
+ json: !!opts.json
1480
+ });
1481
+ process.stdout.write(result.output);
1482
+ if (!opts.json && !result.output.endsWith("\n")) process.stdout.write("\n");
1483
+ process.exit(result.exitCode);
1484
+ });
1485
+ cli.help();
1486
+ cli.version("0.0.0");
1487
+ cli.parse();
1488
+ function splitList(s) {
1489
+ if (!s) return [];
1490
+ return s.split(",").map((x) => x.trim()).filter(Boolean);
1491
+ }
1492
+ //# sourceMappingURL=cli.js.map
1493
+ //# sourceMappingURL=cli.js.map