codebyplan 1.13.37 → 1.13.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +682 -38
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.13.37";
17
+ VERSION = "1.13.38";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -698,7 +698,7 @@ function isRetryable(err) {
698
698
  return false;
699
699
  }
700
700
  function delay(ms) {
701
- return new Promise((resolve11) => setTimeout(resolve11, ms));
701
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
702
702
  }
703
703
  async function request(method, path10, options) {
704
704
  const url = buildUrl(path10, options?.params);
@@ -1056,7 +1056,7 @@ var init_device_flow = __esm({
1056
1056
  this.name = "OAuthInvalidClientError";
1057
1057
  }
1058
1058
  };
1059
- defaultSleep = (ms) => new Promise((resolve11) => setTimeout(resolve11, ms));
1059
+ defaultSleep = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
1060
1060
  }
1061
1061
  });
1062
1062
 
@@ -4670,7 +4670,7 @@ function setRetryDelayMs(ms) {
4670
4670
  RETRY_DELAY_MS = ms;
4671
4671
  }
4672
4672
  function sleep(ms) {
4673
- return new Promise((resolve11) => setTimeout(resolve11, ms));
4673
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
4674
4674
  }
4675
4675
  function isTransientMcpError(err) {
4676
4676
  if (!(err instanceof McpError)) return false;
@@ -8616,11 +8616,11 @@ async function ask(q, opts) {
8616
8616
  try {
8617
8617
  while (true) {
8618
8618
  const choices = q.choices.map((c) => `[${c.key}] ${c.label}`).join(" ");
8619
- const answer = await new Promise((resolve11) => {
8619
+ const answer = await new Promise((resolve12) => {
8620
8620
  rl.question(`${q.message}
8621
8621
  ${choices}
8622
8622
  > `, (input) => {
8623
- resolve11(input.trim().toLowerCase());
8623
+ resolve12(input.trim().toLowerCase());
8624
8624
  });
8625
8625
  });
8626
8626
  const match = q.choices.find(
@@ -9326,6 +9326,293 @@ var init_structure_generator = __esm({
9326
9326
  }
9327
9327
  });
9328
9328
 
9329
+ // src/lib/readme-generator.ts
9330
+ function managedStartMarker(section) {
9331
+ return `${MANAGED_START_PREFIX}${section}${MANAGED_MARKER_SUFFIX}`;
9332
+ }
9333
+ function managedEndMarker(section) {
9334
+ return `${MANAGED_END_PREFIX}${section}${MANAGED_MARKER_SUFFIX}`;
9335
+ }
9336
+ function hasManagedMarkers(content) {
9337
+ return content.includes(MANAGED_START_PREFIX) || content.includes(MANAGED_END_PREFIX);
9338
+ }
9339
+ function validateManagedMarkers(content) {
9340
+ const errors = [];
9341
+ const startRe = /<!-- codebyplan:managed:start:([a-z0-9][a-z0-9-]*) -->/g;
9342
+ const endRe = /<!-- codebyplan:managed:end:([a-z0-9][a-z0-9-]*) -->/g;
9343
+ const starts = [...content.matchAll(startRe)].map((m) => m[1]);
9344
+ const ends = [...content.matchAll(endRe)].map((m) => m[1]);
9345
+ const seenStarts = /* @__PURE__ */ new Set();
9346
+ for (const s of starts) {
9347
+ if (seenStarts.has(s)) {
9348
+ errors.push(`duplicate managed section '${s}'`);
9349
+ }
9350
+ seenStarts.add(s);
9351
+ if (!ends.includes(s)) {
9352
+ errors.push(`missing end marker for section '${s}'`);
9353
+ }
9354
+ }
9355
+ for (const e of ends) {
9356
+ if (!starts.includes(e)) {
9357
+ errors.push(`end marker without start marker for section '${e}'`);
9358
+ }
9359
+ }
9360
+ return errors;
9361
+ }
9362
+ function renderTechStackSection(techStack) {
9363
+ const entries = Object.entries(techStack).filter(([, value]) => value.trim().length > 0).sort(([a], [b]) => a.localeCompare(b));
9364
+ if (entries.length === 0) return "";
9365
+ const lines = entries.map(([cat, names]) => `- **${cat}**: ${names}`);
9366
+ return `## Tech Stack
9367
+
9368
+ ${lines.join("\n")}
9369
+ `;
9370
+ }
9371
+ function renderStructureSection(packages) {
9372
+ if (packages.length === 0) return "";
9373
+ const sorted = [...packages].sort((a, b) => a.path.localeCompare(b.path));
9374
+ const rows = sorted.map(
9375
+ (p) => `| \`${p.path}\` | \`${p.name}\` | ${p.purpose ?? ""} |`
9376
+ );
9377
+ return `## Structure
9378
+
9379
+ | Location | Package | Purpose |
9380
+ |----------|---------|----------|
9381
+ ` + rows.join("\n") + "\n";
9382
+ }
9383
+ function renderScriptsSection(scripts, installCommand) {
9384
+ const sortedEntries = Object.entries(scripts).filter(([, cmd]) => typeof cmd === "string" && cmd.trim().length > 0).sort(([a], [b]) => a.localeCompare(b));
9385
+ const lines = [];
9386
+ lines.push(`\`\`\`bash`);
9387
+ lines.push(`# Install`);
9388
+ lines.push(installCommand);
9389
+ lines.push(``);
9390
+ if (sortedEntries.length > 0) {
9391
+ lines.push(`# Available scripts`);
9392
+ for (const [name, cmd] of sortedEntries) {
9393
+ lines.push(`# ${name}`);
9394
+ lines.push(cmd);
9395
+ }
9396
+ }
9397
+ lines.push(`\`\`\``);
9398
+ return `## Scripts
9399
+
9400
+ ${lines.join("\n")}
9401
+ `;
9402
+ }
9403
+ function buildManagedSections(config) {
9404
+ const sections = {};
9405
+ if (config.techStack && Object.keys(config.techStack).length > 0) {
9406
+ const rendered = renderTechStackSection(config.techStack);
9407
+ if (rendered) {
9408
+ sections["tech-stack"] = rendered;
9409
+ }
9410
+ }
9411
+ if (config.packages && config.packages.length > 0) {
9412
+ const rendered = renderStructureSection(config.packages);
9413
+ if (rendered) {
9414
+ sections["structure"] = rendered;
9415
+ }
9416
+ }
9417
+ if (config.scripts && Object.keys(config.scripts).length > 0) {
9418
+ const pm = config.packageManager ?? "pnpm";
9419
+ const pmBase = pm.split("@")[0] ?? "pnpm";
9420
+ const installCmd = config.installCommand ?? `${pmBase} install`;
9421
+ const rendered = renderScriptsSection(config.scripts, installCmd);
9422
+ if (rendered) {
9423
+ sections["scripts"] = rendered;
9424
+ }
9425
+ }
9426
+ return sections;
9427
+ }
9428
+ function hashManagedContent(sections) {
9429
+ const sortedKeys = Object.keys(sections).sort();
9430
+ const payload = {};
9431
+ for (const k of sortedKeys) {
9432
+ payload[k] = sections[k];
9433
+ }
9434
+ return sha256(JSON.stringify(payload));
9435
+ }
9436
+ function scaffoldReadme(config) {
9437
+ const title = config.name ?? "Project";
9438
+ const description = config.description ?? "";
9439
+ const sections = buildManagedSections(config);
9440
+ const hash = hashManagedContent(sections);
9441
+ const lines = [];
9442
+ lines.push(`# ${title}`);
9443
+ lines.push(``);
9444
+ if (description) {
9445
+ lines.push(description);
9446
+ lines.push(``);
9447
+ }
9448
+ const sectionKeys = Object.keys(sections).sort();
9449
+ for (const key of sectionKeys) {
9450
+ lines.push(managedStartMarker(key));
9451
+ lines.push(sections[key].trimEnd());
9452
+ lines.push(managedEndMarker(key));
9453
+ lines.push(``);
9454
+ }
9455
+ lines.push(`${HASH_MARKER_PREFIX}${hash}${MANAGED_MARKER_SUFFIX}`);
9456
+ lines.push(``);
9457
+ return lines.join("\n");
9458
+ }
9459
+ function refreshManagedSections(existing, sections) {
9460
+ let content = existing;
9461
+ const startPattern = new RegExp(
9462
+ `${escapeRegex(MANAGED_START_PREFIX)}([a-z0-9][a-z0-9-]*)${escapeRegex(MANAGED_MARKER_SUFFIX)}[\\s\\S]*?${escapeRegex(MANAGED_END_PREFIX)}\\1${escapeRegex(MANAGED_MARKER_SUFFIX)}`,
9463
+ "g"
9464
+ );
9465
+ const seen = /* @__PURE__ */ new Set();
9466
+ content = content.replace(startPattern, (_, sectionName) => {
9467
+ const key = sectionName.trim();
9468
+ seen.add(key);
9469
+ if (Object.prototype.hasOwnProperty.call(sections, key)) {
9470
+ const newBody = sections[key].trimEnd();
9471
+ return managedStartMarker(key) + "\n" + newBody + "\n" + managedEndMarker(key);
9472
+ } else {
9473
+ return "";
9474
+ }
9475
+ });
9476
+ content = content.replace(/\n{3,}/g, "\n\n");
9477
+ const newSectionKeys = Object.keys(sections).sort().filter((k) => !seen.has(k));
9478
+ if (newSectionKeys.length > 0) {
9479
+ const hashCommentPattern = new RegExp(
9480
+ `
9481
+ ${escapeRegex(HASH_MARKER_PREFIX)}[^
9482
+ ]*${escapeRegex(MANAGED_MARKER_SUFFIX)}(
9483
+ |$)`
9484
+ );
9485
+ content = content.replace(hashCommentPattern, "\n");
9486
+ if (!content.endsWith("\n")) content += "\n";
9487
+ for (const key of newSectionKeys) {
9488
+ content += "\n" + managedStartMarker(key) + "\n" + sections[key].trimEnd() + "\n" + managedEndMarker(key) + "\n";
9489
+ }
9490
+ }
9491
+ const newHash = hashManagedContent(sections);
9492
+ const hashLine = `${HASH_MARKER_PREFIX}${newHash}${MANAGED_MARKER_SUFFIX}`;
9493
+ const existingHashPattern = new RegExp(
9494
+ `${escapeRegex(HASH_MARKER_PREFIX)}[^
9495
+ ]*${escapeRegex(MANAGED_MARKER_SUFFIX)}`
9496
+ );
9497
+ if (existingHashPattern.test(content)) {
9498
+ content = content.replace(existingHashPattern, hashLine);
9499
+ } else {
9500
+ if (!content.endsWith("\n")) content += "\n";
9501
+ content += `${hashLine}
9502
+ `;
9503
+ }
9504
+ content = content.trimEnd() + "\n";
9505
+ return content;
9506
+ }
9507
+ function escapeRegex(str) {
9508
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9509
+ }
9510
+ function extractManagedHash(content) {
9511
+ const hashPattern = new RegExp(
9512
+ `${escapeRegex(HASH_MARKER_PREFIX)}(sha256:[0-9a-f]{64})${escapeRegex(MANAGED_MARKER_SUFFIX)}`
9513
+ );
9514
+ const m = hashPattern.exec(content);
9515
+ return m?.[1] ?? null;
9516
+ }
9517
+ var MANAGED_START_PREFIX, MANAGED_END_PREFIX, MANAGED_MARKER_SUFFIX, HASH_MARKER_PREFIX;
9518
+ var init_readme_generator = __esm({
9519
+ "src/lib/readme-generator.ts"() {
9520
+ "use strict";
9521
+ init_hash();
9522
+ MANAGED_START_PREFIX = "<!-- codebyplan:managed:start:";
9523
+ MANAGED_END_PREFIX = "<!-- codebyplan:managed:end:";
9524
+ MANAGED_MARKER_SUFFIX = " -->";
9525
+ HASH_MARKER_PREFIX = "<!-- codebyplan:hash:";
9526
+ }
9527
+ });
9528
+
9529
+ // src/lib/agents-generator.ts
9530
+ function stripStructureSentinels(structureContent) {
9531
+ const lines = structureContent.split("\n");
9532
+ const filtered = lines.filter((line) => {
9533
+ const trimmed = line.trim();
9534
+ return !trimmed.startsWith("<!-- @codebyplan-generated:");
9535
+ });
9536
+ let result = filtered.join("\n");
9537
+ result = result.replace(/^\n+/, "").replace(/\n+$/, "");
9538
+ return result;
9539
+ }
9540
+ function buildCuratedGuidance(cleanStructureContent) {
9541
+ const lines = [];
9542
+ lines.push(`## Project Context`);
9543
+ lines.push(``);
9544
+ lines.push(
9545
+ `This file is auto-generated for cross-tool compatibility. The canonical source of truth for project instructions is \`.claude/CLAUDE.md\`. If your tool supports Claude Code's \`@\` import syntax, read \`.claude/CLAUDE.md\` directly.`
9546
+ );
9547
+ lines.push(``);
9548
+ if (cleanStructureContent.trim().length > 0) {
9549
+ lines.push(cleanStructureContent.trim());
9550
+ } else {
9551
+ lines.push(
9552
+ `_Structure not yet generated. Run \`codebyplan claude generate\` to populate._`
9553
+ );
9554
+ }
9555
+ lines.push(``);
9556
+ lines.push(`## Source of Truth`);
9557
+ lines.push(``);
9558
+ lines.push(
9559
+ `The database is the sole source of truth for all development data. No local markdown files for tracking \u2014 everything is in the DB, accessed via MCP tools or the Web UI.`
9560
+ );
9561
+ lines.push(``);
9562
+ lines.push(`## \`.claude/\` Layout`);
9563
+ lines.push(``);
9564
+ lines.push(`| Directory | Purpose |`);
9565
+ lines.push(`|-----------|---------|`);
9566
+ lines.push(`| \`rules/\` | Auto-loaded behavioral constraints |`);
9567
+ lines.push(`| \`skills/\` | User-invocable \`/cbp-*\` commands |`);
9568
+ lines.push(`| \`agents/\` | Spawned sub-agents |`);
9569
+ lines.push(`| \`context/\` | Agent/skill input data |`);
9570
+ lines.push(
9571
+ `| \`CLAUDE.md\` | Canonical project context (always loaded by Claude Code) |`
9572
+ );
9573
+ lines.push(``);
9574
+ lines.push(`## Port Rule`);
9575
+ lines.push(``);
9576
+ lines.push(
9577
+ `Never hardcode port numbers. Resolve from \`.codebyplan/server.json\` \`port_allocations[]\`. Each entry carries \`port\`, \`label\`, and \`server_type\`.`
9578
+ );
9579
+ lines.push(``);
9580
+ lines.push(`## Git`);
9581
+ lines.push(``);
9582
+ lines.push(
9583
+ `Do not include any mention of AI tools or code-assistant products in git commit messages.`
9584
+ );
9585
+ lines.push(``);
9586
+ return lines.join("\n");
9587
+ }
9588
+ function generateAgentsMd(structureContent) {
9589
+ const cleanStructure = stripStructureSentinels(structureContent);
9590
+ const bodyContent = buildCuratedGuidance(cleanStructure);
9591
+ const managedBody = [
9592
+ managedStartMarker(AGENTS_SECTION_NAME),
9593
+ bodyContent.trimEnd(),
9594
+ managedEndMarker(AGENTS_SECTION_NAME)
9595
+ ].join("\n");
9596
+ const hash = hashManagedContent({ [AGENTS_SECTION_NAME]: bodyContent });
9597
+ const hashLine = `${HASH_MARKER_PREFIX}${hash}${MANAGED_MARKER_SUFFIX}`;
9598
+ const header = [
9599
+ `<!-- GENERATED \u2014 do not hand-edit. Canonical source: .claude/CLAUDE.md -->`,
9600
+ `<!-- Regenerate: codebyplan claude generate -->`
9601
+ ].join("\n");
9602
+ return [header, ``, managedBody, ``, hashLine, ``].join("\n");
9603
+ }
9604
+ function extractAgentsHash(content) {
9605
+ return extractManagedHash(content);
9606
+ }
9607
+ var AGENTS_SECTION_NAME;
9608
+ var init_agents_generator = __esm({
9609
+ "src/lib/agents-generator.ts"() {
9610
+ "use strict";
9611
+ init_readme_generator();
9612
+ AGENTS_SECTION_NAME = "agents-context";
9613
+ }
9614
+ });
9615
+
9329
9616
  // src/cli/claude/generate.ts
9330
9617
  var generate_exports = {};
9331
9618
  __export(generate_exports, {
@@ -9353,6 +9640,7 @@ async function readPkgName(absPath) {
9353
9640
  async function runGenerate(opts) {
9354
9641
  const projectDir = opts.projectDir ?? opts["project-dir"] ?? process.cwd();
9355
9642
  const dryRun = opts.dryRun ?? opts["dryRun"] ?? false;
9643
+ const check = opts.check ?? opts["check"] ?? false;
9356
9644
  const rootDir = resolve8(projectDir);
9357
9645
  let packageManager;
9358
9646
  try {
@@ -9454,28 +9742,362 @@ async function runGenerate(opts) {
9454
9742
  branchModel,
9455
9743
  shipmentSurfaces: shipmentSurfaces.length > 0 ? shipmentSurfaces : void 0
9456
9744
  };
9457
- const content = generateStructureMd(config);
9745
+ const structureMdContent = generateStructureMd(config);
9746
+ const agentsContent = generateAgentsMd(structureMdContent);
9747
+ if (check) {
9748
+ const agentsMdPath2 = join30(rootDir, "AGENTS.md");
9749
+ let existingAgents = null;
9750
+ try {
9751
+ existingAgents = await readFile21(agentsMdPath2, "utf-8");
9752
+ } catch {
9753
+ existingAgents = null;
9754
+ }
9755
+ if (existingAgents === null) {
9756
+ process.stdout.write(`AGENTS.md missing
9757
+ `);
9758
+ process.exitCode = 1;
9759
+ } else {
9760
+ const existingHash = extractAgentsHash(existingAgents);
9761
+ const freshEmbeddedHash = extractAgentsHash(agentsContent);
9762
+ if (existingHash !== freshEmbeddedHash) {
9763
+ process.stdout.write(`AGENTS.md drifted
9764
+ `);
9765
+ process.exitCode = 1;
9766
+ } else {
9767
+ process.stdout.write(`AGENTS.md up to date
9768
+ `);
9769
+ }
9770
+ }
9771
+ return;
9772
+ }
9458
9773
  if (dryRun) {
9459
9774
  process.stdout.write(
9460
9775
  `[dry-run] Would write: .claude/generated/structure.md
9461
9776
 
9462
9777
  `
9463
9778
  );
9464
- process.stdout.write(content);
9779
+ process.stdout.write(structureMdContent);
9780
+ process.stdout.write(`
9781
+ [dry-run] Would write: AGENTS.md
9782
+
9783
+ `);
9784
+ process.stdout.write(agentsContent);
9465
9785
  return;
9466
9786
  }
9467
9787
  const outputDir = join30(rootDir, ".claude", "generated");
9468
9788
  await mkdir10(outputDir, { recursive: true });
9469
9789
  const outputPath = join30(outputDir, "structure.md");
9470
- await writeFile16(outputPath, content, "utf-8");
9790
+ await writeFile16(outputPath, structureMdContent, "utf-8");
9471
9791
  process.stdout.write(`Wrote: .claude/generated/structure.md
9472
9792
  `);
9793
+ const agentsMdPath = join30(rootDir, "AGENTS.md");
9794
+ let existingAgentsContent = null;
9795
+ try {
9796
+ existingAgentsContent = await readFile21(agentsMdPath, "utf-8");
9797
+ } catch {
9798
+ existingAgentsContent = null;
9799
+ }
9800
+ if (existingAgentsContent !== null && existingAgentsContent === agentsContent) {
9801
+ process.stdout.write(`Up to date: AGENTS.md
9802
+ `);
9803
+ } else {
9804
+ await writeFile16(agentsMdPath, agentsContent, "utf-8");
9805
+ process.stdout.write(`Wrote: AGENTS.md
9806
+ `);
9807
+ }
9473
9808
  }
9474
9809
  var init_generate = __esm({
9475
9810
  "src/cli/claude/generate.ts"() {
9476
9811
  "use strict";
9477
9812
  init_tech_detect();
9478
9813
  init_structure_generator();
9814
+ init_agents_generator();
9815
+ }
9816
+ });
9817
+
9818
+ // src/cli/claude/readme.ts
9819
+ var readme_exports = {};
9820
+ __export(readme_exports, {
9821
+ runReadme: () => runReadme,
9822
+ runReadmeCommand: () => runReadmeCommand
9823
+ });
9824
+ import { readFile as readFile22, writeFile as writeFile17 } from "node:fs/promises";
9825
+ import { join as join31, resolve as resolve9, relative as relative8 } from "node:path";
9826
+ async function readJsonFile4(filePath) {
9827
+ try {
9828
+ const raw = await readFile22(filePath, "utf-8");
9829
+ return JSON.parse(raw);
9830
+ } catch {
9831
+ return null;
9832
+ }
9833
+ }
9834
+ async function buildConfig(absPath, isRoot, allPackages, rootPkgJson, pkgJson) {
9835
+ let techStack;
9836
+ try {
9837
+ const techResult = await detectTechStack(absPath);
9838
+ const CATEGORY_LABELS = {
9839
+ "component-lib": "Component Library",
9840
+ graphql: "GraphQL",
9841
+ quality: "Code Quality"
9842
+ };
9843
+ const categoryMap = {};
9844
+ for (const entry of techResult.flat) {
9845
+ if (entry.name === SYNTHETIC_CARRIER_NAME) continue;
9846
+ if (!categoryMap[entry.category]) {
9847
+ categoryMap[entry.category] = [];
9848
+ }
9849
+ categoryMap[entry.category].push(entry.name);
9850
+ }
9851
+ if ((categoryMap["mobile"]?.length ?? 0) > 0) {
9852
+ const frameworkEntries = categoryMap["framework"] ?? [];
9853
+ const filtered = frameworkEntries.filter((n) => n !== "React");
9854
+ if (filtered.length > 0) {
9855
+ categoryMap["framework"] = filtered;
9856
+ } else {
9857
+ delete categoryMap["framework"];
9858
+ }
9859
+ }
9860
+ if (Object.keys(categoryMap).length > 0) {
9861
+ techStack = {};
9862
+ for (const [cat, names] of Object.entries(categoryMap)) {
9863
+ const label = CATEGORY_LABELS[cat] ?? cat.charAt(0).toUpperCase() + cat.slice(1);
9864
+ techStack[label] = [...names].sort().join(" + ");
9865
+ }
9866
+ }
9867
+ } catch {
9868
+ }
9869
+ const packages = isRoot ? allPackages : void 0;
9870
+ const packageManager = typeof rootPkgJson?.packageManager === "string" ? rootPkgJson.packageManager : void 0;
9871
+ const pmBase = packageManager?.split("@")[0] ?? "pnpm";
9872
+ const installCommand = `${pmBase} install`;
9873
+ return {
9874
+ name: pkgJson?.name ?? void 0,
9875
+ description: pkgJson?.description ?? void 0,
9876
+ scripts: pkgJson?.scripts && Object.keys(pkgJson.scripts).length > 0 ? pkgJson.scripts : void 0,
9877
+ packageManager,
9878
+ techStack,
9879
+ packages: packages && packages.length > 0 ? packages : void 0,
9880
+ installCommand
9881
+ };
9882
+ }
9883
+ async function discoverUnits(rootDir, rootPkgJson) {
9884
+ const units = [];
9885
+ const allPackages = [];
9886
+ const pkgJsonByPath = /* @__PURE__ */ new Map();
9887
+ units.push({
9888
+ name: "root",
9889
+ path: ".",
9890
+ absPath: rootDir,
9891
+ isRoot: true
9892
+ });
9893
+ pkgJsonByPath.set(rootDir, rootPkgJson);
9894
+ const discovered = await discoverMonorepoApps(rootDir);
9895
+ for (const app of discovered) {
9896
+ const pkgJson = await readJsonFile4(
9897
+ join31(app.absPath, "package.json")
9898
+ );
9899
+ pkgJsonByPath.set(app.absPath, pkgJson);
9900
+ allPackages.push({
9901
+ path: app.path,
9902
+ name: pkgJson?.name ?? app.name,
9903
+ purpose: pkgJson?.description ?? void 0
9904
+ });
9905
+ const isApp = app.path === "apps" || app.path.startsWith("apps/");
9906
+ if (!isApp && pkgJson?.private === true) continue;
9907
+ units.push({
9908
+ name: app.name,
9909
+ path: app.path,
9910
+ absPath: app.absPath,
9911
+ isRoot: false
9912
+ });
9913
+ }
9914
+ return { units, allPackages, pkgJsonByPath };
9915
+ }
9916
+ async function runReadme(opts) {
9917
+ const projectDir = opts.projectDir ?? opts["project-dir"] ?? process.cwd();
9918
+ const dryRun = opts.dryRun ?? opts["dryRun"] ?? false;
9919
+ const check = opts.check ?? opts["check"] ?? false;
9920
+ const init = opts.init ?? opts["init"] ?? false;
9921
+ const rootDir = resolve9(projectDir);
9922
+ const rootPkgJson = await readJsonFile4(
9923
+ join31(rootDir, "package.json")
9924
+ );
9925
+ const { units, allPackages, pkgJsonByPath } = await discoverUnits(
9926
+ rootDir,
9927
+ rootPkgJson
9928
+ );
9929
+ const driftUnits = [];
9930
+ const missingUnits = [];
9931
+ for (const unit of units) {
9932
+ const readmePath = join31(unit.absPath, "README.md");
9933
+ const relPath = unit.isRoot ? "README.md" : join31(relative8(rootDir, unit.absPath), "README.md");
9934
+ let existingContent = null;
9935
+ try {
9936
+ existingContent = await readFile22(readmePath, "utf-8");
9937
+ } catch {
9938
+ existingContent = null;
9939
+ }
9940
+ let config;
9941
+ try {
9942
+ config = await buildConfig(
9943
+ unit.absPath,
9944
+ unit.isRoot,
9945
+ allPackages,
9946
+ rootPkgJson,
9947
+ pkgJsonByPath.get(unit.absPath) ?? null
9948
+ );
9949
+ } catch (err) {
9950
+ process.stderr.write(
9951
+ `readme: error building config for ${relPath}: ${err instanceof Error ? err.message : String(err)}
9952
+ `
9953
+ );
9954
+ continue;
9955
+ }
9956
+ const sections = buildManagedSections(config);
9957
+ if (existingContent === null) {
9958
+ if (check) {
9959
+ missingUnits.push(relPath);
9960
+ continue;
9961
+ }
9962
+ const newContent = scaffoldReadme(config);
9963
+ if (dryRun) {
9964
+ process.stdout.write(
9965
+ `[dry-run] Would write (scaffold): ${relPath}
9966
+
9967
+ ${newContent}
9968
+ `
9969
+ );
9970
+ } else {
9971
+ await writeFile17(readmePath, newContent, "utf-8");
9972
+ process.stdout.write(`Wrote (scaffold): ${relPath}
9973
+ `);
9974
+ }
9975
+ } else if (hasManagedMarkers(existingContent)) {
9976
+ const markerErrors = validateManagedMarkers(existingContent);
9977
+ if (markerErrors.length > 0) {
9978
+ process.stderr.write(
9979
+ `readme: skipping ${relPath} \u2014 malformed managed markers: ${markerErrors.join("; ")}
9980
+ `
9981
+ );
9982
+ if (check) {
9983
+ driftUnits.push(`${relPath} (malformed markers)`);
9984
+ }
9985
+ continue;
9986
+ }
9987
+ const existingHash = extractManagedHash(existingContent);
9988
+ const newHash = hashManagedContent(sections);
9989
+ if (existingHash === newHash) {
9990
+ if (!check) {
9991
+ process.stdout.write(`Up to date: ${relPath}
9992
+ `);
9993
+ }
9994
+ continue;
9995
+ }
9996
+ if (check) {
9997
+ driftUnits.push(relPath);
9998
+ continue;
9999
+ }
10000
+ const newContent = refreshManagedSections(existingContent, sections);
10001
+ if (dryRun) {
10002
+ process.stdout.write(
10003
+ `[dry-run] Would write (refresh): ${relPath}
10004
+
10005
+ ${newContent}
10006
+ `
10007
+ );
10008
+ } else {
10009
+ await writeFile17(readmePath, newContent, "utf-8");
10010
+ process.stdout.write(`Wrote (refresh): ${relPath}
10011
+ `);
10012
+ }
10013
+ } else {
10014
+ if (init) {
10015
+ if (check) {
10016
+ driftUnits.push(`${relPath} (no markers, needs --init)`);
10017
+ continue;
10018
+ }
10019
+ const newContent = appendManagedBlock(existingContent, sections);
10020
+ if (dryRun) {
10021
+ process.stdout.write(
10022
+ `[dry-run] Would write (init): ${relPath}
10023
+
10024
+ ${newContent}
10025
+ `
10026
+ );
10027
+ } else {
10028
+ await writeFile17(readmePath, newContent, "utf-8");
10029
+ process.stdout.write(`Wrote (init): ${relPath}
10030
+ `);
10031
+ }
10032
+ } else {
10033
+ process.stdout.write(`Skipped (no markers, use --init): ${relPath}
10034
+ `);
10035
+ }
10036
+ }
10037
+ }
10038
+ if (check) {
10039
+ if (driftUnits.length > 0 || missingUnits.length > 0) {
10040
+ if (driftUnits.length > 0) {
10041
+ process.stdout.write(
10042
+ `
10043
+ README drift detected:
10044
+ ${driftUnits.map((f) => ` - ${f}`).join("\n")}
10045
+ `
10046
+ );
10047
+ }
10048
+ if (missingUnits.length > 0) {
10049
+ process.stdout.write(
10050
+ `
10051
+ README missing:
10052
+ ${missingUnits.map((f) => ` - ${f}`).join("\n")}
10053
+ `
10054
+ );
10055
+ }
10056
+ process.exitCode = 1;
10057
+ } else {
10058
+ process.stdout.write(
10059
+ `README check passed \u2014 all managed sections current.
10060
+ `
10061
+ );
10062
+ }
10063
+ }
10064
+ }
10065
+ function appendManagedBlock(existing, sections) {
10066
+ let content = existing.trimEnd();
10067
+ const sectionKeys = Object.keys(sections).sort();
10068
+ for (const key of sectionKeys) {
10069
+ content += "\n\n" + managedStartMarker(key) + "\n" + sections[key].trimEnd() + "\n" + managedEndMarker(key);
10070
+ }
10071
+ const newHash = hashManagedContent(sections);
10072
+ content += `
10073
+
10074
+ ${HASH_MARKER_PREFIX}${newHash}${MANAGED_MARKER_SUFFIX}`;
10075
+ content += "\n";
10076
+ return content;
10077
+ }
10078
+ async function runReadmeCommand(args) {
10079
+ const dryRun = args.includes("--dry-run");
10080
+ const check = args.includes("--check");
10081
+ const init = args.includes("--init");
10082
+ let projectDir;
10083
+ const pdIdx = args.indexOf("--project-dir");
10084
+ if (pdIdx !== -1) {
10085
+ const pdVal = args[pdIdx + 1];
10086
+ if (pdVal && !pdVal.startsWith("--")) {
10087
+ projectDir = pdVal;
10088
+ } else {
10089
+ process.stderr.write("error: --project-dir requires a path argument.\n");
10090
+ process.exitCode = 1;
10091
+ return;
10092
+ }
10093
+ }
10094
+ await runReadme({ dryRun, check, init, projectDir });
10095
+ }
10096
+ var init_readme = __esm({
10097
+ "src/cli/claude/readme.ts"() {
10098
+ "use strict";
10099
+ init_tech_detect();
10100
+ init_readme_generator();
9479
10101
  }
9480
10102
  });
9481
10103
 
@@ -9491,18 +10113,18 @@ __export(migrate_memory_exports, {
9491
10113
  runMigrateMemory: () => runMigrateMemory
9492
10114
  });
9493
10115
  import {
9494
- readFile as readFile22,
9495
- writeFile as writeFile17,
10116
+ readFile as readFile23,
10117
+ writeFile as writeFile18,
9496
10118
  mkdir as mkdir11,
9497
10119
  unlink as unlink4,
9498
10120
  rmdir,
9499
10121
  readdir as readdir4
9500
10122
  } from "node:fs/promises";
9501
10123
  import { existsSync as existsSync10 } from "node:fs";
9502
- import { join as join31, resolve as resolve9, dirname as dirname11, sep as sep2 } from "node:path";
10124
+ import { join as join32, resolve as resolve10, dirname as dirname11, sep as sep2 } from "node:path";
9503
10125
  import { homedir as homedir8 } from "node:os";
9504
10126
  function encodeProjectPath(absPath) {
9505
- return resolve9(absPath).replace(/[/\\]/g, "-");
10127
+ return resolve10(absPath).replace(/[/\\]/g, "-");
9506
10128
  }
9507
10129
  function resolveAutoMemoryDir(opts) {
9508
10130
  if (opts.autoMemoryDir) {
@@ -9510,7 +10132,7 @@ function resolveAutoMemoryDir(opts) {
9510
10132
  }
9511
10133
  const projectDir = opts.projectDir ?? process.cwd();
9512
10134
  const encoded = encodeProjectPath(projectDir);
9513
- return join31(homedir8(), ".claude", "projects", encoded, "memory");
10135
+ return join32(homedir8(), ".claude", "projects", encoded, "memory");
9514
10136
  }
9515
10137
  function parseFrontmatter(content) {
9516
10138
  content = content.replace(/\r\n/g, "\n");
@@ -9576,10 +10198,10 @@ async function inventoryFiles(dir) {
9576
10198
  }
9577
10199
  const results = [];
9578
10200
  for (const filename of filenames) {
9579
- const sourcePath = join31(dir, filename);
10201
+ const sourcePath = join32(dir, filename);
9580
10202
  let raw;
9581
10203
  try {
9582
- raw = await readFile22(sourcePath, "utf-8");
10204
+ raw = await readFile23(sourcePath, "utf-8");
9583
10205
  } catch (err) {
9584
10206
  const msg = err instanceof Error ? err.message : String(err);
9585
10207
  results.push({
@@ -9659,15 +10281,15 @@ function buildPlan(entries, opts) {
9659
10281
  return plan;
9660
10282
  }
9661
10283
  async function applyPlan(plan, opts) {
9662
- const projectDir = resolve9(opts.projectDir);
10284
+ const projectDir = resolve10(opts.projectDir);
9663
10285
  const dryRun = opts.dryRun ?? false;
9664
10286
  for (const entry of plan.entries) {
9665
10287
  if (entry.suggested_action !== "keep") continue;
9666
10288
  if (!entry.suggested_target?.startsWith("nested:")) continue;
9667
10289
  const relPath = entry.suggested_target.slice("nested:".length);
9668
- const targetDir = resolve9(join31(projectDir, relPath));
9669
- const targetFile = join31(targetDir, "CLAUDE.md");
9670
- if (!targetDir.startsWith(resolve9(projectDir) + sep2)) {
10290
+ const targetDir = resolve10(join32(projectDir, relPath));
10291
+ const targetFile = join32(targetDir, "CLAUDE.md");
10292
+ if (!targetDir.startsWith(resolve10(projectDir) + sep2)) {
9671
10293
  process.stderr.write(
9672
10294
  `migrate-memory: skipping unsafe suggested_target "${entry.suggested_target}" \u2014 resolves outside projectDir
9673
10295
  `
@@ -9685,8 +10307,8 @@ ${anchor}
9685
10307
  if (dryRun) {
9686
10308
  process.stdout.write(`[dry-run] Would create/append: ${targetFile}
9687
10309
  `);
9688
- if (resolve9(entry.source_path).startsWith(
9689
- resolve9(plan.auto_memory_dir) + sep2
10310
+ if (resolve10(entry.source_path).startsWith(
10311
+ resolve10(plan.auto_memory_dir) + sep2
9690
10312
  )) {
9691
10313
  process.stdout.write(
9692
10314
  `[dry-run] Would delete migrated keep source: ${entry.source_path}
@@ -9698,13 +10320,13 @@ ${anchor}
9698
10320
  await mkdir11(targetDir, { recursive: true });
9699
10321
  let existing = "";
9700
10322
  try {
9701
- existing = await readFile22(targetFile, "utf-8");
10323
+ existing = await readFile23(targetFile, "utf-8");
9702
10324
  } catch {
9703
10325
  }
9704
10326
  if (!existing.includes(anchor)) {
9705
- await writeFile17(targetFile, existing + appendContent, "utf-8");
10327
+ await writeFile18(targetFile, existing + appendContent, "utf-8");
9706
10328
  }
9707
- if (resolve9(entry.source_path).startsWith(resolve9(plan.auto_memory_dir) + sep2)) {
10329
+ if (resolve10(entry.source_path).startsWith(resolve10(plan.auto_memory_dir) + sep2)) {
9708
10330
  try {
9709
10331
  await unlink4(entry.source_path);
9710
10332
  } catch {
@@ -9716,7 +10338,7 @@ ${anchor}
9716
10338
  );
9717
10339
  }
9718
10340
  }
9719
- const rootClaudeMd = join31(projectDir, ".claude", "CLAUDE.md");
10341
+ const rootClaudeMd = join32(projectDir, ".claude", "CLAUDE.md");
9720
10342
  if (dryRun) {
9721
10343
  process.stdout.write(
9722
10344
  `[dry-run] Would ensure ${rootClaudeMd} contains: ${IMPORT_LINE}
@@ -9725,12 +10347,12 @@ ${anchor}
9725
10347
  } else {
9726
10348
  let claudeMdContent = "";
9727
10349
  try {
9728
- claudeMdContent = await readFile22(rootClaudeMd, "utf-8");
10350
+ claudeMdContent = await readFile23(rootClaudeMd, "utf-8");
9729
10351
  } catch {
9730
10352
  await mkdir11(dirname11(rootClaudeMd), { recursive: true });
9731
10353
  }
9732
10354
  if (!claudeMdContent.includes(IMPORT_LINE)) {
9733
- await writeFile17(
10355
+ await writeFile18(
9734
10356
  rootClaudeMd,
9735
10357
  claudeMdContent + `
9736
10358
  ${IMPORT_LINE}
@@ -9741,8 +10363,8 @@ ${IMPORT_LINE}
9741
10363
  }
9742
10364
  for (const entry of plan.entries) {
9743
10365
  if (entry.suggested_action !== "drop") continue;
9744
- if (!resolve9(entry.source_path).startsWith(
9745
- resolve9(plan.auto_memory_dir) + sep2
10366
+ if (!resolve10(entry.source_path).startsWith(
10367
+ resolve10(plan.auto_memory_dir) + sep2
9746
10368
  )) {
9747
10369
  process.stderr.write(
9748
10370
  `migrate-memory: skipping delete of "${entry.source_path}" \u2014 resolves outside auto_memory_dir
@@ -9760,13 +10382,13 @@ ${IMPORT_LINE}
9760
10382
  } catch {
9761
10383
  }
9762
10384
  }
9763
- const memoryMd = join31(plan.auto_memory_dir, "MEMORY.md");
9764
- const safeRmdirBase = join31(homedir8(), ".claude", "projects");
10385
+ const memoryMd = join32(plan.auto_memory_dir, "MEMORY.md");
10386
+ const safeRmdirBase = join32(homedir8(), ".claude", "projects");
9765
10387
  if (dryRun) {
9766
10388
  process.stdout.write(`[dry-run] Would delete MEMORY.md: ${memoryMd}
9767
10389
  `);
9768
10390
  } else {
9769
- if (resolve9(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
10391
+ if (resolve10(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
9770
10392
  try {
9771
10393
  await unlink4(memoryMd);
9772
10394
  } catch {
@@ -9784,7 +10406,7 @@ ${IMPORT_LINE}
9784
10406
  `
9785
10407
  );
9786
10408
  } else {
9787
- if (!resolve9(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
10409
+ if (!resolve10(plan.auto_memory_dir).startsWith(safeRmdirBase + sep2)) {
9788
10410
  process.stderr.write(
9789
10411
  `migrate-memory: skipping rmdir of "${plan.auto_memory_dir}" \u2014 not under ~/.claude/projects
9790
10412
  `
@@ -9814,7 +10436,7 @@ async function runMigrateMemory(opts) {
9814
10436
  if (applyFile) {
9815
10437
  let planJson;
9816
10438
  try {
9817
- planJson = await readFile22(resolve9(applyFile), "utf-8");
10439
+ planJson = await readFile23(resolve10(applyFile), "utf-8");
9818
10440
  } catch (err) {
9819
10441
  const msg = err instanceof Error ? err.message : String(err);
9820
10442
  process.stderr.write(
@@ -9886,11 +10508,11 @@ var init_migrate_memory = __esm({
9886
10508
  // src/index.ts
9887
10509
  init_version();
9888
10510
  import { readFileSync as readFileSync10 } from "node:fs";
9889
- import { resolve as resolve10 } from "node:path";
10511
+ import { resolve as resolve11 } from "node:path";
9890
10512
  void (async () => {
9891
10513
  if (!process.env.CODEBYPLAN_API_KEY) {
9892
10514
  try {
9893
- const envPath = resolve10(process.cwd(), ".env.local");
10515
+ const envPath = resolve11(process.cwd(), ".env.local");
9894
10516
  const content = readFileSync10(envPath, "utf-8");
9895
10517
  for (const line of content.split("\n")) {
9896
10518
  const trimmed = line.trim();
@@ -10096,11 +10718,17 @@ void (async () => {
10096
10718
  process.exit(1);
10097
10719
  }
10098
10720
  const { runGenerate: runGenerate2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
10721
+ const check = process.argv.slice(4).includes("--check");
10099
10722
  await runGenerate2(
10100
- parsed.projectDir != null ? { ...parsed.opts, projectDir: parsed.projectDir } : parsed.opts
10723
+ parsed.projectDir != null ? { ...parsed.opts, check, projectDir: parsed.projectDir } : { ...parsed.opts, check }
10101
10724
  );
10102
10725
  process.exit(process.exitCode ?? 0);
10103
10726
  }
10727
+ if (subcommand === "readme") {
10728
+ const { runReadmeCommand: runReadmeCommand2 } = await Promise.resolve().then(() => (init_readme(), readme_exports));
10729
+ await runReadmeCommand2(process.argv.slice(4));
10730
+ process.exit(process.exitCode ?? 0);
10731
+ }
10104
10732
  if (subcommand === "migrate-memory") {
10105
10733
  if (parsed === null) {
10106
10734
  process.exit(1);
@@ -10124,6 +10752,7 @@ void (async () => {
10124
10752
  --write-cache Write result to .codebyplan/claude-status.local.json
10125
10753
  --quiet Suppress stdout output
10126
10754
  codebyplan claude generate [flags] Write .claude/generated/structure.md from local config
10755
+ codebyplan claude readme [flags] Generate / refresh README.md for outward-facing units
10127
10756
  codebyplan claude migrate-memory [flags] Inventory auto-memory files and emit migration plan
10128
10757
 
10129
10758
  Flags (apply to install/update/uninstall):
@@ -10142,6 +10771,13 @@ void (async () => {
10142
10771
  Flags (apply to generate):
10143
10772
  --project-dir <path> Override the project root (default: cwd)
10144
10773
  --dry-run Print what would be written, write nothing
10774
+ --check Exit non-zero when AGENTS.md is missing or drifted
10775
+
10776
+ Flags (apply to readme):
10777
+ --project-dir <path> Override the project root (default: cwd)
10778
+ --dry-run Print what would be written, write nothing
10779
+ --check Exit non-zero on drift or missing READMEs
10780
+ --init Append managed block to marker-less existing READMEs
10145
10781
  `);
10146
10782
  process.exit(0);
10147
10783
  }
@@ -10206,6 +10842,7 @@ void (async () => {
10206
10842
  codebyplan claude update [flags] Update installed assets to latest versions
10207
10843
  codebyplan claude uninstall [flags] Remove installed assets from ./.claude/
10208
10844
  codebyplan claude generate [flags] Write .claude/generated/structure.md from local config
10845
+ codebyplan claude readme [flags] Generate / refresh README.md for outward-facing units
10209
10846
  codebyplan claude migrate-memory [flags] Inventory auto-memory files and emit migration plan
10210
10847
 
10211
10848
  Flags (install/update/uninstall):
@@ -10221,6 +10858,13 @@ void (async () => {
10221
10858
  Flags (generate):
10222
10859
  --project-dir <path> Override the project root (default: cwd)
10223
10860
  --dry-run Print what would be written, write nothing
10861
+ --check Exit non-zero when AGENTS.md is missing or drifted
10862
+
10863
+ Flags (readme):
10864
+ --project-dir <path> Override the project root (default: cwd)
10865
+ --dry-run Print what would be written, write nothing
10866
+ --check Exit non-zero on drift or missing READMEs
10867
+ --init Append managed block to marker-less existing READMEs
10224
10868
 
10225
10869
  Flags (migrate-memory):
10226
10870
  --project-dir <path> Override the project root (default: cwd)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.37",
3
+ "version": "1.13.38",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {