create-shibumi 0.2.8 → 0.2.9

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 (30) hide show
  1. package/package.json +1 -1
  2. package/scripts/ship.lock.json +2 -2
  3. package/src/templates/blog/agents.md +3 -1
  4. package/src/templates/blog/bun.lock +1 -0
  5. package/src/templates/blog/package.json +2 -1
  6. package/src/templates/blog/public/favicon.svg +1 -0
  7. package/src/templates/blog/public/style.css +77 -4
  8. package/src/templates/blog/src/components/BaseHead.astro +4 -0
  9. package/src/templates/blog/src/content/blog/one-vps-is-plenty.md +7 -5
  10. package/src/templates/blog/src/content/blog/own-your-source.md +9 -5
  11. package/src/templates/blog/src/content/blog/writing-for-agents.md +7 -5
  12. package/src/templates/blog/src/content.config.ts +3 -0
  13. package/src/templates/blog/src/layouts/Base.astro +12 -1
  14. package/src/templates/blog/src/pages/index.astro +5 -2
  15. package/src/templates/blog/src/pages/llms.txt.ts +1 -1
  16. package/src/templates/blog/src/pages/posts/[id].astro +2 -1
  17. package/src/templates/blog/src/pages/posts/[id].md.ts +1 -1
  18. package/src/templates/blog/src/pages/rss.xml.ts +1 -1
  19. package/src/templates/full-stack/public/favicon.svg +1 -0
  20. package/src/templates/full-stack/public/style.css +5 -6
  21. package/src/templates/full-stack/src/app.ts +5 -3
  22. package/src/templates/ship.ts +27 -6
  23. package/src/templates/static/bun.lock +25 -0
  24. package/src/templates/static/package.json +4 -1
  25. package/src/templates/static/public/favicon.svg +1 -0
  26. package/src/templates/static/public/index.html +11 -3
  27. package/src/templates/static/public/style.css +66 -20
  28. package/src/templates/web/public/favicon.svg +1 -0
  29. package/src/templates/web/public/style.css +5 -6
  30. package/src/templates/web/src/app.ts +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shibumi",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Scaffold a Shibumi Stack project: Bun, Hono, Zod, Drizzle, SQLite, Alpine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
1
  {
2
- "url": "https://shibumistack.dev/ship/v46.ts",
3
- "sha256": "bf1a6efb177a8a5e20e36b7e2c0bcec6bc5a8598dfd4f86e7ff129af92a0e648"
2
+ "url": "https://shibumistack.dev/ship/v47.ts",
3
+ "sha256": "7c4bee760498bea9f857934088e9fc1e3c1b835dfd49deb382d103248adedc63"
4
4
  }
@@ -14,7 +14,9 @@ bun ship # verify dist/, build the static image, upload, deploy
14
14
 
15
15
  ## Routes and templates
16
16
 
17
- - Posts are markdown files in `src/content/blog/`; the schema in `src/content.config.ts` requires title (max 60), description (50 to 160 characters, drives meta/RSS/llms.txt), date, optional ogImage.
17
+ - Posts are markdown files in `src/content/blog/`; the schema in `src/content.config.ts` requires title (max 60), description (50 to 160 characters, drives meta/RSS/llms.txt), date, optional ogImage, and an optional `draft` flag (default false).
18
+ - `draft: true` keeps a post out of everything: no page, no listing, no RSS, no llms.txt, no markdown alternate. Publish by removing the flag and running `bun ship`.
19
+ - Images: put files in `src/assets/` and reference them relatively from the markdown (`![alt](../../assets/photo.jpg)`); Astro optimizes them at build. Files in `public/` are served verbatim by URL.
18
20
  - `src/pages/index.astro` lists posts; `src/pages/posts/[id].astro` renders one; `src/layouts/Base.astro` and `src/components/BaseHead.astro` own the frame and every meta tag.
19
21
  - Generated endpoints: `/rss.xml`, `/llms.txt`, `/robots.txt`, and a markdown alternate for every post at `/posts/<id>.md`. All derive from the content collection; never hand-edit their output.
20
22
  - Site identity lives in `src/site.ts`. Design lives in `public/style.css` (one file, shibumi tokens, semantic selectors).
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "devDependencies": {
13
13
  "@astrojs/check": "0.9.10",
14
+ "@clack/prompts": "1.7.0",
14
15
  "typescript": "5.9.2",
15
16
  },
16
17
  },
@@ -9,7 +9,7 @@
9
9
  "check": "astro check",
10
10
  "preview": "astro preview",
11
11
  "ship": "bun scripts/ship.ts",
12
- "ship:setup": "bun scripts/ship.ts --setup --static --output-dir dist --build-script build",
12
+ "ship:setup": "bun scripts/ship.ts --setup --static --output-dir dist --build-script build --no-spa",
13
13
  "ship:update": "bun scripts/ship.ts --update",
14
14
  "ship:status": "bun scripts/ship.ts --status",
15
15
  "ship:logs": "bun scripts/ship.ts --logs"
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@astrojs/check": "0.9.10",
24
+ "@clack/prompts": "1.7.0",
24
25
  "typescript": "5.9.2"
25
26
  },
26
27
  "engines": {
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>text{fill:#e95f19;font-family:"Hiragino Sans","Noto Sans JP",sans-serif}@media (prefers-color-scheme:dark){text{fill:#ff8648}}</style><text x="50" y="55" font-size="62" text-anchor="middle" dominant-baseline="central">渋</text></svg>
@@ -36,14 +36,27 @@
36
36
  line-height: 1.6;
37
37
 
38
38
  > header {
39
- border-bottom: 1px solid var(--hairline);
40
- padding-bottom: 1rem;
39
+ max-width: 40.5rem;
40
+ margin: 0 auto;
41
+ padding-top: 1rem;
41
42
 
42
43
  a {
44
+ display: inline-flex;
45
+ align-items: center;
46
+ gap: 0.7rem;
43
47
  color: var(--ink);
44
- font-weight: 600;
48
+ font-family: ui-serif, Georgia, "Iowan Old Style", Cambria, serif;
49
+ font-size: 1.25rem;
50
+ font-weight: 500;
51
+ letter-spacing: 0.01em;
45
52
  text-decoration: none;
46
53
  }
54
+
55
+ .mark-badge {
56
+ color: var(--accent);
57
+ font-family: "Hiragino Sans", "Noto Sans JP", sans-serif;
58
+ font-size: 1.125rem;
59
+ }
47
60
  }
48
61
 
49
62
  > main {
@@ -53,7 +66,6 @@
53
66
  }
54
67
 
55
68
  > footer {
56
- border-top: 1px solid var(--hairline);
57
69
  padding-block: 4rem;
58
70
  text-align: center;
59
71
  color: var(--muted);
@@ -145,3 +157,64 @@
145
157
  margin-top: 0;
146
158
  }
147
159
  }
160
+
161
+ /* Post images: full column width, soft corners, credit line stays quiet. */
162
+ article img {
163
+ display: block;
164
+ width: 100%;
165
+ height: auto;
166
+ border-radius: 0.6rem;
167
+ opacity: 0.9;
168
+ transition: opacity 200ms ease;
169
+ }
170
+
171
+ article img:hover,
172
+ article img:focus-visible {
173
+ opacity: 1;
174
+ }
175
+
176
+ article small {
177
+ display: block;
178
+ margin-top: 0.5rem;
179
+ color: var(--muted);
180
+ font-size: 0.8rem;
181
+ }
182
+
183
+ .home-title {
184
+ display: flex;
185
+ align-items: baseline;
186
+ gap: 1rem;
187
+ }
188
+
189
+ .home-title .mark-badge {
190
+ color: var(--accent);
191
+ font-family: "Hiragino Sans", "Noto Sans JP", sans-serif;
192
+ font-size: 0.42em;
193
+ }
194
+
195
+ .post-end {
196
+ margin-top: 4rem;
197
+ }
198
+
199
+ .ghost-pill {
200
+ display: inline-block;
201
+ padding: 0.5rem 1.3rem;
202
+ border: 1px solid var(--hairline);
203
+ border-radius: 999px;
204
+ color: var(--muted);
205
+ text-decoration: none;
206
+ transition: color 160ms ease, border-color 160ms ease;
207
+ }
208
+
209
+ .ghost-pill:hover,
210
+ .ghost-pill:focus-visible {
211
+ color: var(--accent);
212
+ border-color: var(--accent);
213
+ }
214
+
215
+ article :not(pre) > code {
216
+ padding: 0.12em 0.42em;
217
+ border-radius: 0.35rem;
218
+ background: color-mix(in srgb, var(--ink) 6%, transparent);
219
+ font-size: 0.85em;
220
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { ClientRouter } from "astro:transitions";
2
3
  import { SITE } from "../site";
3
4
 
4
5
  interface Props {
@@ -16,6 +17,9 @@ const isArticle = Boolean(publishedTime);
16
17
  ---
17
18
 
18
19
  <meta charset="utf-8" />
20
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
21
+ <meta name="generator" content="create-shibumi (shibumistack.dev)" />
22
+ <ClientRouter />
19
23
  <meta name="viewport" content="width=device-width, initial-scale=1" />
20
24
  <title>{title}</title>
21
25
  <meta name="description" content={description} />
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  title: One VPS is plenty
3
- description: A five-euro server runs more blogs than most of us will ever write. The hard part was never capacity; it was confidence.
3
+ description: This blog is static files in a 1.4 MB container on a shared five-euro VPS. The deploy script, not the hardware, is what platforms were selling.
4
4
  date: 2026-07-15
5
5
  ---
6
6
 
7
- This site is static files behind a 1.4 MB container on a small VPS. It could serve every reader I will ever have from a machine that also runs four other projects.
7
+ This site is static files behind a 1.4 MB container on a small VPS that also runs four other projects. A five-euro machine serves this traffic without noticing.
8
8
 
9
- What kept people off their own servers was never capacity. It was the deploy story: what happens when the build breaks, how you roll back, whether the thing you tested is the thing that shipped. Platforms answered those questions and charged rent on the answer.
9
+ People stayed on platforms for the deploy story: what happens when the build breaks, how to roll back, whether the thing you tested is the thing that shipped.
10
10
 
11
- The answers turn out to be portable. Build the exact commit, verify the output, ship the image, health-check before cutover, keep the previous image for rollback. None of that requires a platform; it requires a script you can read.
11
+ ![A single stack of balanced stones by the sea](https://images.pexels.com/photos/289586/pexels-photo-289586.jpeg?auto=compress&cs=tinysrgb&w=1200)
12
12
 
13
- Rent is fine. Owning is calmer.
13
+ <small>Photo via [Pexels](https://www.pexels.com/photo/289586/)</small>
14
+
15
+ Those answers fit in a script. Build the exact commit, verify the output, upload the image over SSH, health-check before cutover, keep the previous image for rollback. `bun ship` runs all of it, and the script is short enough to read before you trust it.
@@ -1,13 +1,17 @@
1
1
  ---
2
2
  title: Own your source
3
- description: Frameworks lend you abstractions and charge interest. Generated code you can read is a debt-free way to start a project.
3
+ description: This starter copies routes, styles, and config into your repository as plain files. What that costs you, and what it saves, six months in.
4
4
  date: 2026-08-01
5
5
  ---
6
6
 
7
- Most starters hand you a dependency. This one hands you files.
7
+ This starter does not install itself as a dependency. It copies files into your repository and gets out of the way.
8
8
 
9
- The difference shows up six months in. When behavior lives in a package, changing it means reading someone else's issue tracker. When behavior lives in your repository, changing it means editing a file you have already read.
9
+ The difference shows up six months in. To change behavior that lives in a package, you read someone else's issue tracker and wait for a release. To change behavior that lives in your repository, you edit a file you have already read.
10
10
 
11
- Generated code has a bad reputation because generators used to produce piles you could not maintain. The fix is not less generation; it is generating less. A route file, a layout, a stylesheet with five variables. Small enough to read in one sitting, which means small enough to own.
11
+ ![Six calligraphy brushes laid out on paper](https://images.pexels.com/photos/6315632/pexels-photo-6315632.jpeg?auto=compress&cs=tinysrgb&w=1200)
12
12
 
13
- There is a test for this: delete the tool that made the project. If the project still builds, still deploys, and still makes sense, you own it. If not, you rented it.
13
+ <small>Photo via [Pexels](https://www.pexels.com/photo/close-up-of-ink-brushes-6315632/)</small>
14
+
15
+ Generated code earned its bad reputation from generators that produced more code than anyone could maintain. So this one generates little: a route file, a layout, a stylesheet with five variables. Reading all of it takes one sitting.
16
+
17
+ One test tells you whether you own a project: delete the tool that made it. This site still builds, still deploys, and still makes sense with create-shibumi gone.
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  title: Writing for readers who curl
3
- description: Half your readers are now programs. Serving markdown alternates and an llms.txt costs one endpoint and respects both audiences.
3
+ description: Every post here is served twice, as rendered HTML and as plain markdown, with an llms.txt index. One endpoint covers the agent readers.
4
4
  date: 2026-06-20
5
5
  ---
6
6
 
7
- Someone will read this post in a browser. Something will read it with an HTTP client and a token budget.
7
+ Half the readers of this post are agents: HTTP clients with a token budget instead of a viewport.
8
8
 
9
- The polite response is to serve both well. Every post here exists twice: the HTML you may be reading, and a plain markdown file at the same path with `.md` on the end. An `llms.txt` at the root lists them all with one-line summaries, so an agent can survey the whole site in a single small request instead of scraping navigation markup.
9
+ So every post here exists twice. There is the HTML you may be reading, and a plain markdown file at the same URL with `.md` appended. An `llms.txt` at the root lists every post with a one-line summary, so an agent can survey the whole site in one small request instead of scraping navigation markup.
10
10
 
11
- This is not much work. The markdown already exists; the build just refrains from destroying it. One endpoint, a dozen lines.
11
+ ![A hand writing calligraphy with a brush and black ink](https://images.pexels.com/photos/9478289/pexels-photo-9478289.jpeg?auto=compress&cs=tinysrgb&w=1200)
12
12
 
13
- Formats are a courtesy. HTML for eyes, markdown for context windows, RSS for the patient. The content is the same; only the reader changes.
13
+ <small>Photo via [Pexels](https://www.pexels.com/photo/calligraphy-using-a-blank-ink-and-paint-brush-9478289/)</small>
14
+
15
+ The markdown already existed; the build publishes it instead of throwing it away. One endpoint, about a dozen lines of it.
@@ -11,6 +11,9 @@ const blog = defineCollection({
11
11
  description: z.string().min(50).max(160),
12
12
  date: z.coerce.date(),
13
13
  ogImage: z.string().optional(),
14
+ // Draft posts build nowhere: no page, no listing, no RSS, no llms.txt.
15
+ // Flip to false (or remove the line) to publish.
16
+ draft: z.boolean().default(false),
14
17
  }),
15
18
  });
16
19
 
@@ -2,6 +2,8 @@
2
2
  import BaseHead from "../components/BaseHead.astro";
3
3
  import { SITE } from "../site";
4
4
 
5
+ const isHome = Astro.url.pathname === "/";
6
+
5
7
  interface Props {
6
8
  title: string;
7
9
  description: string;
@@ -17,7 +19,16 @@ interface Props {
17
19
  <BaseHead {...Astro.props} />
18
20
  </head>
19
21
  <body>
20
- <header><a href="/">{SITE.name}</a></header>
22
+ {
23
+ !isHome && (
24
+ <header>
25
+ <a href="/" class="mark">
26
+ <span class="mark-badge" transition:name="site-mark">渋み</span>
27
+ <span transition:name="site-title">{SITE.name}</span>
28
+ </a>
29
+ </header>
30
+ )
31
+ }
21
32
  <main>
22
33
  <slot />
23
34
  </main>
@@ -3,7 +3,7 @@ import { getCollection } from "astro:content";
3
3
  import Base from "../layouts/Base.astro";
4
4
  import { SITE } from "../site";
5
5
 
6
- const posts = (await getCollection("blog")).sort(
6
+ const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
7
7
  (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
8
8
  );
9
9
  const formatDate = (date: Date) =>
@@ -12,7 +12,10 @@ const formatDate = (date: Date) =>
12
12
 
13
13
  <Base title={SITE.name} description={SITE.description}>
14
14
  <article>
15
- <h1>{SITE.name}</h1>
15
+ <h1 class="home-title">
16
+ <span class="mark-badge" transition:name="site-mark">渋み</span>
17
+ <span transition:name="site-title">{SITE.name}</span>
18
+ </h1>
16
19
  <ul class="posts">
17
20
  {
18
21
  posts.map((post) => (
@@ -5,7 +5,7 @@ import { getCollection } from "astro:content";
5
5
  import { SITE } from "../site";
6
6
 
7
7
  export const GET: APIRoute = async (context) => {
8
- const posts = (await getCollection("blog")).sort(
8
+ const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
9
9
  (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
10
10
  );
11
11
  const lines = [
@@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content";
3
3
  import Base from "../../layouts/Base.astro";
4
4
 
5
5
  export async function getStaticPaths() {
6
- return (await getCollection("blog")).map((post) => ({
6
+ return (await getCollection("blog", ({ data }) => !data.draft)).map((post) => ({
7
7
  params: { id: post.id },
8
8
  props: { post },
9
9
  }));
@@ -27,5 +27,6 @@ const formatDate = (date: Date) =>
27
27
  <h1>{post.data.title}</h1>
28
28
  <p class="lede">{post.data.description}</p>
29
29
  <Content />
30
+ <p class="post-end"><a class="ghost-pill" href="/">← Back to the blog</a></p>
30
31
  </article>
31
32
  </Base>
@@ -4,7 +4,7 @@ import { getCollection, getEntry } from "astro:content";
4
4
  import type { APIRoute } from "astro";
5
5
 
6
6
  export async function getStaticPaths() {
7
- return (await getCollection("blog")).map((post) => ({ params: { id: post.id } }));
7
+ return (await getCollection("blog", ({ data }) => !data.draft)).map((post) => ({ params: { id: post.id } }));
8
8
  }
9
9
 
10
10
  export const GET: APIRoute = async ({ params }) => {
@@ -4,7 +4,7 @@ import { getCollection } from "astro:content";
4
4
  import { SITE } from "../site";
5
5
 
6
6
  export const GET: APIRoute = async (context) => {
7
- const posts = (await getCollection("blog")).sort(
7
+ const posts = (await getCollection("blog", ({ data }) => !data.draft)).sort(
8
8
  (a, b) => b.data.date.valueOf() - a.data.date.valueOf()
9
9
  );
10
10
  return rss({
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>text{fill:#e95f19;font-family:"Hiragino Sans","Noto Sans JP",sans-serif}@media (prefers-color-scheme:dark){text{fill:#ff8648}}</style><text x="50" y="55" font-size="62" text-anchor="middle" dominant-baseline="central">渋</text></svg>
@@ -89,7 +89,7 @@ body {
89
89
  border-top: 1px solid var(--line);
90
90
  }
91
91
 
92
- main .endpoints a {
92
+ main .endpoints a:not(.btn):not(.button) {
93
93
  display: flex;
94
94
  align-items: baseline;
95
95
  justify-content: space-between;
@@ -101,13 +101,12 @@ main .endpoints a {
101
101
  transition: background 160ms ease;
102
102
  }
103
103
 
104
- main .endpoints a:hover,
105
- main .endpoints a:focus-visible {
104
+ main .endpoints a:not(.btn):not(.button):hover,
105
+ main .endpoints a:not(.btn):not(.button):focus-visible {
106
106
  background: var(--faint);
107
107
  }
108
108
 
109
- .endpoint-path {
110
- font-family: var(--font-mono);
109
+ main .endpoints code {
111
110
  font-size: var(--text-sm);
112
111
  }
113
112
  /* Inline code as rounded chips on the faint layer. */
@@ -120,7 +119,7 @@ main .endpoints a:focus-visible {
120
119
  }
121
120
 
122
121
 
123
- main .endpoints a:hover .endpoint-path {
122
+ main .endpoints a:hover code {
124
123
  color: var(--accent);
125
124
  }
126
125
 
@@ -107,6 +107,8 @@ const page = `<!doctype html>
107
107
  <meta charset="utf-8" />
108
108
  <meta name="viewport" content="width=device-width, initial-scale=1" />
109
109
  <title>Full Stack app · shibumi</title>
110
+ <link rel="icon" type="image/svg+xml" href="/public/favicon.svg" />
111
+ <meta name="generator" content="create-shibumi (shibumistack.dev)" />
110
112
  <link rel="stylesheet" href="/public/vendor/shibumi.css" />
111
113
  <link rel="stylesheet" href="/public/style.css" />
112
114
  <!-- app.js must load before Alpine so the alpine:init listener exists
@@ -125,9 +127,9 @@ const page = `<!doctype html>
125
127
  <span class="demo-hint">stored in SQLite; reloads and deploys keep it</span>
126
128
  </section>
127
129
  <ul class="endpoints">
128
- <li><a href="/api/hello?name=you"><span class="endpoint-path">GET /api/hello</span><span>query validated with Zod</span></a></li>
129
- <li><a href="/api/notes"><span class="endpoint-path">GET /api/notes</span><span>Drizzle read from SQLite on the data volume</span></a></li>
130
- <li><a href="/healthz"><span class="endpoint-path">GET /healthz</span><span>the check every deploy waits for</span></a></li>
130
+ <li><a href="/api/hello?name=you"><code>GET /api/hello</code><span>query validated with Zod</span></a></li>
131
+ <li><a href="/api/notes"><code>GET /api/notes</code><span>Drizzle read from SQLite on the data volume</span></a></li>
132
+ <li><a href="/healthz"><code>GET /healthz</code><span>the check every deploy waits for</span></a></li>
131
133
  </ul>
132
134
  <footer class="colophon">
133
135
  <p>This page lives in <code>src/app.ts</code>. House rules for coding agents are in <code>agents.md</code>. Add features with <code>bun shi add auth</code>.</p>
@@ -25,7 +25,7 @@ const SERVER_HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
25
25
  const COMMIT = /^[a-f0-9]{40}$/;
26
26
  const SERVER_CLI = "~/.local/bin/shibumi-server";
27
27
  const LATEST_SOURCE = "https://shibumistack.dev/ship/latest.ts";
28
- const CURRENT_SOURCE = "https://shibumistack.dev/ship/v46.ts";
28
+ const CURRENT_SOURCE = "https://shibumistack.dev/ship/v47.ts";
29
29
  let sshControlDirectory: string | undefined;
30
30
  let sshControlTarget: string | undefined;
31
31
 
@@ -96,6 +96,7 @@ interface ShipOptions {
96
96
  outputDir?: string;
97
97
  buildScript?: string;
98
98
  spa: boolean;
99
+ noSpa?: boolean;
99
100
  }
100
101
 
101
102
  export interface StaticSiteConfig {
@@ -160,6 +161,7 @@ export function parseShipArgs(args: string[]): ShipOptions {
160
161
  else if (argument === "--yes" || argument === "-y") parsed.yes = true;
161
162
  else if (argument === "--static") parsed.staticSite = true;
162
163
  else if (argument === "--spa") parsed.spa = true;
164
+ else if (argument === "--no-spa") parsed.noSpa = true;
163
165
  else if (argument === "--server" || argument === "--domain" || argument === "--trigger" || argument === "--output-dir" || argument === "--build-script") {
164
166
  const value = args[index + 1];
165
167
  if (!value || value.startsWith("-")) throw new Error(`${argument} requires a value`);
@@ -175,8 +177,9 @@ export function parseShipArgs(args: string[]): ShipOptions {
175
177
  if ([parsed.setup, parsed.update, parsed.rollback, parsed.logs, parsed.status, parsed.dev].filter(Boolean).length > 1) throw new Error("choose only one ship action");
176
178
  if (parsed.rebuild && (parsed.setup || parsed.update || parsed.rollback || parsed.logs || parsed.status || parsed.dev)) throw new Error("--rebuild applies only to shipping");
177
179
  if (parsed.trigger && !parsed.setup) throw new Error("--trigger requires --setup");
178
- if ((parsed.staticSite || parsed.outputDir || parsed.buildScript || parsed.spa) && !parsed.setup) throw new Error("--static, --output-dir, --build-script, and --spa require --setup");
179
- if ((parsed.outputDir || parsed.buildScript || parsed.spa) && !parsed.staticSite) throw new Error("--output-dir, --build-script, and --spa require --static");
180
+ if (parsed.spa && parsed.noSpa) throw new Error("--spa and --no-spa are mutually exclusive");
181
+ if ((parsed.staticSite || parsed.outputDir || parsed.buildScript || parsed.spa || parsed.noSpa) && !parsed.setup) throw new Error("--static, --output-dir, --build-script, and --spa require --setup");
182
+ if ((parsed.outputDir || parsed.buildScript || parsed.spa || parsed.noSpa) && !parsed.staticSite) throw new Error("--output-dir, --build-script, and --spa require --static");
180
183
  if (parsed.outputDir) {
181
184
  const problem = staticOutputDirProblem(parsed.outputDir);
182
185
  if (problem) throw new Error(problem);
@@ -483,7 +486,6 @@ CMD ["bun", "run", "start"]
483
486
  environment:
484
487
  HOST: 0.0.0.0
485
488
  PORT: "3000"
486
- init: true
487
489
  restart: unless-stopped
488
490
  healthcheck:
489
491
  test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
@@ -657,7 +659,6 @@ ENTRYPOINT ["/busybox", "httpd", "-f", "-p", "3000", "-h", "/www", "-c", "/www/h
657
659
  - "127.0.0.1:\${SHIBUMI_PORT:-9001}:3000"
658
660
  labels:
659
661
  ${staticComposeLabels(config)}
660
- init: true
661
662
  restart: unless-stopped
662
663
  healthcheck:
663
664
  test: ${healthcheck}
@@ -806,6 +807,7 @@ async function prepareCompose(): Promise<boolean> {
806
807
 
807
808
  if (wantStatic) {
808
809
  await generateStaticDeployment();
810
+ if (await offerGeneratedCommit(["Dockerfile", "compose.yaml", ".dockerignore", "scripts/static-server.ts", "package.json", "bun.lock"])) return false;
809
811
  outro("Review generated deployment files.\n\nNext: commit and push these changes, then run bun ship:setup.");
810
812
  return true;
811
813
  }
@@ -824,6 +826,8 @@ async function prepareCompose(): Promise<boolean> {
824
826
  written.push(name);
825
827
  }
826
828
  log.success(`Generated ${written.join(", ")}`);
829
+ log.info("Verify the app binds to 0.0.0.0 and reads PORT before shipping.");
830
+ if (await offerGeneratedCommit(written)) return false;
827
831
  outro("Review generated deployment files and verify app binds to 0.0.0.0 and reads PORT.\n\nNext: commit and push these changes, then run bun ship:setup.");
828
832
  return true;
829
833
  }
@@ -836,6 +840,23 @@ const SHIP_SCRIPTS = {
836
840
  "ship:logs": "bun scripts/ship.ts --logs",
837
841
  };
838
842
 
843
+
844
+ // After generating deployment files interactively, offer to commit and push
845
+ // them in the same run so setup continues without a manual rerun. Returns
846
+ // true when the files are committed and pushed.
847
+ async function offerGeneratedCommit(files: string[]): Promise<boolean> {
848
+ if (agentRun || !process.stdin.isTTY || !process.stdout.isTTY) return false;
849
+ const accepted = await confirm({ message: "Commit and push the generated files, then continue setup?", initialValue: true });
850
+ if (isCancel(accepted) || !accepted) return false;
851
+ const present: string[] = [];
852
+ for (const file of files) if (await Bun.file(join(root, file)).exists()) present.push(file);
853
+ await run(["git", "add", "--", ...present]);
854
+ await run(["git", "commit", "--only", "-m", "Add deployment configuration", "--", ...present], { inherit: true });
855
+ await run(["git", "push"], { inherit: true });
856
+ log.success("Committed and pushed deployment files");
857
+ return true;
858
+ }
859
+
839
860
  async function generateStaticDeployment(): Promise<void> {
840
861
  // Script-less generators (Jekyll) have no package.json; create a minimal one
841
862
  // so bun ship commands and an optional build script have a home.
@@ -880,7 +901,7 @@ async function generateStaticDeployment(): Promise<void> {
880
901
  if (problem) throw new Error(problem);
881
902
 
882
903
  let spa = options.spa;
883
- if (!spa && !agentRun && !options.yes) {
904
+ if (!spa && !options.noSpa && !agentRun && !options.yes) {
884
905
  const answer = await confirm({ message: "Single-page app? (unknown paths serve index.html)", initialValue: false });
885
906
  if (isCancel(answer)) throw new Error("setup cancelled");
886
907
  spa = answer;
@@ -0,0 +1,25 @@
1
+ {
2
+ "lockfileVersion": 2,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "shibumi-static-site",
7
+ "devDependencies": {
8
+ "@clack/prompts": "1.7.0",
9
+ },
10
+ },
11
+ },
12
+ "packages": {
13
+ "@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
14
+
15
+ "@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
16
+
17
+ "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
18
+
19
+ "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
20
+
21
+ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
22
+
23
+ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
24
+ }
25
+ }
@@ -6,12 +6,15 @@
6
6
  "scripts": {
7
7
  "dev": "bun scripts/preview.ts",
8
8
  "ship": "bun scripts/ship.ts",
9
- "ship:setup": "bun scripts/ship.ts --setup --static --output-dir public",
9
+ "ship:setup": "bun scripts/ship.ts --setup --static --output-dir public --no-spa",
10
10
  "ship:update": "bun scripts/ship.ts --update",
11
11
  "ship:status": "bun scripts/ship.ts --status",
12
12
  "ship:logs": "bun scripts/ship.ts --logs"
13
13
  },
14
14
  "engines": {
15
15
  "bun": ">=1.4.0"
16
+ },
17
+ "devDependencies": {
18
+ "@clack/prompts": "1.7.0"
16
19
  }
17
20
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>text{fill:#e95f19;font-family:"Hiragino Sans","Noto Sans JP",sans-serif}@media (prefers-color-scheme:dark){text{fill:#ff8648}}</style><text x="50" y="55" font-size="62" text-anchor="middle" dominant-baseline="central">渋</text></svg>
@@ -3,14 +3,22 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Shibumi static site</title>
6
+ <title>It ships · shibumi static</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <meta name="generator" content="create-shibumi (shibumistack.dev)" />
7
9
  <link rel="stylesheet" href="/style.css" />
8
10
  </head>
9
11
  <body>
10
12
  <main>
13
+ <p class="masthead"><span class="masthead-mark">渋み</span> shibumi static</p>
11
14
  <h1>It ships.</h1>
12
- <p>To get started, open <code>public/index.html</code>; this page is that file. Edit, commit, run <code>bun ship</code>.</p>
13
- <p>Using Astro, Jekyll, Eleventy, or another generator? Point its output here, or run <code>bun ship:setup</code> in that project instead; the Ship client packages any verified build directory.</p>
15
+ <p class="lede">Every file in <code>public/</code> is a page on your site. You are looking at <code>public/index.html</code>.</p>
16
+ <ol class="steps">
17
+ <li>Edit this file. <code>bun dev</code> shows it locally.</li>
18
+ <li>Connect your server once with <code>bun ship:setup</code>.</li>
19
+ <li>After that, every deploy is <code>git commit</code> and <code>bun ship</code>.</li>
20
+ </ol>
21
+ <p class="aside">Writing with Astro, Jekyll, or Eleventy instead? Run <code>bun ship:setup</code> inside that project; Ship deploys any build folder it can verify.</p>
14
22
  </main>
15
23
  </body>
16
24
  </html>
@@ -1,36 +1,71 @@
1
+ /* Self-contained styles on the shibumi palette. Yours to edit. */
1
2
  :root {
2
3
  color-scheme: light dark;
3
- --paper: #f5f0e4;
4
- --ink: #1b130f;
5
- --accent: #e95f19;
6
- }
7
-
8
- @media (prefers-color-scheme: dark) {
9
- :root {
10
- --paper: #1b130f;
11
- --ink: #f5f0e4;
12
- --accent: #ff8648;
13
- }
4
+ --paper: light-dark(#f5f0e4, #1b130f);
5
+ --ink: light-dark(#272016, #eee5d7);
6
+ --muted: light-dark(#746b5d, #aaa092);
7
+ --faint: light-dark(rgba(39, 32, 22, 0.055), rgba(238, 229, 215, 0.07));
8
+ --line: light-dark(rgba(39, 32, 22, 0.14), rgba(238, 229, 215, 0.16));
9
+ --accent: light-dark(#e95f19, #ff8648);
10
+ --font-serif: ui-serif, Georgia, "Iowan Old Style", Cambria, serif;
11
+ --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
14
12
  }
15
13
 
16
14
  body {
17
15
  margin: 0;
18
16
  background: var(--paper);
19
17
  color: var(--ink);
20
- font-family: system-ui, sans-serif;
18
+ font-family: ui-sans-serif, system-ui, sans-serif;
21
19
  font-size: 1.125rem;
22
- line-height: 1.6;
20
+ line-height: 1.65;
23
21
  }
24
22
 
25
23
  main {
26
- max-width: 40rem;
24
+ max-width: 40.625rem;
27
25
  margin: 0 auto;
28
- padding: 4rem 1.5rem;
26
+ padding: clamp(3rem, 9vh, 6.5rem) 1.5rem 4rem;
27
+ }
28
+
29
+ .masthead {
30
+ display: flex;
31
+ align-items: baseline;
32
+ gap: 0.6rem;
33
+ margin: 0 0 3.5rem;
34
+ padding-bottom: 1rem;
35
+ border-bottom: 1px solid var(--line);
36
+ color: var(--muted);
37
+ font-size: 0.875rem;
38
+ letter-spacing: 0.04em;
39
+ }
40
+
41
+ .masthead-mark {
42
+ color: var(--accent);
43
+ font-size: 1rem;
29
44
  }
30
45
 
31
46
  h1 {
32
- font-family: Georgia, "Times New Roman", serif;
33
- font-weight: 500;
47
+ margin: 0 0 0.75rem;
48
+ font-family: var(--font-serif);
49
+ font-size: clamp(2.6rem, 7vw, 3.8rem);
50
+ font-weight: 450;
51
+ letter-spacing: -0.03em;
52
+ line-height: 1.05;
53
+ }
54
+
55
+ .lede {
56
+ margin: 0 0 3rem;
57
+ max-width: 34rem;
58
+ color: var(--muted);
59
+ font-size: 1.3125rem;
60
+ line-height: 1.55;
61
+ }
62
+
63
+ code {
64
+ padding: 0.12em 0.42em;
65
+ border-radius: 0.35rem;
66
+ background: var(--faint);
67
+ font-family: var(--font-mono);
68
+ font-size: 0.85em;
34
69
  }
35
70
 
36
71
  a {
@@ -44,7 +79,18 @@ a:hover {
44
79
  text-decoration-color: var(--accent);
45
80
  }
46
81
 
47
- code {
48
- font-family: ui-monospace, monospace;
49
- font-size: 0.9em;
82
+ .steps {
83
+ margin: 0 0 2.5rem;
84
+ padding-left: 1.4rem;
85
+ }
86
+
87
+ .steps li {
88
+ margin-bottom: 0.6rem;
89
+ }
90
+
91
+ .aside {
92
+ padding-top: 1.5rem;
93
+ border-top: 1px solid var(--line);
94
+ color: var(--muted);
95
+ font-size: 1rem;
50
96
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><style>text{fill:#e95f19;font-family:"Hiragino Sans","Noto Sans JP",sans-serif}@media (prefers-color-scheme:dark){text{fill:#ff8648}}</style><text x="50" y="55" font-size="62" text-anchor="middle" dominant-baseline="central">渋</text></svg>
@@ -89,7 +89,7 @@ body {
89
89
  border-top: 1px solid var(--line);
90
90
  }
91
91
 
92
- main .endpoints a {
92
+ main .endpoints a:not(.btn):not(.button) {
93
93
  display: flex;
94
94
  align-items: baseline;
95
95
  justify-content: space-between;
@@ -101,13 +101,12 @@ main .endpoints a {
101
101
  transition: background 160ms ease;
102
102
  }
103
103
 
104
- main .endpoints a:hover,
105
- main .endpoints a:focus-visible {
104
+ main .endpoints a:not(.btn):not(.button):hover,
105
+ main .endpoints a:not(.btn):not(.button):focus-visible {
106
106
  background: var(--faint);
107
107
  }
108
108
 
109
- .endpoint-path {
110
- font-family: var(--font-mono);
109
+ main .endpoints code {
111
110
  font-size: var(--text-sm);
112
111
  }
113
112
  /* Inline code as rounded chips on the faint layer. */
@@ -120,7 +119,7 @@ main .endpoints a:focus-visible {
120
119
  }
121
120
 
122
121
 
123
- main .endpoints a:hover .endpoint-path {
122
+ main .endpoints a:hover code {
124
123
  color: var(--accent);
125
124
  }
126
125
 
@@ -63,6 +63,8 @@ const page = `<!doctype html>
63
63
  <meta charset="utf-8" />
64
64
  <meta name="viewport" content="width=device-width, initial-scale=1" />
65
65
  <title>Web app · shibumi</title>
66
+ <link rel="icon" type="image/svg+xml" href="/public/favicon.svg" />
67
+ <meta name="generator" content="create-shibumi (shibumistack.dev)" />
66
68
  <link rel="stylesheet" href="/public/vendor/shibumi.css" />
67
69
  <link rel="stylesheet" href="/public/style.css" />
68
70
  <!-- app.js must load before Alpine so the alpine:init listener exists
@@ -81,8 +83,8 @@ const page = `<!doctype html>
81
83
  <span class="demo-hint">client state without a build step</span>
82
84
  </section>
83
85
  <ul class="endpoints">
84
- <li><a href="/api/hello?name=you"><span class="endpoint-path">GET /api/hello</span><span>query validated with Zod</span></a></li>
85
- <li><a href="/healthz"><span class="endpoint-path">GET /healthz</span><span>the check every deploy waits for</span></a></li>
86
+ <li><a href="/api/hello?name=you"><code>GET /api/hello</code><span>query validated with Zod</span></a></li>
87
+ <li><a href="/healthz"><code>GET /healthz</code><span>the check every deploy waits for</span></a></li>
86
88
  </ul>
87
89
  <footer class="colophon">
88
90
  <p>This page lives in <code>src/app.ts</code>. House rules for coding agents are in <code>agents.md</code>. Add features with <code>bun shi add email</code>.</p>