honestdocs 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
+ <a href="https://www.npmjs.com/package/honestdocs"><img alt="npm version" src="https://img.shields.io/npm/v/honestdocs?label=npm&color=1f2328"></a>
13
14
  <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-16a34a"></a>
14
15
  <img alt="Node 20+ or Bun" src="https://img.shields.io/badge/node-%E2%89%A520%20%C2%B7%20bun-1f2328">
15
16
  <img alt="Zero dependencies" src="https://img.shields.io/badge/dependencies-0-1f2328">
@@ -287,6 +288,7 @@ Markdown, so your own documents are never overwritten.
287
288
  "rules": "Standing rules every change must keep"
288
289
  },
289
290
  "trailer": "Doc", // the commit trailer key
291
+ "command": "npx honestdocs", // how this project runs it (see below)
290
292
  "codeRepos": ["."], // where trailer commits are read from
291
293
  "templates": ".honestdocs/_templates", // optional: your own <type>.md templates
292
294
  "types": { // optional: add or remove types
@@ -298,6 +300,12 @@ Markdown, so your own documents are never overwritten.
298
300
  }
299
301
  ```
300
302
 
303
+ `command` is what honestdocs writes into `AGENTS.md`, the skill and its own messages
304
+ whenever it tells someone to run it. `init` records the runner that started it —
305
+ `bunx honestdocs` under Bun, `pnpm dlx honestdocs` under pnpm, otherwise `npx honestdocs` —
306
+ and never a bare `honestdocs`, which exists only where someone installed it globally.
307
+ Change it, then `init --agents` (and `--skill`) to rewrite those sections.
308
+
301
309
  Templates may use `{{title}}`, `{{id}}` and `{{date}}`. Write the bodies in any language;
302
310
  filenames and frontmatter stay plain ASCII so every tool can read them.
303
311
 
package/guide/METHOD.md CHANGED
@@ -18,7 +18,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
18
18
  - **D2 A field someone has to remember to update will be wrong.** So people write four
19
19
  fields — `title`, `status`, `supersedes`, `related` — and everything else is derived:
20
20
  `progress` from the checklist, `files` from git, `superseded-by` from the newer
21
- document, `updated` from git. `honestdocs index` derives; nobody types those.
21
+ document, `updated` from git. `{{command}} index` derives; nobody types those.
22
22
 
23
23
  - **D3 Write it down before building.** Non-trivial work starts with a document: a design
24
24
  for "how should this work", a plan for "what are the steps", research for "what is
@@ -32,7 +32,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
32
32
  - **D5 Write down what was rejected.** Every template has *Alternatives considered*. The
33
33
  option that lost, and why, is what stops the same argument three months later — and
34
34
  stops an AI agent from proposing it again as if it were new. Before proposing a design,
35
- search for it: `honestdocs search <words> --section alternatives`.
35
+ search for it: `{{command}} search <words> --section alternatives`.
36
36
 
37
37
  - **D6 One document replaces another from the newer side.** Write
38
38
  `supersedes: [<old id>]` in the new document; the old one is marked automatically.
@@ -48,7 +48,7 @@ what keep it right. The `honestdocs` command enforces the ones a machine can che
48
48
  truths.
49
49
 
50
50
  - **D9 A rule that can be a test is one.** Rules are numbered (`R1`, `R2`, …) so they can
51
- be cited in a review. A rule names the test that proves it, and `honestdocs check`
51
+ be cited in a review. A rule names the test that proves it, and `{{command}} check`
52
52
  fails when that test file is gone. A rule that lives only in prose is the one that gets
53
53
  forgotten.
54
54
 
@@ -4,19 +4,19 @@ This project keeps its decisions, plans, research, postmortems and standing rule
4
4
  `{{root}}/`, managed by `honestdocs`. The code is the truth: where a document and the code
5
5
  disagree, believe the code and correct the document's status.
6
6
 
7
- - **Starting a session:** run `honestdocs brief` — what is in progress, its next unticked
7
+ - **Starting a session:** run `{{command}} brief` — what is in progress, its next unticked
8
8
  items, what changed lately, which rules are in force.
9
- - **Before building anything non-trivial:** run `honestdocs search <words>` (add
9
+ - **Before building anything non-trivial:** run `{{command}} search <words>` (add
10
10
  `--section alternatives` to see what was already rejected, and why), read the rules a
11
- change touches (`honestdocs rules`), then open a document:
12
- `honestdocs new <area> <slug> --type design|plan|research --title "…"`.
11
+ change touches (`{{command}} rules`), then open a document:
12
+ `{{command}} new <area> <slug> --type design|plan|research --title "…"`.
13
13
  - **While building:** put `{{trailer}}: <id>` on its own line at the end of every commit
14
14
  message that does the work. Tick the document's checklist as items land; add dated
15
15
  notes to its Log — they are the hand-over to the next session.
16
- - **Never type** `progress`, `files`, `superseded-by` or `updated` — `honestdocs index`
16
+ - **Never type** `progress`, `files`, `superseded-by` or `updated` — `{{command}} index`
17
17
  derives them. To replace a document, write `supersedes: [<old id>]` in the new one.
18
- - **Finishing:** `honestdocs done <id>` (or `--abandoned`, with the reason in the Log),
19
- then `honestdocs check`. A change is not finished while a document or rule it affects
18
+ - **Finishing:** `{{command}} done <id>` (or `--abandoned`, with the reason in the Log),
19
+ then `{{command}} check`. A change is not finished while a document or rule it affects
20
20
  still describes the old behaviour.
21
21
  - Ids are filenames and never change. Add `--json` to any query command for
22
- machine-readable output. The full method: `honestdocs guide`.
22
+ machine-readable output. The full method: `{{command}} guide`.
@@ -18,7 +18,7 @@ is finished.
18
18
  - **B2** A change is not finished while a document or rule it affects still describes the
19
19
  old behaviour.
20
20
  - **B3** A rule that can be checked by a test names that test on a `Test:` line, so
21
- `honestdocs check` notices when it disappears.
21
+ `{{command}} check` notices when it disappears.
22
22
 
23
23
  ## The checklist
24
24
 
@@ -27,18 +27,18 @@ public API, a setting — each pointing at the rule document that governs it. St
27
27
  add a line the first time something is forgotten._
28
28
 
29
29
  **Any change**
30
- - ☐ Searched for earlier decisions: `honestdocs search <words> --section alternatives`.
30
+ - ☐ Searched for earlier decisions: `{{command}} search <words> --section alternatives`.
31
31
  - ☐ The documents and rules it affects are updated in the same batch (B2).
32
32
 
33
33
  **Before saying it works**
34
34
  - ☐ The project's tests pass.
35
- - ☐ `honestdocs index && honestdocs check` passes.
35
+ - ☐ `{{command}} index && {{command}} check` passes.
36
36
 
37
37
  ## Keeping this folder right
38
38
 
39
39
  - A rule changes ⇒ edit its document here, in place, and add a line to its Log.
40
40
  - A new cross-cutting concern ⇒ a new rule document
41
- (`honestdocs new rules <slug> --type rule --status current --title "…"`), its rules
41
+ (`{{command}} new rules <slug> --type rule --status current --title "…"`), its rules
42
42
  numbered with a fresh letter, and its lines added to the checklist above.
43
43
 
44
44
  ## Log
@@ -14,11 +14,16 @@ the code disagree, believe the code — then come back and correct the document'
14
14
  ## Working with them
15
15
 
16
16
  ```sh
17
- honestdocs brief # what is under way
18
- honestdocs new <area> <slug> --type plan --title "…"
19
- git commit -m "…" -m "{{trailer}}: <id>" # link the work to its document
20
- honestdocs done <id> # close it
21
- honestdocs index && honestdocs check # derive, regenerate, validate
17
+ # what is under way
18
+ {{command}} brief
19
+ # start a document
20
+ {{command}} new <area> <slug> --type plan --title "…"
21
+ # link the work to it
22
+ git commit -m "…" -m "{{trailer}}: <id>"
23
+ # close it
24
+ {{command}} done <id>
25
+ # derive, regenerate, validate
26
+ {{command}} index && {{command}} check
22
27
  ```
23
28
 
24
29
  Write by hand only `title`, `status`, `supersedes` and `related`. The rest is derived —
@@ -26,4 +31,4 @@ Write by hand only `title`, `status`, `supersedes` and `related`. The rest is de
26
31
  `superseded-by` from the newer document, `updated` from git — because a field someone has
27
32
  to remember to update will be wrong.
28
33
 
29
- The method in full: `honestdocs guide`.
34
+ The method in full: `{{command}} guide`.
@@ -12,7 +12,7 @@ and the code disagree, believe the code and correct the document.
12
12
  ## When a session starts
13
13
 
14
14
  ```sh
15
- honestdocs brief
15
+ {{command}} brief
16
16
  ```
17
17
 
18
18
  Work in progress with its next unticked items, what is planned, what changed lately,
@@ -21,16 +21,16 @@ the hand-over from the last session.
21
21
 
22
22
  ## Before building
23
23
 
24
- 1. **Has this been weighed before?** `honestdocs search <words>`; with
24
+ 1. **Has this been weighed before?** `{{command}} search <words>`; with
25
25
  `--section alternatives` it shows only what was considered and rejected, with the
26
26
  reason. Superseded and abandoned documents are included on purpose. Do not propose a
27
27
  rejected option again without saying what has changed.
28
- 2. **Which rules apply?** `honestdocs rules` lists the numbered rules in force and the
28
+ 2. **Which rules apply?** `{{command}} rules` lists the numbered rules in force and the
29
29
  tests that prove them. Go through the `before-you-build` checklist in `{{root}}/rules/`.
30
30
  3. **Open a document** for anything non-trivial:
31
31
 
32
32
  ```sh
33
- honestdocs new <area> <slug> --type design --title "Token rotation"
33
+ {{command}} new <area> <slug> --type design --title "Token rotation"
34
34
  ```
35
35
 
36
36
  Types: `design` (how it should work), `plan` (ordered steps, checklist), `research`
@@ -46,26 +46,26 @@ the hand-over from the last session.
46
46
  {{trailer}}: 2026-11-02-token-rotation
47
47
  ```
48
48
 
49
- That is how `honestdocs index` learns which files the document touched. Several ids:
49
+ That is how `{{command}} index` learns which files the document touched. Several ids:
50
50
  comma-separated or one line each.
51
51
  - Tick checklist items (`- [x]`) as they land. Add dated notes to the Log.
52
52
  - Never write `progress`, `files`, `superseded-by` or `updated` by hand. Write only
53
53
  `title`, `status`, `supersedes`, `related`.
54
54
  - Replacing a document: `supersedes: [<old id>]` in the **new** one, then
55
- `honestdocs index` marks the old one.
55
+ `{{command}} index` marks the old one.
56
56
  - A rule changed: edit its rule document in place and add a Log line — never a second
57
57
  document beside it.
58
58
 
59
59
  ## Finishing
60
60
 
61
61
  ```sh
62
- honestdocs done <id> # refuses while checklist items are unticked
63
- honestdocs done <id> --abandoned # dropped on purpose; the reason goes in the Log
64
- honestdocs status <id> current # living documents: draft | current | retired
65
- honestdocs index && honestdocs check
62
+ {{command}} done <id> # refuses while checklist items are unticked
63
+ {{command}} done <id> --abandoned # dropped on purpose; the reason goes in the Log
64
+ {{command}} status <id> current # living documents: draft | current | retired
65
+ {{command}} index && {{command}} check
66
66
  ```
67
67
 
68
68
  `check` exits 1 on anything that makes the set lie. A change is not finished while a
69
69
  document or rule it affects still describes the old behaviour.
70
70
 
71
- Every query command takes `--json`. The method in full: `honestdocs guide`.
71
+ Every query command takes `--json`. The method in full: `{{command}} guide`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honestdocs",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Development documents that stay true — decisions, designs, plans, research, postmortems and standing rules, kept honest for people and AI agents.",
5
5
  "keywords": [
6
6
  "documentation",
@@ -37,6 +37,7 @@ function runCheck(config, { fix = false } = {}) {
37
37
  const fixed = [];
38
38
  const err = (d, msg) => errors.push({ file: d.rel, msg });
39
39
  const warnAt = (d, msg) => warnings.push({ file: d.rel, msg });
40
+ const cmd = config.command;
40
41
 
41
42
  const ids = new Map();
42
43
  for (const d of docs) {
@@ -47,7 +48,7 @@ function runCheck(config, { fix = false } = {}) {
47
48
  for (const d of docs) {
48
49
  const m = d.meta;
49
50
  if (!d.hasMeta) {
50
- err(d, "no frontmatter — start documents with `honestdocs new`, or add the block by hand");
51
+ err(d, `no frontmatter — start documents with \`${cmd} new\`, or add the block by hand`);
51
52
  continue;
52
53
  }
53
54
  if (!m.id) err(d, "missing `id`");
@@ -91,7 +92,7 @@ function runCheck(config, { fix = false } = {}) {
91
92
  warnAt(d, "superseded, but nothing says by what — set `supersedes` on the newer document");
92
93
  }
93
94
  if ((m.status === "done" || m.status === "abandoned") && !m.completed && m.legacy !== true) {
94
- warnAt(d, `${m.status} but \`completed\` is empty — close documents with \`honestdocs done\``);
95
+ warnAt(d, `${m.status} but \`completed\` is empty — close documents with \`${cmd} done\``);
95
96
  }
96
97
  if (m.status === "done") {
97
98
  const p = checklistProgress(d.body);
@@ -127,8 +128,8 @@ function runCheck(config, { fix = false } = {}) {
127
128
  for (const ch of derive(config, docs.filter((d) => d.hasMeta && d.meta.id === d.id), facts)) {
128
129
  for (const k of new Set([...Object.keys(ch.before), ...Object.keys(ch.meta)])) {
129
130
  if (JSON.stringify(ch.before[k]) === JSON.stringify(ch.meta[k])) continue;
130
- const msg = `\`${k}\` is ${JSON.stringify(ch.before[k] ?? "")}, should be ${JSON.stringify(ch.meta[k])} — run \`honestdocs index\``;
131
- if (k === "status") err(ch.doc, `replaced by ${ch.meta["superseded-by"]} but still says "${ch.before.status}" — run \`honestdocs index\``);
131
+ const msg = `\`${k}\` is ${JSON.stringify(ch.before[k] ?? "")}, should be ${JSON.stringify(ch.meta[k])} — run \`${cmd} index\``;
132
+ if (k === "status") err(ch.doc, `replaced by ${ch.meta["superseded-by"]} but still says "${ch.before.status}" — run \`${cmd} index\``);
132
133
  else if (k === "progress" || k === "superseded-by") err(ch.doc, msg);
133
134
  else warnAt(ch.doc, msg);
134
135
  }
@@ -138,9 +139,9 @@ function runCheck(config, { fix = false } = {}) {
138
139
  // so a commit can never make it stale on its own.
139
140
  const indexFile = join(config.docsDir, "INDEX.md");
140
141
  const expected = renderIndex(config, docs.filter((d) => d.hasMeta));
141
- if (!existsSync(indexFile)) errors.push({ file: "INDEX.md", msg: "missing — run `honestdocs index`" });
142
+ if (!existsSync(indexFile)) errors.push({ file: "INDEX.md", msg: `missing — run \`${cmd} index\`` });
142
143
  else if (readFileSync(indexFile, "utf8") !== expected) {
143
- errors.push({ file: "INDEX.md", msg: "stale: frontmatter changed since it was generated — run `honestdocs index`" });
144
+ errors.push({ file: "INDEX.md", msg: `stale: frontmatter changed since it was generated — run \`${cmd} index\`` });
144
145
  }
145
146
 
146
147
  return { documents: docs.length, git: facts.available ? "ok" : facts.reason, errors, warnings, fixed };
@@ -29,7 +29,7 @@ export function run(argv) {
29
29
 
30
30
  if (lifecycleOf(config, doc.meta.type) !== "work") {
31
31
  die(`${id} is a ${doc.meta.type} — a living document is never finished. ` +
32
- "Keep it current, or retire it: `honestdocs status " + id + " retired`");
32
+ `Keep it current, or retire it: \`${config.command} status ${id} retired\``);
33
33
  }
34
34
 
35
35
  const status = args.abandoned ? "abandoned" : "done";
@@ -1,5 +1,5 @@
1
1
  import { UsageError, parseArgs } from "../cli.mjs";
2
- import { DEFAULT_ROOT, findConfig, loadConfig } from "../config.mjs";
2
+ import { DEFAULT_ROOT, findConfig, loadConfig, runnerCommand } from "../config.mjs";
3
3
  import { fill, packageFile } from "../templates.mjs";
4
4
 
5
5
  export const summary = "print the method, the agent instructions or the skill";
@@ -24,6 +24,10 @@ export function run(argv) {
24
24
  const topic = args._[0] ?? "method";
25
25
  if (!TOPICS[topic]) throw new UsageError(`unknown topic "${topic}" — expected: ${Object.keys(TOPICS).join(", ")}`);
26
26
  const config = findConfig() ? loadConfig() : null;
27
- const values = { root: config?.root ?? DEFAULT_ROOT, trailer: config?.trailer ?? "Doc" };
27
+ const values = {
28
+ root: config?.root ?? DEFAULT_ROOT,
29
+ trailer: config?.trailer ?? "Doc",
30
+ command: config?.command ?? runnerCommand(),
31
+ };
28
32
  console.log(fill(packageFile("guide", ...TOPICS[topic]), values).trimEnd());
29
33
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
3
  import { c, die, ok, parseArgs, say } from "../cli.mjs";
4
- import { CONFIG_FILE, CONFIG_IN_ROOT, DEFAULT_AREAS, DEFAULT_ROOT, findConfig, loadConfig } from "../config.mjs";
4
+ import { CONFIG_FILE, CONFIG_IN_ROOT, DEFAULT_AREAS, DEFAULT_ROOT, findConfig, loadConfig, runnerCommand } from "../config.mjs";
5
5
  import { today } from "../docs.mjs";
6
6
  import { stringify } from "../frontmatter.mjs";
7
7
  import { fill, packageFile } from "../templates.mjs";
@@ -68,8 +68,12 @@ function upsertBlock(file, block) {
68
68
  return "appended";
69
69
  }
70
70
 
71
- function installAgentBits(base, args, root, trailer) {
72
- const values = { root, trailer };
71
+ /** What the guide texts are filled with: where the documents live, and how to run the tool. */
72
+ const guideValues = (config) => ({ root: config.root, trailer: config.trailer, command: config.command });
73
+
74
+ function installAgentBits(config, args) {
75
+ const base = config.base;
76
+ const values = guideValues(config);
73
77
  if (args.agents || args["agents-file"]) {
74
78
  const file = resolve(base, args["agents-file"] ?? "AGENTS.md");
75
79
  const how = upsertBlock(file, fill(packageFile("guide", "agents-snippet.md"), values));
@@ -96,7 +100,7 @@ export function run(argv) {
96
100
  if (!args.agents && !args["agents-file"] && !args.skill) {
97
101
  die(`already set up: ${existing.file} — re-run with --agents or --skill to refresh those`);
98
102
  }
99
- installAgentBits(config.base, args, config.root, config.trailer);
103
+ installAgentBits(config, args);
100
104
  return;
101
105
  }
102
106
 
@@ -117,6 +121,7 @@ export function run(argv) {
117
121
  title: args.title ?? "Development documents",
118
122
  areas,
119
123
  trailer: "Doc",
124
+ command: runnerCommand(),
120
125
  codeRepos: args["code-repo"].length ? args["code-repo"] : ["."],
121
126
  };
122
127
  const configPath = root === DEFAULT_ROOT ? CONFIG_IN_ROOT : CONFIG_FILE;
@@ -129,7 +134,7 @@ export function run(argv) {
129
134
 
130
135
  const readme = join(config.docsDir, "README.md");
131
136
  if (!existsSync(readme)) {
132
- writeFileSync(readme, fill(packageFile("guide", "docs-readme.md"), { root, trailer: config.trailer }));
137
+ writeFileSync(readme, fill(packageFile("guide", "docs-readme.md"), guideValues(config)));
133
138
  ok(`${root}/README.md`);
134
139
  }
135
140
 
@@ -150,17 +155,18 @@ export function run(argv) {
150
155
  related: [],
151
156
  files: [],
152
157
  };
153
- writeFileSync(rulesFile, stringify(meta) + fill(packageFile("guide", "before-you-build.md"), { root, trailer: config.trailer }));
158
+ writeFileSync(rulesFile, stringify(meta) + fill(packageFile("guide", "before-you-build.md"), guideValues(config)));
154
159
  ok(`${root}/rules/${id}.md`);
155
160
 
156
- installAgentBits(config.base, args, root, config.trailer);
161
+ installAgentBits(config, args);
157
162
  report(config, runIndex(config), false);
158
163
 
159
164
  say("");
160
165
  const area = Object.keys(config.areas).find((a) => a !== "rules") ?? "rules";
166
+ const cmd = config.command;
161
167
  say("Next:");
162
- say(c.dim(` start a document honestdocs new ${area} my-first-design --title "…"`));
168
+ say(c.dim(` start a document ${cmd} new ${area} my-first-design --title "…"`));
163
169
  say(c.dim(` link work to it git commit -m "…" -m "${config.trailer}: <id>"`));
164
- say(c.dim(" keep it honest honestdocs index && honestdocs check"));
165
- if (!args.agents && !args["agents-file"]) say(c.dim(" teach your agents honestdocs init --agents"));
170
+ say(c.dim(` keep it honest ${cmd} index && ${cmd} check`));
171
+ if (!args.agents && !args["agents-file"]) say(c.dim(` teach your agents ${cmd} init --agents`));
166
172
  }
@@ -2,8 +2,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
  import { UsageError, c, die, emitJson, ok, parseArgs, say } from "../cli.mjs";
4
4
  import { loadConfig, statusesFor } from "../config.mjs";
5
- import { loadDocs, slugify, today } from "../docs.mjs";
5
+ import { checklistProgress, loadDocs, slugify, today } from "../docs.mjs";
6
6
  import { stringify } from "../frontmatter.mjs";
7
+ import { renderIndex } from "../render.mjs";
7
8
  import { fill, templateFor } from "../templates.mjs";
8
9
 
9
10
  export const summary = "start a document";
@@ -70,7 +71,9 @@ export function run(argv) {
70
71
  related: args.related,
71
72
  files: [],
72
73
  };
73
- const content = stringify(meta) + fill(templateFor(config, type), { title: args.title, id, date: today() });
74
+ const body = fill(templateFor(config, type), { title: args.title, id, date: today() });
75
+ meta.progress = checklistProgress(body);
76
+ const content = stringify(meta) + body;
74
77
  const file = join(config.docsDir, area, `${id}.md`);
75
78
  const shown = relative(config.base, file).split("\\").join("/");
76
79
 
@@ -82,10 +85,13 @@ export function run(argv) {
82
85
  if (existsSync(file)) die(`${shown} already exists`);
83
86
  mkdirSync(join(config.docsDir, area), { recursive: true });
84
87
  writeFileSync(file, content);
88
+ // A new document has no commits yet, so nothing git knows can change: rendering the
89
+ // index is enough for `check` to pass, without the git pass a full `index` makes.
90
+ writeFileSync(join(config.docsDir, "INDEX.md"), renderIndex(config, loadDocs(config).filter((d) => d.hasMeta)));
85
91
 
86
92
  if (args.json) return emitJson({ id, path: shown, type, status });
87
93
  ok(c.dim(`${type} · ${area} · ${status}`));
88
94
  say(c.dim(` Put "${config.trailer}: ${id}" in the commits that do this work —`));
89
- say(c.dim(" that is how `honestdocs index` learns which files it touched."));
95
+ say(c.dim(` that is how \`${config.command} index\` learns which files it touched.`));
90
96
  console.log(shown);
91
97
  }
@@ -28,9 +28,9 @@ export function run(argv) {
28
28
  const allowed = statusesFor(config, doc.meta.type);
29
29
  if (!allowed) die(`${doc.rel} has an unknown type "${doc.meta.type}"`);
30
30
  if (status === "superseded") {
31
- die("set `supersedes: [" + id + "]` on the document that replaces it, then run `honestdocs index`");
31
+ die(`set \`supersedes: [${id}]\` on the document that replaces it, then run \`${config.command} index\``);
32
32
  }
33
- if (status === "done" || status === "abandoned") die(`use \`honestdocs done ${id}${status === "abandoned" ? " --abandoned" : ""}\` — it checks the checklist first`);
33
+ if (status === "done" || status === "abandoned") die(`use \`${config.command} done ${id}${status === "abandoned" ? " --abandoned" : ""}\` — it checks the checklist first`);
34
34
  if (!allowed[status]) {
35
35
  die(`"${status}" is not a status for a ${doc.meta.type} (${lifecycleOf(config, doc.meta.type)}) — expected one of: ${Object.keys(allowed).filter((s) => s !== "superseded").join(", ")}`);
36
36
  }
package/src/config.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Finding and reading honestdocs.config.json.
1
+ // Finding and reading the config: .honestdocs/config.json, or honestdocs.config.json.
2
2
  //
3
3
  // The config is found by walking up from the working directory, the way git finds .git,
4
4
  // so every command works from anywhere inside the project. Everything a project may want
@@ -52,6 +52,22 @@ const DEFAULT_TYPES = {
52
52
  rule: { lifecycle: "living", description: "Numbered rules every change must keep, each naming its test where it can" },
53
53
  };
54
54
 
55
+ /**
56
+ * How a project runs honestdocs, for every command the tool prints or writes into
57
+ * AGENTS.md. A bare `honestdocs` exists only after a global install, on one machine; a
58
+ * project set up with `npx honestdocs init` has no such command, and an agent told to run
59
+ * `honestdocs brief` fails on its first step. So `init` records the package runner that
60
+ * started it — never the bare name — and a config without one means npx.
61
+ */
62
+ export const DEFAULT_COMMAND = "npx honestdocs";
63
+
64
+ export function runnerCommand(env = process.env) {
65
+ const agent = env.npm_config_user_agent ?? "";
66
+ if (agent.startsWith("bun/")) return "bunx honestdocs";
67
+ if (agent.startsWith("pnpm/")) return "pnpm dlx honestdocs";
68
+ return DEFAULT_COMMAND;
69
+ }
70
+
55
71
  export const DEFAULT_AREAS = {
56
72
  general: "Anything that does not belong to a narrower area yet",
57
73
  rules: "Standing rules every change must keep — read before building, updated in place",
@@ -82,7 +98,7 @@ export function findConfig(start = process.cwd()) {
82
98
  */
83
99
  export function loadConfig(start = process.cwd()) {
84
100
  const found = findConfig(start);
85
- if (!found) throw new Failure(`no ${CONFIG_IN_ROOT} here or above — run \`honestdocs init\` first`);
101
+ if (!found) throw new Failure(`no ${CONFIG_IN_ROOT} here or above — run \`${runnerCommand()} init\` first`);
86
102
  let raw;
87
103
  try {
88
104
  raw = JSON.parse(readFileSync(found.file, "utf8"));
@@ -133,6 +149,7 @@ function normalise(raw, base, file = join(base, CONFIG_FILE)) {
133
149
  areas,
134
150
  types,
135
151
  trailer,
152
+ command: typeof raw.command === "string" && raw.command.trim() ? raw.command.trim() : DEFAULT_COMMAND,
136
153
  codeRepos,
137
154
  templatesDir: raw.templates ? resolve(base, raw.templates) : null,
138
155
  recent: Number.isInteger(raw.recent) && raw.recent > 0 ? raw.recent : 25,
package/src/render.mjs CHANGED
@@ -33,7 +33,7 @@ export function renderIndex(config, docs) {
33
33
  const lines = [
34
34
  `# ${config.title}`,
35
35
  "",
36
- "> Generated by `honestdocs index`. Do not edit — change the document's own frontmatter",
36
+ `> Generated by \`${config.command} index\`. Do not edit — change the document's own frontmatter`,
37
37
  "> and run it again.",
38
38
  ">",
39
39
  "> **The code is the truth.** A document records what was decided at the time. Where the",