@grimoire-rs/indexer 0.2.2 → 0.3.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 (59) hide show
  1. package/README.md +51 -18
  2. package/dist/ci.d.ts +115 -0
  3. package/dist/ci.d.ts.map +1 -0
  4. package/dist/ci.js +286 -0
  5. package/dist/ci.js.map +1 -0
  6. package/dist/cli/build.d.ts.map +1 -1
  7. package/dist/cli/build.js +3 -9
  8. package/dist/cli/build.js.map +1 -1
  9. package/dist/cli/ci.d.ts +6 -0
  10. package/dist/cli/ci.d.ts.map +1 -0
  11. package/dist/cli/ci.js +43 -0
  12. package/dist/cli/ci.js.map +1 -0
  13. package/dist/cli/dev.d.ts +7 -0
  14. package/dist/cli/dev.d.ts.map +1 -0
  15. package/dist/cli/dev.js +57 -0
  16. package/dist/cli/dev.js.map +1 -0
  17. package/dist/cli/init.d.ts +6 -1
  18. package/dist/cli/init.d.ts.map +1 -1
  19. package/dist/cli/init.js +294 -74
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/main.d.ts.map +1 -1
  22. package/dist/cli/main.js +61 -8
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/cli/out_dir.d.ts +11 -0
  25. package/dist/cli/out_dir.d.ts.map +1 -0
  26. package/dist/cli/out_dir.js +30 -0
  27. package/dist/cli/out_dir.js.map +1 -0
  28. package/dist/cli/validate.d.ts +1 -0
  29. package/dist/cli/validate.d.ts.map +1 -1
  30. package/dist/cli/validate.js +47 -1
  31. package/dist/cli/validate.js.map +1 -1
  32. package/dist/templates.d.ts +12 -0
  33. package/dist/templates.d.ts.map +1 -0
  34. package/dist/templates.js +40 -0
  35. package/dist/templates.js.map +1 -0
  36. package/dist/validate/adapters/files.d.ts.map +1 -1
  37. package/dist/validate/adapters/files.js +13 -3
  38. package/dist/validate/adapters/files.js.map +1 -1
  39. package/dist/validate/index.js +17 -2
  40. package/dist/validate/index.js.map +1 -1
  41. package/package.json +1 -1
  42. package/templates/README.md +115 -19
  43. package/templates/ci/github-automerge.yml +36 -0
  44. package/templates/ci/github-enrich.yml +27 -0
  45. package/templates/ci/github-pages.yml +64 -0
  46. package/templates/ci/github-validate.yml +157 -0
  47. package/templates/ci/github-verify-ci.yml +47 -0
  48. package/templates/ci/gitlab-ci.yml +148 -0
  49. package/templates/ci/gitlab-enrich.sh +26 -0
  50. package/templates/ci/gitlab-verify.yml +16 -0
  51. package/templates/ci/header.txt +19 -0
  52. package/templates/gitattributes +4 -0
  53. package/templates/gitignore +7 -2
  54. package/templates/package.json +18 -0
  55. package/templates/publish.toml +2 -2
  56. package/templates/github-pages.yml +0 -24
  57. package/templates/github-validate.yml +0 -51
  58. package/templates/gitlab-ci.yml +0 -13
  59. package/templates/reusable/gitlab-index-ci.yml +0 -125
package/README.md CHANGED
@@ -6,20 +6,33 @@ servers, and bundles available in one or more OCI registries.
6
6
 
7
7
  ## Subcommands
8
8
 
9
- - `grim-indexer init` — scaffold a new index repo (`index/**` content
10
- tree, Astro site config, CI workflow).
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.
11
14
  - `grim-indexer enrich` — refresh `enrich/**` from the registry: READMEs,
12
15
  changelogs, logos, versions and tag lists. The only step that goes
13
16
  online, and the only one that needs `grim` on `PATH`.
14
17
  - `grim-indexer build` — render `index/**` into a static site.
15
18
  - `grim-indexer validate` — CI gate for contribution PRs/MRs against an
16
19
  index repo.
20
+ - `grim-indexer ci` — render the index repo's workflows from the `ci`
21
+ block of its `index.config.json`; `--check` verifies the committed ones
22
+ still match and exits 65 on drift.
23
+
24
+ A scaffolded index owns its CI: the workflow files are committed in that
25
+ repository and run `npm ci` against its own lockfile, so nothing is
26
+ fetched from here at run time and the version that builds an index is the
27
+ one that repo has locked. The generated `verify-ci` job re-renders and
28
+ diffs on every push, which is what keeps a hand-edit from silently
29
+ forking the pipeline.
17
30
 
18
31
  An index stores nothing but pointers — a ref and who owns it. Everything a
19
32
  reader looks at lives in the registry, so an index that never runs `enrich`
20
33
  renders a catalogue of names with *No README available* on every page. The
21
- scaffolded CI runs it before each build; set `enrich: false` (GitHub) or
22
- `GRIM_INDEXER_ENRICH: "false"` (GitLab) for a pointers-only site.
34
+ scaffolded CI runs it before each build; set `"enrich": false` in the `ci`
35
+ block for a pointers-only site.
23
36
 
24
37
  ## Install
25
38
 
@@ -30,10 +43,19 @@ npm install --save-dev @grimoire-rs/indexer
30
43
  ## Usage
31
44
 
32
45
  ```sh
33
- npx @grimoire-rs/indexer init
34
- npx @grimoire-rs/indexer enrich # needs `grim` on PATH
35
- npx @grimoire-rs/indexer build
36
- npx @grimoire-rs/indexer validate
46
+ npx @grimoire-rs/indexer init # scaffold; writes package.json + lockfile
47
+ ```
48
+
49
+ Everything after that runs through the scaffolded repo's own scripts, so it
50
+ uses the version that repo locked:
51
+
52
+ ```sh
53
+ npm run dev # local preview
54
+ npm run build # index/** -> dist/
55
+ npm run enrich # needs `grim` on PATH
56
+ npm run validate # the contribution gate
57
+ npm run ci # re-render CI after editing index.config.json
58
+ npm run ci:check # fail on drift (what the verify-ci job runs)
37
59
  ```
38
60
 
39
61
  As an Astro integration:
@@ -60,9 +82,11 @@ unreachable OCI ref, unowned namespace).
60
82
 
61
83
  Two things that trial settled, both worth knowing before you scaffold:
62
84
 
63
- - **Requiring the gate needs the check named `validate / validate`**, not
64
- `validate`. Requiring a context that never reports blocks every PR
65
- forever and looks exactly like the gate rejecting your contribution.
85
+ - **Require the check named `validate`.** It was `validate / validate`
86
+ while the scaffold emitted thin callers of reusable workflows; the
87
+ workflow is now committed in the index repo, so the context is just the
88
+ job key. Requiring a context that never reports blocks every PR forever
89
+ and looks exactly like the gate rejecting your contribution.
66
90
  - **In the combined (`--with-skills`) layout the gate does not cover your
67
91
  own CI's announce.** GitHub runs no workflows on a PR opened with
68
92
  `secrets.GITHUB_TOKEN`, so that PR arrives ungated — review it by hand.
@@ -70,8 +94,8 @@ Two things that trial settled, both worth knowing before you scaffold:
70
94
  requests" enabled, which is off by default and which also lets
71
95
  workflows approve PRs.
72
96
 
73
- Not yet proven live: the GitLab leg (hermetic tests only; the
74
- `include: remote:` URL is verified to resolve), and the cross-repository
97
+ Not yet proven live: the GitLab leg (hermetic unit tests only - no live
98
+ GitLab pipeline has run the rendered CI), and the cross-repository
75
99
  announce, which needs a credential beyond the CI token.
76
100
 
77
101
  Two things are frozen and safe to build on: the published URL layout
@@ -83,13 +107,22 @@ is deliberately no component-override API yet — publishing one would
83
107
  freeze a prop contract per slot, and that is not a promise worth making
84
108
  this early.
85
109
 
86
- > **Use `0.1.4` or later.** `0.1.0` installs without an executable npm
110
+ > **Use `0.1.4` or later.** `0.1.0` installs without an executable - npm
87
111
  > silently stripped its `bin` entry at publish time. `0.1.1` and `0.1.2`
88
112
  > scaffold CI that points at reusable workflows those tags do not contain,
89
- > so the first push to a scaffolded index fails before any job runs. An
90
- > index already scaffolded against `0.1.1`/`0.1.2` is fixed by bumping the
91
- > `@v0.1.x` refs in `.github/workflows/{pages,validate}.yml` no
92
- > re-scaffold needed.
113
+ > so the first push to a scaffolded index fails before any job runs.
114
+ >
115
+ > An index already scaffolded against a reusable-workflow version keeps
116
+ > working as long as its pinned `uses:`/`include:` refs stay on an existing
117
+ > tag - old tags are not deleted, so that resolution does not break on its
118
+ > own. It breaks the moment something bumps the pinned ref, because `main`
119
+ > no longer defines any reusable workflow or remote include for it to
120
+ > resolve to: a Renovate update of an `@grimoire-rs/indexer` action ref
121
+ > will now fail hard. Fix it before that happens by re-scaffolding the CI -
122
+ > `npx @grimoire-rs/indexer init . --force` and commit the result, or, if
123
+ > you would rather not touch anything else `init` writes, add a
124
+ > `package.json` pinning this package (if the old scaffold has none) and
125
+ > run `npm run ci` to re-render just the workflow files.
93
126
  >
94
127
  > Through `0.1.3`, `init --with-skills` wrote a `publish.toml` with no
95
128
  > `[announce]` table, so `grim publish --announce` in a combined-layout
package/dist/ci.d.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { CliError } from "./cli/exit.js";
2
+ /**
3
+ * Forges CI can be rendered for. One repository is hosted on one forge, so
4
+ * this is a choice rather than a set — rendering both left every index with a
5
+ * pipeline it would never run, and a second gate to keep honest for nothing.
6
+ */
7
+ export type Forge = "github" | "gitlab";
8
+ /**
9
+ * The `ci` block of `index.config.json` — everything that varies the
10
+ * generated pipeline. Every key is optional; [`resolveCi`] fills the gaps.
11
+ */
12
+ export interface CiConfig {
13
+ /** Which forge's CI to render. Default `github`. */
14
+ forge?: Forge;
15
+ /** Node.js the jobs run on — `setup-node` version, and the GitLab image tag. Default `22`. */
16
+ nodeVersion?: string;
17
+ /**
18
+ * Refresh READMEs, changelogs, logos and version lists from the registry
19
+ * before building. `false` renders the enrichment steps away entirely, for
20
+ * a pointers-only site whose build never leaves the runner. Default `true`.
21
+ */
22
+ enrich?: boolean;
23
+ /** grim release the enrichment step reads the registry with — `latest` or a tag. Default `latest`. */
24
+ grimVersion?: string;
25
+ /**
26
+ * Squash-merge a pull request the gate passed, then deploy. Default `false`:
27
+ * merging an untrusted contribution unattended is a policy this package will
28
+ * not assume on a repository's behalf.
29
+ *
30
+ * GitHub only, and deliberately. GitHub always runs the base branch's copy
31
+ * of a `pull_request_target` workflow, so the gate a pull request faces is
32
+ * one it cannot edit. GitLab has no equivalent — the merge request supplies
33
+ * its own `.gitlab-ci.yml`, and a fork's pipeline runs in the fork — so an
34
+ * auto-merge there would act on a verdict the contributor could write.
35
+ */
36
+ autoMerge?: boolean;
37
+ /**
38
+ * Own these files by hand. The `verify-ci` drift guard is then not emitted,
39
+ * `ci --check` is the only thing left that would notice an edit, and CI
40
+ * fixes stop arriving by re-render. Discouraged, and deliberately not the
41
+ * default — but it is the repository's CI, so it is the repository's call.
42
+ */
43
+ allowManualEdits?: boolean;
44
+ }
45
+ export type ResolvedCiConfig = Required<CiConfig>;
46
+ /** Fill every gap from the defaults documented on [`CiConfig`]. */
47
+ export declare function resolveCi(ci: CiConfig | undefined): ResolvedCiConfig;
48
+ /** Validate the `ci` block of an already-parsed config object. */
49
+ export declare function validateCi(raw: unknown): CiConfig;
50
+ /** Read the `ci` block out of `index.config.json`. A missing file means defaults. */
51
+ export declare function loadCiConfig(root: string): Promise<CiConfig>;
52
+ /**
53
+ * First line of every file this renderer writes. Stale detection keys on it,
54
+ * so a repository's hand-written workflows are never mistaken for ours.
55
+ *
56
+ * Deliberately carries no version stamp. The version that renders these files
57
+ * is the one in the repository's own `package-lock.json`; stamping it here too
58
+ * would be a second copy that goes stale, and every dependency bump would red
59
+ * the drift guard until somebody re-rendered a file whose content had not
60
+ * changed.
61
+ */
62
+ export declare const GENERATED_HEADER = "# Generated by @grimoire-rs/indexer";
63
+ /**
64
+ * Render the complete CI file set. Nothing touches disk — the caller writes
65
+ * it or diffs it against what is committed.
66
+ */
67
+ export declare function renderCi(ci: ResolvedCiConfig): Map<string, string>;
68
+ /**
69
+ * Canonicalize a generated file for drift comparison.
70
+ *
71
+ * The index repository owns the *pin* on every `uses:` action reference —
72
+ * its own Renovate or Dependabot will bump `uses: owner/action@<ref> # vX`,
73
+ * and the guard must not read that as a hand-edit. Only the `@<ref>` suffix
74
+ * and any trailing version comment are stripped; `owner/action` survives, so
75
+ * swapping in a different action still trips drift.
76
+ */
77
+ export declare function normalizeForDrift(content: string): string;
78
+ /**
79
+ * Files on disk that carry our header but that the current config no longer
80
+ * renders — a `forge` narrowed from `both`, or `allowManualEdits` turned on
81
+ * after a guard was already committed. Reported, never deleted: removing a
82
+ * file from somebody's repository is their call, not the renderer's.
83
+ */
84
+ export declare function staleCi(root: string, files: Map<string, string>): Promise<string[]>;
85
+ export interface CiOutcome {
86
+ path: string;
87
+ /** `written` on a render; `drift`/`stale`/`ok` on a check. */
88
+ status: "written" | "unchanged" | "ok" | "drift" | "stale";
89
+ }
90
+ /**
91
+ * Write the rendered set, overwriting whatever is there.
92
+ *
93
+ * "Whatever is there" is judged through [`normalizeForDrift`], the same lens
94
+ * the guard uses — not raw bytes. Otherwise the two disagreed: `ci --check`
95
+ * deliberately tolerates a bumped `uses: owner/action@<ref>`, while a plain
96
+ * `npm run ci` rewrote it back to the pin baked into this package, silently
97
+ * undoing every Renovate bump the repository is told it may make. The repo
98
+ * owns its pins; a render must not take them back.
99
+ */
100
+ export declare function writeCi(root: string, files: Map<string, string>): Promise<CiOutcome[]>;
101
+ /**
102
+ * Compare the rendered set against what is committed. Missing counts as
103
+ * drift; a generated file the config no longer renders counts as stale.
104
+ * Hints are path-only — the contents of a repository's CI never go to stderr.
105
+ */
106
+ export declare function checkCi(root: string, files: Map<string, string>): Promise<CiOutcome[]>;
107
+ /**
108
+ * The re-render command a drift message tells the reader to run. It is the
109
+ * repository's own npm script, so it runs the renderer the lock pins rather
110
+ * than whatever `npx` would fetch today.
111
+ */
112
+ export declare const RE_RENDER_COMMAND = "npm run ci";
113
+ /** Turn a failed check into the error the CLI exits on. */
114
+ export declare function driftError(outcomes: CiOutcome[]): CliError;
115
+ //# sourceMappingURL=ci.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../src/ci.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,QAAQ,EAAQ,MAAM,eAAe,CAAC;AAI/C;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAIxC;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,oDAAoD;IACpD,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sGAAsG;IACtG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAElD,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,EAAE,EAAE,QAAQ,GAAG,SAAS,GAAG,gBAAgB,CASpE;AAMD,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAiCjD;AAED,qFAAqF;AACrF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkBlE;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,wCAAwC,CAAC;AA2CtE;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuClE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBzF;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;CAC5D;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAc5F;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAW5F;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C,2DAA2D;AAC3D,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,QAAQ,CAO1D"}
package/dist/ci.js ADDED
@@ -0,0 +1,286 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ // The CI renderer: `index.config.json`'s `ci` block in, complete workflow
4
+ // files out. The index repository owns and commits what it runs — there is no
5
+ // reusable workflow to call, no remote include to fetch, and nothing about the
6
+ // pipeline that lives anywhere but in the repository being pushed.
7
+ //
8
+ // The trade that buys is the drift guard: because the files are generated,
9
+ // `grim-indexer ci --check` can re-render and diff, and the generated
10
+ // `verify-ci` job runs exactly that on every push and pull request. So a
11
+ // hand-edit is caught rather than silently forked, while the repository keeps
12
+ // full ownership of its own CI — set `allowManualEdits` and the guard is not
13
+ // emitted at all.
14
+ import fs from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { CliError, EXIT } from "./cli/exit.js";
17
+ import { CONFIG_FILE, SiteConfigError } from "./config.js";
18
+ import { fromTemplate } from "./templates.js";
19
+ const FORGES = ["github", "gitlab"];
20
+ /** Fill every gap from the defaults documented on [`CiConfig`]. */
21
+ export function resolveCi(ci) {
22
+ return {
23
+ forge: ci?.forge ?? "github",
24
+ nodeVersion: ci?.nodeVersion ?? "22",
25
+ enrich: ci?.enrich ?? true,
26
+ grimVersion: ci?.grimVersion ?? "latest",
27
+ allowManualEdits: ci?.allowManualEdits ?? false,
28
+ autoMerge: ci?.autoMerge ?? false,
29
+ };
30
+ }
31
+ function fail(msg) {
32
+ throw new SiteConfigError(`${CONFIG_FILE}: ci.${msg}`);
33
+ }
34
+ /** Validate the `ci` block of an already-parsed config object. */
35
+ export function validateCi(raw) {
36
+ if (raw === undefined || raw === null)
37
+ return {};
38
+ if (typeof raw !== "object" || Array.isArray(raw)) {
39
+ throw new SiteConfigError(`${CONFIG_FILE}: ci must be an object`);
40
+ }
41
+ const ci = raw;
42
+ if (ci.forge !== undefined && !FORGES.includes(ci.forge)) {
43
+ fail(`forge must be one of ${FORGES.join(", ")}`);
44
+ }
45
+ // These land inside a double-quoted shell word or a docker image tag in the
46
+ // generated YAML. The repository owner writes this file, so it is a paste
47
+ // guard rather than a trust boundary — but a value carrying a quote or a `$`
48
+ // would rewrite the command around it, and no version string needs either.
49
+ for (const key of ["nodeVersion", "grimVersion"]) {
50
+ const value = ci[key];
51
+ if (value !== undefined && typeof value !== "string")
52
+ fail(`${key} must be a string`);
53
+ if (typeof value === "string" && !/^[\w.@^~><=* +-]+$/.test(value)) {
54
+ fail(`${key} must be a plain version or range`);
55
+ }
56
+ }
57
+ for (const key of ["enrich", "allowManualEdits", "autoMerge"]) {
58
+ if (ci[key] !== undefined && typeof ci[key] !== "boolean")
59
+ fail(`${key} must be a boolean`);
60
+ }
61
+ // Refused rather than ignored. Rendering GitLab CI while the config asks for
62
+ // auto-merge would leave a repository believing contributions land by
63
+ // themselves, and the honest answer is that they cannot be made to safely
64
+ // there — see the field's doc comment.
65
+ if (ci.autoMerge === true && ci.forge === "gitlab") {
66
+ fail("autoMerge is github-only - the GitLab gate cannot be made tamper-proof");
67
+ }
68
+ return ci;
69
+ }
70
+ /** Read the `ci` block out of `index.config.json`. A missing file means defaults. */
71
+ export async function loadCiConfig(root) {
72
+ let text;
73
+ try {
74
+ text = await fs.readFile(path.join(root, CONFIG_FILE), "utf8");
75
+ }
76
+ catch (err) {
77
+ if (err.code === "ENOENT")
78
+ return {};
79
+ throw err;
80
+ }
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(text);
84
+ }
85
+ catch (err) {
86
+ throw new SiteConfigError(`${CONFIG_FILE}: ${err.message}`);
87
+ }
88
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
89
+ throw new SiteConfigError(`${CONFIG_FILE}: must contain a JSON object`);
90
+ }
91
+ return validateCi(parsed.ci);
92
+ }
93
+ // ── Rendering ────────────────────────────────────────────────────────────────
94
+ /**
95
+ * First line of every file this renderer writes. Stale detection keys on it,
96
+ * so a repository's hand-written workflows are never mistaken for ours.
97
+ *
98
+ * Deliberately carries no version stamp. The version that renders these files
99
+ * is the one in the repository's own `package-lock.json`; stamping it here too
100
+ * would be a second copy that goes stale, and every dependency bump would red
101
+ * the drift guard until somebody re-rendered a file whose content had not
102
+ * changed.
103
+ */
104
+ export const GENERATED_HEADER = "# Generated by @grimoire-rs/indexer";
105
+ /**
106
+ * What CI scaffolded by a pre-header release (<= 0.2.2) looks like: a thin
107
+ * caller pointing back at this package's reusable workflow, or a GitLab
108
+ * remote include of the same. Those files carry no generated header, so
109
+ * without this the reaper was blind to exactly the repositories that most
110
+ * need telling — an index scaffolded with the old `--forge both` resolves to
111
+ * `forge: "github"` today (its config has no `ci` block at all) and would
112
+ * keep serving an orphaned `.gitlab-ci.yml` with nothing to say so.
113
+ */
114
+ const LEGACY_SIGNATURE = /uses:\s*grimoire-rs\/indexer\/\.github\/workflows\/|raw\.githubusercontent\.com\/grimoire-rs\/indexer\//;
115
+ /** Where each forge's files land. */
116
+ const GITHUB_FILES = {
117
+ pages: ".github/workflows/pages.yml",
118
+ validate: ".github/workflows/validate.yml",
119
+ verify: ".github/workflows/verify-ci.yml",
120
+ };
121
+ const GITLAB_FILE = ".gitlab-ci.yml";
122
+ /**
123
+ * Where the grim release tarball is fetched from. `latest` has its own URL
124
+ * shape on GitHub releases; anything else is a tag. Resolving it here rather
125
+ * than in the generated shell is the point of rendering CI from config: a
126
+ * value that is fixed at render time should not be a branch at run time.
127
+ */
128
+ function grimReleaseBase(grimVersion) {
129
+ const releases = "https://github.com/grimoire-rs/grimoire/releases";
130
+ return grimVersion === "latest"
131
+ ? `${releases}/latest/download`
132
+ : `${releases}/download/${grimVersion}`;
133
+ }
134
+ /**
135
+ * `paths:` entries that must re-run the drift guard — every file it renders,
136
+ * plus the config they are rendered from. `package-lock.json` is in there
137
+ * because the lock is what decides which renderer produces them.
138
+ */
139
+ const VERIFY_PATHS = [CONFIG_FILE, "package.json", "package-lock.json", ".github/workflows/**"];
140
+ /**
141
+ * Render the complete CI file set. Nothing touches disk — the caller writes
142
+ * it or diffs it against what is committed.
143
+ */
144
+ export function renderCi(ci) {
145
+ const base = {
146
+ nodeVersion: ci.nodeVersion,
147
+ grimVersion: ci.grimVersion,
148
+ grimReleaseBase: grimReleaseBase(ci.grimVersion),
149
+ };
150
+ // Rendered first: `render` is single-pass, so a block handed in as a value
151
+ // must already have its own placeholders resolved. The trailing newline is
152
+ // trimmed because the template supplies one on the placeholder's own line —
153
+ // keeping both would leave a stray blank line, or an empty block would not.
154
+ const block = (rel) => fromTemplate(rel, base).replace(/\n$/, "");
155
+ const vars = {
156
+ ...base,
157
+ header: fromTemplate("ci/header.txt", base),
158
+ githubEnrichSteps: ci.enrich ? block("ci/github-enrich.yml") : "",
159
+ gitlabEnrichScript: ci.enrich ? block("ci/gitlab-enrich.sh") : "",
160
+ verifyPaths: VERIFY_PATHS.map((entry) => ` - ${entry}`).join("\n"),
161
+ gitlabVerifyJob: ci.allowManualEdits ? "" : block("ci/gitlab-verify.yml"),
162
+ // GitHub only — `validateCi` refuses the GitLab combination outright, so
163
+ // there is no silently-inert case to render around here.
164
+ githubAutoMergeJob: ci.autoMerge ? block("ci/github-automerge.yml") : "",
165
+ };
166
+ // Exactly one trailing newline, whatever the placeholders resolved to. An
167
+ // optional block that renders empty at the end of a file otherwise leaves
168
+ // its own blank line behind, so the same config produced two shapes.
169
+ const file = (rel) => fromTemplate(rel, vars).replace(/\n*$/, "\n");
170
+ const files = new Map();
171
+ if (ci.forge === "github") {
172
+ files.set(GITHUB_FILES.pages, file("ci/github-pages.yml"));
173
+ files.set(GITHUB_FILES.validate, file("ci/github-validate.yml"));
174
+ if (!ci.allowManualEdits) {
175
+ files.set(GITHUB_FILES.verify, file("ci/github-verify-ci.yml"));
176
+ }
177
+ }
178
+ else {
179
+ files.set(GITLAB_FILE, file("ci/gitlab-ci.yml"));
180
+ }
181
+ return files;
182
+ }
183
+ /**
184
+ * Canonicalize a generated file for drift comparison.
185
+ *
186
+ * The index repository owns the *pin* on every `uses:` action reference —
187
+ * its own Renovate or Dependabot will bump `uses: owner/action@<ref> # vX`,
188
+ * and the guard must not read that as a hand-edit. Only the `@<ref>` suffix
189
+ * and any trailing version comment are stripped; `owner/action` survives, so
190
+ * swapping in a different action still trips drift.
191
+ */
192
+ export function normalizeForDrift(content) {
193
+ // Line endings first. Git for Windows installs with `core.autocrlf=true` by
194
+ // default, so an untouched clone has CRLF on disk while the renderer emits
195
+ // LF — every generated file read as drift, and `npm run ci` could not clear
196
+ // it because the rewrite was LF too. The scaffold ships a `.gitattributes`
197
+ // that prevents it; this unsticks the checkouts that already have it.
198
+ return content
199
+ .replace(/\r\n/g, "\n")
200
+ .replace(/^([ \t]*(?:-[ \t]+)?uses:[ \t]*[^@\s]+)@[^\s#]+(?:[ \t]*#[^\n]*)?$/gm, "$1");
201
+ }
202
+ /**
203
+ * Files on disk that carry our header but that the current config no longer
204
+ * renders — a `forge` narrowed from `both`, or `allowManualEdits` turned on
205
+ * after a guard was already committed. Reported, never deleted: removing a
206
+ * file from somebody's repository is their call, not the renderer's.
207
+ */
208
+ export async function staleCi(root, files) {
209
+ const candidates = [GITLAB_FILE];
210
+ try {
211
+ for (const entry of await fs.readdir(path.join(root, ".github/workflows"))) {
212
+ if (/\.ya?ml$/.test(entry))
213
+ candidates.push(`.github/workflows/${entry}`);
214
+ }
215
+ }
216
+ catch {
217
+ /* no workflows directory — nothing of ours can be stale in it */
218
+ }
219
+ const stale = [];
220
+ for (const relative of candidates) {
221
+ if (files.has(relative))
222
+ continue;
223
+ const content = await fs.readFile(path.join(root, relative), "utf8").catch(() => null);
224
+ if (content === null)
225
+ continue;
226
+ if (content.startsWith(GENERATED_HEADER) || LEGACY_SIGNATURE.test(content)) {
227
+ stale.push(relative);
228
+ }
229
+ }
230
+ return stale.sort();
231
+ }
232
+ /**
233
+ * Write the rendered set, overwriting whatever is there.
234
+ *
235
+ * "Whatever is there" is judged through [`normalizeForDrift`], the same lens
236
+ * the guard uses — not raw bytes. Otherwise the two disagreed: `ci --check`
237
+ * deliberately tolerates a bumped `uses: owner/action@<ref>`, while a plain
238
+ * `npm run ci` rewrote it back to the pin baked into this package, silently
239
+ * undoing every Renovate bump the repository is told it may make. The repo
240
+ * owns its pins; a render must not take them back.
241
+ */
242
+ export async function writeCi(root, files) {
243
+ const outcomes = [];
244
+ for (const [relative, content] of files) {
245
+ const abs = path.join(root, relative);
246
+ const current = await fs.readFile(abs, "utf8").catch(() => null);
247
+ if (current !== null && normalizeForDrift(current) === normalizeForDrift(content)) {
248
+ outcomes.push({ path: relative, status: "unchanged" });
249
+ continue;
250
+ }
251
+ await fs.mkdir(path.dirname(abs), { recursive: true });
252
+ await fs.writeFile(abs, content);
253
+ outcomes.push({ path: relative, status: "written" });
254
+ }
255
+ return outcomes;
256
+ }
257
+ /**
258
+ * Compare the rendered set against what is committed. Missing counts as
259
+ * drift; a generated file the config no longer renders counts as stale.
260
+ * Hints are path-only — the contents of a repository's CI never go to stderr.
261
+ */
262
+ export async function checkCi(root, files) {
263
+ const outcomes = [];
264
+ for (const [relative, expected] of files) {
265
+ const actual = await fs.readFile(path.join(root, relative), "utf8").catch(() => null);
266
+ const same = actual !== null && normalizeForDrift(actual) === normalizeForDrift(expected);
267
+ outcomes.push({ path: relative, status: same ? "ok" : "drift" });
268
+ }
269
+ for (const relative of await staleCi(root, files)) {
270
+ outcomes.push({ path: relative, status: "stale" });
271
+ }
272
+ return outcomes;
273
+ }
274
+ /**
275
+ * The re-render command a drift message tells the reader to run. It is the
276
+ * repository's own npm script, so it runs the renderer the lock pins rather
277
+ * than whatever `npx` would fetch today.
278
+ */
279
+ export const RE_RENDER_COMMAND = "npm run ci";
280
+ /** Turn a failed check into the error the CLI exits on. */
281
+ export function driftError(outcomes) {
282
+ const bad = outcomes.filter((outcome) => outcome.status !== "ok");
283
+ return new CliError(`${bad.length} generated CI file(s) do not match ${CONFIG_FILE} — ` +
284
+ `re-run \`${RE_RENDER_COMMAND}\` and commit the result`, EXIT.data);
285
+ }
286
+ //# sourceMappingURL=ci.js.map
package/dist/ci.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.js","sourceRoot":"","sources":["../src/ci.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6B9E"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAQA,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGhD,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkB9E"}
package/dist/cli/build.js CHANGED
@@ -4,17 +4,11 @@
4
4
  // site over it. Order is load-bearing: `compileIndex` clears `outDir` and
5
5
  // writes `all.json`, which the renderer then reads.
6
6
  import path from "node:path";
7
- import { CliError, EXIT } from "./exit.js";
7
+ import { EXIT } from "./exit.js";
8
+ import { resolveOutDir } from "./out_dir.js";
8
9
  export async function build(root, flags) {
9
10
  const rootDir = path.resolve(root);
10
- const outDir = path.resolve(rootDir, flags.outDir ?? "dist");
11
- // `compileIndex` starts by removing `outDir` outright, so an out-dir that
12
- // is the repo root — or an ancestor of it — would delete the index it is
13
- // about to compile.
14
- if (outDir === rootDir || rootDir.startsWith(outDir + path.sep)) {
15
- throw new CliError(`--out-dir ${JSON.stringify(outDir)} contains the index root ${JSON.stringify(rootDir)}; ` +
16
- `the build would delete it`, EXIT.usage);
17
- }
11
+ const outDir = resolveOutDir(rootDir, flags.outDir);
18
12
  const [{ loadConfig }, { compileIndex }, { buildSite }] = await Promise.all([
19
13
  import("../config.js"),
20
14
  import("../data/index.js"),
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC,0EAA0E;AAC1E,0EAA0E;AAC1E,oDAAoD;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAM1D,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,KAAiB;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,yEAAyE;IACzE,oBAAoB;IACpB,IAAI,MAAM,KAAK,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,QAAQ,CAChB,aAAa,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI;YACxF,2BAA2B,EAC7B,IAAI,CAAC,KAAK,CACX,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC1E,MAAM,CAAC,cAAc,CAAC;QACtB,MAAM,CAAC,kBAAkB,CAAC;QAC1B,MAAM,CAAC,sBAAsB,CAAC;KAC/B,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5E,MAAM,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEnD,OAAO,CAAC,GAAG,CACT,GAAG,KAAK,sBAAsB,UAAU,CAAC,MAAM,oBAAoB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,IAAI,MAAM,EAAE,CACpH,CAAC;IACF,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC,0EAA0E;AAC1E,0EAA0E;AAC1E,oDAAoD;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAM7C,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,KAAiB;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpD,MAAM,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC1E,MAAM,CAAC,cAAc,CAAC;QACtB,MAAM,CAAC,kBAAkB,CAAC;QAC1B,MAAM,CAAC,sBAAsB,CAAC;KAC/B,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5E,MAAM,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEnD,OAAO,CAAC,GAAG,CACT,GAAG,KAAK,sBAAsB,UAAU,CAAC,MAAM,oBAAoB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,IAAI,MAAM,EAAE,CACpH,CAAC;IACF,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type ExitCode } from "./exit.js";
2
+ export interface CiFlags {
3
+ check?: boolean;
4
+ }
5
+ export declare function ci(root: string, flags: CiFlags): Promise<ExitCode>;
6
+ //# sourceMappingURL=ci.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.d.ts","sourceRoot":"","sources":["../../src/cli/ci.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,WAAW,OAAO;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD,wBAAsB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA+BxE"}
package/dist/cli/ci.js ADDED
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ // `grim-indexer ci` — render this repository's CI from `index.config.json`,
4
+ // or (`--check`) verify that what is committed still matches. The generated
5
+ // `verify-ci` job runs the second form, which is what keeps a hand-edit from
6
+ // silently forking the pipeline.
7
+ import path from "node:path";
8
+ import { checkCi, driftError, loadCiConfig, renderCi, RE_RENDER_COMMAND, resolveCi, staleCi, writeCi, } from "../ci.js";
9
+ import { CONFIG_FILE } from "../config.js";
10
+ import { EXIT } from "./exit.js";
11
+ function report(outcome) {
12
+ console.log(` ${outcome.status.padEnd(12)}${outcome.path}`);
13
+ }
14
+ export async function ci(root, flags) {
15
+ const rootDir = path.resolve(root);
16
+ const resolved = resolveCi(await loadCiConfig(rootDir));
17
+ const files = renderCi(resolved);
18
+ if (flags.check) {
19
+ const outcomes = await checkCi(rootDir, files);
20
+ for (const outcome of outcomes) {
21
+ // Path-only, on stderr: the contents of a repository's CI are not this
22
+ // command's to print, and a log is the wrong place for them.
23
+ if (outcome.status !== "ok")
24
+ console.error(`${outcome.status}: ${outcome.path}`);
25
+ }
26
+ if (outcomes.some((outcome) => outcome.status !== "ok"))
27
+ throw driftError(outcomes);
28
+ console.log(`${files.size} generated CI file(s) match ${CONFIG_FILE}`);
29
+ return EXIT.ok;
30
+ }
31
+ const written = await writeCi(rootDir, files);
32
+ written.forEach(report);
33
+ const stale = await staleCi(rootDir, files);
34
+ for (const relative of stale) {
35
+ report({ path: relative, status: "stale" });
36
+ }
37
+ if (stale.length > 0) {
38
+ console.error(`\n${stale.length} generated file(s) are no longer rendered by ${CONFIG_FILE} — ` +
39
+ `delete them, or \`${RE_RENDER_COMMAND}:check\` keeps failing`);
40
+ }
41
+ return EXIT.ok;
42
+ }
43
+ //# sourceMappingURL=ci.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.js","sourceRoot":"","sources":["../../src/cli/ci.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,iCAAiC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EACL,OAAO,EACP,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,OAAO,GAER,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAMhD,SAAS,MAAM,CAAC,OAAkB;IAChC,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,IAAY,EAAE,KAAc;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAEjC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC/C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,uEAAuE;YACvE,6DAA6D;YAC7D,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI;gBAAE,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACnF,CAAC;QACD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC;YAAE,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;QACpF,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,+BAA+B,WAAW,EAAE,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC,EAAE,CAAC;IACjB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAExB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5C,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC7B,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CACX,KAAK,KAAK,CAAC,MAAM,gDAAgD,WAAW,KAAK;YAC/E,qBAAqB,iBAAiB,wBAAwB,CACjE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { type ExitCode } from "./exit.js";
2
+ export interface DevFlags {
3
+ outDir?: string;
4
+ port?: string;
5
+ }
6
+ export declare function dev(root: string, flags: DevFlags): Promise<ExitCode>;
7
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AASA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAG1D,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAgBD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoC1E"}