@agentproto/catalog-sync 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.mjs CHANGED
@@ -137,6 +137,13 @@ async function runGenerators(gens, opts) {
137
137
  ...src.body !== void 0 ? { body: JSON.stringify(src.body) } : {}
138
138
  });
139
139
  if (!res.ok) {
140
+ if (existing !== void 0) {
141
+ process.stderr.write(
142
+ `catalog-sync: skipping refresh of "${src.id}" \u2014 fetch ${src.url} failed: ${res.status} ${res.statusText}; reusing committed snapshot.
143
+ `
144
+ );
145
+ return JSON.parse(existing);
146
+ }
140
147
  throw new Error(
141
148
  `catalog-sync: fetch ${src.url} failed: ${res.status} ${res.statusText}`
142
149
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/changelog.ts","../src/added-at.ts","../src/runner.ts"],"names":["__dirname","dirname","fileURLToPath","resolve","join","existsSync","readFileSync"],"mappings":"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","file":"runner.mjs","sourcesContent":["/**\n * `CATALOG-CHANGELOG.md` — a human-readable \"what models changed\" log,\n * distinct from the changesets-driven `CHANGELOG.md` next to it (that's\n * package-version history, one entry per publish). This file answers \"what\n * models did catalog-sync add or remove, and when\" by reading top-to-bottom.\n *\n * Wired into `runner.ts`: every `runGenerators` call diffs each changed\n * `*.generated.ts` file's top-level id keys (before vs after) and, when a\n * run actually adds or removes model ids for at least one generator,\n * APPENDS one dated section (`## YYYY-MM-DD`) with a `### <generator name>`\n * subsection per affected generator. Sections are appended at the END of\n * the file (newest last) — a plain, non-destructive `+=`, so the writer\n * never has to parse and re-emit prior history.\n *\n * A run with no id-level drift (pricing-only changes, or a `--check` with\n * nothing to report) appends NOTHING — no empty section, no touched file,\n * no diff. addedAt ledger files (`ledger/*.json`) are intentionally NOT\n * diffed here — they never lose an id (see `added-at.ts`), so they carry no\n * \"removed\" signal, and every id they'd report as \"added\" is already\n * covered by the generator's own `*.generated.ts` diff.\n */\n\n/** Matches a top-level (2-space-indented) quoted key of a `Record<string, X>` literal. */\nconst RECORD_KEY_RE = /^ {2}\"([^\"]+)\":\\s*\\{/gm\n\n/** Extracts the top-level quoted ids of a generated `Record<string, X>` map. */\nexport function extractRecordIds(source: string): Set<string> {\n const ids = new Set<string>()\n for (const m of source.matchAll(RECORD_KEY_RE)) {\n if (m[1] !== undefined) ids.add(m[1])\n }\n return ids\n}\n\nexport interface ModelIdDiff {\n added: string[]\n removed: string[]\n}\n\n/** Diffs the id sets of a generated file's before/after content. `before` undefined = new file (no removals possible). */\nexport function diffModelIds(before: string | undefined, after: string): ModelIdDiff {\n const beforeIds = before !== undefined ? extractRecordIds(before) : new Set<string>()\n const afterIds = extractRecordIds(after)\n const added = [...afterIds].filter((id) => !beforeIds.has(id)).sort()\n const removed = [...beforeIds].filter((id) => !afterIds.has(id)).sort()\n return { added, removed }\n}\n\nexport interface GeneratorChangelogEntry {\n /** The owning generator's `name` (e.g. `\"llm:openrouter\"`). */\n generator: string\n added: string[]\n removed: string[]\n}\n\n/** Renders a dated section, or `\"\"` when every entry is empty (nothing to report). */\nexport function renderChangelogSection(date: string, entries: readonly GeneratorChangelogEntry[]): string {\n const nonEmpty = entries.filter((e) => e.added.length > 0 || e.removed.length > 0)\n if (nonEmpty.length === 0) return \"\"\n const lines: string[] = [`## ${date}`, \"\"]\n for (const e of nonEmpty) {\n lines.push(`### ${e.generator}`)\n if (e.added.length > 0) lines.push(`- Added: ${e.added.join(\", \")}`)\n if (e.removed.length > 0) lines.push(`- Removed: ${e.removed.join(\", \")}`)\n lines.push(\"\")\n }\n return lines.join(\"\\n\").replace(/\\n+$/, \"\\n\")\n}\n\nexport const CATALOG_CHANGELOG_HEADER = `# Catalog Changelog\n\nHuman-readable log of model additions/removals detected by\n\\`@agentproto/catalog-sync\\` runs — NOT \\`CHANGELOG.md\\` next to it (that one is\nchangesets-driven package-version history). A dated section is appended\nautomatically here whenever a sync run adds or removes model ids; sections\nare appended at the END of the file (newest last). Pricing-only drift (no id\nadded/removed) is not logged here — see the generated files' own git history\nfor that.\n`\n\n/**\n * Appends a new dated section to `existing` changelog content.\n *\n * Returns `undefined` when there is nothing to report (no generator has an\n * added/removed id this run) — the caller should leave the file untouched\n * in that case, not write an unchanged copy.\n */\nexport function appendChangelog(\n existing: string | undefined,\n date: string,\n entries: readonly GeneratorChangelogEntry[]\n): string | undefined {\n const section = renderChangelogSection(date, entries)\n if (section === \"\") return undefined\n const base = existing !== undefined && existing.length > 0 ? existing.replace(/\\n+$/, \"\\n\") : CATALOG_CHANGELOG_HEADER\n return `${base}\\n${section}`\n}\n","/**\n * `addedAt` ledger — tracks, per generator, the ISO date each model id was\n * FIRST seen by a sync run. See `README.md` (\"addedAt convention\") for the\n * full contract; this module is the mechanism.\n *\n * Invariant: once an id has a ledger entry, that date is NEVER overwritten —\n * not even if the source's own `created` timestamp for that id later\n * resolves to something different (a provider backdating/correcting its own\n * metadata must not rewrite catalog history). New ids are backfilled from\n * the source's own creation timestamp when it has one, else stamped with\n * the current run's date.\n *\n * Ledger entries are never dropped when an id disappears from a run — the\n * ledger records \"first ever seen\", not \"currently present\". A model that\n * vanishes from a provider's list and later reappears keeps its original\n * `addedAt` rather than looking newly added.\n *\n * Each generator owns one ledger file, committed at\n * `packages/catalog-sync/ledger/<generator-id>.json` (id → ISO date, sorted\n * keys, deterministic serialization) — included as a normal entry in the\n * generator's `GeneratedFiles` return value, so `runner.ts`'s existing\n * diff/write logic covers it for free (including `--check` drift detection).\n */\n\nimport { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/** The catalog-sync package directory — see `runner.ts`'s `catalogSyncDir`. */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a generator's committed addedAt ledger. */\nexport function ledgerAbsPath(ledgerId: string): string {\n return join(catalogSyncDir(), \"ledger\", `${ledgerId}.json`)\n}\n\n/** Repo-relative path for a ledger — for inclusion in a generator's `GeneratedFiles`. */\nexport function ledgerRelPath(ledgerId: string): string {\n return `packages/catalog-sync/ledger/${ledgerId}.json`\n}\n\nexport type AddedAtLedger = Record<string, string>\n\n/** Reads a committed ledger from disk. Missing/unparseable → empty ledger. */\nexport function readLedger(ledgerId: string): AddedAtLedger {\n const path = ledgerAbsPath(ledgerId)\n if (!existsSync(path)) return {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"))\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return {}\n return parsed as AddedAtLedger\n } catch {\n return {}\n }\n}\n\n/** Deterministic (sorted-key) JSON serialization, matching the `*.generated.ts` style of a trailing newline. */\nexport function serializeLedger(ledger: AddedAtLedger): string {\n const sorted: AddedAtLedger = {}\n for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id]!\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/**\n * Merge the current run's ids into a ledger.\n *\n * - id already in `previous` → keep the existing stamp verbatim (never\n * mutated), regardless of what `createdAt` says this run.\n * - id new to the ledger, source has a native creation timestamp for it →\n * backfill with `createdAt[id]`.\n * - id new to the ledger, no native timestamp → stamp with `today`.\n * - ids in `previous` but absent from `currentIds` this run are kept as-is\n * (see module doc comment — the ledger is \"first ever seen\").\n */\nexport function computeAddedAtLedger(\n currentIds: readonly string[],\n previous: AddedAtLedger,\n createdAt: Readonly<Record<string, string>>,\n today: string\n): AddedAtLedger {\n const next: AddedAtLedger = { ...previous }\n for (const id of currentIds) {\n if (next[id] !== undefined) continue\n next[id] = createdAt[id] ?? today\n }\n return next\n}\n\n/** Today's date as `YYYY-MM-DD`, in UTC. */\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n/** Converts a provider's Unix-seconds creation timestamp to `YYYY-MM-DD`. */\nexport function isoDateFromUnixSeconds(seconds: number): string {\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport type {\n CatalogGenerator,\n CatalogSource,\n GeneratedFiles,\n GeneratorContext,\n} from \"./types.js\"\nimport { appendChangelog, diffModelIds, type GeneratorChangelogEntry } from \"./changelog.js\"\nimport { todayIso } from \"./added-at.js\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * The catalog-sync package directory. Resolves the same way whether the\n * module is loaded from `src/` (vitest, type-checking) or `dist/` (built) —\n * both sit one level below the package root, so walking up one directory\n * from this file lands on `packages/catalog-sync/`.\n */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a committed/pinned source snapshot. */\nfunction snapshotPath(id: string): string {\n return join(catalogSyncDir(), \"snapshots\", `${id}.json`)\n}\n\n/**\n * The monorepo root. Resolved by walking up from the catalog-sync package\n * until `pnpm-workspace.yaml` is found. Generated-file paths in\n * {@link GeneratedFiles} are repo-relative and are resolved against this.\n */\nfunction repoRoot(): string {\n let dir = catalogSyncDir()\n for (let i = 0; i < 12; i++) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n // Fallback: catalog-sync lives at <root>/packages/catalog-sync.\n return dirname(dirname(catalogSyncDir()))\n}\n\n/** Reads an existing file as UTF-8, or returns undefined when absent. */\nfunction readIfExists(absPath: string): string | undefined {\n if (!existsSync(absPath)) return undefined\n return readFileSync(absPath, \"utf8\")\n}\n\n/**\n * Resolve `env:VAR_NAME` tokens inside header values from `process.env`.\n * Returns the materialized headers plus the names of any referenced env vars\n * that are unset — the caller uses `missing` to decide whether the source can\n * be refreshed (we never fetch an authed endpoint without its key).\n */\nfunction resolveHeaders(headers?: Record<string, string>): {\n headers: Record<string, string>\n missing: string[]\n} {\n const out: Record<string, string> = {}\n const missing: string[] = []\n if (!headers) return { headers: out, missing }\n for (const [key, value] of Object.entries(headers)) {\n out[key] = value.replace(/env:([A-Z0-9_]+)/g, (_match, name: string) => {\n const v = process.env[name]\n if (v === undefined || v === \"\") {\n missing.push(name)\n return \"\"\n }\n return v\n })\n }\n return { headers: out, missing }\n}\n\n/** Repo-relative path of the human-readable model-add/remove log (see `changelog.ts`). */\nexport const CATALOG_CHANGELOG_PATH = \"packages/model-catalog/CATALOG-CHANGELOG.md\"\n\n/** Generated files whose top-level id keys are worth diffing for the changelog. */\nfunction isChangelogEligible(relPath: string): boolean {\n if (!relPath.endsWith(\".generated.ts\")) return false\n // The catalog-sync/generated/ dir is gitignored scratch output: on every\n // fresh run (the weekly Action) the file isn't on disk, so `existing` is\n // undefined and the id diff reports the whole roster as \"Added\" — one\n // identical changelog section per sync. Only tracked outputs (the consumed\n // model-catalog artifacts) carry a meaningful before/after diff.\n if (relPath.startsWith(\"packages/catalog-sync/generated/\")) return false\n return true\n}\n\nexport interface RunGeneratorsOptions {\n /** When true, missing snapshots are fetched from their pinned `url`. */\n refresh: boolean\n /** When true, generated files are written to disk at their repo-relative path. */\n write: boolean\n}\n\nexport interface RunGeneratorsResult {\n files: GeneratedFiles\n /** Repo-relative paths whose generated content differs from what is on disk. */\n changed: string[]\n}\n\n/**\n * Runs each generator, gathering its emitted files, then (optionally) writes\n * them. `changed` is ALWAYS computed — it is the diff of generated content\n * vs disk — so `--check` (write=false) can surface drift without mutating\n * anything. Writes only happen when `write=true` AND a path differs.\n *\n * Also appends `CATALOG_CHANGELOG_PATH` (see `changelog.ts`) when this run's\n * per-generator diffs add or remove any model id — folded into the same\n * `files`/`changed` bookkeeping as every other generated file, so `--check`\n * catches an un-committed changelog entry too.\n */\nexport async function runGenerators(\n gens: CatalogGenerator[],\n opts: RunGeneratorsOptions\n): Promise<RunGeneratorsResult> {\n const files: GeneratedFiles = {}\n const pathToGenerator: Record<string, string> = {}\n\n for (const gen of gens) {\n const ctx: GeneratorContext = {\n refresh: opts.refresh,\n async fetchSource(src: CatalogSource): Promise<unknown> {\n const path = snapshotPath(src.id)\n const existing = readIfExists(path)\n\n // Offline path (default): reuse the committed snapshot; error if none.\n if (!opts.refresh) {\n if (existing !== undefined) return JSON.parse(existing)\n throw new Error(\n `catalog-sync: no committed snapshot for source \"${src.id}\" ` +\n `(${path}), and --refresh is not set. Run ` +\n `\\`catalog-sync generate --refresh\\` to fetch ${src.url}.`\n )\n }\n\n // --refresh: force a live re-fetch (do NOT short-circuit on an existing\n // snapshot — refreshing is the whole point). Auth headers may embed\n // `env:VAR` tokens; if a referenced var is unset we can't authenticate,\n // so reuse the committed snapshot rather than fetch unauthenticated.\n const { headers, missing } = resolveHeaders(src.headers)\n if (missing.length > 0) {\n if (existing !== undefined) {\n process.stderr.write(\n `catalog-sync: skipping refresh of \"${src.id}\" — missing env ` +\n `${missing.join(\", \")}; reusing committed snapshot.\\n`\n )\n return JSON.parse(existing)\n }\n throw new Error(\n `catalog-sync: cannot refresh \"${src.id}\" — missing env ` +\n `${missing.join(\", \")} and no committed snapshot.`\n )\n }\n const res = await fetch(src.url, {\n method: src.method ?? \"GET\",\n headers,\n ...(src.body !== undefined\n ? { body: JSON.stringify(src.body) }\n : {}),\n })\n if (!res.ok) {\n throw new Error(\n `catalog-sync: fetch ${src.url} failed: ${res.status} ${res.statusText}`\n )\n }\n const text = await res.text()\n const parsed: unknown = JSON.parse(text)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\\n`, \"utf8\")\n return parsed\n },\n }\n\n const out = await gen.generate(ctx)\n for (const [path, content] of Object.entries(out)) {\n files[path] = content\n pathToGenerator[path] = gen.name\n }\n }\n\n const root = repoRoot()\n const changed: string[] = []\n const changelogEntries: GeneratorChangelogEntry[] = []\n for (const [relPath, content] of Object.entries(files)) {\n const abs = join(root, relPath)\n const existing = readIfExists(abs)\n if (existing !== content) {\n changed.push(relPath)\n if (opts.write) {\n mkdirSync(dirname(abs), { recursive: true })\n writeFileSync(abs, content, \"utf8\")\n }\n }\n if (isChangelogEligible(relPath)) {\n const { added, removed } = diffModelIds(existing, content)\n if (added.length > 0 || removed.length > 0) {\n changelogEntries.push({ generator: pathToGenerator[relPath] ?? relPath, added, removed })\n }\n }\n }\n\n // CATALOG-CHANGELOG.md: appended (never touched when there's nothing to\n // report) from the per-generator id diffs gathered above. Goes through the\n // same changed/write bookkeeping as any other generated file.\n const changelogAbs = join(root, CATALOG_CHANGELOG_PATH)\n const changelogExisting = readIfExists(changelogAbs)\n const newChangelog = appendChangelog(changelogExisting, todayIso(), changelogEntries)\n if (newChangelog !== undefined) {\n files[CATALOG_CHANGELOG_PATH] = newChangelog\n changed.push(CATALOG_CHANGELOG_PATH)\n if (opts.write) {\n mkdirSync(dirname(changelogAbs), { recursive: true })\n writeFileSync(changelogAbs, newChangelog, \"utf8\")\n }\n }\n\n return { files, changed }\n}\n"]}
1
+ {"version":3,"sources":["../src/changelog.ts","../src/added-at.ts","../src/runner.ts"],"names":["__dirname","dirname","fileURLToPath","resolve","join","existsSync","readFileSync"],"mappings":"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","file":"runner.mjs","sourcesContent":["/**\n * `CATALOG-CHANGELOG.md` — a human-readable \"what models changed\" log,\n * distinct from the changesets-driven `CHANGELOG.md` next to it (that's\n * package-version history, one entry per publish). This file answers \"what\n * models did catalog-sync add or remove, and when\" by reading top-to-bottom.\n *\n * Wired into `runner.ts`: every `runGenerators` call diffs each changed\n * `*.generated.ts` file's top-level id keys (before vs after) and, when a\n * run actually adds or removes model ids for at least one generator,\n * APPENDS one dated section (`## YYYY-MM-DD`) with a `### <generator name>`\n * subsection per affected generator. Sections are appended at the END of\n * the file (newest last) — a plain, non-destructive `+=`, so the writer\n * never has to parse and re-emit prior history.\n *\n * A run with no id-level drift (pricing-only changes, or a `--check` with\n * nothing to report) appends NOTHING — no empty section, no touched file,\n * no diff. addedAt ledger files (`ledger/*.json`) are intentionally NOT\n * diffed here — they never lose an id (see `added-at.ts`), so they carry no\n * \"removed\" signal, and every id they'd report as \"added\" is already\n * covered by the generator's own `*.generated.ts` diff.\n */\n\n/** Matches a top-level (2-space-indented) quoted key of a `Record<string, X>` literal. */\nconst RECORD_KEY_RE = /^ {2}\"([^\"]+)\":\\s*\\{/gm\n\n/** Extracts the top-level quoted ids of a generated `Record<string, X>` map. */\nexport function extractRecordIds(source: string): Set<string> {\n const ids = new Set<string>()\n for (const m of source.matchAll(RECORD_KEY_RE)) {\n if (m[1] !== undefined) ids.add(m[1])\n }\n return ids\n}\n\nexport interface ModelIdDiff {\n added: string[]\n removed: string[]\n}\n\n/** Diffs the id sets of a generated file's before/after content. `before` undefined = new file (no removals possible). */\nexport function diffModelIds(before: string | undefined, after: string): ModelIdDiff {\n const beforeIds = before !== undefined ? extractRecordIds(before) : new Set<string>()\n const afterIds = extractRecordIds(after)\n const added = [...afterIds].filter((id) => !beforeIds.has(id)).sort()\n const removed = [...beforeIds].filter((id) => !afterIds.has(id)).sort()\n return { added, removed }\n}\n\nexport interface GeneratorChangelogEntry {\n /** The owning generator's `name` (e.g. `\"llm:openrouter\"`). */\n generator: string\n added: string[]\n removed: string[]\n}\n\n/** Renders a dated section, or `\"\"` when every entry is empty (nothing to report). */\nexport function renderChangelogSection(date: string, entries: readonly GeneratorChangelogEntry[]): string {\n const nonEmpty = entries.filter((e) => e.added.length > 0 || e.removed.length > 0)\n if (nonEmpty.length === 0) return \"\"\n const lines: string[] = [`## ${date}`, \"\"]\n for (const e of nonEmpty) {\n lines.push(`### ${e.generator}`)\n if (e.added.length > 0) lines.push(`- Added: ${e.added.join(\", \")}`)\n if (e.removed.length > 0) lines.push(`- Removed: ${e.removed.join(\", \")}`)\n lines.push(\"\")\n }\n return lines.join(\"\\n\").replace(/\\n+$/, \"\\n\")\n}\n\nexport const CATALOG_CHANGELOG_HEADER = `# Catalog Changelog\n\nHuman-readable log of model additions/removals detected by\n\\`@agentproto/catalog-sync\\` runs — NOT \\`CHANGELOG.md\\` next to it (that one is\nchangesets-driven package-version history). A dated section is appended\nautomatically here whenever a sync run adds or removes model ids; sections\nare appended at the END of the file (newest last). Pricing-only drift (no id\nadded/removed) is not logged here — see the generated files' own git history\nfor that.\n`\n\n/**\n * Appends a new dated section to `existing` changelog content.\n *\n * Returns `undefined` when there is nothing to report (no generator has an\n * added/removed id this run) — the caller should leave the file untouched\n * in that case, not write an unchanged copy.\n */\nexport function appendChangelog(\n existing: string | undefined,\n date: string,\n entries: readonly GeneratorChangelogEntry[]\n): string | undefined {\n const section = renderChangelogSection(date, entries)\n if (section === \"\") return undefined\n const base = existing !== undefined && existing.length > 0 ? existing.replace(/\\n+$/, \"\\n\") : CATALOG_CHANGELOG_HEADER\n return `${base}\\n${section}`\n}\n","/**\n * `addedAt` ledger — tracks, per generator, the ISO date each model id was\n * FIRST seen by a sync run. See `README.md` (\"addedAt convention\") for the\n * full contract; this module is the mechanism.\n *\n * Invariant: once an id has a ledger entry, that date is NEVER overwritten —\n * not even if the source's own `created` timestamp for that id later\n * resolves to something different (a provider backdating/correcting its own\n * metadata must not rewrite catalog history). New ids are backfilled from\n * the source's own creation timestamp when it has one, else stamped with\n * the current run's date.\n *\n * Ledger entries are never dropped when an id disappears from a run — the\n * ledger records \"first ever seen\", not \"currently present\". A model that\n * vanishes from a provider's list and later reappears keeps its original\n * `addedAt` rather than looking newly added.\n *\n * Each generator owns one ledger file, committed at\n * `packages/catalog-sync/ledger/<generator-id>.json` (id → ISO date, sorted\n * keys, deterministic serialization) — included as a normal entry in the\n * generator's `GeneratedFiles` return value, so `runner.ts`'s existing\n * diff/write logic covers it for free (including `--check` drift detection).\n */\n\nimport { existsSync, readFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/** The catalog-sync package directory — see `runner.ts`'s `catalogSyncDir`. */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a generator's committed addedAt ledger. */\nexport function ledgerAbsPath(ledgerId: string): string {\n return join(catalogSyncDir(), \"ledger\", `${ledgerId}.json`)\n}\n\n/** Repo-relative path for a ledger — for inclusion in a generator's `GeneratedFiles`. */\nexport function ledgerRelPath(ledgerId: string): string {\n return `packages/catalog-sync/ledger/${ledgerId}.json`\n}\n\nexport type AddedAtLedger = Record<string, string>\n\n/** Reads a committed ledger from disk. Missing/unparseable → empty ledger. */\nexport function readLedger(ledgerId: string): AddedAtLedger {\n const path = ledgerAbsPath(ledgerId)\n if (!existsSync(path)) return {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"))\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return {}\n return parsed as AddedAtLedger\n } catch {\n return {}\n }\n}\n\n/** Deterministic (sorted-key) JSON serialization, matching the `*.generated.ts` style of a trailing newline. */\nexport function serializeLedger(ledger: AddedAtLedger): string {\n const sorted: AddedAtLedger = {}\n for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id]!\n return `${JSON.stringify(sorted, null, 2)}\\n`\n}\n\n/**\n * Merge the current run's ids into a ledger.\n *\n * - id already in `previous` → keep the existing stamp verbatim (never\n * mutated), regardless of what `createdAt` says this run.\n * - id new to the ledger, source has a native creation timestamp for it →\n * backfill with `createdAt[id]`.\n * - id new to the ledger, no native timestamp → stamp with `today`.\n * - ids in `previous` but absent from `currentIds` this run are kept as-is\n * (see module doc comment — the ledger is \"first ever seen\").\n */\nexport function computeAddedAtLedger(\n currentIds: readonly string[],\n previous: AddedAtLedger,\n createdAt: Readonly<Record<string, string>>,\n today: string\n): AddedAtLedger {\n const next: AddedAtLedger = { ...previous }\n for (const id of currentIds) {\n if (next[id] !== undefined) continue\n next[id] = createdAt[id] ?? today\n }\n return next\n}\n\n/** Today's date as `YYYY-MM-DD`, in UTC. */\nexport function todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\n/** Converts a provider's Unix-seconds creation timestamp to `YYYY-MM-DD`. */\nexport function isoDateFromUnixSeconds(seconds: number): string {\n return new Date(seconds * 1000).toISOString().slice(0, 10)\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport type {\n CatalogGenerator,\n CatalogSource,\n GeneratedFiles,\n GeneratorContext,\n} from \"./types.js\"\nimport { appendChangelog, diffModelIds, type GeneratorChangelogEntry } from \"./changelog.js\"\nimport { todayIso } from \"./added-at.js\"\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * The catalog-sync package directory. Resolves the same way whether the\n * module is loaded from `src/` (vitest, type-checking) or `dist/` (built) —\n * both sit one level below the package root, so walking up one directory\n * from this file lands on `packages/catalog-sync/`.\n */\nfunction catalogSyncDir(): string {\n return resolve(__dirname, \"..\")\n}\n\n/** Absolute path to a committed/pinned source snapshot. */\nfunction snapshotPath(id: string): string {\n return join(catalogSyncDir(), \"snapshots\", `${id}.json`)\n}\n\n/**\n * The monorepo root. Resolved by walking up from the catalog-sync package\n * until `pnpm-workspace.yaml` is found. Generated-file paths in\n * {@link GeneratedFiles} are repo-relative and are resolved against this.\n */\nfunction repoRoot(): string {\n let dir = catalogSyncDir()\n for (let i = 0; i < 12; i++) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n // Fallback: catalog-sync lives at <root>/packages/catalog-sync.\n return dirname(dirname(catalogSyncDir()))\n}\n\n/** Reads an existing file as UTF-8, or returns undefined when absent. */\nfunction readIfExists(absPath: string): string | undefined {\n if (!existsSync(absPath)) return undefined\n return readFileSync(absPath, \"utf8\")\n}\n\n/**\n * Resolve `env:VAR_NAME` tokens inside header values from `process.env`.\n * Returns the materialized headers plus the names of any referenced env vars\n * that are unset — the caller uses `missing` to decide whether the source can\n * be refreshed (we never fetch an authed endpoint without its key).\n */\nfunction resolveHeaders(headers?: Record<string, string>): {\n headers: Record<string, string>\n missing: string[]\n} {\n const out: Record<string, string> = {}\n const missing: string[] = []\n if (!headers) return { headers: out, missing }\n for (const [key, value] of Object.entries(headers)) {\n out[key] = value.replace(/env:([A-Z0-9_]+)/g, (_match, name: string) => {\n const v = process.env[name]\n if (v === undefined || v === \"\") {\n missing.push(name)\n return \"\"\n }\n return v\n })\n }\n return { headers: out, missing }\n}\n\n/** Repo-relative path of the human-readable model-add/remove log (see `changelog.ts`). */\nexport const CATALOG_CHANGELOG_PATH = \"packages/model-catalog/CATALOG-CHANGELOG.md\"\n\n/** Generated files whose top-level id keys are worth diffing for the changelog. */\nfunction isChangelogEligible(relPath: string): boolean {\n if (!relPath.endsWith(\".generated.ts\")) return false\n // The catalog-sync/generated/ dir is gitignored scratch output: on every\n // fresh run (the weekly Action) the file isn't on disk, so `existing` is\n // undefined and the id diff reports the whole roster as \"Added\" — one\n // identical changelog section per sync. Only tracked outputs (the consumed\n // model-catalog artifacts) carry a meaningful before/after diff.\n if (relPath.startsWith(\"packages/catalog-sync/generated/\")) return false\n return true\n}\n\nexport interface RunGeneratorsOptions {\n /** When true, missing snapshots are fetched from their pinned `url`. */\n refresh: boolean\n /** When true, generated files are written to disk at their repo-relative path. */\n write: boolean\n}\n\nexport interface RunGeneratorsResult {\n files: GeneratedFiles\n /** Repo-relative paths whose generated content differs from what is on disk. */\n changed: string[]\n}\n\n/**\n * Runs each generator, gathering its emitted files, then (optionally) writes\n * them. `changed` is ALWAYS computed — it is the diff of generated content\n * vs disk — so `--check` (write=false) can surface drift without mutating\n * anything. Writes only happen when `write=true` AND a path differs.\n *\n * Also appends `CATALOG_CHANGELOG_PATH` (see `changelog.ts`) when this run's\n * per-generator diffs add or remove any model id — folded into the same\n * `files`/`changed` bookkeeping as every other generated file, so `--check`\n * catches an un-committed changelog entry too.\n */\nexport async function runGenerators(\n gens: CatalogGenerator[],\n opts: RunGeneratorsOptions\n): Promise<RunGeneratorsResult> {\n const files: GeneratedFiles = {}\n const pathToGenerator: Record<string, string> = {}\n\n for (const gen of gens) {\n const ctx: GeneratorContext = {\n refresh: opts.refresh,\n async fetchSource(src: CatalogSource): Promise<unknown> {\n const path = snapshotPath(src.id)\n const existing = readIfExists(path)\n\n // Offline path (default): reuse the committed snapshot; error if none.\n if (!opts.refresh) {\n if (existing !== undefined) return JSON.parse(existing)\n throw new Error(\n `catalog-sync: no committed snapshot for source \"${src.id}\" ` +\n `(${path}), and --refresh is not set. Run ` +\n `\\`catalog-sync generate --refresh\\` to fetch ${src.url}.`\n )\n }\n\n // --refresh: force a live re-fetch (do NOT short-circuit on an existing\n // snapshot — refreshing is the whole point). Auth headers may embed\n // `env:VAR` tokens; if a referenced var is unset we can't authenticate,\n // so reuse the committed snapshot rather than fetch unauthenticated.\n const { headers, missing } = resolveHeaders(src.headers)\n if (missing.length > 0) {\n if (existing !== undefined) {\n process.stderr.write(\n `catalog-sync: skipping refresh of \"${src.id}\" — missing env ` +\n `${missing.join(\", \")}; reusing committed snapshot.\\n`\n )\n return JSON.parse(existing)\n }\n throw new Error(\n `catalog-sync: cannot refresh \"${src.id}\" — missing env ` +\n `${missing.join(\", \")} and no committed snapshot.`\n )\n }\n const res = await fetch(src.url, {\n method: src.method ?? \"GET\",\n headers,\n ...(src.body !== undefined\n ? { body: JSON.stringify(src.body) }\n : {}),\n })\n if (!res.ok) {\n // A failing endpoint (e.g. xAI 403 on exhausted credits) must not\n // crash the whole multi-provider run when a committed snapshot\n // exists — degrade to offline, like the missing-env branch above.\n if (existing !== undefined) {\n process.stderr.write(\n `catalog-sync: skipping refresh of \"${src.id}\" — fetch ${src.url} ` +\n `failed: ${res.status} ${res.statusText}; reusing committed snapshot.\\n`\n )\n return JSON.parse(existing)\n }\n throw new Error(\n `catalog-sync: fetch ${src.url} failed: ${res.status} ${res.statusText}`\n )\n }\n const text = await res.text()\n const parsed: unknown = JSON.parse(text)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\\n`, \"utf8\")\n return parsed\n },\n }\n\n const out = await gen.generate(ctx)\n for (const [path, content] of Object.entries(out)) {\n files[path] = content\n pathToGenerator[path] = gen.name\n }\n }\n\n const root = repoRoot()\n const changed: string[] = []\n const changelogEntries: GeneratorChangelogEntry[] = []\n for (const [relPath, content] of Object.entries(files)) {\n const abs = join(root, relPath)\n const existing = readIfExists(abs)\n if (existing !== content) {\n changed.push(relPath)\n if (opts.write) {\n mkdirSync(dirname(abs), { recursive: true })\n writeFileSync(abs, content, \"utf8\")\n }\n }\n if (isChangelogEligible(relPath)) {\n const { added, removed } = diffModelIds(existing, content)\n if (added.length > 0 || removed.length > 0) {\n changelogEntries.push({ generator: pathToGenerator[relPath] ?? relPath, added, removed })\n }\n }\n }\n\n // CATALOG-CHANGELOG.md: appended (never touched when there's nothing to\n // report) from the per-generator id diffs gathered above. Goes through the\n // same changed/write bookkeeping as any other generated file.\n const changelogAbs = join(root, CATALOG_CHANGELOG_PATH)\n const changelogExisting = readIfExists(changelogAbs)\n const newChangelog = appendChangelog(changelogExisting, todayIso(), changelogEntries)\n if (newChangelog !== undefined) {\n files[CATALOG_CHANGELOG_PATH] = newChangelog\n changed.push(CATALOG_CHANGELOG_PATH)\n if (opts.write) {\n mkdirSync(dirname(changelogAbs), { recursive: true })\n writeFileSync(changelogAbs, newChangelog, \"utf8\")\n }\n }\n\n return { files, changed }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/catalog-sync",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "description": "Build-time generator framework for @agentproto/model-catalog. Generates *.generated.ts from pinned provider sources (LLM, image, video, audio, voice).",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -79,7 +79,7 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "zod": "^4.6.5",
82
- "@agentproto/model-catalog": "0.10.0"
82
+ "@agentproto/model-catalog": "0.10.3"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^25.9.5",