launchframe 0.2.3 → 0.2.5

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 (31) hide show
  1. package/README.md +147 -147
  2. package/bin/launchframe.mjs +315 -315
  3. package/package.json +1 -1
  4. package/template/.amazonq/cli-agents/clone-website.json +1 -1
  5. package/template/.amazonq/rules/project.md +180 -109
  6. package/template/.augment/commands/clone-website.md +33 -3
  7. package/template/.claude/skills/clone-website/SKILL.md +564 -534
  8. package/template/.claude/skills/marketing-landing-production/SKILL.md +36 -0
  9. package/template/.clinerules +180 -109
  10. package/template/.codex/skills/clone-website/SKILL.md +33 -3
  11. package/template/.continue/commands/clone-website.md +33 -3
  12. package/template/.continue/rules/project.md +180 -109
  13. package/template/.cursor/commands/clone-website.md +33 -3
  14. package/template/.cursor/commands/marketing-landing-production.md +31 -0
  15. package/template/.cursor/rules/project.mdc +25 -20
  16. package/template/.gemini/commands/clone-website.toml +33 -3
  17. package/template/.github/copilot-instructions.md +180 -109
  18. package/template/.github/skills/clone-website/SKILL.md +33 -3
  19. package/template/.opencode/commands/clone-website.md +33 -3
  20. package/template/.windsurf/workflows/clone-website.md +33 -3
  21. package/template/AGENTS.md +148 -89
  22. package/template/README.md +121 -120
  23. package/template/START_HERE.md +15 -15
  24. package/template/docs/design-references/playwright-example.com-1440px.png +0 -0
  25. package/template/docs/design-references/playwright-example.com-390px.png +0 -0
  26. package/template/docs/research/INSPECTION_GUIDE.md +121 -109
  27. package/template/package.json +63 -60
  28. package/template/scripts/recon-playwright.mjs +323 -0
  29. package/template/src/app/globals.css +93 -1
  30. package/template/src/app/layout.tsx +3 -2
  31. package/template/src/app/page.tsx +3 -7
@@ -1,315 +1,315 @@
1
- #!/usr/bin/env node
2
- // @ts-check
3
- /**
4
- * launchframe — scaffold an AI-cloner project pointed at any URL + SaaS idea.
5
- *
6
- * Usage:
7
- * npx launchframe@latest <url> "<saas idea>" [--dir <path>] [--force] [--skip-install]
8
- *
9
- * Behavior:
10
- * 1. Validates the URL and SaaS-idea string.
11
- * 2. Copies the bundled `template/` payload into the project root (current
12
- * directory by default) so dotfolders like `.cursor` and `.claude` live at
13
- * the workspace root when you open that folder in your editor. Use
14
- * `--dir <name>` for a subdirectory if you prefer.
15
- * 3. Writes `launchframe.config.json` into the new project so the bundled
16
- * `/clone-website` skill knows which URL to clone and how to re-skin it
17
- * for your SaaS idea.
18
- * 4. Runs `npm install` in the new project (so the user does not have to).
19
- * 5. Prints one line: open the folder in Cursor and tell the AI **Build it**.
20
- */
21
-
22
- import { spawnSync } from "node:child_process";
23
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
24
- import { createRequire } from "node:module";
25
- import { dirname, basename, isAbsolute, join, resolve } from "node:path";
26
- import { fileURLToPath } from "node:url";
27
-
28
- const __filename = fileURLToPath(import.meta.url);
29
- const __dirname = dirname(__filename);
30
- const pkgRoot = resolve(__dirname, "..");
31
- const templateDir = join(pkgRoot, "template");
32
- const require = createRequire(import.meta.url);
33
- const pkgJson = require(join(pkgRoot, "package.json"));
34
-
35
- const COLORS = {
36
- reset: "\x1b[0m",
37
- bold: "\x1b[1m",
38
- dim: "\x1b[2m",
39
- red: "\x1b[31m",
40
- green: "\x1b[32m",
41
- yellow: "\x1b[33m",
42
- cyan: "\x1b[36m",
43
- };
44
- const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
45
- const c = (color, s) => (supportsColor ? COLORS[color] + s + COLORS.reset : s);
46
-
47
- function printHelp() {
48
- console.log(`
49
- ${c("bold", "launchframe")} ${c("dim", `v${pkgJson.version}`)}
50
- Scaffold an AI-cloner project pointed at any URL + SaaS idea.
51
-
52
- ${c("bold", "Usage:")}
53
- npx launchframe@latest <url> "<saas idea>" [options]
54
-
55
- ${c("bold", "Arguments:")}
56
- <url> URL of the site you want to clone (e.g. https://linear.app)
57
- <saas idea> One-line description of the SaaS you're building
58
- (used to re-skin copy/branding after the visual clone)
59
-
60
- ${c("bold", "Options:")}
61
- --dir <path> Output folder (default: . — current directory / project root)
62
- --force Overwrite merging files into a non-empty directory (use with care)
63
- --skip-install Skip npm install after scaffold (faster for CI / debugging)
64
- --help, -h Show this message
65
- --version, -v Show the launchframe version
66
-
67
- ${c("bold", "Example:")}
68
- npx launchframe@latest https://linear.app "AI-powered customer feedback platform"
69
- # From an empty folder (or git init only), files land in . so .cursor/ works at workspace root
70
- npx launchframe@latest https://vercel.com "DevOps for ML" --dir launchframe-app
71
- npx launchframe@latest https://stripe.com "Billing for AI agents" --skip-install
72
- `);
73
- }
74
-
75
- function exitErr(msg, code = 1) {
76
- console.error(c("red", `\nlaunchframe: ${msg}`));
77
- console.error(c("dim", "Run `npx launchframe --help` for usage.\n"));
78
- process.exit(code);
79
- }
80
-
81
- function parseArgs(argv) {
82
- const positional = [];
83
- const opts = {
84
- dir: ".",
85
- force: false,
86
- skipInstall: false,
87
- help: false,
88
- version: false,
89
- };
90
- for (let i = 0; i < argv.length; i++) {
91
- const a = argv[i];
92
- if (a === "--help" || a === "-h") opts.help = true;
93
- else if (a === "--version" || a === "-v") opts.version = true;
94
- else if (a === "--force" || a === "-f") opts.force = true;
95
- else if (a === "--skip-install") opts.skipInstall = true;
96
- else if (a === "--dir" || a === "-d") {
97
- const next = argv[++i];
98
- if (!next || next.startsWith("-")) exitErr("`--dir` requires a value");
99
- opts.dir = next;
100
- } else if (a.startsWith("--dir=")) {
101
- opts.dir = a.slice("--dir=".length);
102
- } else if (a.startsWith("-")) {
103
- exitErr(`unknown option: ${a}`);
104
- } else {
105
- positional.push(a);
106
- }
107
- }
108
- return { positional, opts };
109
- }
110
-
111
- function normalizeUrl(raw) {
112
- let candidate = raw.trim();
113
- if (!/^https?:\/\//i.test(candidate)) candidate = `https://${candidate}`;
114
- try {
115
- const u = new URL(candidate);
116
- if (!u.hostname || !u.hostname.includes(".")) throw new Error("missing hostname");
117
- return u.toString();
118
- } catch {
119
- exitErr(`invalid URL: ${raw}`);
120
- }
121
- }
122
-
123
- function isDirEmpty(dir) {
124
- try {
125
- return readdirSync(dir).length === 0;
126
- } catch {
127
- return true;
128
- }
129
- }
130
-
131
- /** True if the folder is empty or only has git/bootstrap noise (so we can scaffold at project root). */
132
- function isScaffoldRootUsable(dir) {
133
- let names;
134
- try {
135
- names = readdirSync(dir);
136
- } catch {
137
- return true;
138
- }
139
- if (names.length === 0) return true;
140
- const allowedOnly = new Set([".git", ".gitignore", ".gitattributes"]);
141
- return names.every((n) => allowedOnly.has(n));
142
- }
143
-
144
- function npmPackageSlug(dirName, targetDir) {
145
- const raw =
146
- dirName === "." || dirName === "./"
147
- ? basename(targetDir) || "launchframe-app"
148
- : dirName;
149
- const slug = raw
150
- .toLowerCase()
151
- .replace(/[^a-z0-9-_]/g, "-")
152
- .replace(/-+/g, "-")
153
- .replace(/^-|-$/g, "")
154
- .slice(0, 64);
155
- return slug || "launchframe-app";
156
- }
157
-
158
- function rewritePackageJson(targetDir, packageSlug, url, idea) {
159
- const pkgPath = join(targetDir, "package.json");
160
- if (!existsSync(pkgPath)) return;
161
- let pkg;
162
- try {
163
- pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
164
- } catch {
165
- return;
166
- }
167
- pkg.name = packageSlug;
168
- pkg.version = "0.1.0";
169
- pkg.private = true;
170
- pkg.description = `Launchframe project — clone of ${url} reframed as: ${idea}`;
171
- delete pkg.author;
172
- delete pkg.homepage;
173
- delete pkg.repository;
174
- delete pkg.bugs;
175
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
176
- }
177
-
178
- function writeLaunchframeConfig(targetDir, url, idea) {
179
- const cfg = {
180
- $schema: "https://launchframe.dev/schema/launchframe.config.json",
181
- url,
182
- idea,
183
- createdAt: new Date().toISOString(),
184
- launchframeVersion: pkgJson.version,
185
- notes: [
186
- "The /clone-website skill reads this file at the start of every run.",
187
- "After scaffold: open this folder in Cursor (or your AI editor) and say **Build it** — same workflow.",
188
- "`url` is the visual source-of-truth (clone its layout, spacing, tokens, motion).",
189
- "`idea` is the rebranding directive applied AFTER the pixel-perfect clone.",
190
- "Edit either field and re-invoke the skill to re-run.",
191
- ],
192
- };
193
- writeFileSync(
194
- join(targetDir, "launchframe.config.json"),
195
- JSON.stringify(cfg, null, 2) + "\n",
196
- "utf8"
197
- );
198
- }
199
-
200
- function runNpmInstall(targetDir) {
201
- console.log(c("dim", "\nRunning npm install (this may take a minute)...\n"));
202
- const result = spawnSync("npm", ["install", "--no-fund", "--no-audit"], {
203
- cwd: targetDir,
204
- stdio: "inherit",
205
- shell: true,
206
- env: process.env,
207
- });
208
- if (result.status !== 0) {
209
- console.error(
210
- c("yellow", "\nlaunchframe: npm install exited with an error. ") +
211
- c("dim", `Fix the issue and run \`npm install\` inside the project folder, or re-run with \`--force\`.\n`)
212
- );
213
- process.exit(result.status === null ? 1 : result.status);
214
- }
215
- console.log(c("green", "\n\u2713 npm install finished.\n"));
216
- }
217
-
218
- function nextSteps(projectDir, dirLabel, openHint, url, idea) {
219
- console.log(`
220
- ${c("green", "\u2713")} Done — ${c("bold", dirLabel)} is ready.
221
-
222
- ${c("dim", "Target URL:")} ${c("cyan", url)}
223
- ${c("dim", "SaaS idea:")} ${c("cyan", idea)}
224
- ${c("dim", "Folder:")} ${projectDir}
225
-
226
- ${c("bold", "All you do next:")}
227
- 1. Open ${c("cyan", openHint)} in ${c("bold", "Cursor")} ${c("dim", "(File \u2192 Open Folder)")}
228
- 2. In chat, say: ${c("bold", "Build it")}
229
- ${c("dim", "Your AI reads launchframe.config.json + AGENTS.md and runs the full clone + rebrand workflow.")}
230
- ${c("dim", "You can also type ") + c("cyan", "/clone-website") + c("dim", " if you prefer.")}
231
-
232
- ${c("dim", "Dotfolders (.cursor, .claude, …) are at this project root so rules and skills apply when you open this folder.")}
233
- ${c("dim", "Other editors: same folder — say Build it, or run the /clone-website skill for your tool.")}
234
- ${c("dim", "Edit launchframe.config.json anytime to change URL or SaaS idea.")}
235
- `);
236
- }
237
-
238
- function main() {
239
- const { positional, opts } = parseArgs(process.argv.slice(2));
240
- if (opts.help) return printHelp();
241
- if (opts.version) return console.log(pkgJson.version);
242
-
243
- if (positional.length < 2) {
244
- if (positional.length === 0) {
245
- console.error(c("red", "\nlaunchframe: missing <url> and <saas idea>"));
246
- } else {
247
- console.error(c("red", '\nlaunchframe: missing <saas idea> (wrap it in quotes: "...")'));
248
- }
249
- printHelp();
250
- process.exit(1);
251
- }
252
- if (positional.length > 2) {
253
- exitErr(
254
- `too many positional arguments. Wrap your SaaS idea in quotes: ` +
255
- `\`npx launchframe ${positional[0]} "${positional.slice(1).join(" ")}"\``
256
- );
257
- }
258
-
259
- const url = normalizeUrl(positional[0]);
260
- const idea = positional[1].trim();
261
- if (!idea) exitErr("SaaS idea cannot be empty");
262
-
263
- const dirNameRaw = opts.dir;
264
- const targetDir = isAbsolute(dirNameRaw) ? dirNameRaw : resolve(process.cwd(), dirNameRaw);
265
-
266
- const isRootDot = dirNameRaw === "." || dirNameRaw === "./";
267
- const usable = isDirEmpty(targetDir) || isScaffoldRootUsable(targetDir);
268
- if (existsSync(targetDir) && !usable && !opts.force) {
269
- exitErr(
270
- `target directory \`${dirNameRaw}\` is not empty.\n` +
271
- `Create an empty folder (or \`git init\` only), run again from there, or pass \`--force\` to merge files, ` +
272
- `or use \`--dir launchframe-app\` to scaffold into a subdirectory.`
273
- );
274
- }
275
-
276
- if (!existsSync(templateDir)) {
277
- exitErr(
278
- `bundled template not found at \`${templateDir}\`.\n` +
279
- `This usually means the launchframe package was installed incompletely. ` +
280
- `Try: \`npm install -g launchframe@latest\` or re-run \`npx launchframe@latest ...\`.`
281
- );
282
- }
283
-
284
- mkdirSync(targetDir, { recursive: true });
285
-
286
- console.log(c("dim", `\nCopying template \u2192 ${targetDir} ...`));
287
- cpSync(templateDir, targetDir, {
288
- recursive: true,
289
- force: opts.force,
290
- filter: (src) => {
291
- const lower = src.toLowerCase();
292
- if (lower.endsWith(`${"\\"}node_modules`) || lower.endsWith("/node_modules")) return false;
293
- if (lower.endsWith(`${"\\"}.next`) || lower.endsWith("/.next")) return false;
294
- if (lower.endsWith("package-lock.json")) return false;
295
- return true;
296
- },
297
- });
298
-
299
- const packageSlug = npmPackageSlug(dirNameRaw, targetDir);
300
- rewritePackageJson(targetDir, packageSlug, url, idea);
301
- writeLaunchframeConfig(targetDir, url, idea);
302
-
303
- if (!opts.skipInstall) runNpmInstall(targetDir);
304
-
305
- const dirLabel = isRootDot ? "this folder (project root)" : dirNameRaw;
306
- const openHint = isRootDot ? "this folder" : `./${dirNameRaw}`;
307
- nextSteps(targetDir, dirLabel, openHint, url, idea);
308
- }
309
-
310
- try {
311
- main();
312
- } catch (err) {
313
- console.error(c("red", `\nlaunchframe: ${err?.message || err}\n`));
314
- process.exit(1);
315
- }
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * launchframe — scaffold an AI-cloner project pointed at any URL + SaaS idea.
5
+ *
6
+ * Usage:
7
+ * npx launchframe@latest <url> "<saas idea>" [--dir <path>] [--force] [--skip-install]
8
+ *
9
+ * Behavior:
10
+ * 1. Validates the URL and SaaS-idea string.
11
+ * 2. Copies the bundled `template/` payload into the project root (current
12
+ * directory by default) so dotfolders like `.cursor` and `.claude` live at
13
+ * the workspace root when you open that folder in your editor. Use
14
+ * `--dir <name>` for a subdirectory if you prefer.
15
+ * 3. Writes `launchframe.config.json` into the new project so the bundled
16
+ * `/clone-website` skill knows which URL to clone and how to re-skin it
17
+ * for your SaaS idea.
18
+ * 4. Runs `npm install` in the new project (so the user does not have to).
19
+ * 5. Prints one line: open the folder in Cursor and tell the AI **Build it**.
20
+ */
21
+
22
+ import { spawnSync } from "node:child_process";
23
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
24
+ import { createRequire } from "node:module";
25
+ import { dirname, basename, isAbsolute, join, resolve } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+ const pkgRoot = resolve(__dirname, "..");
31
+ const templateDir = join(pkgRoot, "template");
32
+ const require = createRequire(import.meta.url);
33
+ const pkgJson = require(join(pkgRoot, "package.json"));
34
+
35
+ const COLORS = {
36
+ reset: "\x1b[0m",
37
+ bold: "\x1b[1m",
38
+ dim: "\x1b[2m",
39
+ red: "\x1b[31m",
40
+ green: "\x1b[32m",
41
+ yellow: "\x1b[33m",
42
+ cyan: "\x1b[36m",
43
+ };
44
+ const supportsColor = process.stdout.isTTY && !process.env.NO_COLOR;
45
+ const c = (color, s) => (supportsColor ? COLORS[color] + s + COLORS.reset : s);
46
+
47
+ function printHelp() {
48
+ console.log(`
49
+ ${c("bold", "launchframe")} ${c("dim", `v${pkgJson.version}`)}
50
+ Scaffold an AI-cloner project pointed at any URL + SaaS idea.
51
+
52
+ ${c("bold", "Usage:")}
53
+ npx launchframe@latest <url> "<saas idea>" [options]
54
+
55
+ ${c("bold", "Arguments:")}
56
+ <url> URL of the site you want to clone (e.g. https://linear.app)
57
+ <saas idea> One-line description of the SaaS you're building
58
+ (used to re-skin copy/branding after the visual clone)
59
+
60
+ ${c("bold", "Options:")}
61
+ --dir <path> Output folder (default: . — current directory / project root)
62
+ --force Overwrite merging files into a non-empty directory (use with care)
63
+ --skip-install Skip npm install after scaffold (faster for CI / debugging)
64
+ --help, -h Show this message
65
+ --version, -v Show the launchframe version
66
+
67
+ ${c("bold", "Example:")}
68
+ npx launchframe@latest https://linear.app "AI-powered customer feedback platform"
69
+ # From an empty folder (or git init only), files land in . so .cursor/ works at workspace root
70
+ npx launchframe@latest https://vercel.com "DevOps for ML" --dir launchframe-app
71
+ npx launchframe@latest https://stripe.com "Billing for AI agents" --skip-install
72
+ `);
73
+ }
74
+
75
+ function exitErr(msg, code = 1) {
76
+ console.error(c("red", `\nlaunchframe: ${msg}`));
77
+ console.error(c("dim", "Run `npx launchframe --help` for usage.\n"));
78
+ process.exit(code);
79
+ }
80
+
81
+ function parseArgs(argv) {
82
+ const positional = [];
83
+ const opts = {
84
+ dir: ".",
85
+ force: false,
86
+ skipInstall: false,
87
+ help: false,
88
+ version: false,
89
+ };
90
+ for (let i = 0; i < argv.length; i++) {
91
+ const a = argv[i];
92
+ if (a === "--help" || a === "-h") opts.help = true;
93
+ else if (a === "--version" || a === "-v") opts.version = true;
94
+ else if (a === "--force" || a === "-f") opts.force = true;
95
+ else if (a === "--skip-install") opts.skipInstall = true;
96
+ else if (a === "--dir" || a === "-d") {
97
+ const next = argv[++i];
98
+ if (!next || next.startsWith("-")) exitErr("`--dir` requires a value");
99
+ opts.dir = next;
100
+ } else if (a.startsWith("--dir=")) {
101
+ opts.dir = a.slice("--dir=".length);
102
+ } else if (a.startsWith("-")) {
103
+ exitErr(`unknown option: ${a}`);
104
+ } else {
105
+ positional.push(a);
106
+ }
107
+ }
108
+ return { positional, opts };
109
+ }
110
+
111
+ function normalizeUrl(raw) {
112
+ let candidate = raw.trim();
113
+ if (!/^https?:\/\//i.test(candidate)) candidate = `https://${candidate}`;
114
+ try {
115
+ const u = new URL(candidate);
116
+ if (!u.hostname || !u.hostname.includes(".")) throw new Error("missing hostname");
117
+ return u.toString();
118
+ } catch {
119
+ exitErr(`invalid URL: ${raw}`);
120
+ }
121
+ }
122
+
123
+ function isDirEmpty(dir) {
124
+ try {
125
+ return readdirSync(dir).length === 0;
126
+ } catch {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ /** True if the folder is empty or only has git/bootstrap noise (so we can scaffold at project root). */
132
+ function isScaffoldRootUsable(dir) {
133
+ let names;
134
+ try {
135
+ names = readdirSync(dir);
136
+ } catch {
137
+ return true;
138
+ }
139
+ if (names.length === 0) return true;
140
+ const allowedOnly = new Set([".git", ".gitignore", ".gitattributes"]);
141
+ return names.every((n) => allowedOnly.has(n));
142
+ }
143
+
144
+ function npmPackageSlug(dirName, targetDir) {
145
+ const raw =
146
+ dirName === "." || dirName === "./"
147
+ ? basename(targetDir) || "launchframe-app"
148
+ : dirName;
149
+ const slug = raw
150
+ .toLowerCase()
151
+ .replace(/[^a-z0-9-_]/g, "-")
152
+ .replace(/-+/g, "-")
153
+ .replace(/^-|-$/g, "")
154
+ .slice(0, 64);
155
+ return slug || "launchframe-app";
156
+ }
157
+
158
+ function rewritePackageJson(targetDir, packageSlug, url, idea) {
159
+ const pkgPath = join(targetDir, "package.json");
160
+ if (!existsSync(pkgPath)) return;
161
+ let pkg;
162
+ try {
163
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
164
+ } catch {
165
+ return;
166
+ }
167
+ pkg.name = packageSlug;
168
+ pkg.version = "0.1.0";
169
+ pkg.private = true;
170
+ pkg.description = `Launchframe project — clone of ${url} reframed as: ${idea}`;
171
+ delete pkg.author;
172
+ delete pkg.homepage;
173
+ delete pkg.repository;
174
+ delete pkg.bugs;
175
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
176
+ }
177
+
178
+ function writeLaunchframeConfig(targetDir, url, idea) {
179
+ const cfg = {
180
+ $schema: "https://launchframe.dev/schema/launchframe.config.json",
181
+ url,
182
+ idea,
183
+ createdAt: new Date().toISOString(),
184
+ launchframeVersion: pkgJson.version,
185
+ notes: [
186
+ "The /clone-website skill reads this file at the start of every run.",
187
+ "After scaffold: open this folder in Cursor (or your AI editor) and say **Build it** — same workflow.",
188
+ "`url` is the visual source-of-truth (clone its layout, spacing, tokens, motion).",
189
+ "`idea` is the rebranding directive applied AFTER the pixel-perfect clone.",
190
+ "Edit either field and re-invoke the skill to re-run.",
191
+ ],
192
+ };
193
+ writeFileSync(
194
+ join(targetDir, "launchframe.config.json"),
195
+ JSON.stringify(cfg, null, 2) + "\n",
196
+ "utf8"
197
+ );
198
+ }
199
+
200
+ function runNpmInstall(targetDir) {
201
+ console.log(c("dim", "\nRunning npm install (this may take a minute)...\n"));
202
+ const result = spawnSync("npm", ["install", "--no-fund", "--no-audit"], {
203
+ cwd: targetDir,
204
+ stdio: "inherit",
205
+ shell: true,
206
+ env: process.env,
207
+ });
208
+ if (result.status !== 0) {
209
+ console.error(
210
+ c("yellow", "\nlaunchframe: npm install exited with an error. ") +
211
+ c("dim", `Fix the issue and run \`npm install\` inside the project folder, or re-run with \`--force\`.\n`)
212
+ );
213
+ process.exit(result.status === null ? 1 : result.status);
214
+ }
215
+ console.log(c("green", "\n\u2713 npm install finished.\n"));
216
+ }
217
+
218
+ function nextSteps(projectDir, dirLabel, openHint, url, idea) {
219
+ console.log(`
220
+ ${c("green", "\u2713")} Done — ${c("bold", dirLabel)} is ready.
221
+
222
+ ${c("dim", "Target URL:")} ${c("cyan", url)}
223
+ ${c("dim", "SaaS idea:")} ${c("cyan", idea)}
224
+ ${c("dim", "Folder:")} ${projectDir}
225
+
226
+ ${c("bold", "All you do next:")}
227
+ 1. Open ${c("cyan", openHint)} in ${c("bold", "Cursor")} ${c("dim", "(File \u2192 Open Folder)")}
228
+ 2. In chat, say: ${c("bold", "Build it")}
229
+ ${c("dim", "Your AI reads launchframe.config.json + AGENTS.md and runs the full clone + rebrand workflow.")}
230
+ ${c("dim", "You can also type ") + c("cyan", "/clone-website") + c("dim", " if you prefer.")}
231
+
232
+ ${c("dim", "Dotfolders (.cursor, .claude, …) are at this project root so rules and skills apply when you open this folder.")}
233
+ ${c("dim", "Other editors: same folder — say Build it, or run the /clone-website skill for your tool.")}
234
+ ${c("dim", "Edit launchframe.config.json anytime to change URL or SaaS idea.")}
235
+ `);
236
+ }
237
+
238
+ function main() {
239
+ const { positional, opts } = parseArgs(process.argv.slice(2));
240
+ if (opts.help) return printHelp();
241
+ if (opts.version) return console.log(pkgJson.version);
242
+
243
+ if (positional.length < 2) {
244
+ if (positional.length === 0) {
245
+ console.error(c("red", "\nlaunchframe: missing <url> and <saas idea>"));
246
+ } else {
247
+ console.error(c("red", '\nlaunchframe: missing <saas idea> (wrap it in quotes: "...")'));
248
+ }
249
+ printHelp();
250
+ process.exit(1);
251
+ }
252
+ if (positional.length > 2) {
253
+ exitErr(
254
+ `too many positional arguments. Wrap your SaaS idea in quotes: ` +
255
+ `\`npx launchframe ${positional[0]} "${positional.slice(1).join(" ")}"\``
256
+ );
257
+ }
258
+
259
+ const url = normalizeUrl(positional[0]);
260
+ const idea = positional[1].trim();
261
+ if (!idea) exitErr("SaaS idea cannot be empty");
262
+
263
+ const dirNameRaw = opts.dir;
264
+ const targetDir = isAbsolute(dirNameRaw) ? dirNameRaw : resolve(process.cwd(), dirNameRaw);
265
+
266
+ const isRootDot = dirNameRaw === "." || dirNameRaw === "./";
267
+ const usable = isDirEmpty(targetDir) || isScaffoldRootUsable(targetDir);
268
+ if (existsSync(targetDir) && !usable && !opts.force) {
269
+ exitErr(
270
+ `target directory \`${dirNameRaw}\` is not empty.\n` +
271
+ `Create an empty folder (or \`git init\` only), run again from there, or pass \`--force\` to merge files, ` +
272
+ `or use \`--dir launchframe-app\` to scaffold into a subdirectory.`
273
+ );
274
+ }
275
+
276
+ if (!existsSync(templateDir)) {
277
+ exitErr(
278
+ `bundled template not found at \`${templateDir}\`.\n` +
279
+ `This usually means the launchframe package was installed incompletely. ` +
280
+ `Try: \`npm install -g launchframe@latest\` or re-run \`npx launchframe@latest ...\`.`
281
+ );
282
+ }
283
+
284
+ mkdirSync(targetDir, { recursive: true });
285
+
286
+ console.log(c("dim", `\nCopying template \u2192 ${targetDir} ...`));
287
+ cpSync(templateDir, targetDir, {
288
+ recursive: true,
289
+ force: opts.force,
290
+ filter: (src) => {
291
+ const lower = src.toLowerCase();
292
+ if (lower.endsWith(`${"\\"}node_modules`) || lower.endsWith("/node_modules")) return false;
293
+ if (lower.endsWith(`${"\\"}.next`) || lower.endsWith("/.next")) return false;
294
+ if (lower.endsWith("package-lock.json")) return false;
295
+ return true;
296
+ },
297
+ });
298
+
299
+ const packageSlug = npmPackageSlug(dirNameRaw, targetDir);
300
+ rewritePackageJson(targetDir, packageSlug, url, idea);
301
+ writeLaunchframeConfig(targetDir, url, idea);
302
+
303
+ if (!opts.skipInstall) runNpmInstall(targetDir);
304
+
305
+ const dirLabel = isRootDot ? "this folder (project root)" : dirNameRaw;
306
+ const openHint = isRootDot ? "this folder" : `./${dirNameRaw}`;
307
+ nextSteps(targetDir, dirLabel, openHint, url, idea);
308
+ }
309
+
310
+ try {
311
+ main();
312
+ } catch (err) {
313
+ console.error(c("red", `\nlaunchframe: ${err?.message || err}\n`));
314
+ process.exit(1);
315
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchframe",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Scaffold a SaaS-ready Next.js codebase from any URL. Point launchframe at a website you admire and describe the SaaS you want to build — it drops in the AI-cloner template wired with your URL and idea so your AI agent (Claude Code, Cursor, Codex, etc.) can run /clone-website and produce a pixel-perfect, re-skinned starting point.",
5
5
  "license": "MIT",
6
6
  "author": "Evan Gruhlkey",