@micacms/runtime 0.5.0 → 0.6.0-rc.1

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 (87) hide show
  1. package/README.md +3 -1
  2. package/dist/app.js +1 -1
  3. package/dist/app.js.map +1 -1
  4. package/dist/config.d.ts +13 -13
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +9 -9
  7. package/dist/config.js.map +1 -1
  8. package/dist/context.js +2 -2
  9. package/dist/context.js.map +1 -1
  10. package/dist/http.d.ts +1 -3
  11. package/dist/http.d.ts.map +1 -1
  12. package/dist/http.js +2 -9
  13. package/dist/http.js.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/lib/errors.d.ts +1 -2
  19. package/dist/lib/errors.d.ts.map +1 -1
  20. package/dist/lib/errors.js +0 -2
  21. package/dist/lib/errors.js.map +1 -1
  22. package/dist/lib/id.d.ts +0 -10
  23. package/dist/lib/id.d.ts.map +1 -1
  24. package/dist/lib/id.js +0 -50
  25. package/dist/lib/id.js.map +1 -1
  26. package/dist/lib/rate-limit.d.ts +0 -2
  27. package/dist/lib/rate-limit.d.ts.map +1 -1
  28. package/dist/lib/rate-limit.js +0 -3
  29. package/dist/lib/rate-limit.js.map +1 -1
  30. package/dist/middleware/index.d.ts +1 -1
  31. package/dist/middleware/index.js +2 -2
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/routes/admin-content.d.ts.map +1 -1
  34. package/dist/routes/admin-content.js +3 -2
  35. package/dist/routes/admin-content.js.map +1 -1
  36. package/dist/routes/admin-system.js +4 -4
  37. package/dist/routes/admin-system.js.map +1 -1
  38. package/dist/schema.d.ts +6 -0
  39. package/dist/schema.d.ts.map +1 -1
  40. package/dist/schema.js +19 -1
  41. package/dist/schema.js.map +1 -1
  42. package/dist/services/changes.d.ts.map +1 -1
  43. package/dist/services/changes.js +89 -13
  44. package/dist/services/changes.js.map +1 -1
  45. package/dist/services/content-store.d.ts.map +1 -1
  46. package/dist/services/content-store.js +9 -10
  47. package/dist/services/content-store.js.map +1 -1
  48. package/dist/services/staging.d.ts.map +1 -1
  49. package/dist/services/staging.js +7 -4
  50. package/dist/services/staging.js.map +1 -1
  51. package/dist/workspace/tree.d.ts +6 -0
  52. package/dist/workspace/tree.d.ts.map +1 -1
  53. package/dist/workspace/tree.js +28 -0
  54. package/dist/workspace/tree.js.map +1 -1
  55. package/package.json +12 -4
  56. package/src/app.ts +127 -0
  57. package/src/config.ts +113 -0
  58. package/src/context.ts +167 -0
  59. package/src/http.ts +46 -0
  60. package/src/index.ts +141 -0
  61. package/src/lib/errors.ts +84 -0
  62. package/src/lib/file-type.ts +108 -0
  63. package/src/lib/id.ts +6 -0
  64. package/src/lib/logger.ts +87 -0
  65. package/src/lib/ports.ts +17 -0
  66. package/src/lib/rate-limit.ts +72 -0
  67. package/src/lib/schema-stats.ts +183 -0
  68. package/src/lib/version.ts +29 -0
  69. package/src/middleware/index.ts +234 -0
  70. package/src/routes/admin-content.ts +197 -0
  71. package/src/routes/admin-media.ts +96 -0
  72. package/src/routes/admin-spa.ts +82 -0
  73. package/src/routes/admin-staging.ts +126 -0
  74. package/src/routes/admin-system.ts +146 -0
  75. package/src/routes/health.ts +67 -0
  76. package/src/schema.ts +217 -0
  77. package/src/server.ts +68 -0
  78. package/src/services/changes.ts +605 -0
  79. package/src/services/content-store.ts +492 -0
  80. package/src/services/media.ts +229 -0
  81. package/src/services/staging.ts +330 -0
  82. package/src/workspace/developer.ts +46 -0
  83. package/src/workspace/files.ts +243 -0
  84. package/src/workspace/git.ts +108 -0
  85. package/src/workspace/locks.ts +103 -0
  86. package/src/workspace/policy.ts +77 -0
  87. package/src/workspace/tree.ts +314 -0
@@ -14,14 +14,15 @@ import { ApiError, badRequest, notFound } from "../lib/errors.js";
14
14
  import { developerPolicy } from "../workspace/developer.js";
15
15
  import { GitError } from "../workspace/git.js";
16
16
  import { createWorkspaceLocks } from "../workspace/locks.js";
17
- import { aheadOfUpstream, commitPaths, currentBranch, discardChange, headRevision, headSource, listTreeChanges, locateTree, } from "../workspace/tree.js";
17
+ import { aheadOfUpstream, commitPaths, currentBranch, discardChange, headRevision, headSource, overlaySource, listTreeChanges, locateTree, } from "../workspace/tree.js";
18
18
  import { analyseChanges, compareSources, describeProblems, flattenProblems, } from "./changes.js";
19
19
  export function createWorkingTreeStagingService(deps) {
20
20
  const { model, files, contentRoot, logger } = deps;
21
21
  const policy = deps.policy ?? developerPolicy({ publishEnabled: deps.publishEnabled, logger });
22
22
  const locks = deps.locks ?? createWorkspaceLocks();
23
- const analyse = async (tree, treeChanges) => {
24
- const { changes, validation } = await analyseChanges(model, files, treeChanges, headSource(tree));
23
+ /** `source` is what the result holds: the whole working tree, or HEAD with a selection. */
24
+ const analyse = async (tree, treeChanges, source = files) => {
25
+ const { changes, validation } = await analyseChanges(model, source, treeChanges, headSource(tree));
25
26
  // The field validator cannot see a merge conflict: the file holds Git's conflict format.
26
27
  for (const change of treeChanges) {
27
28
  if (!change.conflicted)
@@ -149,7 +150,9 @@ export function createWorkingTreeStagingService(deps) {
149
150
  "the new model, then review the pending changes and publish again.");
150
151
  }
151
152
  const selected = (await listTreeChanges(tree)).filter((change) => change.path in input.paths);
152
- const { validation } = await analyse(tree, selected);
153
+ // Checked against what the commit will hold: drafts left behind are not on the site.
154
+ const result = overlaySource(headSource(tree), files, selected.map((change) => change.path));
155
+ const { validation } = await analyse(tree, selected, result);
153
156
  if (!validation.ok) {
154
157
  throw new ApiError("validation_failed", describeProblems(validation.problems), {
155
158
  issues: flattenProblems(validation.problems),
@@ -1 +1 @@
1
- {"version":3,"file":"staging.js","sourceRoot":"","sources":["../../src/services/staging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The staging service on a Git working tree. Autosave has already written the\n * files; Publish commits the reviewed paths.\n *\n * - Every Git operation is limited to an explicit path set, so other work in\n * the same checkout is never swept in.\n * - Only the reviewed files are published, as the editor saw them. Drafts of\n * other entries stay drafts.\n * - When the push fails, the commit stays as `committed-pending-push`; a\n * retry pushes and never commits again.\n */\n\nimport { MANIFEST_PATH, memorySource, type PublishResult, type Site } from \"@micacms/core\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport type { Logger } from \"../lib/logger.js\";\nimport { developerPolicy } from \"../workspace/developer.js\";\nimport type { ContentFiles } from \"../workspace/files.js\";\nimport { GitError } from \"../workspace/git.js\";\nimport { createWorkspaceLocks, type WorkspaceLocks } from \"../workspace/locks.js\";\nimport type { OwnerPolicy } from \"../workspace/policy.js\";\nimport {\n aheadOfUpstream,\n commitPaths,\n currentBranch,\n discardChange,\n headRevision,\n headSource,\n listTreeChanges,\n locateTree,\n type GitTree,\n type TreeChange,\n} from \"../workspace/tree.js\";\nimport {\n analyseChanges,\n compareSources,\n describeProblems,\n flattenProblems,\n type AnnotatedChange,\n type CompareTarget,\n type Comparison,\n type ContentStagingService,\n type PublishInput,\n type RepositoryInfo,\n type StagingStatus,\n type StagingValidation,\n} from \"./changes.js\";\n\nexport interface WorkingTreeStagingDeps {\n model: Site;\n /**\n * The working tree's files without the lock wrapper: this service holds the\n * locks around whole operations.\n */\n files: ContentFiles;\n contentRoot: string;\n schemaRevision: string | null;\n /** From `readOnlyParts`: a change to one of these is never published by this build. */\n readOnly?: ReadonlyMap<string, string>;\n /** `MICA_LOCAL_PUBLISH`, for the default developer policy. */\n publishEnabled: boolean;\n logger: Logger;\n /** Defaults to the developer. */\n policy?: OwnerPolicy;\n /** Shared with the services that write content. */\n locks?: WorkspaceLocks;\n /**\n * Set by a managed clone, whose Git runs in an isolated environment. Without\n * it, the developer's Git finds the tree.\n */\n tree?: GitTree;\n}\n\nexport function createWorkingTreeStagingService(\n deps: WorkingTreeStagingDeps,\n): ContentStagingService {\n const { model, files, contentRoot, logger } = deps;\n const policy = deps.policy ?? developerPolicy({ publishEnabled: deps.publishEnabled, logger });\n const locks = deps.locks ?? createWorkspaceLocks();\n\n const analyse = async (tree: GitTree, treeChanges: TreeChange[]) => {\n const { changes, validation } = await analyseChanges(\n model,\n files,\n treeChanges,\n headSource(tree),\n );\n\n // The field validator cannot see a merge conflict: the file holds Git's conflict format.\n for (const change of treeChanges) {\n if (!change.conflicted) continue;\n validation.problems.push({\n path: change.path,\n label: change.path,\n issues: [\n { path: \"\", message: \"This file has a Git merge conflict. Resolve it with your Git tools.\" },\n ],\n });\n }\n // By the first path segment, so that a stray file in a read-only collection is caught too.\n for (const change of changes) {\n const head = change.path.split(\"/\")[0] ?? \"\";\n const reason = deps.readOnly?.get(`collections.${head}`) ?? deps.readOnly?.get(`singletons.${head.replace(/\\.json$/, \"\")}`);\n if (reason) {\n validation.problems.push({ path: change.path, label: change.owner.label, issues: [{ path: \"\", message: reason }] });\n }\n }\n validation.ok = validation.problems.length === 0;\n\n return { changes, validation };\n };\n\n const locate = async (): Promise<GitTree | { reason: string }> => deps.tree ?? locateTree(contentRoot);\n\n const located = async (): Promise<GitTree> => {\n const tree = await locate();\n if (\"reason\" in tree) throw badRequest(tree.reason);\n return tree;\n };\n\n const publishable = async (): Promise<GitTree> => {\n const availability = policy.publishAvailability();\n if (!availability.enabled) throw badRequest(availability.reason);\n return located();\n };\n\n /** The key that keeps an entry's files together: a publication never splits one. */\n const groupOf = (change: AnnotatedChange): string => {\n const owner = change.owner;\n if (owner.kind === \"entry\") return `entry:${owner.collection}/${owner.slug}`;\n if (owner.kind === \"singleton\") return `singleton:${owner.name}`;\n if (owner.kind === \"order\") return `order:${owner.collection}`;\n return `file:${change.path}`;\n };\n\n const withRevisions = async (changes: AnnotatedChange[]): Promise<AnnotatedChange[]> =>\n Promise.all(\n changes.map(async (change) => ({\n ...change,\n revision: change.kind === \"delete\" ? null : await files.revision(change.path),\n })),\n );\n\n /** Refused for the developer's checkout: Mica never fetches or merges there. */\n const managedOnly = <T>(action: T | undefined): T => {\n if (action === undefined) throw notFound(\"This is the developer's checkout; Git there is theirs.\");\n return action;\n };\n\n return {\n async status(): Promise<StagingStatus> {\n const tree = await locate();\n const sync = policy.syncStatus?.() ?? null;\n\n if (\"reason\" in tree) {\n return {\n owner: policy.owner,\n publish: { enabled: false, reason: tree.reason },\n head: null,\n branch: null,\n ahead: null,\n changes: [],\n validation: emptyValidation(),\n sync,\n };\n }\n\n const { changes, validation, head, branch } = await locks.tree.read(async () => {\n const analysed = await analyse(tree, await listTreeChanges(tree));\n return {\n ...analysed,\n changes: await withRevisions(analysed.changes),\n head: await headRevision(tree),\n branch: await currentBranch(tree),\n };\n });\n\n return {\n owner: policy.owner,\n publish: policy.publishAvailability(),\n head,\n branch,\n ahead: await aheadOfUpstream(tree),\n changes,\n validation,\n sync,\n };\n },\n\n async publish(input: PublishInput): Promise<PublishResult> {\n const message = input.message.trim();\n if (!message) throw badRequest(\"A publication needs a commit message.\");\n const reviewed = Object.keys(input.paths);\n if (reviewed.length === 0) throw badRequest(\"Choose at least one change to publish.\");\n\n const tree = await publishable();\n\n // One publication at a time from sync to push: no sync can run in\n // between, and no second commit can stack up.\n return locks.remote.run(async () => {\n await policy.prepare(tree);\n\n const revision = await locks.tree.write(async () => {\n const pending = await withRevisions((await analyse(tree, await listTreeChanges(tree))).changes);\n const byPath = new Map(pending.map((change) => [change.path, change]));\n\n // The review gate refuses a file saved again since the review, a\n // reviewed file that is no longer pending, or another file of a\n // reviewed entry. Drafts of other entries never block a publication.\n const reviewedGroups = new Set(\n reviewed.map((path) => byPath.get(path)).flatMap((change) => (change ? [groupOf(change)] : [])),\n );\n const stale = [\n ...reviewed.filter((path) => byPath.get(path)?.revision !== input.paths[path] || !byPath.has(path)),\n ...pending\n .filter((change) => reviewedGroups.has(groupOf(change)) && !(change.path in input.paths))\n .map((change) => change.path),\n ];\n if (stale.length > 0) {\n throw new ApiError(\n \"conflict\",\n \"Some of these changes were edited after you reviewed them. Review them again, then publish.\",\n );\n }\n\n // The model drift gate: validation used the model that this process started with.\n if ((await files.revision(MANIFEST_PATH)) !== deps.schemaRevision) {\n throw new ApiError(\n \"conflict\",\n \"The content model changed after this server started. Restart Mica to load \" +\n \"the new model, then review the pending changes and publish again.\",\n );\n }\n\n const selected = (await listTreeChanges(tree)).filter((change) => change.path in input.paths);\n const { validation } = await analyse(tree, selected);\n if (!validation.ok) {\n throw new ApiError(\"validation_failed\", describeProblems(validation.problems), {\n issues: flattenProblems(validation.problems),\n });\n }\n\n try {\n const revision = await commitPaths(tree, reviewed, message, policy.commitAuthor(input.author));\n logger.info(\"Published\", { revision, files: reviewed.length, owner: policy.owner });\n return revision;\n } catch (error) {\n if (error instanceof GitError) {\n // Git's message is the actionable part: no identity, a hook that\n // refused, a rebase in progress.\n throw new ApiError(\"conflict\", `Git refused the commit: ${error.message}`);\n }\n throw error;\n }\n });\n\n return policy.push(tree, revision);\n });\n },\n\n async push(): Promise<PublishResult> {\n const tree = await publishable();\n\n return locks.remote.run(async () => {\n const revision = await headRevision(tree);\n if (revision === null) throw badRequest(\"There is no commit to push yet.\");\n return policy.push(tree, revision);\n });\n },\n\n async discard(requested: string[]): Promise<{ discarded: string[] }> {\n const tree = await located();\n\n return locks.tree.write(async () => {\n // Discard is destructive, so it reaches only paths that the change list names.\n const current = new Map((await listTreeChanges(tree)).map((change) => [change.path, change]));\n\n const discarded: string[] = [];\n for (const path of requested) {\n const change = current.get(path);\n if (!change) continue;\n await discardChange(tree, change);\n discarded.push(path);\n }\n\n return { discarded };\n });\n },\n\n async compare(target: CompareTarget, against: \"head\" | \"origin\" = \"head\"): Promise<Comparison> {\n const tree = await locate();\n if (against === \"origin\" && !policy.upstreamRef) throw badRequest(\"There is no website version to compare with here.\");\n // Without a repository nothing is published, so \"before\" is an empty source.\n const published =\n \"reason\" in tree\n ? memorySource({}, \"unpublished\")\n : headSource(tree, against === \"origin\" ? policy.upstreamRef : \"HEAD\");\n return locks.tree.read(() => compareSources(model, published, files, target));\n },\n\n async sync(): Promise<void> {\n await managedOnly(policy.sync)();\n },\n\n async resolve(group: string, choice: \"mine\" | \"website\"): Promise<void> {\n await managedOnly(policy.resolve)(group, choice);\n },\n\n async repository(): Promise<RepositoryInfo> {\n const tree = await locate();\n const described = (await policy.describe?.()) ?? null;\n return {\n owner: policy.owner,\n branch: \"reason\" in tree ? null : await currentBranch(tree),\n head: \"reason\" in tree ? null : await headRevision(tree),\n url: described?.url ?? null,\n cloneBytes: described?.cloneBytes ?? null,\n sync: policy.syncStatus?.() ?? null,\n };\n },\n };\n}\n\nfunction emptyValidation(): StagingValidation {\n return { ok: true, problems: [] };\n}\n"]}
1
+ {"version":3,"file":"staging.js","sourceRoot":"","sources":["../../src/services/staging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The staging service on a Git working tree. Autosave has already written the\n * files; Publish commits the reviewed paths.\n *\n * - Every Git operation is limited to an explicit path set, so other work in\n * the same checkout is never swept in.\n * - Only the reviewed files are published, as the editor saw them. Drafts of\n * other entries stay drafts.\n * - When the push fails, the commit stays as `committed-pending-push`; a\n * retry pushes and never commits again.\n */\n\nimport { MANIFEST_PATH, memorySource, type ContentSource, type PublishResult, type Site } from \"@micacms/core\";\n\nimport { ApiError, badRequest, notFound } from \"../lib/errors.js\";\nimport type { Logger } from \"../lib/logger.js\";\nimport { developerPolicy } from \"../workspace/developer.js\";\nimport type { ContentFiles } from \"../workspace/files.js\";\nimport { GitError } from \"../workspace/git.js\";\nimport { createWorkspaceLocks, type WorkspaceLocks } from \"../workspace/locks.js\";\nimport type { OwnerPolicy } from \"../workspace/policy.js\";\nimport {\n aheadOfUpstream,\n commitPaths,\n currentBranch,\n discardChange,\n headRevision,\n headSource,\n overlaySource,\n listTreeChanges,\n locateTree,\n type GitTree,\n type TreeChange,\n} from \"../workspace/tree.js\";\nimport {\n analyseChanges,\n compareSources,\n describeProblems,\n flattenProblems,\n type AnnotatedChange,\n type CompareTarget,\n type Comparison,\n type ContentStagingService,\n type PublishInput,\n type RepositoryInfo,\n type StagingStatus,\n type StagingValidation,\n} from \"./changes.js\";\n\nexport interface WorkingTreeStagingDeps {\n model: Site;\n /**\n * The working tree's files without the lock wrapper: this service holds the\n * locks around whole operations.\n */\n files: ContentFiles;\n contentRoot: string;\n schemaRevision: string | null;\n /** From `readOnlyParts`: a change to one of these is never published by this build. */\n readOnly?: ReadonlyMap<string, string>;\n /** `MICA_LOCAL_PUBLISH`, for the default developer policy. */\n publishEnabled: boolean;\n logger: Logger;\n /** Defaults to the developer. */\n policy?: OwnerPolicy;\n /** Shared with the services that write content. */\n locks?: WorkspaceLocks;\n /**\n * Set by a managed clone, whose Git runs in an isolated environment. Without\n * it, the developer's Git finds the tree.\n */\n tree?: GitTree;\n}\n\nexport function createWorkingTreeStagingService(\n deps: WorkingTreeStagingDeps,\n): ContentStagingService {\n const { model, files, contentRoot, logger } = deps;\n const policy = deps.policy ?? developerPolicy({ publishEnabled: deps.publishEnabled, logger });\n const locks = deps.locks ?? createWorkspaceLocks();\n\n /** `source` is what the result holds: the whole working tree, or HEAD with a selection. */\n const analyse = async (tree: GitTree, treeChanges: TreeChange[], source: ContentSource = files) => {\n const { changes, validation } = await analyseChanges(\n model,\n source,\n treeChanges,\n headSource(tree),\n );\n\n // The field validator cannot see a merge conflict: the file holds Git's conflict format.\n for (const change of treeChanges) {\n if (!change.conflicted) continue;\n validation.problems.push({\n path: change.path,\n label: change.path,\n issues: [\n { path: \"\", message: \"This file has a Git merge conflict. Resolve it with your Git tools.\" },\n ],\n });\n }\n // By the first path segment, so that a stray file in a read-only collection is caught too.\n for (const change of changes) {\n const head = change.path.split(\"/\")[0] ?? \"\";\n const reason = deps.readOnly?.get(`collections.${head}`) ?? deps.readOnly?.get(`singletons.${head.replace(/\\.json$/, \"\")}`);\n if (reason) {\n validation.problems.push({ path: change.path, label: change.owner.label, issues: [{ path: \"\", message: reason }] });\n }\n }\n validation.ok = validation.problems.length === 0;\n\n return { changes, validation };\n };\n\n const locate = async (): Promise<GitTree | { reason: string }> => deps.tree ?? locateTree(contentRoot);\n\n const located = async (): Promise<GitTree> => {\n const tree = await locate();\n if (\"reason\" in tree) throw badRequest(tree.reason);\n return tree;\n };\n\n const publishable = async (): Promise<GitTree> => {\n const availability = policy.publishAvailability();\n if (!availability.enabled) throw badRequest(availability.reason);\n return located();\n };\n\n /** The key that keeps an entry's files together: a publication never splits one. */\n const groupOf = (change: AnnotatedChange): string => {\n const owner = change.owner;\n if (owner.kind === \"entry\") return `entry:${owner.collection}/${owner.slug}`;\n if (owner.kind === \"singleton\") return `singleton:${owner.name}`;\n if (owner.kind === \"order\") return `order:${owner.collection}`;\n return `file:${change.path}`;\n };\n\n const withRevisions = async (changes: AnnotatedChange[]): Promise<AnnotatedChange[]> =>\n Promise.all(\n changes.map(async (change) => ({\n ...change,\n revision: change.kind === \"delete\" ? null : await files.revision(change.path),\n })),\n );\n\n /** Refused for the developer's checkout: Mica never fetches or merges there. */\n const managedOnly = <T>(action: T | undefined): T => {\n if (action === undefined) throw notFound(\"This is the developer's checkout; Git there is theirs.\");\n return action;\n };\n\n return {\n async status(): Promise<StagingStatus> {\n const tree = await locate();\n const sync = policy.syncStatus?.() ?? null;\n\n if (\"reason\" in tree) {\n return {\n owner: policy.owner,\n publish: { enabled: false, reason: tree.reason },\n head: null,\n branch: null,\n ahead: null,\n changes: [],\n validation: emptyValidation(),\n sync,\n };\n }\n\n const { changes, validation, head, branch } = await locks.tree.read(async () => {\n const analysed = await analyse(tree, await listTreeChanges(tree));\n return {\n ...analysed,\n changes: await withRevisions(analysed.changes),\n head: await headRevision(tree),\n branch: await currentBranch(tree),\n };\n });\n\n return {\n owner: policy.owner,\n publish: policy.publishAvailability(),\n head,\n branch,\n ahead: await aheadOfUpstream(tree),\n changes,\n validation,\n sync,\n };\n },\n\n async publish(input: PublishInput): Promise<PublishResult> {\n const message = input.message.trim();\n if (!message) throw badRequest(\"A publication needs a commit message.\");\n const reviewed = Object.keys(input.paths);\n if (reviewed.length === 0) throw badRequest(\"Choose at least one change to publish.\");\n\n const tree = await publishable();\n\n // One publication at a time from sync to push: no sync can run in\n // between, and no second commit can stack up.\n return locks.remote.run(async () => {\n await policy.prepare(tree);\n\n const revision = await locks.tree.write(async () => {\n const pending = await withRevisions((await analyse(tree, await listTreeChanges(tree))).changes);\n const byPath = new Map(pending.map((change) => [change.path, change]));\n\n // The review gate refuses a file saved again since the review, a\n // reviewed file that is no longer pending, or another file of a\n // reviewed entry. Drafts of other entries never block a publication.\n const reviewedGroups = new Set(\n reviewed.map((path) => byPath.get(path)).flatMap((change) => (change ? [groupOf(change)] : [])),\n );\n const stale = [\n ...reviewed.filter((path) => byPath.get(path)?.revision !== input.paths[path] || !byPath.has(path)),\n ...pending\n .filter((change) => reviewedGroups.has(groupOf(change)) && !(change.path in input.paths))\n .map((change) => change.path),\n ];\n if (stale.length > 0) {\n throw new ApiError(\n \"conflict\",\n \"Some of these changes were edited after you reviewed them. Review them again, then publish.\",\n );\n }\n\n // The model drift gate: validation used the model that this process started with.\n if ((await files.revision(MANIFEST_PATH)) !== deps.schemaRevision) {\n throw new ApiError(\n \"conflict\",\n \"The content model changed after this server started. Restart Mica to load \" +\n \"the new model, then review the pending changes and publish again.\",\n );\n }\n\n const selected = (await listTreeChanges(tree)).filter((change) => change.path in input.paths);\n // Checked against what the commit will hold: drafts left behind are not on the site.\n const result = overlaySource(headSource(tree), files, selected.map((change) => change.path));\n const { validation } = await analyse(tree, selected, result);\n if (!validation.ok) {\n throw new ApiError(\"validation_failed\", describeProblems(validation.problems), {\n issues: flattenProblems(validation.problems),\n });\n }\n\n try {\n const revision = await commitPaths(tree, reviewed, message, policy.commitAuthor(input.author));\n logger.info(\"Published\", { revision, files: reviewed.length, owner: policy.owner });\n return revision;\n } catch (error) {\n if (error instanceof GitError) {\n // Git's message is the actionable part: no identity, a hook that\n // refused, a rebase in progress.\n throw new ApiError(\"conflict\", `Git refused the commit: ${error.message}`);\n }\n throw error;\n }\n });\n\n return policy.push(tree, revision);\n });\n },\n\n async push(): Promise<PublishResult> {\n const tree = await publishable();\n\n return locks.remote.run(async () => {\n const revision = await headRevision(tree);\n if (revision === null) throw badRequest(\"There is no commit to push yet.\");\n return policy.push(tree, revision);\n });\n },\n\n async discard(requested: string[]): Promise<{ discarded: string[] }> {\n const tree = await located();\n\n return locks.tree.write(async () => {\n // Discard is destructive, so it reaches only paths that the change list names.\n const current = new Map((await listTreeChanges(tree)).map((change) => [change.path, change]));\n\n const discarded: string[] = [];\n for (const path of requested) {\n const change = current.get(path);\n if (!change) continue;\n await discardChange(tree, change);\n discarded.push(path);\n }\n\n return { discarded };\n });\n },\n\n async compare(target: CompareTarget, against: \"head\" | \"origin\" = \"head\"): Promise<Comparison> {\n const tree = await locate();\n if (against === \"origin\" && !policy.upstreamRef) throw badRequest(\"There is no website version to compare with here.\");\n // Without a repository nothing is published, so \"before\" is an empty source.\n const published =\n \"reason\" in tree\n ? memorySource({}, \"unpublished\")\n : headSource(tree, against === \"origin\" ? policy.upstreamRef : \"HEAD\");\n return locks.tree.read(() => compareSources(model, published, files, target));\n },\n\n async sync(): Promise<void> {\n await managedOnly(policy.sync)();\n },\n\n async resolve(group: string, choice: \"mine\" | \"website\"): Promise<void> {\n await managedOnly(policy.resolve)(group, choice);\n },\n\n async repository(): Promise<RepositoryInfo> {\n const tree = await locate();\n const described = (await policy.describe?.()) ?? null;\n return {\n owner: policy.owner,\n branch: \"reason\" in tree ? null : await currentBranch(tree),\n head: \"reason\" in tree ? null : await headRevision(tree),\n url: described?.url ?? null,\n cloneBytes: described?.cloneBytes ?? null,\n sync: policy.syncStatus?.() ?? null,\n };\n },\n };\n}\n\nfunction emptyValidation(): StagingValidation {\n return { ok: true, problems: [] };\n}\n"]}
@@ -61,6 +61,12 @@ export declare function aheadOfUpstream(tree: GitTree): Promise<number | null>;
61
61
  * working tree holds the drafts.
62
62
  */
63
63
  export declare function headSource(tree: GitTree, ref?: string): ContentSource;
64
+ /**
65
+ * `base` with `paths` taken from `top`: what a commit of exactly those paths
66
+ * would hold. Publish checks a selection against it, not against drafts that
67
+ * stay behind.
68
+ */
69
+ export declare function overlaySource(base: ContentSource, top: ContentSource, paths: Iterable<string>): ContentSource;
64
70
  /**
65
71
  * Parses `git status --porcelain=v1 -z` into content-root-relative changes.
66
72
  * `-z` never changes a path; a rename is two fields and becomes a create and a
@@ -1 +1 @@
1
- {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAkB,MAAM,eAAe,CAAC;AAGlF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAE7C,8CAA8C;AAC9C,MAAM,WAAW,OAAO;IACtB,mCAAmC;IACnC,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBvC;AAED,6DAA6D;AAC7D,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ1E;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIrG;AAsBD,sFAAsF;AACtF,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpF;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAElE;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnE;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAK3E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,CAyBrE;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAsDrE;AAQD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D"}
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAkB,MAAM,eAAe,CAAC;AAGlF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAE7C,8CAA8C;AAC9C,MAAM,WAAW,OAAO;IACtB,mCAAmC;IACnC,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBvC;AAED,6DAA6D;AAC7D,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ1E;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIrG;AAsBD,sFAAsF;AACtF,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpF;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAElE;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnE;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAK3E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,CAyBrE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,aAAa,CAqB7G;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAsDrE;AAQD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -149,6 +149,34 @@ export function headSource(tree, ref = "HEAD") {
149
149
  },
150
150
  };
151
151
  }
152
+ /**
153
+ * `base` with `paths` taken from `top`: what a commit of exactly those paths
154
+ * would hold. Publish checks a selection against it, not against drafts that
155
+ * stay behind.
156
+ */
157
+ export function overlaySource(base, top, paths) {
158
+ const selected = new Set(paths);
159
+ return {
160
+ origin: `${base.origin} with the selected changes`,
161
+ read: (path) => (selected.has(path) ? top.read(path) : base.read(path)),
162
+ async list(directory) {
163
+ const prefix = directory === "" ? "" : `${directory}/`;
164
+ const items = new Map((await base.list(directory)).map((item) => [item.name, item]));
165
+ for (const path of selected) {
166
+ if (!path.startsWith(prefix))
167
+ continue;
168
+ const [name, ...deeper] = path.slice(prefix.length).split("/");
169
+ if (deeper.length > 0)
170
+ items.set(name, { name: name, type: "directory" });
171
+ else if ((await top.read(path)) === null)
172
+ items.delete(name);
173
+ else
174
+ items.set(name, { name: name, type: "file" });
175
+ }
176
+ return [...items.values()];
177
+ },
178
+ };
179
+ }
152
180
  /**
153
181
  * A save's temporary file (`*.mica-<pid>-<n>`), seen during a save or after a
154
182
  * crash. Never content.
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git operations on a working tree whose content root is the draft workspace.\n * The owner policy does the push. Every operation is limited to paths under the\n * content root, never `git add -A` over the repository, so other work in the\n * same checkout is never swept into a commit.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { realpath, rm } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\n\nimport type { ContentChange, ContentSource, DirectoryEntry } from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\n\nimport type { Author } from \"./files.js\";\nimport { GitError, gitRunner, type GitRunner, type GitRunnerOptions } from \"./git.js\";\n\n/**\n * Makes every path argument literal: `tour[1].json` is not a pattern that also\n * matches `tour1.json`.\n */\nexport const LITERAL = \"--literal-pathspecs\";\n\n/** A content root inside a Git repository. */\nexport interface GitTree {\n /** Runs in the repository root. */\n git: GitRunner;\n /** Content root relative to the repository root; \"\" when they coincide. */\n prefix: string;\n /** Absolute path of the content root. */\n contentRoot: string;\n}\n\n/** A changed file plus what only Git status knows about it. */\nexport interface TreeChange extends ContentChange {\n /** Not yet known to Git — discarding means deleting, not restoring. */\n untracked: boolean;\n /** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */\n conflicted: boolean;\n}\n\n/**\n * Finds the repository the content root sits in, freshly each time: a\n * `git init` after startup should start working, not require a restart.\n */\nexport async function locateTree(\n contentRoot: string,\n options: GitRunnerOptions = {},\n): Promise<GitTree | { reason: string }> {\n const probe = gitRunner(contentRoot, options);\n try {\n const repoRoot = await probe.run([\"rev-parse\", \"--show-toplevel\"]);\n // Git resolves symlinks in the toplevel (`/private/var` for `/var` on\n // macOS); do the same, or the prefix is wrong.\n const resolved = await realpath(contentRoot);\n return {\n git: gitRunner(repoRoot, options),\n prefix: toPosix(relative(repoRoot, resolved)),\n contentRoot,\n };\n } catch (error) {\n return {\n reason:\n error instanceof GitError && error.exitCode === null\n ? error.message\n : \"The content directory is not inside a Git repository.\",\n };\n }\n}\n\n/** The content root's changes, as `git status` sees them. */\nexport async function listTreeChanges(tree: GitTree): Promise<TreeChange[]> {\n // `-uall`: Git otherwise collapses a new, untracked entry directory to one line.\n // `GIT_OPTIONAL_LOCKS=0`: no `index.lock`, so a parallel commit cannot fail on it.\n const raw = await tree.git.run(\n [LITERAL, \"status\", \"--porcelain=v1\", \"-z\", \"-uall\", \"--\", tree.prefix === \"\" ? \".\" : tree.prefix],\n { env: { GIT_OPTIONAL_LOCKS: \"0\" } },\n );\n return parseStatus(raw, tree.prefix);\n}\n\n/**\n * Stages and commits only `paths` (content-root relative) and returns the new\n * HEAD. The pathspec limits both steps, so all other staged or unstaged work\n * stays. Without `author`, Git's configured identity applies.\n */\nexport async function commitPaths(\n tree: GitTree,\n paths: string[],\n message: string,\n author?: Author,\n): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n const as = author ? [`--author=${author.name} <${author.email}>`] : [];\n await tree.git.run([LITERAL, \"commit\", \"-m\", message, ...as, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Commits `paths` again with the message, author and date of `original`, after\n * a rejected push. Returns the new HEAD.\n */\nexport async function recommitPaths(tree: GitTree, paths: string[], original: string): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n await tree.git.run([LITERAL, \"commit\", \"-C\", original, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Stages `paths`, because `git commit -- <paths>` refuses a file Git has never\n * seen. A path gone from the disk uses `rm --cached`, because `git add` refuses\n * a deletion that is already staged (as `reset --soft` leaves it).\n */\nasync function stagePaths(tree: GitTree, paths: string[]): Promise<string[]> {\n const present: string[] = [];\n const gone: string[] = [];\n for (const path of paths) {\n (existsSync(within(tree.contentRoot, path)) ? present : gone).push(inRepo(tree.prefix, path));\n }\n for (const batch of chunked(present, 500)) {\n await tree.git.run([LITERAL, \"add\", \"-A\", \"--\", ...batch]);\n }\n for (const batch of chunked(gone, 500)) {\n await tree.git.run([LITERAL, \"rm\", \"--cached\", \"--ignore-unmatch\", \"--quiet\", \"--\", ...batch]);\n }\n return paths.map((path) => inRepo(tree.prefix, path));\n}\n\n/** Puts one changed file back to its HEAD version, or removes it if HEAD has none. */\nexport async function discardChange(tree: GitTree, change: TreeChange): Promise<void> {\n if (change.kind === \"create\") {\n // Unstage a staged creation first, so the index does not point at a deleted file.\n if (!change.untracked) {\n await tree.git.tryRun([LITERAL, \"rm\", \"--cached\", \"--force\", \"--\", inRepo(tree.prefix, change.path)]);\n }\n await rm(within(tree.contentRoot, change.path), { force: true });\n return;\n }\n await tree.git.run([\n LITERAL,\n \"restore\",\n \"--source=HEAD\",\n \"--staged\",\n \"--worktree\",\n \"--\",\n inRepo(tree.prefix, change.path),\n ]);\n}\n\n/** HEAD's commit id, or null before the first commit. */\nexport function headRevision(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"HEAD\"]);\n}\n\n/** The checked-out branch, or null when there is none to name. */\nexport function currentBranch(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n}\n\n/** Commits waiting to be pushed, or null when there is no upstream to say. */\nexport async function aheadOfUpstream(tree: GitTree): Promise<number | null> {\n const counted = await tree.git.tryRun([\"rev-list\", \"--count\", \"@{upstream}..HEAD\"]);\n if (counted === null) return null;\n const ahead = Number(counted);\n return Number.isFinite(ahead) ? ahead : null;\n}\n\n/**\n * The content root as `ref` has it, read from Git's object store, because the\n * working tree holds the drafts.\n */\nexport function headSource(tree: GitTree, ref = \"HEAD\"): ContentSource {\n const at = (path: string) => `${ref}:${path === \"\" ? tree.prefix : inRepo(tree.prefix, path)}`;\n\n return {\n origin: ref,\n\n async read(path) {\n // \"Not in HEAD\" and \"no commits yet\" both mean that there is no published version.\n return tree.git.tryRun([\"show\", at(path)]);\n },\n\n async list(path) {\n const raw = await tree.git.tryRun([\"ls-tree\", \"-z\", at(path)]);\n if (raw === null) return [];\n\n const entries: DirectoryEntry[] = [];\n for (const field of raw.split(\"\\0\")) {\n const tab = field.indexOf(\"\\t\");\n if (tab === -1) continue;\n const type = field.slice(0, tab).split(\" \")[1];\n entries.push({ name: field.slice(tab + 1), type: type === \"tree\" ? \"directory\" : \"file\" });\n }\n return entries;\n },\n };\n}\n\n/**\n * A save's temporary file (`*.mica-<pid>-<n>`), seen during a save or after a\n * crash. Never content.\n */\nconst TEMPORARY_FILE = /\\.mica-\\d+-\\d+$/;\n\n/**\n * Parses `git status --porcelain=v1 -z` into content-root-relative changes.\n * `-z` never changes a path; a rename is two fields and becomes a create and a\n * delete. `.mica/` is excluded: the developer commits the manifest, and the\n * drift check guards model changes.\n */\nexport function parseStatus(raw: string, prefix: string): TreeChange[] {\n const fields = raw.split(\"\\0\").filter((field) => field !== \"\");\n const changes: TreeChange[] = [];\n\n const push = (\n repoPath: string,\n kind: TreeChange[\"kind\"],\n flags: { untracked?: boolean; conflicted?: boolean } = {},\n ) => {\n const path = relativeToContent(repoPath, prefix);\n if (path === null || path === \".mica\" || path.startsWith(\".mica/\")) return;\n if (TEMPORARY_FILE.test(path)) return;\n changes.push({\n path,\n kind,\n untracked: flags.untracked ?? false,\n conflicted: flags.conflicted ?? false,\n });\n };\n\n for (let at = 0; at < fields.length; at += 1) {\n const field = fields[at]!;\n const x = field[0] ?? \" \";\n const y = field[1] ?? \" \";\n const repoPath = field.slice(3);\n\n if (x === \"?\" && y === \"?\") {\n push(repoPath, \"create\", { untracked: true });\n continue;\n }\n\n // A merge conflict state, whatever the tree holds.\n if (x === \"U\" || y === \"U\" || (x === \"A\" && y === \"A\") || (x === \"D\" && y === \"D\")) {\n push(repoPath, \"update\", { conflicted: true });\n continue;\n }\n\n if (x === \"R\" || x === \"C\") {\n // The next field is the source path; only a rename deletes it.\n const from = fields[++at]!;\n push(repoPath, \"create\");\n if (x === \"R\") push(from, \"delete\");\n continue;\n }\n\n if (x === \"D\" || y === \"D\") {\n push(repoPath, \"delete\");\n continue;\n }\n\n push(repoPath, x === \"A\" ? \"create\" : \"update\");\n }\n\n return changes;\n}\n\nfunction relativeToContent(repoPath: string, prefix: string): string | null {\n if (prefix === \"\") return repoPath;\n if (repoPath === prefix) return null;\n return repoPath.startsWith(`${prefix}/`) ? repoPath.slice(prefix.length + 1) : null;\n}\n\n/** A content-root-relative path as Git sees it, from the repository root. */\nexport function inRepo(prefix: string, path: string): string {\n return prefix === \"\" ? path : join(prefix, path).split(sep).join(\"/\");\n}\n\nfunction toPosix(path: string): string {\n return sep === \"/\" ? path : path.split(sep).join(\"/\");\n}\n\nfunction chunked<T>(items: T[], size: number): T[][] {\n const batches: T[][] = [];\n for (let at = 0; at < items.length; at += size) batches.push(items.slice(at, at + size));\n return batches;\n}\n"]}
1
+ {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git operations on a working tree whose content root is the draft workspace.\n * The owner policy does the push. Every operation is limited to paths under the\n * content root, never `git add -A` over the repository, so other work in the\n * same checkout is never swept into a commit.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { realpath, rm } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\n\nimport type { ContentChange, ContentSource, DirectoryEntry } from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\n\nimport type { Author } from \"./files.js\";\nimport { GitError, gitRunner, type GitRunner, type GitRunnerOptions } from \"./git.js\";\n\n/**\n * Makes every path argument literal: `tour[1].json` is not a pattern that also\n * matches `tour1.json`.\n */\nexport const LITERAL = \"--literal-pathspecs\";\n\n/** A content root inside a Git repository. */\nexport interface GitTree {\n /** Runs in the repository root. */\n git: GitRunner;\n /** Content root relative to the repository root; \"\" when they coincide. */\n prefix: string;\n /** Absolute path of the content root. */\n contentRoot: string;\n}\n\n/** A changed file plus what only Git status knows about it. */\nexport interface TreeChange extends ContentChange {\n /** Not yet known to Git — discarding means deleting, not restoring. */\n untracked: boolean;\n /** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */\n conflicted: boolean;\n}\n\n/**\n * Finds the repository the content root sits in, freshly each time: a\n * `git init` after startup should start working, not require a restart.\n */\nexport async function locateTree(\n contentRoot: string,\n options: GitRunnerOptions = {},\n): Promise<GitTree | { reason: string }> {\n const probe = gitRunner(contentRoot, options);\n try {\n const repoRoot = await probe.run([\"rev-parse\", \"--show-toplevel\"]);\n // Git resolves symlinks in the toplevel (`/private/var` for `/var` on\n // macOS); do the same, or the prefix is wrong.\n const resolved = await realpath(contentRoot);\n return {\n git: gitRunner(repoRoot, options),\n prefix: toPosix(relative(repoRoot, resolved)),\n contentRoot,\n };\n } catch (error) {\n return {\n reason:\n error instanceof GitError && error.exitCode === null\n ? error.message\n : \"The content directory is not inside a Git repository.\",\n };\n }\n}\n\n/** The content root's changes, as `git status` sees them. */\nexport async function listTreeChanges(tree: GitTree): Promise<TreeChange[]> {\n // `-uall`: Git otherwise collapses a new, untracked entry directory to one line.\n // `GIT_OPTIONAL_LOCKS=0`: no `index.lock`, so a parallel commit cannot fail on it.\n const raw = await tree.git.run(\n [LITERAL, \"status\", \"--porcelain=v1\", \"-z\", \"-uall\", \"--\", tree.prefix === \"\" ? \".\" : tree.prefix],\n { env: { GIT_OPTIONAL_LOCKS: \"0\" } },\n );\n return parseStatus(raw, tree.prefix);\n}\n\n/**\n * Stages and commits only `paths` (content-root relative) and returns the new\n * HEAD. The pathspec limits both steps, so all other staged or unstaged work\n * stays. Without `author`, Git's configured identity applies.\n */\nexport async function commitPaths(\n tree: GitTree,\n paths: string[],\n message: string,\n author?: Author,\n): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n const as = author ? [`--author=${author.name} <${author.email}>`] : [];\n await tree.git.run([LITERAL, \"commit\", \"-m\", message, ...as, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Commits `paths` again with the message, author and date of `original`, after\n * a rejected push. Returns the new HEAD.\n */\nexport async function recommitPaths(tree: GitTree, paths: string[], original: string): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n await tree.git.run([LITERAL, \"commit\", \"-C\", original, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Stages `paths`, because `git commit -- <paths>` refuses a file Git has never\n * seen. A path gone from the disk uses `rm --cached`, because `git add` refuses\n * a deletion that is already staged (as `reset --soft` leaves it).\n */\nasync function stagePaths(tree: GitTree, paths: string[]): Promise<string[]> {\n const present: string[] = [];\n const gone: string[] = [];\n for (const path of paths) {\n (existsSync(within(tree.contentRoot, path)) ? present : gone).push(inRepo(tree.prefix, path));\n }\n for (const batch of chunked(present, 500)) {\n await tree.git.run([LITERAL, \"add\", \"-A\", \"--\", ...batch]);\n }\n for (const batch of chunked(gone, 500)) {\n await tree.git.run([LITERAL, \"rm\", \"--cached\", \"--ignore-unmatch\", \"--quiet\", \"--\", ...batch]);\n }\n return paths.map((path) => inRepo(tree.prefix, path));\n}\n\n/** Puts one changed file back to its HEAD version, or removes it if HEAD has none. */\nexport async function discardChange(tree: GitTree, change: TreeChange): Promise<void> {\n if (change.kind === \"create\") {\n // Unstage a staged creation first, so the index does not point at a deleted file.\n if (!change.untracked) {\n await tree.git.tryRun([LITERAL, \"rm\", \"--cached\", \"--force\", \"--\", inRepo(tree.prefix, change.path)]);\n }\n await rm(within(tree.contentRoot, change.path), { force: true });\n return;\n }\n await tree.git.run([\n LITERAL,\n \"restore\",\n \"--source=HEAD\",\n \"--staged\",\n \"--worktree\",\n \"--\",\n inRepo(tree.prefix, change.path),\n ]);\n}\n\n/** HEAD's commit id, or null before the first commit. */\nexport function headRevision(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"HEAD\"]);\n}\n\n/** The checked-out branch, or null when there is none to name. */\nexport function currentBranch(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n}\n\n/** Commits waiting to be pushed, or null when there is no upstream to say. */\nexport async function aheadOfUpstream(tree: GitTree): Promise<number | null> {\n const counted = await tree.git.tryRun([\"rev-list\", \"--count\", \"@{upstream}..HEAD\"]);\n if (counted === null) return null;\n const ahead = Number(counted);\n return Number.isFinite(ahead) ? ahead : null;\n}\n\n/**\n * The content root as `ref` has it, read from Git's object store, because the\n * working tree holds the drafts.\n */\nexport function headSource(tree: GitTree, ref = \"HEAD\"): ContentSource {\n const at = (path: string) => `${ref}:${path === \"\" ? tree.prefix : inRepo(tree.prefix, path)}`;\n\n return {\n origin: ref,\n\n async read(path) {\n // \"Not in HEAD\" and \"no commits yet\" both mean that there is no published version.\n return tree.git.tryRun([\"show\", at(path)]);\n },\n\n async list(path) {\n const raw = await tree.git.tryRun([\"ls-tree\", \"-z\", at(path)]);\n if (raw === null) return [];\n\n const entries: DirectoryEntry[] = [];\n for (const field of raw.split(\"\\0\")) {\n const tab = field.indexOf(\"\\t\");\n if (tab === -1) continue;\n const type = field.slice(0, tab).split(\" \")[1];\n entries.push({ name: field.slice(tab + 1), type: type === \"tree\" ? \"directory\" : \"file\" });\n }\n return entries;\n },\n };\n}\n\n/**\n * `base` with `paths` taken from `top`: what a commit of exactly those paths\n * would hold. Publish checks a selection against it, not against drafts that\n * stay behind.\n */\nexport function overlaySource(base: ContentSource, top: ContentSource, paths: Iterable<string>): ContentSource {\n const selected = new Set(paths);\n\n return {\n origin: `${base.origin} with the selected changes`,\n\n read: (path) => (selected.has(path) ? top.read(path) : base.read(path)),\n\n async list(directory) {\n const prefix = directory === \"\" ? \"\" : `${directory}/`;\n const items = new Map((await base.list(directory)).map((item) => [item.name, item]));\n for (const path of selected) {\n if (!path.startsWith(prefix)) continue;\n const [name, ...deeper] = path.slice(prefix.length).split(\"/\");\n if (deeper.length > 0) items.set(name!, { name: name!, type: \"directory\" });\n else if ((await top.read(path)) === null) items.delete(name!);\n else items.set(name!, { name: name!, type: \"file\" });\n }\n return [...items.values()];\n },\n };\n}\n\n/**\n * A save's temporary file (`*.mica-<pid>-<n>`), seen during a save or after a\n * crash. Never content.\n */\nconst TEMPORARY_FILE = /\\.mica-\\d+-\\d+$/;\n\n/**\n * Parses `git status --porcelain=v1 -z` into content-root-relative changes.\n * `-z` never changes a path; a rename is two fields and becomes a create and a\n * delete. `.mica/` is excluded: the developer commits the manifest, and the\n * drift check guards model changes.\n */\nexport function parseStatus(raw: string, prefix: string): TreeChange[] {\n const fields = raw.split(\"\\0\").filter((field) => field !== \"\");\n const changes: TreeChange[] = [];\n\n const push = (\n repoPath: string,\n kind: TreeChange[\"kind\"],\n flags: { untracked?: boolean; conflicted?: boolean } = {},\n ) => {\n const path = relativeToContent(repoPath, prefix);\n if (path === null || path === \".mica\" || path.startsWith(\".mica/\")) return;\n if (TEMPORARY_FILE.test(path)) return;\n changes.push({\n path,\n kind,\n untracked: flags.untracked ?? false,\n conflicted: flags.conflicted ?? false,\n });\n };\n\n for (let at = 0; at < fields.length; at += 1) {\n const field = fields[at]!;\n const x = field[0] ?? \" \";\n const y = field[1] ?? \" \";\n const repoPath = field.slice(3);\n\n if (x === \"?\" && y === \"?\") {\n push(repoPath, \"create\", { untracked: true });\n continue;\n }\n\n // A merge conflict state, whatever the tree holds.\n if (x === \"U\" || y === \"U\" || (x === \"A\" && y === \"A\") || (x === \"D\" && y === \"D\")) {\n push(repoPath, \"update\", { conflicted: true });\n continue;\n }\n\n if (x === \"R\" || x === \"C\") {\n // The next field is the source path; only a rename deletes it.\n const from = fields[++at]!;\n push(repoPath, \"create\");\n if (x === \"R\") push(from, \"delete\");\n continue;\n }\n\n if (x === \"D\" || y === \"D\") {\n push(repoPath, \"delete\");\n continue;\n }\n\n push(repoPath, x === \"A\" ? \"create\" : \"update\");\n }\n\n return changes;\n}\n\nfunction relativeToContent(repoPath: string, prefix: string): string | null {\n if (prefix === \"\") return repoPath;\n if (repoPath === prefix) return null;\n return repoPath.startsWith(`${prefix}/`) ? repoPath.slice(prefix.length + 1) : null;\n}\n\n/** A content-root-relative path as Git sees it, from the repository root. */\nexport function inRepo(prefix: string, path: string): string {\n return prefix === \"\" ? path : join(prefix, path).split(sep).join(\"/\");\n}\n\nfunction toPosix(path: string): string {\n return sep === \"/\" ? path : path.split(sep).join(\"/\");\n}\n\nfunction chunked<T>(items: T[], size: number): T[][] {\n const batches: T[][] = [];\n for (let at = 0; at < items.length; at += size) batches.push(items.slice(at, at + size));\n return batches;\n}\n"]}
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@micacms/runtime",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-rc.1",
4
4
  "type": "module",
5
5
  "description": "The Mica single-site runtime: the Hono API, application services and staging orchestration, authentication-neutral. Composed by @micacms/editor locally and @micacms/self-hosted behind OIDC.",
6
6
  "license": "MIT",
7
+ "homepage": "https://micacms.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/micacms/mica.git",
11
+ "directory": "packages/runtime"
12
+ },
13
+ "bugs": "https://github.com/micacms/mica/issues",
7
14
  "author": "Alexander Müdespacher",
8
15
  "keywords": [
9
16
  "mica",
@@ -22,14 +29,15 @@
22
29
  }
23
30
  },
24
31
  "files": [
25
- "dist"
32
+ "dist",
33
+ "src"
26
34
  ],
27
35
  "dependencies": {
28
36
  "@hono/node-server": "^1.13.8",
29
37
  "hono": "^4.13.7",
30
38
  "image-size": "^2.0.3",
31
39
  "zod": "^4.3.6",
32
- "@micacms/core": "0.5.0"
40
+ "@micacms/core": "0.6.0-rc.1"
33
41
  },
34
42
  "devDependencies": {
35
43
  "@types/node": "^22.20.1",
@@ -37,7 +45,7 @@
37
45
  "typescript": "^5.7.3"
38
46
  },
39
47
  "engines": {
40
- "node": ">=22.12.0"
48
+ "node": ">=24"
41
49
  },
42
50
  "scripts": {
43
51
  "build": "tsc -p tsconfig.json",
package/src/app.ts ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Assembles the app: health, the admin API and the admin SPA. There is no
3
+ * public content API and no `/media/*`; a website builds from the repository,
4
+ * not from this server. Authentication comes from the composing app.
5
+ */
6
+
7
+ import { Hono } from "hono";
8
+ import { cors } from "hono/cors";
9
+ import type { MiddlewareHandler } from "hono";
10
+
11
+ import type { RuntimeContext } from "./context.js";
12
+ import type { Env } from "./http.js";
13
+ import {
14
+ allowedOrigins,
15
+ errorHandler,
16
+ originGuard,
17
+ requestSizeLimit,
18
+ rateLimit,
19
+ requestContext,
20
+ securityHeaders,
21
+ } from "./middleware/index.js";
22
+ import { adminContentRoutes } from "./routes/admin-content.js";
23
+ import { adminMediaRoutes } from "./routes/admin-media.js";
24
+ import { adminSpaRoutes, adminAssetsExist } from "./routes/admin-spa.js";
25
+ import { adminStagingRoutes } from "./routes/admin-staging.js";
26
+ import { adminSystemRoutes } from "./routes/admin-system.js";
27
+ import { healthRoutes } from "./routes/health.js";
28
+
29
+ /** What a composing app contributes around the auth-neutral runtime. */
30
+ export interface AuthSurface {
31
+ /** Mounted at `/api/auth`: sign-in, callback, logout, whatever auth needs. */
32
+ routes: Hono<Env>;
33
+ /** Applied before every `/api/admin` route. Must set `access` or refuse the request. */
34
+ gate: MiddlewareHandler<Env>[];
35
+ }
36
+
37
+ /**
38
+ * Allows every request with no identity. Safe only because the local editor is
39
+ * loopback-bound and Host-guarded.
40
+ */
41
+ export function allowAll(): MiddlewareHandler<Env> {
42
+ return async (c, next) => {
43
+ c.set("access", { identity: null });
44
+ await next();
45
+ };
46
+ }
47
+
48
+ /** `/api/auth/config` with nothing behind it: the SPA's cue to skip sign-in. */
49
+ function unauthenticatedSurface(): AuthSurface {
50
+ const routes = new Hono<Env>();
51
+ routes.get("/config", (c) => c.json({ authRequired: false, provider: null }));
52
+ return { routes, gate: [allowAll()] };
53
+ }
54
+
55
+ export interface CreateAppOptions {
56
+ /** Absent means the unauthenticated local surface. */
57
+ auth?: AuthSurface;
58
+ /**
59
+ * Middleware in front of everything, so the local editor's Host allowlist
60
+ * also guards the health endpoints.
61
+ */
62
+ guards?: MiddlewareHandler<Env>[];
63
+ adminDir?: string;
64
+ }
65
+
66
+ export function createApp(ctx: RuntimeContext, options: CreateAppOptions = {}): Hono<Env> {
67
+ const app = new Hono<Env>();
68
+ const auth = options.auth ?? unauthenticatedSurface();
69
+
70
+ app.use("*", requestContext(ctx));
71
+ for (const guard of options.guards ?? []) app.use("*", guard);
72
+ app.use("*", securityHeaders());
73
+ app.onError(errorHandler());
74
+
75
+ app.route("/", healthRoutes());
76
+
77
+ // Only for the Vite dev server on another origin; in production the SPA is same-origin.
78
+ if (ctx.config.adminOrigins.length > 0) {
79
+ app.use(
80
+ "/api/*",
81
+ cors({
82
+ origin: allowedOrigins(ctx.config),
83
+ credentials: true,
84
+ allowHeaders: ["content-type", "x-csrf-token"],
85
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
86
+ }),
87
+ );
88
+ }
89
+
90
+ app.use("/api/*", rateLimit());
91
+ app.use("/api/*", originGuard());
92
+ app.use("/api/*", requestSizeLimit());
93
+
94
+ app.route("/api/auth", auth.routes);
95
+
96
+ // One gate and no roles: whoever the surface admits is an editor of this site.
97
+ for (const middleware of auth.gate) app.use("/api/admin/*", middleware);
98
+
99
+ app.route("/api/admin", adminSystemRoutes());
100
+ app.route("/api/admin", adminContentRoutes());
101
+ app.route("/api/admin", adminStagingRoutes());
102
+ app.route("/api/admin/media", adminMediaRoutes());
103
+
104
+ app.all("/api/*", (c) =>
105
+ c.json({ error: { code: "not_found" as const, message: "No such endpoint." } }, 404),
106
+ );
107
+
108
+ const adminDir = options.adminDir ?? ctx.config.MICA_ADMIN_DIR;
109
+
110
+ if (adminDir && adminAssetsExist(adminDir)) {
111
+ app.route("/admin", adminSpaRoutes(adminDir));
112
+ } else {
113
+ app.get("/admin/*", (c) =>
114
+ c.text(
115
+ "The admin interface has not been built.\n" +
116
+ "Run `rush build`, or use `rush dev` and open the Vite dev server.",
117
+ 503,
118
+ ),
119
+ );
120
+ }
121
+
122
+ app.get("/", (c) => c.redirect("/admin/"));
123
+
124
+ app.notFound((c) => c.text("Not found", 404));
125
+
126
+ return app;
127
+ }
package/src/config.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The environment variables the runtime reads. There is no `loadConfig`: each
3
+ * app builds its own schema from `runtimeEnv`, because the variables and
4
+ * defaults are what make one app different from another.
5
+ */
6
+
7
+ import path from "node:path";
8
+ import { z } from "zod";
9
+
10
+ import { DEFAULT_BASE_URL, DEFAULT_PORT } from "./lib/ports.js";
11
+
12
+ /** In Zod 4, `.default` takes the output value and skips the transform. */
13
+ export const bool = (fallback: boolean) =>
14
+ z
15
+ .enum(["true", "false", "1", "0"])
16
+ .transform((value) => value === "true" || value === "1")
17
+ .default(fallback);
18
+
19
+ /** Splits a comma-separated list, trims entries and drops empty ones. */
20
+ export const list = (value: string | undefined): string[] =>
21
+ (value ?? "")
22
+ .split(",")
23
+ .map((entry) => entry.trim())
24
+ .filter(Boolean);
25
+
26
+ export class ConfigError extends Error {
27
+ override name = "ConfigError";
28
+ }
29
+
30
+ /** Hostnames that are this machine, and nowhere else. */
31
+ export const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0:0:0:0:0:0:0:1"]);
32
+
33
+ /** The variables the runtime itself reads. Apps spread this into their schema. */
34
+ export const runtimeEnv = {
35
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
36
+ MICA_LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "silent"]).default("info"),
37
+
38
+ /** The listen address. The app sets the default. */
39
+ HOST: z.string().optional(),
40
+ PORT: z.coerce.number().int().min(1).max(65535).default(DEFAULT_PORT),
41
+
42
+ /** Public origin of the CMS itself. Used for cookies, CORS and media URLs. */
43
+ MICA_PUBLIC_URL: z.string().url().default(DEFAULT_BASE_URL),
44
+
45
+ /** The content root this Mica serves: a directory in a Git working tree. Each app sets it. */
46
+ MICA_CONTENT_ROOT: z.string(),
47
+
48
+ /** `false` disables commit and push: Publish shows the pending changes read-only. */
49
+ MICA_LOCAL_PUBLISH: bool(true),
50
+
51
+ MICA_MAX_UPLOAD_BYTES: z.coerce
52
+ .number()
53
+ .int()
54
+ .positive()
55
+ .default(25 * 1024 * 1024),
56
+
57
+ /** Read the client IP from X-Forwarded-For. Only enable behind a proxy. */
58
+ MICA_TRUST_PROXY: bool(false),
59
+ /** Extra browser origins allowed to call the admin API (dev Vite server). */
60
+ MICA_ADMIN_ORIGINS: z.string().optional(),
61
+ /** Overrides where the built admin SPA is served from. */
62
+ MICA_ADMIN_DIR: z.string().optional(),
63
+ };
64
+
65
+ const runtimeSchema = z.object(runtimeEnv);
66
+ export type RuntimeEnv = z.infer<typeof runtimeSchema>;
67
+
68
+ /**
69
+ * Adds the derived values to the parsed environment. Each app calls it from its
70
+ * own `transform`.
71
+ */
72
+ export function deriveRuntime<T extends RuntimeEnv>(env: T) {
73
+ return {
74
+ ...env,
75
+ isProduction: env.NODE_ENV === "production",
76
+ contentRoot: path.resolve(env.MICA_CONTENT_ROOT),
77
+ adminOrigins: list(env.MICA_ADMIN_ORIGINS),
78
+ };
79
+ }
80
+
81
+ /** What the runtime requires of a finished configuration. */
82
+ export type RuntimeConfig = ReturnType<typeof deriveRuntime<RuntimeEnv>> & {
83
+ /** Resolved by the app: the runtime never guesses an address. */
84
+ HOST: string;
85
+ };
86
+
87
+ /** Loads `./.env` into `process.env`. Variables that are already set win. */
88
+ export function loadEnvFile(): void {
89
+ try {
90
+ process.loadEnvFile();
91
+ } catch (error) {
92
+ // A missing `.env` is normal; an unreadable or malformed one must not start
93
+ // the server with defaults.
94
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
95
+ throw new ConfigError(`Could not read .env: ${(error as Error).message}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Treats a blank variable as unset, so a verbatim copy of `.env.example` with blanks is valid. */
101
+ export function withoutBlanks(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
102
+ return Object.fromEntries(
103
+ Object.entries(env).filter(([, value]) => value !== undefined && value.trim() !== ""),
104
+ );
105
+ }
106
+
107
+ /** Formats a Zod refusal the way a person at a terminal reads it. */
108
+ export function describeConfigIssues(error: z.ZodError): string {
109
+ const details = error.issues
110
+ .map((issue) => ` ${issue.path.join(".") || "(root)"}: ${issue.message}`)
111
+ .join("\n");
112
+ return `Invalid environment configuration:\n${details}`;
113
+ }