@minamocms/framework 0.0.0-stage → 0.10.2
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/CHANGELOG.md +240 -0
- package/LICENSE +105 -0
- package/README.md +133 -2
- package/dist/boot.d.ts +19 -0
- package/dist/boot.js +22 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +44 -0
- package/dist/create-site/cli.d.ts +2 -0
- package/dist/create-site/cli.js +31 -0
- package/dist/create-site/generate-site.d.ts +12 -0
- package/dist/create-site/generate-site.js +168 -0
- package/dist/create-site/mint-token-cli.d.ts +2 -0
- package/dist/create-site/mint-token-cli.js +26 -0
- package/dist/create-site/mint-token.d.ts +7 -0
- package/dist/create-site/mint-token.js +49 -0
- package/dist/create-site/template/AGENTS.md +473 -0
- package/dist/create-site/template/content/menus/footerCompany.json +9 -0
- package/dist/create-site/template/content/menus/footerProduct.json +9 -0
- package/dist/create-site/template/content/menus/footerResources.json +9 -0
- package/dist/create-site/template/content/menus/main.json +9 -0
- package/dist/create-site/template/content/pages/404.json +23 -0
- package/dist/create-site/template/content/pages/about/careers.json +22 -0
- package/dist/create-site/template/content/pages/about/team.json +22 -0
- package/dist/create-site/template/content/pages/about.json +22 -0
- package/dist/create-site/template/content/pages/docs/deployment.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started/quickstart.json +22 -0
- package/dist/create-site/template/content/pages/docs/getting-started.json +24 -0
- package/dist/create-site/template/content/pages/docs.json +22 -0
- package/dist/create-site/template/content/pages/index.json +182 -0
- package/dist/create-site/template/content/redirects.json +4 -0
- package/dist/create-site/template/dockerignore +6 -0
- package/dist/create-site/template/gitignore +11 -0
- package/dist/create-site/template/theme/assets/site.js +49 -0
- package/dist/create-site/template/theme/assets/style.css +1223 -0
- package/dist/create-site/template/theme/blocks/button.liquid +21 -0
- package/dist/create-site/template/theme/blocks/comparison-row.liquid +19 -0
- package/dist/create-site/template/theme/blocks/faq-item.liquid +22 -0
- package/dist/create-site/template/theme/blocks/feature-card.liquid +23 -0
- package/dist/create-site/template/theme/blocks/logo-mark.liquid +13 -0
- package/dist/create-site/template/theme/blocks/pricing-tier.liquid +42 -0
- package/dist/create-site/template/theme/blocks/step-card.liquid +19 -0
- package/dist/create-site/template/theme/blocks/testimonial-card.liquid +25 -0
- package/dist/create-site/template/theme/config/site_settings.json +11 -0
- package/dist/create-site/template/theme/layouts/theme.liquid +102 -0
- package/dist/create-site/template/theme/sections/comparison.liquid +41 -0
- package/dist/create-site/template/theme/sections/cta-banner.liquid +36 -0
- package/dist/create-site/template/theme/sections/faq.liquid +27 -0
- package/dist/create-site/template/theme/sections/feature-grid.liquid +27 -0
- package/dist/create-site/template/theme/sections/hero.liquid +48 -0
- package/dist/create-site/template/theme/sections/how-it-works.liquid +27 -0
- package/dist/create-site/template/theme/sections/pricing.liquid +27 -0
- package/dist/create-site/template/theme/sections/social-proof.liquid +21 -0
- package/dist/create-site/template/theme/sections/testimonials.liquid +27 -0
- package/dist/create-site/template/theme/snippets/icon.liquid +24 -0
- package/dist/create-site/template/theme/snippets/logo.liquid +7 -0
- package/dist/create-site/template/theme/snippets/responsive-media.liquid +107 -0
- package/dist/create-site/template/theme/snippets/site-name.liquid +1 -0
- package/dist/create-site/template/vhost/Dockerfile +33 -0
- package/dist/create-site/template/vhost/docker-entrypoint.sh +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/media/drivers/driver.d.ts +11 -0
- package/dist/media/drivers/driver.js +1 -0
- package/dist/media/drivers/local-fs-driver.d.ts +3 -0
- package/dist/media/drivers/local-fs-driver.js +94 -0
- package/dist/media/filename.d.ts +2 -0
- package/dist/media/filename.js +54 -0
- package/dist/media/manage-media.d.ts +19 -0
- package/dist/media/manage-media.js +45 -0
- package/dist/media/seed-media-cli.d.ts +2 -0
- package/dist/media/seed-media-cli.js +22 -0
- package/dist/media/seed-media.d.ts +11 -0
- package/dist/media/seed-media.js +76 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +83 -0
- package/dist/pull-site/cli.d.ts +2 -0
- package/dist/pull-site/cli.js +78 -0
- package/dist/pull-site/prompt-token.d.ts +16 -0
- package/dist/pull-site/prompt-token.js +69 -0
- package/dist/pull-site/prompts.d.ts +22 -0
- package/dist/pull-site/prompts.js +127 -0
- package/dist/pull-site/pull-site.d.ts +23 -0
- package/dist/pull-site/pull-site.js +211 -0
- package/dist/renderer/engine.d.ts +2 -0
- package/dist/renderer/engine.js +48 -0
- package/dist/renderer/render-cache.d.ts +11 -0
- package/dist/renderer/render-cache.js +11 -0
- package/dist/renderer/render-page.d.ts +34 -0
- package/dist/renderer/render-page.js +209 -0
- package/dist/renderer/theme-templates.d.ts +7 -0
- package/dist/renderer/theme-templates.js +82 -0
- package/dist/routes/admin-redirect.d.ts +5 -0
- package/dist/routes/admin-redirect.js +26 -0
- package/dist/routes/assets.d.ts +6 -0
- package/dist/routes/assets.js +30 -0
- package/dist/routes/batch.d.ts +10 -0
- package/dist/routes/batch.js +148 -0
- package/dist/routes/capabilities.d.ts +6 -0
- package/dist/routes/capabilities.js +27 -0
- package/dist/routes/content.d.ts +8 -0
- package/dist/routes/content.js +169 -0
- package/dist/routes/deleted-pages.d.ts +8 -0
- package/dist/routes/deleted-pages.js +9 -0
- package/dist/routes/drafts.d.ts +10 -0
- package/dist/routes/drafts.js +73 -0
- package/dist/routes/git.d.ts +8 -0
- package/dist/routes/git.js +158 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +86 -0
- package/dist/routes/links.d.ts +8 -0
- package/dist/routes/links.js +18 -0
- package/dist/routes/media-public.d.ts +11 -0
- package/dist/routes/media-public.js +125 -0
- package/dist/routes/media.d.ts +9 -0
- package/dist/routes/media.js +80 -0
- package/dist/routes/menus.d.ts +8 -0
- package/dist/routes/menus.js +148 -0
- package/dist/routes/preview-revision.d.ts +10 -0
- package/dist/routes/preview-revision.js +96 -0
- package/dist/routes/preview.d.ts +10 -0
- package/dist/routes/preview.js +56 -0
- package/dist/routes/public.d.ts +10 -0
- package/dist/routes/public.js +123 -0
- package/dist/routes/publish.d.ts +10 -0
- package/dist/routes/publish.js +139 -0
- package/dist/routes/redirects.d.ts +8 -0
- package/dist/routes/redirects.js +132 -0
- package/dist/routes/search-public.d.ts +6 -0
- package/dist/routes/search-public.js +104 -0
- package/dist/routes/search.d.ts +8 -0
- package/dist/routes/search.js +15 -0
- package/dist/routes/settings.d.ts +10 -0
- package/dist/routes/settings.js +68 -0
- package/dist/routes/sitemap.d.ts +7 -0
- package/dist/routes/sitemap.js +79 -0
- package/dist/routes/theme.d.ts +10 -0
- package/dist/routes/theme.js +116 -0
- package/dist/schemas/instance.schema.json +17 -0
- package/dist/schemas/menu.schema.json +24 -0
- package/dist/schemas/page.schema.json +26 -0
- package/dist/schemas/redirects.schema.json +24 -0
- package/dist/search/drivers/driver.d.ts +9 -0
- package/dist/search/drivers/driver.js +1 -0
- package/dist/search/drivers/node-sqlite-driver.d.ts +7 -0
- package/dist/search/drivers/node-sqlite-driver.js +19 -0
- package/dist/search/query-content.d.ts +32 -0
- package/dist/search/query-content.js +207 -0
- package/dist/search/query-index.d.ts +5 -0
- package/dist/search/query-index.js +21 -0
- package/dist/search/rebuild-index.d.ts +3 -0
- package/dist/search/rebuild-index.js +320 -0
- package/dist/server-config.d.ts +23 -0
- package/dist/server-config.js +237 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +235 -0
- package/dist/services/batch.d.ts +35 -0
- package/dist/services/batch.js +145 -0
- package/dist/services/checkpoint.d.ts +4 -0
- package/dist/services/checkpoint.js +45 -0
- package/dist/services/content-read.d.ts +27 -0
- package/dist/services/content-read.js +151 -0
- package/dist/services/delete-content.d.ts +12 -0
- package/dist/services/delete-content.js +166 -0
- package/dist/services/deleted-pages.d.ts +11 -0
- package/dist/services/deleted-pages.js +83 -0
- package/dist/services/dev-tunnel.d.ts +5 -0
- package/dist/services/dev-tunnel.js +5 -0
- package/dist/services/drafts.d.ts +16 -0
- package/dist/services/drafts.js +164 -0
- package/dist/services/etag.d.ts +2 -0
- package/dist/services/etag.js +30 -0
- package/dist/services/fs-walk.d.ts +1 -0
- package/dist/services/fs-walk.js +26 -0
- package/dist/services/git-commit.d.ts +3 -0
- package/dist/services/git-commit.js +12 -0
- package/dist/services/git-history.d.ts +28 -0
- package/dist/services/git-history.js +131 -0
- package/dist/services/git-revert.d.ts +12 -0
- package/dist/services/git-revert.js +140 -0
- package/dist/services/git.d.ts +16 -0
- package/dist/services/git.js +127 -0
- package/dist/services/interval-job.d.ts +4 -0
- package/dist/services/interval-job.js +9 -0
- package/dist/services/ip-allowlist.d.ts +7 -0
- package/dist/services/ip-allowlist.js +31 -0
- package/dist/services/links.d.ts +22 -0
- package/dist/services/links.js +169 -0
- package/dist/services/manage-menus.d.ts +15 -0
- package/dist/services/manage-menus.js +138 -0
- package/dist/services/manage-redirects.d.ts +25 -0
- package/dist/services/manage-redirects.js +156 -0
- package/dist/services/menu-references.d.ts +3 -0
- package/dist/services/menu-references.js +38 -0
- package/dist/services/menus.d.ts +9 -0
- package/dist/services/menus.js +43 -0
- package/dist/services/migration-runner.d.ts +16 -0
- package/dist/services/migration-runner.js +169 -0
- package/dist/services/mime-types.d.ts +3 -0
- package/dist/services/mime-types.js +43 -0
- package/dist/services/move.d.ts +16 -0
- package/dist/services/move.js +236 -0
- package/dist/services/path-safety.d.ts +6 -0
- package/dist/services/path-safety.js +112 -0
- package/dist/services/pid-file.d.ts +13 -0
- package/dist/services/pid-file.js +54 -0
- package/dist/services/post-urls.d.ts +3 -0
- package/dist/services/post-urls.js +27 -0
- package/dist/services/prepared-operation.d.ts +4 -0
- package/dist/services/prepared-operation.js +1 -0
- package/dist/services/publish.d.ts +15 -0
- package/dist/services/publish.js +313 -0
- package/dist/services/rate-limit-config.d.ts +9 -0
- package/dist/services/rate-limit-config.js +22 -0
- package/dist/services/redirects.d.ts +27 -0
- package/dist/services/redirects.js +175 -0
- package/dist/services/reindex-on-write.d.ts +3 -0
- package/dist/services/reindex-on-write.js +30 -0
- package/dist/services/resolve-blog-url.d.ts +11 -0
- package/dist/services/resolve-blog-url.js +31 -0
- package/dist/services/resolve-url.d.ts +11 -0
- package/dist/services/resolve-url.js +20 -0
- package/dist/services/site-settings.d.ts +28 -0
- package/dist/services/site-settings.js +116 -0
- package/dist/services/startup-checks.d.ts +10 -0
- package/dist/services/startup-checks.js +81 -0
- package/dist/services/static-file.d.ts +7 -0
- package/dist/services/static-file.js +37 -0
- package/dist/services/theme-component-file.d.ts +6 -0
- package/dist/services/theme-component-file.js +39 -0
- package/dist/services/theme-files.d.ts +33 -0
- package/dist/services/theme-files.js +148 -0
- package/dist/services/theme-page-templates.d.ts +7 -0
- package/dist/services/theme-page-templates.js +53 -0
- package/dist/services/theme-schemas.d.ts +2 -0
- package/dist/services/theme-schemas.js +137 -0
- package/dist/services/token-auth.d.ts +10 -0
- package/dist/services/token-auth.js +64 -0
- package/dist/services/urls.d.ts +2 -0
- package/dist/services/urls.js +20 -0
- package/dist/services/validation.d.ts +26 -0
- package/dist/services/validation.js +195 -0
- package/dist/services/write-queue.d.ts +1 -0
- package/dist/services/write-queue.js +14 -0
- package/dist/site-check/cli.d.ts +2 -0
- package/dist/site-check/cli.js +48 -0
- package/dist/site-check/run-check.d.ts +12 -0
- package/dist/site-check/run-check.js +188 -0
- package/dist/site-sync/cli-helpers.d.ts +23 -0
- package/dist/site-sync/cli-helpers.js +75 -0
- package/dist/site-sync/deploy.d.ts +36 -0
- package/dist/site-sync/deploy.js +124 -0
- package/dist/site-sync/live-content.d.ts +30 -0
- package/dist/site-sync/live-content.js +68 -0
- package/dist/site-sync/prompts.d.ts +30 -0
- package/dist/site-sync/prompts.js +237 -0
- package/dist/site-sync/pull-cli.d.ts +2 -0
- package/dist/site-sync/pull-cli.js +75 -0
- package/dist/site-sync/pull-site.d.ts +28 -0
- package/dist/site-sync/pull-site.js +171 -0
- package/dist/site-sync/push-cli.d.ts +2 -0
- package/dist/site-sync/push-cli.js +268 -0
- package/dist/site-sync/push-site.d.ts +96 -0
- package/dist/site-sync/push-site.js +376 -0
- package/dist/site-sync/remote-site.d.ts +38 -0
- package/dist/site-sync/remote-site.js +122 -0
- package/dist/site-sync/sync-record.d.ts +26 -0
- package/dist/site-sync/sync-record.js +76 -0
- package/dist/site-sync/theme-sync.d.ts +34 -0
- package/dist/site-sync/theme-sync.js +128 -0
- package/dist/stop-site/cli.d.ts +2 -0
- package/dist/stop-site/cli.js +28 -0
- package/dist/stop-site/stop-site.d.ts +26 -0
- package/dist/stop-site/stop-site.js +56 -0
- package/dist/theme-state.d.ts +20 -0
- package/dist/theme-state.js +33 -0
- package/dist/upgrade/changelog.d.ts +8 -0
- package/dist/upgrade/changelog.js +46 -0
- package/dist/upgrade/finish.d.ts +9 -0
- package/dist/upgrade/finish.js +30 -0
- package/dist/upgrade/owned-files.d.ts +11 -0
- package/dist/upgrade/owned-files.js +42 -0
- package/dist/upgrade/package-names.d.ts +4 -0
- package/dist/upgrade/package-names.js +26 -0
- package/dist/upgrade/upgrade-cli.d.ts +2 -0
- package/dist/upgrade/upgrade-cli.js +170 -0
- package/package.json +64 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { listFilesRecursively } from "./fs-walk.js";
|
|
3
|
+
import { commitPaths } from "./git.js";
|
|
4
|
+
import { sanitisePath } from "./path-safety.js";
|
|
5
|
+
import { RedirectError, addRedirect, isValidRedirectTarget, loadRedirects, serialiseRedirects, } from "./redirects.js";
|
|
6
|
+
import { pagePathToUrl } from "./urls.js";
|
|
7
|
+
import { enqueue } from "./write-queue.js";
|
|
8
|
+
import { reindexInBackground } from "./reindex-on-write.js";
|
|
9
|
+
const PAGES_PREFIX = 'pages/';
|
|
10
|
+
// Returns null for anything else (menus have no public URL at all, so
|
|
11
|
+
// redirects are meaningless there).
|
|
12
|
+
function urlForDeletedEntry(relativePath) {
|
|
13
|
+
if (relativePath.startsWith(PAGES_PREFIX)) {
|
|
14
|
+
return pagePathToUrl(relativePath.slice(PAGES_PREFIX.length));
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export class DeleteContentError extends Error {
|
|
19
|
+
reason;
|
|
20
|
+
constructor(reason, message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.name = 'DeleteContentError';
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Validates and writes everything a delete needs, stopping short of
|
|
27
|
+
// the commit itself - used both by the standalone deleteContentJob
|
|
28
|
+
// (which commits immediately after) and by batch.ts (which accumulates
|
|
29
|
+
// this alongside other operations and commits once for the whole
|
|
30
|
+
// batch). A write-phase failure (including a client-triggered
|
|
31
|
+
// redirect-cycle) is caught and rolled back here, inside prepare
|
|
32
|
+
// itself, so prepare either fully succeeds or fully undoes itself
|
|
33
|
+
// before throwing.
|
|
34
|
+
export function prepareDeleteContent(config, relativePath, redirectTo) {
|
|
35
|
+
const livePath = sanitisePath(config.contentRoot, relativePath);
|
|
36
|
+
if (!existsSync(livePath)) {
|
|
37
|
+
throw new DeleteContentError('page-not-found', `No live page found at "${relativePath}"`);
|
|
38
|
+
}
|
|
39
|
+
// Subtree rejection: a deliberate scope decision (reject outright,
|
|
40
|
+
// not cascade) - an accidental single DELETE can't silently take a
|
|
41
|
+
// whole subtree with it. "Has children" means a real child page
|
|
42
|
+
// exists (checked via listFilesRecursively), never bare directory
|
|
43
|
+
// existence: a leftover *empty* sibling directory would otherwise
|
|
44
|
+
// make this page permanently undeletable, since no rmdir/cleanup
|
|
45
|
+
// endpoint exists anywhere in this API to ever clear it.
|
|
46
|
+
if (relativePath.startsWith(PAGES_PREFIX)) {
|
|
47
|
+
const dirRelative = relativePath.slice(PAGES_PREFIX.length).replace(/\.json$/, '');
|
|
48
|
+
const dirPath = sanitisePath(config.pagesRoot, dirRelative);
|
|
49
|
+
if (existsSync(dirPath) &&
|
|
50
|
+
statSync(dirPath).isDirectory() &&
|
|
51
|
+
listFilesRecursively(dirPath, dirPath, '.json').length > 0) {
|
|
52
|
+
throw new DeleteContentError('has-children', `"${relativePath}" has child pages; delete them first`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Never touches a draft at the same path: /v1/content and /v1/drafts
|
|
56
|
+
// are separate resources with their own dedicated DELETEs, and a
|
|
57
|
+
// draft-only page is already a legitimate, independently-listable
|
|
58
|
+
// state (Group D's listContent unions contentRoot/draftsRoot).
|
|
59
|
+
const original = readFileSync(livePath);
|
|
60
|
+
let redirectsBefore = null;
|
|
61
|
+
let redirectsChanged = false;
|
|
62
|
+
// Reverses the unlink and restores/removes redirects.json. Matches
|
|
63
|
+
// publish.ts's/move.ts's rollback() philosophy: attempt everything,
|
|
64
|
+
// report every failure.
|
|
65
|
+
function undoDeleteWrites() {
|
|
66
|
+
const restoreFailures = [];
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(livePath, original);
|
|
69
|
+
}
|
|
70
|
+
catch (restoreError) {
|
|
71
|
+
restoreFailures.push(restoreError);
|
|
72
|
+
}
|
|
73
|
+
if (redirectsChanged) {
|
|
74
|
+
try {
|
|
75
|
+
if (redirectsBefore === null) {
|
|
76
|
+
rmSync(config.redirectsPath, { force: true });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
writeFileSync(config.redirectsPath, redirectsBefore);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (restoreError) {
|
|
83
|
+
restoreFailures.push(restoreError);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return restoreFailures;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
unlinkSync(livePath);
|
|
90
|
+
// redirectTo is client-supplied, unlike publish.ts/move.ts's own
|
|
91
|
+
// automatic redirect bookkeeping - a client can trivially trigger a
|
|
92
|
+
// real cycle (e.g. redirectTo pointing back into an existing
|
|
93
|
+
// chain), so that's surfaced as its own reason, not left to fall
|
|
94
|
+
// into the generic write-failed/500 bucket an unlikely automatic-
|
|
95
|
+
// bookkeeping cycle would. Format is validated the same way as the
|
|
96
|
+
// dedicated manage-redirects.ts endpoints, internal-paths-only,
|
|
97
|
+
// so the two write paths never disagree on what's allowed into
|
|
98
|
+
// redirects.json.
|
|
99
|
+
const fromUrl = urlForDeletedEntry(relativePath);
|
|
100
|
+
if (redirectTo !== undefined && fromUrl !== null) {
|
|
101
|
+
if (!isValidRedirectTarget(redirectTo)) {
|
|
102
|
+
throw new DeleteContentError('invalid-redirect-target', `redirectTo "${redirectTo}" must be an internal path starting with "/"`);
|
|
103
|
+
}
|
|
104
|
+
redirectsBefore = existsSync(config.redirectsPath)
|
|
105
|
+
? readFileSync(config.redirectsPath, 'utf-8')
|
|
106
|
+
: null;
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = addRedirect(loadRedirects(config).entries, fromUrl, redirectTo).entries;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error instanceof RedirectError) {
|
|
113
|
+
throw new DeleteContentError('redirect-cycle', error.message, { cause: error });
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const serialised = serialiseRedirects(entries);
|
|
118
|
+
if (serialised !== (redirectsBefore ?? '')) {
|
|
119
|
+
writeFileSync(config.redirectsPath, serialised);
|
|
120
|
+
redirectsChanged = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const failures = undoDeleteWrites();
|
|
126
|
+
if (failures.length > 0) {
|
|
127
|
+
throw new DeleteContentError('rollback-failed', 'Delete failed and rolling back afterwards also failed; the working tree may be inconsistent and needs manual inspection', { cause: error });
|
|
128
|
+
}
|
|
129
|
+
// A redirect-cycle is already a DeleteContentError with its own
|
|
130
|
+
// reason - preserve it rather than reclassifying it as write-failed.
|
|
131
|
+
if (error instanceof DeleteContentError) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
// Always write-failed here, structurally: commitPaths hasn't been
|
|
135
|
+
// called yet, so this can never be a GitOperationError.
|
|
136
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
137
|
+
throw new DeleteContentError('write-failed', `Delete failed: ${detail}`, { cause: error });
|
|
138
|
+
}
|
|
139
|
+
const paths = [livePath];
|
|
140
|
+
if (redirectsChanged) {
|
|
141
|
+
paths.push(config.redirectsPath);
|
|
142
|
+
}
|
|
143
|
+
return { paths, undo: undoDeleteWrites };
|
|
144
|
+
}
|
|
145
|
+
async function deleteContentJob(config, relativePath, redirectTo, message, author) {
|
|
146
|
+
const { paths, undo } = prepareDeleteContent(config, relativePath, redirectTo);
|
|
147
|
+
try {
|
|
148
|
+
commitPaths(config.siteRoot, paths, message, author);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const failures = undo();
|
|
152
|
+
if (failures.length > 0) {
|
|
153
|
+
throw new DeleteContentError('rollback-failed', 'Delete failed and rolling back afterwards also failed; the working tree may be inconsistent and needs manual inspection', { cause: error });
|
|
154
|
+
}
|
|
155
|
+
// Always commit-failed here, structurally: prepare already
|
|
156
|
+
// succeeded, so anything caught in this try can only have come
|
|
157
|
+
// from commitPaths itself.
|
|
158
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
159
|
+
throw new DeleteContentError('commit-failed', `Delete failed: ${detail}`, { cause: error });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function deleteContent(config, relativePath, redirectTo, message, author) {
|
|
163
|
+
const result = enqueue(() => deleteContentJob(config, relativePath, redirectTo, message, author));
|
|
164
|
+
result.then(() => reindexInBackground(config), () => undefined);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SiteConfig } from '../config.ts';
|
|
2
|
+
export interface DeletedPage {
|
|
3
|
+
path: string;
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
title: string;
|
|
7
|
+
deletedAt: string;
|
|
8
|
+
deletedBy: string;
|
|
9
|
+
ref: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listDeletedPages(config: SiteConfig, limit?: number): DeletedPage[];
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readFileAtRevision } from "./git-history.js";
|
|
4
|
+
import { sanitisePath } from "./path-safety.js";
|
|
5
|
+
import { pagePathToUrl } from "./urls.js";
|
|
6
|
+
const FIELD_SEP = '\x1f';
|
|
7
|
+
const RECORD_SEP = '\x1e';
|
|
8
|
+
const DEFAULT_LIMIT = 50;
|
|
9
|
+
// How far back "recently" reaches (agreed with the owner). Older deletions
|
|
10
|
+
// can still be restored from git; they just aren't listed.
|
|
11
|
+
const RECENT_DAYS = 90;
|
|
12
|
+
function hasCommits(config) {
|
|
13
|
+
try {
|
|
14
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd: config.siteRoot, stdio: 'ignore' });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function existsNow(config, path) {
|
|
22
|
+
try {
|
|
23
|
+
return existsSync(sanitisePath(config.contentRoot, path)) || (existsSync(config.draftsRoot) && existsSync(sanitisePath(config.draftsRoot, path)));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function nonEmpty(value) {
|
|
30
|
+
return typeof value === 'string' && value.trim() !== '' ? value : null;
|
|
31
|
+
}
|
|
32
|
+
// The page's name and title as they were at ref, each falling back to
|
|
33
|
+
// the other (and to its address) when missing.
|
|
34
|
+
function labelsAt(config, ref, path, url) {
|
|
35
|
+
let name = null;
|
|
36
|
+
let title = null;
|
|
37
|
+
try {
|
|
38
|
+
const page = JSON.parse(readFileAtRevision(config, ref, `content/${path}`).toString('utf-8'));
|
|
39
|
+
name = nonEmpty(page.name);
|
|
40
|
+
title = nonEmpty(page.title);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Unreadable at ref: fall back to the address.
|
|
44
|
+
}
|
|
45
|
+
return { name: name ?? title ?? url, title: title ?? name ?? url };
|
|
46
|
+
}
|
|
47
|
+
export function listDeletedPages(config, limit = DEFAULT_LIMIT) {
|
|
48
|
+
if (!hasCommits(config)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const raw = execFileSync('git', ['log', `--since=${RECENT_DAYS} days ago`, '--diff-filter=D', '--name-only', `--format=${RECORD_SEP}%P${FIELD_SEP}%an${FIELD_SEP}%aI`, '--', 'content/pages'], { cwd: config.siteRoot, maxBuffer: 32 * 1024 * 1024 }).toString('utf-8');
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const deleted = [];
|
|
54
|
+
for (const record of raw.split(RECORD_SEP)) {
|
|
55
|
+
const [header, ...files] = record.split('\n');
|
|
56
|
+
// %P: the deleting commit's parent - the last version with the page.
|
|
57
|
+
// A full commit id, since refs like "abc^" aren't accepted.
|
|
58
|
+
const [parents, author, date] = (header ?? '').split(FIELD_SEP);
|
|
59
|
+
const parent = parents?.split(' ')[0];
|
|
60
|
+
if (!parent || !author || !date) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const path = file.trim().replace(/^content\//, '');
|
|
65
|
+
if (!/^pages\/.+\.json$/.test(path) || seen.has(path)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Most recent deletion first: an older one of the same path is
|
|
69
|
+
// history, not something to restore.
|
|
70
|
+
seen.add(path);
|
|
71
|
+
if (existsNow(config, path)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const ref = parent;
|
|
75
|
+
const url = pagePathToUrl(path.slice('pages/'.length));
|
|
76
|
+
deleted.push({ path, url, ...labelsAt(config, ref, path, url), deletedAt: date, deletedBy: author, ref });
|
|
77
|
+
if (deleted.length >= limit) {
|
|
78
|
+
return deleted;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return deleted;
|
|
83
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SiteConfig } from '../config.ts';
|
|
2
|
+
import type { PreparedOperation } from './prepared-operation.ts';
|
|
3
|
+
import type { ThemeSchemas, ValidationError } from './validation.ts';
|
|
4
|
+
export declare function ensureDraftsRoot(config: SiteConfig): void;
|
|
5
|
+
export type DraftReason = 'validation-failed' | 'conflict';
|
|
6
|
+
export declare class DraftError extends Error {
|
|
7
|
+
readonly reason: DraftReason;
|
|
8
|
+
readonly errors: ValidationError[];
|
|
9
|
+
constructor(reason: DraftReason, message: string, errors?: ValidationError[]);
|
|
10
|
+
}
|
|
11
|
+
export declare function saveDraft(config: SiteConfig, themeSchemas: ThemeSchemas, relativePath: string, content: unknown, expectedEtag: string): Promise<string>;
|
|
12
|
+
export declare function discardDraft(config: SiteConfig, relativePath: string): Promise<void>;
|
|
13
|
+
export declare function prepareSaveDraft(config: SiteConfig, themeSchemas: ThemeSchemas, relativePath: string, content: unknown, expectedEtag: string): Promise<PreparedOperation & {
|
|
14
|
+
etag: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function prepareDiscardDraft(config: SiteConfig, relativePath: string): Promise<PreparedOperation>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { ContentReadError, readContentFile } from "./content-read.js";
|
|
4
|
+
import { computeEtag, etagsMatch } from "./etag.js";
|
|
5
|
+
import { sanitisePath } from "./path-safety.js";
|
|
6
|
+
import { validateContent } from "./validation.js";
|
|
7
|
+
import { enqueue } from "./write-queue.js";
|
|
8
|
+
// content/drafts/ may not exist: git keeps no empty folders, so a site
|
|
9
|
+
// cloned or deployed with no drafts has none. Every draft read and write
|
|
10
|
+
// resolves its path under this folder (sanitisePath needs the root to
|
|
11
|
+
// exist), so without it the first draft save failed with a 500 - found
|
|
12
|
+
// running a fresh clone of a real site. Made once at start-up, before any
|
|
13
|
+
// request, rather than on every write.
|
|
14
|
+
export function ensureDraftsRoot(config) {
|
|
15
|
+
mkdirSync(config.draftsRoot, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
export class DraftError extends Error {
|
|
18
|
+
reason;
|
|
19
|
+
// Group I: the real ajv errors (already field-pointing, e.g.
|
|
20
|
+
// /sections/0/settings/columns) - previously only available
|
|
21
|
+
// stringified inside .message. Always [] for the 'conflict' reason.
|
|
22
|
+
errors;
|
|
23
|
+
constructor(reason, message, errors = []) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'DraftError';
|
|
26
|
+
this.reason = reason;
|
|
27
|
+
this.errors = errors;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Draft takes precedence over live: the ETag a client's If-Match must
|
|
31
|
+
// match is whatever they would have read from a prior GET, and D1/D2
|
|
32
|
+
// already prefer the draft over the live file at the same path. Reuses
|
|
33
|
+
// readContentFile/computeEtag (etag.ts's own doc comment anticipates
|
|
34
|
+
// this exact reuse) rather than reimplementing hashing here. Returns
|
|
35
|
+
// null when neither a draft nor a live file exists yet (a brand-new
|
|
36
|
+
// page) - see saveDraftJob's own comment for what that means.
|
|
37
|
+
function readCurrentEtag(config, relativePath) {
|
|
38
|
+
try {
|
|
39
|
+
return readContentFile(config.draftsRoot, relativePath).etag;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!(error instanceof ContentReadError) || error.reason !== 'not-found') {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return readContentFile(config.contentRoot, relativePath).etag;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!(error instanceof ContentReadError) || error.reason !== 'not-found') {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function saveDraftJob(config, themeSchemas, relativePath, content, expectedEtag) {
|
|
57
|
+
const draftPath = sanitisePath(config.draftsRoot, relativePath);
|
|
58
|
+
// The If-Match check happens here, inside the queued job, never at
|
|
59
|
+
// the route layer before enqueuing: the queue only serialises the
|
|
60
|
+
// writes themselves, not a check that happened earlier, so a
|
|
61
|
+
// route-layer check-then-enqueue has a real TOCTOU gap (verified
|
|
62
|
+
// empirically: run 2000 times, a check-before-enqueue design lets
|
|
63
|
+
// both of two racing requests succeed every single time; the same
|
|
64
|
+
// check run inside the job, like this, produced exactly one winner
|
|
65
|
+
// every time, no exceptions).
|
|
66
|
+
const currentEtag = readCurrentEtag(config, relativePath);
|
|
67
|
+
if (currentEtag !== null && !etagsMatch(currentEtag, expectedEtag)) {
|
|
68
|
+
throw new DraftError('conflict', `If-Match "${expectedEtag}" does not match the current ETag for "${relativePath}"`);
|
|
69
|
+
}
|
|
70
|
+
// currentEtag === null means neither a draft nor a live file exists
|
|
71
|
+
// yet (a brand-new page) - deliberately not checked further here.
|
|
72
|
+
// This isn't a race loophole: the *next* write to this same path
|
|
73
|
+
// lands after this job's write has created a real draft, so that
|
|
74
|
+
// job's own currentEtag is no longer null and gets checked for
|
|
75
|
+
// real. Only the very first write to a wholly new path skips the
|
|
76
|
+
// comparison, and nothing (E1-E6) tests or specifies what that first
|
|
77
|
+
// write's If-Match value should mean.
|
|
78
|
+
// Validate before touching disk at all, so a validation failure
|
|
79
|
+
// writes nothing (checklist C3) structurally, not via cleanup.
|
|
80
|
+
const result = validateContent(relativePath, content, themeSchemas);
|
|
81
|
+
if (!result.valid) {
|
|
82
|
+
throw new DraftError('validation-failed', `Draft at "${relativePath}" failed validation: ${JSON.stringify(result.errors)}`, result.errors);
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(dirname(draftPath), { recursive: true });
|
|
85
|
+
const bytes = Buffer.from(JSON.stringify(content, null, 2));
|
|
86
|
+
writeFileSync(draftPath, bytes);
|
|
87
|
+
// No git call: saves are cheap and frequent, and never create history
|
|
88
|
+
// (checklist C2, constraint 5).
|
|
89
|
+
// Computed from the exact bytes just written, never a second disk
|
|
90
|
+
// read (inside or outside the queue) - a second read could itself
|
|
91
|
+
// race a subsequent queued job and hand the client a stale value.
|
|
92
|
+
return computeEtag(bytes);
|
|
93
|
+
}
|
|
94
|
+
async function discardDraftJob(config, relativePath) {
|
|
95
|
+
const draftPath = sanitisePath(config.draftsRoot, relativePath);
|
|
96
|
+
// Idempotent, matching DELETE semantics: silently succeed if the
|
|
97
|
+
// draft is already absent rather than inventing a not-found error
|
|
98
|
+
// the checklist doesn't ask for.
|
|
99
|
+
if (existsSync(draftPath)) {
|
|
100
|
+
unlinkSync(draftPath);
|
|
101
|
+
}
|
|
102
|
+
// No git call: discard never touches /content/ or history.
|
|
103
|
+
}
|
|
104
|
+
export function saveDraft(config, themeSchemas, relativePath, content, expectedEtag) {
|
|
105
|
+
return enqueue(() => saveDraftJob(config, themeSchemas, relativePath, content, expectedEtag));
|
|
106
|
+
}
|
|
107
|
+
export function discardDraft(config, relativePath) {
|
|
108
|
+
return enqueue(() => discardDraftJob(config, relativePath));
|
|
109
|
+
}
|
|
110
|
+
// The batch.ts-composable shape, mirroring publish.ts/move.ts/
|
|
111
|
+
// delete-content.ts's own prepare* exports for a single uniform
|
|
112
|
+
// vocabulary - even though saveDraftJob/discardDraftJob never call
|
|
113
|
+
// commitPaths at all (drafts are never git-tracked, so paths is
|
|
114
|
+
// always []), batch.ts still needs an undo for them: F6 requires "no
|
|
115
|
+
// partial writes" for a whole failed batch, which includes rolling
|
|
116
|
+
// back an earlier draft write if a later operation in the same batch
|
|
117
|
+
// fails. Snapshots the draft's prior bytes/existence *before*
|
|
118
|
+
// delegating to the existing job function, rather than reimplementing
|
|
119
|
+
// its logic here.
|
|
120
|
+
export async function prepareSaveDraft(config, themeSchemas, relativePath, content, expectedEtag) {
|
|
121
|
+
const draftPath = sanitisePath(config.draftsRoot, relativePath);
|
|
122
|
+
const existedBefore = existsSync(draftPath);
|
|
123
|
+
const originalBytes = existedBefore ? readFileSync(draftPath) : null;
|
|
124
|
+
const etag = await saveDraftJob(config, themeSchemas, relativePath, content, expectedEtag);
|
|
125
|
+
return {
|
|
126
|
+
paths: [],
|
|
127
|
+
etag,
|
|
128
|
+
undo: () => {
|
|
129
|
+
try {
|
|
130
|
+
if (existedBefore) {
|
|
131
|
+
writeFileSync(draftPath, originalBytes);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
unlinkSync(draftPath);
|
|
135
|
+
}
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return [error];
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export async function prepareDiscardDraft(config, relativePath) {
|
|
145
|
+
const draftPath = sanitisePath(config.draftsRoot, relativePath);
|
|
146
|
+
const existedBefore = existsSync(draftPath);
|
|
147
|
+
const originalBytes = existedBefore ? readFileSync(draftPath) : null;
|
|
148
|
+
await discardDraftJob(config, relativePath);
|
|
149
|
+
return {
|
|
150
|
+
paths: [],
|
|
151
|
+
undo: () => {
|
|
152
|
+
try {
|
|
153
|
+
if (existedBefore) {
|
|
154
|
+
mkdirSync(dirname(draftPath), { recursive: true });
|
|
155
|
+
writeFileSync(draftPath, originalBytes);
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return [error];
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// A plain content hash, not a git blob hash: git.ts only shells out to
|
|
3
|
+
// git for actual commits, never for a per-request read, and nothing
|
|
4
|
+
// downstream needs the ETag to coincide with a real git blob hash
|
|
5
|
+
// (the git log/show endpoints address content by ref+path, not by
|
|
6
|
+
// hash lookup). A quoted strong ETag, matching HTTP's own syntax -
|
|
7
|
+
// this is a byte-for-byte digest, so strong semantics are correct.
|
|
8
|
+
//
|
|
9
|
+
// The one hash function every ETag-producing read (D1/D2) and every
|
|
10
|
+
// future If-Match comparison (Group E) must share - never inlined
|
|
11
|
+
// separately, or the two could silently drift.
|
|
12
|
+
export function computeEtag(bytes) {
|
|
13
|
+
return `"${createHash('sha256').update(bytes).digest('hex')}"`;
|
|
14
|
+
}
|
|
15
|
+
// RFC 7232 weak comparison: a compressing proxy sitting in front of a
|
|
16
|
+
// real deployment (confirmed live against a Railway-hosted admin - its
|
|
17
|
+
// edge downgrades this server's own strong ETag to weak in transit,
|
|
18
|
+
// since the compressed bytes on the wire differ from what computeEtag
|
|
19
|
+
// hashed) is standard, spec-compliant proxy behaviour, not a bug on
|
|
20
|
+
// the proxy's part. A client that captured and echoes back that
|
|
21
|
+
// now-weak value represents the exact same content a strict `===`
|
|
22
|
+
// would wrongly reject as changed - every If-Match check must compare
|
|
23
|
+
// this way, never with a bare `!==`, or any real host behind a
|
|
24
|
+
// compressing proxy/CDN produces spurious conflicts on every save.
|
|
25
|
+
function stripWeakPrefix(etag) {
|
|
26
|
+
return etag.startsWith('W/') ? etag.slice(2) : etag;
|
|
27
|
+
}
|
|
28
|
+
export function etagsMatch(a, b) {
|
|
29
|
+
return stripWeakPrefix(a) === stripWeakPrefix(b);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listFilesRecursively(dir: string, base: string, extension: string): string[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
// Recursively lists files matching extension under dir, returned as
|
|
4
|
+
// paths relative to base. Agent-configured roots only (content/drafts
|
|
5
|
+
// roots, theme directories), never a request-supplied :path - callers
|
|
6
|
+
// that walk request-shaped paths must go through sanitisePath instead.
|
|
7
|
+
export function listFilesRecursively(dir, base, extension) {
|
|
8
|
+
let out = [];
|
|
9
|
+
let entries;
|
|
10
|
+
try {
|
|
11
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const full = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
out = out.concat(listFilesRecursively(full, base, extension));
|
|
20
|
+
}
|
|
21
|
+
else if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
22
|
+
out.push(relative(base, full));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { commitWorkingTree } from "./git.js";
|
|
2
|
+
import { enqueue } from "./write-queue.js";
|
|
3
|
+
// The escape hatch (checklist G5). No PreparedOperation/undo-stack
|
|
4
|
+
// here, unlike publish/move/delete/revert: those services write new
|
|
5
|
+
// content themselves on request, so a write can fail independently of
|
|
6
|
+
// the git step, needing an undo. Here the "write" already exists in
|
|
7
|
+
// the working tree before this is ever called - the only failure
|
|
8
|
+
// modes are git add/git commit themselves, which commitWorkingTree's
|
|
9
|
+
// own bare `git reset` already unwinds (git.ts).
|
|
10
|
+
export function commitWorkingTreeChanges(config, message, author) {
|
|
11
|
+
return enqueue(() => Promise.resolve(commitWorkingTree(config.siteRoot, message, author)));
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SiteConfig } from '../config.ts';
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
author: {
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
};
|
|
8
|
+
date: string;
|
|
9
|
+
message: string;
|
|
10
|
+
isCheckpoint: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LogResult {
|
|
13
|
+
commits: LogEntry[];
|
|
14
|
+
hasMore: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const CHECKPOINT_MESSAGE = "chore: draft checkpoint";
|
|
17
|
+
export declare function getCommitLog(config: SiteConfig, options: {
|
|
18
|
+
path?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
}): LogResult;
|
|
21
|
+
export type GitShowReason = 'invalid-ref' | 'not-found-at-ref';
|
|
22
|
+
export declare class GitShowError extends Error {
|
|
23
|
+
readonly reason: GitShowReason;
|
|
24
|
+
constructor(reason: GitShowReason, message: string, options?: {
|
|
25
|
+
cause?: unknown;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export declare function readFileAtRevision(config: SiteConfig, ref: string, relativePath: string): Buffer;
|