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.
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/package.json +57 -5
- package/scripts/shibumi.lock.json +22 -0
- package/scripts/ship.lock.json +4 -0
- package/src/args.ts +162 -0
- package/src/cli.ts +232 -0
- package/src/create.ts +272 -0
- package/src/templates/blog/README.md +10 -0
- package/src/templates/blog/agents.md +36 -0
- package/src/templates/blog/astro.config.mjs +13 -0
- package/src/templates/blog/bun.lock +787 -0
- package/src/templates/blog/gitignore +4 -0
- package/src/templates/blog/package.json +29 -0
- package/src/templates/blog/public/og-default.png +0 -0
- package/src/templates/blog/public/style.css +147 -0
- package/src/templates/blog/src/components/BaseHead.astro +37 -0
- package/src/templates/blog/src/content/blog/one-vps-is-plenty.md +13 -0
- package/src/templates/blog/src/content/blog/own-your-source.md +13 -0
- package/src/templates/blog/src/content/blog/writing-for-agents.md +13 -0
- package/src/templates/blog/src/content.config.ts +17 -0
- package/src/templates/blog/src/layouts/Base.astro +26 -0
- package/src/templates/blog/src/pages/404.astro +11 -0
- package/src/templates/blog/src/pages/index.astro +30 -0
- package/src/templates/blog/src/pages/llms.txt.ts +29 -0
- package/src/templates/blog/src/pages/posts/[id].astro +31 -0
- package/src/templates/blog/src/pages/posts/[id].md.ts +18 -0
- package/src/templates/blog/src/pages/robots.txt.ts +6 -0
- package/src/templates/blog/src/pages/rss.xml.ts +21 -0
- package/src/templates/blog/src/site.ts +7 -0
- package/src/templates/blog/tsconfig.json +5 -0
- package/src/templates/full-stack/.dockerignore +14 -0
- package/src/templates/full-stack/Dockerfile +23 -0
- package/src/templates/full-stack/README.md +10 -0
- package/src/templates/full-stack/agents.md +60 -0
- package/src/templates/full-stack/bun.lock +225 -0
- package/src/templates/full-stack/compose.yaml +24 -0
- package/src/templates/full-stack/drizzle.config.ts +11 -0
- package/src/templates/full-stack/gitignore +6 -0
- package/src/templates/full-stack/package.json +39 -0
- package/src/templates/full-stack/public/app.js +11 -0
- package/src/templates/full-stack/public/style.css +60 -0
- package/src/templates/full-stack/public/vendor/alpine-csp-3.16.2.min.js +23 -0
- package/src/templates/full-stack/scripts/db.ts +39 -0
- package/src/templates/full-stack/src/app.ts +106 -0
- package/src/templates/full-stack/src/db/index.ts +11 -0
- package/src/templates/full-stack/src/db/lifecycle.ts +214 -0
- package/src/templates/full-stack/src/db/migrations/0001_notes.sql +9 -0
- package/src/templates/full-stack/src/db/schema.ts +13 -0
- package/src/templates/full-stack/src/env.ts +24 -0
- package/src/templates/full-stack/src/server.ts +30 -0
- package/src/templates/full-stack/test/app.test.ts +144 -0
- package/src/templates/full-stack/test/db.test.ts +169 -0
- package/src/templates/full-stack/tsconfig.json +19 -0
- package/src/templates/shibumi.ts +831 -0
- package/src/templates/ship.ts +2056 -0
- package/src/templates/static/README.md +10 -0
- package/src/templates/static/agents.md +31 -0
- package/src/templates/static/gitignore +2 -0
- package/src/templates/static/package.json +17 -0
- package/src/templates/static/public/404.html +15 -0
- package/src/templates/static/public/index.html +16 -0
- package/src/templates/static/public/style.css +50 -0
- package/src/templates/static/scripts/preview.ts +42 -0
- package/src/templates/web/.dockerignore +12 -0
- package/src/templates/web/Dockerfile +24 -0
- package/src/templates/web/README.md +10 -0
- package/src/templates/web/agents.md +54 -0
- package/src/templates/web/bun.lock +45 -0
- package/src/templates/web/compose.yaml +17 -0
- package/src/templates/web/gitignore +5 -0
- package/src/templates/web/package.json +33 -0
- package/src/templates/web/public/app.js +11 -0
- package/src/templates/web/public/style.css +60 -0
- package/src/templates/web/public/vendor/alpine-csp-3.16.2.min.js +23 -0
- package/src/templates/web/src/app.ts +96 -0
- package/src/templates/web/src/env.ts +21 -0
- package/src/templates/web/src/server.ts +23 -0
- package/src/templates/web/test/app.test.ts +137 -0
- 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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Scaffold a
|
|
5
|
-
"
|
|
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
|
-
"
|
|
8
|
-
|
|
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
|
+
}
|
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
|
+
});
|