@williamthorsen/nmr 0.25.0 → 0.26.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.
@@ -40,8 +40,7 @@ var workspaceScripts = {
40
40
  "test:unit": "pnpm exec vitest --project unit",
41
41
  "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
42
42
  typecheck: "tsgo --noEmit",
43
- // `--include-locked` is required rather than stylistic: a repo that pins exactly (pnpm's
44
- // `savePrefix: ''`) has no dependency taze considers unlocked, so without it nothing is reported.
43
+ // Without `--include-locked`, nothing would be reported in a repo that pins exact version numbers.
45
44
  upgrade: "nmr-taze --include-locked",
46
45
  "view-coverage": "open coverage/index.html"
47
46
  };
@@ -51,21 +50,17 @@ var rootScripts = {
51
50
  "audit:prod": "pnpm exec v11y --prod",
52
51
  build: "pnpm --recursive exec nmr build",
53
52
  check: ["typecheck", "fmt:check", "lint:check", "test"],
54
- "check:agent-files": "nmr-sync-agent-files --check",
55
- "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage", "check:agent-files"],
56
- // Excludes the audit, which has a workflow of its own.
53
+ "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
54
+ // Excludes the audit, which in CI has a workflow of its own.
57
55
  ci: ["build", "check:strict"],
58
56
  clean: "nmr-clean",
59
57
  fix: ["lint", "fmt"],
60
58
  "fix:check": ["fmt:check", "lint:check"],
61
- // Identical to the workspace entries: the bin scopes its own selection to the working directory, so
62
- // neither registry needs the `sh -c '… "${@:-.}"' --` wrapper that gave the root form a default path.
63
59
  fmt: "nmr-fmt --write",
64
60
  "fmt:check": "nmr-fmt --check",
65
61
  lint: "nmr root:lint && pnpm --recursive exec nmr lint",
66
62
  "lint:check": "nmr root:lint:check && pnpm --recursive exec nmr lint:check",
67
63
  "lint:strict": "nmr root:lint:strict && pnpm --recursive exec nmr lint:strict",
68
- // Everything the remote runs: the code-quality gate plus the audit workflow.
69
64
  prepush: ["ci", "audit"],
70
65
  "report-overrides": "nmr-report-overrides",
71
66
  "root:check": ["root:typecheck", "fmt:check", "root:lint:check", "root:test"],
@@ -78,23 +73,16 @@ var rootScripts = {
78
73
  "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
79
74
  "root:typecheck": "tsgo --noEmit",
80
75
  "root:upgrade": "nmr-taze --include-locked",
81
- "sync-agent-files": "nmr-sync-agent-files",
82
76
  test: "nmr root:test && pnpm --recursive exec nmr test",
83
77
  "test:all": "nmr root:test:all && pnpm --recursive exec nmr test:all",
84
- // Chains `root:test`, not a `root:test:coverage`: the root config reports no coverage of its own.
85
78
  "test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
86
79
  "test:tool": "nmr root:test:tool && pnpm --recursive exec nmr test:tool",
87
80
  "test:unit": "nmr root:test:unit && pnpm --recursive exec nmr test:unit",
88
- // Omitting `--config` is deliberate: bare `vitest` at the monorepo root resolves the root `vitest.config.ts`,
89
- // covering the whole tree in one process. A chain like the others would never advance past its first watcher.
81
+ // Bare `vitest` at the monorepo root resolves the root `vitest.config.ts`, covering the whole tree in one
82
+ // process; a chain like the others would never advance past its first watcher.
90
83
  "test:watch": `vitest ${GATE_PROJECTS} --watch`,
91
84
  typecheck: "nmr root:typecheck && pnpm --recursive exec nmr typecheck",
92
- // A string rather than a composite because passthrough args attach to the chain's last command — as a
93
- // composite, `nmr upgrade major` would hand `major` to the override report instead of the upgrade tool.
94
- //
95
- // Overrides print first, as context for the report that follows: a pinned transitive dependency is why
96
- // an expected upgrade may be missing from it. taze handles pnpm workspaces natively, so this needs no
97
- // `pnpm --recursive` fan-out: one process covers the root and every package.
85
+ // A string rather than a composite: Passthrough args attach to the chain's last command.
98
86
  upgrade: "nmr-report-overrides && nmr-taze --include-locked --recursive"
99
87
  };
100
88
 
@@ -375,7 +363,7 @@ function formatPaths(paths) {
375
363
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
376
364
  }
377
365
  function getMinVersion() {
378
- const picked = { "version": "0.25.0" };
366
+ const picked = { "version": "0.26.0" };
379
367
  if (typeof picked.version !== "string") {
380
368
  throw new TypeError("nmr/package.json: 'version' must be a string");
381
369
  }
@@ -10,7 +10,7 @@
10
10
  "readyupVersion": "0.23.0",
11
11
  "source": "kits/default.ts",
12
12
  "sourceHash": "f646e7fb",
13
- "targetHash": "514c9b6f"
13
+ "targetHash": "56564046"
14
14
  }
15
15
  ]
16
16
  }
package/README.md CHANGED
@@ -3,20 +3,21 @@
3
3
  Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
4
4
 
5
5
  <!-- section:release-notes -->
6
- ## Release notes — v0.25.0 (2026-08-03)
6
+ ## Release notes — v0.26.0 (2026-08-04)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - Mark a cache hit's time saving with a speed-boost icon (#579)
10
+ - Publish each package's readyup kit from the package that owns it (#590)
11
11
 
12
- Time saved due to `nmr`'s cache feature now earns a rocket emoji in the terminal output.
13
- Savings less than 1 second are no longer reported.
12
+ ReadyUp checks for `@williamthorsen/nmr`, `@williamthorsen/release-kit`, and `v11y-check` are now run directly against these packages, guaranteeing a version-appropriate check, instead of against their host repo. The package must be installed as a direct dependency in the calling repo.
14
13
 
15
- - Publish nmr's readyup kit with a test-tier conformance check (#580)
14
+ The syntax is `rdy run --from npm:<package>`. `rdy run --packages` runs the kits contained in any packages listed in `.config/readyup.config.ts`. Requires `readyup` 0.23 or later.
16
15
 
17
- `@williamthorsen/nmr` is now bundled with ReadyUp kits that check a repo's alignment with `nmr` requirements and recommendations.
16
+ - Ship agent guidance as CodeAssembly package content (#591)
18
17
 
19
- `rdy run --from npm:@williamthorsen/nmr` checks the repo against the kits without prior configuration. Once the package is added to the ReadyUp config, `rdy run --packages` will run the `nmr` kits along with those of any other registered package. Requires `readyup` v0.23 or greater.
18
+ A repo that consumes `@williamthorsen/nmr` can now adopt `nmr`'s agent guidance by naming the package in `.agents/codeassembly.yaml`. `codeassembly sync` then delivers that guidance to every agent harness. Guidance updates are thus delivered directly with each package, eliminating the need to keep a guidance file in sync. The `nmr sync-agent-files` command has been removed.
19
+
20
+ Migration guide: Delete `.agents/nmr/`, remove the `@nmr/AGENTS.md` include line, and remove all uses of `check:agent-files`.
20
21
  <!-- /section:release-notes -->
21
22
 
22
23
  ## Installation
@@ -201,23 +202,23 @@ import { defineConfig } from '@williamthorsen/nmr/config';
201
202
 
202
203
  export default defineConfig({
203
204
  workspaceScripts: {
204
- 'build:pre': 'npx rdy compile',
205
+ 'build:pre': 'node scripts/generate-manifest.ts',
205
206
  },
206
207
  });
207
208
  ```
208
209
 
209
- Attach a post-build step to one package:
210
+ Attach a step to one package's hook:
210
211
 
211
212
  ```jsonc
212
- // packages/nmr/package.json
213
+ // packages/my-package/package.json
213
214
  {
214
215
  "scripts": {
215
- "build:post": "nmr-sync-agent-files",
216
+ "upgrade:post": "nmr-report-overrides",
216
217
  },
217
218
  }
218
219
  ```
219
220
 
220
- The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
221
+ The second example calls the bin directly: `report-overrides` is a root-registry command, so `nmr report-overrides` from inside a package fails with `Unknown command`, while the bin runs from either scope.
221
222
 
222
223
  ## Check-result cache
223
224
 
@@ -237,7 +238,7 @@ The cache is a working-tree cache, not a build cache: it answers "has this exact
237
238
 
238
239
  ### What is cached
239
240
 
240
- Cacheable by default: `check`, `check:agent-files`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
241
+ Cacheable by default: `check`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
241
242
 
242
243
  Everything else runs every time:
243
244
 
@@ -437,11 +438,10 @@ Neither is bound to a git hook. `prepush` is named for when you run it, not for
437
438
 
438
439
  #### Check and quality
439
440
 
440
- | Command | Runs |
441
- | ------------------- | ----------------------------------------------------------------------------- |
442
- | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
443
- | `check:agent-files` | `nmr-sync-agent-files --check` |
444
- | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
441
+ | Command | Runs |
442
+ | -------------- | -------------------------------------------------------- |
443
+ | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
444
+ | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
445
445
 
446
446
  #### Fix
447
447
 
@@ -531,7 +531,6 @@ These scripts operate on root-level code only (not workspace packages):
531
531
  | Command | Runs |
532
532
  | ------------------ | ---------------------- |
533
533
  | `report-overrides` | `nmr-report-overrides` |
534
- | `sync-agent-files` | `nmr-sync-agent-files` |
535
534
 
536
535
  ## CLI reference
537
536
 
@@ -585,35 +584,33 @@ Report any active `pnpm.overrides` in the root `package.json`, reminding develop
585
584
  nmr report-overrides
586
585
  ```
587
586
 
588
- ### `sync-agent-files`
587
+ ## Agent guidance
589
588
 
590
- Sync the agent-facing guidance shipped with nmr into the consuming repo.
589
+ nmr ships the rules an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
591
590
 
592
- ```bash
593
- nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
594
- nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
591
+ ```yaml
592
+ packages:
593
+ use:
594
+ - '@williamthorsen/nmr'
595
595
  ```
596
596
 
597
- Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
598
-
599
- The default root `check:strict` composite includes `check:agent-files`, which runs `--check` automatically — so any CI pipeline already running `check:strict` catches drift without per-consumer wiring.
597
+ Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it with no second step.
600
598
 
601
- To expose the synced guidance to Claude Code sessions, add this include to the consuming repo's `.agents/PROJECT.md`:
599
+ Wiring the sync to `postinstall` keeps it current without a hand-run command:
602
600
 
603
- ```markdown
604
- @nmr/AGENTS.md
601
+ ```json
602
+ {
603
+ "scripts": {
604
+ "postinstall": "codeassembly sync --warn-only"
605
+ }
606
+ }
605
607
  ```
606
608
 
607
- #### What belongs in the synced file
608
-
609
- The synced file is injected at launch in every consuming repo, so every line is paid for on every task whether or not it is relevant. It is a cheatsheet, not a manual.
610
-
611
- A line earns a place in `AGENTS.md` only if both hold:
609
+ `--warn-only` reports a sync failure and exits 0; without it, a failed sync aborts `pnpm install`.
612
610
 
613
- 1. It is absent from nmr's own output. Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
614
- 2. The obvious action goes wrong without it. Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
611
+ ### Migrating from `sync-agent-files`
615
612
 
616
- Everything else belongs in this README, reachable on demand at `node_modules/@williamthorsen/nmr/README.md`, with a pointer from `AGENTS.md` naming the topic so an agent knows to look. When adding a feature, document it here and extend that pointer rather than the cheatsheet.
613
+ The `nmr sync-agent-files` command, its `nmr-sync-agent-files` bin, and the `check:agent-files` script were removed in favor of the above. After upgrading, delete the generated `.agents/nmr/` directory and the `@nmr/AGENTS.md` line it was exposed through, drop `check:agent-files` from any `check:strict` override that enumerates it, and remove any direct call to the bin.
617
614
 
618
615
  ## Standalone utilities
619
616
 
@@ -0,0 +1,24 @@
1
+ # Authoring nmr's agent guidance
2
+
3
+ This directory is a CodeAssembly content root, declared by the `codeassembly.content` key in `packages/nmr/package.json`.
4
+
5
+ ## The ambient cheatsheet
6
+
7
+ `guidance/rulebooks/nmr.md` carries `delivery: ambient`, so its body is injected at launch in every consuming repo. Every line is paid for on every task, whether or not it is relevant. It is a cheatsheet, not a manual.
8
+
9
+ A line earns a place in it only if both hold:
10
+
11
+ 1. **It is absent from nmr's own output.** Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
12
+ 2. **The obvious action goes wrong without it.** Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
13
+
14
+ Everything else belongs in `packages/nmr/README.md`, reachable on demand at `node_modules/@williamthorsen/nmr/README.md`, with a pointer from the cheatsheet naming the topic so an agent knows to look. When adding a feature, document it there and extend that pointer rather than the cheatsheet.
15
+
16
+ ## Constraints on rulebook bodies
17
+
18
+ Rulebook bodies carry no links to other rulebooks. CodeAssembly's renderer rewrites link targets under `skills/` and `scripts/` and rejects everything else, so a cross-reference names the deployed `consult-<slug>` in prose and declares the edge under `dependencies:`.
19
+
20
+ Anchor-only links are checked against the body they appear in, so a fragment naming a heading that lives in another document fails the run. Check `](#` before moving a section between bodies.
21
+
22
+ ## Publishing
23
+
24
+ The directory is published because `files` in `packages/nmr/package.json` lists it. Dropping that entry fails nothing locally, because this repo resolves the guidance through a `workspace:*` self-link that `files` does not govern. `packaging.tool.test.ts` is what catches it.
@@ -1,8 +1,10 @@
1
1
  ---
2
- source: '@williamthorsen/nmr@0.0.0-source'
2
+ slug: nmr
3
+ description: Invocation rules for the nmr script runner in a pnpm monorepo.
4
+ delivery: ambient
3
5
  ---
4
6
 
5
- # nmr: agent guidance (generated; do not edit)
7
+ # nmr: agent guidance
6
8
 
7
9
  - Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command it resolves to; a wrong guess reports only `Unknown command`, with no list.
8
10
  - Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
@@ -18,7 +18,6 @@ export const CURRENT_RUNTIME = {
18
18
  export const DEBUG_ENV_VAR = 'NMR_DEBUG';
19
19
  export const DEFAULT_CACHEABLE_COMMANDS = [
20
20
  'check',
21
- 'check:agent-files',
22
21
  'check:strict',
23
22
  'ci',
24
23
  'fix:check',
@@ -28,8 +28,7 @@ export const rootScripts = {
28
28
  'audit:prod': 'pnpm exec v11y --prod',
29
29
  build: 'pnpm --recursive exec nmr build',
30
30
  check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
31
- 'check:agent-files': 'nmr-sync-agent-files --check',
32
- 'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage', 'check:agent-files'],
31
+ 'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
33
32
  ci: ['build', 'check:strict'],
34
33
  clean: 'nmr-clean',
35
34
  fix: ['lint', 'fmt'],
@@ -51,7 +50,6 @@ export const rootScripts = {
51
50
  'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
52
51
  'root:typecheck': 'tsgo --noEmit',
53
52
  'root:upgrade': 'nmr-taze --include-locked',
54
- 'sync-agent-files': 'nmr-sync-agent-files',
55
53
  test: 'nmr root:test && pnpm --recursive exec nmr test',
56
54
  'test:all': 'nmr root:test:all && pnpm --recursive exec nmr test:all',
57
55
  'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -54,13 +54,12 @@
54
54
  "nmr-compile": "bin/nmr-compile.js",
55
55
  "nmr-fmt": "bin/nmr-fmt.js",
56
56
  "nmr-report-overrides": "bin/nmr-report-overrides.js",
57
- "nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
58
57
  "nmr-taze": "bin/nmr-taze.js"
59
58
  },
60
59
  "files": [
61
60
  ".readyup/kits/*.js",
62
61
  ".readyup/manifest.json",
63
- "AGENTS.md",
62
+ "agents",
64
63
  "bin",
65
64
  "dist"
66
65
  ],
@@ -96,7 +95,11 @@
96
95
  "publishConfig": {
97
96
  "access": "public"
98
97
  },
98
+ "codeassembly": {
99
+ "content": "agents"
100
+ },
99
101
  "scripts": {
100
- "build:post": "nmr-sync-agent-files && rdy compile"
102
+ "build:post": "rdy compile",
103
+ "build:pre": "rdy verify"
101
104
  }
102
105
  }
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
- try {
3
- await import('../dist/esm/cli-sync-agent-files.js');
4
- } catch (error) {
5
- if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-sync-agent-files: build output not found — run `pnpm run build` first\n');
7
- } else {
8
- process.stderr.write(`nmr-sync-agent-files: failed to load: ${error.message}\n`);
9
- }
10
- process.exit(1);
11
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,29 +0,0 @@
1
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
- import { check, sync } from "./commands/sync-agent-files.js";
3
- import { findMonorepoRoot } from "./workspace.js";
4
- const flagSchema = {
5
- check: { long: '--check', type: 'boolean' },
6
- };
7
- const { flags } = parseArgsOrExit(process.argv.slice(2), flagSchema);
8
- try {
9
- runCommand(flags.check);
10
- }
11
- catch (error) {
12
- reportError(error instanceof Error ? error.message : String(error));
13
- process.exit(1);
14
- }
15
- function runCommand(checkOnly) {
16
- const monorepoRoot = findMonorepoRoot();
17
- if (!checkOnly) {
18
- const { path: destinationPath, packageSpecifier, changed } = sync(monorepoRoot);
19
- console.info(changed ? `✓ Wrote ${destinationPath} (${packageSpecifier})` : `✓ ${destinationPath} already up to date`);
20
- process.exit(0);
21
- }
22
- const result = check(monorepoRoot);
23
- if (result.ok) {
24
- console.info('✓ .agents/nmr/AGENTS.md is in sync');
25
- process.exit(0);
26
- }
27
- process.stderr.write(`${result.reason}\n`);
28
- process.exit(1);
29
- }
@@ -1,13 +0,0 @@
1
- export declare function check(destinationDir: string): CheckResult;
2
- export interface SyncResult {
3
- path: string;
4
- packageSpecifier: string;
5
- changed: boolean;
6
- }
7
- export declare function sync(destinationDir: string): SyncResult;
8
- export type CheckResult = {
9
- ok: true;
10
- } | {
11
- ok: false;
12
- reason: string;
13
- };
@@ -1,58 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { readPackageVersion } from '@williamthorsen/nmr-core';
5
- const VERSION = readPackageVersion(import.meta.url);
6
- const PACKAGE_NAME = '@williamthorsen/nmr';
7
- const DESTINATION_RELATIVE_PATH = '.agents/nmr/AGENTS.md';
8
- const SOURCE_FILENAME = 'AGENTS.md';
9
- const FRONTMATTER_REGEX = /^---\n((?:.*\n)*?)---\n/;
10
- export function check(destinationDir) {
11
- const destinationPath = getDestinationPath(destinationDir);
12
- if (!existsSync(destinationPath)) {
13
- return {
14
- ok: false,
15
- reason: `${DESTINATION_RELATIVE_PATH} is missing. Run \`nmr sync-agent-files\`.`,
16
- };
17
- }
18
- const expectedBody = stripFrontmatter(readFileSync(getSourcePath(), 'utf8'));
19
- const foundBody = stripFrontmatter(readFileSync(destinationPath, 'utf8'));
20
- if (foundBody !== expectedBody) {
21
- return {
22
- ok: false,
23
- reason: `${DESTINATION_RELATIVE_PATH} content is out of date. Run \`nmr sync-agent-files\`.`,
24
- };
25
- }
26
- return { ok: true };
27
- }
28
- export function sync(destinationDir) {
29
- const body = stripFrontmatter(readFileSync(getSourcePath(), 'utf8'));
30
- const packageSpecifier = buildPackageSpecifier();
31
- const destinationPath = getDestinationPath(destinationDir);
32
- if (existsSync(destinationPath) && stripFrontmatter(readFileSync(destinationPath, 'utf8')) === body) {
33
- return { path: destinationPath, packageSpecifier, changed: false };
34
- }
35
- const output = `---\nsource: '${packageSpecifier}'\n---\n${body}`;
36
- mkdirSync(path.dirname(destinationPath), { recursive: true });
37
- writeFileSync(destinationPath, output, 'utf8');
38
- return { path: destinationPath, packageSpecifier, changed: true };
39
- }
40
- function buildPackageSpecifier() {
41
- return `${PACKAGE_NAME}@${VERSION}`;
42
- }
43
- function getDestinationPath(destinationDir) {
44
- return path.join(destinationDir, DESTINATION_RELATIVE_PATH);
45
- }
46
- function getSourcePath() {
47
- let dir = path.dirname(fileURLToPath(import.meta.url));
48
- while (dir !== path.dirname(dir)) {
49
- const candidate = path.join(dir, SOURCE_FILENAME);
50
- if (existsSync(candidate))
51
- return candidate;
52
- dir = path.dirname(dir);
53
- }
54
- throw new Error(`Could not locate ${SOURCE_FILENAME} in any parent of ${fileURLToPath(import.meta.url)}`);
55
- }
56
- function stripFrontmatter(content) {
57
- return content.replace(FRONTMATTER_REGEX, '');
58
- }