onflyt-cli 0.1.0-beta

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.
@@ -0,0 +1,587 @@
1
+ import React from "react";
2
+ import { Text, Box, useInput } from "ink";
3
+ import { existsSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { GitDetector } from "../lib/git.js";
6
+ import { FrameworkDetector } from "../lib/framework.js";
7
+ import {
8
+ hasProjectConfig,
9
+ saveProjectConfig,
10
+ ProjectConfig,
11
+ } from "../lib/config.js";
12
+ import { initGitRepo } from "../lib/scaffold.js";
13
+ import {
14
+ FRAMEWORKS,
15
+ TEMPLATES,
16
+ getDefaultBuildCommand,
17
+ getDefaultOutputDirectory,
18
+ getDefaultStartCommand,
19
+ getInstallCommand,
20
+ } from "../shared";
21
+ import { Logo } from "../components/Loading.js";
22
+
23
+ interface InitProps {
24
+ name?: string;
25
+ framework?: string;
26
+ template?: string;
27
+ packageManager?: string;
28
+ git?: boolean;
29
+ yes?: boolean;
30
+ }
31
+
32
+ type Step =
33
+ | "name"
34
+ | "setupType"
35
+ | "template"
36
+ | "framework"
37
+ | "packageManager"
38
+ | "git"
39
+ | "gitRemote"
40
+ | "saving"
41
+ | "done"
42
+ | "error";
43
+
44
+ const FRAMEWORK_LIST = Object.entries(FRAMEWORKS).map(([id, config]) => ({
45
+ id,
46
+ name: config.label,
47
+ }));
48
+
49
+ const PACKAGE_MANAGERS = [
50
+ { id: "npm", name: "npm" },
51
+ { id: "bun", name: "Bun" },
52
+ { id: "yarn", name: "Yarn" },
53
+ { id: "pnpm", name: "pnpm" },
54
+ { id: "pip", name: "pip" },
55
+ { id: "poetry", name: "Poetry" },
56
+ ];
57
+
58
+ const FOOTER = "↑↓ select/navigate • Enter confirm • Esc back • Ctrl+C cancel";
59
+
60
+ const Init: React.FC<InitProps> = ({
61
+ name,
62
+ framework,
63
+ template,
64
+ packageManager,
65
+ git,
66
+ yes,
67
+ }) => {
68
+ const [step, setStep] = React.useState<Step>("name");
69
+ const [history, setHistory] = React.useState<Step[]>(["name"]);
70
+ const [projectName, setProjectName] = React.useState("");
71
+ const [isEditingName, setIsEditingName] = React.useState(false);
72
+ const [useTemplate, setUseTemplate] = React.useState(0);
73
+ const [selectedTemplate, setSelectedTemplate] = React.useState(0);
74
+ const [selectedFramework, setSelectedFramework] = React.useState(0);
75
+ const [selectedPackageManager, setSelectedPackageManager] = React.useState(0);
76
+ const [connectGit, setConnectGit] = React.useState(true);
77
+ const [hasGit, setHasGit] = React.useState(false);
78
+ const [detectedName, setDetectedName] = React.useState("");
79
+ const [detectedFrameworkId, setDetectedFrameworkId] = React.useState("");
80
+ const [gitUrl, setGitUrl] = React.useState<string | null>(null);
81
+ const [gitBranch, setGitBranch] = React.useState("main");
82
+ const [remoteUrl, setRemoteUrl] = React.useState("");
83
+ const [errorMsg, setErrorMsg] = React.useState("");
84
+ const [savedConfig, setSavedConfig] = React.useState<ProjectConfig | null>(
85
+ null,
86
+ );
87
+
88
+ const goTo = (nextStep: Step) => {
89
+ setHistory([...history, nextStep]);
90
+ setStep(nextStep);
91
+ };
92
+
93
+ const goBack = () => {
94
+ if (history.length > 1) {
95
+ const newHistory = history.slice(0, -1);
96
+ setHistory(newHistory);
97
+ setStep(newHistory[newHistory.length - 1]);
98
+ }
99
+ };
100
+
101
+ React.useEffect(() => {
102
+ detect();
103
+ }, []);
104
+
105
+ React.useEffect(() => {
106
+ if (step === "done" || step === "error") {
107
+ const timer = setTimeout(() => {
108
+ process.exit(step === "error" ? 1 : 0);
109
+ }, 100);
110
+ return () => clearTimeout(timer);
111
+ }
112
+ }, [step]);
113
+
114
+ const detect = async () => {
115
+ const cwd = process.cwd();
116
+
117
+ if (hasProjectConfig(cwd)) {
118
+ setErrorMsg(
119
+ "Already initialized. Run 'onflyt deploy' to deploy, or delete onflyt.json to reinitialize.",
120
+ );
121
+ setStep("error");
122
+ return;
123
+ }
124
+
125
+ const gitDetector = new GitDetector(cwd);
126
+ const gitInfo = await gitDetector.detect();
127
+ const detectedHasGit = gitInfo.isGitRepo && gitInfo.remotes.length > 0;
128
+
129
+ const frameworkDetector = new FrameworkDetector(cwd);
130
+ const detectedFw = frameworkDetector.detect();
131
+ const fwId = detectedFw?.id || "node";
132
+
133
+ let detected =
134
+ cwd
135
+ .split("/")
136
+ .pop()
137
+ ?.replace(/[^a-z0-9-]/g, "-")
138
+ .toLowerCase() || "my-project";
139
+ if (detectedHasGit && gitInfo.remotes[0]) {
140
+ const match = gitInfo.remotes[0].url.match(/\/([^\/]+?)(?:\.git)?$/);
141
+ if (match) {
142
+ detected = match[1].replace(/[^a-z0-9-]/g, "-").toLowerCase();
143
+ }
144
+ }
145
+
146
+ const fwIndex = FRAMEWORK_LIST.findIndex(
147
+ (f) => f.id === (framework || fwId),
148
+ );
149
+ const pmIndex = PACKAGE_MANAGERS.findIndex((p) => p.id === packageManager);
150
+
151
+ setDetectedName(detected);
152
+ setDetectedFrameworkId(fwId);
153
+ setProjectName(name || detected);
154
+ setConnectGit(
155
+ git !== undefined ? git : detectedHasGit && !!gitInfo.remotes[0],
156
+ );
157
+ setSelectedFramework(fwIndex >= 0 ? fwIndex : 0);
158
+ setSelectedPackageManager(pmIndex >= 0 ? pmIndex : 0);
159
+ setHasGit(detectedHasGit);
160
+
161
+ if (gitInfo.remotes[0]) {
162
+ setGitUrl(gitInfo.remotes[0].url);
163
+ setGitBranch(gitInfo.currentBranch || "main");
164
+ }
165
+
166
+ if (yes) {
167
+ autoSave();
168
+ }
169
+ };
170
+
171
+ const getFinalProjectName = () => {
172
+ return projectName || detectedName;
173
+ };
174
+
175
+ const cancel = () => {
176
+ console.log("\nCancelled.");
177
+ process.exit(0);
178
+ };
179
+
180
+ const autoSave = async () => {
181
+ setStep("saving");
182
+
183
+ let fwId: string;
184
+ if (useTemplate === 1) {
185
+ fwId = TEMPLATES[selectedTemplate].framework;
186
+ } else {
187
+ fwId = FRAMEWORK_LIST[selectedFramework].id;
188
+ }
189
+
190
+ const pmId = PACKAGE_MANAGERS[selectedPackageManager].id;
191
+ const cwd = process.cwd();
192
+
193
+ const frameworkDetector = new FrameworkDetector(cwd);
194
+ const detectedOutputDir = frameworkDetector.detectOutputDirectory(fwId);
195
+
196
+ const buildCmd = getDefaultBuildCommand(fwId) || "";
197
+ const installCmd = getInstallCommand(fwId, pmId);
198
+ const outputDir =
199
+ detectedOutputDir || getDefaultOutputDirectory(fwId) || ".";
200
+ const startCmd = getDefaultStartCommand(fwId) || "";
201
+
202
+ if (connectGit && !hasGit) {
203
+ initGitRepo(cwd, remoteUrl || undefined);
204
+ createGitignore(cwd);
205
+ }
206
+
207
+ const projectConfig: ProjectConfig = {
208
+ name: getFinalProjectName(),
209
+ framework: fwId,
210
+ buildCommand: buildCmd,
211
+ outputDirectory: outputDir,
212
+ installCommand: installCmd,
213
+ startCommand: startCmd,
214
+ gitRepoUrl: connectGit ? remoteUrl || gitUrl || undefined : undefined,
215
+ gitBranch: connectGit ? "main" : undefined,
216
+ };
217
+
218
+ saveProjectConfig(projectConfig);
219
+ setSavedConfig(projectConfig);
220
+ setStep("done");
221
+ };
222
+
223
+ const createGitignore = (cwd: string) => {
224
+ const gitignorePath = join(cwd, ".gitignore");
225
+ if (!existsSync(gitignorePath)) {
226
+ const defaultGitignore = `node_modules/
227
+ dist/
228
+ build/
229
+ .next/
230
+ .output/
231
+ .env
232
+ .env.local
233
+ *.log
234
+ .DS_Store
235
+ `;
236
+ writeFileSync(gitignorePath, defaultGitignore);
237
+ }
238
+ };
239
+
240
+ useInput((input, key) => {
241
+ if (key.ctrl && input === "c") {
242
+ cancel();
243
+ return;
244
+ }
245
+
246
+ switch (step) {
247
+ case "name":
248
+ if (key.return) {
249
+ goTo("setupType");
250
+ } else if (key.escape) {
251
+ setIsEditingName(false);
252
+ setProjectName(detectedName);
253
+ } else if (key.backspace || key.delete) {
254
+ setProjectName(projectName.slice(0, -1));
255
+ } else if (input && input.match(/^[a-zA-Z0-9-_]$/)) {
256
+ if (projectName === detectedName && !isEditingName) {
257
+ setProjectName(input);
258
+ } else {
259
+ setProjectName(projectName + input);
260
+ }
261
+ setIsEditingName(true);
262
+ }
263
+ break;
264
+
265
+ case "setupType":
266
+ if (key.downArrow || input === "j") {
267
+ setUseTemplate(1);
268
+ } else if (key.upArrow || input === "k") {
269
+ setUseTemplate(0);
270
+ } else if (key.return) {
271
+ if (useTemplate === 0) {
272
+ goTo("framework");
273
+ } else {
274
+ goTo("template");
275
+ }
276
+ } else if (key.escape) {
277
+ goBack();
278
+ }
279
+ break;
280
+
281
+ case "template":
282
+ if (key.downArrow || input === "j") {
283
+ setSelectedTemplate((prev) =>
284
+ Math.min(prev + 1, TEMPLATES.length - 1),
285
+ );
286
+ } else if (key.upArrow || input === "k") {
287
+ setSelectedTemplate((prev) => Math.max(prev - 1, 0));
288
+ } else if (key.return) {
289
+ goTo("git");
290
+ } else if (key.escape) {
291
+ goBack();
292
+ }
293
+ break;
294
+
295
+ case "framework":
296
+ if (key.downArrow || input === "j") {
297
+ setSelectedFramework((prev) =>
298
+ Math.min(prev + 1, FRAMEWORK_LIST.length - 1),
299
+ );
300
+ } else if (key.upArrow || input === "k") {
301
+ setSelectedFramework((prev) => Math.max(prev - 1, 0));
302
+ } else if (key.return) {
303
+ goTo("packageManager");
304
+ } else if (key.escape) {
305
+ goBack();
306
+ }
307
+ break;
308
+
309
+ case "packageManager":
310
+ if (key.downArrow || input === "j") {
311
+ setSelectedPackageManager((prev) =>
312
+ Math.min(prev + 1, PACKAGE_MANAGERS.length - 1),
313
+ );
314
+ } else if (key.upArrow || input === "k") {
315
+ setSelectedPackageManager((prev) => Math.max(prev - 1, 0));
316
+ } else if (key.return) {
317
+ goTo("git");
318
+ } else if (key.escape) {
319
+ goBack();
320
+ }
321
+ break;
322
+
323
+ case "git":
324
+ if (key.downArrow || input === "j" || input === " ") {
325
+ setConnectGit((prev) => !prev);
326
+ } else if (key.upArrow || input === "k") {
327
+ setConnectGit((prev) => !prev);
328
+ } else if (key.return) {
329
+ if (connectGit) {
330
+ if (hasGit) {
331
+ autoSave();
332
+ } else {
333
+ goTo("gitRemote");
334
+ }
335
+ } else {
336
+ autoSave();
337
+ }
338
+ } else if (key.escape) {
339
+ goBack();
340
+ }
341
+ break;
342
+
343
+ case "gitRemote":
344
+ if (key.return) {
345
+ autoSave();
346
+ } else if (key.escape) {
347
+ goBack();
348
+ } else if (key.backspace || key.delete) {
349
+ setRemoteUrl(remoteUrl.slice(0, -1));
350
+ } else if (input && input.length > 0) {
351
+ setRemoteUrl(remoteUrl + input);
352
+ }
353
+ break;
354
+ }
355
+ });
356
+
357
+ if (step === "error") {
358
+ return (
359
+ <Box flexDirection="column" padding={1}>
360
+ <Text bold color="red">
361
+ Error
362
+ </Text>
363
+ <Box marginTop={1}>
364
+ <Text color="red">{errorMsg}</Text>
365
+ </Box>
366
+ </Box>
367
+ );
368
+ }
369
+
370
+ if (step === "saving") {
371
+ return (
372
+ <Box flexDirection="column" padding={1}>
373
+ <Logo />
374
+ <Box marginTop={1}>
375
+ <Text>Saving configuration...</Text>
376
+ </Box>
377
+ </Box>
378
+ );
379
+ }
380
+
381
+ if (step === "done") {
382
+ return (
383
+ <Box flexDirection="column" padding={1}>
384
+ <Text bold color="green">
385
+ ✓ Project initialized!
386
+ </Text>
387
+ <Box marginTop={1}>
388
+ <Text>onflyt.json saved to current directory</Text>
389
+ </Box>
390
+ {savedConfig && (
391
+ <>
392
+ <Box marginTop={1}>
393
+ <Text dimColor>Project: {savedConfig.name}</Text>
394
+ </Box>
395
+ <Box>
396
+ <Text dimColor>
397
+ Framework: {FRAMEWORKS[savedConfig.framework]?.label}
398
+ </Text>
399
+ </Box>
400
+ {savedConfig.gitRepoUrl && (
401
+ <Box>
402
+ <Text dimColor>Git: {savedConfig.gitRepoUrl}</Text>
403
+ </Box>
404
+ )}
405
+ </>
406
+ )}
407
+ <Box marginTop={2}>
408
+ <Text bold>Next steps:</Text>
409
+ </Box>
410
+ <Box marginTop={1}>
411
+ <Text dimColor>1. Run 'onflyt deploy' to deploy</Text>
412
+ </Box>
413
+ <Box>
414
+ <Text dimColor>2. Team selection will happen during deploy</Text>
415
+ </Box>
416
+ </Box>
417
+ );
418
+ }
419
+
420
+ return (
421
+ <Box flexDirection="column" padding={1}>
422
+ <Logo />
423
+
424
+ {step === "name" && (
425
+ <>
426
+ <Box marginTop={1}>
427
+ <Text>Project Name</Text>
428
+ </Box>
429
+ <Box marginTop={1}>
430
+ <Text dimColor>Press keys to edit, Enter to continue</Text>
431
+ </Box>
432
+ <Box marginTop={1}>
433
+ <Text bold color="cyan">
434
+ &gt;{" "}
435
+ </Text>
436
+ <Text>{projectName || detectedName}</Text>
437
+ <Text bold color="cyan">
438
+ _
439
+ </Text>
440
+ </Box>
441
+ </>
442
+ )}
443
+
444
+ {step === "setupType" && (
445
+ <>
446
+ <Box marginTop={1}>
447
+ <Text>Setup Type</Text>
448
+ </Box>
449
+ <Box marginTop={1} flexDirection="column">
450
+ <Box>
451
+ <Text>
452
+ {useTemplate === 0 ? "❯ " : " "}
453
+ Use existing files
454
+ </Text>
455
+ </Box>
456
+ <Box>
457
+ <Text>
458
+ {useTemplate === 1 ? "❯ " : " "}
459
+ Use a template (scaffold from starter)
460
+ </Text>
461
+ </Box>
462
+ </Box>
463
+ <Box marginTop={1}>
464
+ <Text dimColor>{FOOTER}</Text>
465
+ </Box>
466
+ </>
467
+ )}
468
+
469
+ {step === "template" && (
470
+ <>
471
+ <Box marginTop={1}>
472
+ <Text>Select Template</Text>
473
+ </Box>
474
+ <Box marginTop={1} flexDirection="column">
475
+ {TEMPLATES.map((t, i) => (
476
+ <Box key={t.id}>
477
+ <Text>
478
+ {i === selectedTemplate ? "❯ " : " "}
479
+ {t.name} - {t.description}
480
+ </Text>
481
+ </Box>
482
+ ))}
483
+ </Box>
484
+ <Box marginTop={1}>
485
+ <Text dimColor>{FOOTER}</Text>
486
+ </Box>
487
+ </>
488
+ )}
489
+
490
+ {step === "framework" && (
491
+ <>
492
+ <Box marginTop={1}>
493
+ <Text>Select Framework</Text>
494
+ </Box>
495
+ <Box marginTop={1}>
496
+ <Text dimColor>Detected: {detectedFrameworkId}</Text>
497
+ </Box>
498
+ <Box marginTop={1} flexDirection="column">
499
+ {FRAMEWORK_LIST.map((fw, i) => (
500
+ <Box key={fw.id}>
501
+ <Text>
502
+ {i === selectedFramework ? "❯ " : " "}
503
+ {fw.name}
504
+ </Text>
505
+ </Box>
506
+ ))}
507
+ </Box>
508
+ <Box marginTop={1}>
509
+ <Text dimColor>{FOOTER}</Text>
510
+ </Box>
511
+ </>
512
+ )}
513
+
514
+ {step === "packageManager" && (
515
+ <>
516
+ <Box marginTop={1}>
517
+ <Text>Select Package Manager</Text>
518
+ </Box>
519
+ <Box marginTop={1} flexDirection="column">
520
+ {PACKAGE_MANAGERS.map((pm, i) => (
521
+ <Box key={pm.id}>
522
+ <Text>
523
+ {i === selectedPackageManager ? "❯ " : " "}
524
+ {pm.name}
525
+ </Text>
526
+ </Box>
527
+ ))}
528
+ </Box>
529
+ <Box marginTop={1}>
530
+ <Text dimColor>{FOOTER}</Text>
531
+ </Box>
532
+ </>
533
+ )}
534
+
535
+ {step === "git" && (
536
+ <>
537
+ <Box marginTop={1}>
538
+ <Text>Git Repository</Text>
539
+ </Box>
540
+ {gitUrl && (
541
+ <Box marginTop={1}>
542
+ <Text dimColor>
543
+ Detected: {gitUrl} ({gitBranch})
544
+ </Text>
545
+ </Box>
546
+ )}
547
+ <Box marginTop={1} flexDirection="column">
548
+ <Box>
549
+ <Text>
550
+ {connectGit ? "❯ " : " "}
551
+ {hasGit ? "Yes, connect git repo" : "Initialize git repo"}
552
+ </Text>
553
+ </Box>
554
+ <Box>
555
+ <Text>{!connectGit ? "❯ " : " "}Skip git (manual deploys)</Text>
556
+ </Box>
557
+ </Box>
558
+ <Box marginTop={1}>
559
+ <Text dimColor>{FOOTER}</Text>
560
+ </Box>
561
+ </>
562
+ )}
563
+
564
+ {step === "gitRemote" && (
565
+ <>
566
+ <Box marginTop={1}>
567
+ <Text>Git Remote URL (optional)</Text>
568
+ </Box>
569
+ <Box marginTop={1}>
570
+ <Text dimColor>Press Enter to skip, or enter GitHub repo URL</Text>
571
+ </Box>
572
+ <Box marginTop={1}>
573
+ <Text>&gt; {remoteUrl}</Text>
574
+ <Text bold>_</Text>
575
+ </Box>
576
+ <Box marginTop={1}>
577
+ <Text dimColor>
578
+ Enter paste URL • Backspace delete • Enter continue
579
+ </Text>
580
+ </Box>
581
+ </>
582
+ )}
583
+ </Box>
584
+ );
585
+ };
586
+
587
+ export default Init;