ship-create 1.4.0 → 1.6.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.
- package/create.mjs +261 -123
- package/package.json +1 -1
- package/templates/.claude/commands/build.md +90 -45
- package/templates/.claude/skills/ship-method/SKILL.md +26 -53
- package/templates/.claude/skills/uiux-frontend/SKILL.md +321 -0
- package/templates/.cursorrules +7 -5
- package/templates/.windsurfrules +7 -5
- package/templates/AGENTS.md +7 -5
- package/templates/CLAUDE.md +7 -5
- package/templates/docs/DESIGN_SPEC.md +50 -0
- package/templates/docs/DESIGN_SYSTEM.md +145 -0
package/create.mjs
CHANGED
|
@@ -10,18 +10,6 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* npx ship-create
|
|
13
|
-
*
|
|
14
|
-
* All templates (starter-kit app, agent rule files, Claude skill) are
|
|
15
|
-
* bundled inside this package's own templates/ folder — nothing is read
|
|
16
|
-
* from outside, so it works standalone via npx.
|
|
17
|
-
*
|
|
18
|
-
* What changed from v1.3.1 → v1.4.0:
|
|
19
|
-
* - Removed the AI-tool picker (no longer needed)
|
|
20
|
-
* - Added 4 idea questions so PROJECT.md + HUMAN_FLOW.md are pre-filled
|
|
21
|
-
* with real content (no [bracket placeholders] in the core sections)
|
|
22
|
-
* - Runs `npm install` automatically so the project is ready to open
|
|
23
|
-
* - End message tells the user exactly how to start building in their
|
|
24
|
-
* AI coding tool — no intermediate manual steps
|
|
25
13
|
*/
|
|
26
14
|
|
|
27
15
|
import fs from "node:fs";
|
|
@@ -34,19 +22,105 @@ import { spawnSync } from "node:child_process";
|
|
|
34
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
23
|
const TEMPLATES_DIR = path.join(__dirname, "templates");
|
|
36
24
|
|
|
25
|
+
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const C = {
|
|
28
|
+
reset: "\x1b[0m",
|
|
29
|
+
bold: "\x1b[1m",
|
|
30
|
+
dim: "\x1b[2m",
|
|
31
|
+
cyan: "\x1b[36m",
|
|
32
|
+
green: "\x1b[32m",
|
|
33
|
+
white: "\x1b[37m",
|
|
34
|
+
hideCursor: "\x1b[?25l",
|
|
35
|
+
showCursor: "\x1b[?25h",
|
|
36
|
+
clearLine: "\x1b[2K",
|
|
37
|
+
up: (n) => `\x1b[${n}A`,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ─── Data ─────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
37
42
|
const PRODUCT_TYPES = [
|
|
38
|
-
{ key: "SAAS",
|
|
39
|
-
{ key: "CRM",
|
|
40
|
-
{ key: "MEMBERSHIP",
|
|
41
|
-
{ key: "LEADGEN",
|
|
42
|
-
{ key: "DIRECTORY",
|
|
43
|
-
{ key: "DASHBOARD",
|
|
44
|
-
{ key: "INTERNAL_TOOL",
|
|
45
|
-
{ key: "MARKETPLACE",
|
|
43
|
+
{ key: "SAAS", name: "Web app / SaaS", desc: "users sign up and use a tool online", file: "SAAS_TEMPLATE.md" },
|
|
44
|
+
{ key: "CRM", name: "CRM / Sales pipeline", desc: "track leads, deals, and customers", file: "CRM_TEMPLATE.md" },
|
|
45
|
+
{ key: "MEMBERSHIP", name: "Membership / community", desc: "paid access to content or a community", file: "MEMBERSHIP_TEMPLATE.md" },
|
|
46
|
+
{ key: "LEADGEN", name: "Landing page / lead capture", desc: "get visitors to fill a form or book a call", file: "LEADGEN_TEMPLATE.md" },
|
|
47
|
+
{ key: "DIRECTORY", name: "Directory / listing site", desc: "browse and find providers, products, or places", file: "DIRECTORY_TEMPLATE.md" },
|
|
48
|
+
{ key: "DASHBOARD", name: "Dashboard / analytics", desc: "visualise data and KPIs for a team or business", file: "DASHBOARD_TEMPLATE.md" },
|
|
49
|
+
{ key: "INTERNAL_TOOL", name: "Internal tool", desc: "replaces a manual or spreadsheet-based process inside a company", file: "INTERNAL_TOOL_TEMPLATE.md" },
|
|
50
|
+
{ key: "MARKETPLACE", name: "Marketplace", desc: "connects buyers and sellers; transactions happen on the platform", file: "MARKETPLACE_TEMPLATE.md" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const LANGUAGES = [
|
|
54
|
+
{ code: "en", name: "English", desc: "UI text, labels, and copy in English" },
|
|
55
|
+
{ code: "th", name: "Thai (ภาษาไทย)", desc: "ข้อความ label และ copy ในระบบเป็นภาษาไทย" },
|
|
46
56
|
];
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const I18N = {
|
|
59
|
+
en: {
|
|
60
|
+
sectionIdea: "── About your idea ────────────────────────────",
|
|
61
|
+
qName: "Project name?",
|
|
62
|
+
qNameDefault: "My Product",
|
|
63
|
+
qIdea: "Describe it in 1–3 sentences (who it's for and what it does)",
|
|
64
|
+
qUser: "Who is your primary user? (e.g. 'freelance designers', 'gym owners')",
|
|
65
|
+
qUserDefault: "small business owners",
|
|
66
|
+
qProblem: "What is the #1 problem they face today?",
|
|
67
|
+
qValue: "What makes them say 'I need this'? (the aha moment)",
|
|
68
|
+
scaffolding: (slug) => `Scaffolding ./${slug} ...`,
|
|
69
|
+
installing: "Installing packages (this takes ~30 seconds)...",
|
|
70
|
+
installFail: (slug) => `npm install failed. Run it manually:\n cd ${slug} && npm install`,
|
|
71
|
+
folderExists: (slug) => `Folder ./${slug} already exists — pick a different name and run again.`,
|
|
72
|
+
templatesMissing:"Bundled templates missing. Try: npx ship-create@latest",
|
|
73
|
+
done: (slug) => `
|
|
74
|
+
✔ Done! Your project is at ./${slug}/
|
|
75
|
+
|
|
76
|
+
docs/PROJECT.md — product spec (pre-filled)
|
|
77
|
+
docs/HUMAN_FLOW.md — UX flow (pre-filled)
|
|
78
|
+
docs/DESIGN_SYSTEM.md — design tokens (filled by /build)
|
|
79
|
+
|
|
80
|
+
──────────────────────────────────────────
|
|
81
|
+
Open in your AI coding tool and type /build
|
|
82
|
+
|
|
83
|
+
Claude Code → claude ./${slug}
|
|
84
|
+
Cursor → cursor ./${slug}
|
|
85
|
+
Windsurf → windsurf ./${slug}
|
|
86
|
+
|
|
87
|
+
Then type: /build
|
|
88
|
+
`,
|
|
89
|
+
},
|
|
90
|
+
th: {
|
|
91
|
+
sectionIdea: "── เกี่ยวกับ idea ของคุณ ─────────────────────",
|
|
92
|
+
qName: "ชื่อโปรเจค?",
|
|
93
|
+
qNameDefault: "สินค้าของฉัน",
|
|
94
|
+
qIdea: "อธิบายในไม่เกิน 1–3 ประโยค (สำหรับใคร และทำอะไร)",
|
|
95
|
+
qUser: "ผู้ใช้หลักของคุณคือใคร? (เช่น 'ฟรีแลนซ์ดีไซเนอร์', 'เจ้าของยิม')",
|
|
96
|
+
qUserDefault: "เจ้าของธุรกิจขนาดเล็ก",
|
|
97
|
+
qProblem: "ปัญหาอันดับ 1 ที่พวกเขาเจออยู่ทุกวันคืออะไร?",
|
|
98
|
+
qValue: "อะไรทำให้พวกเขาบอกว่า 'ฉันต้องการสิ่งนี้!'? (จุด aha moment)",
|
|
99
|
+
scaffolding: (slug) => `กำลัง scaffold ./${slug} ...`,
|
|
100
|
+
installing: "กำลังติดตั้ง packages (ใช้เวลาประมาณ 30 วินาที)...",
|
|
101
|
+
installFail: (slug) => `npm install ล้มเหลว รันเองได้ที่:\n cd ${slug} && npm install`,
|
|
102
|
+
folderExists: (slug) => `โฟลเดอร์ ./${slug} มีอยู่แล้ว — เลือกชื่ออื่นแล้วรันใหม่`,
|
|
103
|
+
templatesMissing:"ไม่พบ template ที่ bundle ไว้ ลองรัน: npx ship-create@latest",
|
|
104
|
+
done: (slug) => `
|
|
105
|
+
✔ เสร็จแล้ว! โปรเจคของคุณอยู่ที่ ./${slug}/
|
|
106
|
+
|
|
107
|
+
docs/PROJECT.md — spec สินค้า (กรอกข้อมูลแล้ว)
|
|
108
|
+
docs/HUMAN_FLOW.md — UX flow (กรอกข้อมูลแล้ว)
|
|
109
|
+
docs/DESIGN_SYSTEM.md — design tokens (เติมโดย /build)
|
|
110
|
+
|
|
111
|
+
──────────────────────────────────────────
|
|
112
|
+
เปิดใน AI coding tool แล้วพิมพ์ /build
|
|
113
|
+
|
|
114
|
+
Claude Code → claude ./${slug}
|
|
115
|
+
Cursor → cursor ./${slug}
|
|
116
|
+
Windsurf → windsurf ./${slug}
|
|
117
|
+
|
|
118
|
+
จากนั้นพิมพ์: /build
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Pre-seeded journey stages per product type so HUMAN_FLOW.md has real content.
|
|
50
124
|
const JOURNEY_SEEDS = {
|
|
51
125
|
SAAS: {
|
|
52
126
|
discovery: "Googles a pain they have, or sees a post about the tool from someone they follow",
|
|
@@ -98,19 +172,87 @@ const JOURNEY_SEEDS = {
|
|
|
98
172
|
},
|
|
99
173
|
};
|
|
100
174
|
|
|
101
|
-
// ───
|
|
175
|
+
// ─── Interactive selector (arrow keys) ───────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function selectInteractive(title, items) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
let idx = 0;
|
|
180
|
+
|
|
181
|
+
const printItems = () => {
|
|
182
|
+
items.forEach((item, i) => {
|
|
183
|
+
const selected = i === idx;
|
|
184
|
+
process.stdout.write(C.clearLine + "\r");
|
|
185
|
+
if (selected) {
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
` ${C.cyan}${C.bold}❯ ${item.name}${C.reset}` +
|
|
188
|
+
` ${C.dim}${item.desc}${C.reset}\n`
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
process.stdout.write(` ${C.dim} ${item.name}${C.reset}\n`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// hint line (no trailing newline so cursor stays here)
|
|
195
|
+
process.stdout.write(
|
|
196
|
+
C.clearLine + `\r ${C.dim}↑↓ navigate · Enter to select${C.reset}`
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// initial render
|
|
201
|
+
process.stdout.write(`\n ${C.bold}${title}${C.reset}\n\n`);
|
|
202
|
+
process.stdout.write(C.hideCursor);
|
|
203
|
+
printItems();
|
|
204
|
+
|
|
205
|
+
process.stdin.setRawMode(true);
|
|
206
|
+
process.stdin.resume();
|
|
207
|
+
process.stdin.setEncoding("utf8");
|
|
208
|
+
|
|
209
|
+
const onData = (key) => {
|
|
210
|
+
if (key === "\x03") { // Ctrl+C
|
|
211
|
+
process.stdout.write(C.showCursor + "\n");
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
if (key === "\r" || key === "\n") { // Enter
|
|
215
|
+
process.stdin.removeListener("data", onData);
|
|
216
|
+
process.stdin.setRawMode(false);
|
|
217
|
+
process.stdin.pause();
|
|
218
|
+
// replace hint line with confirmed selection
|
|
219
|
+
process.stdout.write(
|
|
220
|
+
"\r" + C.clearLine +
|
|
221
|
+
` ${C.green}${C.bold}✔ ${items[idx].name}${C.reset}\n`
|
|
222
|
+
);
|
|
223
|
+
process.stdout.write(C.showCursor);
|
|
224
|
+
resolve(items[idx]);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const prev = idx;
|
|
228
|
+
if (key === "\x1b[A") idx = Math.max(0, idx - 1); // up
|
|
229
|
+
if (key === "\x1b[B") idx = Math.min(items.length - 1, idx + 1); // down
|
|
230
|
+
if (idx !== prev) {
|
|
231
|
+
process.stdout.write(C.up(items.length));
|
|
232
|
+
printItems();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
process.stdin.on("data", onData);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
102
239
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
|
|
240
|
+
// Fallback for non-TTY environments (CI, piped input)
|
|
241
|
+
async function selectFallback(title, items, nextLine) {
|
|
242
|
+
console.log(`\n ${title}`);
|
|
243
|
+
items.forEach((item, i) =>
|
|
244
|
+
console.log(` ${i + 1}. ${item.name} — ${item.desc}`)
|
|
245
|
+
);
|
|
246
|
+
while (true) {
|
|
247
|
+
const raw = await nextLine(` Pick a number (1-${items.length}): `);
|
|
248
|
+
const n = parseInt(raw, 10) - 1;
|
|
249
|
+
if (n >= 0 && n < items.length) return items[n];
|
|
250
|
+
console.log(" Not a valid number, try again.");
|
|
251
|
+
}
|
|
109
252
|
}
|
|
110
253
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
// lines after the first question() call resolves.
|
|
254
|
+
// ─── Text question helper ─────────────────────────────────────────────────────
|
|
255
|
+
|
|
114
256
|
function makeLineReader(rl) {
|
|
115
257
|
const it = rl[Symbol.asyncIterator]();
|
|
116
258
|
return async function nextLine(promptText) {
|
|
@@ -122,22 +264,12 @@ function makeLineReader(rl) {
|
|
|
122
264
|
}
|
|
123
265
|
|
|
124
266
|
async function ask(nextLine, question, defaultValue) {
|
|
125
|
-
const suffix = defaultValue ? `
|
|
126
|
-
|
|
267
|
+
const suffix = defaultValue ? ` ${C.dim}(${defaultValue})${C.reset}` : "";
|
|
268
|
+
output.write(` ${question}${suffix}\n ${C.cyan}›${C.reset} `);
|
|
269
|
+
const answer = await nextLine("");
|
|
127
270
|
return answer || defaultValue || "";
|
|
128
271
|
}
|
|
129
272
|
|
|
130
|
-
async function pickFromList(nextLine, title, items, labelFn) {
|
|
131
|
-
console.log(`\n${title}`);
|
|
132
|
-
items.forEach((item, i) => console.log(` ${i + 1}. ${labelFn(item)}`));
|
|
133
|
-
while (true) {
|
|
134
|
-
const raw = await nextLine(`Pick a number (1-${items.length}): `);
|
|
135
|
-
const idx = parseInt(raw, 10) - 1;
|
|
136
|
-
if (idx >= 0 && idx < items.length) return items[idx];
|
|
137
|
-
console.log(" Not a valid number, try again.");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
273
|
function copyRecursiveExcluding(src, dest, excludeNames) {
|
|
142
274
|
fs.mkdirSync(dest, { recursive: true });
|
|
143
275
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -152,9 +284,17 @@ function copyRecursiveExcluding(src, dest, excludeNames) {
|
|
|
152
284
|
}
|
|
153
285
|
}
|
|
154
286
|
|
|
155
|
-
|
|
287
|
+
function toKebabCase(str) {
|
|
288
|
+
return str
|
|
289
|
+
.trim()
|
|
290
|
+
.toLowerCase()
|
|
291
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
292
|
+
.replace(/(^-|-$)/g, "") || "my-product";
|
|
293
|
+
}
|
|
156
294
|
|
|
157
|
-
|
|
295
|
+
// ─── Doc builders ─────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
function buildProjectMd(projectName, productTypeName, { targetUser, problem, valueProp, idea, uiLanguage }) {
|
|
158
298
|
return `# PROJECT.md
|
|
159
299
|
|
|
160
300
|
**Phase:** S — Structure
|
|
@@ -164,7 +304,7 @@ function buildProjectMd(projectName, productTypeLabel, { targetUser, problem, va
|
|
|
164
304
|
|
|
165
305
|
## 1. Product Vision
|
|
166
306
|
|
|
167
|
-
${projectName} is a ${
|
|
307
|
+
${projectName} is a ${productTypeName.toLowerCase()} that helps **${targetUser}** ${valueProp}.
|
|
168
308
|
|
|
169
309
|
> *Expand this into a fuller paragraph before moving to HUMAN_FLOW.md — what does this product become in 2 years if it succeeds?*
|
|
170
310
|
|
|
@@ -242,7 +382,15 @@ ${projectName} is a ${productTypeLabel.toLowerCase()} that helps **${targetUser}
|
|
|
242
382
|
|
|
243
383
|
---
|
|
244
384
|
|
|
245
|
-
## 9.
|
|
385
|
+
## 9. Technical Decisions
|
|
386
|
+
|
|
387
|
+
- **UI language:** ${uiLanguage || "English"}
|
|
388
|
+
- **Auth method:** [magic link / OAuth / password — fill in]
|
|
389
|
+
- **Hosting:** [Vercel / Railway / other — fill in]
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## 10. Original Idea (keep for reference)
|
|
246
394
|
|
|
247
395
|
> *Your words from when you scaffolded this project.*
|
|
248
396
|
|
|
@@ -336,94 +484,94 @@ function buildHumanFlowMd(projectName, productType, { targetUser, problem, value
|
|
|
336
484
|
`;
|
|
337
485
|
}
|
|
338
486
|
|
|
339
|
-
// ───
|
|
487
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
340
488
|
|
|
341
489
|
async function main() {
|
|
490
|
+
const isTTY = process.stdin.isTTY;
|
|
491
|
+
|
|
492
|
+
// ── Banner ─────────────────────────────────────────────────────────────────
|
|
493
|
+
console.log("");
|
|
494
|
+
console.log(`${C.bold}${C.cyan} ███████╗██╗ ██╗██╗██████╗ ${C.reset}`);
|
|
495
|
+
console.log(`${C.bold}${C.cyan} ██╔════╝██║ ██║██║██╔══██╗${C.reset}`);
|
|
496
|
+
console.log(`${C.bold}${C.cyan} ███████╗███████║██║██████╔╝${C.reset}`);
|
|
497
|
+
console.log(`${C.bold}${C.cyan} ╚════██║██╔══██║██║██╔═══╝ ${C.reset}`);
|
|
498
|
+
console.log(`${C.bold}${C.cyan} ███████║██║ ██║██║██║ ${C.reset}`);
|
|
499
|
+
console.log(`${C.bold}${C.cyan} ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ${C.reset}`);
|
|
500
|
+
console.log("");
|
|
501
|
+
console.log(` ${C.bold}The SHIP Method${C.reset} ${C.dim}· scaffold your project${C.reset}`);
|
|
502
|
+
console.log(` ${C.dim}──────────────────────────────────────────${C.reset}`);
|
|
503
|
+
console.log("");
|
|
504
|
+
|
|
505
|
+
// ── Interactive selections (no readline needed) ────────────────────────────
|
|
506
|
+
let productType, uiLanguage;
|
|
507
|
+
|
|
508
|
+
if (isTTY) {
|
|
509
|
+
productType = await selectInteractive("What type of system are you building?", PRODUCT_TYPES);
|
|
510
|
+
uiLanguage = await selectInteractive("UI language — what will your app display to users?", LANGUAGES);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Text questions (readline) — switch language after uiLanguage is known ──
|
|
342
514
|
const rl = readline.createInterface({ input, terminal: false });
|
|
343
515
|
const nextLine = makeLineReader(rl);
|
|
344
516
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// ── 1. Core questions ──────────────────────────────────────────────────
|
|
350
|
-
const projectNameRaw = await ask(nextLine, "Project name?", "My Product");
|
|
351
|
-
const projectSlug = toKebabCase(projectNameRaw);
|
|
517
|
+
if (!isTTY) {
|
|
518
|
+
productType = await selectFallback("What type of system are you building?", PRODUCT_TYPES, nextLine);
|
|
519
|
+
uiLanguage = await selectFallback("UI language — what will your app display to users?", LANGUAGES, nextLine);
|
|
520
|
+
}
|
|
352
521
|
|
|
353
|
-
const
|
|
354
|
-
nextLine,
|
|
355
|
-
"What are you building?",
|
|
356
|
-
PRODUCT_TYPES,
|
|
357
|
-
(t) => t.label
|
|
358
|
-
);
|
|
522
|
+
const t = I18N[uiLanguage.code] ?? I18N.en;
|
|
359
523
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
""
|
|
366
|
-
);
|
|
367
|
-
const targetUser = await ask(
|
|
368
|
-
nextLine,
|
|
369
|
-
"Who is your primary user? (e.g. 'freelance designers', 'gym owners')",
|
|
370
|
-
"small business owners"
|
|
371
|
-
);
|
|
372
|
-
const problem = await ask(
|
|
373
|
-
nextLine,
|
|
374
|
-
"What is the #1 problem they face today?",
|
|
375
|
-
""
|
|
376
|
-
);
|
|
377
|
-
const valueProp = await ask(
|
|
378
|
-
nextLine,
|
|
379
|
-
"What makes them say 'I need this'? (the aha moment)",
|
|
380
|
-
""
|
|
381
|
-
);
|
|
524
|
+
console.log(`\n ${C.dim}${t.sectionIdea}${C.reset}`);
|
|
525
|
+
const projectNameRaw = await ask(nextLine, t.qName, t.qNameDefault);
|
|
526
|
+
const projectSlug = toKebabCase(projectNameRaw);
|
|
527
|
+
const idea = await ask(nextLine, t.qIdea, "");
|
|
528
|
+
const targetUser = await ask(nextLine, t.qUser, t.qUserDefault);
|
|
529
|
+
const problem = await ask(nextLine, t.qProblem, "");
|
|
530
|
+
const valueProp = await ask(nextLine, t.qValue, "");
|
|
382
531
|
|
|
383
|
-
// Close readline before any child processes touch stdin
|
|
384
532
|
rl.close();
|
|
385
533
|
|
|
386
|
-
// ──
|
|
534
|
+
// ── Guard: don't overwrite an existing folder ──────────────────────────────
|
|
387
535
|
const outDir = path.join(process.cwd(), projectSlug);
|
|
388
536
|
if (fs.existsSync(outDir)) {
|
|
389
|
-
console.log(`\n
|
|
537
|
+
console.log(`\n ${C.dim}${t.folderExists(projectSlug)}${C.reset}`);
|
|
390
538
|
process.exit(1);
|
|
391
539
|
}
|
|
392
540
|
|
|
393
|
-
console.log(`\
|
|
541
|
+
console.log(`\n ${C.dim}${t.scaffolding(projectSlug)}${C.reset}\n`);
|
|
394
542
|
|
|
395
543
|
const starterKitSrc = path.join(TEMPLATES_DIR, "starter-kit");
|
|
396
544
|
if (!fs.existsSync(starterKitSrc) || !fs.existsSync(path.join(TEMPLATES_DIR, "docs"))) {
|
|
397
|
-
console.log(
|
|
545
|
+
console.log(`\n ${t.templatesMissing}\n`);
|
|
398
546
|
process.exit(1);
|
|
399
547
|
}
|
|
400
548
|
|
|
401
|
-
// ──
|
|
549
|
+
// ── Copy app shell ─────────────────────────────────────────────────────────
|
|
402
550
|
copyRecursiveExcluding(
|
|
403
551
|
starterKitSrc,
|
|
404
552
|
outDir,
|
|
405
553
|
new Set(["node_modules", ".next", "package-lock.json", "next-env.d.ts", "tsconfig.tsbuildinfo"])
|
|
406
554
|
);
|
|
407
555
|
|
|
408
|
-
// ──
|
|
556
|
+
// ── Copy agent rule files ──────────────────────────────────────────────────
|
|
409
557
|
for (const ruleFile of ["AGENTS.md", "CLAUDE.md", ".cursorrules", ".windsurfrules"]) {
|
|
410
558
|
const src = path.join(TEMPLATES_DIR, ruleFile);
|
|
411
559
|
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(outDir, ruleFile));
|
|
412
560
|
}
|
|
413
561
|
|
|
414
|
-
// ──
|
|
562
|
+
// ── Copy Claude Code skill ─────────────────────────────────────────────────
|
|
415
563
|
const claudeSkillSrc = path.join(TEMPLATES_DIR, ".claude");
|
|
416
564
|
if (fs.existsSync(claudeSkillSrc)) {
|
|
417
565
|
copyRecursiveExcluding(claudeSkillSrc, path.join(outDir, ".claude"), new Set());
|
|
418
566
|
}
|
|
419
567
|
|
|
420
|
-
// ──
|
|
568
|
+
// ── Write pre-filled docs ──────────────────────────────────────────────────
|
|
421
569
|
const docsDir = path.join(outDir, "docs");
|
|
422
570
|
fs.mkdirSync(docsDir, { recursive: true });
|
|
423
571
|
|
|
424
572
|
fs.writeFileSync(
|
|
425
573
|
path.join(docsDir, "PROJECT.md"),
|
|
426
|
-
buildProjectMd(projectNameRaw, productType.
|
|
574
|
+
buildProjectMd(projectNameRaw, productType.name, { targetUser, problem, valueProp, idea, uiLanguage: uiLanguage.name })
|
|
427
575
|
);
|
|
428
576
|
|
|
429
577
|
fs.writeFileSync(
|
|
@@ -431,59 +579,49 @@ async function main() {
|
|
|
431
579
|
buildHumanFlowMd(projectNameRaw, productType, { targetUser, problem, valueProp })
|
|
432
580
|
);
|
|
433
581
|
|
|
434
|
-
// Product-type feature checklist
|
|
435
582
|
const templateSrc = path.join(TEMPLATES_DIR, "docs", "product-types", productType.file);
|
|
436
583
|
if (fs.existsSync(templateSrc)) {
|
|
437
584
|
fs.copyFileSync(templateSrc, path.join(docsDir, productType.file));
|
|
438
585
|
}
|
|
439
586
|
|
|
440
|
-
// Full prompt chain (Stages 3-6) for users who want it
|
|
441
587
|
const promptsSrc = path.join(TEMPLATES_DIR, "docs", "PROMPTS.md");
|
|
442
588
|
if (fs.existsSync(promptsSrc)) {
|
|
443
589
|
fs.copyFileSync(promptsSrc, path.join(docsDir, "PROMPTS.md"));
|
|
444
590
|
}
|
|
445
591
|
|
|
446
|
-
// Tech-stack reference — agent reads this when making stack decisions.
|
|
447
|
-
// (Design system is NOT bundled as a static file — the /build command
|
|
448
|
-
// invokes the ui-ux-pro-max skill to generate one specific to this project.)
|
|
449
592
|
const techStackSrc = path.join(TEMPLATES_DIR, "docs", "tech-stack");
|
|
450
593
|
if (fs.existsSync(techStackSrc)) {
|
|
451
594
|
copyRecursiveExcluding(techStackSrc, path.join(docsDir, "tech-stack"), new Set());
|
|
452
595
|
}
|
|
453
596
|
|
|
454
|
-
|
|
455
|
-
|
|
597
|
+
for (const f of ["DESIGN_SYSTEM.md", "DESIGN_SPEC.md"]) {
|
|
598
|
+
const src = path.join(TEMPLATES_DIR, "docs", f);
|
|
599
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(docsDir, f));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── npm install ────────────────────────────────────────────────────────────
|
|
603
|
+
console.log(` ${C.dim}${t.installing}${C.reset}\n`);
|
|
456
604
|
const install = spawnSync("npm", ["install"], {
|
|
457
605
|
cwd: outDir,
|
|
458
606
|
stdio: "inherit",
|
|
459
607
|
shell: true,
|
|
460
608
|
});
|
|
461
609
|
if (install.status !== 0) {
|
|
462
|
-
console.log(`\n
|
|
610
|
+
console.log(`\n ${t.installFail(projectSlug)}`);
|
|
463
611
|
}
|
|
464
612
|
|
|
465
|
-
// ──
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
── Open in your AI coding tool and type /build ─────────────────
|
|
474
|
-
|
|
475
|
-
Claude Code → claude ./${projectSlug}
|
|
476
|
-
Cursor → cursor ./${projectSlug}
|
|
477
|
-
Windsurf → windsurf ./${projectSlug}
|
|
478
|
-
|
|
479
|
-
Then type: /build
|
|
480
|
-
|
|
481
|
-
The agent will read your docs, create the build spec, pick a theme,
|
|
482
|
-
and start coding the MVP — no copy-paste, no extra setup.
|
|
483
|
-
`);
|
|
613
|
+
// ── Done ───────────────────────────────────────────────────────────────────
|
|
614
|
+
const doneLines = t.done(projectSlug).split("\n");
|
|
615
|
+
const styledDone = doneLines.map((line, i) => {
|
|
616
|
+
if (i === 1) return ` ${C.green}${C.bold}${line.trim()}${C.reset}`;
|
|
617
|
+
if (line.includes("/build")) return line.replace("/build", `${C.cyan}${C.bold}/build${C.reset}`);
|
|
618
|
+
return `${C.dim}${line}${C.reset}`;
|
|
619
|
+
}).join("\n");
|
|
620
|
+
console.log(styledDone);
|
|
484
621
|
}
|
|
485
622
|
|
|
486
623
|
main().catch((err) => {
|
|
487
|
-
|
|
624
|
+
process.stdout.write(C.showCursor);
|
|
625
|
+
console.error(`\n ${C.dim}Something went wrong:${C.reset}`, err.message);
|
|
488
626
|
process.exit(1);
|
|
489
627
|
});
|
package/package.json
CHANGED