@hutusi/amytis 1.13.0 → 1.15.0

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 (91) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/publish.yml +2 -2
  3. package/CHANGELOG.md +32 -0
  4. package/GEMINI.md +9 -1
  5. package/README.md +36 -2
  6. package/README.zh.md +36 -2
  7. package/TODO.md +10 -0
  8. package/bun.lock +123 -91
  9. package/content/flows/2026/03/05.md +1 -0
  10. package/content/flows/2026/03/07.md +2 -0
  11. package/content/series/modern-web-dev/index.mdx +4 -2
  12. package/content/series/rst-legacy/deeper-notes/images/test.svg +4 -0
  13. package/content/series/rst-legacy/deeper-notes/index.rst +15 -0
  14. package/content/series/rst-legacy/getting-started.rst +24 -0
  15. package/content/series/rst-legacy/index.rst +9 -0
  16. package/content/series/rst-readme/README.rst +9 -0
  17. package/content/series/rst-readme/readme-index-post.rst +10 -0
  18. package/content/series/rst-toctree/first-post.rst +6 -0
  19. package/content/series/rst-toctree/index.rst +10 -0
  20. package/content/series/rst-toctree/second-post.rst +6 -0
  21. package/content/series/rst-toctree-precedence/first-post.rst +6 -0
  22. package/content/series/rst-toctree-precedence/index.rst +12 -0
  23. package/content/series/rst-toctree-precedence/second-post.rst +6 -0
  24. package/docs/ARCHITECTURE.md +30 -4
  25. package/docs/CONTRIBUTING.md +11 -0
  26. package/docs/DIGITAL_GARDEN.md +22 -1
  27. package/eslint.config.mjs +2 -0
  28. package/next.config.ts +2 -2
  29. package/package.json +27 -21
  30. package/packages/create-amytis/package.json +1 -1
  31. package/packages/create-amytis/src/index.test.ts +43 -1
  32. package/packages/create-amytis/src/index.ts +64 -8
  33. package/public/next-image-export-optimizer-hashes.json +14 -73
  34. package/scripts/build-pagefind.ts +172 -0
  35. package/scripts/copy-assets.ts +246 -56
  36. package/scripts/generate-knowledge-graph.ts +2 -1
  37. package/scripts/new-flow.ts +1 -0
  38. package/scripts/render-rst.py +719 -0
  39. package/scripts/run-with-rst-python.ts +42 -0
  40. package/src/app/[slug]/[postSlug]/page.tsx +20 -10
  41. package/src/app/[slug]/page/[page]/page.tsx +15 -0
  42. package/src/app/all.atom/route.ts +7 -0
  43. package/src/app/all.xml/route.ts +7 -0
  44. package/src/app/archive/page.tsx +7 -4
  45. package/src/app/feed.atom/route.ts +2 -57
  46. package/src/app/feed.xml/route.ts +2 -64
  47. package/src/app/flows/[year]/[month]/[day]/page.tsx +13 -0
  48. package/src/app/flows/feed.atom/route.ts +7 -0
  49. package/src/app/flows/feed.xml/route.ts +7 -0
  50. package/src/app/globals.css +165 -0
  51. package/src/app/page.tsx +1 -0
  52. package/src/app/posts/feed.atom/route.ts +9 -0
  53. package/src/app/posts/feed.xml/route.ts +9 -0
  54. package/src/app/series/[slug]/page/[page]/page.tsx +74 -6
  55. package/src/app/series/[slug]/page.tsx +11 -13
  56. package/src/app/series/page.tsx +3 -3
  57. package/src/components/AuthorCard.tsx +25 -16
  58. package/src/components/CoverImage.tsx +5 -2
  59. package/src/components/FlowCalendarSidebar.tsx +1 -1
  60. package/src/components/FlowContent.tsx +2 -1
  61. package/src/components/FlowTimelineEntry.tsx +7 -1
  62. package/src/components/Footer.tsx +1 -1
  63. package/src/components/MarkdownRenderer.test.tsx +22 -0
  64. package/src/components/MarkdownRenderer.tsx +22 -17
  65. package/src/components/Navbar.tsx +1 -1
  66. package/src/components/PostSidebar.tsx +1 -1
  67. package/src/components/RecentNotesSection.tsx +4 -0
  68. package/src/components/RstRenderer.test.tsx +93 -0
  69. package/src/components/RstRenderer.tsx +122 -0
  70. package/src/layouts/PostLayout.tsx +5 -1
  71. package/src/layouts/SimpleLayout.tsx +10 -3
  72. package/src/lib/feed-utils.ts +158 -18
  73. package/src/lib/image-utils.test.ts +19 -0
  74. package/src/lib/image-utils.ts +11 -0
  75. package/src/lib/markdown.test.ts +140 -2
  76. package/src/lib/markdown.ts +747 -214
  77. package/src/lib/rehype-image-metadata.ts +2 -2
  78. package/src/lib/rst-renderer.test.ts +355 -0
  79. package/src/lib/rst-renderer.ts +617 -0
  80. package/src/lib/rst.test.ts +140 -0
  81. package/src/lib/rst.ts +470 -0
  82. package/src/lib/series-redirects.ts +42 -0
  83. package/tests/e2e/navigation.test.ts +26 -0
  84. package/tests/integration/collections.test.ts +17 -2
  85. package/tests/integration/feed-utils.test.ts +65 -0
  86. package/tests/integration/flow-title.test.ts +53 -0
  87. package/tests/integration/reading-time-headings.test.ts +5 -9
  88. package/tests/integration/series-draft.test.ts +16 -2
  89. package/tests/integration/series.test.ts +93 -0
  90. package/tests/tooling/build-pagefind.test.ts +66 -0
  91. package/tests/unit/static-params.test.ts +140 -0
@@ -1,4 +1,5 @@
1
1
  ---
2
+ title: 'JSDoc type comments'
2
3
  tags: ["typescript", "tooling"]
3
4
  ---
4
5
 
@@ -2,6 +2,8 @@
2
2
  tags: ["ai", "workflow"]
3
3
  ---
4
4
 
5
+ # Using Claude Code
6
+
5
7
  Been using Claude Code for a week now as a daily coding assistant. A few observations so far.
6
8
 
7
9
  It is most useful for tasks where the shape of the solution is clear but the execution is tedious — refactoring, writing tests, tracking down why a CSS rule isn't applying. For genuinely novel design problems, talking through the approach matters more than generating code.
@@ -5,9 +5,11 @@ excerpt: "A curated path through modern web development: JavaScript fundamentals
5
5
  date: "2026-03-01"
6
6
  featured: true
7
7
  items:
8
- - post: asynchronous-javascript
9
- - post: understanding-react-hooks
8
+ - post: posts/asynchronous-javascript
9
+ - post: posts/understanding-react-hooks
10
10
  - series: nextjs-deep-dive
11
+ - post: markdown-showcase/中文测试文章
12
+ - post: digital-garden/02-architecture
11
13
  ---
12
14
 
13
15
  This collection assembles the essential reading for anyone building modern web applications. Start with the JavaScript fundamentals that underpin everything, move into React patterns, then go deep on Next.js.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="160" height="80" viewBox="0 0 160 80">
2
+ <rect width="160" height="80" rx="10" fill="#d7efe3"/>
3
+ <text x="80" y="48" text-anchor="middle" font-family="sans-serif" font-size="18" fill="#21543d">rST</text>
4
+ </svg>
@@ -0,0 +1,15 @@
1
+ Deeper Notes
2
+ ============
3
+
4
+ :date: 2026-01-05
5
+ :category: Legacy
6
+ :tags: rst, notes
7
+ :authors: John Hu
8
+
9
+ This post uses a folder layout.
10
+
11
+ Images
12
+ ------
13
+
14
+ .. image:: ./images/test.svg
15
+ :alt: Test image
@@ -0,0 +1,24 @@
1
+ Getting Started With rST
2
+ ========================
3
+
4
+ :date: 2026-01-03
5
+ :excerpt: A simple rST-based post inside a legacy series.
6
+ :category: Legacy
7
+ :tags: rst, migration
8
+ :authors: John Hu
9
+ :redirectFrom: /posts/getting-started-rst
10
+
11
+ Intro paragraph with an inline link to `Amytis <https://github.com/hutusi/amytis>`_.
12
+
13
+ Overview
14
+ --------
15
+
16
+ - Keep the routing layer unchanged.
17
+ - Parse the source format at the series boundary.
18
+
19
+ Code Sample
20
+ ~~~~~~~~~~~
21
+
22
+ .. code-block:: ts
23
+
24
+ export const feature = "rst";
@@ -0,0 +1,9 @@
1
+ Rst Legacy Series
2
+ =================
3
+
4
+ :excerpt: Legacy notes imported from reStructuredText.
5
+ :authors: John Hu
6
+ :sort: manual
7
+ :posts: getting-started, deeper-notes
8
+
9
+ This is a small legacy series used to validate series-scoped rST support.
@@ -0,0 +1,9 @@
1
+ Rst README Series
2
+ =================
3
+
4
+ :excerpt: Legacy series metadata loaded from README.rst.
5
+ :authors: John Hu
6
+ :sort: manual
7
+ :posts: readme-index-post
8
+
9
+ This series uses README.rst as its series index file.
@@ -0,0 +1,10 @@
1
+ README Index Post
2
+ =================
3
+
4
+ :date: 2026-01-09
5
+ :excerpt: Post inside a series whose metadata is loaded from README.rst.
6
+ :category: Legacy
7
+ :tags: rst, readme
8
+ :authors: John Hu
9
+
10
+ Content for the README-indexed series.
@@ -0,0 +1,6 @@
1
+ First Post
2
+ ==========
3
+
4
+ :date: 2024-01-01
5
+
6
+ This post appears second in the toctree order.
@@ -0,0 +1,10 @@
1
+ Rst Toctree Series
2
+ ==================
3
+
4
+ :excerpt: Legacy series order derived from toctree.
5
+
6
+ .. toctree::
7
+ :maxdepth: 1
8
+
9
+ second-post
10
+ first-post
@@ -0,0 +1,6 @@
1
+ Second Post
2
+ ===========
3
+
4
+ :date: 2024-01-02
5
+
6
+ This post appears first in the toctree order.
@@ -0,0 +1,6 @@
1
+ First Post
2
+ ==========
3
+
4
+ :date: 2024-01-01
5
+
6
+ This post should remain first because explicit posts metadata wins.
@@ -0,0 +1,12 @@
1
+ Rst Toctree Precedence Series
2
+ =============================
3
+
4
+ :excerpt: Explicit posts metadata should override toctree order.
5
+ :sort: manual
6
+ :posts: first-post, second-post
7
+
8
+ .. toctree::
9
+ :maxdepth: 1
10
+
11
+ second-post
12
+ first-post
@@ -0,0 +1,6 @@
1
+ Second Post
2
+ ===========
3
+
4
+ :date: 2024-01-02
5
+
6
+ This post should remain second because explicit posts metadata wins.
@@ -1,20 +1,21 @@
1
1
  # Architecture Overview
2
2
 
3
- Amytis is a static-export-first Next.js 16 App Router project for Markdown/MDX publishing across posts, series, books, flows, and notes.
3
+ Amytis is a static-export-first Next.js 16 App Router project for Markdown/MDX publishing across posts, series, books, flows, and notes, with optional series-scoped rST support for legacy content.
4
4
 
5
5
  ## Core Stack
6
6
 
7
- - Framework: Next.js 16.1.6 + React 19
7
+ - Framework: Next.js 16.2.1 + React 19
8
8
  - Runtime/tooling: Bun
9
9
  - Styling: Tailwind CSS v4 + CSS variables + `next-themes`
10
10
  - Content parsing: `gray-matter` + Zod validation in `src/lib/markdown.ts`
11
+ - rST rendering: Python `docutils` bridge in `scripts/render-rst.py` plus normalization in `src/lib/rst-renderer.ts`
11
12
  - Search: Pagefind (`/pagefind/pagefind.js` loaded at runtime)
12
13
  - Tests: Bun test suites in `src/` and `tests/`
13
14
 
14
15
  ## Content Model
15
16
 
16
17
  - `content/posts/`: standalone posts (`.md/.mdx`) and folder posts (`index.mdx`)
17
- - `content/series/<slug>/`: series metadata (`index.mdx`) + series posts
18
+ - `content/series/<slug>/`: series metadata (`index.mdx` / `index.md` / `README.mdx` / `README.md` / `index.rst` / `README.rst`) + series posts in the same format
18
19
  - `content/books/<slug>/`: book metadata + chapter files
19
20
  - `content/flows/YYYY/MM/DD.(md|mdx)`: daily flow entries
20
21
  - `content/notes/`: evergreen notes
@@ -27,6 +28,8 @@ Amytis is a static-export-first Next.js 16 App Router project for Markdown/MDX p
27
28
  3. Draft/future filtering and sorting are applied (based on `site.config.ts`).
28
29
  4. Route files consume typed helpers (`getAllPosts`, `getBookData`, `getAllFlows`, `getAllNotes`, etc.).
29
30
  5. `generateStaticParams()` precomputes dynamic routes for static export.
31
+ 6. Series content format is inferred from the series index file; ambiguous or mixed-format series fail fast during content loading.
32
+ 7. When `docutils` is available, rST files are rendered to HTML through the Python bridge; if the Python runtime is unavailable, Amytis falls back to the lightweight built-in rST compatibility path.
30
33
 
31
34
  ## Route Map (App Router)
32
35
 
@@ -57,7 +60,14 @@ src/app/
57
60
  authors/[author]/page.tsx
58
61
  archive/page.tsx
59
62
  graph/page.tsx
60
- feed.xml/route.ts
63
+ feed.xml/route.ts # Main curated RSS feed
64
+ feed.atom/route.ts # Main curated Atom feed
65
+ all.xml/route.ts # Firehose RSS feed
66
+ all.atom/route.ts # Firehose Atom feed
67
+ posts/feed.xml/route.ts # Posts-only RSS feed
68
+ posts/feed.atom/route.ts # Posts-only Atom feed
69
+ flows/feed.xml/route.ts # Flows-only RSS feed
70
+ flows/feed.atom/route.ts # Flows-only Atom feed
61
71
  search.json/route.ts
62
72
  sitemap.ts
63
73
  [slug]/page.tsx # Static pages + custom series listing path
@@ -68,10 +78,16 @@ src/app/
68
78
  ## URL Routing Rules
69
79
 
70
80
  - `next.config.ts` sets `output: "export"` and `trailingSlash: true`.
81
+ - Series format is inferred from the index file:
82
+ - Markdown series: `index.md`, `index.mdx`, `README.md`, or `README.mdx`
83
+ - rST series: `index.rst` or `README.rst`
84
+ - A series may not mix Markdown and rST content files; ambiguous or mixed layouts are treated as build errors.
71
85
  - Post URLs use `getPostUrl()` in `src/lib/urls.ts`:
72
86
  - Default: `/<posts.basePath>/<post.slug>` (basePath defaults to `posts`)
73
87
  - Series auto path: `/<series.slug>/<post.slug>` when `series.autoPaths` is enabled
74
88
  - Series override: `/<series.customPaths[seriesSlug]>/<post.slug>`
89
+ - Legacy aliases declared in frontmatter `redirectFrom` are emitted as static redirect pages, so old URLs can continue resolving after a rename or path migration.
90
+ - Dynamic route handlers validate whether a request is canonical or legacy, then either render the content or return `RedirectPage`.
75
91
  - Dynamic route params should return raw segment values from `generateStaticParams()` (do not pre-encode values).
76
92
  - Links should always target concrete paths, not route placeholders such as `/posts/[slug]`.
77
93
  - When moving series posts off the default posts path, `scripts/add-series-redirects.ts` updates frontmatter `redirectFrom` entries so static redirect pages can be generated.
@@ -92,6 +108,16 @@ src/app/
92
108
  - Notes: `getAllNotes`, `getNoteBySlug`, `getNotesByTag`
93
109
  - Discovery: `buildSlugRegistry`, `getBacklinks`, `getAllTags`, `getAllAuthors`
94
110
 
111
+ ## rST Notes
112
+
113
+ - Full-fidelity rST rendering depends on a Python environment with `docutils` (and ideally `pygments`) available.
114
+ - `src/lib/rst-renderer.ts` uses `AMYTIS_RST_PYTHON` when set; otherwise it falls back to `python3`.
115
+ - Top-of-document docinfo is parsed into Amytis metadata, but it is stripped from rendered article HTML so blog-style posts do not show duplicate author/version blocks above the content.
116
+ - Supported legacy roles are normalized or degraded intentionally:
117
+ - `:doc:` resolves to local site URLs when the target exists in the imported content tree
118
+ - `:ref:` / `:numref:` prefer local anchors
119
+ - unresolved legacy roles degrade to readable inline HTML instead of docutils system-message blocks
120
+
95
121
  ## Build Pipeline
96
122
 
97
123
  1. `bun scripts/copy-assets.ts`: copy co-located media into `public/`
@@ -47,6 +47,17 @@ bun run new-flow
47
47
  bun run new-series "My Series Name"
48
48
  ```
49
49
 
50
+ Series are format-scoped:
51
+
52
+ - Markdown series use `content/series/<slug>/index.mdx` or `index.md`
53
+ - Markdown series may also use `README.mdx` or `README.md`
54
+ - rST series use `content/series/<slug>/index.rst` or `README.rst`
55
+ - Child posts inside the series must use the same format as the index file
56
+ - Mixed Markdown and rST files in one series are treated as build errors
57
+ - For full-fidelity rST rendering, install `docutils` (and `pygments` for code highlighting) in a Python environment available to the project
58
+ - If needed, set `AMYTIS_RST_PYTHON=/absolute/path/to/python` so Amytis uses a specific interpreter
59
+ - Without Python/docutils, Amytis falls back to a lightweight compatibility parser; that keeps loading/building working, but some legacy rST constructs will render with lower fidelity
60
+
50
61
  ### Creating Books
51
62
 
52
63
  Books are manually structured in `content/books/`.
@@ -37,12 +37,33 @@ The `/graph` route visualizes your entire digital garden as an interactive netwo
37
37
  - **Edges**: Represent wiki-links connecting them.
38
38
  - **Interaction**: Click a node to navigate to that page.
39
39
 
40
- ### 5. Flows (`/flows`)
40
+ ### 5. Collections (`/series`)
41
+ While a "Series" is typically a linear progression of posts (e.g., Part 1, Part 2), a **Collection** allows you to manually curate an arbitrary list of posts and other series into a single grouped page.
42
+
43
+ To create a collection, set `type: collection` in a series index file:
44
+
45
+ - **Location:** `content/series/[collection-slug]/index.mdx`
46
+ - **Frontmatter:**
47
+ ```yaml
48
+ ---
49
+ title: "Modern Web Development"
50
+ type: collection
51
+ items:
52
+ - post: posts/asynchronous-javascript # Namespaced reference to a standalone post
53
+ - post: ai-nexus-weekly/week-11 # Namespaced reference to a post inside another series
54
+ - post: understanding-react-hooks # Fallback reference (searches all posts by slug)
55
+ - series: nextjs-deep-dive # Embeds all posts from another series
56
+ ---
57
+ ```
58
+ Using a namespace (`folder/slug`) in the `post` definition prevents slug collisions and explicitly targets the exact file location.
59
+
60
+ ### 6. Flows (`/flows`)
41
61
  Flows are a stream-style collection of daily notes, micro-blogging, or imported chat logs. They are ideal for quick thoughts that don't necessarily warrant a full blog post.
42
62
 
43
63
  - **Location:** `content/flows/YYYY/MM/DD.mdx`
44
64
  - **Navigation:** Grouped by date in a timeline view.
45
65
  - **Importing:** Use `bun run new-flow-from-chat` to bring in external conversations.
66
+ - **Optional Title:** Set `title` in frontmatter or use an `# Heading` in the content to display a title alongside the date.
46
67
 
47
68
  ## How to Use
48
69
 
package/eslint.config.mjs CHANGED
@@ -16,6 +16,8 @@ const eslintConfig = defineConfig([
16
16
  "public/pagefind/**",
17
17
  // Package compiled output — not authored code
18
18
  "packages/*/dist/**",
19
+ // Local Python renderer virtualenv
20
+ ".venv-rst/**",
19
21
  ]),
20
22
  ]);
21
23
 
package/next.config.ts CHANGED
@@ -15,8 +15,8 @@ const nextConfig: NextConfig = {
15
15
  output: "export",
16
16
  images: {
17
17
  loader: "custom",
18
- imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
19
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
18
+ imageSizes: [64, 128, 256],
19
+ deviceSizes: [640, 1200, 1920],
20
20
  },
21
21
  transpilePackages: ["next-image-export-optimizer"],
22
22
  env: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hutusi/amytis",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,14 +11,18 @@
11
11
  },
12
12
  "homepage": "https://github.com/hutusi/amytis#readme",
13
13
  "private": false,
14
- "packageManager": "bun@1.3.4",
14
+ "packageManager": "bun@1.3.11",
15
15
  "scripts": {
16
- "dev": "next dev",
17
- "build": "bun scripts/copy-assets.ts && bun run build:graph && next build && next-image-export-optimizer && pagefind --site out",
18
- "build:dev": "bun scripts/copy-assets.ts && bun run build:graph && next build && pagefind --site out --output-path public/pagefind",
16
+ "dev": "bun scripts/run-with-rst-python.ts next dev",
17
+ "build": "bun scripts/copy-assets.ts && bun run build:graph && bun scripts/run-with-rst-python.ts next build && next-image-export-optimizer && bun run build:search",
18
+ "build:dev": "bun scripts/copy-assets.ts && bun run build:graph && bun scripts/run-with-rst-python.ts next build && bun run build:search:dev",
19
19
  "build:graph": "NODE_ENV=production bun scripts/generate-knowledge-graph.ts",
20
+ "build:search": "bun scripts/build-pagefind.ts --site out --output-path out/pagefind",
21
+ "build:search:dev": "bun scripts/build-pagefind.ts --site out --output-path public/pagefind",
22
+ "build:search:cli": "pagefind --site out",
23
+ "build:search:cli:dev": "pagefind --site out --output-path public/pagefind",
20
24
  "validate": "bun run lint && bun run test && bun run build:dev",
21
- "clean": "rm -rf .next out public/posts public/books public/flows",
25
+ "clean": "rm -rf .next out public/posts public/books public/flows public/images/nextImageExportOptimizer",
22
26
  "new": "bun scripts/new-post.ts",
23
27
  "new-weekly": "bun scripts/new-post.ts --series ai-nexus-weekly --md --folder --prefix weekly",
24
28
  "new-series": "bun scripts/new-series.ts",
@@ -48,16 +52,16 @@
48
52
  "github-slugger": "^2.0.0",
49
53
  "gray-matter": "^4.0.3",
50
54
  "image-size": "^2.0.2",
51
- "katex": "^0.16.33",
52
- "mermaid": "^11.12.3",
53
- "next": "16.1.6",
55
+ "katex": "^0.16.45",
56
+ "mermaid": "^11.14.0",
57
+ "next": "16.2.3",
54
58
  "next-image-export-optimizer": "^1.20.1",
55
59
  "next-themes": "^0.4.6",
56
- "react": "19.2.4",
57
- "react-dom": "19.2.4",
58
- "react-icons": "^5.5.0",
60
+ "react": "19.2.5",
61
+ "react-dom": "19.2.5",
62
+ "react-icons": "^5.6.0",
59
63
  "react-markdown": "^10.1.0",
60
- "react-syntax-highlighter": "^16.1.0",
64
+ "react-syntax-highlighter": "^16.1.1",
61
65
  "rehype-katex": "^7.0.1",
62
66
  "rehype-raw": "^7.0.0",
63
67
  "rehype-slug": "^6.0.0",
@@ -66,28 +70,30 @@
66
70
  "remark-math": "^6.0.0",
67
71
  "remark-parse": "^11.0.0",
68
72
  "remark-rehype": "^11.1.2",
73
+ "sanitize-html": "^2.17.2",
69
74
  "unified": "^11.0.5",
70
75
  "unist-util-visit": "^5.1.0",
71
76
  "zod": "^4.3.6"
72
77
  },
73
78
  "devDependencies": {
74
- "@playwright/test": "^1.58.2",
75
- "@tailwindcss/postcss": "^4.1.18",
76
- "@types/bun": "^1.3.9",
79
+ "@playwright/test": "^1.59.1",
80
+ "@tailwindcss/postcss": "^4.2.2",
81
+ "@types/bun": "^1.3.12",
77
82
  "@types/d3": "^7.4.3",
78
83
  "@types/hast": "^3.0.4",
79
84
  "@types/image-size": "^0.8.0",
80
85
  "@types/mdast": "^4.0.4",
81
- "@types/node": "^24.10.13",
86
+ "@types/node": "^24.12.2",
82
87
  "@types/react": "^19.2.14",
83
88
  "@types/react-dom": "^19.2.3",
84
89
  "@types/react-syntax-highlighter": "^15.5.13",
90
+ "@types/sanitize-html": "^2.16.1",
85
91
  "babel-plugin-react-compiler": "1.0.0",
86
- "eslint": "^9.0.0",
87
- "eslint-config-next": "16.1.6",
88
- "pagefind": "^1.4.0",
92
+ "eslint": "^9.39.4",
93
+ "eslint-config-next": "16.2.3",
94
+ "pagefind": "^1.5.0",
89
95
  "pdf-to-img": "^5.0.0",
90
- "tailwindcss": "^4.1.18",
96
+ "tailwindcss": "^4.2.2",
91
97
  "typescript": "^5.9.3"
92
98
  },
93
99
  "ignoreScripts": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-amytis",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Create a new Amytis digital garden",
5
5
  "license": "MIT",
6
6
  "bin": { "create-amytis": "./dist/index.js" },
@@ -2,7 +2,7 @@ import { describe, test, expect, afterAll } from "bun:test";
2
2
  import * as fs from "fs";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
- import { patchSiteConfig, patchPackageJson } from "./index";
5
+ import { buildExtractCommand, getArchiveMetadata, patchSiteConfig, patchPackageJson } from "./index";
6
6
 
7
7
  // Minimal site.config.ts that mirrors the real file's patchable fields.
8
8
  // Inner backticks and `${` are escaped so they appear literally in the string.
@@ -185,3 +185,45 @@ describe("patchPackageJson", () => {
185
185
  expect(() => patchPackageJson(dir, "my-garden")).not.toThrow();
186
186
  });
187
187
  });
188
+
189
+ describe("getArchiveMetadata", () => {
190
+ test("uses zip archives on Windows", () => {
191
+ const archive = getArchiveMetadata("v1.13.0", "win32");
192
+ expect(archive.kind).toBe("zip");
193
+ expect(archive.filename).toBe("amytis-v1.13.0.zip");
194
+ expect(archive.url).toBe("https://github.com/hutusi/amytis/archive/refs/tags/v1.13.0.zip");
195
+ });
196
+
197
+ test("uses tar.gz archives on non-Windows platforms", () => {
198
+ const archive = getArchiveMetadata("v1.13.0", "darwin");
199
+ expect(archive.kind).toBe("tar.gz");
200
+ expect(archive.filename).toBe("amytis-v1.13.0.tar.gz");
201
+ expect(archive.url).toBe("https://github.com/hutusi/amytis/archive/refs/tags/v1.13.0.tar.gz");
202
+ });
203
+ });
204
+
205
+ describe("buildExtractCommand", () => {
206
+ test("builds the PowerShell Expand-Archive command on Windows", () => {
207
+ const command = buildExtractCommand("C:\\tmp\\amytis-v1.13.0.zip", "C:\\tmp\\my-garden.__tmp__", "zip", "win32");
208
+ expect(command.command).toBe("powershell.exe");
209
+ expect(command.args).toEqual([
210
+ "-NoProfile",
211
+ "-NonInteractive",
212
+ "-ExecutionPolicy",
213
+ "Bypass",
214
+ "-Command",
215
+ "Expand-Archive",
216
+ "-LiteralPath",
217
+ "C:\\tmp\\amytis-v1.13.0.zip",
218
+ "-DestinationPath",
219
+ "C:\\tmp\\my-garden.__tmp__",
220
+ "-Force",
221
+ ]);
222
+ });
223
+
224
+ test("builds the tar extraction command on non-Windows platforms", () => {
225
+ const command = buildExtractCommand("/tmp/amytis-v1.13.0.tar.gz", "/tmp/my-garden.__tmp__", "tar.gz", "darwin");
226
+ expect(command.command).toBe("tar");
227
+ expect(command.args).toEqual(["xzf", "/tmp/amytis-v1.13.0.tar.gz", "-C", "/tmp/my-garden.__tmp__"]);
228
+ });
229
+ });
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as https from "https";
6
6
  import * as readline from "readline";
7
- import { execSync } from "child_process";
7
+ import { execFileSync, execSync } from "child_process";
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
10
  // Helpers
@@ -77,11 +77,67 @@ function downloadFile(url: string, dest: string): Promise<void> {
77
77
  });
78
78
  }
79
79
 
80
- function extractTarball(tarPath: string, outDir: string): void {
80
+ export function getArchiveMetadata(tag: string, platform: NodeJS.Platform = process.platform): {
81
+ url: string;
82
+ filename: string;
83
+ kind: "zip" | "tar.gz";
84
+ } {
85
+ if (platform === "win32") {
86
+ return {
87
+ url: `https://github.com/hutusi/amytis/archive/refs/tags/${tag}.zip`,
88
+ filename: `amytis-${tag}.zip`,
89
+ kind: "zip",
90
+ };
91
+ }
92
+
93
+ return {
94
+ url: `https://github.com/hutusi/amytis/archive/refs/tags/${tag}.tar.gz`,
95
+ filename: `amytis-${tag}.tar.gz`,
96
+ kind: "tar.gz",
97
+ };
98
+ }
99
+
100
+ export function buildExtractCommand(
101
+ archivePath: string,
102
+ tmpDir: string,
103
+ kind: "zip" | "tar.gz",
104
+ platform: NodeJS.Platform = process.platform,
105
+ ): { command: string; args: string[] } {
106
+ if (kind === "zip") {
107
+ if (platform !== "win32") {
108
+ throw new Error("ZIP extraction is only supported on Windows in create-amytis");
109
+ }
110
+
111
+ return {
112
+ command: "powershell.exe",
113
+ args: [
114
+ "-NoProfile",
115
+ "-NonInteractive",
116
+ "-ExecutionPolicy",
117
+ "Bypass",
118
+ "-Command",
119
+ "Expand-Archive",
120
+ "-LiteralPath",
121
+ archivePath,
122
+ "-DestinationPath",
123
+ tmpDir,
124
+ "-Force",
125
+ ],
126
+ };
127
+ }
128
+
129
+ return {
130
+ command: "tar",
131
+ args: ["xzf", archivePath, "-C", tmpDir],
132
+ };
133
+ }
134
+
135
+ function extractArchive(archivePath: string, outDir: string, kind: "zip" | "tar.gz", platform: NodeJS.Platform = process.platform): void {
81
136
  // Extract into a temp dir, then move the inner folder out
82
137
  const tmpDir = `${outDir}.__tmp__`;
83
138
  fs.mkdirSync(tmpDir, { recursive: true });
84
- execSync(`tar xzf "${tarPath}" -C "${tmpDir}"`);
139
+ const { command, args } = buildExtractCommand(archivePath, tmpDir, kind, platform);
140
+ execFileSync(command, args, { stdio: "inherit" });
85
141
 
86
142
  // The tarball unpacks to a single top-level dir like "amytis-1.2.0/"
87
143
  const entries = fs.readdirSync(tmpDir);
@@ -91,7 +147,7 @@ function extractTarball(tarPath: string, outDir: string): void {
91
147
  const innerDir = path.join(tmpDir, entries[0]);
92
148
  fs.renameSync(innerDir, outDir);
93
149
  fs.rmdirSync(tmpDir);
94
- fs.unlinkSync(tarPath);
150
+ fs.unlinkSync(archivePath);
95
151
  }
96
152
 
97
153
  // ---------------------------------------------------------------------------
@@ -180,14 +236,14 @@ async function main(): Promise<void> {
180
236
  console.log(` Found: ${tag}`);
181
237
 
182
238
  // 3. Download tarball
183
- const tarUrl = `https://github.com/hutusi/amytis/archive/refs/tags/${tag}.tar.gz`;
184
- const tarDest = path.join(process.cwd(), `amytis-${tag}.tar.gz`);
239
+ const archive = getArchiveMetadata(tag);
240
+ const archiveDest = path.join(process.cwd(), archive.filename);
185
241
  console.log("Downloading tarball...");
186
- await downloadFile(tarUrl, tarDest);
242
+ await downloadFile(archive.url, archiveDest);
187
243
 
188
244
  // 4. Extract
189
245
  console.log("Extracting...");
190
- extractTarball(tarDest, targetDir);
246
+ extractArchive(archiveDest, targetDir, archive.kind);
191
247
  console.log(` Scaffolded: ${targetDir}`);
192
248
 
193
249
  // 5-6. Prompt for site metadata