meowpress 0.3.0-alpha.1 → 0.3.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,30 +1,75 @@
1
1
  # Meowpress
2
2
 
3
- `meowpress` provides content reading, page rendering, themes and the build pipeline for static sites.
4
- Consumers own the site configuration and content and build through this package; the package does not own the application's posts or output directory.
3
+ Meowpress builds a static blog from Markdown and MDX, with a built-in theme for posts, short notes
4
+ and public skills. A project can supply its own theme or page map through `meowpress/build`.
5
5
 
6
- Published history: `0.1.1-alpha.0` was accepted from the registry, followed by `0.1.1-alpha.1` with the English site and CLI defaults; `latest` remained `0.1.0`. The current workspace package is `0.2.0-alpha.0`, and its required CSS and JSX peer ranges accept `^0.2.0-alpha.0`. The host selects the shared CSS/JSX modules, and this package does not privately install another CSS copy; it does not depend on Meowtail or MeowSlides. Out-of-package builds and theme identity acceptance are bound to exact package archives, and peer contracts are not bypassed through overrides. Publishing requires separate explicit authorization.
6
+ ## Build and preview a site
7
7
 
8
- This package is a standalone content publishing library with source in `packages/meowpress`; it already has a public build entry, independent tests and typechecking,
9
- and does not depend on Assistant, WorkCell, Deno or the content and runtime state of a specific Blog. Style generation comes from `meowcss`,
10
- and HTML rendering comes from `meowjsx/web`; MeowUI does not need to be installed.
8
+ Use Bun and install this version of `meowpress` with compatible `meowcss` and `meowjsx` peers.
9
+ When trying a workspace build, install the archives from `bun run build` and `bun run pack` in your
10
+ project. From that project, add your first post and build the site.
11
11
 
12
- ## Entries
12
+ ```sh
13
+ mkdir -p content/posts
14
+ printf '# First post\n\nHello from Meowpress.\n' > content/posts/first.md
15
+ bunx --no-install meowpress build
16
+ ```
17
+
18
+ Meowpress builds `dist/index.html` and `dist/posts/first/index.html`. Put short notes in
19
+ `content/thoughts/` and public skills in `skills/<name>/SKILL.md`. `llms.txt` and `agents.md`
20
+ describe the generated site. The default theme includes `404.html` for static hosts that serve a
21
+ custom error page.
22
+
23
+ Run `bunx --no-install meowpress preview` to serve the built files at `http://127.0.0.1:4173`.
24
+ Run the build again after editing content. Deploy the `dist/` directory to a static host.
25
+
26
+ ## Configure an existing repository
13
27
 
14
- - `meowpress/build`: `meowSite()` defines configuration and `buildSite()` performs the build; this is the only public export.
15
- Content reading, page rendering, skill indexing and themes are internal package modules.
16
- - `meowpress --config ./site.config.ts`: Bun CLI that loads the consumer-provided configuration and builds.
28
+ Place `meowpress.config.ts` in the project root when your content lives outside the default
29
+ folders or when you want to change the site name. This example uses `blog/` for posts, `notes/`
30
+ for short notes and `blog/assets/images/` for the homepage image.
17
31
 
18
32
  ```ts
19
33
  import { meowSite } from "meowpress/build";
20
34
 
21
35
  export default meowSite({
22
- content: [{ directory: "/path/to/content" }],
23
- skillsDirectory: "/path/to/workspace",
24
- outputDirectory: "/path/to/dist",
36
+ content: {
37
+ directory: ".",
38
+ postsDirectory: "blog",
39
+ thoughtsDirectory: "notes",
40
+ assetsDirectory: "blog/assets",
41
+ },
42
+ site: {
43
+ name: "My site",
44
+ images: { hero: "images/cover.svg" },
45
+ },
25
46
  });
26
47
  ```
27
48
 
49
+ The directory names in `content` are relative to its `directory`. Posts from `blog/` still have
50
+ URLs under `/posts/`, and notes still have URLs under `/thoughts/`. Meowpress copies `posts/assets/`
51
+ to `/posts/assets/`. Set `postAssetsDirectory` to change the source of those files. Posts and assets
52
+ whose paths contain a file or folder name starting with `_` are treated as drafts.
53
+
54
+ Pass a project path after `build` or `preview` to run from another directory. The project root
55
+ defaults to the current directory and is the base for relative config paths and output.
56
+ The CLI looks for `meowpress.config.ts`, then `site.config.ts`. Use `--config <file>` to choose a
57
+ file explicitly. `meowpress` and `meowpress --config ./site.config.ts` also build, and `preview`
58
+ accepts `--port <port>`.
59
+
60
+ Builds replace the generated output after they succeed. Meowpress reuses a nonempty default
61
+ `dist/` only when it contains the previous build's `agents.md` and `llms.txt`. If another tool
62
+ owns `dist/`, set `outputDirectory` to a separate directory and reserve it for generated files.
63
+
64
+ `includeProjectSkills: true` opts in to publishing `.agents/skills/`. Set `projectRootDirectory`
65
+ to choose the root for those skills. Set `notFound: false` to omit the built-in `404.html`, or
66
+ supply `notFound: { title, message, homeLabel }` to change its copy.
67
+
68
+ ## Build from code
69
+
70
+ `meowSite()` resolves configuration and `buildSite()` writes the site. Both are exported from
71
+ `meowpress/build`. Use trusted local configuration and MDX, which execute code during a build.
72
+
28
73
  ## Pages a consumer owns
29
74
 
30
75
  A consumer that supplies `pages` owns the whole site map. The provider receives the content that was
@@ -50,13 +95,20 @@ export default meowSite({
50
95
 
51
96
  - `pages` is a provider of complete HTML documents. A path that resolves above the site root is
52
97
  refused rather than clamped.
98
+ - A page's optional `markdown` is the same page for agents. It is published next to the route
99
+ (`components/button.md` for `/components/button/`, `index.md` for the root), and `llms.txt`,
100
+ `llm.txt` and `agents.md` link it instead of the HTML. `llms.txt` follows the
101
+ [llms.txt](https://llmstxt.org/) shape: the site summary, then one section of links per page
102
+ `group` with each page's `description`. `llms-full.txt` holds every Markdown copy in one file.
103
+ - `agentGuide` is Markdown that opens `llms.txt` and `llms-full.txt` after the summary: what an
104
+ agent must know before following any link, such as how to install and set up the documented
105
+ packages.
53
106
  - `mdxComponents` adds named components to the MDX content pages; the built-in `Callout` stays
54
107
  available.
55
108
  - `themeStylesheet` names the theme stylesheet the build writes, so a consumer page can link it. It
56
109
  applies to the built-in pages as well, so a renamed stylesheet never leaves a page linking a file
57
110
  that was not written.
58
111
 
59
- Directories in the configuration are provided explicitly by the application; the default theme resolves from this package and does not depend on the startup directory.
60
112
  The whole theme can be replaced with `theme: defineTheme(...)`; all static pages output that theme's root attributes and default options.
61
113
  The build writes `theme.cssText` to a temporary CSS file, then lets Bun handle the other assets, and finally delivers through a stylesheet link,
62
114
  so the theme applies without client-side JavaScript.
@@ -72,19 +124,8 @@ Install the listed packages in the site; nothing depends on an external font ser
72
124
  Default illustrations use in-package vectors and CSS and require no business images. The `hero`, `feed` and `about` keys of `site.images`
73
125
  can explicitly set decorative image paths, for example `images/cover.svg`; paths are relative to the content root's `assets/`, the library handles each page depth,
74
126
  and assets are delivered along the existing asset build flow. When no About image is set, the first letter of the site name is used.
75
- The content source contains `posts/`, `thoughts/` and optional assets; drafts whose names start with an underscore do not enter the published output.
76
- The site has its own theme and does not implicitly install MeowUI's application theme.
77
-
78
- ## Verification and build
79
-
80
- After installing `meowpress` and its `meowcss` and `meowjsx` dependencies, run in the consumer directory:
81
-
82
- ```sh
83
- bunx --no-install meowpress --config ./site.config.ts
84
- ```
85
-
86
- The CLI requires Bun, and the output location is determined by `outputDirectory`. Font packages resolve from the startup directory first, then from
87
- this package, which carries the default Geist, so consumers are not required to copy source, install workspace-root dependencies or access an external font service.
127
+ Font packages resolve from the project root first, then from this package, which carries the
128
+ default Geist.
88
129
 
89
130
  ## Independent verification
90
131
 
@@ -1,27 +1,37 @@
1
1
  import { type ThemeDefinition } from "meowcss";
2
2
  import { type SiteFont } from "./fonts.js";
3
- import { type SiteIdentity } from "./identity.js";
3
+ import { type SiteIdentity, type SiteNotFound } from "./identity.js";
4
4
  import { type ContentSourceInput, type MdxComponent } from "./content.js";
5
5
  import { type SitePageProvider } from "./pages.js";
6
+ export { sitePageMarkdownFile } from "./pages.js";
6
7
  export type { SitePage, SitePageContext, SitePageProvider } from "./pages.js";
7
- export type { ContentItem } from "./content.js";
8
+ export type { ContentItem, ContentSource, ContentSourceInput } from "./content.js";
8
9
  export type { SiteFont } from "./fonts.js";
9
- export type { SiteIdentity } from "./identity.js";
10
+ export type { SiteIdentity, SiteNotFound } from "./identity.js";
10
11
  export interface SiteConfig {
11
- /** One or more roots containing `posts/`, `thoughts/`, and optional assets. */
12
- content: ContentSourceInput | readonly ContentSourceInput[];
13
- /** Workspace root used to discover public `skills/` and project `.agents/skills/`. */
14
- skillsDirectory: string;
15
- /** Static output directory for the generated site. */
16
- outputDirectory: string;
12
+ /** Roots containing posts, thoughts and optional assets. Defaults to `content/`. */
13
+ content?: ContentSourceInput | readonly ContentSourceInput[];
14
+ /** Root used to discover public `skills/`. Defaults to the current project directory. */
15
+ skillsDirectory?: string;
16
+ /** Static output directory for the generated site. Defaults to `dist/`. */
17
+ outputDirectory?: string;
17
18
  /** Defaults to this package's own Blog theme. */
18
19
  theme?: ThemeDefinition;
19
- /** Optional project root when project Skills live outside `skillsDirectory`. */
20
+ /** Include project `.agents/skills/` in the public build. Defaults to false. */
21
+ includeProjectSkills?: boolean;
22
+ /** Alternate root for `.agents/skills/` when `includeProjectSkills` is enabled. */
20
23
  projectRootDirectory?: string;
21
24
  /** Site name, description, repository links and About copy; unset fields use neutral defaults. */
22
25
  site?: Partial<SiteIdentity>;
23
- /** Consumer-supplied pages; the site writes and indexes them next to its own. */
26
+ /** Consumer-supplied pages replace the built-in site map. */
24
27
  pages?: SitePageProvider;
28
+ /** Copy for the built-in root `404.html`, or false to omit it. */
29
+ notFound?: SiteNotFound | false;
30
+ /**
31
+ * Markdown that opens `llms.txt` and `llms-full.txt` after the site summary: what an agent must
32
+ * know before following any link, such as how to install and set up what the site documents.
33
+ */
34
+ agentGuide?: string;
25
35
  /** Extra components available to MDX content pages. */
26
36
  mdxComponents?: Record<string, MdxComponent>;
27
37
  /** Stylesheet file name for the theme; defaults to "blog-theme.css". */
@@ -37,13 +47,16 @@ export interface ResolvedSiteConfig {
37
47
  skillsDirectory: string;
38
48
  outputDirectory: string;
39
49
  theme: ThemeDefinition;
50
+ includeProjectSkills?: boolean;
40
51
  projectRootDirectory?: string;
41
52
  site: SiteIdentity;
42
53
  pages?: SitePageProvider;
54
+ notFound?: SiteNotFound | false;
55
+ agentGuide?: string;
43
56
  mdxComponents: Record<string, MdxComponent>;
44
57
  themeStylesheet: string;
45
58
  fonts: readonly SiteFont[];
46
59
  }
47
60
  /** Define a site config in the app package without taking on the build pipeline. */
48
- export declare function meowSite(config: SiteConfig): ResolvedSiteConfig;
49
- export declare function buildSite(input: SiteConfig | ResolvedSiteConfig): Promise<void>;
61
+ export declare function meowSite(config?: SiteConfig): ResolvedSiteConfig;
62
+ export declare function buildSite(input?: SiteConfig | ResolvedSiteConfig): Promise<void>;
package/dist/src/build.js CHANGED
@@ -2,26 +2,35 @@ import { removeLegacyIsFallbacks } from "meowcss/bundle";
2
2
  import { blogFonts, blogTheme } from "./theme.js";
3
3
  import { FONT_EXTERNAL, publishFonts } from "./fonts.js";
4
4
  import { DEFAULT_SITE } from "./identity.js";
5
- import { mkdir, readFile, rm, writeFile, cp } from "node:fs/promises";
6
- import { isAbsolute, join, relative, resolve } from "node:path";
5
+ import { constants } from "node:fs";
6
+ import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
7
8
  import { collectContentAssets, isDraftAsset, loadSection, normalizeContentSources, } from "./content.js";
8
- import { aboutPage, agentMarkdown, DEFAULT_THEME_STYLESHEET, homePage, listPage, llmText, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
9
+ import { aboutPage, agentMarkdown, DEFAULT_THEME_STYLESHEET, homePage, listPage, llmFullText, llmText, notFoundPage, postPage, skillPage, skillsIndexPage, thoughtPage, } from "./site.js";
9
10
  import { loadSkills } from "./skills.js";
10
- import { sitePageFile } from "./pages.js";
11
+ import { sitePageFile, sitePageMarkdownFile } from "./pages.js";
12
+ import { publishSite, validateBuildDestination, validateOutputDirectory } from "./output.js";
13
+ export { sitePageMarkdownFile } from "./pages.js";
11
14
  /** Define a site config in the app package without taking on the build pipeline. */
12
- export function meowSite(config) {
15
+ export function meowSite(config = {}) {
16
+ const themeStylesheet = config.themeStylesheet ?? DEFAULT_THEME_STYLESHEET;
17
+ if (basename(themeStylesheet) !== themeStylesheet || !themeStylesheet.endsWith(".css"))
18
+ throw new Error(`Expected a theme stylesheet file name: ${themeStylesheet}`);
13
19
  return {
14
- content: normalizeContentSources(config.content),
15
- skillsDirectory: resolve(config.skillsDirectory),
16
- outputDirectory: resolve(config.outputDirectory),
20
+ content: normalizeContentSources(config.content ?? "content"),
21
+ skillsDirectory: resolve(config.skillsDirectory ?? "."),
22
+ outputDirectory: resolve(config.outputDirectory ?? "dist"),
17
23
  theme: config.theme ?? blogTheme,
24
+ includeProjectSkills: config.includeProjectSkills,
18
25
  projectRootDirectory: config.projectRootDirectory
19
26
  ? resolve(config.projectRootDirectory)
20
27
  : undefined,
21
28
  site: { ...DEFAULT_SITE, ...config.site },
22
29
  pages: config.pages,
30
+ notFound: config.notFound,
31
+ agentGuide: config.agentGuide,
23
32
  mdxComponents: config.mdxComponents ?? {},
24
- themeStylesheet: config.themeStylesheet ?? DEFAULT_THEME_STYLESHEET,
33
+ themeStylesheet,
25
34
  fonts: config.fonts ?? (config.theme === undefined || config.theme === blogTheme ? blogFonts : []),
26
35
  };
27
36
  }
@@ -31,25 +40,23 @@ async function writePage(generatedDirectory, pathname, html) {
31
40
  await writeFile(target, html, "utf8");
32
41
  return target;
33
42
  }
34
- async function stageAssets(generatedDirectory, sources) {
35
- const assets = await collectContentAssets(sources, { directory: "assets" });
36
- await Promise.all(assets
37
- .filter((asset) => !isDraftAsset(asset.sourcePath, asset.assetRoot))
38
- .map(async (asset) => {
39
- const target = join(generatedDirectory, asset.relativePath);
40
- await mkdir(resolve(target, ".."), { recursive: true });
41
- await cp(asset.sourcePath, target, { force: true });
42
- }));
43
- }
44
- async function copyPostAssets(outputDirectory, sources) {
45
- const assets = await collectContentAssets(sources, { directory: "posts/assets" });
46
- await Promise.all(assets
47
- .filter((asset) => !isDraftAsset(asset.sourcePath, asset.assetRoot))
48
- .map(async (asset) => {
49
- const target = join(outputDirectory, "posts/assets", asset.relativePath);
50
- await mkdir(resolve(target, ".."), { recursive: true });
51
- await cp(asset.sourcePath, target, { force: true });
52
- }));
43
+ async function writeAssets(destination, sources, directory) {
44
+ const assets = await collectContentAssets(sources, { directory });
45
+ for (const asset of assets) {
46
+ if (isDraftAsset(asset.sourcePath, asset.assetRoot))
47
+ continue;
48
+ const path = directory === "assets" ? asset.relativePath : join(directory, asset.relativePath);
49
+ const target = join(destination, path);
50
+ await mkdir(dirname(target), { recursive: true });
51
+ try {
52
+ await copyFile(asset.sourcePath, target, constants.COPYFILE_EXCL);
53
+ }
54
+ catch (error) {
55
+ if (error.code !== "EEXIST")
56
+ throw error;
57
+ throw new Error(`Content asset would replace a generated file: ${path}`);
58
+ }
59
+ }
53
60
  }
54
61
  async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, outputDirectory) {
55
62
  const result = await Bun.build({
@@ -58,21 +65,20 @@ async function buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, o
58
65
  outdir: outputDirectory,
59
66
  target: "browser",
60
67
  minify: true,
68
+ throw: false,
61
69
  // Published font files stay separate, so subsets load by unicode-range instead of inline.
62
70
  external: [FONT_EXTERNAL],
63
71
  });
64
- if (!result.success) {
65
- console.error(result.logs);
66
- throw new Error("Unable to build site static HTML");
67
- }
72
+ if (!result.success)
73
+ throw new Error(["Unable to build site static HTML", ...result.logs.map((log) => log.message)].join("\n"));
68
74
  // Bun writes legacy :-webkit-any()/:-moz-any() copies of every :is() rule; Safari misreads them.
69
75
  for (const output of result.outputs)
70
76
  if (output.path.endsWith(".css"))
71
77
  await writeFile(output.path, removeLegacyIsFallbacks(await readFile(output.path, "utf8")));
72
78
  }
73
79
  /** Consumer pages are written inside the generated directory and never above it. */
74
- function consumerPageFile(generatedDirectory, path) {
75
- const file = sitePageFile(path);
80
+ function consumerPageFile(generatedDirectory, path, fileFor = sitePageFile) {
81
+ const file = fileFor(path);
76
82
  const target = resolve(generatedDirectory, file);
77
83
  const inside = relative(generatedDirectory, target);
78
84
  if (!inside || inside.startsWith("..") || isAbsolute(inside)) {
@@ -87,6 +93,18 @@ async function writeConsumerPages(generatedDirectory, pages) {
87
93
  }
88
94
  return entrypoints;
89
95
  }
96
+ /** Files the build writes itself; a page's Markdown copy may not take their place. */
97
+ const RESERVED_MARKDOWN = new Set(["agents.md"]);
98
+ /** Each page's Markdown copy, written straight into the output next to its route. */
99
+ async function writeConsumerMarkdown(outputDirectory, pages) {
100
+ for (const page of pages) {
101
+ if (page.markdown === undefined)
102
+ continue;
103
+ if (RESERVED_MARKDOWN.has(sitePageMarkdownFile(page.path)))
104
+ throw new Error(`Site page Markdown would replace the agent index: ${page.path}`);
105
+ await writePage(outputDirectory, consumerPageFile(outputDirectory, page.path, sitePageMarkdownFile), page.markdown);
106
+ }
107
+ }
90
108
  async function writeContentPages(generatedDirectory, posts, thoughts, theme, site, stylesheet) {
91
109
  const entrypoints = [];
92
110
  for (const post of posts) {
@@ -97,21 +115,36 @@ async function writeContentPages(generatedDirectory, posts, thoughts, theme, sit
97
115
  }
98
116
  return entrypoints;
99
117
  }
100
- export async function buildSite(input) {
118
+ export async function buildSite(input = {}) {
101
119
  const config = meowSite(input);
102
- const generatedDirectory = resolve(config.outputDirectory, "../.generated");
120
+ await validateOutputDirectory(config);
121
+ await validateBuildDestination(config.outputDirectory);
122
+ const outputParent = dirname(config.outputDirectory);
123
+ await mkdir(outputParent, { recursive: true });
124
+ const workspace = await mkdtemp(join(outputParent, ".meowpress-build-"));
125
+ try {
126
+ const outputDirectory = join(workspace, "output");
127
+ const result = await writeSite({ ...config, outputDirectory }, join(workspace, "source"));
128
+ await validateBuildDestination(config.outputDirectory);
129
+ await publishSite(outputDirectory, config.outputDirectory);
130
+ console.log(`Built ${result} into ${config.outputDirectory}.`);
131
+ }
132
+ finally {
133
+ await rm(workspace, { recursive: true, force: true });
134
+ }
135
+ }
136
+ async function writeSite(config, generatedDirectory) {
103
137
  const [posts, thoughts, skills] = await Promise.all([
104
138
  loadSection(config.content, "post", { components: config.mdxComponents }),
105
139
  loadSection(config.content, "thought", { components: config.mdxComponents }),
106
140
  config.pages
107
141
  ? Promise.resolve([])
108
142
  : loadSkills(config.skillsDirectory, {
143
+ includeProjectSkills: config.includeProjectSkills,
109
144
  projectRootDirectory: config.projectRootDirectory,
110
145
  }),
111
146
  ]);
112
147
  const timeline = [...posts, ...thoughts].sort((left, right) => (right.date?.getTime() ?? 0) - (left.date?.getTime() ?? 0));
113
- await rm(config.outputDirectory, { recursive: true, force: true });
114
- await rm(generatedDirectory, { recursive: true, force: true });
115
148
  await mkdir(config.outputDirectory, { recursive: true });
116
149
  await mkdir(generatedDirectory, { recursive: true });
117
150
  // A consumer that supplies its own pages owns the whole site map; the package's
@@ -136,6 +169,9 @@ export async function buildSite(input) {
136
169
  writePage(generatedDirectory, "thoughts/index.html", listPage("thought", thoughts, config.theme, config.site, config.themeStylesheet)),
137
170
  writePage(generatedDirectory, "skills/index.html", skillsIndexPage(skills, config.theme, config.site, config.themeStylesheet)),
138
171
  ])), ...(await writeContentPages(generatedDirectory, posts, thoughts, config.theme, config.site, config.themeStylesheet)));
172
+ if (config.notFound !== false) {
173
+ entrypoints.push(await writePage(generatedDirectory, "404.html", notFoundPage(config.notFound ?? {}, config.theme, config.site, config.themeStylesheet)));
174
+ }
139
175
  for (const skill of skills) {
140
176
  const skillPath = skill.scope === "public" ? `skills/${skill.slug}` : `skills/project/${skill.slug}`;
141
177
  entrypoints.push(await writePage(generatedDirectory, `${skillPath}/index.html`, skillPage(skill, config.theme, config.site, config.themeStylesheet)));
@@ -145,16 +181,22 @@ export async function buildSite(input) {
145
181
  // root, next to the published font directory its rules reference.
146
182
  const fontFaces = await publishFonts(config.fonts, config.outputDirectory);
147
183
  await writeFile(join(generatedDirectory, config.themeStylesheet), fontFaces ? `${fontFaces}\n${config.theme.cssText}` : config.theme.cssText, "utf8");
148
- await stageAssets(generatedDirectory, config.content);
184
+ await writeAssets(generatedDirectory, config.content, "assets");
149
185
  await buildStaticHtmlFromEntrypoints(entrypoints, generatedDirectory, config.outputDirectory);
150
- await copyPostAssets(config.outputDirectory, config.content);
186
+ if (pages)
187
+ await writeConsumerMarkdown(config.outputDirectory, pages);
188
+ await writeAssets(config.outputDirectory, config.content, "posts/assets");
189
+ const full = pages ? llmFullText(config.site, pages, config.agentGuide) : undefined;
151
190
  await Promise.all([
152
- writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site, pages), "utf8"),
153
- writeFile(join(config.outputDirectory, "llms.txt"), llmText(posts, thoughts, config.site, pages), "utf8"),
154
- writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site, pages), "utf8"),
191
+ writeFile(join(config.outputDirectory, "llm.txt"), llmText(posts, thoughts, config.site, pages, config.agentGuide), "utf8"),
192
+ writeFile(join(config.outputDirectory, "llms.txt"), llmText(posts, thoughts, config.site, pages, config.agentGuide), "utf8"),
193
+ ...(full === undefined
194
+ ? []
195
+ : [writeFile(join(config.outputDirectory, "llms-full.txt"), full, "utf8")]),
196
+ writeFile(join(config.outputDirectory, "agents.md"), agentMarkdown(posts, thoughts, config.site, pages, config.content.some((source) => typeof source !== "string" &&
197
+ (source.postsDirectory !== undefined || source.thoughtsDirectory !== undefined))), "utf8"),
155
198
  ]);
156
- await rm(generatedDirectory, { recursive: true, force: true });
157
- console.log(pages
158
- ? `Built ${posts.length} content items and ${pages.length} site pages into site dist/.`
159
- : `Built ${posts.length} posts and ${thoughts.length} thoughts into site dist/.`);
199
+ return pages
200
+ ? `${posts.length} content items and ${pages.length} site pages`
201
+ : `${posts.length} posts and ${thoughts.length} thoughts`;
160
202
  }
package/dist/src/cli.js CHANGED
@@ -1,22 +1,113 @@
1
1
  #!/usr/bin/env bun
2
+ import { stat } from "node:fs/promises";
2
3
  import { resolve } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
- import { buildSite } from "./build.js";
5
- function configPathFromArgs(args) {
6
- const index = args.indexOf("--config");
7
- const configPath = index >= 0 ? args[index + 1] : undefined;
8
- if (!configPath || configPath.startsWith("--")) {
9
- throw new Error("Usage: meowpress --config <site-config.ts>");
5
+ import { buildSite, meowSite } from "./build.js";
6
+ import { hasBuiltSite, validateOutputDirectory } from "./output.js";
7
+ import { serveSite } from "./preview.js";
8
+ const USAGE = [
9
+ "Usage: meowpress [--config <file>]",
10
+ " meowpress build [root] [--config <file>]",
11
+ " meowpress preview [root] [--config <file>] [--port <port>]",
12
+ ].join("\n");
13
+ const CONFIG_FILES = ["meowpress.config.ts", "site.config.ts"];
14
+ function parseArgs(args) {
15
+ const command = args[0] === "preview" ? "preview" : "build";
16
+ const hasCommand = args[0] === command;
17
+ let root;
18
+ let config;
19
+ let port;
20
+ for (let index = hasCommand ? 1 : 0; index < args.length; index++) {
21
+ const arg = args[index];
22
+ if (arg === "--config" && config === undefined) {
23
+ const value = args[++index];
24
+ if (!value || value.startsWith("-"))
25
+ throw new Error("Missing file for --config");
26
+ config = value;
27
+ }
28
+ else if (arg === "--port" && command === "preview" && port === undefined) {
29
+ const value = args[++index];
30
+ port = Number(value);
31
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
32
+ throw new Error("Port must be an integer between 1 and 65535");
33
+ }
34
+ else if (hasCommand && !arg.startsWith("-") && root === undefined) {
35
+ root = arg;
36
+ }
37
+ else {
38
+ throw new Error(`Unexpected argument: ${arg}\n${USAGE}`);
39
+ }
10
40
  }
11
- return resolve(process.cwd(), configPath);
41
+ return { command, root: resolve(root ?? "."), config, port };
42
+ }
43
+ async function loadConfig(config) {
44
+ for (const candidate of config === undefined ? CONFIG_FILES : [config]) {
45
+ const configPath = resolve(candidate);
46
+ try {
47
+ if (!(await stat(configPath)).isFile())
48
+ throw new Error(`Site config is not a file: ${configPath}`);
49
+ }
50
+ catch (error) {
51
+ if (error.code !== "ENOENT")
52
+ throw error;
53
+ if (config !== undefined)
54
+ throw new Error(`Site config not found: ${configPath}`);
55
+ continue;
56
+ }
57
+ const module = (await import(pathToFileURL(configPath).href));
58
+ const site = module.default ?? module.site;
59
+ if (typeof site !== "object" || site === null || Array.isArray(site))
60
+ throw new Error(`Expected a default site config object or a named site export in ${configPath}`);
61
+ return site;
62
+ }
63
+ return {};
12
64
  }
13
65
  export async function runSiteCli(args = Bun.argv.slice(2)) {
14
- const configPath = configPathFromArgs(args);
15
- const module = (await import(pathToFileURL(configPath).href));
16
- const config = module.default ?? module.site;
17
- if (!config)
18
- throw new Error(`No default site config exported from ${configPath}`);
66
+ if (args.includes("--help") || args.includes("-h")) {
67
+ console.log(USAGE);
68
+ return;
69
+ }
70
+ const options = parseArgs(args);
71
+ if (options.root !== process.cwd()) {
72
+ if (!(await stat(options.root).catch(() => undefined))?.isDirectory())
73
+ throw new Error(`Project root is not a directory: ${options.root}`);
74
+ // Trusted config can call meowSite while loading, so it and its imports need the selected cwd.
75
+ const child = Bun.spawn([
76
+ process.execPath,
77
+ import.meta.path,
78
+ options.command,
79
+ ...(options.config ? ["--config", options.config] : []),
80
+ ...(options.port ? ["--port", String(options.port)] : []),
81
+ ], { cwd: options.root, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
82
+ const interrupt = () => child.kill("SIGINT");
83
+ const terminate = () => child.kill("SIGTERM");
84
+ process.once("SIGINT", interrupt);
85
+ process.once("SIGTERM", terminate);
86
+ try {
87
+ process.exitCode = await child.exited;
88
+ }
89
+ finally {
90
+ process.off("SIGINT", interrupt);
91
+ process.off("SIGTERM", terminate);
92
+ }
93
+ return;
94
+ }
95
+ const config = await loadConfig(options.config);
96
+ if (options.command === "preview") {
97
+ const resolved = meowSite(config);
98
+ await validateOutputDirectory(resolved);
99
+ if (!(await hasBuiltSite(resolved.outputDirectory))) {
100
+ throw new Error(`Built site not found at ${resolved.outputDirectory}. Run meowpress build first.`);
101
+ }
102
+ const server = await serveSite(resolved.outputDirectory, options.port);
103
+ console.log(`Meowpress preview: ${server.url}`);
104
+ return;
105
+ }
19
106
  await buildSite(config);
20
107
  }
21
- if (import.meta.main)
22
- await runSiteCli();
108
+ if (import.meta.main) {
109
+ await runSiteCli().catch((error) => {
110
+ console.error(error instanceof Error ? error.message : error);
111
+ process.exitCode = 1;
112
+ });
113
+ }
@@ -4,6 +4,14 @@ export type Frontmatter = Record<string, unknown>;
4
4
  /** A content root containing `posts/`, `thoughts/`, and optional asset folders. */
5
5
  export interface ContentSource {
6
6
  directory: string;
7
+ /** Markdown and MDX posts, relative to the source root. Defaults to "posts". */
8
+ postsDirectory?: string;
9
+ /** Markdown and MDX thoughts, relative to the source root. Defaults to "thoughts". */
10
+ thoughtsDirectory?: string;
11
+ /** Assets for site pages and styles, relative to the source root. Defaults to "assets". */
12
+ assetsDirectory?: string;
13
+ /** Assets copied to /posts/assets/, relative to the source root. Defaults to "posts/assets". */
14
+ postAssetsDirectory?: string;
7
15
  /** Namespace slugs and asset paths from this source to avoid collisions. */
8
16
  slugPrefix?: string;
9
17
  }
@@ -47,7 +55,7 @@ export interface CollectContentAssetsOptions {
47
55
  directory?: string;
48
56
  duplicate?: DuplicateContentPolicy;
49
57
  }
50
- export declare function normalizeContentSources(input: ContentSourceInput | readonly ContentSourceInput[]): ContentSource[];
58
+ export declare function normalizeContentSources(input: ContentSourceInput | readonly ContentSourceInput[], baseDirectory?: string): ContentSource[];
51
59
  export declare function loadSection(sources: ContentSourceInput | readonly ContentSourceInput[], kind: ContentKind, options?: LoadSectionOptions): Promise<ContentItem[]>;
52
60
  export declare function withoutLeadingHeading(html: string): string;
53
61
  export declare function collectAssetFiles(directory: string): Promise<string[]>;