create-runway-app 1.0.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/README.md +50 -0
- package/index.mjs +359 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# create-runway-app
|
|
2
|
+
|
|
3
|
+
Scaffold a RunwayJS project from the terminal.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-runway-app@latest
|
|
9
|
+
npx create-runway-app@latest my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **RunwayJS access** — Purchase at [runwayjs.com/pricing](https://runwayjs.com/pricing)
|
|
15
|
+
- **API key** — Generate at [runwayjs.com/dashboard/download](https://runwayjs.com/dashboard/download) (CLI Access section)
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
18
|
+
|
|
19
|
+
**Enter your API key once** — it's stored in `~/.config/runwayjs/api-key` for future runs. No need to type it again.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# First run: prompted for key, then saved
|
|
23
|
+
npx create-runway-app@latest
|
|
24
|
+
|
|
25
|
+
# Later runs: key read automatically
|
|
26
|
+
npx create-runway-app@latest my-new-app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Override with environment variable:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
RUNWAY_API_KEY=runway_xxx npx create-runway-app@latest
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Clear stored key:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx create-runway-app@latest --clear-key
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Publishing to npm
|
|
42
|
+
|
|
43
|
+
From the package directory:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd packages/create-runway-app
|
|
47
|
+
npm publish --access public
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For scoped or private registry, configure `.npmrc` with your token.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-runway-app
|
|
5
|
+
* Scaffold a RunwayJS project from the terminal.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx create-runway-app@latest
|
|
9
|
+
* npx create-runway-app@latest my-app
|
|
10
|
+
* RUNWAY_API_KEY=xxx npx create-runway-app@latest
|
|
11
|
+
* npx create-runway-app@latest --defaults
|
|
12
|
+
* npx create-runway-app@latest --clear-key # remove stored API key
|
|
13
|
+
*
|
|
14
|
+
* API key: Enter once, stored in ~/.config/runwayjs/api-key. Set RUNWAY_API_KEY
|
|
15
|
+
* to override. Subsequent runs skip the key prompt.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { homedir, tmpdir } from "node:os";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import extract from "extract-zip";
|
|
23
|
+
import minimist from "minimist";
|
|
24
|
+
import prompts from "prompts";
|
|
25
|
+
|
|
26
|
+
const API_BASE =
|
|
27
|
+
process.env.RUNWAY_API_URL || "https://runwayjs.com";
|
|
28
|
+
|
|
29
|
+
const CONFIG_DIR = join(
|
|
30
|
+
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
|
|
31
|
+
"runwayjs"
|
|
32
|
+
);
|
|
33
|
+
const API_KEY_FILE = join(CONFIG_DIR, "api-key");
|
|
34
|
+
|
|
35
|
+
const DEFAULT_THEME = {
|
|
36
|
+
primary: "262 83% 58%",
|
|
37
|
+
background: "0 0% 100%",
|
|
38
|
+
foreground: "222 47% 11%",
|
|
39
|
+
muted: "210 40% 96%",
|
|
40
|
+
card: "0 0% 100%",
|
|
41
|
+
border: "214 32% 91%",
|
|
42
|
+
radius: "0.5rem",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const FALLBACK_SELECTIONS = {
|
|
46
|
+
homepage: "default",
|
|
47
|
+
auth: "default",
|
|
48
|
+
dashboard: "default",
|
|
49
|
+
docs: "default",
|
|
50
|
+
blog: "default",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function getStoredApiKey() {
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(API_KEY_FILE)) {
|
|
56
|
+
return readFileSync(API_KEY_FILE, "utf8").trim();
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveApiKey(key) {
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(dirname(API_KEY_FILE))) {
|
|
67
|
+
mkdirSync(dirname(API_KEY_FILE), { recursive: true, mode: 0o700 });
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(API_KEY_FILE, key.trim() + "\n", { mode: 0o600 });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// non-fatal — user can still use env
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearStoredApiKey() {
|
|
76
|
+
try {
|
|
77
|
+
if (existsSync(API_KEY_FILE)) {
|
|
78
|
+
writeFileSync(API_KEY_FILE, "");
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getApiKey() {
|
|
86
|
+
return (
|
|
87
|
+
process.env.RUNWAY_API_KEY ||
|
|
88
|
+
process.env.RUNWAY_TOKEN ||
|
|
89
|
+
getStoredApiKey()
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function fetchTemplates() {
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${API_BASE}/api/cli/templates`);
|
|
96
|
+
if (!res.ok) return null;
|
|
97
|
+
return await res.json();
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fetchProjects(apiKey) {
|
|
104
|
+
const res = await fetch(`${API_BASE}/api/projects`, {
|
|
105
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) return [];
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
return data.projects ?? [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function validateApiKey(apiKey) {
|
|
113
|
+
const res = await fetch(`${API_BASE}/api/projects`, {
|
|
114
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
115
|
+
});
|
|
116
|
+
return res.ok || res.status === 403; // 403 = has key but no access
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function downloadProject(apiKey, projectIdOrConfig, projectDir) {
|
|
120
|
+
const body = typeof projectIdOrConfig === "string"
|
|
121
|
+
? { projectId: projectIdOrConfig }
|
|
122
|
+
: { selections: projectIdOrConfig.selections, theme: projectIdOrConfig.theme };
|
|
123
|
+
|
|
124
|
+
const res = await fetch(`${API_BASE}/api/download`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
Authorization: `Bearer ${apiKey}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
const data = await res.json().catch(() => ({}));
|
|
135
|
+
const msg = data.error || `Download failed (${res.status})`;
|
|
136
|
+
const err = new Error(msg);
|
|
137
|
+
err.status = res.status;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const buffer = await res.arrayBuffer();
|
|
142
|
+
const zipPath = join(tmpdir(), `runwayjs-${Date.now()}.zip`);
|
|
143
|
+
writeFileSync(zipPath, Buffer.from(buffer));
|
|
144
|
+
await extract(zipPath, { dir: projectDir });
|
|
145
|
+
return zipPath;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toChoices(items) {
|
|
149
|
+
return items.map((t) => ({ title: t.name, value: t.slug }));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main() {
|
|
153
|
+
const argv = minimist(process.argv.slice(2));
|
|
154
|
+
const projectArg = argv._[0];
|
|
155
|
+
const useDefaults = argv.defaults === true;
|
|
156
|
+
const clearKey = argv["clear-key"] === true;
|
|
157
|
+
|
|
158
|
+
if (clearKey) {
|
|
159
|
+
clearStoredApiKey();
|
|
160
|
+
console.log(chalk.gray("\n Stored API key cleared.\n"));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(chalk.bold("\n Runway.js\n"));
|
|
165
|
+
console.log(chalk.gray(" Scaffold a production-ready Next.js project.\n"));
|
|
166
|
+
|
|
167
|
+
let apiKey = getApiKey();
|
|
168
|
+
|
|
169
|
+
if (!apiKey && !useDefaults) {
|
|
170
|
+
const { token } = await prompts({
|
|
171
|
+
type: "password",
|
|
172
|
+
name: "token",
|
|
173
|
+
message: "API key (get one at runwayjs.com/dashboard/download):",
|
|
174
|
+
validate: (v) => (v && v.startsWith("runway_") ? true : "Invalid key format"),
|
|
175
|
+
});
|
|
176
|
+
if (!token) process.exit(1);
|
|
177
|
+
apiKey = token;
|
|
178
|
+
|
|
179
|
+
// Validate and persist — only save if it works
|
|
180
|
+
const valid = await validateApiKey(apiKey);
|
|
181
|
+
if (valid) {
|
|
182
|
+
saveApiKey(apiKey);
|
|
183
|
+
console.log(chalk.gray(" Key saved for future runs.\n"));
|
|
184
|
+
} else {
|
|
185
|
+
const data = await fetch(`${API_BASE}/api/projects`, {
|
|
186
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
187
|
+
}).then((r) => r.json().catch(() => ({})));
|
|
188
|
+
if (data.error?.includes("Purchase") || data.error?.includes("access")) {
|
|
189
|
+
console.error(chalk.red(`\n ${data.error}\n`));
|
|
190
|
+
console.error(chalk.yellow(" Get access at runwayjs.com/pricing\n"));
|
|
191
|
+
} else {
|
|
192
|
+
console.error(chalk.red("\n Invalid API key. Check runwayjs.com/dashboard/download\n"));
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!apiKey) {
|
|
199
|
+
if (useDefaults) {
|
|
200
|
+
console.error(chalk.red("\n --defaults requires RUNWAY_API_KEY or a stored key.\n"));
|
|
201
|
+
} else {
|
|
202
|
+
console.error(chalk.red("\n Invalid API key. Get one at runwayjs.com/dashboard/download\n"));
|
|
203
|
+
}
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!apiKey.startsWith("runway_")) {
|
|
208
|
+
console.error(chalk.red("\n Invalid API key. Get one at runwayjs.com/dashboard/download\n"));
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Scaffold from: saved apps or scratch ─────────────────────────────────
|
|
213
|
+
let downloadConfig;
|
|
214
|
+
let projects = [];
|
|
215
|
+
|
|
216
|
+
if (useDefaults) {
|
|
217
|
+
const tmpl = await fetchTemplates();
|
|
218
|
+
const defaultSelections = tmpl?.defaultSelections ?? FALLBACK_SELECTIONS;
|
|
219
|
+
downloadConfig = {
|
|
220
|
+
selections: { ...defaultSelections },
|
|
221
|
+
theme: DEFAULT_THEME,
|
|
222
|
+
};
|
|
223
|
+
} else {
|
|
224
|
+
projects = await fetchProjects(apiKey);
|
|
225
|
+
const choices = projects.length > 0
|
|
226
|
+
? [
|
|
227
|
+
{ title: "From a saved project", value: "project" },
|
|
228
|
+
{ title: "From scratch (pick templates)", value: "scratch" },
|
|
229
|
+
]
|
|
230
|
+
: [{ title: "From scratch (pick templates)", value: "scratch" }];
|
|
231
|
+
|
|
232
|
+
const { scaffoldFrom } = await prompts({
|
|
233
|
+
type: "select",
|
|
234
|
+
name: "scaffoldFrom",
|
|
235
|
+
message: "How do you want to scaffold?",
|
|
236
|
+
choices,
|
|
237
|
+
initial: projects.length > 0 ? 0 : 0,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (scaffoldFrom === undefined) process.exit(1);
|
|
241
|
+
|
|
242
|
+
if (scaffoldFrom === "project") {
|
|
243
|
+
const { projectId } = await prompts({
|
|
244
|
+
type: "select",
|
|
245
|
+
name: "projectId",
|
|
246
|
+
message: "Select a project:",
|
|
247
|
+
choices: projects.map((p) => ({
|
|
248
|
+
title: p.title,
|
|
249
|
+
value: p.id,
|
|
250
|
+
description: p.slug ? `→ ${p.slug}` : undefined,
|
|
251
|
+
})),
|
|
252
|
+
});
|
|
253
|
+
if (!projectId) process.exit(1);
|
|
254
|
+
downloadConfig = projectId;
|
|
255
|
+
} else {
|
|
256
|
+
const tmpl = await fetchTemplates();
|
|
257
|
+
const templatesByCat = tmpl?.templates ?? {
|
|
258
|
+
homepage: [
|
|
259
|
+
{ slug: "default", name: "Default" },
|
|
260
|
+
{ slug: "runway-cloud", name: "Runway Cloud" },
|
|
261
|
+
{ slug: "runway-code", name: "Runway Code" },
|
|
262
|
+
{ slug: "modern", name: "Runway Modern" },
|
|
263
|
+
{ slug: "runway-mvp", name: "Runway MVP" },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
const defaultSelections = tmpl?.defaultSelections ?? FALLBACK_SELECTIONS;
|
|
267
|
+
const homepages = templatesByCat.homepage ?? templatesByCat.Homepage ?? [];
|
|
268
|
+
const items = homepages.length > 0 ? homepages : [
|
|
269
|
+
{ slug: "default", name: "Default" },
|
|
270
|
+
{ slug: "runway-cloud", name: "Runway Cloud" },
|
|
271
|
+
{ slug: "runway-code", name: "Runway Code" },
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const { homepage } = await prompts({
|
|
275
|
+
type: "select",
|
|
276
|
+
name: "homepage",
|
|
277
|
+
message: "Homepage template:",
|
|
278
|
+
choices: toChoices(items),
|
|
279
|
+
initial: Math.max(0, items.findIndex((t) => t.slug === defaultSelections.homepage)),
|
|
280
|
+
});
|
|
281
|
+
if (homepage === undefined) process.exit(1);
|
|
282
|
+
|
|
283
|
+
downloadConfig = {
|
|
284
|
+
selections: { ...defaultSelections, homepage },
|
|
285
|
+
theme: DEFAULT_THEME,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Project directory ───────────────────────────────────────────────────
|
|
291
|
+
let projectDir = projectArg;
|
|
292
|
+
let defaultDir = "my-app";
|
|
293
|
+
if (typeof downloadConfig === "string" && projects?.length) {
|
|
294
|
+
const proj = projects.find((p) => p.id === downloadConfig);
|
|
295
|
+
if (proj?.slug) defaultDir = proj.slug;
|
|
296
|
+
}
|
|
297
|
+
if (!projectDir) {
|
|
298
|
+
if (useDefaults) {
|
|
299
|
+
projectDir = "my-app";
|
|
300
|
+
} else {
|
|
301
|
+
const { dir } = await prompts({
|
|
302
|
+
type: "text",
|
|
303
|
+
name: "dir",
|
|
304
|
+
message: "Project directory:",
|
|
305
|
+
initial: defaultDir,
|
|
306
|
+
validate: (v) => (v ? true : "Required"),
|
|
307
|
+
});
|
|
308
|
+
if (!dir) process.exit(1);
|
|
309
|
+
projectDir = dir;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const absoluteDir = join(process.cwd(), projectDir);
|
|
314
|
+
try {
|
|
315
|
+
mkdirSync(absoluteDir, { recursive: true });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
if (err.code === "EEXIST") {
|
|
318
|
+
const count = readdirSync(absoluteDir).length;
|
|
319
|
+
if (count > 0) {
|
|
320
|
+
console.error(chalk.red(`\n Directory ${projectDir} already exists and is not empty.\n`));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
} else throw err;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(chalk.gray("\n Downloading project…\n"));
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await downloadProject(apiKey, downloadConfig, absoluteDir);
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const msg = err.message || "Unknown error";
|
|
332
|
+
console.error(chalk.red(`\n ${msg}\n`));
|
|
333
|
+
const status = err.status || 0;
|
|
334
|
+
if (
|
|
335
|
+
msg.includes("Purchase required") ||
|
|
336
|
+
status === 401 ||
|
|
337
|
+
status === 403
|
|
338
|
+
) {
|
|
339
|
+
console.error(chalk.yellow(" Get access at runwayjs.com/pricing\n"));
|
|
340
|
+
} else if (status >= 500) {
|
|
341
|
+
console.error(chalk.yellow(" Server error. Try again later.\n"));
|
|
342
|
+
}
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(chalk.green(" Done! Created"), chalk.bold(projectDir));
|
|
347
|
+
console.log(chalk.gray("\n Next steps:\n"));
|
|
348
|
+
console.log(chalk.cyan(` cd ${projectDir}`));
|
|
349
|
+
console.log(chalk.cyan(" cp .env.example .env.local # fill in your keys"));
|
|
350
|
+
console.log(chalk.cyan(" pnpm install # or npm install / yarn / bun install"));
|
|
351
|
+
console.log(chalk.cyan(" pnpm db:push # or npm run db:push / yarn db:push / bun run db:push"));
|
|
352
|
+
console.log(chalk.cyan(" pnpm dev # or npm run dev / yarn dev / bun dev"));
|
|
353
|
+
console.log("");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main().catch((err) => {
|
|
357
|
+
console.error(err);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-runway-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a RunwayJS project from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-runway-app": "./index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.3.0",
|
|
14
|
+
"extract-zip": "^2.0.1",
|
|
15
|
+
"minimist": "^1.2.8",
|
|
16
|
+
"prompts": "^2.4.2"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"runwayjs",
|
|
20
|
+
"nextjs",
|
|
21
|
+
"boilerplate",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"create-app"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|