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,225 @@
1
+ import { execSync } from "child_process";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ writeFileSync,
6
+ readFileSync,
7
+ cpSync,
8
+ rmSync,
9
+ } from "fs";
10
+ import { join } from "path";
11
+ import { createHash } from "crypto";
12
+
13
+ export interface ScaffoldProgress {
14
+ stage: "cloning" | "installing" | "cleaning" | "done";
15
+ message: string;
16
+ progress?: number;
17
+ }
18
+
19
+ export class Scaffold {
20
+ private cwd: string;
21
+ private onProgress?: (progress: ScaffoldProgress) => void;
22
+ private tempDir: string;
23
+
24
+ constructor(cwd: string, onProgress?: (p: ScaffoldProgress) => void) {
25
+ this.cwd = cwd;
26
+ this.onProgress = onProgress;
27
+ this.tempDir = join("/tmp", `onflyt-scaffold-${Date.now()}`);
28
+ }
29
+
30
+ report(progress: ScaffoldProgress) {
31
+ if (this.onProgress) {
32
+ this.onProgress(progress);
33
+ }
34
+ }
35
+
36
+ async cloneTemplate(repoUrl: string, branch: string = "main"): Promise<void> {
37
+ this.report({
38
+ stage: "cloning",
39
+ message: "Cloning template...",
40
+ progress: 0,
41
+ });
42
+
43
+ try {
44
+ mkdirSync(this.tempDir, { recursive: true });
45
+
46
+ const gitUrl = repoUrl.endsWith(".git") ? repoUrl : `${repoUrl}.git`;
47
+
48
+ execSync(
49
+ `git clone --depth 1 --branch ${branch} ${gitUrl} "${this.tempDir}"`,
50
+ { stdio: "pipe" },
51
+ );
52
+
53
+ this.report({
54
+ stage: "cloning",
55
+ message: "Template cloned",
56
+ progress: 100,
57
+ });
58
+ } catch (error: any) {
59
+ this.cleanup();
60
+ throw new Error(`Failed to clone template: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ async installDependencies(packageManager: string): Promise<void> {
65
+ if (!packageManager || packageManager === "none") {
66
+ return;
67
+ }
68
+
69
+ this.report({
70
+ stage: "installing",
71
+ message: `Installing dependencies with ${packageManager}...`,
72
+ progress: 0,
73
+ });
74
+
75
+ try {
76
+ let installCmd = "";
77
+ switch (packageManager) {
78
+ case "npm":
79
+ installCmd = "npm install";
80
+ break;
81
+ case "yarn":
82
+ installCmd = "yarn install";
83
+ break;
84
+ case "pnpm":
85
+ installCmd = "pnpm install";
86
+ break;
87
+ case "pip":
88
+ installCmd = "pip install -r requirements.txt";
89
+ break;
90
+ case "poetry":
91
+ installCmd = "poetry install";
92
+ break;
93
+ default:
94
+ return;
95
+ }
96
+
97
+ execSync(installCmd, {
98
+ cwd: this.tempDir,
99
+ stdio: "inherit",
100
+ });
101
+
102
+ this.report({
103
+ stage: "installing",
104
+ message: "Dependencies installed",
105
+ progress: 100,
106
+ });
107
+ } catch (error: any) {
108
+ console.error(
109
+ `Warning: Failed to install dependencies: ${error.message}`,
110
+ );
111
+ console.error("You may need to run the install command manually.");
112
+ }
113
+ }
114
+
115
+ async moveFilesToProject(): Promise<void> {
116
+ this.report({
117
+ stage: "cleaning",
118
+ message: "Preparing project files...",
119
+ progress: 50,
120
+ });
121
+
122
+ try {
123
+ const items = [
124
+ "package.json",
125
+ "requirements.txt",
126
+ "src",
127
+ "app",
128
+ "lib",
129
+ "components",
130
+ "pages",
131
+ "routes",
132
+ "api",
133
+ "main.py",
134
+ "index.js",
135
+ "server.js",
136
+ "app.js",
137
+ "main.go",
138
+ "Cargo.toml",
139
+ "go.mod",
140
+ "README.md",
141
+ "next.config.js",
142
+ "next.config.mjs",
143
+ "vite.config.ts",
144
+ "vite.config.js",
145
+ "nuxt.config.ts",
146
+ "pyproject.toml",
147
+ ];
148
+
149
+ for (const item of items) {
150
+ const srcPath = join(this.tempDir, item);
151
+ const destPath = join(this.cwd, item);
152
+ if (existsSync(srcPath) && !existsSync(destPath)) {
153
+ cpSync(srcPath, destPath, { recursive: true });
154
+ }
155
+ }
156
+
157
+ this.report({ stage: "cleaning", message: "Files copied", progress: 90 });
158
+ } catch (error: any) {
159
+ console.error(`Warning: Failed to copy some files: ${error.message}`);
160
+ }
161
+ }
162
+
163
+ cleanup() {
164
+ try {
165
+ if (existsSync(this.tempDir)) {
166
+ rmSync(this.tempDir, { recursive: true, force: true });
167
+ }
168
+ } catch {
169
+ // Ignore cleanup errors
170
+ }
171
+ }
172
+
173
+ async scaffold(repoUrl: string, packageManager: string): Promise<void> {
174
+ try {
175
+ await this.cloneTemplate(repoUrl);
176
+ await this.installDependencies(packageManager);
177
+ await this.moveFilesToProject();
178
+ this.cleanup();
179
+ this.report({
180
+ stage: "done",
181
+ message: "Project scaffolded",
182
+ progress: 100,
183
+ });
184
+ } catch (error) {
185
+ this.cleanup();
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+
191
+ export function isGitInstalled(): boolean {
192
+ try {
193
+ execSync("git --version", { stdio: "ignore" });
194
+ return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ export function isNodeInstalled(): boolean {
201
+ try {
202
+ execSync("node --version", { stdio: "ignore" });
203
+ return true;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ export function initGitRepo(cwd: string, remoteUrl?: string): void {
210
+ try {
211
+ execSync("git init", { cwd, stdio: "pipe" });
212
+
213
+ if (remoteUrl) {
214
+ execSync(`git remote add origin ${remoteUrl}`, { cwd, stdio: "pipe" });
215
+ }
216
+ } catch (error: any) {
217
+ throw new Error(`Failed to init git: ${error.message}`);
218
+ }
219
+ }
220
+
221
+ export function generateProjectId(): string {
222
+ const timestamp = Date.now().toString(36);
223
+ const random = Math.random().toString(36).substring(2, 8);
224
+ return `${timestamp}-${random}`;
225
+ }
package/src/shared.ts ADDED
@@ -0,0 +1,353 @@
1
+ export type PodTier = "micro" | "lite" | "standard" | "pro" | "business";
2
+
3
+ export const TIER_HOURLY_PRICE: Record<PodTier, number> = {
4
+ micro: 0,
5
+ lite: 0.015,
6
+ standard: 0.05,
7
+ pro: 0.08,
8
+ business: 0.12,
9
+ };
10
+
11
+ export type SpendTier = "free" | "low" | "medium" | "high";
12
+
13
+ export interface TeamLimits {
14
+ maxProjects: number;
15
+ maxInstancesPerProject: number;
16
+ tier: SpendTier;
17
+ tierLabel: string;
18
+ }
19
+
20
+ export const SPEND_THRESHOLDS = {
21
+ free: 0,
22
+ low: 10,
23
+ medium: 50,
24
+ high: 200,
25
+ } as const;
26
+
27
+ export const TIER_LIMITS: Record<SpendTier, TeamLimits> = {
28
+ free: {
29
+ maxProjects: 3,
30
+ maxInstancesPerProject: 1,
31
+ tier: "free",
32
+ tierLabel: "Free Usage",
33
+ },
34
+ low: {
35
+ maxProjects: 10,
36
+ maxInstancesPerProject: 2,
37
+ tier: "low",
38
+ tierLabel: "Low Spend",
39
+ },
40
+ medium: {
41
+ maxProjects: Infinity,
42
+ maxInstancesPerProject: 4,
43
+ tier: "medium",
44
+ tierLabel: "Medium Spend",
45
+ },
46
+ high: {
47
+ maxProjects: Infinity,
48
+ maxInstancesPerProject: 10,
49
+ tier: "high",
50
+ tierLabel: "High Spend",
51
+ },
52
+ };
53
+
54
+ export function getLimitsBySpend(totalSpend: number): TeamLimits {
55
+ if (totalSpend >= SPEND_THRESHOLDS.high) return TIER_LIMITS.high;
56
+ if (totalSpend >= SPEND_THRESHOLDS.medium) return TIER_LIMITS.medium;
57
+ if (totalSpend >= SPEND_THRESHOLDS.low) return TIER_LIMITS.low;
58
+ return TIER_LIMITS.free;
59
+ }
60
+
61
+ export function getTierLabel(totalSpend: number): string {
62
+ return getLimitsBySpend(totalSpend).tierLabel;
63
+ }
64
+
65
+ export type FrameworkFamily = "node" | "nixpacks";
66
+ export type DeploymentType = "static" | "container";
67
+
68
+ export interface FrameworkDefaults {
69
+ buildCommand?: string;
70
+ installCommand?: string;
71
+ outputDirectory?: string;
72
+ startCommand?: string;
73
+ }
74
+
75
+ export interface FrameworkConfig {
76
+ label: string;
77
+ family: FrameworkFamily;
78
+ deploymentType: DeploymentType;
79
+ defaults: FrameworkDefaults;
80
+ isServer: boolean;
81
+ }
82
+
83
+ export const FRAMEWORKS: Record<string, FrameworkConfig> = {
84
+ nextjs: {
85
+ label: "Next.js",
86
+ family: "node",
87
+ deploymentType: "container",
88
+ defaults: {
89
+ buildCommand: "next build",
90
+ installCommand: "bun install",
91
+ outputDirectory: ".next",
92
+ },
93
+ isServer: true,
94
+ },
95
+ remix: {
96
+ label: "Remix",
97
+ family: "node",
98
+ deploymentType: "container",
99
+ defaults: {
100
+ buildCommand: "npm run build",
101
+ installCommand: "bun install",
102
+ outputDirectory: "build",
103
+ },
104
+ isServer: true,
105
+ },
106
+ react: {
107
+ label: "React",
108
+ family: "node",
109
+ deploymentType: "static",
110
+ defaults: {
111
+ buildCommand: "npm run build",
112
+ installCommand: "bun install",
113
+ outputDirectory: "build",
114
+ },
115
+ isServer: false,
116
+ },
117
+ vue: {
118
+ label: "Vue.js",
119
+ family: "node",
120
+ deploymentType: "static",
121
+ defaults: {
122
+ buildCommand: "npm run build",
123
+ installCommand: "bun install",
124
+ outputDirectory: "dist",
125
+ },
126
+ isServer: false,
127
+ },
128
+ nuxt: {
129
+ label: "Nuxt",
130
+ family: "node",
131
+ deploymentType: "container",
132
+ defaults: {
133
+ buildCommand: "npm run build",
134
+ installCommand: "bun install",
135
+ outputDirectory: ".output",
136
+ },
137
+ isServer: true,
138
+ },
139
+ svelte: {
140
+ label: "Svelte",
141
+ family: "node",
142
+ deploymentType: "static",
143
+ defaults: {
144
+ buildCommand: "npm run build",
145
+ installCommand: "bun install",
146
+ outputDirectory: "build",
147
+ },
148
+ isServer: false,
149
+ },
150
+ vite: {
151
+ label: "Vite",
152
+ family: "node",
153
+ deploymentType: "static",
154
+ defaults: {
155
+ buildCommand: "npm run build",
156
+ installCommand: "bun install",
157
+ outputDirectory: "dist",
158
+ },
159
+ isServer: false,
160
+ },
161
+ node: {
162
+ label: "Node.js",
163
+ family: "node",
164
+ deploymentType: "container",
165
+ defaults: {
166
+ buildCommand: "npm run build",
167
+ installCommand: "bun install",
168
+ outputDirectory: "dist",
169
+ startCommand: "node dist/index.js",
170
+ },
171
+ isServer: true,
172
+ },
173
+ express: {
174
+ label: "Express",
175
+ family: "node",
176
+ deploymentType: "container",
177
+ defaults: {
178
+ buildCommand: "npm run build",
179
+ installCommand: "bun install",
180
+ outputDirectory: "dist",
181
+ startCommand: "node dist/index.js",
182
+ },
183
+ isServer: true,
184
+ },
185
+ hono: {
186
+ label: "Hono",
187
+ family: "node",
188
+ deploymentType: "container",
189
+ defaults: {
190
+ buildCommand: "npm run build",
191
+ installCommand: "bun install",
192
+ outputDirectory: "dist",
193
+ startCommand: "node dist/index.js",
194
+ },
195
+ isServer: true,
196
+ },
197
+ nestjs: {
198
+ label: "NestJS",
199
+ family: "node",
200
+ deploymentType: "container",
201
+ defaults: {
202
+ buildCommand: "npm run build",
203
+ installCommand: "bun install",
204
+ outputDirectory: "dist",
205
+ startCommand: "node dist/main.js",
206
+ },
207
+ isServer: true,
208
+ },
209
+ astro: {
210
+ label: "Astro",
211
+ family: "node",
212
+ deploymentType: "static",
213
+ defaults: {
214
+ buildCommand: "npm run build",
215
+ installCommand: "bun install",
216
+ outputDirectory: "dist",
217
+ },
218
+ isServer: false,
219
+ },
220
+ static: {
221
+ label: "Static Site",
222
+ family: "node",
223
+ deploymentType: "static",
224
+ defaults: { buildCommand: "", installCommand: "", outputDirectory: "." },
225
+ isServer: false,
226
+ },
227
+ fastapi: {
228
+ label: "FastAPI",
229
+ family: "nixpacks",
230
+ deploymentType: "container",
231
+ defaults: {
232
+ buildCommand: "uv pip install --python 3.11 -r requirements.txt",
233
+ startCommand: "uvicorn main:app --host 0.0.0.0 --port 8080",
234
+ },
235
+ isServer: true,
236
+ },
237
+ flask: {
238
+ label: "Flask",
239
+ family: "nixpacks",
240
+ deploymentType: "container",
241
+ defaults: {
242
+ buildCommand: "uv pip install --python 3.11 -r requirements.txt",
243
+ startCommand: "flask run --host 0.0.0.0 --port 8080",
244
+ },
245
+ isServer: true,
246
+ },
247
+ django: {
248
+ label: "Django",
249
+ family: "nixpacks",
250
+ deploymentType: "container",
251
+ defaults: {
252
+ buildCommand: "uv pip install --python 3.11 -r requirements.txt",
253
+ startCommand: "python3 manage.py runserver 0.0.0.0:8080",
254
+ },
255
+ isServer: true,
256
+ },
257
+ } as const;
258
+
259
+ export function getFramework(id: string): FrameworkConfig | undefined {
260
+ return FRAMEWORKS[id.toLowerCase()];
261
+ }
262
+
263
+ export function getDefaultBuildCommand(
264
+ frameworkId: string,
265
+ ): string | undefined {
266
+ return FRAMEWORKS[frameworkId.toLowerCase()]?.defaults.buildCommand;
267
+ }
268
+
269
+ export function getDefaultOutputDirectory(
270
+ frameworkId: string,
271
+ ): string | undefined {
272
+ return FRAMEWORKS[frameworkId.toLowerCase()]?.defaults.outputDirectory;
273
+ }
274
+
275
+ export function getDefaultStartCommand(
276
+ frameworkId: string,
277
+ ): string | undefined {
278
+ return FRAMEWORKS[frameworkId.toLowerCase()]?.defaults.startCommand;
279
+ }
280
+
281
+ export function getInstallCommand(
282
+ frameworkId: string,
283
+ packageManager: string,
284
+ ): string {
285
+ const pm = packageManager.toLowerCase();
286
+ const framework = FRAMEWORKS[frameworkId.toLowerCase()];
287
+ if (pm === "pip" || pm === "poetry") {
288
+ if (pm === "poetry") return "poetry install";
289
+ return (
290
+ framework?.defaults.installCommand || "pip install -r requirements.txt"
291
+ );
292
+ }
293
+ const installCommands: Record<string, string> = {
294
+ npm: "npm install",
295
+ bun: "bun install",
296
+ yarn: "yarn install",
297
+ pnpm: "pnpm install",
298
+ };
299
+ return installCommands[pm] || `${pm} install`;
300
+ }
301
+
302
+ export interface Template {
303
+ id: string;
304
+ name: string;
305
+ description: string;
306
+ repoUrl: string;
307
+ framework: string;
308
+ }
309
+
310
+ export const TEMPLATES: Template[] = [
311
+ {
312
+ id: "blank",
313
+ name: "Blank",
314
+ description: "Minimal project setup",
315
+ repoUrl: "https://github.com/onflyt/blank",
316
+ framework: "static",
317
+ },
318
+ {
319
+ id: "nextjs",
320
+ name: "Next.js Starter",
321
+ description: "Next.js with App Router",
322
+ repoUrl: "https://github.com/onflyt/nextjs-starter",
323
+ framework: "nextjs",
324
+ },
325
+ {
326
+ id: "react-vite",
327
+ name: "React + Vite",
328
+ description: "React with Vite bundler",
329
+ repoUrl: "https://github.com/onflyt/react-vite",
330
+ framework: "react",
331
+ },
332
+ {
333
+ id: "node-api",
334
+ name: "Node.js API",
335
+ description: "Express/Fastify REST API",
336
+ repoUrl: "https://github.com/onflyt/node-api",
337
+ framework: "node",
338
+ },
339
+ {
340
+ id: "fastapi",
341
+ name: "FastAPI",
342
+ description: "Python FastAPI backend",
343
+ repoUrl: "https://github.com/onflyt/fastapi-starter",
344
+ framework: "fastapi",
345
+ },
346
+ {
347
+ id: "ai-agent",
348
+ name: "AI Agent",
349
+ description: "AI Agent starter with OpenAI",
350
+ repoUrl: "https://github.com/onflyt/ai-agent",
351
+ framework: "python",
352
+ },
353
+ ];
package/src/types.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module "*.jsx" {
2
+ import React from "react";
3
+ const Component: React.FC;
4
+ export default Component;
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "types": ["node"],
14
+ "allowJs": true
15
+ },
16
+ "include": ["src/**/*"]
17
+ }