launchframe 0.3.1 → 0.4.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/.claude/skills/clone-website/SKILL.md +473 -0
- package/.clinerules +147 -0
- package/.codex/skills/clone-website/SKILL.md +473 -0
- package/{template/.continue → .continue}/commands/clone-website.md +475 -566
- package/.continue/rules/project.md +151 -0
- package/{template/.augment → .cursor}/commands/clone-website.md +470 -565
- package/.cursor/rules/project.mdc +7 -0
- package/{template/.gemini → .gemini}/commands/clone-website.toml +476 -567
- package/.github/copilot-instructions.md +147 -0
- package/.github/skills/clone-website/SKILL.md +473 -0
- package/.gitignore +49 -0
- package/{template/.opencode → .opencode}/commands/clone-website.md +473 -564
- package/{template/.cursor/commands → .windsurf/workflows}/clone-website.md +470 -561
- package/AGENTS.md +65 -0
- package/README.md +137 -31
- package/bin/launchframe.mjs +343 -315
- package/docs/research/INSPECTION_GUIDE.md +80 -0
- package/package.json +71 -26
- package/{template/src → src}/app/globals.css +1 -93
- package/{template/src → src}/app/layout.tsx +16 -5
- package/src/app/page.tsx +40 -0
- package/src/lib/launchframe-config.ts +8 -0
- package/template/.amazonq/cli-agents/clone-website.json +0 -9
- package/template/.amazonq/rules/project.md +0 -281
- package/template/.claude/skills/clone-website/SKILL.md +0 -564
- package/template/.claude/skills/marketing-social-proof-motion/SKILL.md +0 -47
- package/template/.clinerules +0 -285
- package/template/.codex/skills/clone-website/SKILL.md +0 -564
- package/template/.continue/rules/project.md +0 -285
- package/template/.cursor/commands/marketing-social-proof-motion.md +0 -42
- package/template/.cursor/rules/project.mdc +0 -22
- package/template/.github/copilot-instructions.md +0 -281
- package/template/.github/skills/clone-website/SKILL.md +0 -564
- package/template/.nvmrc +0 -1
- package/template/.windsurf/workflows/clone-website.md +0 -561
- package/template/AGENTS.md +0 -160
- package/template/LICENSE +0 -21
- package/template/README.md +0 -121
- package/template/START_HERE.md +0 -15
- package/template/docs/design-references/playwright-example.com-1440px.png +0 -0
- package/template/docs/design-references/playwright-example.com-390px.png +0 -0
- package/template/docs/research/INSPECTION_GUIDE.md +0 -124
- package/template/launchframe.config.json +0 -14
- package/template/package-lock.json +0 -9873
- package/template/package.json +0 -54
- package/template/scripts/.gitkeep +0 -0
- package/template/scripts/recon-playwright.mjs +0 -396
- package/template/src/app/page.tsx +0 -5
- package/template/src/components/marketing/scribewise-landing.tsx +0 -34
- package/template/src/hooks/.gitkeep +0 -0
- package/template/src/types/.gitkeep +0 -0
- /package/{template/.aider.conf.yml → .aider.conf.yml} +0 -0
- /package/{template/.dockerignore → .dockerignore} +0 -0
- /package/{template/.gitattributes → .gitattributes} +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/bug_report.yml +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/config.yml +0 -0
- /package/{template/.github → .github}/ISSUE_TEMPLATE/feature_request.yml +0 -0
- /package/{template/.github → .github}/PULL_REQUEST_TEMPLATE.md +0 -0
- /package/{template/.github → .github}/copilot-setup-steps.yml +0 -0
- /package/{template/.github → .github}/workflows/ci.yml +0 -0
- /package/{template/.windsurfrules → .windsurfrules} +0 -0
- /package/{template/CLAUDE.md → CLAUDE.md} +0 -0
- /package/{template/Dockerfile → Dockerfile} +0 -0
- /package/{template/Dockerfile.dev → Dockerfile.dev} +0 -0
- /package/{template/GEMINI.md → GEMINI.md} +0 -0
- /package/{template/components.json → components.json} +0 -0
- /package/{template/docker-compose.yml → docker-compose.yml} +0 -0
- /package/{template/docs → docs}/design-references/.gitkeep +0 -0
- /package/{template/docs → docs}/design-references/comparison.png +0 -0
- /package/{template/eslint.config.mjs → eslint.config.mjs} +0 -0
- /package/{template/next.config.ts → next.config.ts} +0 -0
- /package/{template/postcss.config.mjs → postcss.config.mjs} +0 -0
- /package/{template/public/images → scripts}/.gitkeep +0 -0
- /package/{template/scripts → scripts}/sync-agent-rules.sh +0 -0
- /package/{template/scripts → scripts}/sync-skills.mjs +0 -0
- /package/{template/src → src}/app/favicon.ico +0 -0
- /package/{template/src → src}/components/ui/button.tsx +0 -0
- /package/{template/public/seo → src/hooks}/.gitkeep +0 -0
- /package/{template/src → src}/lib/utils.ts +0 -0
- /package/{template/public/videos → src/types}/.gitkeep +0 -0
- /package/{template/tsconfig.json → tsconfig.json} +0 -0
package/bin/launchframe.mjs
CHANGED
|
@@ -1,315 +1,343 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
pkg
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
${
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scaffold a Launchframe project: clone reference URL via AI workflow + SaaS landing copy.
|
|
4
|
+
* Usage: npx launchframe@latest <url> "<saas-idea>" [--dir=name] [--skip-install]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const PKG_ROOT = resolve(__dirname, "..");
|
|
14
|
+
|
|
15
|
+
const SKIP_DIR_NAMES = new Set(["bin", "node_modules", ".git", ".next", "dist", "out"]);
|
|
16
|
+
|
|
17
|
+
/** Root dot-directories we ship for agent tooling */
|
|
18
|
+
const ALLOW_DOT_DIRS = new Set([
|
|
19
|
+
".cursor",
|
|
20
|
+
".claude",
|
|
21
|
+
".codex",
|
|
22
|
+
".continue",
|
|
23
|
+
".gemini",
|
|
24
|
+
".opencode",
|
|
25
|
+
".windsurf",
|
|
26
|
+
".github",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SKIP_ROOT_FILES = new Set(["package-lock.json"]);
|
|
30
|
+
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(`
|
|
33
|
+
launchframe — scaffold a Next.js app for cloning a reference site + your SaaS idea
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
npx launchframe@latest <url> "<saas-idea>" [options]
|
|
37
|
+
|
|
38
|
+
Arguments:
|
|
39
|
+
<url> HTTPS URL of the site to reverse-engineer (visual reference)
|
|
40
|
+
"<saas-idea>" Short pitch / positioning that should appear on the landing page
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--dir, -o Output folder (default: first label of hostname + "-launchframe")
|
|
44
|
+
--skip-install Do not run npm install after scaffolding
|
|
45
|
+
-h, --help Show this message
|
|
46
|
+
|
|
47
|
+
Note:
|
|
48
|
+
The output folder must not live inside the Launchframe package directory (the folder that contains this CLI).
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
npx launchframe@latest https://stripe.com "AI invoicing for freelancers"
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const out = {
|
|
57
|
+
url: null,
|
|
58
|
+
idea: null,
|
|
59
|
+
dir: null,
|
|
60
|
+
skipInstall: false,
|
|
61
|
+
help: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const positional = [];
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === "-h" || a === "--help") {
|
|
68
|
+
out.help = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (a === "--skip-install") {
|
|
72
|
+
out.skipInstall = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (a === "--dir" || a === "-o") {
|
|
76
|
+
out.dir = argv[++i];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (a.startsWith("--dir=")) {
|
|
80
|
+
out.dir = a.slice("--dir=".length);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (a.startsWith("-o=")) {
|
|
84
|
+
out.dir = a.slice("-o=".length);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
positional.push(a);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (positional[0]) out.url = positional[0];
|
|
91
|
+
if (positional[1]) out.idea = positional[1];
|
|
92
|
+
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateUrl(raw) {
|
|
97
|
+
let u;
|
|
98
|
+
try {
|
|
99
|
+
u = new URL(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
102
|
+
}
|
|
103
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
104
|
+
throw new Error("URL must start with http:// or https://");
|
|
105
|
+
}
|
|
106
|
+
return u.href;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defaultDirName(urlStr) {
|
|
110
|
+
try {
|
|
111
|
+
const host = new URL(urlStr).hostname.replace(/^www\./i, "");
|
|
112
|
+
const label = host.split(".")[0] || "site";
|
|
113
|
+
const safe = label.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
|
|
114
|
+
return `${safe || "site"}-launchframe`;
|
|
115
|
+
} catch {
|
|
116
|
+
return "launchframe-app";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function slugFromDir(dir) {
|
|
122
|
+
const base = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "launchframe-app";
|
|
123
|
+
return base
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9-_]/g, "-")
|
|
126
|
+
.replace(/-+/g, "-")
|
|
127
|
+
.replace(/^-|-$/g, "") || "launchframe-app";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function tsStringLiteral(value) {
|
|
131
|
+
return JSON.stringify(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function shouldCopyRootEntry(baseName, isDirectory) {
|
|
135
|
+
if (SKIP_DIR_NAMES.has(baseName)) return false;
|
|
136
|
+
if (isDirectory && baseName.startsWith(".")) {
|
|
137
|
+
if (!ALLOW_DOT_DIRS.has(baseName)) return false;
|
|
138
|
+
}
|
|
139
|
+
if (!isDirectory && SKIP_ROOT_FILES.has(baseName)) return false;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Output cannot be inside the template package tree (would recurse while copying). */
|
|
144
|
+
function isForbiddenOutput(packageRootAbs, destRootAbs) {
|
|
145
|
+
const pkg = resolve(packageRootAbs);
|
|
146
|
+
const dest = resolve(destRootAbs);
|
|
147
|
+
if (dest === pkg) return true;
|
|
148
|
+
const rel = relative(pkg, dest);
|
|
149
|
+
return Boolean(rel) && !rel.startsWith("..") && !isAbsolute(rel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function copyTemplateTree(sourceRoot, destRootAbs) {
|
|
153
|
+
await mkdir(destRootAbs, { recursive: true });
|
|
154
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
155
|
+
const destResolved = resolve(destRootAbs);
|
|
156
|
+
for (const ent of entries) {
|
|
157
|
+
const from = join(sourceRoot, ent.name);
|
|
158
|
+
if (resolve(from) === destResolved) continue;
|
|
159
|
+
if (!shouldCopyRootEntry(ent.name, ent.isDirectory())) continue;
|
|
160
|
+
const to = join(destRootAbs, ent.name);
|
|
161
|
+
await cp(from, to, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function writeGeneratedPackageJson(destRoot, npmPackageName) {
|
|
166
|
+
const raw = await readFile(join(PKG_ROOT, "package.json"), "utf8");
|
|
167
|
+
const pkg = JSON.parse(raw);
|
|
168
|
+
|
|
169
|
+
const nextPkg = {
|
|
170
|
+
name: npmPackageName,
|
|
171
|
+
version: "0.1.0",
|
|
172
|
+
private: true,
|
|
173
|
+
description: pkg.description,
|
|
174
|
+
license: pkg.license,
|
|
175
|
+
engines: pkg.engines,
|
|
176
|
+
scripts: pkg.scripts,
|
|
177
|
+
dependencies: pkg.dependencies,
|
|
178
|
+
devDependencies: pkg.devDependencies,
|
|
179
|
+
keywords: pkg.keywords,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
await writeFile(
|
|
183
|
+
join(destRoot, "package.json"),
|
|
184
|
+
`${JSON.stringify(nextPkg, null, 2)}\n`,
|
|
185
|
+
"utf8",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function writeLaunchframeArtifacts(destRoot, url, idea) {
|
|
190
|
+
const configTs = `/**
|
|
191
|
+
* Written by Launchframe CLI — edit freely after scaffolding.
|
|
192
|
+
*/
|
|
193
|
+
export const LAUNCHFRAME_SOURCE_URL = ${tsStringLiteral(url)} as const;
|
|
194
|
+
|
|
195
|
+
export const LAUNCHFRAME_SAAS_IDEA = ${tsStringLiteral(idea)} as const;
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
await writeFile(join(destRoot, "src", "lib", "launchframe-config.ts"), configTs, "utf8");
|
|
199
|
+
|
|
200
|
+
const ctx = {
|
|
201
|
+
sourceUrl: url,
|
|
202
|
+
saasIdea: idea,
|
|
203
|
+
notes:
|
|
204
|
+
"Use /clone-website with sourceUrl for pixel-perfect extraction. Align landing copy with saasIdea.",
|
|
205
|
+
};
|
|
206
|
+
await writeFile(
|
|
207
|
+
join(destRoot, "launchframe.context.json"),
|
|
208
|
+
`${JSON.stringify(ctx, null, 2)}\n`,
|
|
209
|
+
"utf8",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const md = `# Launchframe context
|
|
213
|
+
|
|
214
|
+
## Reference URL (clone target)
|
|
215
|
+
|
|
216
|
+
${url}
|
|
217
|
+
|
|
218
|
+
## SaaS idea (landing copy)
|
|
219
|
+
|
|
220
|
+
${idea}
|
|
221
|
+
|
|
222
|
+
When running \`/clone-website\`, pass the reference URL above. After structural cloning, rewrite headings and hero copy so they clearly communicate the SaaS idea while respecting attribution and copyright for third-party brands.
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
await mkdir(join(destRoot, "docs", "research"), { recursive: true });
|
|
226
|
+
await writeFile(join(destRoot, "docs", "research", "LAUNCHFRAME.md"), md, "utf8");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function writeReadme(destRoot, npmPackageName, url, idea) {
|
|
230
|
+
const body = `# ${npmPackageName}
|
|
231
|
+
|
|
232
|
+
Created with [\`launchframe\`](https://www.npmjs.com/package/launchframe).
|
|
233
|
+
|
|
234
|
+
## Inputs
|
|
235
|
+
|
|
236
|
+
- **Reference site:** ${url}
|
|
237
|
+
- **SaaS idea:** ${idea}
|
|
238
|
+
|
|
239
|
+
## Quick start
|
|
240
|
+
|
|
241
|
+
\`\`\`bash
|
|
242
|
+
npm install
|
|
243
|
+
npm run dev
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
Open your AI agent (Cursor, Claude Code, etc.) and run:
|
|
247
|
+
|
|
248
|
+
\`\`\`
|
|
249
|
+
/clone-website ${url}
|
|
250
|
+
\`\`\`
|
|
251
|
+
|
|
252
|
+
Keep the SaaS positioning from \`launchframe.context.json\` / \`src/lib/launchframe-config.ts\` when adapting cloned sections.
|
|
253
|
+
|
|
254
|
+
See \`AGENTS.md\` for full agent instructions.
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
await writeFile(join(destRoot, "README.md"), body, "utf8");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function runNpmInstall(cwd) {
|
|
261
|
+
return new Promise((resolvePromise, reject) => {
|
|
262
|
+
const child = spawn("npm", ["install"], {
|
|
263
|
+
cwd,
|
|
264
|
+
stdio: "inherit",
|
|
265
|
+
shell: process.platform === "win32",
|
|
266
|
+
});
|
|
267
|
+
child.on("exit", (code) => {
|
|
268
|
+
if (code === 0) resolvePromise();
|
|
269
|
+
else reject(new Error(`npm install exited with code ${code}`));
|
|
270
|
+
});
|
|
271
|
+
child.on("error", reject);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function main() {
|
|
276
|
+
const args = parseArgs(process.argv.slice(2));
|
|
277
|
+
if (args.help) {
|
|
278
|
+
printHelp();
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!args.url || !args.idea) {
|
|
283
|
+
console.error("Error: missing URL or SaaS idea.\n");
|
|
284
|
+
printHelp();
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let url;
|
|
289
|
+
try {
|
|
290
|
+
url = validateUrl(args.url);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error(String(e.message));
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const idea = args.idea.trim();
|
|
297
|
+
if (!idea) {
|
|
298
|
+
console.error("Error: SaaS idea cannot be empty.");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const dirName = args.dir?.trim() || defaultDirName(url);
|
|
303
|
+
const destRoot = resolve(process.cwd(), dirName);
|
|
304
|
+
const npmPackageName = slugFromDir(dirName);
|
|
305
|
+
|
|
306
|
+
if (isForbiddenOutput(PKG_ROOT, destRoot)) {
|
|
307
|
+
console.error(
|
|
308
|
+
"Output folder cannot be inside the Launchframe package directory. Run from a parent folder or choose another path.",
|
|
309
|
+
);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await mkdir(destRoot, { recursive: true });
|
|
314
|
+
|
|
315
|
+
await copyTemplateTree(PKG_ROOT, destRoot);
|
|
316
|
+
|
|
317
|
+
await writeGeneratedPackageJson(destRoot, npmPackageName);
|
|
318
|
+
await writeLaunchframeArtifacts(destRoot, url, idea);
|
|
319
|
+
await writeReadme(destRoot, npmPackageName, url, idea);
|
|
320
|
+
|
|
321
|
+
console.log(`\nCreated Launchframe project at ${destRoot}`);
|
|
322
|
+
console.log(` Reference URL: ${url}`);
|
|
323
|
+
console.log(` SaaS idea: ${idea}\n`);
|
|
324
|
+
|
|
325
|
+
if (!args.skipInstall) {
|
|
326
|
+
console.log("Running npm install...\n");
|
|
327
|
+
try {
|
|
328
|
+
await runNpmInstall(destRoot);
|
|
329
|
+
console.log("\nDone. Next: cd " + JSON.stringify(dirName) + " && npm run dev\n");
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.error(String(e.message));
|
|
332
|
+
console.error("\nDependencies were not installed. Run npm install inside the folder manually.\n");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
console.log("Skipped npm install (--skip-install). Run npm install inside the folder.\n");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main().catch((err) => {
|
|
341
|
+
console.error(err);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
});
|