@grimoire-rs/indexer 0.4.4 → 0.5.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/NOTICE +30 -0
  3. package/README.md +76 -331
  4. package/dist/cli/init.d.ts.map +1 -1
  5. package/dist/cli/init.js +35 -4
  6. package/dist/cli/init.js.map +1 -1
  7. package/dist/config.d.ts +107 -7
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +181 -36
  10. package/dist/config.js.map +1 -1
  11. package/dist/renderer/astro/components/CardLogo.d.ts +5 -0
  12. package/dist/renderer/astro/components/CardLogo.js +58 -0
  13. package/dist/renderer/astro/components/CardLogo.tsx +96 -0
  14. package/dist/renderer/astro/components/Catalog.d.ts +14 -1
  15. package/dist/renderer/astro/components/Catalog.js +467 -108
  16. package/dist/renderer/astro/components/Catalog.tsx +756 -349
  17. package/dist/renderer/astro/components/CommandBar.astro +66 -0
  18. package/dist/renderer/astro/components/CopyButton.d.ts +7 -0
  19. package/dist/renderer/astro/components/CopyButton.js +28 -0
  20. package/dist/renderer/astro/components/CopyButton.tsx +56 -0
  21. package/dist/renderer/astro/components/KindMark.d.ts +69 -0
  22. package/dist/renderer/astro/components/KindMark.js +66 -0
  23. package/dist/renderer/astro/components/KindMark.tsx +141 -0
  24. package/dist/renderer/astro/components/PackageCard.d.ts +18 -0
  25. package/dist/renderer/astro/components/PackageCard.js +50 -0
  26. package/dist/renderer/astro/components/PackageCard.tsx +273 -0
  27. package/dist/renderer/astro/components/PackageRow.d.ts +10 -0
  28. package/dist/renderer/astro/components/PackageRow.js +32 -0
  29. package/dist/renderer/astro/components/PackageRow.tsx +126 -0
  30. package/dist/renderer/astro/components/PickerMenu.astro +5 -14
  31. package/dist/renderer/astro/components/SiteFooter.astro +64 -0
  32. package/dist/renderer/astro/components/SiteHeader.astro +74 -0
  33. package/dist/renderer/astro/components/VersionMenu.astro +2 -2
  34. package/dist/renderer/astro/layouts/Base.astro +860 -206
  35. package/dist/renderer/astro/lib/base.d.ts +25 -0
  36. package/dist/renderer/astro/lib/base.js +23 -0
  37. package/dist/renderer/astro/lib/base.ts +27 -0
  38. package/dist/renderer/astro/lib/catalog.d.ts +24 -0
  39. package/dist/renderer/astro/lib/catalog.js +36 -0
  40. package/dist/renderer/astro/lib/catalog.ts +37 -0
  41. package/dist/renderer/astro/lib/commands.d.ts +58 -0
  42. package/dist/renderer/astro/lib/commands.js +86 -0
  43. package/dist/renderer/astro/lib/commands.ts +117 -0
  44. package/dist/renderer/astro/lib/keywordRail.d.ts +44 -0
  45. package/dist/renderer/astro/lib/keywordRail.js +99 -0
  46. package/dist/renderer/astro/lib/keywordRail.ts +110 -0
  47. package/dist/renderer/astro/pages/index.astro +40 -87
  48. package/dist/renderer/astro/pages/p/[...slug].astro +340 -195
  49. package/dist/renderer/astro/styles/tokens.css +40 -5
  50. package/dist/renderer/index.d.ts +58 -0
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +547 -5
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/renderer/types.d.ts +9 -0
  55. package/dist/renderer/types.d.ts.map +1 -1
  56. package/package.json +9 -4
  57. package/templates/README.md +6 -0
  58. package/templates/gitignore +4 -1
  59. package/templates/theme/README.md +38 -0
  60. package/templates/tsconfig.json +47 -0
package/README.md CHANGED
@@ -4,39 +4,8 @@ CLI + Astro integration for running your own [Grimoire](https://github.com/grimo
4
4
  package index — a static site that lists the skills, rules, agents, mcp
5
5
  servers, and bundles available in one or more OCI registries.
6
6
 
7
- ## Subcommands
8
-
9
- - `grim-indexer init` — scaffold a new index repo: the `index/**` content
10
- tree, site config, a `package.json` that pins this package, and the CI
11
- its forge runs.
12
- - `grim-indexer dev` — serve the index locally, through the same renderer
13
- `build` uses. The review loop for an entry or a branding change.
14
- - `grim-indexer enrich` — refresh `enrich/**` from the registry: READMEs,
15
- changelogs, logos, versions, tag lists, and the curated annotations grim
16
- reports (`revision`, `authors`, `vendor`, `url`, `documentation`,
17
- `compatibility` and the repository's `support` channels). The only step
18
- that goes online, and the only one that needs `grim` on `PATH`.
19
- `--seed` restores the sidecars from `<site>/enrich.json` first, so a
20
- pipeline that commits nothing still only downloads what moved.
21
- - `grim-indexer build` — render `index/**` into a static site.
22
- - `grim-indexer validate` — CI gate for contribution PRs/MRs against an
23
- index repo.
24
- - `grim-indexer ci` — render the index repo's workflows from the `ci`
25
- block of its `index.config.json`; `--check` verifies the committed ones
26
- still match and exits 65 on drift.
27
-
28
- A scaffolded index owns its CI: the workflow files are committed in that
29
- repository and run `npm ci` against its own lockfile, so nothing is
30
- fetched from here at run time and the version that builds an index is the
31
- one that repo has locked. The generated `verify-ci` job re-renders and
32
- diffs on every push, which is what keeps a hand-edit from silently
33
- forking the pipeline.
34
-
35
- An index stores nothing but pointers — a ref and who owns it. Everything a
36
- reader looks at lives in the registry, so an index that never runs `enrich`
37
- renders a catalogue of names with *No README available* on every page. The
38
- scaffolded CI runs it before each build; set `"enrich": false` in the `ci`
39
- block for a pointers-only site.
7
+ **Documentation: <https://grimoire-rs.github.io/indexer/>** — source in
8
+ [`docs/`](./docs/index.md).
40
9
 
41
10
  ## Install
42
11
 
@@ -44,16 +13,14 @@ block for a pointers-only site.
44
13
  npm install --save-dev @grimoire-rs/indexer
45
14
  ```
46
15
 
47
- ## Usage
16
+ ## Quickstart
48
17
 
49
18
  ```sh
50
19
  npx @grimoire-rs/indexer init # scaffold; writes package.json + lockfile
51
20
  ```
52
21
 
53
22
  Or start from [`grimoire-rs/index-template`](https://github.com/grimoire-rs/index-template):
54
- "Use this template", clone, then `npm install && npm run setup` — which is
55
- `init` run in place, so it reads your `origin` remote and already knows the
56
- forge and the Pages URL.
23
+ "Use this template", clone, then `npm install && npm run setup`.
57
24
 
58
25
  Everything after that runs through the scaffolded repo's own scripts, so it
59
26
  uses the version that repo locked:
@@ -63,317 +30,91 @@ npm run dev # local preview
63
30
  npm run build # index/** -> dist/
64
31
  npm run enrich # needs `grim` on PATH
65
32
  npm run validate # the contribution gate
66
- npm run ci # re-render CI after editing index.config.json
67
- npm run ci:check # fail on drift (what the verify-ci job runs)
33
+ npm run ci:check # fail on CI drift
68
34
  ```
69
35
 
70
- As an Astro integration:
36
+ Full walkthrough: [Quickstart](./docs/how-to/quickstart.md).
71
37
 
72
- ```js
73
- // astro.config.mjs
74
- import { defineConfig } from "astro/config";
75
- import grimoireIndexer from "@grimoire-rs/indexer/integration";
38
+ ## Subcommands
76
39
 
77
- export default defineConfig({
78
- integrations: [grimoireIndexer()],
79
- });
80
- ```
40
+ `init` · `dev` · `enrich` · `build` · `validate` · `ci` · `ratings` — each one
41
+ and its flags in the [CLI reference](./docs/reference/cli.md).
81
42
 
82
- ## The `stats.json` sidecar
83
-
84
- An index may publish `stats.json` beside `all.json`. It carries per-artifact
85
- signals that are not part of a package's own metadata — today two: `rating`,
86
- the upvote count on the forge thread that owns that artifact, and `updated`,
87
- when that artifact last moved. It is the read contract every client shares:
88
- `grim`, this renderer, and the VS Code extension all read the same file, and
89
- none of them writes it.
90
-
91
- ```json
92
- {
93
- "schema_version": 1,
94
- "generated_at": "2026-08-18T09:30:00Z",
95
- "providers": { "rating": "github", "updated": "indexer" },
96
- "entries": {
97
- "ghcr.io/acme/code-review": {
98
- "rating": {
99
- "up": 12,
100
- "target": "DIC_kwDOAbc123",
101
- "url": "https://github.com/acme/index/discussions/42"
102
- },
103
- "updated": { "at": "2026-07-01T10:00:00+00:00" }
104
- }
105
- }
106
- }
107
- ```
43
+ An index stores nothing but pointers — a ref and who owns it. Everything a
44
+ reader looks at lives in the registry, so an index that never runs `enrich`
45
+ renders a catalogue of names. See
46
+ [Index vs. site](./docs/explanation/index-vs-site.md).
108
47
 
109
- | Key | Type | Meaning |
110
- |---|---|---|
111
- | `schema_version` | int | Monotonic. Currently `1`. |
112
- | `generated_at` | string | RFC 3339, UTC. |
113
- | `providers` | object | Which backend produced each signal, keyed by stat name. `providers.rating` is `"github"` or `"gitlab"`. |
114
- | `entries` | object | Keyed by artifact ref, **exactly as that ref appears in `all.json`**. |
115
- | `entries[ref]` | object | One key per signal. |
116
- | `entries[ref].rating.up` | int | Upvotes, `0` included. A thread that exists but has no votes is published as `0`, so its `url` is there to vote at. |
117
- | `entries[ref].rating.target` | string | The forge's own id for the thread. **Opaque.** |
118
- | `entries[ref].rating.url` | string | Where a human goes to vote. **Opaque.** |
119
- | `entries[ref].updated.at` | string | RFC 3339. When the artifact last moved. |
120
-
121
- `target` and `url` are opaque: no client parses one and no client constructs
122
- one. They differ per forge and may change shape without a `schema_version`
123
- bump, which is exactly what "opaque" buys.
124
-
125
- **`entries[ref]` is a bag of stats, not a record.** A ref may carry `updated`
126
- and no `rating`, or the reverse. A further signal arrives as a sibling key
127
- with a sibling entry in `providers`; that is additive and needs no version
128
- bump.
129
-
130
- Some fixtures under `test/ratings/fixtures/` carry a `downloads` key. It is
131
- there as an *unknown* key — the thing a reader must carry forward without
132
- understanding — and is **not a specification**. No producer writes it, its
133
- shape is not fixed, and a client must not code against it.
134
-
135
- ### Absent is first-class
136
-
137
- Five distinct levels of absence. None of them is an error, a warning above
138
- `debug`, or a failed build:
139
-
140
- | Absent | Means |
141
- |---|---|
142
- | The file (404) | This index publishes no stats |
143
- | `entries` | Nothing is rated yet |
144
- | A ref within `entries` | That artifact has no stats at all |
145
- | `rating` on a ref that is present | No rating thread exists for it — not the same as a thread with `up: 0`. Any other stat on that ref is unaffected |
146
- | `rating` on a rendered catalog entry | Unrated. No consumer may assume the field is there |
147
-
148
- ### Reading a document you do not fully understand
149
-
150
- A client that understands version *N* accepts any document declaring `≤ N`,
151
- ignoring fields it does not know. A document declaring `> N` may degrade to
152
- "no rating", but must never be a parse error. So fields are added and never
153
- repurposed, and `schema_version` rises only when an existing field changes
154
- meaning.
155
-
156
- `test/ratings/fixtures/` holds one document per rule above — the minimal valid
157
- v1, unknown fields at two levels, an unrecognised `providers.rating` value, the
158
- absence levels, and a document from the future. They are the reference
159
- documents for every client's parser tests, in this repository and outside it.
160
-
161
- ### Producing it
162
-
163
- `updated` needs no configuration and no forge. `enrich` already runs `describe`
164
- per package, so it writes the date into the sidecar — the artifact's own
165
- `created` (a commit date, so a re-publish of the same commit keeps the same
166
- answer) or, for an artifact published outside a repository, the first build
167
- that saw its current digest. `build` joins that onto the document it
168
- publishes. An index that runs `enrich` gets it whether or not it wants
169
- ratings.
170
-
171
- `rating` is opt-in. Add a `ratings` block to `index.config.json` and re-render
172
- CI (`npm run ci`). The block is optional; without it nothing is tallied.
173
-
174
- ```jsonc
175
- "ratings": {
176
- "provider": "github", // "github" | "gitlab"
177
- "container": "Ratings", // GitHub: Discussions category. GitLab: work item type.
178
- "createBudget": 400, // threads created per run; default 400
179
- "lockThreads": false // default FALSE - a locked thread cannot be voted on
180
- }
181
- ```
48
+ ## Making it yours
182
49
 
183
- `provider` and `container` are required; the other two have the defaults shown.
184
- Unknown keys are ignored. There is deliberately **no `botIds` key** — the
185
- author allowlist is `index-policy.json`'s `trustedBots[].id`, and a second copy
186
- of the same ids in a second file is a consistency hazard rather than a
187
- convenience.
188
-
189
- `lockThreads` defaults to `false`, and did not always. A lock looks like the
190
- low-moderation default — a rating signal without a comment forum to moderate —
191
- but on GitLab it also stops the voting. The work-item UI draws the thumbs-up
192
- control on a locked item and ignores the click, guarding on `discussionLocked`
193
- without sending the mutation, while REST and GraphQL both accept a reaction on
194
- that same item and read it back. So nothing warns you: the tally runs, the
195
- threads look healthy, and every one of them is unvotable by the only means most
196
- people have.
197
-
198
- Setting it costs nothing in marker authority — R-1 reads the thread body and
199
- never a comment, so a reply cannot forge a marker either way. Turn it on if you
200
- would rather moderate nothing and have checked that your forge still lets a
201
- human react; GitHub Discussions are untested here.
202
-
203
- Re-rendering with the block present adds one job to the generated pipeline
204
- (`ratings` on GitHub, `grim-indexer:ratings` on GitLab), an hourly schedule, and
205
- a seed step in the deploy. The seed step is what keeps a failed tally from
206
- emptying a published rating set: it reads the currently published `stats.json`
207
- and carries it forward per stat key, and it fails the job rather than treating
208
- an unreadable seed as an empty one.
209
-
210
- ### Turning it off
211
-
212
- Two steps, and the first alone is not enough:
213
-
214
- 1. Remove the `ratings` block and re-render CI. That stops the tally.
215
- 2. **Delete the published `stats.json` from the deploy.** Until it is gone the
216
- last tally keeps being served, frozen, forever.
217
-
218
- After both, clients read a 404 and every artifact shows as unrated on its next
219
- refresh — unless the index still runs `enrich`, in which case the next build
220
- republishes a `stats.json` carrying `updated` and nothing else. The sidecar is
221
- never committed — it is a build input the deploy publishes — so there is no
222
- history to unwind either way.
223
-
224
- ## The enrichment checkpoint
225
-
226
- `enrich` skips work by digest, and every one of those comparisons reads
227
- `enrich/<namespace>/<name>/data.json` off disk. The scaffolded CI never has
228
- that file — the sidecars live only in the deploy job's workspace and are
229
- committed nowhere — so without help every deploy re-downloads every README,
230
- changelog, logo and payload, and re-dates every artifact that carries no
231
- `created` of its own.
232
-
233
- So `build` publishes `enrich.json` beside `all.json`, and
234
- `grim-indexer enrich --seed` reads it back from `<site>/enrich.json` before
235
- refreshing. The live site is the checkpoint, the same arrangement the ratings
236
- sidecar already uses. The generated CI passes `--seed`; re-render with
237
- `npm run ci` to pick it up.
238
-
239
- Unlike `stats.json`, **this is not a read contract.** Nothing outside this
240
- package reads it, its shape may change without notice, and no client should
241
- code against it.
242
-
243
- Failure is never fatal: an unreachable, oversized, unparseable or
244
- unrecognised checkpoint warns and seeds nothing, and the run does the full
245
- download it would have done anyway. A checkpoint that disagrees with itself —
246
- claiming a README it does not carry — has the digest that guards that file
247
- dropped, so the next run fetches it rather than trusting a stale flag.
248
-
249
- `describe` still runs once per package and is never skipped, so CI still
250
- installs `grim` and still makes one round trip each. The checkpoint saves the
251
- downloads, not the probe.
50
+ - **Branding, colour, shape** `index.config.json` plus one CSS file:
51
+ [Brand the site](./docs/how-to/customize-branding.md).
52
+ - **Your own pages** a setup guide, a publish guide, anything else, in
53
+ `theme/pages/`: [Add your own pages](./docs/how-to/add-pages.md).
54
+ - **Your own components** — replace the header, a card, a row:
55
+ [Theme overlay](./docs/reference/theme-overlay.md).
252
56
 
253
57
  ## Status
254
58
 
255
- Pre-1.0. The end-to-end loop was proven against live GitHub repositories
256
- on 2026-07-28: `init` push Pages `grim publish --announce` PR
257
- gate auto-merge → Pages → `grim config registry add` `grim search`
258
- `grim add`. The gate accepted a genuine pointer and refused all five
259
- hostile variants (author not the namespace owner, path outside the
260
- pointer layout, registry host outside the committed allowlist,
261
- unreachable OCI ref, unowned namespace).
262
-
263
- Two things that trial settled, both worth knowing before you scaffold:
264
-
265
- - **Require the check named `validate`.** It was `validate / validate`
266
- while the scaffold emitted thin callers of reusable workflows; the
267
- workflow is now committed in the index repo, so the context is just the
268
- job key. Requiring a context that never reports blocks every PR forever
269
- and looks exactly like the gate rejecting your contribution.
270
- - **In the combined (`--with-skills`) layout the gate does not cover your
271
- own CI's announce.** GitHub runs no workflows on a PR opened with
272
- `secrets.GITHUB_TOKEN`, so that PR arrives ungated — review it by hand.
273
- It also needs "Allow GitHub Actions to create and approve pull
274
- requests" enabled, which is off by default and which also lets
275
- workflows approve PRs.
276
-
277
- Not yet proven live: the GitLab leg (hermetic unit tests only - no live
278
- GitLab pipeline has run the rendered CI), and the cross-repository
279
- announce, which needs a credential beyond the CI token.
280
-
281
- Two things are frozen and safe to build on: the published URL layout
282
- (`/p/<namespace>/<name>/` and `/all.json`) and the per-record `schema`
283
- field. Everything else may still move — including `/enrich.json`, which is
284
- this package's own checkpoint and not a read contract.
285
-
286
- Theming is CSS custom properties, all named `--grim-<category>-<role>` and
287
- all declared in one file — `src/renderer/astro/styles/tokens.css`, whose
288
- contract comment is the only reference. There is no second table here to go
289
- stale.
290
-
291
- | Family | Tokens |
292
- |---|---|
293
- | Colour | `--grim-color-*` — surface, accent, package kind, state |
294
- | Space | `--grim-space-1` … `-9`, a sparse scale whose steps grow apart |
295
- | Type | `--grim-text-2xs` … `-2xl` |
296
- | Radius | `--grim-radius-sm` … `-xl`, `-pill` |
297
- | Border, motion, elevation | `--grim-border-width`, `--grim-duration-*`, `--grim-shadow-*` |
298
-
299
- **Colour is the only family that differs per scheme**, so it is the only one
300
- you override twice — under `:root` *and* `[data-theme="dark"]`. `:root` is
301
- scheme-agnostic, so a `:root`-only colour override silently takes dark mode
302
- with it. Everything else is a measurement: declare it once.
303
-
304
- Everything the renderer ships sits in `@layer grimoire`, and your file is
305
- emitted unlayered and last. Unlayered CSS beats a layered rule outright, at
306
- any specificity, so an ordinary selector wins with no `!important` and no
307
- knowledge of where Astro injected its bundle.
308
-
309
- To target one element rather than retheme globally, use its `data-slot`:
310
-
311
- ```css
312
- [data-slot="package-card"] { border-radius: 2px; }
313
- ```
59
+ Pre-1.0. The end-to-end loop was proven against live GitHub repositories on
60
+ 2026-07-28. Two things are frozen and safe to build on: the published URL
61
+ layout (`/p/<namespace>/<name>/` and `/all.json`) and the per-record `schema`
62
+ field.
314
63
 
315
- The slots are `brand`, `catalog`, `catalog-search`, `catalog-toolbar`,
316
- `deprecated-banner`, `detail-body`, `detail-header`, `detail-rail`,
317
- `filter-chip`, `install-command`, `package-card`, `package-keywords`,
318
- `package-kind`, `package-meta`, `package-name`, `site-footer`, `site-header`
319
- and `version-pill`. Those names are stable; **class names are not** — they
320
- are internal and unversioned, so `@layer` will make a rule targeting one
321
- win, but nothing promises the class is still there next release.
322
-
323
- There is deliberately no component-override API (`--grim-card-radius` and
324
- friends). With the layer and the slots, it would reach nothing the CSS above
325
- cannot already reach, and it would freeze a per-slot prop contract that is
326
- not worth promising this early.
327
-
328
- > **If you wrote a `theme.css` against `0.4.0` or earlier**, it no longer
329
- > applies — silently, without an error. The tokens were unnamespaced
330
- > (`--accent`, `--bg`, `--fg`), which collides with any other stylesheet on
331
- > the page using those names. They are now `--grim-color-accent`,
332
- > `--grim-color-bg`, `--grim-color-fg` and so on: the same role names behind
333
- > a `--grim-color-` prefix.
334
-
335
- > **Use `0.1.4` or later.** `0.1.0` installs without an executable - npm
336
- > silently stripped its `bin` entry at publish time. `0.1.1` and `0.1.2`
337
- > scaffold CI that points at reusable workflows those tags do not contain,
338
- > so the first push to a scaffolded index fails before any job runs.
339
- >
340
- > An index already scaffolded against a reusable-workflow version keeps
341
- > working as long as its pinned `uses:`/`include:` refs stay on an existing
342
- > tag - old tags are not deleted, so that resolution does not break on its
343
- > own. It breaks the moment something bumps the pinned ref, because `main`
344
- > no longer defines any reusable workflow or remote include for it to
345
- > resolve to: a Renovate update of an `@grimoire-rs/indexer` action ref
346
- > will now fail hard. Fix it before that happens by adding a `package.json`
347
- > pinning this package (if the old scaffold has none) and running
348
- > `npm run ci`, which re-renders the workflow files and nothing else.
349
- > **Not `init --force`**: that rewrites the whole scaffold from its
350
- > templates, discarding the added scripts in `package.json`, your
351
- > `.gitignore` rules, your `README.md`, and any `uses:` pin Renovate has
352
- > bumped since - `npm run ci` keeps all four. `publish.toml` and
353
- > `index-policy.json` are never rewritten, by `--force` or anything else.
354
- >
355
- > Through `0.1.3`, `init --with-skills` wrote a `publish.toml` with no
356
- > `[announce]` table, so `grim publish --announce` in a combined-layout
357
- > repo proposed its packages into the **public** first-party index rather
358
- > than the one beside them. If you scaffolded that layout on `0.1.3` or
359
- > earlier, add an `[announce]` table naming your own repository before
360
- > announcing.
64
+ What is not yet proven, and what is known to be awkward:
65
+ [Known limitations](./docs/ops/known-limitations.md). What moved in each
66
+ release including the component paths and props the theme overlay's Unstable
67
+ tier does not freeze — is [`CHANGELOG.md`](./CHANGELOG.md), which ships inside
68
+ the npm package as well.
361
69
 
362
70
  ## Developing the renderer
363
71
 
72
+ The toolchain — `task`, `node`, `grim` — is pinned in `ocx.toml`. Once:
73
+
74
+ ```sh
75
+ ocx shell allow # per-prompt activation for this project, once
76
+ task install
77
+ ```
78
+
79
+ Node is deliberately not in `ocx.toml`'s default `[tools]` table. `engines`
80
+ claims node 22 *and* 24, so both are groups and every invocation names one:
81
+ `ocx run -g default,node22 -- task check` is the other half of what CI runs.
82
+
364
83
  Changing how the site *looks* needs a way to see it that does not cost an
365
- npm release. `npm run dev` serves the catalog with hot reload:
84
+ npm release. `task dev` serves the catalog with hot reload:
366
85
 
367
86
  ```sh
368
- npm run dev # the bundled test fixture
369
- npm run dev -- --port 4400
370
- npm run dev -- --root /path/to/an/index # your own index, or a checkout
87
+ task dev # the bundled dev index
88
+ task dev -- --port 4400
89
+ task dev FIXTURE=/path/to/an/index # your own index, or a checkout
371
90
  # of github.com/grimoire-rs/index
372
- npm run dev -- --config ./variant.json # try an index.config.json without
91
+ task dev -- --config ./variant.json # try an index.config.json without
373
92
  # editing the index it renders
374
- npm run dev -- --help
93
+ task dev -- --help
375
94
  ```
376
95
 
96
+ `task --list` has the rest; `task check` is the whole gate CI runs.
97
+
98
+ The docs site is a separate toolchain and deliberately not part of `check` —
99
+ `check` must stay runnable with no Python present. `task docs:serve` previews
100
+ it, `task docs:build` is the strict build the `pages` workflow gates PRs with.
101
+
102
+ ### The dev index
103
+
104
+ `test/fixtures/dev/` is what `task dev` renders, and it holds **one artifact
105
+ per rendering state** — rated, zero-vote and unrated; deprecated with and
106
+ without a replacement; a logo present, absent, and declared-but-not-shipped;
107
+ every kind; enriched and pointer-only; a second namespace on a second forge.
108
+ Its `README.md` is the table of which artifact exists for which state.
109
+
110
+ It ships an `index/` tree, so `task dev` runs the real `compileIndex` — which
111
+ is the only way the three logo states differ, since publishing
112
+ `enrich/<ns>/<name>/logo.<ext>` is something only the compile path does.
113
+
114
+ Add a state by adding an artifact and a row in that table. It is not a test
115
+ fixture: `test/renderer/fixture/` is, and `test/renderer/build.test.ts`
116
+ counts its contents, so the two are kept apart on purpose.
117
+
377
118
  Every part of the hero is config, so `--config` is how you review the site
378
119
  with a piece switched off — `{"install": []}` drops the installer buttons,
379
120
  `{"registry": null}` the add-this-index ones, `{"vscodeExtension": null}`
@@ -383,13 +124,14 @@ It renders through the same `inlineConfig` as `grim-indexer build`, so the
383
124
  preview is the release output, not an approximation. Edits under
384
125
  `src/renderer/astro/` (templates, components, the tokens in
385
126
  `styles/tokens.css`) reload in place; changing the renderer's own
386
- TypeScript needs a restart, because `npm run dev` builds `dist/` on start.
127
+ TypeScript needs a restart, because `task dev` builds `dist/` on start.
387
128
 
388
129
  The scratch index root lives in the gitignored `.dev/`, rebuilt on every
389
130
  run — the repo you point `--root` at is copied, never rendered in place.
390
131
 
391
- `npm run dev:smoke` boots the server, asserts the landing and detail pages
392
- render, and checks the staged directory is cleaned up on shutdown. That
132
+ `task smoke` (`npm run dev:smoke`) boots the server, asserts the landing and
133
+ detail pages render, and checks the staged directory is cleaned up on
134
+ shutdown. It is part of `task check`, so CI runs it too. That
393
135
  check lives here rather than in the vitest suite because Astro's dev server
394
136
  does not route correctly when nested inside vitest's own Vite; the build
395
137
  path is covered by `test/renderer/build.test.ts`.
@@ -399,4 +141,7 @@ install the resulting tarball into a scratch index repo.
399
141
 
400
142
  ## License
401
143
 
402
- Apache-2.0
144
+ Apache-2.0. See `NOTICE` for the third-party assets a built index carries:
145
+ the artifact-kind marks are Microsoft's codicons under CC BY 4.0 — the same
146
+ glyphs the VS Code extension uses, so one catalogue reads the same in both —
147
+ alongside Lucide (ISC) and Material Design Icons (Apache-2.0).
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAYA,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAoE1D,YAAY,EAAE,KAAK,EAAE,CAAC;AAEtB,gGAAgG;AAChG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAE5F,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACrD,cAAc,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;CACpB;AA23BD,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA+G5F"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAYA,OAAO,EAQL,KAAK,KAAK,EACV,KAAK,cAAc,EACpB,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAoE1D,YAAY,EAAE,KAAK,EAAE,CAAC;AAEtB,gGAAgG;AAChG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,CAAC;AAE5F,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACrD,cAAc,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;CACpB;AAy5BD,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA+G5F"}
package/dist/cli/init.js CHANGED
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import * as prompts from "@clack/prompts";
10
10
  import { FORGES, loadCiConfig, PUBLISH_TRIGGERS, renderCi, resolveCi, staleCi, } from "../ci.js";
11
- import { CONFIG_FILE, validateSite } from "../config.js";
11
+ import { CONFIG_FILE, validateSite, validateUrlShape } from "../config.js";
12
12
  import { fromTemplate } from "../templates.js";
13
13
  import { CliError, EXIT } from "./exit.js";
14
14
  const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
@@ -137,11 +137,31 @@ function badName(value) {
137
137
  }
138
138
  return null;
139
139
  }
140
- /** What `SiteConfig.logo` accepts — checked here so a typo fails now, not at build time. */
140
+ /**
141
+ * What `SiteConfig.logo` accepts — borrowed from `config.ts` for the reason
142
+ * `badSiteUrl` gives above, and checked here so a typo fails now rather than
143
+ * at build time.
144
+ *
145
+ * The copy this replaces read `/^(\/|https?:\/\/)/i`, which accepted
146
+ * `//evil.test/x.svg`, `/\evil.test/x.svg`, a tab-smuggled `/\t/evil.test/x`
147
+ * and `https://good.test@evil.test/x` — every one of them off-origin. The
148
+ * owner authors `index.config.json`, so this is a typo guard rather than a
149
+ * trust boundary; the reason to share one guard is that three copies of the
150
+ * rule had stopped agreeing about what a site-root path is, and `config.ts`
151
+ * documents each shape it refuses.
152
+ */
141
153
  function badLogo(value) {
142
- if (value === "" || /^(\/|https?:\/\/)/i.test(value))
154
+ // Blank is "no logo": the answer is omitted from `index.config.json`
155
+ // entirely, so the shared guard never sees an empty string.
156
+ if (value === "")
157
+ return null;
158
+ try {
159
+ validateUrlShape(value, "logo", true);
143
160
  return null;
144
- return "must be a site-root path (/logo.svg) or an http(s) URL";
161
+ }
162
+ catch (err) {
163
+ return err.message;
164
+ }
145
165
  }
146
166
  /**
147
167
  * The target dir's `origin` remote as an https URL — set when the repo was
@@ -710,11 +730,22 @@ function plan(answers, version, ci) {
710
730
  const from = (rel, dest) => ({ path: dest, content: fromTemplate(rel, vars) });
711
731
  const files = [
712
732
  { path: "index/.gitkeep", content: "" },
733
+ // The overlay has to be scaffolded or nobody finds the feature, and this
734
+ // is the only thing that says where a page goes. A README rather than the
735
+ // `.gitkeep` it replaces, because what someone needs before they overwrite
736
+ // a component — which parts of the overlay are promised — is not
737
+ // discoverable from a directory listing, and stating it only in the
738
+ // tsconfig comment put it where nobody reads it in time. Both files
739
+ // reserve `theme/` for git equally well; only one of them answers that.
740
+ from("theme/README.md", "theme/README.md"),
713
741
  { path: "index.config.json", content: siteConfig(answers) },
714
742
  { path: POLICY_FILE, content: indexPolicy(answers) },
715
743
  from("gitignore", ".gitignore"),
716
744
  from("gitattributes", ".gitattributes"),
717
745
  from("package.json", "package.json"),
746
+ // Editor-only: it is what makes `@grim/*` resolve while a theme page is
747
+ // being written. The build never reads it.
748
+ from("tsconfig.json", "tsconfig.json"),
718
749
  from("README.md", "README.md"),
719
750
  ];
720
751
  // The same renderer `grim-indexer ci` uses, driven by the `ci` block that