diff --git a/apps/loopover-ui/content/docs/ams-config-precedence.mdx b/apps/loopover-ui/content/docs/ams-config-precedence.mdx index b86de459f5..ebbe74655f 100644 --- a/apps/loopover-ui/content/docs/ams-config-precedence.mdx +++ b/apps/loopover-ui/content/docs/ams-config-precedence.mdx @@ -182,5 +182,5 @@ goal-spec override. ## See also - [`packages/loopover-miner/docs/miner-goal-spec.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/miner-goal-spec.md) — goal-spec field reference -- [`packages/loopover-miner/docs/env-reference.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/env-reference.md) — generated operator env list +- [AMS environment variable reference](/docs/ams-env-reference) — generated operator env list - ORB's `.loopover.yml` precedence (`yml > DB > defaults`) in [Self-hosting configuration](/docs/self-hosting-configuration) — analogous documentation style, different runtime diff --git a/apps/loopover-ui/content/docs/ams-env-reference.mdx b/apps/loopover-ui/content/docs/ams-env-reference.mdx new file mode 100644 index 0000000000..90addd381d --- /dev/null +++ b/apps/loopover-ui/content/docs/ams-env-reference.mdx @@ -0,0 +1,24 @@ +--- +title: AMS environment variable reference +description: Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied. +--- + +import { AMS_ENV_REFERENCE_MARKDOWN } from "@/lib/ams-env-reference"; + +AMS's environment-variable reference: every `LOOPOVER_MINER_*` / `MINER_*` var the miner reads, +where it's first referenced in code, and its default. This is the AMS-side counterpart to +[Self-hosting configuration](/docs/self-hosting-configuration)'s generated env reference — see +the [AMS deployment guide](/docs/ams-deployment) and [AMS config +precedence](/docs/ams-config-precedence) for how these variables fit into the broader +laptop/fleet setup and precedence model. + + + This table is generated from `env.NAME` reads (and the `resolveLocalStoreDbPath` / + `LOOPOVER_MINER_*_DB` per-store override pattern) under + `packages/loopover-miner/{bin,lib,scripts}`. It intentionally includes names, first source + references, and defaults only, never example values — and it's checked for drift in CI + (`npm run miner:env-reference:check`), so a new `LOOPOVER_MINER_*`/`MINER_*` read that isn't + reflected here fails the build until this reference is regenerated. + + + diff --git a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx index bf4cbea18f..0a78b2641a 100644 --- a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx +++ b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx @@ -275,6 +275,6 @@ store filename. - [AMS deployment guide](/docs/ams-deployment) — laptop vs fleet, volumes, systemd, scaling rules - [`packages/loopover-miner/README.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/README.md#local-storage) — store inventory -- [`packages/loopover-miner/docs/env-reference.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/env-reference.md) — per-store path overrides +- [AMS environment variable reference](/docs/ams-env-reference) — per-store path overrides - [`packages/loopover-miner/docs/coding-agent-driver.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/coding-agent-driver.md) — attempt log semantics - [`packages/loopover-miner/docs/discovery-plane-operator-guide.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/discovery-plane-operator-guide.md) — optional hosted plane (distinct from local ledger ops) diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx index 3c5683df22..a8b2149855 100644 --- a/apps/loopover-ui/src/components/site/docs-nav.tsx +++ b/apps/loopover-ui/src/components/site/docs-nav.tsx @@ -81,6 +81,7 @@ export const docsNav: DocsGroup[] = [ { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" }, { to: "/docs/ams-sizing", label: "Resource sizing" }, { to: "/docs/ams-config-precedence", label: "Config precedence" }, + { to: "/docs/ams-env-reference", label: "Env var reference" }, ], }, ], diff --git a/apps/loopover-ui/src/lib/ams-env-reference.ts b/apps/loopover-ui/src/lib/ams-env-reference.ts new file mode 100644 index 0000000000..3029566d56 --- /dev/null +++ b/apps/loopover-ui/src/lib/ams-env-reference.ts @@ -0,0 +1,199 @@ +// Generated by scripts/generate-env-reference.mjs (npm run miner:env-reference). Do not edit manually. +export type MinerEnvReferenceRow = { + name: string; + firstReference: string; + defaultValue: string | null; +}; + +export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ + { + name: "LOOPOVER_MINER_AMS_COLLECTOR_TOKEN", + firstReference: "lib/orb-export.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_AMS_COLLECTOR_URL", + firstReference: "lib/orb-export.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID", + firstReference: "lib/oauth-device-flow.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_AMS_POLICY_PATH", + firstReference: "lib/ams-policy.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_ATTEMPT_LOG_DB", + firstReference: "lib/attempt-log.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_CLAIM_LEDGER_DB", + firstReference: "lib/claim-ledger.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_CONFIG_DIR", + firstReference: "lib/deny-hook-synthesis.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB", + firstReference: "lib/deny-hook-synthesis.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_EVENT_LEDGER_DB", + firstReference: "lib/event-ledger.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_GOVERNOR_LEDGER_DB", + firstReference: "lib/governor-ledger.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_GOVERNOR_STATE_DB", + firstReference: "lib/governor-state.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_KILL_SWITCH", + firstReference: "lib/config-precedence.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_LOG_LEVEL", + firstReference: "lib/logger.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_NO_UPDATE_CHECK", + firstReference: "lib/update-check.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_ORB_EXPORT_DB", + firstReference: "lib/orb-export.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_PLAN_STORE_DB", + firstReference: "lib/plan-store.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_POLICY_DOC_CACHE_DB", + firstReference: "lib/policy-doc-cache.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB", + firstReference: "lib/policy-verdict-cache.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_PORTFOLIO_QUEUE_DB", + firstReference: "lib/portfolio-queue.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_PREDICTION_LEDGER_DB", + firstReference: "lib/prediction-ledger.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_RANKED_CANDIDATES_DB", + firstReference: "lib/ranked-candidates.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_REPLAY_SNAPSHOT_DB", + firstReference: "lib/replay-snapshot.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_REPO_CLONE_DIR", + firstReference: "lib/repo-clone.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_RUN_STATE_DB", + firstReference: "lib/run-state.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_SENTRY_DSN", + firstReference: "lib/sentry.js", + defaultValue: null, + }, + { + name: "LOOPOVER_MINER_SENTRY_ENVIRONMENT", + firstReference: "lib/sentry.js", + defaultValue: "production", + }, + { + name: "LOOPOVER_MINER_VERSION", + firstReference: "lib/version.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB", + firstReference: "lib/worktree-allocator.js", + defaultValue: "", + }, + { + name: "LOOPOVER_MINER_WORKTREE_DIR", + firstReference: "lib/worktree-allocator.js", + defaultValue: "", + }, + { + name: "MINER_CODING_AGENT_PROVIDER", + firstReference: "lib/laptop-init.js", + defaultValue: null, + }, +]; + +export const AMS_ENV_REFERENCE_MARKDOWN = [ + "# loopover-miner environment variable reference", + "", + "Generated by `npm run miner:env-reference`. Do not edit manually.", + "", + "| Name | First reference | Default |", + "| --- | --- | --- |", + '| `LOOPOVER_MINER_AMS_COLLECTOR_TOKEN` | `lib/orb-export.js` | `""` |', + '| `LOOPOVER_MINER_AMS_COLLECTOR_URL` | `lib/orb-export.js` | `""` |', + '| `LOOPOVER_MINER_AMS_OAUTH_CLIENT_ID` | `lib/oauth-device-flow.js` | `""` |', + "| `LOOPOVER_MINER_AMS_POLICY_PATH` | `lib/ams-policy.js` | (none) |", + "| `LOOPOVER_MINER_ATTEMPT_LOG_DB` | `lib/attempt-log.js` | (none) |", + "| `LOOPOVER_MINER_CLAIM_LEDGER_DB` | `lib/claim-ledger.js` | (none) |", + '| `LOOPOVER_MINER_CONFIG_DIR` | `lib/deny-hook-synthesis.js` | `""` |', + '| `LOOPOVER_MINER_DENY_HOOK_SYNTHESIS_DB` | `lib/deny-hook-synthesis.js` | `""` |', + "| `LOOPOVER_MINER_EVENT_LEDGER_DB` | `lib/event-ledger.js` | (none) |", + '| `LOOPOVER_MINER_GOVERNOR_LEDGER_DB` | `lib/governor-ledger.js` | `""` |', + "| `LOOPOVER_MINER_GOVERNOR_STATE_DB` | `lib/governor-state.js` | (none) |", + '| `LOOPOVER_MINER_KILL_SWITCH` | `lib/config-precedence.js` | `""` |', + '| `LOOPOVER_MINER_LOG_LEVEL` | `lib/logger.js` | `""` |', + '| `LOOPOVER_MINER_NO_UPDATE_CHECK` | `lib/update-check.js` | `""` |', + '| `LOOPOVER_MINER_ORB_EXPORT_DB` | `lib/orb-export.js` | `""` |', + '| `LOOPOVER_MINER_PLAN_STORE_DB` | `lib/plan-store.js` | `""` |', + "| `LOOPOVER_MINER_POLICY_DOC_CACHE_DB` | `lib/policy-doc-cache.js` | (none) |", + "| `LOOPOVER_MINER_POLICY_VERDICT_CACHE_DB` | `lib/policy-verdict-cache.js` | (none) |", + "| `LOOPOVER_MINER_PORTFOLIO_QUEUE_DB` | `lib/portfolio-queue.js` | (none) |", + '| `LOOPOVER_MINER_PREDICTION_LEDGER_DB` | `lib/prediction-ledger.js` | `""` |', + "| `LOOPOVER_MINER_RANKED_CANDIDATES_DB` | `lib/ranked-candidates.js` | (none) |", + "| `LOOPOVER_MINER_REPLAY_SNAPSHOT_DB` | `lib/replay-snapshot.js` | (none) |", + '| `LOOPOVER_MINER_REPO_CLONE_DIR` | `lib/repo-clone.js` | `""` |', + "| `LOOPOVER_MINER_RUN_STATE_DB` | `lib/run-state.js` | (none) |", + "| `LOOPOVER_MINER_SENTRY_DSN` | `lib/sentry.js` | (none) |", + '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `"production"` |', + '| `LOOPOVER_MINER_VERSION` | `lib/version.js` | `""` |', + '| `LOOPOVER_MINER_WORKTREE_ALLOCATOR_DB` | `lib/worktree-allocator.js` | `""` |', + '| `LOOPOVER_MINER_WORKTREE_DIR` | `lib/worktree-allocator.js` | `""` |', + "| `MINER_CODING_AGENT_PROVIDER` | `lib/laptop-init.js` | (none) |", + "", +].join("\n"); diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts index bac1be6423..f8fc03779f 100644 --- a/apps/loopover-ui/src/routeTree.gen.ts +++ b/apps/loopover-ui/src/routeTree.gen.ts @@ -61,6 +61,7 @@ import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.a import { Route as DocsAmsSizingRouteImport } from './routes/docs.ams-sizing' import { Route as DocsAmsOperationsRunbookRouteImport } from './routes/docs.ams-operations-runbook' import { Route as DocsAmsObservabilityRouteImport } from './routes/docs.ams-observability' +import { Route as DocsAmsEnvReferenceRouteImport } from './routes/docs.ams-env-reference' import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment' import { Route as DocsAmsConfigPrecedenceRouteImport } from './routes/docs.ams-config-precedence' import { Route as DocsAiSummariesRouteImport } from './routes/docs.ai-summaries' @@ -356,6 +357,11 @@ const DocsAmsObservabilityRoute = DocsAmsObservabilityRouteImport.update({ path: '/ams-observability', getParentRoute: () => DocsRoute, } as any) +const DocsAmsEnvReferenceRoute = DocsAmsEnvReferenceRouteImport.update({ + id: '/ams-env-reference', + path: '/ams-env-reference', + getParentRoute: () => DocsRoute, +} as any) const DocsAmsDeploymentRoute = DocsAmsDeploymentRouteImport.update({ id: '/ams-deployment', path: '/ams-deployment', @@ -475,6 +481,7 @@ export interface FileRoutesByFullPath { '/docs/ai-summaries': typeof DocsAiSummariesRoute '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute '/docs/ams-deployment': typeof DocsAmsDeploymentRoute + '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute '/docs/ams-sizing': typeof DocsAmsSizingRoute @@ -544,6 +551,7 @@ export interface FileRoutesByTo { '/docs/ai-summaries': typeof DocsAiSummariesRoute '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute '/docs/ams-deployment': typeof DocsAmsDeploymentRoute + '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute '/docs/ams-sizing': typeof DocsAmsSizingRoute @@ -617,6 +625,7 @@ export interface FileRoutesById { '/docs/ai-summaries': typeof DocsAiSummariesRoute '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute '/docs/ams-deployment': typeof DocsAmsDeploymentRoute + '/docs/ams-env-reference': typeof DocsAmsEnvReferenceRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute '/docs/ams-sizing': typeof DocsAmsSizingRoute @@ -691,6 +700,7 @@ export interface FileRouteTypes { | '/docs/ai-summaries' | '/docs/ams-config-precedence' | '/docs/ams-deployment' + | '/docs/ams-env-reference' | '/docs/ams-observability' | '/docs/ams-operations-runbook' | '/docs/ams-sizing' @@ -760,6 +770,7 @@ export interface FileRouteTypes { | '/docs/ai-summaries' | '/docs/ams-config-precedence' | '/docs/ams-deployment' + | '/docs/ams-env-reference' | '/docs/ams-observability' | '/docs/ams-operations-runbook' | '/docs/ams-sizing' @@ -832,6 +843,7 @@ export interface FileRouteTypes { | '/docs/ai-summaries' | '/docs/ams-config-precedence' | '/docs/ams-deployment' + | '/docs/ams-env-reference' | '/docs/ams-observability' | '/docs/ams-operations-runbook' | '/docs/ams-sizing' @@ -1257,6 +1269,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsAmsObservabilityRouteImport parentRoute: typeof DocsRoute } + '/docs/ams-env-reference': { + id: '/docs/ams-env-reference' + path: '/ams-env-reference' + fullPath: '/docs/ams-env-reference' + preLoaderRoute: typeof DocsAmsEnvReferenceRouteImport + parentRoute: typeof DocsRoute + } '/docs/ams-deployment': { id: '/docs/ams-deployment' path: '/ams-deployment' @@ -1438,6 +1457,7 @@ interface DocsRouteChildren { DocsAiSummariesRoute: typeof DocsAiSummariesRoute DocsAmsConfigPrecedenceRoute: typeof DocsAmsConfigPrecedenceRoute DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute + DocsAmsEnvReferenceRoute: typeof DocsAmsEnvReferenceRoute DocsAmsObservabilityRoute: typeof DocsAmsObservabilityRoute DocsAmsOperationsRunbookRoute: typeof DocsAmsOperationsRunbookRoute DocsAmsSizingRoute: typeof DocsAmsSizingRoute @@ -1484,6 +1504,7 @@ const DocsRouteChildren: DocsRouteChildren = { DocsAiSummariesRoute: DocsAiSummariesRoute, DocsAmsConfigPrecedenceRoute: DocsAmsConfigPrecedenceRoute, DocsAmsDeploymentRoute: DocsAmsDeploymentRoute, + DocsAmsEnvReferenceRoute: DocsAmsEnvReferenceRoute, DocsAmsObservabilityRoute: DocsAmsObservabilityRoute, DocsAmsOperationsRunbookRoute: DocsAmsOperationsRunbookRoute, DocsAmsSizingRoute: DocsAmsSizingRoute, diff --git a/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx b/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx new file mode 100644 index 0000000000..f78ef9d061 --- /dev/null +++ b/apps/loopover-ui/src/routes/docs.ams-env-reference.tsx @@ -0,0 +1,53 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { Suspense } from "react"; + +import { DocsPage } from "@/components/site/docs-page"; +import { docsClientLoader } from "@/lib/docs-client-loader"; + +// Rendered from content/docs/ams-env-reference.mdx via fumadocs-mdx's browser entry +// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock primitives -- +// not fumadocs-ui's bundled components. See docs-source.ts's comment for why the +// loader below resolves only a plain, serializable path string. The .mdx content +// itself imports AMS_ENV_REFERENCE_MARKDOWN from @/lib/ams-env-reference (generated by +// packages/loopover-miner/scripts/generate-env-reference.mjs, drift-checked in CI), +// the same pattern docs.self-hosting-configuration.tsx uses for +// SELFHOST_ENV_REFERENCE_MARKDOWN. +export const Route = createFileRoute("/docs/ams-env-reference")({ + loader: async () => { + const { docsSource } = await import("@/lib/docs-source"); + const page = docsSource.getPage(["ams-env-reference"]); + if (!page) throw notFound(); + return { path: page.path, title: page.data.title, description: page.data.description }; + }, + head: () => ({ + meta: [ + { title: "AMS environment variable reference — LoopOver docs" }, + { + name: "description", + content: + "Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.", + }, + { property: "og:title", content: "AMS environment variable reference — LoopOver docs" }, + { + property: "og:description", + content: + "Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.", + }, + { property: "og:url", content: "/docs/ams-env-reference" }, + ], + links: [{ rel: "canonical", href: "/docs/ams-env-reference" }], + }), + component: AmsEnvReference, +}); + +function AmsEnvReference() { + const { path, title, description } = Route.useLoaderData(); + const Content = docsClientLoader.getComponent(path); + return ( + + Loading…

}> + +
+
+ ); +} diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx index a80c6012db..81a91e0bf1 100644 --- a/apps/loopover-ui/src/routes/docs.index.tsx +++ b/apps/loopover-ui/src/routes/docs.index.tsx @@ -79,6 +79,7 @@ const AUDIENCES: Audience[] = [ { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" }, { to: "/docs/ams-sizing", label: "Resource sizing" }, { to: "/docs/ams-config-precedence", label: "Config precedence" }, + { to: "/docs/ams-env-reference", label: "Env var reference" }, { to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" }, { to: "/docs/maintainer-install-trust", label: "Install & trust guide" }, { to: "/docs/github-app", label: "GitHub App configuration" }, diff --git a/packages/loopover-miner/scripts/generate-env-reference.d.mts b/packages/loopover-miner/scripts/generate-env-reference.d.mts index 98318573ff..c2bef270f0 100644 --- a/packages/loopover-miner/scripts/generate-env-reference.d.mts +++ b/packages/loopover-miner/scripts/generate-env-reference.d.mts @@ -15,6 +15,7 @@ export type WriteMinerEnvReferenceOptions = MinerEnvReferenceOptions & { }; export declare const DEFAULT_OUTPUT_PATH: string; +export declare const DEFAULT_MODULE_OUTPUT_PATH: string; export declare const DEFAULT_SOURCE_ROOTS: readonly string[]; export declare function isMinerEnvVar(name: string): boolean; @@ -37,3 +38,13 @@ export declare function writeMinerEnvReference( outputPath: string; rows: MinerEnvReferenceRow[]; }; + +export declare function renderMinerEnvReferenceModule(rows: MinerEnvReferenceRow[]): string; + +export declare function writeMinerEnvReferenceModule( + options?: WriteMinerEnvReferenceOptions, +): { + changed: boolean; + outputPath: string; + rows: MinerEnvReferenceRow[]; +}; diff --git a/packages/loopover-miner/scripts/generate-env-reference.mjs b/packages/loopover-miner/scripts/generate-env-reference.mjs index e719c4a83b..90188462a7 100644 --- a/packages/loopover-miner/scripts/generate-env-reference.mjs +++ b/packages/loopover-miner/scripts/generate-env-reference.mjs @@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url"; import { collectSelfHostEnvVars } from "../../../scripts/gen-selfhost-env-reference.mjs"; export const DEFAULT_OUTPUT_PATH = "packages/loopover-miner/docs/env-reference.md"; +export const DEFAULT_MODULE_OUTPUT_PATH = "apps/loopover-ui/src/lib/ams-env-reference.ts"; export const DEFAULT_SOURCE_ROOTS = [ "packages/loopover-miner/bin", "packages/loopover-miner/lib", @@ -100,6 +101,73 @@ export function writeMinerEnvReference({ return { changed, outputPath, rows }; } +// Mirrors scripts/gen-selfhost-env-reference.mjs's renderSelfHostEnvReferenceModule -- same shape +// (a typed rows array plus a pre-joined markdown string), so docs.ams-env-reference.tsx's content +// can import AMS_ENV_REFERENCE_MARKDOWN and render it exactly the way +// docs.self-hosting-configuration.tsx already renders SELFHOST_ENV_REFERENCE_MARKDOWN. The +// row-level default value is AMS-specific (ORB's self-host reference has no equivalent column). +// Project Prettier config defaults to double-quoted JS string literals (singleQuote: false), but +// still switches a given string to single quotes when that needs fewer backslash-escapes -- which +// every markdown line whose row has a quoted string defaultValue (rendered as `""`/`"production"` +// in the table) hits, since JSON.stringify always double-quotes. Replicate that per-string choice +// here so the generator's own output is already Prettier-clean, not dependent on a separate --fix. +function quoteJsStringLiteral(value) { + const doubleQuoteCount = (value.match(/"/g) ?? []).length; + const singleQuoteCount = (value.match(/'/g) ?? []).length; + if (singleQuoteCount < doubleQuoteCount) { + const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n"); + return `'${escaped}'`; + } + return JSON.stringify(value); +} + +export function renderMinerEnvReferenceModule(rows) { + const markdown = renderMinerEnvReferenceMarkdown(rows); + const rowLines = rows + .map( + (row) => + ` {\n name: ${quoteJsStringLiteral(row.name)},\n firstReference: ${quoteJsStringLiteral(row.firstReference)},\n defaultValue: ${row.defaultValue === null ? "null" : quoteJsStringLiteral(row.defaultValue)},\n },`, + ) + .join("\n"); + const markdownLines = markdown + .split("\n") + .map((line) => ` ${quoteJsStringLiteral(line)},`) + .join("\n"); + return `// Generated by scripts/generate-env-reference.mjs (npm run miner:env-reference). Do not edit manually. +export type MinerEnvReferenceRow = { + name: string; + firstReference: string; + defaultValue: string | null; +}; + +export const AMS_ENV_REFERENCE_ROWS: MinerEnvReferenceRow[] = [ +${rowLines} +]; + +export const AMS_ENV_REFERENCE_MARKDOWN = [ +${markdownLines} +].join("\\n"); +`; +} + +export function writeMinerEnvReferenceModule({ + rootDir = process.cwd(), + outputPath = DEFAULT_MODULE_OUTPUT_PATH, + sourceRoots = DEFAULT_SOURCE_ROOTS, + check = false, +} = {}) { + const rows = collectMinerEnvVars({ rootDir, sourceRoots }); + const output = renderMinerEnvReferenceModule(rows); + const absOutput = resolve(rootDir, outputPath); + const current = existsSync(absOutput) ? readFileSync(absOutput, "utf8") : null; + const changed = current !== output; + if (!check && changed) { + mkdirSync(dirname(absOutput), { recursive: true }); + writeFileSync(absOutput, output); + } + return { changed, outputPath, rows }; +} + function sourceFiles(rootDir, sourceRoots) { const files = []; for (const sourceRoot of sourceRoots) { @@ -143,16 +211,21 @@ function toPosixPath(path) { function main(argv) { const check = argv.includes("--check"); - const result = writeMinerEnvReference({ check }); - if (check && result.changed) { - process.stderr.write( - `generate-env-reference: ${result.outputPath} is stale; run npm run miner:env-reference.\n`, - ); + const results = [writeMinerEnvReference({ check }), writeMinerEnvReferenceModule({ check })]; + const stale = results.filter((result) => result.changed); + if (check && stale.length > 0) { + for (const result of stale) { + process.stderr.write( + `generate-env-reference: ${result.outputPath} is stale; run npm run miner:env-reference.\n`, + ); + } process.exit(1); } - process.stdout.write( - `generate-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`, - ); + for (const result of results) { + process.stdout.write( + `generate-env-reference: ${check ? "checked" : "wrote"} ${result.rows.length} env var references in ${result.outputPath}\n`, + ); + } } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { diff --git a/test/unit/miner-env-reference-script.test.ts b/test/unit/miner-env-reference-script.test.ts index 24fc434b55..2ebc02e972 100644 --- a/test/unit/miner-env-reference-script.test.ts +++ b/test/unit/miner-env-reference-script.test.ts @@ -6,7 +6,9 @@ import { collectMinerEnvVars, isMinerEnvVar, renderMinerEnvReferenceMarkdown, + renderMinerEnvReferenceModule, writeMinerEnvReference, + writeMinerEnvReferenceModule, } from "../../packages/loopover-miner/scripts/generate-env-reference.mjs"; function fixtureRoot(): string { @@ -116,4 +118,67 @@ describe("generate-env-reference (#5179)", () => { const second = readFileSync(join(root, outputPath), "utf8"); expect(second).toBe(first); }); + + it("renders a generated TS module with a typed rows array and a joined markdown constant (#6028)", () => { + const output = renderMinerEnvReferenceModule([ + { name: "LOOPOVER_MINER_CONFIG_DIR", firstReference: "lib/a.js", defaultValue: "" }, + { name: "LOOPOVER_MINER_RUN_STATE_DB", firstReference: "lib/run-state.js", defaultValue: null }, + ]); + expect(output).toContain("export const AMS_ENV_REFERENCE_ROWS"); + expect(output).toContain("export const AMS_ENV_REFERENCE_MARKDOWN"); + expect(output).toContain('name: "LOOPOVER_MINER_CONFIG_DIR"'); + expect(output).toContain('defaultValue: ""'); + expect(output).toContain("defaultValue: null"); + expect(output).toContain("# loopover-miner environment variable reference"); + }); + + it("REGRESSION: quotes a generated markdown line with single quotes when it contains embedded double quotes, matching Prettier's own singleQuote:false-but-fewer-escapes preference (#6028)", () => { + // The rendered markdown table cell for a quoted string default (`""`, `"production"`, ...) + // embeds literal " characters in the line -- JSON.stringify would always double-quote the JS + // string literal and escape those, which Prettier then reformats to single quotes to avoid the + // escaping. Generating single-quoted output directly keeps the generator's own output already + // Prettier-clean instead of depending on a separate --fix pass that a fresh `npm run + // miner:env-reference` would immediately undo. + const output = renderMinerEnvReferenceModule([ + { name: "LOOPOVER_MINER_SENTRY_ENVIRONMENT", firstReference: "lib/sentry.js", defaultValue: "production" }, + ]); + expect(output).toContain( + " '| `LOOPOVER_MINER_SENTRY_ENVIRONMENT` | `lib/sentry.js` | `\"production\"` |',", + ); + expect(output).not.toMatch(/"\| `LOOPOVER_MINER_SENTRY_ENVIRONMENT`/); + }); + + it("writes the generated TS module and reports stale output in check mode (#6028)", () => { + const root = fixtureRoot(); + const outputPath = "apps/loopover-ui/src/lib/ams-env-reference.ts"; + const outputAbs = join(root, outputPath); + + const written = writeMinerEnvReferenceModule({ rootDir: root, outputPath }); + expect(written.changed).toBe(true); + expect(existsSync(outputAbs)).toBe(true); + const generated = readFileSync(outputAbs, "utf8"); + expect(generated).toContain("AMS_ENV_REFERENCE_MARKDOWN"); + expect(generated).toContain("LOOPOVER_MINER_CONFIG_DIR"); + + expect(writeMinerEnvReferenceModule({ rootDir: root, outputPath, check: true }).changed).toBe(false); + + writeFileSync(outputAbs, "stale\n"); + const stale = writeMinerEnvReferenceModule({ rootDir: root, outputPath, check: true }); + expect(stale.changed).toBe(true); + expect(readFileSync(outputAbs, "utf8")).toBe("stale\n"); + + const rewritten = writeMinerEnvReferenceModule({ rootDir: root, outputPath }); + expect(rewritten.changed).toBe(true); + expect(readFileSync(outputAbs, "utf8")).toBe(generated); + }); + + it("REGRESSION: TS module output is byte-for-byte deterministic across two runs (#6028)", () => { + const root = fixtureRoot(); + const outputPath = "apps/loopover-ui/src/lib/ams-env-reference.ts"; + writeMinerEnvReferenceModule({ rootDir: root, outputPath }); + const first = readFileSync(join(root, outputPath), "utf8"); + writeMinerEnvReferenceModule({ rootDir: root, outputPath }); + const second = readFileSync(join(root, outputPath), "utf8"); + expect(second).toBe(first); + }); });