create-shibumi 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -2
  3. package/package.json +57 -5
  4. package/scripts/shibumi.lock.json +22 -0
  5. package/scripts/ship.lock.json +4 -0
  6. package/src/args.ts +162 -0
  7. package/src/cli.ts +232 -0
  8. package/src/create.ts +272 -0
  9. package/src/templates/blog/README.md +10 -0
  10. package/src/templates/blog/agents.md +36 -0
  11. package/src/templates/blog/astro.config.mjs +13 -0
  12. package/src/templates/blog/bun.lock +787 -0
  13. package/src/templates/blog/gitignore +4 -0
  14. package/src/templates/blog/package.json +29 -0
  15. package/src/templates/blog/public/og-default.png +0 -0
  16. package/src/templates/blog/public/style.css +147 -0
  17. package/src/templates/blog/src/components/BaseHead.astro +37 -0
  18. package/src/templates/blog/src/content/blog/one-vps-is-plenty.md +13 -0
  19. package/src/templates/blog/src/content/blog/own-your-source.md +13 -0
  20. package/src/templates/blog/src/content/blog/writing-for-agents.md +13 -0
  21. package/src/templates/blog/src/content.config.ts +17 -0
  22. package/src/templates/blog/src/layouts/Base.astro +26 -0
  23. package/src/templates/blog/src/pages/404.astro +11 -0
  24. package/src/templates/blog/src/pages/index.astro +30 -0
  25. package/src/templates/blog/src/pages/llms.txt.ts +29 -0
  26. package/src/templates/blog/src/pages/posts/[id].astro +31 -0
  27. package/src/templates/blog/src/pages/posts/[id].md.ts +18 -0
  28. package/src/templates/blog/src/pages/robots.txt.ts +6 -0
  29. package/src/templates/blog/src/pages/rss.xml.ts +21 -0
  30. package/src/templates/blog/src/site.ts +7 -0
  31. package/src/templates/blog/tsconfig.json +5 -0
  32. package/src/templates/full-stack/.dockerignore +14 -0
  33. package/src/templates/full-stack/Dockerfile +23 -0
  34. package/src/templates/full-stack/README.md +10 -0
  35. package/src/templates/full-stack/agents.md +60 -0
  36. package/src/templates/full-stack/bun.lock +225 -0
  37. package/src/templates/full-stack/compose.yaml +24 -0
  38. package/src/templates/full-stack/drizzle.config.ts +11 -0
  39. package/src/templates/full-stack/gitignore +6 -0
  40. package/src/templates/full-stack/package.json +39 -0
  41. package/src/templates/full-stack/public/app.js +11 -0
  42. package/src/templates/full-stack/public/style.css +60 -0
  43. package/src/templates/full-stack/public/vendor/alpine-csp-3.16.2.min.js +23 -0
  44. package/src/templates/full-stack/scripts/db.ts +39 -0
  45. package/src/templates/full-stack/src/app.ts +106 -0
  46. package/src/templates/full-stack/src/db/index.ts +11 -0
  47. package/src/templates/full-stack/src/db/lifecycle.ts +214 -0
  48. package/src/templates/full-stack/src/db/migrations/0001_notes.sql +9 -0
  49. package/src/templates/full-stack/src/db/schema.ts +13 -0
  50. package/src/templates/full-stack/src/env.ts +24 -0
  51. package/src/templates/full-stack/src/server.ts +30 -0
  52. package/src/templates/full-stack/test/app.test.ts +144 -0
  53. package/src/templates/full-stack/test/db.test.ts +169 -0
  54. package/src/templates/full-stack/tsconfig.json +19 -0
  55. package/src/templates/shibumi.ts +831 -0
  56. package/src/templates/ship.ts +2056 -0
  57. package/src/templates/static/README.md +10 -0
  58. package/src/templates/static/agents.md +31 -0
  59. package/src/templates/static/gitignore +2 -0
  60. package/src/templates/static/package.json +17 -0
  61. package/src/templates/static/public/404.html +15 -0
  62. package/src/templates/static/public/index.html +16 -0
  63. package/src/templates/static/public/style.css +50 -0
  64. package/src/templates/static/scripts/preview.ts +42 -0
  65. package/src/templates/web/.dockerignore +12 -0
  66. package/src/templates/web/Dockerfile +24 -0
  67. package/src/templates/web/README.md +10 -0
  68. package/src/templates/web/agents.md +54 -0
  69. package/src/templates/web/bun.lock +45 -0
  70. package/src/templates/web/compose.yaml +17 -0
  71. package/src/templates/web/gitignore +5 -0
  72. package/src/templates/web/package.json +33 -0
  73. package/src/templates/web/public/app.js +11 -0
  74. package/src/templates/web/public/style.css +60 -0
  75. package/src/templates/web/public/vendor/alpine-csp-3.16.2.min.js +23 -0
  76. package/src/templates/web/src/app.ts +96 -0
  77. package/src/templates/web/src/env.ts +21 -0
  78. package/src/templates/web/src/server.ts +23 -0
  79. package/src/templates/web/test/app.test.ts +137 -0
  80. package/src/templates/web/tsconfig.json +13 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shibumi Stack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # create-shibumi
2
2
 
3
- Scaffold a new Shibumi Stack project.
3
+ Scaffold a Shibumi Stack project: simple apps, whole stack, without React, build pipelines, or 600 MB of `node_modules`. Every generated project keeps its source, tests, and deployment config in your repository. No Shibumi runtime dependency.
4
4
 
5
- See [shibumistack.dev](https://shibumistack.dev) for more.
5
+ ```sh
6
+ bun create shibumi@latest my-app
7
+ cd my-app
8
+ bun dev
9
+ ```
10
+
11
+ ## Three starting points
12
+
13
+ 1. **Static site**: publish a verified build directory such as `./dist`, `public`, `build`, or `out` from any framework.
14
+ 2. **Bun web**: Hono, HTML, CSS, Alpine, Zod, tests, and a health endpoint.
15
+ 3. **SQLite full stack**: the Bun web project plus Drizzle, migrations, persistent data, backup, and restore.
16
+
17
+ All three deploy to a Linux VPS or homelab through [shibumi-server](https://server.shibumistack.dev). Other providers can wait until their generated projects pass the same artifact and deployment tests.
18
+
19
+ ## Flags
20
+
21
+ ```text
22
+ --template <id> static, web, or full-stack
23
+ --yes, -y non-interactive; requires name and --template
24
+ --no-git skip git init
25
+ --no-install skip dependency install
26
+ --help, -h show help
27
+ --version show version
28
+ ```
29
+
30
+ Creation is atomic: the project is built in a temporary sibling directory and renamed into place. Failure or cancellation leaves nothing behind, and an existing destination is never touched. Git init stages and commits nothing; the first commit belongs to you.
31
+
32
+ ## Guidance for coding agents
33
+
34
+ Generated projects include a root `agents.md`. It records route locations, data rules, available checks, and files that need extra care.
35
+
36
+ ## Development
37
+
38
+ ```sh
39
+ bun install
40
+ bun test # CLI tests (parsing, atomicity, signals, E2E)
41
+ bun check # TypeScript
42
+ bun sync:ship # re-vendor the Ship client from its locked immutable snapshot
43
+ ```
44
+
45
+ The vendored Ship client in `src/templates/ship.ts` is locked byte-for-byte to a published immutable snapshot via `scripts/ship.lock.json`; tests fail on any drift.
46
+
47
+ Docs: <https://shibumistack.dev/docs>
package/package.json CHANGED
@@ -1,9 +1,61 @@
1
1
  {
2
2
  "name": "create-shibumi",
3
- "version": "0.0.1",
4
- "description": "Scaffold a new Shibumi Stack project. See https://shibumistack.dev",
5
- "keywords": ["shibumi", "stack", "create", "scaffold", "bun", "hono", "sqlite", "alpine", "typescript"],
3
+ "version": "0.2.1",
4
+ "description": "Scaffold a Shibumi Stack project: Bun, Hono, Zod, Drizzle, SQLite, Alpine",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-shibumi": "./src/cli.ts"
8
+ },
9
+ "files": [
10
+ "src/cli.ts",
11
+ "src/args.ts",
12
+ "src/create.ts",
13
+ "src/templates/ship.ts",
14
+ "src/templates/shibumi.ts",
15
+ "scripts/ship.lock.json",
16
+ "scripts/shibumi.lock.json",
17
+ "src/templates/static/",
18
+ "src/templates/blog/",
19
+ "src/templates/full-stack/",
20
+ "src/templates/web/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "start": "bun src/cli.ts",
26
+ "test": "bun test test",
27
+ "check": "tsc --noEmit",
28
+ "sync:ship": "bun scripts/sync-ship.ts",
29
+ "sync:extensions": "bun scripts/sync-extensions.ts",
30
+ "verify:packed": "bun scripts/verify-packed.ts"
31
+ },
32
+ "keywords": [
33
+ "shibumi",
34
+ "scaffold",
35
+ "cli",
36
+ "bun",
37
+ "hono",
38
+ "drizzle",
39
+ "alpine",
40
+ "zod",
41
+ "starter",
42
+ "template"
43
+ ],
44
+ "author": "Shibumi Stack",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/shibumistack/create-shibumi"
49
+ },
6
50
  "homepage": "https://shibumistack.dev",
7
- "repository": "github:shibumistack/create-shibumi",
8
- "license": "MIT"
51
+ "dependencies": {
52
+ "@clack/prompts": "1.7.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/bun": "1.4.0",
56
+ "typescript": "5.9.2"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.4.0"
60
+ }
9
61
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "extensions": [
4
+ {
5
+ "name": "admin",
6
+ "version": "1.0.1"
7
+ },
8
+ {
9
+ "name": "auth",
10
+ "version": "1.0.1"
11
+ },
12
+ {
13
+ "name": "email",
14
+ "version": "1.0.0"
15
+ },
16
+ {
17
+ "name": "uploads",
18
+ "version": "1.0.1"
19
+ }
20
+ ],
21
+ "sha256": "10386d9e6db61626780e85ef6bcc423e88402606b8ce9baa2c222e61cd32f134"
22
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://shibumistack.dev/ship/v45.ts",
3
+ "sha256": "d9bad68786b288d027bb00b2183583905e266069f4506c34e364d3e93ce77cb5"
4
+ }
package/src/args.ts ADDED
@@ -0,0 +1,162 @@
1
+ export const TEMPLATES = ["static", "web", "full-stack", "blog"] as const;
2
+ export type TemplateId = (typeof TEMPLATES)[number];
3
+
4
+ export interface ParsedArgs {
5
+ help: boolean;
6
+ version: boolean;
7
+ yes: boolean;
8
+ git: boolean;
9
+ install: boolean;
10
+ name?: string;
11
+ template?: TemplateId;
12
+ }
13
+
14
+ export type ParseResult =
15
+ | { ok: true; args: ParsedArgs }
16
+ | { ok: false; error: string };
17
+
18
+ // Declared minimum Bun; template lockfiles use lockfileVersion 2, which
19
+ // older Bun cannot parse, so scaffolding must refuse early and clearly.
20
+ export const MINIMUM_BUN = "1.4.0";
21
+
22
+ export function bunVersionProblem(version: string, minimum = MINIMUM_BUN): string | null {
23
+ const parse = (value: string) => value.split("-")[0]!.split(".").map((part) => Number(part) || 0);
24
+ const [major = 0, minor = 0, patch = 0] = parse(version);
25
+ const [minMajor = 0, minMinor = 0, minPatch = 0] = parse(minimum);
26
+ const current = major * 1_000_000 + minor * 1_000 + patch;
27
+ const floor = minMajor * 1_000_000 + minMinor * 1_000 + minPatch;
28
+ if (current >= floor) return null;
29
+ return `create-shibumi needs Bun ${minimum} or newer; you are running ${version}. Update with: bun upgrade`;
30
+ }
31
+
32
+ // Lowercase npm-style project names only; also the directory name.
33
+ export const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
34
+
35
+ const VALUE_FLAGS = new Set(["--template"]);
36
+ const BOOLEAN_FLAGS = new Set([
37
+ "--help",
38
+ "-h",
39
+ "--version",
40
+ "--yes",
41
+ "-y",
42
+ "--no-git",
43
+ "--no-install",
44
+ ]);
45
+
46
+ export function validateName(name: string): string | null {
47
+ if (!NAME_PATTERN.test(name)) {
48
+ return `Invalid project name "${name}". Use lowercase letters, digits, ".", "_" or "-", starting with a letter or digit.`;
49
+ }
50
+ if (name.length > 128) {
51
+ return `Project name too long (max 128 characters).`;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ export function parseArgs(argv: string[]): ParseResult {
57
+ const args: ParsedArgs = {
58
+ help: false,
59
+ version: false,
60
+ yes: false,
61
+ git: true,
62
+ install: true,
63
+ };
64
+ const positionals: string[] = [];
65
+
66
+ for (let i = 0; i < argv.length; i++) {
67
+ const raw = argv[i]!;
68
+
69
+ if (!raw.startsWith("-")) {
70
+ positionals.push(raw);
71
+ continue;
72
+ }
73
+
74
+ let flag = raw;
75
+ let inlineValue: string | undefined;
76
+ const eq = raw.indexOf("=");
77
+ if (raw.startsWith("--") && eq !== -1) {
78
+ flag = raw.slice(0, eq);
79
+ inlineValue = raw.slice(eq + 1);
80
+ }
81
+
82
+ if (VALUE_FLAGS.has(flag)) {
83
+ let value = inlineValue;
84
+ if (value === undefined) {
85
+ const next = argv[i + 1];
86
+ if (next === undefined || next.startsWith("-")) {
87
+ return { ok: false, error: `${flag} requires a value.` };
88
+ }
89
+ value = next;
90
+ i++;
91
+ }
92
+ if (flag === "--template") {
93
+ if (!(TEMPLATES as readonly string[]).includes(value)) {
94
+ return {
95
+ ok: false,
96
+ error: `Unknown template "${value}". Available: ${TEMPLATES.join(", ")}.`,
97
+ };
98
+ }
99
+ args.template = value as TemplateId;
100
+ }
101
+ continue;
102
+ }
103
+
104
+ if (BOOLEAN_FLAGS.has(flag)) {
105
+ if (inlineValue !== undefined) {
106
+ return { ok: false, error: `${flag} does not take a value.` };
107
+ }
108
+ if (flag === "--help" || flag === "-h") args.help = true;
109
+ else if (flag === "--version") args.version = true;
110
+ else if (flag === "--yes" || flag === "-y") args.yes = true;
111
+ else if (flag === "--no-git") args.git = false;
112
+ else if (flag === "--no-install") args.install = false;
113
+ continue;
114
+ }
115
+
116
+ return { ok: false, error: `Unknown flag: ${flag}` };
117
+ }
118
+
119
+ if (args.help || args.version) {
120
+ return { ok: true, args };
121
+ }
122
+
123
+ if (positionals.length > 1) {
124
+ return {
125
+ ok: false,
126
+ error: `Unexpected argument "${positionals[1]}". Pass one project name.`,
127
+ };
128
+ }
129
+ if (positionals.length === 1) {
130
+ const err = validateName(positionals[0]!);
131
+ if (err) return { ok: false, error: err };
132
+ args.name = positionals[0]!;
133
+ }
134
+
135
+ if (args.yes) {
136
+ if (!args.name) {
137
+ return { ok: false, error: `--yes requires a project name.` };
138
+ }
139
+ if (!args.template) {
140
+ return { ok: false, error: `--yes requires --template (${TEMPLATES.join(", ")}).` };
141
+ }
142
+ }
143
+
144
+ return { ok: true, args };
145
+ }
146
+
147
+ export const HELP_TEXT = `create-shibumi: scaffold a Shibumi Stack project
148
+
149
+ Usage
150
+ bun create shibumi@latest [name] [flags]
151
+ bunx create-shibumi [name] [flags]
152
+
153
+ Flags
154
+ --template <id> static, web, full-stack, or blog
155
+ --yes, -y non-interactive; requires name and --template
156
+ --no-git skip git init
157
+ --no-install skip dependency install
158
+ --help, -h show this help
159
+ --version show version
160
+
161
+ Docs: https://shibumistack.dev/docs
162
+ `;
package/src/cli.ts ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { HELP_TEXT, bunVersionProblem, parseArgs, validateName, type TemplateId } from "./args";
7
+ import { CreateError, createProject } from "./create";
8
+
9
+ const VERSION = (
10
+ JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")) as {
11
+ version: string;
12
+ }
13
+ ).version;
14
+
15
+ const CANCELLED = "Cancelled. Nothing was created.";
16
+
17
+ function exitCancelled(): never {
18
+ cancel(CANCELLED);
19
+ process.exit(130);
20
+ }
21
+
22
+ // Emit the vendored extension installer so a generated project can re-vendor it
23
+ // (`bun run shibumi update` runs `bunx create-shibumi@latest --print-installer`).
24
+ // Verifies the checksum lock before printing, so a corrupt package cannot ship
25
+ // a tampered installer through this path.
26
+ async function printInstaller(): Promise<never> {
27
+ const installer = join(import.meta.dir, "templates", "shibumi.ts");
28
+ const lockPath = join(import.meta.dir, "..", "scripts", "shibumi.lock.json");
29
+ const lock = JSON.parse(readFileSync(lockPath, "utf8")) as { sha256: string };
30
+ const bytes = await Bun.file(installer).arrayBuffer();
31
+ const digest = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
32
+ if (digest !== lock.sha256) {
33
+ process.stderr.write("Vendored installer does not match its checksum lock; reinstall create-shibumi.\n");
34
+ process.exit(1);
35
+ }
36
+ process.stdout.write(readFileSync(installer, "utf8"));
37
+ process.exit(0);
38
+ }
39
+
40
+ async function main(): Promise<void> {
41
+ if (process.argv.slice(2).includes("--print-installer")) {
42
+ await printInstaller();
43
+ }
44
+ const bunProblem = bunVersionProblem(Bun.version);
45
+ if (bunProblem) {
46
+ process.stderr.write(`${bunProblem}\n`);
47
+ process.exit(2);
48
+ }
49
+ const parsed = parseArgs(process.argv.slice(2));
50
+ if (!parsed.ok) {
51
+ process.stderr.write(`${parsed.error}\nRun create-shibumi --help for usage.\n`);
52
+ process.exit(2);
53
+ }
54
+ const args = parsed.args;
55
+
56
+ if (args.help) {
57
+ process.stdout.write(HELP_TEXT);
58
+ process.exit(0);
59
+ }
60
+ if (args.version) {
61
+ process.stdout.write(`${VERSION}\n`);
62
+ process.exit(0);
63
+ }
64
+
65
+ const interactive = !args.yes;
66
+ if (interactive && !process.stdin.isTTY) {
67
+ process.stderr.write(
68
+ `No interactive terminal. Use --yes with a project name and --template.\n`
69
+ );
70
+ process.exit(2);
71
+ }
72
+
73
+ intro("渋み shibumi");
74
+
75
+ let name: string;
76
+ if (args.name) {
77
+ name = args.name;
78
+ } else {
79
+ const answer = await text({
80
+ message: "Project name?",
81
+ placeholder: "my-app",
82
+ validate: (value) => validateName(value ?? "") ?? undefined,
83
+ });
84
+ if (isCancel(answer)) exitCancelled();
85
+ name = answer;
86
+ }
87
+
88
+ let template: TemplateId;
89
+ if (args.template) {
90
+ template = args.template;
91
+ } else {
92
+ const dim = (value: string) => `\x1b[2m${value}\x1b[22m`;
93
+ const detail = (value: string) => `\n ${dim(value)}`;
94
+ const answer = await select({
95
+ message: "What are you shipping?",
96
+ options: [
97
+ {
98
+ value: "full-stack" as TemplateId,
99
+ label: `Bun full-stack app ${dim("(recommended)")}${detail("Hono, Alpine, and SQLite with migrations and backups")}`,
100
+ },
101
+ {
102
+ value: "web" as TemplateId,
103
+ label: `Bun web app${detail("Hono, Alpine, and Zod; no database")}`,
104
+ },
105
+ {
106
+ value: "static" as TemplateId,
107
+ label: `Static site${detail("Any framework's build output: dist/, public/, _site/, or plain files")}`,
108
+ },
109
+ ],
110
+ });
111
+ if (isCancel(answer)) exitCancelled();
112
+ template = answer as TemplateId;
113
+ if (template === "static") {
114
+ const start = await select({
115
+ message: "Start from?",
116
+ options: [
117
+ {
118
+ value: "static" as TemplateId,
119
+ label: `Plain files${detail("index.html and friends in public/")}`,
120
+ },
121
+ {
122
+ value: "blog" as TemplateId,
123
+ label: `Astro blog${detail("posts, RSS, sitemap, SEO meta, llms.txt, markdown alternates")}`,
124
+ },
125
+ ],
126
+ });
127
+ if (isCancel(start)) exitCancelled();
128
+ template = start as TemplateId;
129
+ if (template === "static") {
130
+ log.info("Using a generator? Point bun ship:setup at its output directory later.");
131
+ }
132
+ }
133
+ }
134
+
135
+ let deployNow = false;
136
+ if (interactive) {
137
+ const answer = await confirm({
138
+ message: "Deploy to a VPS now?",
139
+ active: "Yes",
140
+ inactive: "Later",
141
+ initialValue: false,
142
+ });
143
+ if (isCancel(answer)) exitCancelled();
144
+ deployNow = answer;
145
+ }
146
+
147
+ const s = spinner();
148
+ s.start("Creating project");
149
+ let dest: string;
150
+ try {
151
+ const result = await createProject(
152
+ {
153
+ name,
154
+ parentDir: process.cwd(),
155
+ template,
156
+ git: args.git,
157
+ install: args.install,
158
+ },
159
+ undefined,
160
+ (step) => {
161
+ if (step === "git") s.message("Initializing git");
162
+ else if (step === "install") s.message("Installing dependencies");
163
+ }
164
+ );
165
+ dest = result.dest;
166
+ s.stop(`Created ${name}`);
167
+ // Receipt: the finished transcript documents what actually happened.
168
+ log.success("Template copied");
169
+ if (args.git) log.success("Git initialized; nothing committed, the first commit is yours");
170
+ else log.info("Git skipped; run git init when you want history");
171
+ if (args.install) log.success("Dependencies installed");
172
+ else log.info("Install skipped; run bun install inside the project");
173
+ if (existsSync(join(result.dest, "scripts", "ship.ts"))) {
174
+ log.success("Ship client vendored (scripts/ship.ts)");
175
+ }
176
+ } catch (err) {
177
+ if (err instanceof CreateError) {
178
+ s.stop("Failed");
179
+ process.stderr.write(`${err.message}\n`);
180
+ process.exit(err.exitCode);
181
+ }
182
+ throw err;
183
+ }
184
+
185
+ // Post-create phase: runs only after the atomic rename. Cancelling here
186
+ // leaves a complete, healthy project behind.
187
+ if (deployNow) {
188
+ const pkg = JSON.parse(readFileSync(join(dest, "package.json"), "utf8")) as {
189
+ scripts?: Record<string, string>;
190
+ };
191
+ if (pkg.scripts?.["ship:setup"] && existsSync(join(dest, "scripts", "ship.ts"))) {
192
+ const proc = Bun.spawn(["bun", "run", "ship:setup"], {
193
+ cwd: dest,
194
+ stdin: "inherit",
195
+ stdout: "inherit",
196
+ stderr: "inherit",
197
+ });
198
+ const code = await proc.exited;
199
+ if (code !== 0) {
200
+ process.stderr.write(
201
+ `ship:setup did not finish. Your project is intact; run "bun ship:setup" inside ${name} to retry.\n`
202
+ );
203
+ process.exit(1);
204
+ }
205
+ } else {
206
+ process.stdout.write(
207
+ `This template has no ship:setup yet. Run "bun ship:setup" inside ${name} once it does.\n`
208
+ );
209
+ }
210
+ }
211
+
212
+ const accent = (value: string) => `\x1b[38;5;208m${value}\x1b[0m`;
213
+ const dim = (value: string) => `\x1b[2m${value}\x1b[22m`;
214
+ log.message(
215
+ [
216
+ `${accent("next")} cd ${name}`,
217
+ ` bun dev ${dim("start the dev server (ctrl+c stops it)")}`,
218
+ deployNow
219
+ ? ` bun ship ${dim("deploy your first commit")}`
220
+ : ` bun ship:setup ${dim("connect your VPS when you're ready")}`,
221
+ "",
222
+ dim("agents.md tells your coding agent the house rules."),
223
+ ].join("\n")
224
+ );
225
+ outro(`Docs: ${accent("https://shibumistack.dev/docs")}`);
226
+ }
227
+
228
+ main().catch((err: unknown) => {
229
+ const message = err instanceof Error ? err.message : String(err);
230
+ process.stderr.write(`Something went wrong: ${message}\n`);
231
+ process.exit(1);
232
+ });