@nimbuslab/cli 0.16.3 → 0.16.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.
- package/README.md +41 -75
- package/dist/index.js +72 -10
- package/package.json +12 -2
- package/.github/workflows/publish.yml +0 -67
- package/CLAUDE.md +0 -106
- package/MIGRATION-ROADMAP.md +0 -201
- package/bun.lock +0 -36
- package/docs/CI-CD.md +0 -181
- package/docs/analyze.md +0 -148
- package/docs/create.md +0 -219
- package/docs/migrate.md +0 -177
- package/docs/package.md +0 -229
- package/docs/upgrade.md +0 -152
- package/src/commands/analyze.ts +0 -210
- package/src/commands/create.ts +0 -1323
- package/src/commands/lola.ts +0 -1026
- package/src/commands/update.ts +0 -267
- package/src/commands/upgrade.ts +0 -251
- package/src/index.ts +0 -161
- package/tsconfig.json +0 -29
package/src/commands/create.ts
DELETED
|
@@ -1,1323 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts"
|
|
2
|
-
import pc from "picocolors"
|
|
3
|
-
import { $ } from "bun"
|
|
4
|
-
import { rm, mkdir } from "node:fs/promises"
|
|
5
|
-
import { join } from "node:path"
|
|
6
|
-
|
|
7
|
-
// AI Assistant configs
|
|
8
|
-
const AI_CONFIGS: Record<string, { filename: string; content: (type: string) => string }> = {
|
|
9
|
-
claude: {
|
|
10
|
-
filename: "CLAUDE.md",
|
|
11
|
-
content: (type) => `# ${type === "landing" ? "Landing Page" : type === "app" ? "Web App" : "Monorepo"}
|
|
12
|
-
|
|
13
|
-
## Stack
|
|
14
|
-
- Next.js 16 (App Router, Turbopack)
|
|
15
|
-
- React 19 (Server Components)
|
|
16
|
-
- TypeScript (strict)
|
|
17
|
-
- Tailwind CSS 4
|
|
18
|
-
- shadcn/ui
|
|
19
|
-
- Bun
|
|
20
|
-
${type === "app" ? "- Better Auth\n- Drizzle + PostgreSQL" : ""}
|
|
21
|
-
|
|
22
|
-
## Commands
|
|
23
|
-
\`\`\`bash
|
|
24
|
-
bun dev # Start development
|
|
25
|
-
bun build # Production build
|
|
26
|
-
bun lint # Run ESLint
|
|
27
|
-
${type === "app" ? "bun setup # Setup database" : ""}
|
|
28
|
-
\`\`\`
|
|
29
|
-
|
|
30
|
-
## Conventions
|
|
31
|
-
- Use \`bun\` for all package operations
|
|
32
|
-
- Server Components by default
|
|
33
|
-
- Dark mode first design
|
|
34
|
-
- Use \`cn()\` for conditional classes
|
|
35
|
-
- Add components: \`bunx --bun shadcn@latest add [component]\`
|
|
36
|
-
`,
|
|
37
|
-
},
|
|
38
|
-
cursor: {
|
|
39
|
-
filename: ".cursorrules",
|
|
40
|
-
content: (type) => `# Cursor Rules
|
|
41
|
-
|
|
42
|
-
Stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, shadcn/ui, Bun
|
|
43
|
-
${type === "app" ? "Auth: Better Auth | DB: Drizzle + PostgreSQL" : ""}
|
|
44
|
-
|
|
45
|
-
- Server Components by default
|
|
46
|
-
- "use client" only when needed
|
|
47
|
-
- Tailwind utility classes
|
|
48
|
-
- cn() for conditional classes
|
|
49
|
-
- Dark mode first
|
|
50
|
-
`,
|
|
51
|
-
},
|
|
52
|
-
gemini: {
|
|
53
|
-
filename: ".gemini/GEMINI.md",
|
|
54
|
-
content: (type) => `# ${type === "landing" ? "Landing Page" : type === "app" ? "Web App" : "Monorepo"}
|
|
55
|
-
|
|
56
|
-
## Stack
|
|
57
|
-
- Next.js 16 (App Router, Turbopack)
|
|
58
|
-
- React 19 (Server Components)
|
|
59
|
-
- TypeScript (strict)
|
|
60
|
-
- Tailwind CSS 4
|
|
61
|
-
- shadcn/ui
|
|
62
|
-
- Bun
|
|
63
|
-
${type === "app" ? "- Better Auth\n- Drizzle + PostgreSQL" : ""}
|
|
64
|
-
|
|
65
|
-
## Conventions
|
|
66
|
-
- Use \`bun\` for all package operations
|
|
67
|
-
- Server Components by default
|
|
68
|
-
- Dark mode first design
|
|
69
|
-
- Use \`cn()\` for conditional classes
|
|
70
|
-
`,
|
|
71
|
-
},
|
|
72
|
-
copilot: {
|
|
73
|
-
filename: ".github/copilot-instructions.md",
|
|
74
|
-
content: (type) => `# GitHub Copilot Instructions
|
|
75
|
-
|
|
76
|
-
Stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, shadcn/ui, Bun
|
|
77
|
-
${type === "app" ? "Auth: Better Auth | DB: Drizzle + PostgreSQL" : ""}
|
|
78
|
-
|
|
79
|
-
## Do
|
|
80
|
-
- Use TypeScript strict mode
|
|
81
|
-
- Prefer Server Components
|
|
82
|
-
- Use Tailwind for styling
|
|
83
|
-
- Use cn() for class merging
|
|
84
|
-
|
|
85
|
-
## Don't
|
|
86
|
-
- Use CSS modules or styled-components
|
|
87
|
-
- Use class components
|
|
88
|
-
- Add unnecessary dependencies
|
|
89
|
-
`,
|
|
90
|
-
},
|
|
91
|
-
windsurf: {
|
|
92
|
-
filename: ".windsurfrules",
|
|
93
|
-
content: (type) => `# Windsurf Rules
|
|
94
|
-
|
|
95
|
-
Stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, shadcn/ui, Bun
|
|
96
|
-
${type === "app" ? "Auth: Better Auth | DB: Drizzle + PostgreSQL" : ""}
|
|
97
|
-
|
|
98
|
-
- Server Components by default
|
|
99
|
-
- "use client" only when needed
|
|
100
|
-
- Tailwind utility classes
|
|
101
|
-
- cn() for conditional classes
|
|
102
|
-
- Dark mode first
|
|
103
|
-
`,
|
|
104
|
-
},
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// GitHub CLI check
|
|
108
|
-
async function checkGitHubCli(): Promise<{
|
|
109
|
-
installed: boolean
|
|
110
|
-
authenticated: boolean
|
|
111
|
-
username: string | null
|
|
112
|
-
orgs: string[]
|
|
113
|
-
}> {
|
|
114
|
-
const checkCmd = process.platform === "win32" ? "where" : "which"
|
|
115
|
-
try {
|
|
116
|
-
const hasGh = await $`${checkCmd} gh`.quiet().nothrow()
|
|
117
|
-
if (hasGh.exitCode !== 0) {
|
|
118
|
-
return { installed: false, authenticated: false, username: null, orgs: [] }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const authStatus = await $`gh auth status`.quiet().nothrow()
|
|
122
|
-
if (authStatus.exitCode !== 0) {
|
|
123
|
-
return { installed: true, authenticated: false, username: null, orgs: [] }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const username = (await $`gh api user --jq '.login'`.quiet().text()).trim()
|
|
127
|
-
const orgsJson = await $`gh api user/orgs --jq '.[].login'`.quiet().text()
|
|
128
|
-
const orgs = orgsJson.trim().split("\n").filter(Boolean)
|
|
129
|
-
|
|
130
|
-
return { installed: true, authenticated: true, username, orgs }
|
|
131
|
-
} catch {
|
|
132
|
-
return { installed: false, authenticated: false, username: null, orgs: [] }
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Templates privados (nimbuslab-templates) - uso interno
|
|
137
|
-
const PRIVATE_TEMPLATES = {
|
|
138
|
-
"fast": "nimbuslab-templates/fast-template",
|
|
139
|
-
"fast+": "nimbuslab-templates/fastplus-template",
|
|
140
|
-
"fast+-monorepo": "nimbuslab-templates/fastplus-monorepo-template",
|
|
141
|
-
"nimbus-core": "nimbuslab/nimbus-core",
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Templates públicos (nimbuslab)
|
|
145
|
-
const PUBLIC_TEMPLATES = {
|
|
146
|
-
"landing": "nimbuslab/create-next-landing",
|
|
147
|
-
"app": "nimbuslab/create-next-app",
|
|
148
|
-
"turborepo": "nimbuslab/create-turborepo",
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Todos os templates
|
|
152
|
-
const TEMPLATES = { ...PRIVATE_TEMPLATES, ...PUBLIC_TEMPLATES }
|
|
153
|
-
|
|
154
|
-
// Dominios nimbuslab para deteccao de membros
|
|
155
|
-
const NIMBUSLAB_DOMAINS = ["@nimbuslab.com.br", "@nimbuslab.net.br"]
|
|
156
|
-
|
|
157
|
-
// Detectar se é membro da nimbuslab (verifica apenas dominio do email)
|
|
158
|
-
async function isNimbuslabMember(): Promise<{ isMember: boolean; user: string | null; email: string | null }> {
|
|
159
|
-
try {
|
|
160
|
-
const user = (await $`git config user.name`.text()).trim()
|
|
161
|
-
const email = (await $`git config user.email`.text()).trim()
|
|
162
|
-
|
|
163
|
-
// Verifica se email termina com dominio nimbuslab
|
|
164
|
-
const isMember = NIMBUSLAB_DOMAINS.some(domain => email.endsWith(domain))
|
|
165
|
-
|
|
166
|
-
return { isMember, user, email }
|
|
167
|
-
} catch {
|
|
168
|
-
return { isMember: false, user: null, email: null }
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
type ProjectType = "fast" | "fast+" | "landing" | "app" | "turborepo" | "nimbus-core"
|
|
173
|
-
|
|
174
|
-
interface ProjectConfig {
|
|
175
|
-
name: string
|
|
176
|
-
type: ProjectType
|
|
177
|
-
monorepo: boolean
|
|
178
|
-
git: boolean
|
|
179
|
-
install: boolean
|
|
180
|
-
github: boolean
|
|
181
|
-
githubOrg: string | null
|
|
182
|
-
githubDescription: string
|
|
183
|
-
// Public template configs
|
|
184
|
-
theme: "dark" | "light" | "system"
|
|
185
|
-
aiAssistant: string | null
|
|
186
|
-
// M26: Número do contrato (fast only)
|
|
187
|
-
contractNumber: string
|
|
188
|
-
// M21-M23: Configs de infra
|
|
189
|
-
resendApiKey: string
|
|
190
|
-
resendFromEmail: string
|
|
191
|
-
contactEmail: string
|
|
192
|
-
// M30: Railway via CLI
|
|
193
|
-
railwayProject: string
|
|
194
|
-
railwayToken: string
|
|
195
|
-
stagingUrl: string
|
|
196
|
-
productionUrl: string
|
|
197
|
-
// Template customizado
|
|
198
|
-
customTemplate: string | null
|
|
199
|
-
// nimbus-core: URL do repo do cliente
|
|
200
|
-
clientRepoUrl?: string | null
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
interface CreateFlags {
|
|
204
|
-
yes: boolean
|
|
205
|
-
// Templates
|
|
206
|
-
landing: boolean
|
|
207
|
-
app: boolean
|
|
208
|
-
turborepo: boolean
|
|
209
|
-
// Templates internos nimbuslab
|
|
210
|
-
fast: boolean
|
|
211
|
-
fastPlus: boolean
|
|
212
|
-
fastTurborepo: boolean
|
|
213
|
-
core: boolean
|
|
214
|
-
// Outros
|
|
215
|
-
noGit: boolean
|
|
216
|
-
noInstall: boolean
|
|
217
|
-
railway: boolean
|
|
218
|
-
template: string | null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function parseFlags(args: string[]): { flags: CreateFlags; projectName: string | undefined } {
|
|
222
|
-
const flags: CreateFlags = {
|
|
223
|
-
yes: false,
|
|
224
|
-
landing: false,
|
|
225
|
-
app: false,
|
|
226
|
-
turborepo: false,
|
|
227
|
-
fast: false,
|
|
228
|
-
fastPlus: false,
|
|
229
|
-
fastTurborepo: false,
|
|
230
|
-
core: false,
|
|
231
|
-
noGit: false,
|
|
232
|
-
noInstall: false,
|
|
233
|
-
railway: false,
|
|
234
|
-
template: null,
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let projectName: string | undefined
|
|
238
|
-
let i = 0
|
|
239
|
-
|
|
240
|
-
while (i < args.length) {
|
|
241
|
-
const arg = args[i]!
|
|
242
|
-
|
|
243
|
-
if (arg === "-y" || arg === "--yes") {
|
|
244
|
-
flags.yes = true
|
|
245
|
-
} else if (arg === "--landing") {
|
|
246
|
-
flags.landing = true
|
|
247
|
-
} else if (arg === "--app") {
|
|
248
|
-
flags.app = true
|
|
249
|
-
} else if (arg === "--turborepo") {
|
|
250
|
-
flags.turborepo = true
|
|
251
|
-
} else if (arg === "--fast") {
|
|
252
|
-
flags.fast = true
|
|
253
|
-
} else if (arg === "--fast-plus") {
|
|
254
|
-
flags.fastPlus = true
|
|
255
|
-
} else if (arg === "--fast-turborepo") {
|
|
256
|
-
flags.fastTurborepo = true
|
|
257
|
-
} else if (arg === "--core") {
|
|
258
|
-
flags.core = true
|
|
259
|
-
} else if (arg === "--no-git") {
|
|
260
|
-
flags.noGit = true
|
|
261
|
-
} else if (arg === "--no-install") {
|
|
262
|
-
flags.noInstall = true
|
|
263
|
-
} else if (arg === "--railway") {
|
|
264
|
-
flags.railway = true
|
|
265
|
-
} else if (arg === "--template") {
|
|
266
|
-
i++
|
|
267
|
-
flags.template = args[i] ?? null
|
|
268
|
-
} else if (!arg.startsWith("-")) {
|
|
269
|
-
projectName = arg
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
i++
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { flags, projectName }
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// M30: Verificar e instalar Railway CLI
|
|
279
|
-
async function ensureRailwayCli(): Promise<boolean> {
|
|
280
|
-
const checkCmd = process.platform === "win32" ? "where" : "which"
|
|
281
|
-
const hasRailway = await $`${checkCmd} railway`.quiet().then(() => true).catch(() => false)
|
|
282
|
-
|
|
283
|
-
if (hasRailway) return true
|
|
284
|
-
|
|
285
|
-
console.log(pc.yellow("Railway CLI not found. Installing..."))
|
|
286
|
-
console.log()
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
if (process.platform === "win32") {
|
|
290
|
-
await $`powershell -c "iwr https://railway.app/install.ps1 -useb | iex"`.quiet()
|
|
291
|
-
} else {
|
|
292
|
-
await $`curl -fsSL https://railway.app/install.sh | sh`.quiet()
|
|
293
|
-
}
|
|
294
|
-
console.log(pc.green("Railway CLI installed successfully!"))
|
|
295
|
-
return true
|
|
296
|
-
} catch (error) {
|
|
297
|
-
console.log(pc.red("Error installing Railway CLI."))
|
|
298
|
-
console.log(pc.dim("Install manually: https://docs.railway.app/guides/cli"))
|
|
299
|
-
return false
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// M30: Listar projetos Railway via CLI
|
|
304
|
-
async function listRailwayProjects(): Promise<string[]> {
|
|
305
|
-
try {
|
|
306
|
-
const result = await $`railway list`.text()
|
|
307
|
-
// Parse output: primeira linha é o team, demais são projetos
|
|
308
|
-
const lines = result.trim().split("\n").filter(l => l.trim())
|
|
309
|
-
// Remove primeira linha (team name) e extrai nomes dos projetos
|
|
310
|
-
return lines.slice(1).map(l => l.trim())
|
|
311
|
-
} catch {
|
|
312
|
-
return []
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// M30: Verificar autenticacao Railway
|
|
317
|
-
async function isRailwayAuthenticated(): Promise<boolean> {
|
|
318
|
-
try {
|
|
319
|
-
await $`railway whoami`.quiet()
|
|
320
|
-
return true
|
|
321
|
-
} catch {
|
|
322
|
-
return false
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export async function create(args: string[]) {
|
|
327
|
-
// Verifica dependencias (cross-platform)
|
|
328
|
-
const checkCmd = process.platform === "win32" ? "where" : "which"
|
|
329
|
-
const hasBun = await $`${checkCmd} bun`.quiet().then(() => true).catch(() => false)
|
|
330
|
-
const hasGit = await $`${checkCmd} git`.quiet().then(() => true).catch(() => false)
|
|
331
|
-
const hasGh = await $`${checkCmd} gh`.quiet().then(() => true).catch(() => false)
|
|
332
|
-
|
|
333
|
-
if (!hasBun) {
|
|
334
|
-
console.log(pc.red("Error: Bun not found."))
|
|
335
|
-
console.log(pc.dim("Install from: https://bun.sh"))
|
|
336
|
-
console.log()
|
|
337
|
-
if (process.platform === "win32") {
|
|
338
|
-
console.log(pc.cyan('powershell -c "irm bun.sh/install.ps1 | iex"'))
|
|
339
|
-
} else {
|
|
340
|
-
console.log(pc.cyan("curl -fsSL https://bun.sh/install | bash"))
|
|
341
|
-
}
|
|
342
|
-
console.log()
|
|
343
|
-
process.exit(1)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!hasGit) {
|
|
347
|
-
console.log(pc.red("Error: Git not found."))
|
|
348
|
-
console.log(pc.dim("Install git to continue."))
|
|
349
|
-
process.exit(1)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// GitHub CLI is optional for public templates
|
|
353
|
-
// Will be checked later if user wants to create a repo
|
|
354
|
-
if (!hasGh) {
|
|
355
|
-
console.log(pc.dim(" GitHub CLI not found (repo creation will be skipped)"))
|
|
356
|
-
console.log(pc.dim(" Install from: https://cli.github.com"))
|
|
357
|
-
console.log()
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// M30: Verificar/instalar Railway CLI
|
|
361
|
-
const hasRailway = await ensureRailwayCli()
|
|
362
|
-
if (hasRailway) {
|
|
363
|
-
const railwayAuth = await isRailwayAuthenticated()
|
|
364
|
-
if (!railwayAuth) {
|
|
365
|
-
console.log(pc.yellow("Railway CLI not authenticated."))
|
|
366
|
-
console.log(pc.dim("Run: railway login"))
|
|
367
|
-
console.log()
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const { flags, projectName } = parseFlags(args)
|
|
372
|
-
|
|
373
|
-
p.intro(pc.bgCyan(pc.black(" New nimbuslab Project ")))
|
|
374
|
-
|
|
375
|
-
let config: ProjectConfig | symbol
|
|
376
|
-
|
|
377
|
-
// Determina tipo baseado nas flags
|
|
378
|
-
const hasTypeFlag = flags.landing || flags.app || flags.turborepo || flags.fast || flags.fastPlus || flags.fastTurborepo || flags.core
|
|
379
|
-
const typeFromFlag: ProjectType | null = flags.landing ? "landing"
|
|
380
|
-
: flags.app ? "app"
|
|
381
|
-
: flags.turborepo ? "turborepo"
|
|
382
|
-
: flags.fastTurborepo ? "fast+"
|
|
383
|
-
: flags.fastPlus ? "fast+"
|
|
384
|
-
: flags.fast ? "fast"
|
|
385
|
-
: flags.core ? "nimbus-core"
|
|
386
|
-
: null
|
|
387
|
-
const monorepoFromFlag = flags.fastTurborepo
|
|
388
|
-
|
|
389
|
-
// Modo automatico: -y com nome OU flags de tipo com nome
|
|
390
|
-
if ((flags.yes || hasTypeFlag) && projectName) {
|
|
391
|
-
// Templates públicos usam "landing" como default
|
|
392
|
-
const defaultType: ProjectType = flags.landing || flags.app || flags.turborepo ? "landing" : "fast"
|
|
393
|
-
config = {
|
|
394
|
-
name: projectName,
|
|
395
|
-
type: typeFromFlag || defaultType,
|
|
396
|
-
monorepo: monorepoFromFlag,
|
|
397
|
-
git: !flags.noGit,
|
|
398
|
-
install: !flags.noInstall,
|
|
399
|
-
github: false,
|
|
400
|
-
githubOrg: null,
|
|
401
|
-
githubDescription: "",
|
|
402
|
-
theme: "dark" as const,
|
|
403
|
-
aiAssistant: null,
|
|
404
|
-
contractNumber: "",
|
|
405
|
-
resendApiKey: "",
|
|
406
|
-
resendFromEmail: "",
|
|
407
|
-
contactEmail: "",
|
|
408
|
-
railwayProject: "",
|
|
409
|
-
railwayToken: "",
|
|
410
|
-
stagingUrl: "",
|
|
411
|
-
productionUrl: "",
|
|
412
|
-
customTemplate: flags.template,
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const typeLabel = flags.turborepo ? "fast+ (monorepo)" : config.type
|
|
416
|
-
console.log(pc.dim(` Project: ${projectName}`))
|
|
417
|
-
console.log(pc.dim(` Type: ${typeLabel}`))
|
|
418
|
-
console.log(pc.dim(` Git: ${config.git ? "yes" : "no"}`))
|
|
419
|
-
console.log(pc.dim(` Install: ${config.install ? "yes" : "no"}`))
|
|
420
|
-
if (flags.railway) console.log(pc.dim(` Railway: configurar`))
|
|
421
|
-
if (flags.template) console.log(pc.dim(` Template: ${flags.template}`))
|
|
422
|
-
console.log()
|
|
423
|
-
|
|
424
|
-
// Railway automatico se flag --railway
|
|
425
|
-
if (flags.railway) {
|
|
426
|
-
const railwayAuthenticated = await isRailwayAuthenticated()
|
|
427
|
-
if (railwayAuthenticated) {
|
|
428
|
-
if (config.type === "fast") {
|
|
429
|
-
const projects = await listRailwayProjects()
|
|
430
|
-
const fastProject = projects.find(p => p.toLowerCase().includes("fast by nimbuslab"))
|
|
431
|
-
if (fastProject) {
|
|
432
|
-
config.railwayProject = fastProject
|
|
433
|
-
console.log(pc.green(` Railway: ${fastProject}`))
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
// Fast+: cria projeto novo automaticamente
|
|
437
|
-
console.log(pc.dim(` Creating project Railway: ${projectName}...`))
|
|
438
|
-
try {
|
|
439
|
-
const result = await $`echo "" | railway init -n ${projectName} -w nimbuslab --json`.text()
|
|
440
|
-
const newProject = JSON.parse(result)
|
|
441
|
-
config.railwayProject = newProject.name || projectName
|
|
442
|
-
console.log(pc.green(` Railway: ${config.railwayProject} criado`))
|
|
443
|
-
} catch {
|
|
444
|
-
console.log(pc.yellow(` Railway: erro ao criar projeto`))
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} else {
|
|
448
|
-
console.log(pc.yellow(` Railway: not authenticated (railway login)`))
|
|
449
|
-
}
|
|
450
|
-
console.log()
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
config = await promptConfig(projectName, flags)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (p.isCancel(config)) {
|
|
457
|
-
p.cancel("Operation cancelled")
|
|
458
|
-
process.exit(0)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
await createProject(config as ProjectConfig)
|
|
462
|
-
|
|
463
|
-
p.outro(pc.green("Project created successfully!"))
|
|
464
|
-
|
|
465
|
-
showNextSteps(config as ProjectConfig)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function promptConfig(initialName?: string, flags?: CreateFlags): Promise<ProjectConfig | symbol> {
|
|
469
|
-
// Detectar usuario git e se é membro da nimbuslab
|
|
470
|
-
const { isMember, user } = await isNimbuslabMember()
|
|
471
|
-
|
|
472
|
-
// Saudacao
|
|
473
|
-
const greeting = user ? `Hello, ${user}!` : "Hello!"
|
|
474
|
-
console.log(pc.dim(` ${greeting}`))
|
|
475
|
-
console.log()
|
|
476
|
-
|
|
477
|
-
const name = await p.text({
|
|
478
|
-
message: "Project name:",
|
|
479
|
-
placeholder: "meu-projeto",
|
|
480
|
-
initialValue: initialName,
|
|
481
|
-
validate: (value) => {
|
|
482
|
-
if (!value) return "Name is required"
|
|
483
|
-
if (!/^[a-z0-9-]+$/.test(value)) {
|
|
484
|
-
return "Use only lowercase letters, numbers and hyphens"
|
|
485
|
-
}
|
|
486
|
-
return undefined
|
|
487
|
-
},
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
if (p.isCancel(name)) return name
|
|
491
|
-
|
|
492
|
-
// Opções públicas (todos veem)
|
|
493
|
-
const publicOptions = [
|
|
494
|
-
{
|
|
495
|
-
value: "landing",
|
|
496
|
-
label: "Landing Page",
|
|
497
|
-
hint: "Next.js 16 + Tailwind 4 + shadcn",
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
value: "app",
|
|
501
|
-
label: "Web App",
|
|
502
|
-
hint: "Landing + Better Auth + Drizzle",
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
value: "turborepo",
|
|
506
|
-
label: "Monorepo",
|
|
507
|
-
hint: "Turborepo + apps/packages",
|
|
508
|
-
},
|
|
509
|
-
]
|
|
510
|
-
|
|
511
|
-
// Opções privadas (só membros nimbuslab)
|
|
512
|
-
const privateOptions = isMember ? [
|
|
513
|
-
{
|
|
514
|
-
value: "fast",
|
|
515
|
-
label: "fast",
|
|
516
|
-
hint: "Landing page fast methodology",
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
value: "fast+",
|
|
520
|
-
label: "fast+",
|
|
521
|
-
hint: "Complete SaaS",
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
value: "nimbus-core",
|
|
525
|
-
label: "nimbus-core",
|
|
526
|
-
hint: "External projects (stealth mode)",
|
|
527
|
-
},
|
|
528
|
-
] : []
|
|
529
|
-
|
|
530
|
-
const type = await p.select({
|
|
531
|
-
message: "Project type:",
|
|
532
|
-
options: [...publicOptions, ...privateOptions],
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
if (p.isCancel(type)) return type
|
|
536
|
-
|
|
537
|
-
// Verifica se é template público (não precisa de auth nimbuslab)
|
|
538
|
-
const isPublicTemplate = ["landing", "app", "turborepo"].includes(type as string)
|
|
539
|
-
|
|
540
|
-
// Se não é membro e tentou usar template privado (via flag), bloqueia
|
|
541
|
-
if (!isMember && !isPublicTemplate) {
|
|
542
|
-
console.log(pc.red("Error: Template available only for nimbuslab members"))
|
|
543
|
-
process.exit(1)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Fluxo especial para nimbus-core (projetos externos)
|
|
547
|
-
if (type === "nimbus-core") {
|
|
548
|
-
console.log()
|
|
549
|
-
console.log(pc.dim(" nimbus-core: Motor para projetos externos (stealth mode)"))
|
|
550
|
-
console.log()
|
|
551
|
-
|
|
552
|
-
// Perguntar se quer criar repo (GitHub ou nenhum)
|
|
553
|
-
const repoOption = await p.select({
|
|
554
|
-
message: "Create repository for this project?",
|
|
555
|
-
options: [
|
|
556
|
-
{ value: "github", label: "GitHub (nimbuslab, private)", hint: "recommended" },
|
|
557
|
-
{ value: "none", label: "No repository", hint: "just local" },
|
|
558
|
-
],
|
|
559
|
-
})
|
|
560
|
-
if (p.isCancel(repoOption)) return repoOption
|
|
561
|
-
|
|
562
|
-
// Perguntar URL do repo do cliente para clonar no workspace
|
|
563
|
-
const clientRepo = await p.text({
|
|
564
|
-
message: "Client repo URL (to clone in workspace):",
|
|
565
|
-
placeholder: "git@github.com:client/repo.git ou Azure DevOps URL",
|
|
566
|
-
})
|
|
567
|
-
if (p.isCancel(clientRepo)) return clientRepo
|
|
568
|
-
|
|
569
|
-
return {
|
|
570
|
-
name: name as string,
|
|
571
|
-
type: "nimbus-core" as ProjectType,
|
|
572
|
-
monorepo: false,
|
|
573
|
-
git: true, // sempre init git
|
|
574
|
-
install: false, // nimbus-core não tem package.json
|
|
575
|
-
github: repoOption === "github",
|
|
576
|
-
githubOrg: "nimbuslab", // sempre na org nimbuslab
|
|
577
|
-
githubDescription: `nimbus-core for ${name} - external project`,
|
|
578
|
-
theme: "dark" as const,
|
|
579
|
-
aiAssistant: null,
|
|
580
|
-
contractNumber: "",
|
|
581
|
-
resendApiKey: "",
|
|
582
|
-
resendFromEmail: "",
|
|
583
|
-
contactEmail: "",
|
|
584
|
-
railwayProject: "",
|
|
585
|
-
railwayToken: "",
|
|
586
|
-
stagingUrl: "",
|
|
587
|
-
productionUrl: "",
|
|
588
|
-
customTemplate: null, // não usa template customizado
|
|
589
|
-
clientRepoUrl: (clientRepo as string) || null, // URL do cliente separada
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Pergunta sobre monorepo apenas para fast+
|
|
594
|
-
let monorepo = false
|
|
595
|
-
if (type === "fast+") {
|
|
596
|
-
const useMonorepo = await p.confirm({
|
|
597
|
-
message: "Use monorepo (Turborepo)?",
|
|
598
|
-
initialValue: false,
|
|
599
|
-
})
|
|
600
|
-
if (p.isCancel(useMonorepo)) return useMonorepo
|
|
601
|
-
monorepo = useMonorepo as boolean
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const git = await p.confirm({
|
|
605
|
-
message: "Initialize Git repository?",
|
|
606
|
-
initialValue: true,
|
|
607
|
-
})
|
|
608
|
-
|
|
609
|
-
if (p.isCancel(git)) return git
|
|
610
|
-
|
|
611
|
-
// GitHub options (only if git is enabled)
|
|
612
|
-
let github = false
|
|
613
|
-
let githubOrg: string | null = null
|
|
614
|
-
let githubDescription = ""
|
|
615
|
-
|
|
616
|
-
if (git) {
|
|
617
|
-
const createGithub = await p.confirm({
|
|
618
|
-
message: "Create GitHub repository?",
|
|
619
|
-
initialValue: false,
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
if (p.isCancel(createGithub)) return createGithub
|
|
623
|
-
|
|
624
|
-
github = createGithub as boolean
|
|
625
|
-
|
|
626
|
-
if (github) {
|
|
627
|
-
// M13: Selecao de organizacao GitHub
|
|
628
|
-
const org = await p.select({
|
|
629
|
-
message: "GitHub organization:",
|
|
630
|
-
options: [
|
|
631
|
-
{ value: "nimbuslab", label: "nimbuslab", hint: "Main org" },
|
|
632
|
-
{ value: "fast-by-nimbuslab", label: "fast-by-nimbuslab", hint: "Client projects" },
|
|
633
|
-
{ value: "nimbuslab-templates", label: "nimbuslab-templates", hint: "Templates" },
|
|
634
|
-
{ value: null, label: "Pessoal", hint: "Personal" },
|
|
635
|
-
],
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
if (p.isCancel(org)) return org
|
|
639
|
-
githubOrg = org as string | null
|
|
640
|
-
|
|
641
|
-
// M14: Descricao do repo
|
|
642
|
-
const description = await p.text({
|
|
643
|
-
message: "Repository description:",
|
|
644
|
-
placeholder: "Landing page para cliente X",
|
|
645
|
-
initialValue: type === "fast" ? "Landing page fast by nimbuslab" : "SaaS fast+ by nimbuslab",
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
if (p.isCancel(description)) return description
|
|
649
|
-
githubDescription = description as string
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// M26: Número do contrato (apenas para fast - privado)
|
|
654
|
-
let contractNumber = ""
|
|
655
|
-
if (type === "fast") {
|
|
656
|
-
const contract = await p.text({
|
|
657
|
-
message: "Contract number (ex: 001):",
|
|
658
|
-
placeholder: "001",
|
|
659
|
-
validate: (v) => v ? undefined : "Contract number is required for fast",
|
|
660
|
-
})
|
|
661
|
-
if (p.isCancel(contract)) return contract
|
|
662
|
-
contractNumber = contract as string
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Templates públicos têm fluxo simplificado mas com configs globais
|
|
666
|
-
if (isPublicTemplate) {
|
|
667
|
-
// Theme selection
|
|
668
|
-
const theme = await p.select({
|
|
669
|
-
message: "Default theme:",
|
|
670
|
-
options: [
|
|
671
|
-
{ value: "dark", label: "Dark", hint: "recommended" },
|
|
672
|
-
{ value: "light", label: "Light" },
|
|
673
|
-
{ value: "system", label: "System", hint: "follows OS preference" },
|
|
674
|
-
],
|
|
675
|
-
})
|
|
676
|
-
if (p.isCancel(theme)) return theme
|
|
677
|
-
|
|
678
|
-
// AI Assistant selection
|
|
679
|
-
const aiAssistant = await p.select({
|
|
680
|
-
message: "Which AI assistant do you use?",
|
|
681
|
-
options: [
|
|
682
|
-
{ value: "claude", label: "Claude Code", hint: "Anthropic" },
|
|
683
|
-
{ value: "cursor", label: "Cursor", hint: "AI-first editor" },
|
|
684
|
-
{ value: "gemini", label: "Gemini CLI", hint: "Google" },
|
|
685
|
-
{ value: "copilot", label: "GitHub Copilot" },
|
|
686
|
-
{ value: "windsurf", label: "Windsurf", hint: "Codeium" },
|
|
687
|
-
{ value: "none", label: "None", hint: "skip AI config" },
|
|
688
|
-
],
|
|
689
|
-
})
|
|
690
|
-
if (p.isCancel(aiAssistant)) return aiAssistant
|
|
691
|
-
|
|
692
|
-
// GitHub repo for public templates (uses user's orgs)
|
|
693
|
-
let publicGithub = false
|
|
694
|
-
let publicGithubOrg: string | null = null
|
|
695
|
-
|
|
696
|
-
if (git) {
|
|
697
|
-
const gh = await checkGitHubCli()
|
|
698
|
-
|
|
699
|
-
if (gh.installed && gh.authenticated) {
|
|
700
|
-
const createRepo = await p.confirm({
|
|
701
|
-
message: "Create GitHub repository?",
|
|
702
|
-
initialValue: false,
|
|
703
|
-
})
|
|
704
|
-
if (p.isCancel(createRepo)) return createRepo
|
|
705
|
-
|
|
706
|
-
publicGithub = createRepo as boolean
|
|
707
|
-
|
|
708
|
-
if (publicGithub) {
|
|
709
|
-
const repoOptions: { value: string | null; label: string; hint?: string }[] = [
|
|
710
|
-
{ value: gh.username, label: gh.username!, hint: "personal account" },
|
|
711
|
-
...gh.orgs.map((org) => ({ value: org, label: org })),
|
|
712
|
-
]
|
|
713
|
-
|
|
714
|
-
const repoOwner = await p.select({
|
|
715
|
-
message: "Where to create the repository?",
|
|
716
|
-
options: repoOptions,
|
|
717
|
-
})
|
|
718
|
-
if (p.isCancel(repoOwner)) return repoOwner
|
|
719
|
-
publicGithubOrg = repoOwner as string | null
|
|
720
|
-
|
|
721
|
-
const repoVisibility = await p.select({
|
|
722
|
-
message: "Repository visibility:",
|
|
723
|
-
options: [
|
|
724
|
-
{ value: "private", label: "Private", hint: "recommended" },
|
|
725
|
-
{ value: "public", label: "Public" },
|
|
726
|
-
],
|
|
727
|
-
})
|
|
728
|
-
if (p.isCancel(repoVisibility)) return repoVisibility
|
|
729
|
-
githubDescription = repoVisibility as string // reusing for visibility
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const install = await p.confirm({
|
|
735
|
-
message: "Install dependencies?",
|
|
736
|
-
initialValue: true,
|
|
737
|
-
})
|
|
738
|
-
|
|
739
|
-
if (p.isCancel(install)) return install
|
|
740
|
-
|
|
741
|
-
return {
|
|
742
|
-
name: name as string,
|
|
743
|
-
type: type as ProjectType,
|
|
744
|
-
monorepo: false,
|
|
745
|
-
git: git as boolean,
|
|
746
|
-
install: install as boolean,
|
|
747
|
-
github: publicGithub,
|
|
748
|
-
githubOrg: publicGithubOrg,
|
|
749
|
-
githubDescription,
|
|
750
|
-
theme: theme as "dark" | "light" | "system",
|
|
751
|
-
aiAssistant: aiAssistant === "none" ? null : aiAssistant as string,
|
|
752
|
-
contractNumber: "",
|
|
753
|
-
resendApiKey: "",
|
|
754
|
-
resendFromEmail: "",
|
|
755
|
-
contactEmail: "",
|
|
756
|
-
railwayProject: "",
|
|
757
|
-
railwayToken: "",
|
|
758
|
-
stagingUrl: "",
|
|
759
|
-
productionUrl: "",
|
|
760
|
-
customTemplate: flags?.template || null,
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Configuracoes de infra (separadas)
|
|
765
|
-
let resendApiKey = ""
|
|
766
|
-
let resendFromEmail = ""
|
|
767
|
-
let contactEmail = ""
|
|
768
|
-
let railwayProject = ""
|
|
769
|
-
let railwayToken = ""
|
|
770
|
-
let stagingUrl = ""
|
|
771
|
-
let productionUrl = ""
|
|
772
|
-
|
|
773
|
-
const currentYear = new Date().getFullYear().toString().slice(-3) // 025, 026, etc
|
|
774
|
-
|
|
775
|
-
// Defaults
|
|
776
|
-
const defaultStagingUrl = type === "fast"
|
|
777
|
-
? `https://fast-${contractNumber}-${currentYear}.nimbuslab.net.br`
|
|
778
|
-
: `https://${name}.nimbuslab.net.br`
|
|
779
|
-
const defaultFromEmail = "no-reply@nimbuslab.com.br"
|
|
780
|
-
const defaultContactEmail = type === "fast"
|
|
781
|
-
? "fast@nimbuslab.com.br"
|
|
782
|
-
: "suporte@nimbuslab.com.br"
|
|
783
|
-
|
|
784
|
-
// Menu de configuracoes opcionais
|
|
785
|
-
const infraOptions = await p.multiselect({
|
|
786
|
-
message: "What do you want to configure now?",
|
|
787
|
-
options: [
|
|
788
|
-
{ value: "urls", label: "URLs", hint: "staging and production" },
|
|
789
|
-
{ value: "resend", label: "Resend", hint: "form emails" },
|
|
790
|
-
{ value: "railway", label: "Railway", hint: "deploy and hosting" },
|
|
791
|
-
],
|
|
792
|
-
required: false,
|
|
793
|
-
})
|
|
794
|
-
|
|
795
|
-
if (p.isCancel(infraOptions)) return infraOptions
|
|
796
|
-
|
|
797
|
-
const configItems = infraOptions as string[]
|
|
798
|
-
|
|
799
|
-
// URLs
|
|
800
|
-
if (configItems.includes("urls")) {
|
|
801
|
-
console.log()
|
|
802
|
-
console.log(pc.dim(" Project URLs"))
|
|
803
|
-
|
|
804
|
-
const staging = await p.text({
|
|
805
|
-
message: "Staging URL:",
|
|
806
|
-
placeholder: defaultStagingUrl,
|
|
807
|
-
initialValue: defaultStagingUrl,
|
|
808
|
-
})
|
|
809
|
-
if (p.isCancel(staging)) return staging
|
|
810
|
-
stagingUrl = staging as string
|
|
811
|
-
|
|
812
|
-
const production = await p.text({
|
|
813
|
-
message: "Production URL:",
|
|
814
|
-
placeholder: defaultStagingUrl.replace('.nimbuslab.net.br', '.com.br'),
|
|
815
|
-
initialValue: "",
|
|
816
|
-
})
|
|
817
|
-
if (p.isCancel(production)) return production
|
|
818
|
-
productionUrl = production as string
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Resend
|
|
822
|
-
if (configItems.includes("resend")) {
|
|
823
|
-
console.log()
|
|
824
|
-
console.log(pc.dim(" Resend (Email)"))
|
|
825
|
-
|
|
826
|
-
const resendKey = await p.text({
|
|
827
|
-
message: "RESEND_API_KEY:",
|
|
828
|
-
placeholder: "re_xxxxxxxxxxxx",
|
|
829
|
-
})
|
|
830
|
-
if (p.isCancel(resendKey)) return resendKey
|
|
831
|
-
resendApiKey = resendKey as string
|
|
832
|
-
|
|
833
|
-
const fromEmail = await p.text({
|
|
834
|
-
message: "From email:",
|
|
835
|
-
placeholder: defaultFromEmail,
|
|
836
|
-
initialValue: defaultFromEmail,
|
|
837
|
-
})
|
|
838
|
-
if (p.isCancel(fromEmail)) return fromEmail
|
|
839
|
-
resendFromEmail = fromEmail as string
|
|
840
|
-
|
|
841
|
-
const contact = await p.text({
|
|
842
|
-
message: "Contact email (receives forms):",
|
|
843
|
-
placeholder: defaultContactEmail,
|
|
844
|
-
initialValue: defaultContactEmail,
|
|
845
|
-
})
|
|
846
|
-
if (p.isCancel(contact)) return contact
|
|
847
|
-
contactEmail = contact as string
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Railway
|
|
851
|
-
if (configItems.includes("railway")) {
|
|
852
|
-
// M30: Railway via CLI
|
|
853
|
-
const railwayAuthenticated = await isRailwayAuthenticated()
|
|
854
|
-
|
|
855
|
-
if (railwayAuthenticated) {
|
|
856
|
-
console.log()
|
|
857
|
-
console.log(pc.dim(" Railway"))
|
|
858
|
-
|
|
859
|
-
const projects = await listRailwayProjects()
|
|
860
|
-
|
|
861
|
-
if (type === "fast") {
|
|
862
|
-
// Fast usa projeto compartilhado "Fast by nimbuslab"
|
|
863
|
-
const fastProject = projects.find(p => p.toLowerCase().includes("fast by nimbuslab"))
|
|
864
|
-
if (fastProject) {
|
|
865
|
-
railwayProject = fastProject
|
|
866
|
-
console.log(pc.green(` Project: ${fastProject} (automatico)`))
|
|
867
|
-
} else {
|
|
868
|
-
console.log(pc.yellow(" Project 'Fast by nimbuslab' not found."))
|
|
869
|
-
console.log(pc.dim(" Configure manually in .env"))
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
// Fast+ pode escolher projeto existente ou criar novo
|
|
873
|
-
const projectOptions = [
|
|
874
|
-
...projects.map(proj => ({ value: proj, label: proj })),
|
|
875
|
-
{ value: "__new__", label: "Create new project", hint: "via railway init" },
|
|
876
|
-
{ value: "__skip__", label: "Skip", hint: "Configure later" },
|
|
877
|
-
]
|
|
878
|
-
|
|
879
|
-
const selectedProject = await p.select({
|
|
880
|
-
message: "Railway project for this SaaS:",
|
|
881
|
-
options: projectOptions,
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
if (p.isCancel(selectedProject)) return selectedProject
|
|
885
|
-
|
|
886
|
-
if (selectedProject === "__new__") {
|
|
887
|
-
// Criar projeto via Railway CLI
|
|
888
|
-
const projectNameForRailway = name as string
|
|
889
|
-
console.log(pc.dim(` Creating project "${projectNameForRailway}" on Railway...`))
|
|
890
|
-
try {
|
|
891
|
-
// Usa echo para passar input vazio e aceitar defaults (workspace)
|
|
892
|
-
const result = await $`echo "" | railway init -n ${projectNameForRailway} -w nimbuslab --json`.text()
|
|
893
|
-
const newProject = JSON.parse(result)
|
|
894
|
-
railwayProject = newProject.name || projectNameForRailway
|
|
895
|
-
console.log(pc.green(` Projeto "${railwayProject}" created successfully!`))
|
|
896
|
-
console.log(pc.dim(` ID: ${newProject.id || "N/A"}`))
|
|
897
|
-
} catch (error) {
|
|
898
|
-
console.log(pc.yellow(" Error creating project via CLI."))
|
|
899
|
-
console.log(pc.dim(" Create manually at: https://railway.app/new"))
|
|
900
|
-
}
|
|
901
|
-
} else if (selectedProject !== "__skip__") {
|
|
902
|
-
railwayProject = selectedProject as string
|
|
903
|
-
console.log(pc.green(` Project selected: ${railwayProject}`))
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
} else {
|
|
907
|
-
console.log()
|
|
908
|
-
console.log(pc.yellow(" Railway: not authenticated (railway login)"))
|
|
909
|
-
console.log(pc.dim(" Configure manually in .env"))
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const install = await p.confirm({
|
|
914
|
-
message: "Install dependencies?",
|
|
915
|
-
initialValue: true,
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
if (p.isCancel(install)) return install
|
|
919
|
-
|
|
920
|
-
return {
|
|
921
|
-
name: name as string,
|
|
922
|
-
type: type as "fast" | "fast+",
|
|
923
|
-
monorepo,
|
|
924
|
-
git: git as boolean,
|
|
925
|
-
install: install as boolean,
|
|
926
|
-
github,
|
|
927
|
-
githubOrg,
|
|
928
|
-
githubDescription,
|
|
929
|
-
theme: "dark" as const,
|
|
930
|
-
aiAssistant: null,
|
|
931
|
-
contractNumber,
|
|
932
|
-
resendApiKey,
|
|
933
|
-
resendFromEmail,
|
|
934
|
-
contactEmail,
|
|
935
|
-
railwayProject,
|
|
936
|
-
railwayToken,
|
|
937
|
-
stagingUrl,
|
|
938
|
-
productionUrl,
|
|
939
|
-
customTemplate: flags?.template || null,
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
async function createProject(config: ProjectConfig) {
|
|
944
|
-
const s = p.spinner()
|
|
945
|
-
|
|
946
|
-
// Determina template: customizado ou padrao
|
|
947
|
-
let templateRepo: string
|
|
948
|
-
let templateLabel: string
|
|
949
|
-
|
|
950
|
-
if (config.customTemplate) {
|
|
951
|
-
templateRepo = config.customTemplate
|
|
952
|
-
templateLabel = `customizado (${config.customTemplate})`
|
|
953
|
-
} else {
|
|
954
|
-
// fast+ com monorepo usa template específico
|
|
955
|
-
const templateKey = config.type === "fast+" && config.monorepo ? "fast+-monorepo" : config.type
|
|
956
|
-
templateRepo = TEMPLATES[templateKey as keyof typeof TEMPLATES]
|
|
957
|
-
templateLabel = config.monorepo ? `${config.type} (monorepo)` : config.type
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Verifica se é template público
|
|
961
|
-
const isPublicTemplate = ["landing", "app", "turborepo"].includes(config.type)
|
|
962
|
-
|
|
963
|
-
s.start(`Cloning template ${templateLabel}...`)
|
|
964
|
-
try {
|
|
965
|
-
if (isPublicTemplate) {
|
|
966
|
-
// Templates públicos: usa HTTPS (não precisa de auth)
|
|
967
|
-
const httpsUrl = `https://github.com/${templateRepo}.git`
|
|
968
|
-
await $`git clone --depth 1 ${httpsUrl} ${config.name}`.quiet()
|
|
969
|
-
} else {
|
|
970
|
-
// Templates privados: usa gh (precisa de auth)
|
|
971
|
-
await $`gh repo clone ${templateRepo} ${config.name} -- --depth 1`.quiet()
|
|
972
|
-
}
|
|
973
|
-
await rm(join(config.name, ".git"), { recursive: true, force: true })
|
|
974
|
-
s.stop(`Template cloned`)
|
|
975
|
-
} catch (error) {
|
|
976
|
-
s.stop("Error cloning template")
|
|
977
|
-
throw new Error(`Failed to clone template ${templateRepo}. Check your connection or repository access.`)
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// nimbus-core: clone client repo in workspace if provided
|
|
981
|
-
if (config.type === "nimbus-core" && config.clientRepoUrl) {
|
|
982
|
-
const clientRepoUrl = config.clientRepoUrl
|
|
983
|
-
s.start(`Cloning client repo in workspace...`)
|
|
984
|
-
try {
|
|
985
|
-
// Extrair nome do projeto da URL (funciona com GitHub, GitLab, Azure DevOps)
|
|
986
|
-
let projectName = "client-project"
|
|
987
|
-
if (clientRepoUrl.includes("visualstudio.com") || clientRepoUrl.includes("dev.azure.com")) {
|
|
988
|
-
// Azure DevOps: serel@vs-ssh.visualstudio.com:v3/serel/Project/Repo
|
|
989
|
-
const parts = clientRepoUrl.split("/")
|
|
990
|
-
projectName = parts[parts.length - 1] || "client-project"
|
|
991
|
-
} else {
|
|
992
|
-
// GitHub/GitLab: git@github.com:org/repo.git
|
|
993
|
-
projectName = clientRepoUrl.split("/").pop()?.replace(".git", "") || "client-project"
|
|
994
|
-
}
|
|
995
|
-
await $`git clone ${clientRepoUrl} ${config.name}/workspace/${projectName}`.quiet()
|
|
996
|
-
s.stop(`Client repo cloned: workspace/${projectName}`)
|
|
997
|
-
} catch (error) {
|
|
998
|
-
s.stop("Error cloning client repo")
|
|
999
|
-
console.log(pc.dim(" You can clone manually: cd workspace && git clone <url>"))
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Update package.json (skip for nimbus-core)
|
|
1004
|
-
if (config.type !== "nimbus-core") {
|
|
1005
|
-
s.start("Configuring project...")
|
|
1006
|
-
try {
|
|
1007
|
-
const pkgPath = `${config.name}/package.json`
|
|
1008
|
-
const pkg = await Bun.file(pkgPath).json()
|
|
1009
|
-
pkg.name = config.name
|
|
1010
|
-
await Bun.write(pkgPath, JSON.stringify(pkg, null, 2))
|
|
1011
|
-
s.stop("Project configured")
|
|
1012
|
-
} catch (error) {
|
|
1013
|
-
s.stop("Error configuring")
|
|
1014
|
-
// Continue anyway
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Apply theme config (public templates only)
|
|
1019
|
-
if (isPublicTemplate && config.theme) {
|
|
1020
|
-
s.start(`Setting theme to ${config.theme}...`)
|
|
1021
|
-
try {
|
|
1022
|
-
const layoutPath = `${config.name}/src/app/layout.tsx`
|
|
1023
|
-
let layout = await Bun.file(layoutPath).text()
|
|
1024
|
-
layout = layout.replace(
|
|
1025
|
-
/defaultTheme="(dark|light|system)"/,
|
|
1026
|
-
`defaultTheme="${config.theme}"`
|
|
1027
|
-
)
|
|
1028
|
-
await Bun.write(layoutPath, layout)
|
|
1029
|
-
s.stop(`Theme set to ${config.theme}`)
|
|
1030
|
-
} catch {
|
|
1031
|
-
s.stop("Theme config skipped")
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Generate AI config (public templates only)
|
|
1036
|
-
if (isPublicTemplate && config.aiAssistant) {
|
|
1037
|
-
const aiConfig = AI_CONFIGS[config.aiAssistant]
|
|
1038
|
-
if (aiConfig) {
|
|
1039
|
-
s.start(`Generating ${config.aiAssistant} config...`)
|
|
1040
|
-
try {
|
|
1041
|
-
const content = aiConfig.content(config.type)
|
|
1042
|
-
const filePath = `${config.name}/${aiConfig.filename}`
|
|
1043
|
-
|
|
1044
|
-
// Create directory if needed
|
|
1045
|
-
if (aiConfig.filename.includes("/")) {
|
|
1046
|
-
const dir = aiConfig.filename.split("/").slice(0, -1).join("/")
|
|
1047
|
-
await mkdir(`${config.name}/${dir}`, { recursive: true })
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
await Bun.write(filePath, content)
|
|
1051
|
-
s.stop(`${aiConfig.filename} created`)
|
|
1052
|
-
} catch {
|
|
1053
|
-
s.stop("AI config skipped")
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Setup fast+ if selected
|
|
1059
|
-
if (config.type === "fast+") {
|
|
1060
|
-
s.start("Configurando fast+ (SaaS)...")
|
|
1061
|
-
// O setup real será feito pelo bun setup do próprio template
|
|
1062
|
-
s.stop("Configuracao fast+ preparada")
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Git init with full branch flow: main → staging → develop
|
|
1066
|
-
if (config.git) {
|
|
1067
|
-
s.start("Initializing Git...")
|
|
1068
|
-
try {
|
|
1069
|
-
const cwd = config.name
|
|
1070
|
-
// Init with main branch (not master)
|
|
1071
|
-
await $`git init -b main`.cwd(cwd).quiet()
|
|
1072
|
-
await $`git add -A`.cwd(cwd).quiet()
|
|
1073
|
-
await $`git commit -m "chore: setup inicial via nimbus create"`.cwd(cwd).quiet()
|
|
1074
|
-
|
|
1075
|
-
// Create branch flow: main → staging → develop
|
|
1076
|
-
await $`git checkout -b staging`.cwd(cwd).quiet()
|
|
1077
|
-
await $`git checkout -b develop`.cwd(cwd).quiet()
|
|
1078
|
-
|
|
1079
|
-
s.stop("Git initialized (main -> staging -> develop)")
|
|
1080
|
-
} catch (error) {
|
|
1081
|
-
s.stop("Error initializing Git")
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// Create GitHub repo if requested (M13 + M14)
|
|
1085
|
-
if (config.github) {
|
|
1086
|
-
s.start("Creating GitHub repository...")
|
|
1087
|
-
try {
|
|
1088
|
-
const cwd = config.name
|
|
1089
|
-
const repoName = config.githubOrg
|
|
1090
|
-
? `${config.githubOrg}/${config.name}`
|
|
1091
|
-
: config.name
|
|
1092
|
-
|
|
1093
|
-
// For public templates, githubDescription contains visibility (private/public)
|
|
1094
|
-
// For private templates, use org-based visibility
|
|
1095
|
-
// nimbus-core is ALWAYS private
|
|
1096
|
-
let visibility: string
|
|
1097
|
-
if (config.type === "nimbus-core") {
|
|
1098
|
-
visibility = "--private" // nimbus-core sempre privado
|
|
1099
|
-
} else if (isPublicTemplate) {
|
|
1100
|
-
visibility = config.githubDescription === "public" ? "--public" : "--private"
|
|
1101
|
-
} else {
|
|
1102
|
-
visibility = config.githubOrg === "fast-by-nimbuslab" ? "--private" : "--public"
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Create repo
|
|
1106
|
-
if (isPublicTemplate) {
|
|
1107
|
-
await $`gh repo create ${repoName} ${visibility} --source . --remote origin`.cwd(cwd).quiet()
|
|
1108
|
-
} else {
|
|
1109
|
-
await $`gh repo create ${repoName} ${visibility} --description ${config.githubDescription} --source . --remote origin`.cwd(cwd).quiet()
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// Push todas as branches na ordem correta: main -> staging -> develop
|
|
1113
|
-
await $`git checkout main`.cwd(cwd).quiet()
|
|
1114
|
-
await $`git push -u origin main`.cwd(cwd).quiet()
|
|
1115
|
-
await $`git checkout staging`.cwd(cwd).quiet()
|
|
1116
|
-
await $`git push -u origin staging`.cwd(cwd).quiet()
|
|
1117
|
-
await $`git checkout develop`.cwd(cwd).quiet()
|
|
1118
|
-
await $`git push -u origin develop`.cwd(cwd).quiet()
|
|
1119
|
-
|
|
1120
|
-
s.stop(`GitHub: ${repoName}`)
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
s.stop("Error creating GitHub repository")
|
|
1123
|
-
console.log(pc.dim(" You can create manually with: gh repo create"))
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
// Railway link (se projeto foi selecionado/criado)
|
|
1129
|
-
if (config.railwayProject) {
|
|
1130
|
-
s.start(`Linkando Railway: ${config.railwayProject}...`)
|
|
1131
|
-
try {
|
|
1132
|
-
await $`railway link -p ${config.railwayProject}`.cwd(config.name).quiet()
|
|
1133
|
-
s.stop(`Railway linkado: ${config.railwayProject}`)
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
s.stop("Error linking Railway")
|
|
1136
|
-
console.log(pc.dim(" Run manually: railway link"))
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// M23: Gerar .env se configs foram fornecidas
|
|
1141
|
-
if (config.resendApiKey || config.stagingUrl) {
|
|
1142
|
-
s.start("Gerando arquivo .env...")
|
|
1143
|
-
try {
|
|
1144
|
-
const envContent = generateEnvFile(config)
|
|
1145
|
-
await Bun.write(`${config.name}/.env`, envContent)
|
|
1146
|
-
s.stop("Arquivo .env criado")
|
|
1147
|
-
} catch (error) {
|
|
1148
|
-
s.stop("Error creating .env")
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// Install deps
|
|
1153
|
-
if (config.install) {
|
|
1154
|
-
s.start("Installing dependencies (pode demorar)...")
|
|
1155
|
-
try {
|
|
1156
|
-
await $`bun install`.cwd(config.name).quiet()
|
|
1157
|
-
s.stop("Dependencies installed")
|
|
1158
|
-
} catch (error) {
|
|
1159
|
-
s.stop("Error installing dependencies")
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// M23: Gerar conteudo do .env
|
|
1165
|
-
function generateEnvFile(config: ProjectConfig): string {
|
|
1166
|
-
const lines = [
|
|
1167
|
-
"# Gerado automaticamente pelo nimbus-cli",
|
|
1168
|
-
"# Nao commitar este arquivo!",
|
|
1169
|
-
"",
|
|
1170
|
-
"# App",
|
|
1171
|
-
`NODE_ENV=development`,
|
|
1172
|
-
"",
|
|
1173
|
-
"# URLs",
|
|
1174
|
-
`NEXT_PUBLIC_APP_URL=${config.productionUrl || "http://localhost:3000"}`,
|
|
1175
|
-
`STAGING_URL=${config.stagingUrl || ""}`,
|
|
1176
|
-
`PRODUCTION_URL=${config.productionUrl || ""}`,
|
|
1177
|
-
"",
|
|
1178
|
-
"# Resend (Email)",
|
|
1179
|
-
`RESEND_API_KEY=${config.resendApiKey || ""}`,
|
|
1180
|
-
`RESEND_FROM_EMAIL=${config.resendFromEmail || ""}`,
|
|
1181
|
-
`CONTACT_EMAIL=${config.contactEmail || ""}`,
|
|
1182
|
-
]
|
|
1183
|
-
|
|
1184
|
-
// Railway (projeto e token)
|
|
1185
|
-
if (config.railwayProject || config.railwayToken) {
|
|
1186
|
-
lines.push("")
|
|
1187
|
-
lines.push("# Railway")
|
|
1188
|
-
if (config.railwayProject) {
|
|
1189
|
-
lines.push(`# Project: ${config.railwayProject}`)
|
|
1190
|
-
}
|
|
1191
|
-
lines.push(`RAILWAY_TOKEN=${config.railwayToken || "# Configure com: railway link"}`)
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// Fast+ specific vars
|
|
1195
|
-
if (config.type === "fast+") {
|
|
1196
|
-
lines.push("")
|
|
1197
|
-
lines.push("# Database (fast+)")
|
|
1198
|
-
lines.push("DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app")
|
|
1199
|
-
lines.push("")
|
|
1200
|
-
lines.push("# Auth (fast+)")
|
|
1201
|
-
lines.push("BETTER_AUTH_SECRET=")
|
|
1202
|
-
lines.push("BETTER_AUTH_URL=http://localhost:3000")
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
return lines.join("\n") + "\n"
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
function showNextSteps(config: ProjectConfig) {
|
|
1209
|
-
const isPublicTemplate = ["landing", "app", "turborepo"].includes(config.type)
|
|
1210
|
-
const needsSetup = config.type === "app" // app needs bun setup for DB
|
|
1211
|
-
|
|
1212
|
-
console.log()
|
|
1213
|
-
console.log(pc.bold("Next steps:"))
|
|
1214
|
-
console.log()
|
|
1215
|
-
console.log(` ${pc.cyan("cd")} ${config.name}`)
|
|
1216
|
-
|
|
1217
|
-
// nimbus-core tem fluxo especial
|
|
1218
|
-
if (config.type === "nimbus-core") {
|
|
1219
|
-
console.log()
|
|
1220
|
-
console.log(pc.dim(" nimbus-core: Motor para projetos externos"))
|
|
1221
|
-
console.log()
|
|
1222
|
-
console.log(pc.dim(" Para usar a Lola:"))
|
|
1223
|
-
console.log(` ${pc.cyan("lola")}`)
|
|
1224
|
-
console.log()
|
|
1225
|
-
console.log(pc.yellow(" STEALTH MODE: Commits sem mencao a nimbuslab/Lola/IA"))
|
|
1226
|
-
console.log()
|
|
1227
|
-
|
|
1228
|
-
// GitHub info
|
|
1229
|
-
if (config.github) {
|
|
1230
|
-
const repoUrl = `https://github.com/nimbuslab/${config.name}`
|
|
1231
|
-
console.log(pc.green(` GitHub (private): ${repoUrl}`))
|
|
1232
|
-
console.log()
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
console.log(pc.dim(" Docs: See README.md for full instructions"))
|
|
1236
|
-
console.log()
|
|
1237
|
-
return
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (!config.install) {
|
|
1241
|
-
console.log(` ${pc.cyan("bun")} install`)
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Templates que precisam de setup adicional
|
|
1245
|
-
if (!isPublicTemplate || needsSetup) {
|
|
1246
|
-
console.log(` ${pc.cyan("bun")} setup`)
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
console.log(` ${pc.cyan("bun")} dev`)
|
|
1250
|
-
console.log()
|
|
1251
|
-
|
|
1252
|
-
// Explicar o que o bun setup faz para o app
|
|
1253
|
-
if (needsSetup && isPublicTemplate) {
|
|
1254
|
-
console.log(pc.dim(" bun setup will:"))
|
|
1255
|
-
console.log(pc.dim(" - Start PostgreSQL with Docker"))
|
|
1256
|
-
console.log(pc.dim(" - Run database migrations"))
|
|
1257
|
-
console.log(pc.dim(" - Create demo user (demo@example.com / demo1234)"))
|
|
1258
|
-
console.log()
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// Git flow info
|
|
1262
|
-
if (config.git) {
|
|
1263
|
-
console.log(pc.dim(" Git: main -> staging -> develop (current branch)"))
|
|
1264
|
-
|
|
1265
|
-
// GitHub info
|
|
1266
|
-
if (config.github) {
|
|
1267
|
-
const repoUrl = config.githubOrg
|
|
1268
|
-
? `https://github.com/${config.githubOrg}/${config.name}`
|
|
1269
|
-
: `https://github.com/${config.name}`
|
|
1270
|
-
console.log(pc.green(` GitHub: ${repoUrl}`))
|
|
1271
|
-
}
|
|
1272
|
-
console.log()
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Theme e AI info
|
|
1276
|
-
if (isPublicTemplate) {
|
|
1277
|
-
if (config.theme !== "dark") {
|
|
1278
|
-
console.log(pc.dim(` Theme: ${config.theme}`))
|
|
1279
|
-
}
|
|
1280
|
-
if (config.aiAssistant) {
|
|
1281
|
-
const aiConfig = AI_CONFIGS[config.aiAssistant]
|
|
1282
|
-
if (aiConfig) {
|
|
1283
|
-
console.log(pc.dim(` AI config: ${aiConfig.filename}`))
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
if (config.theme !== "dark" || config.aiAssistant) {
|
|
1287
|
-
console.log()
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
if (config.type === "fast+") {
|
|
1292
|
-
console.log(pc.dim(" bun setup will:"))
|
|
1293
|
-
console.log(pc.dim(" - Start PostgreSQL with Docker"))
|
|
1294
|
-
console.log(pc.dim(" - Run database migrations"))
|
|
1295
|
-
console.log(pc.dim(" - Create demo user (demo@example.com / demo1234)"))
|
|
1296
|
-
console.log()
|
|
1297
|
-
console.log(pc.dim(" Tip: Configure DATABASE_URL and BETTER_AUTH_SECRET in .env"))
|
|
1298
|
-
if (!config.railwayToken) {
|
|
1299
|
-
console.log(pc.dim(" Railway: Create a project at https://railway.app/new"))
|
|
1300
|
-
}
|
|
1301
|
-
console.log()
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Info sobre .env (templates privados)
|
|
1305
|
-
if (!isPublicTemplate) {
|
|
1306
|
-
if (config.resendApiKey || config.stagingUrl) {
|
|
1307
|
-
console.log(pc.green(" .env configured!"))
|
|
1308
|
-
console.log()
|
|
1309
|
-
} else {
|
|
1310
|
-
console.log(pc.yellow(" Tip: Configure .env manually or use 'bun setup'."))
|
|
1311
|
-
console.log()
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// Info sobre templates
|
|
1316
|
-
if (isPublicTemplate) {
|
|
1317
|
-
console.log(pc.dim(" Open source template (MIT) by nimbuslab"))
|
|
1318
|
-
console.log(pc.dim(` https://github.com/nimbuslab/create-next-${config.type === "turborepo" ? "turborepo" : config.type}`))
|
|
1319
|
-
} else {
|
|
1320
|
-
console.log(pc.dim(" https://github.com/nimbuslab-templates"))
|
|
1321
|
-
}
|
|
1322
|
-
console.log()
|
|
1323
|
-
}
|