@msareen/knowledge-hub-builder 0.2.1 → 0.2.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/AGENTS.md +4 -2
- package/README.md +69 -11
- package/SPEC.md +35 -8
- package/package.json +3 -1
- package/scripts/cli.ts +46 -25
- package/scripts/config.ts +229 -0
- package/scripts/doctor.ts +206 -0
- package/scripts/export.ts +8 -4
- package/scripts/hubs.ts +185 -93
- package/scripts/ingest/acquire.ts +1 -1
- package/scripts/ingest/folder.ts +2 -1
- package/scripts/ingest/index.ts +21 -13
- package/scripts/init.ts +29 -13
- package/scripts/lib/color.ts +75 -0
- package/scripts/lib/config-check.ts +364 -0
- package/scripts/lib/extract.ts +36 -4
- package/scripts/lib/log.ts +4 -2
- package/scripts/lib/registry.ts +20 -1
- package/scripts/lib/relocate.ts +1 -1
- package/scripts/lib/upgrade.ts +17 -5
- package/scripts/lib/util.ts +1 -1
- package/scripts/lint.ts +185 -77
- package/scripts/new-bundle.ts +9 -4
- package/scripts/visualize.ts +7 -6
- package/skills/ingest/SKILL.md +1 -1
- package/skills/lint/SKILL.md +24 -2
- package/templates/hub/gitignore +2 -2
package/AGENTS.md
CHANGED
|
@@ -137,13 +137,14 @@ From outside, pass `--hub <dir>` or set `$KHB_HUB`.
|
|
|
137
137
|
| Command | Purpose |
|
|
138
138
|
|---|---|
|
|
139
139
|
| `khb lint` | validate structure against `skills/lint/SKILL.md` |
|
|
140
|
+
| `khb doctor` | read-only report on the hub: version, location, per-bundle concept/raw/backlog counts, transcriber, the machine config's schema findings, and the command that fixes each finding. Writes nothing, and does not duplicate `lint` |
|
|
140
141
|
| `khb upgrade` | refresh this hub's package-owned contract docs — runs by itself whenever `khb.json`'s stamped version differs from the installed khb, so these docs always match the CLI |
|
|
141
142
|
| `khb visualize [--port N] [--no-open]` | serve the live bundle graph in your browser; aliases `vis`, `viz` |
|
|
142
143
|
| `khb new-bundle <name>` | scaffold + register a bundle |
|
|
143
144
|
| `khb ingest <bundle>` | acquire + extract every source in `sources.yaml` → `raw/`; maintains `log.md`. Name the bundle — with none it lists the hub's bundles and stops, unless there is nothing to choose between (no bundles, or only `default`), where it uses `default` |
|
|
144
145
|
| `khb export <bundle> [dest]` | standalone copy: bundle + common patterns, shareable alone |
|
|
145
146
|
|
|
146
|
-
These
|
|
147
|
+
These six work **outside** any hub, against a per-machine shortcut list at
|
|
147
148
|
`~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows). They hold no knowledge — only
|
|
148
149
|
paths — and fill themselves in as hubs are used, so they need no maintenance.
|
|
149
150
|
|
|
@@ -153,7 +154,8 @@ paths — and fill themselves in as hubs are used, so they need no maintenance.
|
|
|
153
154
|
| `khb go [name\|N]` | open a hub — prints its path, then launches your agent there. Bare `khb` is this: one hub asks, several offer a list, none walks the user through creating the first. `--path` prints only the path |
|
|
154
155
|
| `khb agent [name]` | which agent `khb go` launches — `claude`, `codex`, a custom `--command`, or `none` |
|
|
155
156
|
| `khb update [new-path]` | repair the hub: `--path`/`-p` repoints the list and rewrites every old-path reference after a move (needs no arguments — the move is detected and announced by any khb command run there); `--schema`/`-s` backfills a bundle's `sources.yaml` to the current schema (e.g. a newly added `exclude:` field). No flag runs both. `--dry-run` first. Unrelated to `khb upgrade`, which only touches package-owned contract docs, never bundle content |
|
|
156
|
-
| `khb forget <name
|
|
157
|
+
| `khb forget <name> [more…]` | drop one or more shortcuts; the hub folders are untouched. Every target is resolved before any is removed, so list positions (`khb forget 1 2`) mean what they meant when you typed them. With no name it points at `khb list` |
|
|
158
|
+
| `khb config [view\|edit\|check\|fix\|path]` | the machine config itself: `view` prints it, `edit` opens it in the OS default editor, `check` validates it against the schema, `fix` repairs what can be repaired mechanically (`--dry-run`, `--prune` to drop dead shortcuts), `path` prints just the location. `khb doctor` reports the same findings but never writes |
|
|
157
159
|
|
|
158
160
|
A hub's `name` and `description` in that list come from its own `khb.json`, so they travel
|
|
159
161
|
with it. Set them at creation with `khb init --name --description`, or edit the marker's
|
package/README.md
CHANGED
|
@@ -251,34 +251,59 @@ Commands can be run directly or requested through the matching agent skill.
|
|
|
251
251
|
| `khb new-bundle <name> ["scope"]` | Create and register a bundle |
|
|
252
252
|
| `khb ingest [bundle] [--force] [--skip-ocr] [--skip-audio]` | Acquire and extract declared sources |
|
|
253
253
|
| `khb lint` | Validate routing, bundle structure, and OKF metadata |
|
|
254
|
+
| `khb doctor` | Read-only report: version, location, per-bundle counts, catalog backlog, transcriber |
|
|
254
255
|
| `khb visualize [--port N] [--no-open]` (aliases: `vis`, `viz`) | Serve the live bundle graph on a random free port and open it in your default browser — pan/zoomable cross-bundle map, drill into a bundle for its folder-clustered concepts, rebuild-on-refresh, exits when you close the tab |
|
|
255
256
|
| `khb export <bundle> [dest]` | Export one standalone bundle |
|
|
256
257
|
|
|
257
|
-
|
|
258
|
+
Run against a hub outside the current directory with `--hub <dir>` or `$KHB_HUB`.
|
|
258
259
|
|
|
259
|
-
-
|
|
260
|
-
|
|
260
|
+
Output is colour-coded when the stream is a terminal — headings, the commands you can
|
|
261
|
+
type next, paths, and the difference between an error, a warning and a clean result.
|
|
262
|
+
`NO_COLOR=1` turns it off, `FORCE_COLOR=1` keeps it through a pipe, and a redirected
|
|
263
|
+
stdout is plain text either way.
|
|
261
264
|
|
|
262
|
-
|
|
265
|
+
### `khb doctor` — what state is this hub in?
|
|
266
|
+
|
|
267
|
+
A single read-only report. It writes nothing and repairs nothing; each finding names the
|
|
268
|
+
command that does.
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
Hub name, description, stamped version vs installed, location, registered
|
|
272
|
+
Machine config where it is, the agent it names, how many hubs, schema findings
|
|
273
|
+
Bundles per bundle: concepts, raw/ files, log.md rows, catalog backlog, pending
|
|
274
|
+
Extraction which formats are bundled, and whether a transcriber is ready
|
|
275
|
+
Findings what needs attention, each with its fix
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Every check existed already, spread across the margins of commands that each knew one of
|
|
279
|
+
them — a move is announced by whatever you happen to run next, the `khb update` hint comes
|
|
280
|
+
out of `khb upgrade`, the uncurated row count out of `khb ingest`, and the transcriber probe
|
|
281
|
+
only ever spoke during a run that needed it. `doctor` asks for the whole picture without
|
|
282
|
+
changing anything to get it.
|
|
283
|
+
|
|
284
|
+
It is not a validator: `khb lint` still owns structure and OKF conformance, and `doctor`
|
|
285
|
+
points at it rather than repeating a rule.
|
|
263
286
|
|
|
264
287
|
### Moving between hubs
|
|
265
288
|
|
|
266
|
-
These
|
|
289
|
+
These run **outside** any hub, from any terminal:
|
|
267
290
|
|
|
268
291
|
| Command | Purpose |
|
|
269
292
|
|---|---|
|
|
270
293
|
| `khb` | Open a hub. One registered hub asks; several show a list and take a pick; none walks you through creating the first |
|
|
271
294
|
| `khb list [--json]` | Every hub on this machine, with its description and path |
|
|
272
|
-
| `khb go
|
|
273
|
-
| `khb agent [name] [--command X] [--args "…"]` | Which agent `khb go` launches — `claude`, `codex`, anything on your PATH, or `none` to just print the path |
|
|
295
|
+
| `khb go [name\|N] [--path] [--no-agent] [--agent X]` | Open one by name or list position. `--path` prints just the path, for `cd "$(khb go --path work)"`; `--no-agent` prints the path and the `cd` line without launching anything; `--agent X` launches `X` for this run only, leaving the configured default alone |
|
|
296
|
+
| `khb agent [name\|none] [--command X] [--args "…"]` | Which agent `khb go` launches — `claude`, `codex`, anything on your PATH, or `none` to just print the path |
|
|
274
297
|
| `khb update [new-path] [--path\|-p] [--schema\|-s] [--from <old>] [--dry-run]` | Repair the hub: path references after a move, and/or backfill `sources.yaml` to the current schema. No flag runs both |
|
|
275
|
-
| `khb forget <name
|
|
298
|
+
| `khb forget <name> [more…]` | Drop one or more hubs from the list. The folders are untouched. With no name it points you at `khb list` for the names |
|
|
299
|
+
| `khb config [view\|edit\|check\|fix\|path]` | The config file itself — see below |
|
|
276
300
|
|
|
277
301
|
`khb go` prints the hub's path and then starts your agent there, so a bare `khb` from a
|
|
278
302
|
cold terminal ends with an agent open on the right folder. No program can change its
|
|
279
303
|
parent shell's directory, which is why the `cd` line is printed rather than performed.
|
|
280
304
|
|
|
281
|
-
The list lives in `~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows
|
|
305
|
+
The list lives in `~/.khb/hubs-config.json` (`%USERPROFILE%\.khb` on Windows; `$KHB_HOME`
|
|
306
|
+
overrides the directory, for a portable install or a test run). It holds
|
|
282
307
|
paths and one launch command — no knowledge — and fills itself in: any khb command run
|
|
283
308
|
inside a hub registers it, so hubs you already had show up without a migration step.
|
|
284
309
|
Delete the file and the next command in each hub puts it back.
|
|
@@ -287,6 +312,39 @@ A hub's name and description come from its own `khb.json`, so they travel with t
|
|
|
287
312
|
rather than living in one machine's list. Set them with `khb init --name --description`,
|
|
288
313
|
or edit those two keys in the marker later.
|
|
289
314
|
|
|
315
|
+
### `khb config` — the file itself
|
|
316
|
+
|
|
317
|
+
The list is plain JSON and always has been editable by hand. These five reach it without
|
|
318
|
+
hunting for the path, and say something when a hand edit has gone wrong:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
khb config # or 'view' — the path, the file, and a findings count
|
|
322
|
+
khb config edit # open it in whatever this machine opens .json with
|
|
323
|
+
khb config check # validate it against the schema
|
|
324
|
+
khb config fix # repair what can be repaired mechanically
|
|
325
|
+
khb config fix --dry-run --prune # preview; --prune also drops dead shortcuts
|
|
326
|
+
khb config path # just the path, for cat "$(khb config path)"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The reason `check` exists: `loadConfig` is forgiving on purpose — it ignores keys it does
|
|
330
|
+
not know and treats an unparseable file as an empty one, so a damaged registry never
|
|
331
|
+
blocks `khb lint` in a hub that is fine. The cost is silence. A `defaultagent` typo does
|
|
332
|
+
nothing at all, a pasted duplicate makes `khb forget` look broken, and a stray comma
|
|
333
|
+
loses you every shortcut with no message anywhere. `check` names each of those, `fix`
|
|
334
|
+
repairs the mechanical ones — canonicalizing paths, merging duplicate entries, dropping
|
|
335
|
+
keys the schema does not define, re-deriving a name or description that has drifted from
|
|
336
|
+
the hub's own `khb.json` — and leaves anything needing a decision to you, with the
|
|
337
|
+
command that settles it.
|
|
338
|
+
|
|
339
|
+
`khb doctor` runs the same checks and prints the same fixes, so a hub health check covers
|
|
340
|
+
the machine config too. Doctor never writes; `khb config fix` is the half that does.
|
|
341
|
+
|
|
342
|
+
Two things `fix` will not do on its own: drop a shortcut whose folder has gone (that is
|
|
343
|
+
`--prune`, or `khb forget` — a missing path can be an unplugged drive, and a moved hub
|
|
344
|
+
wants `khb update --path`, not forgetting), and rename a hub. A name is re-derived from
|
|
345
|
+
each hub's own `khb.json` every time a command runs there, so renaming the entry would
|
|
346
|
+
be undone on the next command — the rename belongs in the marker.
|
|
347
|
+
|
|
290
348
|
### `khb update` — repairing a hub
|
|
291
349
|
|
|
292
350
|
Two independent repairs, run together or apart:
|
|
@@ -324,7 +382,7 @@ the same spelling of the new path; matches must end at a path boundary, so movin
|
|
|
324
382
|
never touches a sibling `…/older`; and the new path is matched too and rewritten to itself,
|
|
325
383
|
which is what makes overlapping moves safe — lifting a hub out of its parent, or pushing it
|
|
326
384
|
down into a subdirectory of where it stood — and makes a second run a no-op.
|
|
327
|
-
`.git/`, `node_modules/` and the
|
|
385
|
+
`.git/`, `node_modules/` and the `.ingest-cache/` cache are skipped, as are binaries.
|
|
328
386
|
|
|
329
387
|
**`--schema`/`-s`** — a bundle's `sources.yaml` can predate a field khb's since learned about
|
|
330
388
|
(e.g. `exclude:`), with no way to discover it short of reading the docs. This backfills
|
|
@@ -373,7 +431,7 @@ everything under `bundles/`.
|
|
|
373
431
|
|
|
374
432
|
## Privacy
|
|
375
433
|
|
|
376
|
-
`raw/` and the extraction cache under
|
|
434
|
+
`raw/` and the extraction cache under `.ingest-cache/` are gitignored. `log.md` is committed and
|
|
377
435
|
records source paths, which may be absolute. Ignore `log.md` before the first commit if
|
|
378
436
|
those paths are sensitive.
|
|
379
437
|
|
package/SPEC.md
CHANGED
|
@@ -72,7 +72,7 @@ my-knowledge/ # ~/OneDrive/my-knowledge, a private repo, a shar
|
|
|
72
72
|
│ │ ├── index.md # (each may carry its own index)
|
|
73
73
|
│ │ └── <concept>.md
|
|
74
74
|
│ └── raw/ # ingested/extracted material, pre-curation (gitignored)
|
|
75
|
-
├──
|
|
75
|
+
├── .ingest-cache/extracted/ # hub-wide extraction cache, keyed by content hash (gitignored)
|
|
76
76
|
│
|
|
77
77
|
│ ── below: package-owned copies, refreshed by `khb upgrade`, never hand-edited ──
|
|
78
78
|
├── AGENTS.md # common contract; Codex discovers this directly
|
|
@@ -121,14 +121,24 @@ acting on them** — an agent can never read a protocol the CLI no longer implem
|
|
|
121
121
|
│ ├── new-bundle.ts # scaffold from .bundle_template, register in outer.index.md
|
|
122
122
|
│ ├── export.ts # bundle + common patterns → standalone shareable folder
|
|
123
123
|
│ ├── lint.ts # enforce skills/lint/SKILL.md across the hub
|
|
124
|
+
│ ├── doctor.ts # read-only state report; writes nothing, repairs nothing
|
|
124
125
|
│ ├── visualize.ts # serve the live bundle graph from indexes + refs
|
|
126
|
+
│ ├── config.ts # khb config: view / edit / check / fix the machine config
|
|
125
127
|
│ ├── ingest/ # folder.ts / files.ts / web.ts → acquire.ts → bundle/raw
|
|
126
128
|
│ └── lib/
|
|
129
|
+
│ ├── args.ts # argv helpers; what they consume they remove, leaving positionals
|
|
130
|
+
│ ├── color.ts # semantic terminal colour; one palette per stream
|
|
131
|
+
│ ├── config-check.ts # the machine config's schema: findings + the repairs for them
|
|
132
|
+
│ ├── create.ts # making a hub, shared by `khb init` and the first-run wizard
|
|
127
133
|
│ ├── extract.ts # every local extractor + the content-hash cache
|
|
134
|
+
│ ├── graph.ts # graph data for the visualizer — read-only, never writes
|
|
135
|
+
│ ├── graph-page.ts # the visualizer's browser UI, rendered from that data
|
|
128
136
|
│ ├── ledger.ts # log.md read/write
|
|
137
|
+
│ ├── log.ts # progress reporting: each unit announces itself before it runs
|
|
129
138
|
│ ├── paths.ts # package-side paths — importing it never needs a hub
|
|
130
139
|
│ ├── registry.ts # ~/.khb/hubs-config.json: where this machine's hubs are
|
|
131
140
|
│ ├── relocate.ts # khb update --path's path rewriter — pure text, no judgement
|
|
141
|
+
│ ├── scaffold.ts # bundle creation + lookup, shared by new-bundle and ingest
|
|
132
142
|
│ ├── schema.ts # khb update --schema: sources.yaml field diff/apply
|
|
133
143
|
│ ├── upgrade.ts # the refresh itself: `khb upgrade` and the drift check
|
|
134
144
|
│ └── util.ts # hub resolution + shared helpers
|
|
@@ -180,8 +190,9 @@ Three properties define it:
|
|
|
180
190
|
Delete it and nothing is lost — the next command run inside each hub puts it back.
|
|
181
191
|
- **It fills itself in.** Every khb command that resolves a hub registers it, so hubs made
|
|
182
192
|
before the registry existed appear the first time anything is run in them. There is no
|
|
183
|
-
migration and no `register` command to remember. `khb forget <name
|
|
184
|
-
never touches the
|
|
193
|
+
migration and no `register` command to remember. `khb forget <name> [more…]` drops one or more
|
|
194
|
+
shortcuts and never touches the folders. Every target is resolved before any is removed,
|
|
195
|
+
so list positions still mean what they meant when the command was typed.
|
|
185
196
|
- **The hub is the authority on its own identity.** `name` and `description` are read out
|
|
186
197
|
of the hub's `khb.json` (`khb init --name --description`, or edit the file), so a hub
|
|
187
198
|
moved to another machine or cloned by a colleague describes itself the same way there.
|
|
@@ -189,9 +200,9 @@ Three properties define it:
|
|
|
189
200
|
the bundles inside. `khb upgrade` merges rather than replaces the marker, so keys khb
|
|
190
201
|
does not own survive an upgrade.
|
|
191
202
|
|
|
192
|
-
The commands over it are `khb list`, `khb go`, `khb agent`, `khb update
|
|
193
|
-
the only ones that run **outside** a hub, and therefore the only ones that
|
|
194
|
-
resolution and the version drift check. A bare `khb` is `khb go`: one hub asks to open it,
|
|
203
|
+
The commands over it are `khb list`, `khb go`, `khb agent`, `khb update`, `khb forget` and
|
|
204
|
+
`khb config` — the only ones that run **outside** a hub, and therefore the only ones that
|
|
205
|
+
skip hub resolution and the version drift check. A bare `khb` is `khb go`: one hub asks to open it,
|
|
195
206
|
several show the list and take a pick, none prints the help.
|
|
196
207
|
|
|
197
208
|
`khb go` ends by launching your configured agent with the hub as its working directory.
|
|
@@ -199,6 +210,18 @@ No process can change its parent shell's directory, so `khb go` prints the `cd`
|
|
|
199
210
|
the human and passes the path to the agent as cwd — `khb go --path <name>` prints only the
|
|
200
211
|
path, for `cd "$(khb go --path work)"`. `khb agent none` turns the launch off entirely.
|
|
201
212
|
|
|
213
|
+
**The file is hand-editable, so it is also checked.** `loadConfig` is deliberately
|
|
214
|
+
forgiving — unknown keys ignored, a file that will not parse treated as empty — which is
|
|
215
|
+
right at load time and wrong as the only feedback anyone gets: a typo'd `defaultagent`
|
|
216
|
+
silently does nothing, and broken JSON silently costs you every shortcut. So the schema
|
|
217
|
+
is stated once, in `lib/config-check.ts`, as findings with repairs attached. `khb config
|
|
218
|
+
check` lists them, `khb config fix` applies the mechanical ones, and `khb doctor` reports
|
|
219
|
+
them without writing. A finding carries a repair only when the fix loses nothing:
|
|
220
|
+
canonicalizing a path, merging a duplicate entry, re-deriving a stale name from the hub's
|
|
221
|
+
own marker. Anything needing a human decision — which of two same-named hubs to rename,
|
|
222
|
+
whether a missing folder was deleted or is on an unplugged drive — is reported with the
|
|
223
|
+
command to run and left alone.
|
|
224
|
+
|
|
202
225
|
Every path stored in or compared against the registry is canonicalized first (`realpath`,
|
|
203
226
|
case-folded on Windows). One directory has several true names — `C:\Users\MANASV~1\…` and
|
|
204
227
|
`C:\Users\Manasvi Sareen\…` are the same folder, as is anything reached through a symlink —
|
|
@@ -272,7 +295,7 @@ unattended:
|
|
|
272
295
|
changes nothing. Only old and new naming *the same directory* is refused, there being no
|
|
273
296
|
move to repair.
|
|
274
297
|
|
|
275
|
-
`.git/`, `node_modules/` and the
|
|
298
|
+
`.git/`, `node_modules/` and the `.ingest-cache/` extraction cache are not walked; binary files and
|
|
276
299
|
anything over 8 MB are skipped. `--dry-run` reports the file-by-file hit count and writes
|
|
277
300
|
nothing.
|
|
278
301
|
|
|
@@ -483,7 +506,7 @@ raw files, with one hard rule — subagents write concept docs, the orchestrator
|
|
|
483
506
|
|
|
484
507
|
Extraction is deterministic, so `khb` owns all of it. Common formats use bundled pure-JS
|
|
485
508
|
libraries with no system install; results are cached hub-wide by content hash
|
|
486
|
-
(
|
|
509
|
+
(`.ingest-cache/extracted/<sha256>.md`) and reused across bundles. Nothing here contacts a model:
|
|
487
510
|
tesseract and whisper are local binaries, expensive in CPU but reproducible, which is what
|
|
488
511
|
puts them on the CLI side of the §Division-of-labor line.
|
|
489
512
|
|
|
@@ -526,6 +549,10 @@ drops, and rewrites the raw file with `extract_tool: claude-vision`.
|
|
|
526
549
|
- every concept doc is listed in an index and carries OKF frontmatter (`type` required)
|
|
527
550
|
- `refs.md` targets exist; no cross-bundle inline links from concept docs
|
|
528
551
|
- index files contain links only (routing, not content)
|
|
552
|
+
- intra-bundle links from a concept doc resolve to a file that exists (warning — OKF
|
|
553
|
+
tolerates a link to knowledge not yet written)
|
|
554
|
+
- `log.md` agrees with the bundle: a `curated` value naming a missing concept is an error,
|
|
555
|
+
a `raw` value naming a missing file or a `raw/` file with no row is a warning
|
|
529
556
|
|
|
530
557
|
## 8. Visualizer
|
|
531
558
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msareen/knowledge-hub-builder",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Knowledge Hub Builder — a bundle-of-bundles knowledge base you build with an agent",
|
|
5
5
|
"keywords": ["knowledge-base", "okf", "agent", "wiki", "bundles", "claude", "codex", "rag"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"khb": "bun scripts/cli.ts",
|
|
37
37
|
"lint": "bun scripts/cli.ts lint",
|
|
38
|
+
"upgrade": "bun scripts/cli.ts upgrade",
|
|
39
|
+
"doctor": "bun scripts/cli.ts doctor",
|
|
38
40
|
"visualize": "bun scripts/cli.ts visualize",
|
|
39
41
|
"new-bundle": "bun scripts/cli.ts new-bundle",
|
|
40
42
|
"export": "bun scripts/cli.ts export",
|
package/scripts/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// khb — the CLI. Subcommands are loaded lazily: `init` must run before a hub exists,
|
|
3
3
|
// so nothing that resolves a hub may be imported at module scope.
|
|
4
4
|
import { version, findHub, markerIn, MARKER } from "./lib/paths";
|
|
5
|
+
import { paint, paintErr } from "./lib/color";
|
|
5
6
|
|
|
6
7
|
const COMMANDS: Record<string, { load: () => Promise<unknown>; usage: string; desc: string }> = {
|
|
7
8
|
init: { load: () => import("./init"), usage: 'khb init [dir] [--name N] [--description "…"]', desc: "create a hub here (or in dir)" },
|
|
@@ -13,6 +14,7 @@ const COMMANDS: Record<string, { load: () => Promise<unknown>; usage: string; de
|
|
|
13
14
|
desc: "acquire + extract declared sources → raw/",
|
|
14
15
|
},
|
|
15
16
|
lint: { load: () => import("./lint"), usage: "khb lint", desc: "validate the hub against skills/lint/SKILL.md" },
|
|
17
|
+
doctor: { load: () => import("./doctor"), usage: "khb doctor", desc: "read-only report on this hub's state" },
|
|
16
18
|
visualize: {
|
|
17
19
|
load: () => import("./visualize"),
|
|
18
20
|
usage: "khb visualize [--port N] [--no-open]",
|
|
@@ -35,14 +37,23 @@ const COMMANDS: Record<string, { load: () => Promise<unknown>; usage: string; de
|
|
|
35
37
|
usage: "khb update [new-path] [--path|-p] [--schema|-s] [--from <old>] [--dry-run]",
|
|
36
38
|
desc: "repair a moved hub's paths, and/or backfill sources.yaml",
|
|
37
39
|
},
|
|
38
|
-
forget: {
|
|
40
|
+
forget: {
|
|
41
|
+
load: () => import("./hubs"),
|
|
42
|
+
usage: "khb forget <name|path> [more…]",
|
|
43
|
+
desc: "drop one or more hubs from the list (folders untouched)",
|
|
44
|
+
},
|
|
45
|
+
config: {
|
|
46
|
+
load: () => import("./config"),
|
|
47
|
+
usage: "khb config [view|edit|check|fix|path]",
|
|
48
|
+
desc: "the machine config: show it, open it, validate it, repair it",
|
|
49
|
+
},
|
|
39
50
|
};
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
53
|
* Commands that work *outside* a hub, against ~/.khb/hubs-config.json. They must not
|
|
43
54
|
* resolve or upgrade a hub — their whole job is running before you are in one.
|
|
44
55
|
*/
|
|
45
|
-
const REGISTRY_COMMANDS = new Set(["list", "go", "agent", "forget", "update"]);
|
|
56
|
+
const REGISTRY_COMMANDS = new Set(["list", "go", "agent", "forget", "update", "config"]);
|
|
46
57
|
|
|
47
58
|
// Short forms that just resolve to a canonical command above — kept out of COMMANDS
|
|
48
59
|
// itself so help text lists each command once. `-v` is taken by --version, so
|
|
@@ -78,19 +89,20 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
|
78
89
|
const usages = Object.values(COMMANDS).map((c) => c.usage.length);
|
|
79
90
|
const width = Math.max(...usages.filter((n) => n <= CAP));
|
|
80
91
|
const printSection = (title: string, names: string[]) => {
|
|
81
|
-
console.log(title);
|
|
92
|
+
console.log(paint.head(title));
|
|
82
93
|
for (const name of names) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.log(` ${
|
|
94
|
+
const entry = COMMANDS[name];
|
|
95
|
+
// Pad before painting: escape sequences have width in the string and none on screen.
|
|
96
|
+
if (entry.usage.length > width) {
|
|
97
|
+
console.log(` ${paint.cmd(entry.usage)}`);
|
|
98
|
+
console.log(` ${" ".repeat(width)} ${entry.desc}`);
|
|
87
99
|
} else {
|
|
88
|
-
console.log(` ${
|
|
100
|
+
console.log(` ${paint.cmd(entry.usage.padEnd(width))} ${entry.desc}`);
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
};
|
|
92
104
|
|
|
93
|
-
console.log(`khb ${version()} — Knowledge Hub Builder`);
|
|
105
|
+
console.log(`${paint.head(`khb ${version()}`)} — Knowledge Hub Builder`);
|
|
94
106
|
console.log();
|
|
95
107
|
console.log(`khb is the supporting tool: it handles deterministic extraction, file plumbing,`);
|
|
96
108
|
console.log(`validation, and export. Your AI agent — Claude, Codex, Gemini, or another`);
|
|
@@ -108,14 +120,16 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
|
108
120
|
);
|
|
109
121
|
console.log();
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
console.log(
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(
|
|
115
|
-
console.log(
|
|
116
|
-
console.log(
|
|
117
|
-
console.log(
|
|
118
|
-
console.log(
|
|
123
|
+
const label = (text: string) => paint.head(text.padEnd(9));
|
|
124
|
+
console.log(`${label("Global:")}${paint.cmd("--hub <dir>")} operate on that hub instead of searching upward from cwd`);
|
|
125
|
+
console.log(`${" ".repeat(9)}${paint.cmd("help | --help | -h")} this help ${paint.cmd("--version | -v")} version`);
|
|
126
|
+
console.log(`${label("Env:")}${paint.cmd("KHB_HUB")} same as --hub`);
|
|
127
|
+
console.log(`${" ".repeat(9)}${paint.cmd("KHB_HOME")} where the hub list lives (default ~/.khb)`);
|
|
128
|
+
console.log(`${" ".repeat(9)}${paint.cmd("KHB_NO_AUTO_UPGRADE")} don't refresh a hub's contract docs on version drift`);
|
|
129
|
+
console.log(`${label("Colour:")}on when stdout is a terminal — ${paint.cmd("NO_COLOR")} turns it off, ${paint.cmd("FORCE_COLOR")} forces it on`);
|
|
130
|
+
console.log(`${label("Exit:")}0 on success, 1 on a usage error or a failure. An unknown option is an error;`);
|
|
131
|
+
console.log(`${" ".repeat(9)}a source khb cannot extract is not — it becomes a pending row in log.md.`);
|
|
132
|
+
console.log(`${label("Docs:")}${paint.path("https://github.com/msareen/knowledge-hub-builder")}`);
|
|
119
133
|
process.exit(0);
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -126,8 +140,8 @@ if (cmd === "--version" || cmd === "-v") {
|
|
|
126
140
|
|
|
127
141
|
const entry = COMMANDS[cmd];
|
|
128
142
|
if (!entry) {
|
|
129
|
-
console.error(
|
|
130
|
-
console.error(`Try: khb help`);
|
|
143
|
+
console.error(`${paintErr.bad("Unknown command:")} ${cmd}`);
|
|
144
|
+
console.error(`Try: ${paintErr.cmd("khb help")}`);
|
|
131
145
|
process.exit(1);
|
|
132
146
|
}
|
|
133
147
|
|
|
@@ -153,9 +167,13 @@ if (cmd !== "init" && cmd !== "upgrade" && !REGISTRY_COMMANDS.has(cmd)) {
|
|
|
153
167
|
const { recordLocation } = await import("./lib/upgrade");
|
|
154
168
|
const { moved } = recordLocation(hub);
|
|
155
169
|
if (moved) {
|
|
156
|
-
console.error(
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
console.error(
|
|
171
|
+
`${paintErr.warn("khb:")} this hub was at ${paintErr.path(moved)} and is now at ${paintErr.path(hub)}.`,
|
|
172
|
+
);
|
|
173
|
+
console.error(`${paintErr.warn("khb:")} absolute paths recorded inside it still name the old location.`);
|
|
174
|
+
console.error(
|
|
175
|
+
`${paintErr.warn("khb:")} repair them: ${paintErr.cmd("khb update --path")} (--dry-run to preview)`,
|
|
176
|
+
);
|
|
159
177
|
}
|
|
160
178
|
}
|
|
161
179
|
if (hub && !process.env.KHB_NO_AUTO_UPGRADE) {
|
|
@@ -165,10 +183,13 @@ if (cmd !== "init" && cmd !== "upgrade" && !REGISTRY_COMMANDS.has(cmd)) {
|
|
|
165
183
|
const { from, to, pruned, renamed } = upgradeHub(hub);
|
|
166
184
|
// stderr, so a command's own output stays pipeable.
|
|
167
185
|
console.error(
|
|
168
|
-
|
|
186
|
+
`${paintErr.warn("khb:")} hub was built by ${from ?? "an unknown version"}, khb is ${to} — refreshed its contract docs.`,
|
|
169
187
|
);
|
|
170
|
-
if (renamed) console.error(
|
|
171
|
-
if (pruned.length)
|
|
188
|
+
if (renamed) console.error(`${paintErr.warn("khb:")} renamed ${renamed} -> khb.json`);
|
|
189
|
+
if (pruned.length)
|
|
190
|
+
console.error(
|
|
191
|
+
`${paintErr.warn("khb:")} removed (no longer part of the contract): ${pruned.join(", ")}`,
|
|
192
|
+
);
|
|
172
193
|
const hint = updateHint(hub);
|
|
173
194
|
if (hint) console.error(hint);
|
|
174
195
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// khb config — view, edit, check and repair the machine-level config.
|
|
2
|
+
//
|
|
3
|
+
// ~/.khb/hubs-config.json is the only khb file that is neither knowledge nor package-owned:
|
|
4
|
+
// it is this machine's shortcut list plus the agent `khb go` launches. It has always been
|
|
5
|
+
// hand-editable — the README says where it lives and what is in it — but nothing pointed at
|
|
6
|
+
// it from the CLI, and nothing ever told you when a hand edit had gone wrong. `loadConfig`
|
|
7
|
+
// is forgiving by design (unknown keys ignored, unparseable file treated as empty), so a
|
|
8
|
+
// typo costs you your hub list with no message at all.
|
|
9
|
+
//
|
|
10
|
+
// So: `view` and `edit` to reach it without hunting for the path, `check` to say what is
|
|
11
|
+
// wrong, `fix` to repair what can be repaired mechanically. The rules live once, in
|
|
12
|
+
// lib/config-check.ts, and `khb doctor` reads the same checker — doctor reports and never
|
|
13
|
+
// writes, this is where the writing happens.
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { checkConfig, pruneDead, type Finding } from "./lib/config-check";
|
|
17
|
+
import { CONFIG, KHB_HOME, loadConfig, saveConfig } from "./lib/registry";
|
|
18
|
+
import { takeFlag, rejectUnknownFlags } from "./lib/args";
|
|
19
|
+
import { detail, section, totalElapsed } from "./lib/log";
|
|
20
|
+
import { paint, paintErr } from "./lib/color";
|
|
21
|
+
|
|
22
|
+
const USAGE = "khb config [view|edit|check|fix|path] [--json] [--dry-run] [--prune]";
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
|
|
25
|
+
const asJson = takeFlag(argv, "--json");
|
|
26
|
+
const dryRun = takeFlag(argv, "--dry-run");
|
|
27
|
+
const prune = takeFlag(argv, "--prune");
|
|
28
|
+
rejectUnknownFlags(argv, USAGE);
|
|
29
|
+
|
|
30
|
+
// Bare `khb config` shows the file. A command whose default action is read-only is the
|
|
31
|
+
// right default for the one file whose corruption costs you every shortcut you have.
|
|
32
|
+
const action = argv[0] ?? "view";
|
|
33
|
+
if (argv.length > 1) {
|
|
34
|
+
console.error(`${paintErr.bad("Too many arguments:")} ${argv.slice(1).join(" ")}`);
|
|
35
|
+
console.error(`Usage: ${paintErr.cmd(USAGE)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ensure the file exists before anyone is told to look at it — first run has none. */
|
|
40
|
+
function ensureExists(): void {
|
|
41
|
+
if (!existsSync(CONFIG)) saveConfig(loadConfig());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printFinding(finding: Finding): void {
|
|
45
|
+
const tag = finding.level === "error" ? paint.bad("ERROR") : paint.warn("warn ");
|
|
46
|
+
console.log(` ${tag} ${finding.what}`);
|
|
47
|
+
console.log(` ${paint.dim(finding.repair ? "fix (automatic):" : "fix:")} ${paint.cmd(finding.fix)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The closing line every action shares: how many findings stand, and what clears them. */
|
|
51
|
+
function summarize(findings: Finding[]): void {
|
|
52
|
+
const errors = findings.filter((finding) => finding.level === "error").length;
|
|
53
|
+
const repairable = findings.filter((finding) => finding.repair).length;
|
|
54
|
+
if (!findings.length) {
|
|
55
|
+
console.log(`\n${paint.ok("no problems found")} in ${totalElapsed()}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log(
|
|
59
|
+
`\n${paint.head("config")}: ${errors ? paint.bad(`${errors} error(s)`) : paint.ok("0 errors")}, ` +
|
|
60
|
+
`${paint.warn(`${findings.length - errors} warning(s)`)} in ${totalElapsed()}`,
|
|
61
|
+
);
|
|
62
|
+
if (repairable)
|
|
63
|
+
console.log(`${repairable} of them can be repaired for you: ${paint.cmd("khb config fix")}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------------------------------- path
|
|
67
|
+
|
|
68
|
+
if (action === "path") {
|
|
69
|
+
// Deliberately bare, so `cat "$(khb config path)"` works. Nothing else on stdout.
|
|
70
|
+
console.log(CONFIG);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ------------------------------------------------------------------------------- view
|
|
75
|
+
|
|
76
|
+
if (action === "view") {
|
|
77
|
+
ensureExists();
|
|
78
|
+
const text = readFileSync(CONFIG, "utf8");
|
|
79
|
+
if (asJson) {
|
|
80
|
+
// The file itself, verbatim — pipeable into jq. No header, no findings.
|
|
81
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
console.log(`${paint.head("khb config")} → ${paint.path(CONFIG)}\n`);
|
|
85
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
86
|
+
|
|
87
|
+
const { findings } = checkConfig({ probeAgent: false });
|
|
88
|
+
if (findings.length) {
|
|
89
|
+
console.log(
|
|
90
|
+
`\n${paint.warn(`${findings.length} finding(s)`)} — see them in full: ${paint.cmd("khb config check")}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
console.log(paint.dim(`Edit it: khb config edit Repair it: khb config fix`));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------------------- edit
|
|
98
|
+
|
|
99
|
+
if (action === "edit") {
|
|
100
|
+
ensureExists();
|
|
101
|
+
console.log(`${paint.head("khb config")} → ${paint.path(CONFIG)}`);
|
|
102
|
+
|
|
103
|
+
// The OS association, the same way `khb visualize` opens a browser: whatever this machine
|
|
104
|
+
// already opens a .json with is the editor the person has actually chosen. $EDITOR is not
|
|
105
|
+
// consulted — a terminal editor launched detached would draw over this session.
|
|
106
|
+
const command =
|
|
107
|
+
process.platform === "win32"
|
|
108
|
+
? ["cmd", "/c", "start", "", CONFIG] // the empty string is start's window-title argument
|
|
109
|
+
: process.platform === "darwin"
|
|
110
|
+
? ["open", CONFIG]
|
|
111
|
+
: ["xdg-open", CONFIG];
|
|
112
|
+
|
|
113
|
+
const launched = spawnSync(command[0], command.slice(1), { stdio: "ignore" });
|
|
114
|
+
if (launched.error || (launched.status ?? 0) !== 0) {
|
|
115
|
+
console.error(`\n${paintErr.warn("Could not open an editor")} — open the path above yourself.`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(` ${paint.dim("opened in your default editor for .json")}`);
|
|
120
|
+
// The editor is detached, so khb cannot wait for the save and validate it. Say what to run
|
|
121
|
+
// instead — a config that stops parsing is silently treated as empty, which is exactly the
|
|
122
|
+
// failure someone editing by hand is most likely to cause and least likely to notice.
|
|
123
|
+
console.log(`\nWhen you have saved it: ${paint.cmd("khb config check")}`);
|
|
124
|
+
console.log(paint.dim(`An unparseable file is ignored in full — khb would act as if you had no hubs.`));
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ------------------------------------------------------------------------------ check
|
|
129
|
+
|
|
130
|
+
if (action === "check") {
|
|
131
|
+
console.log(`${paint.head("khb config check")} → ${paint.path(CONFIG)}`);
|
|
132
|
+
const report = checkConfig();
|
|
133
|
+
if (!report.exists) {
|
|
134
|
+
detail(`no config yet — it is written the first time khb registers a hub`);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
section(report.findings.length ? `Findings (${report.findings.length})` : "Findings");
|
|
138
|
+
if (!report.findings.length) detail(paint.ok("none — nothing here needs attention."));
|
|
139
|
+
else for (const finding of report.findings) printFinding(finding);
|
|
140
|
+
summarize(report.findings);
|
|
141
|
+
// Exit 0 with findings, like `khb lint` and `khb doctor`: the exit code says the command
|
|
142
|
+
// ran, not that the thing it inspected is perfect.
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// -------------------------------------------------------------------------------- fix
|
|
147
|
+
|
|
148
|
+
if (action === "fix") {
|
|
149
|
+
console.log(`${paint.head("khb config fix")}${dryRun ? paint.dim(" (dry run)") : ""} → ${paint.path(CONFIG)}`);
|
|
150
|
+
const first = checkConfig();
|
|
151
|
+
if (!first.exists) {
|
|
152
|
+
detail(`no config yet — nothing to repair`);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
if (!first.readable) {
|
|
156
|
+
// Every repair works on the parsed config, and there is none. Rewriting from the
|
|
157
|
+
// normalized (empty) view would silently delete every shortcut in the file.
|
|
158
|
+
section("Findings");
|
|
159
|
+
for (const finding of first.findings) printFinding(finding);
|
|
160
|
+
console.log(`\n${paintErr.bad("Nothing can be repaired while the file does not parse.")}`);
|
|
161
|
+
console.log(`Fix the JSON by hand: ${paint.cmd("khb config edit")}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
section("Repairs");
|
|
166
|
+
const applied: string[] = [];
|
|
167
|
+
// One repair per pass, re-checking in between. Repairs interact — canonicalizing a path
|
|
168
|
+
// changes the key a later repair looks its entry up by, and merging a duplicate shifts
|
|
169
|
+
// every index after it — so the honest way to apply a set of them is to apply one and
|
|
170
|
+
// ask again. It converges: each pass either clears a finding or stops.
|
|
171
|
+
let previous = "";
|
|
172
|
+
for (let pass = 0; pass < 100; pass++) {
|
|
173
|
+
const { findings } = checkConfig({ probeAgent: false });
|
|
174
|
+
const next = findings.find((finding) => finding.repair);
|
|
175
|
+
if (!next) break;
|
|
176
|
+
if (next.what === previous) {
|
|
177
|
+
// The repair ran and the finding came back: report it rather than loop.
|
|
178
|
+
console.log(` ${paint.warn("could not repair:")} ${next.what}`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
previous = next.what;
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
// Nothing is written, so re-checking would return the same finding forever. Show the
|
|
184
|
+
// whole repairable set in one go instead of walking it.
|
|
185
|
+
for (const finding of findings.filter((each) => each.repair))
|
|
186
|
+
console.log(` ${paint.dim("would fix:")} ${finding.what}`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const cfg = loadConfig();
|
|
190
|
+
next.repair!(cfg);
|
|
191
|
+
saveConfig(cfg);
|
|
192
|
+
applied.push(next.what);
|
|
193
|
+
console.log(` ${paint.ok("fixed:")} ${next.what}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (prune) {
|
|
197
|
+
const cfg = loadConfig();
|
|
198
|
+
const dead = pruneDead(cfg);
|
|
199
|
+
if (!dead.length) detail(`--prune: no dead shortcuts to drop`);
|
|
200
|
+
else if (dryRun)
|
|
201
|
+
for (const entry of dead)
|
|
202
|
+
console.log(` ${paint.dim("would drop:")} ${entry.name} (${entry.path})`);
|
|
203
|
+
else {
|
|
204
|
+
saveConfig(cfg);
|
|
205
|
+
for (const entry of dead)
|
|
206
|
+
console.log(` ${paint.ok("dropped:")} ${entry.name} ${paint.path(`(${entry.path})`)}`);
|
|
207
|
+
console.log(paint.dim(` the folders themselves are untouched`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!applied.length && !dryRun) detail(`nothing to repair automatically`);
|
|
212
|
+
|
|
213
|
+
// What is left is what needed a person. Say so explicitly rather than reporting success
|
|
214
|
+
// on a config that still has problems in it.
|
|
215
|
+
const after = checkConfig();
|
|
216
|
+
const manual = after.findings.filter((finding) => !finding.repair);
|
|
217
|
+
if (manual.length) {
|
|
218
|
+
section(`Left for you (${manual.length})`);
|
|
219
|
+
for (const finding of manual) printFinding(finding);
|
|
220
|
+
}
|
|
221
|
+
summarize(after.findings);
|
|
222
|
+
if (dryRun) console.log(`Re-run without ${paint.cmd("--dry-run")} to apply.`);
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.error(`${paintErr.bad("Unknown action:")} ${action}`);
|
|
227
|
+
console.error(`Usage: ${paintErr.cmd(USAGE)}`);
|
|
228
|
+
console.error(`The config lives at ${paintErr.path(CONFIG)} (${paintErr.dim(`$KHB_HOME is ${KHB_HOME}`)})`);
|
|
229
|
+
process.exit(1);
|