@theholocron/cli 3.13.1 → 3.15.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.mjs CHANGED
@@ -264,7 +264,8 @@ function resolveConfig(raw) {
264
264
  doctor: raw.doctor ?? {},
265
265
  agent: raw.agent,
266
266
  skills: raw.skills,
267
- docs
267
+ docs,
268
+ env: raw.env
268
269
  };
269
270
  }
270
271
  //#endregion
@@ -3186,6 +3187,11 @@ async function runSetup(input) {
3186
3187
  const name = typeof entry === "string" ? entry : entry.name;
3187
3188
  const withOverrides = typeof entry === "object" ? entry.with : void 0;
3188
3189
  const additionalPaths = typeof entry === "object" ? entry.paths : void 0;
3190
+ if (name === "test" && withOverrides) {
3191
+ const runUnit = withOverrides["run-unit"];
3192
+ const runStorybook = withOverrides["run-storybook"];
3193
+ if (runUnit === false && runStorybook === false) throw new ConfigError("test workflow: at least one of \"run-unit\" or \"run-storybook\" must be true. Library repos use run-unit: true; UI/Storybook repos use run-storybook: true.");
3194
+ }
3189
3195
  if (!KNOWN_WORKFLOWS.has(name)) {
3190
3196
  steps.push({
3191
3197
  capability: "source",
@@ -4097,7 +4103,7 @@ var stale_default = "name: Stale\n\non: # yamllint disable-line rule:truthy\n w
4097
4103
  var sync_github_default = "name: Sync GitHub Templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
4098
4104
  //#endregion
4099
4105
  //#region src/templates/workflows/test.yml
4100
- var test_default = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-chromatic:\n description: Publish Storybook to Chromatic for visual regression testing\n type: boolean\n required: false\n default: false\n run-user-flow:\n description: Run Cypress E2E user-flow tests (requires cypress.config.*)\n type: boolean\n required: false\n default: false\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n CYPRESS_RECORD_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n validate:\n name: Validate — at least one test type must be enabled\n if: ${{ !inputs.run-unit && !inputs.run-storybook }}\n runs-on: ubuntu-latest\n steps:\n - name: Fail — set run-unit or run-storybook to true\n run: |\n echo \"::error::At least one of run-unit or run-storybook must be true. Library repos use run-unit: true; UI/Storybook repos use run-storybook: true.\"\n exit 1\n\n unit:\n name: Run tests and collect coverage\n if: ${{ inputs.run-unit }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm test:coverage\n name: Run tests with coverage\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n storybook:\n name: Run Storybook interaction tests\n if: ${{ inputs.run-storybook }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install chromium --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run Storybook tests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n visual-and-composition:\n name: Test Visual and Composition\n if: ${{ inputs.run-chromatic }}\n permissions:\n contents: read\n statuses: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18\n name: Publish to Chromatic\n with:\n projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}\n token: ${{ github.token }}\n buildScriptName: build:storybook\n\n interaction-and-accessibility:\n name: Test Interactions and Accessibility\n if: ${{ inputs.run-interaction }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run interaction and accessibility tests\n\n user-flow:\n name: Test User Flow\n if: ${{ inputs.run-user-flow }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n strategy:\n fail-fast: false\n matrix:\n containers: [1, 2]\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: cypress-io/github-action@1052aa98bbbe4f55210f844878213c07d9c8c399 # v6.7.13\n name: Cypress run\n with:\n start: pnpm dev\n wait-on: \"http://localhost:5173\"\n record: true\n parallel: true\n env:\n CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n";
4106
+ var test_default = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-chromatic:\n description: Publish Storybook to Chromatic for visual regression testing\n type: boolean\n required: false\n default: false\n run-user-flow:\n description: Run Cypress E2E user-flow tests (requires cypress.config.*)\n type: boolean\n required: false\n default: false\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n CYPRESS_RECORD_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n unit:\n name: Run tests and collect coverage\n if: ${{ inputs.run-unit }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm test:coverage\n name: Run tests with coverage\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n storybook:\n name: Run Storybook interaction tests\n if: ${{ inputs.run-storybook }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install chromium --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run Storybook tests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n files: '**/test-report.junit.xml'\n\n visual-and-composition:\n name: Test Visual and Composition\n if: ${{ inputs.run-chromatic }}\n permissions:\n contents: read\n statuses: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18\n name: Publish to Chromatic\n with:\n projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}\n token: ${{ github.token }}\n buildScriptName: build:storybook\n\n interaction-and-accessibility:\n name: Test Interactions and Accessibility\n if: ${{ inputs.run-interaction }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run interaction and accessibility tests\n\n user-flow:\n name: Test User Flow\n if: ${{ inputs.run-user-flow }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n strategy:\n fail-fast: false\n matrix:\n containers: [1, 2]\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: cypress-io/github-action@1052aa98bbbe4f55210f844878213c07d9c8c399 # v6.7.13\n name: Cypress run\n with:\n start: pnpm dev\n wait-on: \"http://localhost:5173\"\n record: true\n parallel: true\n env:\n CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n";
4101
4107
  //#endregion
4102
4108
  //#region src/templates/workflows/typecheck.yml
4103
4109
  var typecheck_default = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n typecheck:\n name: tsc --noEmit\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm typecheck\n name: Type check\n";
@@ -4443,6 +4449,86 @@ async function runSyncGithub(input) {
4443
4449
  };
4444
4450
  }
4445
4451
  //#endregion
4452
+ //#region src/commands/sync-readme.ts
4453
+ const README_INSTALL_START = "<!-- holocron:installation -->";
4454
+ const README_INSTALL_END = "<!-- /holocron:installation -->";
4455
+ function generateInstallBlock(pkg) {
4456
+ const { name = "", bin, peerDependencies = {} } = pkg;
4457
+ const isCli = Boolean(bin);
4458
+ const isReact = Boolean(peerDependencies["react"]);
4459
+ const lines = [];
4460
+ lines.push("## Installation", "");
4461
+ if (isCli) lines.push("```bash", `npm install --global ${name}`, "```");
4462
+ else lines.push("```bash", `pnpm install ${name}`, "```");
4463
+ lines.push("", "## Usage", "");
4464
+ if (isCli) {
4465
+ const commands = typeof bin === "string" ? [name.split("/").at(-1)] : Object.keys(bin ?? {});
4466
+ lines.push("```bash");
4467
+ for (const cmd of commands) lines.push(`${cmd} --help`);
4468
+ lines.push("```");
4469
+ } else if (isReact) lines.push("```tsx", `import { } from "${name}";`, "", "function App() {", ` return <></>;`, "}", "```");
4470
+ else lines.push("```typescript", `import { } from "${name}";`, "```");
4471
+ return lines.join("\n");
4472
+ }
4473
+ async function updateReadmeInstallation(repoRoot, block, dryRun, readFileFn, writeFileFn) {
4474
+ const readmePath = join(repoRoot, "README.md");
4475
+ let content;
4476
+ try {
4477
+ content = await readFileFn(readmePath, "utf8");
4478
+ } catch {
4479
+ return false;
4480
+ }
4481
+ const lines = content.split("\n");
4482
+ const startIdx = lines.findIndex((l) => l.trim() === README_INSTALL_START);
4483
+ const endIdx = lines.findIndex((l) => l.trim() === README_INSTALL_END);
4484
+ const blockLines = block.split("\n");
4485
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
4486
+ if (dryRun) return true;
4487
+ lines.splice(startIdx + 1, endIdx - startIdx - 1, "", ...blockLines, "");
4488
+ await writeFileFn(readmePath, lines.join("\n"), "utf8");
4489
+ return true;
4490
+ }
4491
+ const descEnd = lines.findIndex((l) => l.trim() === "<!-- /holocron:description -->");
4492
+ const insertAfter = descEnd !== -1 ? descEnd : lines.findIndex((l) => /^# /.test(l));
4493
+ if (insertAfter === -1) return false;
4494
+ if (dryRun) return true;
4495
+ lines.splice(insertAfter + 1, 0, "", README_INSTALL_START, "", ...blockLines, "", README_INSTALL_END);
4496
+ await writeFileFn(readmePath, lines.join("\n"), "utf8");
4497
+ return true;
4498
+ }
4499
+ async function runSyncReadme(input) {
4500
+ const { loaded, context, print = console.log, readFileFn = readFile, writeFileFn = writeFile } = input;
4501
+ const { repoRoot, dryRun = false } = context;
4502
+ const namespaces = loaded.resolved.env?.namespaces ?? [];
4503
+ print(style.header(`Syncing README installation block${dryRun ? " (dry-run)" : ""}…`));
4504
+ if (namespaces.length > 0) print(style.hint(` namespaces: ${namespaces.join(", ")}`));
4505
+ let pkg;
4506
+ try {
4507
+ const raw = await readFileFn(join(repoRoot, "package.json"), "utf8");
4508
+ pkg = JSON.parse(raw);
4509
+ } catch {
4510
+ return {
4511
+ status: "fail",
4512
+ updated: false,
4513
+ message: "Could not read package.json"
4514
+ };
4515
+ }
4516
+ const updated = await updateReadmeInstallation(repoRoot, generateInstallBlock(pkg), dryRun, readFileFn, writeFileFn);
4517
+ if (!updated) {
4518
+ print(style.warn("README.md not found or has no writable location for installation block"));
4519
+ return {
4520
+ status: "fail",
4521
+ updated: false,
4522
+ message: "README.md update failed"
4523
+ };
4524
+ }
4525
+ print(dryRun ? style.hint(" would update README.md") : style.success(" updated README.md"));
4526
+ return {
4527
+ status: dryRun ? "dry-run" : "ok",
4528
+ updated
4529
+ };
4530
+ }
4531
+ //#endregion
4446
4532
  //#region src/commands/upgrade-node.ts
4447
4533
  const SKIP_DIRS = /* @__PURE__ */ new Set([
4448
4534
  "node_modules",
@@ -5219,6 +5305,18 @@ try {
5219
5305
  ...argv.message ? { message: argv.message } : {},
5220
5306
  ...outputDir ? { outputDir } : {}
5221
5307
  })).status === "fail") process.exitCode = 1;
5308
+ }).command("sync-readme", "Sync the Installation + Usage block in README.md from package.json", (y) => y.option("dry-run", {
5309
+ type: "boolean",
5310
+ describe: "Print what would change without writing",
5311
+ default: false
5312
+ }), async (argv) => {
5313
+ if ((await runSyncReadme({
5314
+ loaded: await loadConfig(argv.cwd),
5315
+ context: {
5316
+ repoRoot: argv.cwd,
5317
+ dryRun: argv.dryRun
5318
+ }
5319
+ })).status === "fail") process.exitCode = 1;
5222
5320
  }).command("config show", "Print the resolved holocron config", () => {}, async (argv) => {
5223
5321
  const loaded = await loadConfig(argv.cwd);
5224
5322
  console.log(JSON.stringify(loaded.resolved, null, 2));