@nimbuslab/cli 0.1.4 → 0.2.2

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/dist/index.js CHANGED
@@ -764,27 +764,55 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
764
764
  l2 = R2(m2 ?? l2);
765
765
  } };
766
766
  };
767
- var Ce = async (t, n) => {
768
- const r2 = {}, i = Object.keys(t);
769
- for (const s of i) {
770
- const c = t[s], a = await c({ results: r2 })?.catch((l2) => {
771
- throw l2;
772
- });
773
- if (typeof n?.onCancel == "function" && pD(a)) {
774
- r2[s] = "canceled", n.onCancel({ results: r2 });
775
- continue;
776
- }
777
- r2[s] = a;
778
- }
779
- return r2;
780
- };
781
767
 
782
768
  // src/commands/create.ts
783
769
  var import_picocolors3 = __toESM(require_picocolors(), 1);
784
770
  var {$: $2 } = globalThis.Bun;
785
771
  import { rm } from "fs/promises";
786
772
  import { join } from "path";
787
- var TEMPLATE_REPO = "nimbuslab-templates/fast-by-nimbuslab";
773
+ var TEMPLATES = {
774
+ fast: "nimbuslab-templates/fast-template",
775
+ "fast+": "nimbuslab-templates/fastplus-template"
776
+ };
777
+ async function ensureRailwayCli() {
778
+ const checkCmd = process.platform === "win32" ? "where" : "which";
779
+ const hasRailway = await $2`${checkCmd} railway`.quiet().then(() => true).catch(() => false);
780
+ if (hasRailway)
781
+ return true;
782
+ console.log(import_picocolors3.default.yellow("Railway CLI nao encontrado. Instalando..."));
783
+ console.log();
784
+ try {
785
+ if (process.platform === "win32") {
786
+ await $2`powershell -c "iwr https://railway.app/install.ps1 -useb | iex"`.quiet();
787
+ } else {
788
+ await $2`curl -fsSL https://railway.app/install.sh | sh`.quiet();
789
+ }
790
+ console.log(import_picocolors3.default.green("Railway CLI instalado com sucesso!"));
791
+ return true;
792
+ } catch (error) {
793
+ console.log(import_picocolors3.default.red("Erro ao instalar Railway CLI."));
794
+ console.log(import_picocolors3.default.dim("Instale manualmente: https://docs.railway.app/guides/cli"));
795
+ return false;
796
+ }
797
+ }
798
+ async function listRailwayProjects() {
799
+ try {
800
+ const result = await $2`railway list`.text();
801
+ const lines = result.trim().split(`
802
+ `).filter((l2) => l2.trim());
803
+ return lines.slice(1).map((l2) => l2.trim());
804
+ } catch {
805
+ return [];
806
+ }
807
+ }
808
+ async function isRailwayAuthenticated() {
809
+ try {
810
+ await $2`railway whoami`.quiet();
811
+ return true;
812
+ } catch {
813
+ return false;
814
+ }
815
+ }
788
816
  async function create(args) {
789
817
  const checkCmd = process.platform === "win32" ? "where" : "which";
790
818
  const hasBun = await $2`${checkCmd} bun`.quiet().then(() => true).catch(() => false);
@@ -825,6 +853,15 @@ async function create(args) {
825
853
  console.log(import_picocolors3.default.dim("Execute: gh auth login"));
826
854
  process.exit(1);
827
855
  }
856
+ const hasRailway = await ensureRailwayCli();
857
+ if (hasRailway) {
858
+ const railwayAuth = await isRailwayAuthenticated();
859
+ if (!railwayAuth) {
860
+ console.log(import_picocolors3.default.yellow("Railway CLI nao autenticado."));
861
+ console.log(import_picocolors3.default.dim("Execute: railway login"));
862
+ console.log();
863
+ }
864
+ }
828
865
  const hasYes = args.includes("-y") || args.includes("--yes");
829
866
  const projectName = args.find((a) => !a.startsWith("-"));
830
867
  Ie(import_picocolors3.default.bgCyan(import_picocolors3.default.black(" Novo Projeto nimbuslab ")));
@@ -834,11 +871,24 @@ async function create(args) {
834
871
  name: projectName,
835
872
  type: "fast",
836
873
  git: true,
837
- install: true
874
+ install: true,
875
+ github: false,
876
+ githubOrg: null,
877
+ githubDescription: "",
878
+ contractNumber: "",
879
+ resendApiKey: "",
880
+ resendFromEmail: "",
881
+ contactEmail: "",
882
+ railwayProject: "",
883
+ railwayToken: "",
884
+ stagingUrl: "",
885
+ productionUrl: ""
838
886
  };
839
887
  console.log(import_picocolors3.default.dim(` Projeto: ${projectName}`));
840
888
  console.log(import_picocolors3.default.dim(` Tipo: fast`));
841
889
  console.log(import_picocolors3.default.dim(` Git: sim`));
890
+ console.log(import_picocolors3.default.dim(` GitHub: nao`));
891
+ console.log(import_picocolors3.default.dim(` Infra: configurar depois`));
842
892
  console.log(import_picocolors3.default.dim(` Instalar: sim`));
843
893
  console.log();
844
894
  } else {
@@ -885,35 +935,193 @@ async function promptConfig(initialName) {
885
935
  });
886
936
  if (pD(type))
887
937
  return type;
888
- const extras = await Ce({
889
- git: () => ye({
890
- message: "Inicializar repositorio Git?",
891
- initialValue: true
892
- }),
893
- install: () => ye({
894
- message: "Instalar dependencias?",
895
- initialValue: true
896
- })
938
+ const git = await ye({
939
+ message: "Inicializar repositorio Git?",
940
+ initialValue: true
897
941
  });
898
- if (pD(extras))
899
- return extras;
942
+ if (pD(git))
943
+ return git;
944
+ let github = false;
945
+ let githubOrg = null;
946
+ let githubDescription = "";
947
+ if (git) {
948
+ const createGithub = await ye({
949
+ message: "Criar repositorio no GitHub?",
950
+ initialValue: false
951
+ });
952
+ if (pD(createGithub))
953
+ return createGithub;
954
+ github = createGithub;
955
+ if (github) {
956
+ const org = await ve({
957
+ message: "Organizacao GitHub:",
958
+ options: [
959
+ { value: "nimbuslab", label: "nimbuslab", hint: "Org principal" },
960
+ { value: "fast-by-nimbuslab", label: "fast-by-nimbuslab", hint: "Projetos de clientes" },
961
+ { value: "nimbuslab-templates", label: "nimbuslab-templates", hint: "Templates" },
962
+ { value: null, label: "Pessoal", hint: "Sem organizacao" }
963
+ ]
964
+ });
965
+ if (pD(org))
966
+ return org;
967
+ githubOrg = org;
968
+ const description = await he({
969
+ message: "Descricao do repositorio:",
970
+ placeholder: "Landing page para cliente X",
971
+ initialValue: type === "fast" ? "Landing page fast by nimbuslab" : "SaaS fast+ by nimbuslab"
972
+ });
973
+ if (pD(description))
974
+ return description;
975
+ githubDescription = description;
976
+ }
977
+ }
978
+ let contractNumber = "";
979
+ if (type === "fast") {
980
+ const contract = await he({
981
+ message: "Numero do contrato (ex: 001):",
982
+ placeholder: "001",
983
+ validate: (v2) => v2 ? undefined : "Numero do contrato e obrigatorio para fast"
984
+ });
985
+ if (pD(contract))
986
+ return contract;
987
+ contractNumber = contract;
988
+ }
989
+ const configureInfra = await ye({
990
+ message: "Configurar infra agora? (Resend, URLs)",
991
+ initialValue: true
992
+ });
993
+ if (pD(configureInfra))
994
+ return configureInfra;
995
+ let resendApiKey = "";
996
+ let resendFromEmail = "";
997
+ let contactEmail = "";
998
+ let railwayProject = "";
999
+ let railwayToken = "";
1000
+ let stagingUrl = "";
1001
+ let productionUrl = "";
1002
+ if (configureInfra) {
1003
+ const currentYear = new Date().getFullYear().toString().slice(-2);
1004
+ const defaultStagingUrl = type === "fast" ? `https://fast-${contractNumber}-${currentYear}.nimbuslab.net.br` : `https://${name}.nimbuslab.net.br`;
1005
+ const defaultFromEmail = "no-reply@nimbuslab.com.br";
1006
+ const defaultContactEmail = type === "fast" ? "fast@nimbuslab.com.br" : "suporte@nimbuslab.com.br";
1007
+ console.log();
1008
+ console.log(import_picocolors3.default.dim(" Resend (Email)"));
1009
+ const resendKey = await he({
1010
+ message: "RESEND_API_KEY:",
1011
+ placeholder: "re_xxxxxxxxxxxx"
1012
+ });
1013
+ if (pD(resendKey))
1014
+ return resendKey;
1015
+ resendApiKey = resendKey;
1016
+ const fromEmail = await he({
1017
+ message: "Email de envio (from):",
1018
+ placeholder: defaultFromEmail,
1019
+ initialValue: defaultFromEmail
1020
+ });
1021
+ if (pD(fromEmail))
1022
+ return fromEmail;
1023
+ resendFromEmail = fromEmail;
1024
+ const contact = await he({
1025
+ message: "Email de contato (recebe formularios):",
1026
+ placeholder: defaultContactEmail,
1027
+ initialValue: defaultContactEmail
1028
+ });
1029
+ if (pD(contact))
1030
+ return contact;
1031
+ contactEmail = contact;
1032
+ console.log();
1033
+ console.log(import_picocolors3.default.dim(" URLs do projeto"));
1034
+ const staging = await he({
1035
+ message: "URL de staging:",
1036
+ placeholder: defaultStagingUrl,
1037
+ initialValue: defaultStagingUrl
1038
+ });
1039
+ if (pD(staging))
1040
+ return staging;
1041
+ stagingUrl = staging;
1042
+ const production = await he({
1043
+ message: "URL de producao:",
1044
+ placeholder: defaultStagingUrl.replace(".nimbuslab.net.br", ".com.br"),
1045
+ initialValue: ""
1046
+ });
1047
+ if (pD(production))
1048
+ return production;
1049
+ productionUrl = production;
1050
+ const railwayAuthenticated = await isRailwayAuthenticated();
1051
+ if (railwayAuthenticated) {
1052
+ console.log();
1053
+ console.log(import_picocolors3.default.dim(" Railway"));
1054
+ const projects = await listRailwayProjects();
1055
+ if (type === "fast") {
1056
+ const fastProject = projects.find((p2) => p2.toLowerCase().includes("fast by nimbuslab"));
1057
+ if (fastProject) {
1058
+ railwayProject = fastProject;
1059
+ console.log(import_picocolors3.default.green(` Projeto: ${fastProject} (automatico)`));
1060
+ } else {
1061
+ console.log(import_picocolors3.default.yellow(" Projeto 'Fast by nimbuslab' nao encontrado."));
1062
+ console.log(import_picocolors3.default.dim(" Configure RAILWAY_TOKEN manualmente no .env"));
1063
+ }
1064
+ } else {
1065
+ const projectOptions = [
1066
+ ...projects.map((proj) => ({ value: proj, label: proj })),
1067
+ { value: "__new__", label: "Criar novo projeto", hint: "Abrir railway.app/new" },
1068
+ { value: "__skip__", label: "Pular", hint: "Configurar depois" }
1069
+ ];
1070
+ const selectedProject = await ve({
1071
+ message: "Projeto Railway para este SaaS:",
1072
+ options: projectOptions
1073
+ });
1074
+ if (pD(selectedProject))
1075
+ return selectedProject;
1076
+ if (selectedProject === "__new__") {
1077
+ console.log(import_picocolors3.default.yellow(" Crie o projeto em: https://railway.app/new"));
1078
+ console.log(import_picocolors3.default.dim(" Configure RAILWAY_TOKEN depois no .env"));
1079
+ } else if (selectedProject !== "__skip__") {
1080
+ railwayProject = selectedProject;
1081
+ console.log(import_picocolors3.default.green(` Projeto selecionado: ${railwayProject}`));
1082
+ }
1083
+ }
1084
+ } else {
1085
+ console.log();
1086
+ console.log(import_picocolors3.default.yellow(" Railway: nao autenticado (railway login)"));
1087
+ console.log(import_picocolors3.default.dim(" Configure RAILWAY_TOKEN manualmente no .env"));
1088
+ }
1089
+ }
1090
+ const install = await ye({
1091
+ message: "Instalar dependencias?",
1092
+ initialValue: true
1093
+ });
1094
+ if (pD(install))
1095
+ return install;
900
1096
  return {
901
1097
  name,
902
1098
  type,
903
- git: extras.git,
904
- install: extras.install
1099
+ git,
1100
+ install,
1101
+ github,
1102
+ githubOrg,
1103
+ githubDescription,
1104
+ contractNumber,
1105
+ resendApiKey,
1106
+ resendFromEmail,
1107
+ contactEmail,
1108
+ railwayProject,
1109
+ railwayToken,
1110
+ stagingUrl,
1111
+ productionUrl
905
1112
  };
906
1113
  }
907
1114
  async function createProject(config) {
908
1115
  const s = Y2();
909
- s.start("Clonando template...");
1116
+ const templateRepo = TEMPLATES[config.type];
1117
+ s.start(`Clonando template ${config.type}...`);
910
1118
  try {
911
- await $2`gh repo clone ${TEMPLATE_REPO} ${config.name} -- --depth 1`.quiet();
1119
+ await $2`gh repo clone ${templateRepo} ${config.name} -- --depth 1`.quiet();
912
1120
  await rm(join(config.name, ".git"), { recursive: true, force: true });
913
- s.stop("Template clonado");
1121
+ s.stop(`Template ${config.type} clonado`);
914
1122
  } catch (error) {
915
1123
  s.stop("Erro ao clonar template");
916
- throw new Error("Falha ao clonar template. Verifique se tem acesso ao repositorio.");
1124
+ throw new Error(`Falha ao clonar template ${templateRepo}. Verifique se tem acesso ao repositorio.`);
917
1125
  }
918
1126
  s.start("Configurando projeto...");
919
1127
  try {
@@ -933,13 +1141,44 @@ async function createProject(config) {
933
1141
  s.start("Inicializando Git...");
934
1142
  try {
935
1143
  const cwd = config.name;
936
- await $2`git init`.cwd(cwd).quiet();
1144
+ await $2`git init -b main`.cwd(cwd).quiet();
937
1145
  await $2`git add -A`.cwd(cwd).quiet();
938
1146
  await $2`git commit -m "chore: setup inicial via nimbus create"`.cwd(cwd).quiet();
939
- s.stop("Git inicializado");
1147
+ await $2`git checkout -b staging`.cwd(cwd).quiet();
1148
+ await $2`git checkout -b develop`.cwd(cwd).quiet();
1149
+ s.stop("Git inicializado (main -> staging -> develop)");
940
1150
  } catch (error) {
941
1151
  s.stop("Erro ao inicializar Git");
942
1152
  }
1153
+ if (config.github) {
1154
+ s.start("Criando repositorio no GitHub...");
1155
+ try {
1156
+ const cwd = config.name;
1157
+ const repoName = config.githubOrg ? `${config.githubOrg}/${config.name}` : config.name;
1158
+ const visibility = config.githubOrg === "fast-by-nimbuslab" ? "--private" : "--public";
1159
+ await $2`gh repo create ${repoName} ${visibility} --description ${config.githubDescription} --source . --remote origin`.cwd(cwd).quiet();
1160
+ await $2`git checkout main`.cwd(cwd).quiet();
1161
+ await $2`git push -u origin main`.cwd(cwd).quiet();
1162
+ await $2`git checkout staging`.cwd(cwd).quiet();
1163
+ await $2`git push -u origin staging`.cwd(cwd).quiet();
1164
+ await $2`git checkout develop`.cwd(cwd).quiet();
1165
+ await $2`git push -u origin develop`.cwd(cwd).quiet();
1166
+ s.stop(`GitHub: ${repoName} criado`);
1167
+ } catch (error) {
1168
+ s.stop("Erro ao criar repositorio GitHub");
1169
+ console.log(import_picocolors3.default.dim(" Voce pode criar manualmente com: gh repo create"));
1170
+ }
1171
+ }
1172
+ }
1173
+ if (config.resendApiKey || config.stagingUrl) {
1174
+ s.start("Gerando arquivo .env...");
1175
+ try {
1176
+ const envContent = generateEnvFile(config);
1177
+ await Bun.write(`${config.name}/.env`, envContent);
1178
+ s.stop("Arquivo .env criado");
1179
+ } catch (error) {
1180
+ s.stop("Erro ao criar .env");
1181
+ }
943
1182
  }
944
1183
  if (config.install) {
945
1184
  s.start("Instalando dependencias (pode demorar)...");
@@ -951,6 +1190,45 @@ async function createProject(config) {
951
1190
  }
952
1191
  }
953
1192
  }
1193
+ function generateEnvFile(config) {
1194
+ const lines = [
1195
+ "# Gerado automaticamente pelo nimbus-cli",
1196
+ "# Nao commitar este arquivo!",
1197
+ "",
1198
+ "# App",
1199
+ `NODE_ENV=development`,
1200
+ "",
1201
+ "# URLs",
1202
+ `NEXT_PUBLIC_APP_URL=${config.productionUrl || "http://localhost:3000"}`,
1203
+ `STAGING_URL=${config.stagingUrl || ""}`,
1204
+ `PRODUCTION_URL=${config.productionUrl || ""}`,
1205
+ "",
1206
+ "# Resend (Email)",
1207
+ `RESEND_API_KEY=${config.resendApiKey || ""}`,
1208
+ `RESEND_FROM_EMAIL=${config.resendFromEmail || ""}`,
1209
+ `CONTACT_EMAIL=${config.contactEmail || ""}`
1210
+ ];
1211
+ if (config.railwayProject || config.railwayToken) {
1212
+ lines.push("");
1213
+ lines.push("# Railway");
1214
+ if (config.railwayProject) {
1215
+ lines.push(`# Projeto: ${config.railwayProject}`);
1216
+ }
1217
+ lines.push(`RAILWAY_TOKEN=${config.railwayToken || "# Configure com: railway link"}`);
1218
+ }
1219
+ if (config.type === "fast+") {
1220
+ lines.push("");
1221
+ lines.push("# Database (fast+)");
1222
+ lines.push("DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app");
1223
+ lines.push("");
1224
+ lines.push("# Auth (fast+)");
1225
+ lines.push("BETTER_AUTH_SECRET=");
1226
+ lines.push("BETTER_AUTH_URL=http://localhost:3000");
1227
+ }
1228
+ return lines.join(`
1229
+ `) + `
1230
+ `;
1231
+ }
954
1232
  function showNextSteps(config) {
955
1233
  console.log();
956
1234
  console.log(import_picocolors3.default.bold("Proximos passos:"));
@@ -962,11 +1240,33 @@ function showNextSteps(config) {
962
1240
  console.log(` ${import_picocolors3.default.cyan("bun")} setup`);
963
1241
  console.log(` ${import_picocolors3.default.cyan("bun")} dev`);
964
1242
  console.log();
1243
+ if (config.git) {
1244
+ console.log(import_picocolors3.default.dim(" Git flow: main -> staging -> develop (branch atual)"));
1245
+ console.log();
1246
+ if (config.github) {
1247
+ const repoUrl = config.githubOrg ? `https://github.com/${config.githubOrg}/${config.name}` : `https://github.com/${config.name}`;
1248
+ console.log(import_picocolors3.default.green(` GitHub: ${repoUrl}`));
1249
+ console.log();
1250
+ } else {
1251
+ console.log(import_picocolors3.default.yellow(" Dica: Para criar repo GitHub, use 'gh repo create' ou 'bun setup'."));
1252
+ console.log();
1253
+ }
1254
+ }
965
1255
  if (config.type === "fast+") {
966
- console.log(import_picocolors3.default.dim(" Dica: Execute 'bun setup' e escolha fast+ para configurar backend"));
1256
+ console.log(import_picocolors3.default.dim(" Dica: Para fast+, configure DATABASE_URL e BETTER_AUTH_SECRET no .env"));
1257
+ if (!config.railwayToken) {
1258
+ console.log(import_picocolors3.default.dim(" Railway: Crie um projeto em https://railway.app/new"));
1259
+ }
1260
+ console.log();
1261
+ }
1262
+ if (config.resendApiKey || config.stagingUrl) {
1263
+ console.log(import_picocolors3.default.green(" .env configurado com sucesso!"));
1264
+ console.log();
1265
+ } else {
1266
+ console.log(import_picocolors3.default.yellow(" Dica: Configure .env manualmente ou use 'bun setup'."));
967
1267
  console.log();
968
1268
  }
969
- console.log(import_picocolors3.default.dim(" Documentacao: https://github.com/nimbuslab/fast-by-nimbuslab"));
1269
+ console.log(import_picocolors3.default.dim(" Documentacao: https://github.com/nimbuslab-templates"));
970
1270
  console.log();
971
1271
  }
972
1272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbuslab/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.2",
4
4
  "description": "CLI para criar projetos nimbuslab",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,13 +4,79 @@ import { $} from "bun"
4
4
  import { rm } from "node:fs/promises"
5
5
  import { join } from "node:path"
6
6
 
7
- const TEMPLATE_REPO = "nimbuslab-templates/fast-by-nimbuslab"
7
+ // Templates separados por tipo
8
+ const TEMPLATES = {
9
+ "fast": "nimbuslab-templates/fast-template",
10
+ "fast+": "nimbuslab-templates/fastplus-template",
11
+ }
8
12
 
9
13
  interface ProjectConfig {
10
14
  name: string
11
15
  type: "fast" | "fast+"
12
16
  git: boolean
13
17
  install: boolean
18
+ github: boolean
19
+ githubOrg: string | null
20
+ githubDescription: string
21
+ // M26: Número do contrato (fast only)
22
+ contractNumber: string
23
+ // M21-M23: Configs de infra
24
+ resendApiKey: string
25
+ resendFromEmail: string
26
+ contactEmail: string
27
+ // M30: Railway via CLI
28
+ railwayProject: string
29
+ railwayToken: string
30
+ stagingUrl: string
31
+ productionUrl: string
32
+ }
33
+
34
+ // M30: Verificar e instalar Railway CLI
35
+ async function ensureRailwayCli(): Promise<boolean> {
36
+ const checkCmd = process.platform === "win32" ? "where" : "which"
37
+ const hasRailway = await $`${checkCmd} railway`.quiet().then(() => true).catch(() => false)
38
+
39
+ if (hasRailway) return true
40
+
41
+ console.log(pc.yellow("Railway CLI nao encontrado. Instalando..."))
42
+ console.log()
43
+
44
+ try {
45
+ if (process.platform === "win32") {
46
+ await $`powershell -c "iwr https://railway.app/install.ps1 -useb | iex"`.quiet()
47
+ } else {
48
+ await $`curl -fsSL https://railway.app/install.sh | sh`.quiet()
49
+ }
50
+ console.log(pc.green("Railway CLI instalado com sucesso!"))
51
+ return true
52
+ } catch (error) {
53
+ console.log(pc.red("Erro ao instalar Railway CLI."))
54
+ console.log(pc.dim("Instale manualmente: https://docs.railway.app/guides/cli"))
55
+ return false
56
+ }
57
+ }
58
+
59
+ // M30: Listar projetos Railway via CLI
60
+ async function listRailwayProjects(): Promise<string[]> {
61
+ try {
62
+ const result = await $`railway list`.text()
63
+ // Parse output: primeira linha é o team, demais são projetos
64
+ const lines = result.trim().split("\n").filter(l => l.trim())
65
+ // Remove primeira linha (team name) e extrai nomes dos projetos
66
+ return lines.slice(1).map(l => l.trim())
67
+ } catch {
68
+ return []
69
+ }
70
+ }
71
+
72
+ // M30: Verificar autenticacao Railway
73
+ async function isRailwayAuthenticated(): Promise<boolean> {
74
+ try {
75
+ await $`railway whoami`.quiet()
76
+ return true
77
+ } catch {
78
+ return false
79
+ }
14
80
  }
15
81
 
16
82
  export async function create(args: string[]) {
@@ -60,6 +126,17 @@ export async function create(args: string[]) {
60
126
  process.exit(1)
61
127
  }
62
128
 
129
+ // M30: Verificar/instalar Railway CLI
130
+ const hasRailway = await ensureRailwayCli()
131
+ if (hasRailway) {
132
+ const railwayAuth = await isRailwayAuthenticated()
133
+ if (!railwayAuth) {
134
+ console.log(pc.yellow("Railway CLI nao autenticado."))
135
+ console.log(pc.dim("Execute: railway login"))
136
+ console.log()
137
+ }
138
+ }
139
+
63
140
  const hasYes = args.includes("-y") || args.includes("--yes")
64
141
  const projectName = args.find(a => !a.startsWith("-"))
65
142
 
@@ -68,16 +145,29 @@ export async function create(args: string[]) {
68
145
  let config: ProjectConfig | symbol
69
146
 
70
147
  if (hasYes && projectName) {
71
- // Modo automatico com defaults
148
+ // Modo automatico com defaults (sem configs de infra)
72
149
  config = {
73
150
  name: projectName,
74
151
  type: "fast",
75
152
  git: true,
76
153
  install: true,
154
+ github: false,
155
+ githubOrg: null,
156
+ githubDescription: "",
157
+ contractNumber: "",
158
+ resendApiKey: "",
159
+ resendFromEmail: "",
160
+ contactEmail: "",
161
+ railwayProject: "",
162
+ railwayToken: "",
163
+ stagingUrl: "",
164
+ productionUrl: "",
77
165
  }
78
166
  console.log(pc.dim(` Projeto: ${projectName}`))
79
167
  console.log(pc.dim(` Tipo: fast`))
80
168
  console.log(pc.dim(` Git: sim`))
169
+ console.log(pc.dim(` GitHub: nao`))
170
+ console.log(pc.dim(` Infra: configurar depois`))
81
171
  console.log(pc.dim(` Instalar: sim`))
82
172
  console.log()
83
173
  } else {
@@ -130,41 +220,237 @@ async function promptConfig(initialName?: string): Promise<ProjectConfig | symbo
130
220
 
131
221
  if (p.isCancel(type)) return type
132
222
 
133
- const extras = await p.group({
134
- git: () =>
135
- p.confirm({
136
- message: "Inicializar repositorio Git?",
137
- initialValue: true,
138
- }),
139
- install: () =>
140
- p.confirm({
141
- message: "Instalar dependencias?",
142
- initialValue: true,
143
- }),
223
+ const git = await p.confirm({
224
+ message: "Inicializar repositorio Git?",
225
+ initialValue: true,
226
+ })
227
+
228
+ if (p.isCancel(git)) return git
229
+
230
+ // GitHub options (only if git is enabled)
231
+ let github = false
232
+ let githubOrg: string | null = null
233
+ let githubDescription = ""
234
+
235
+ if (git) {
236
+ const createGithub = await p.confirm({
237
+ message: "Criar repositorio no GitHub?",
238
+ initialValue: false,
239
+ })
240
+
241
+ if (p.isCancel(createGithub)) return createGithub
242
+
243
+ github = createGithub as boolean
244
+
245
+ if (github) {
246
+ // M13: Selecao de organizacao GitHub
247
+ const org = await p.select({
248
+ message: "Organizacao GitHub:",
249
+ options: [
250
+ { value: "nimbuslab", label: "nimbuslab", hint: "Org principal" },
251
+ { value: "fast-by-nimbuslab", label: "fast-by-nimbuslab", hint: "Projetos de clientes" },
252
+ { value: "nimbuslab-templates", label: "nimbuslab-templates", hint: "Templates" },
253
+ { value: null, label: "Pessoal", hint: "Sem organizacao" },
254
+ ],
255
+ })
256
+
257
+ if (p.isCancel(org)) return org
258
+ githubOrg = org as string | null
259
+
260
+ // M14: Descricao do repo
261
+ const description = await p.text({
262
+ message: "Descricao do repositorio:",
263
+ placeholder: "Landing page para cliente X",
264
+ initialValue: type === "fast" ? "Landing page fast by nimbuslab" : "SaaS fast+ by nimbuslab",
265
+ })
266
+
267
+ if (p.isCancel(description)) return description
268
+ githubDescription = description as string
269
+ }
270
+ }
271
+
272
+ // M26: Número do contrato (apenas para fast)
273
+ let contractNumber = ""
274
+ if (type === "fast") {
275
+ const contract = await p.text({
276
+ message: "Numero do contrato (ex: 001):",
277
+ placeholder: "001",
278
+ validate: (v) => v ? undefined : "Numero do contrato e obrigatorio para fast",
279
+ })
280
+ if (p.isCancel(contract)) return contract
281
+ contractNumber = contract as string
282
+ }
283
+
284
+ // M21-M23: Configuracoes de infra (opcional)
285
+ const configureInfra = await p.confirm({
286
+ message: "Configurar infra agora? (Resend, URLs)",
287
+ initialValue: true,
288
+ })
289
+
290
+ if (p.isCancel(configureInfra)) return configureInfra
291
+
292
+ let resendApiKey = ""
293
+ let resendFromEmail = ""
294
+ let contactEmail = ""
295
+ let railwayProject = ""
296
+ let railwayToken = ""
297
+ let stagingUrl = ""
298
+ let productionUrl = ""
299
+
300
+ if (configureInfra) {
301
+ const currentYear = new Date().getFullYear().toString().slice(-2) // 25, 26, etc
302
+
303
+ // M26: URLs padrão baseadas no tipo
304
+ // fast: fast-{contrato}-{ano}.nimbuslab.net.br
305
+ // fast+: {nome}.nimbuslab.net.br
306
+ const defaultStagingUrl = type === "fast"
307
+ ? `https://fast-${contractNumber}-${currentYear}.nimbuslab.net.br`
308
+ : `https://${name}.nimbuslab.net.br`
309
+
310
+ // M27: Emails padrão
311
+ // from: no-reply@nimbuslab.com.br
312
+ // to fast: fast@nimbuslab.com.br
313
+ // to fast+: suporte@nimbuslab.com.br
314
+ const defaultFromEmail = "no-reply@nimbuslab.com.br"
315
+ const defaultContactEmail = type === "fast"
316
+ ? "fast@nimbuslab.com.br"
317
+ : "suporte@nimbuslab.com.br"
318
+
319
+ // M27: Resend config
320
+ console.log()
321
+ console.log(pc.dim(" Resend (Email)"))
322
+
323
+ const resendKey = await p.text({
324
+ message: "RESEND_API_KEY:",
325
+ placeholder: "re_xxxxxxxxxxxx",
326
+ })
327
+ if (p.isCancel(resendKey)) return resendKey
328
+ resendApiKey = resendKey as string
329
+
330
+ const fromEmail = await p.text({
331
+ message: "Email de envio (from):",
332
+ placeholder: defaultFromEmail,
333
+ initialValue: defaultFromEmail,
334
+ })
335
+ if (p.isCancel(fromEmail)) return fromEmail
336
+ resendFromEmail = fromEmail as string
337
+
338
+ const contact = await p.text({
339
+ message: "Email de contato (recebe formularios):",
340
+ placeholder: defaultContactEmail,
341
+ initialValue: defaultContactEmail,
342
+ })
343
+ if (p.isCancel(contact)) return contact
344
+ contactEmail = contact as string
345
+
346
+ // M26: URLs
347
+ console.log()
348
+ console.log(pc.dim(" URLs do projeto"))
349
+
350
+ const staging = await p.text({
351
+ message: "URL de staging:",
352
+ placeholder: defaultStagingUrl,
353
+ initialValue: defaultStagingUrl,
354
+ })
355
+ if (p.isCancel(staging)) return staging
356
+ stagingUrl = staging as string
357
+
358
+ const production = await p.text({
359
+ message: "URL de producao:",
360
+ placeholder: defaultStagingUrl.replace('.nimbuslab.net.br', '.com.br'),
361
+ initialValue: "",
362
+ })
363
+ if (p.isCancel(production)) return production
364
+ productionUrl = production as string
365
+
366
+ // M30: Railway via CLI
367
+ const railwayAuthenticated = await isRailwayAuthenticated()
368
+
369
+ if (railwayAuthenticated) {
370
+ console.log()
371
+ console.log(pc.dim(" Railway"))
372
+
373
+ const projects = await listRailwayProjects()
374
+
375
+ if (type === "fast") {
376
+ // Fast usa projeto compartilhado "Fast by nimbuslab"
377
+ const fastProject = projects.find(p => p.toLowerCase().includes("fast by nimbuslab"))
378
+ if (fastProject) {
379
+ railwayProject = fastProject
380
+ console.log(pc.green(` Projeto: ${fastProject} (automatico)`))
381
+ } else {
382
+ console.log(pc.yellow(" Projeto 'Fast by nimbuslab' nao encontrado."))
383
+ console.log(pc.dim(" Configure RAILWAY_TOKEN manualmente no .env"))
384
+ }
385
+ } else {
386
+ // Fast+ pode escolher projeto existente ou criar novo
387
+ const projectOptions = [
388
+ ...projects.map(proj => ({ value: proj, label: proj })),
389
+ { value: "__new__", label: "Criar novo projeto", hint: "Abrir railway.app/new" },
390
+ { value: "__skip__", label: "Pular", hint: "Configurar depois" },
391
+ ]
392
+
393
+ const selectedProject = await p.select({
394
+ message: "Projeto Railway para este SaaS:",
395
+ options: projectOptions,
396
+ })
397
+
398
+ if (p.isCancel(selectedProject)) return selectedProject
399
+
400
+ if (selectedProject === "__new__") {
401
+ console.log(pc.yellow(" Crie o projeto em: https://railway.app/new"))
402
+ console.log(pc.dim(" Configure RAILWAY_TOKEN depois no .env"))
403
+ } else if (selectedProject !== "__skip__") {
404
+ railwayProject = selectedProject as string
405
+ console.log(pc.green(` Projeto selecionado: ${railwayProject}`))
406
+ }
407
+ }
408
+ } else {
409
+ console.log()
410
+ console.log(pc.yellow(" Railway: nao autenticado (railway login)"))
411
+ console.log(pc.dim(" Configure RAILWAY_TOKEN manualmente no .env"))
412
+ }
413
+ }
414
+
415
+ const install = await p.confirm({
416
+ message: "Instalar dependencias?",
417
+ initialValue: true,
144
418
  })
145
419
 
146
- if (p.isCancel(extras)) return extras
420
+ if (p.isCancel(install)) return install
147
421
 
148
422
  return {
149
423
  name: name as string,
150
424
  type: type as "fast" | "fast+",
151
- git: extras.git as boolean,
152
- install: extras.install as boolean,
425
+ git: git as boolean,
426
+ install: install as boolean,
427
+ github,
428
+ githubOrg,
429
+ githubDescription,
430
+ contractNumber,
431
+ resendApiKey,
432
+ resendFromEmail,
433
+ contactEmail,
434
+ railwayProject,
435
+ railwayToken,
436
+ stagingUrl,
437
+ productionUrl,
153
438
  }
154
439
  }
155
440
 
156
441
  async function createProject(config: ProjectConfig) {
157
442
  const s = p.spinner()
158
443
 
159
- // Clone template usando gh (funciona com token OAuth)
160
- s.start("Clonando template...")
444
+ // Clone template baseado no tipo (fast ou fast+)
445
+ const templateRepo = TEMPLATES[config.type]
446
+ s.start(`Clonando template ${config.type}...`)
161
447
  try {
162
- await $`gh repo clone ${TEMPLATE_REPO} ${config.name} -- --depth 1`.quiet()
448
+ await $`gh repo clone ${templateRepo} ${config.name} -- --depth 1`.quiet()
163
449
  await rm(join(config.name, ".git"), { recursive: true, force: true })
164
- s.stop("Template clonado")
450
+ s.stop(`Template ${config.type} clonado`)
165
451
  } catch (error) {
166
452
  s.stop("Erro ao clonar template")
167
- throw new Error("Falha ao clonar template. Verifique se tem acesso ao repositorio.")
453
+ throw new Error(`Falha ao clonar template ${templateRepo}. Verifique se tem acesso ao repositorio.`)
168
454
  }
169
455
 
170
456
  // Update package.json
@@ -205,6 +491,48 @@ async function createProject(config: ProjectConfig) {
205
491
  } catch (error) {
206
492
  s.stop("Erro ao inicializar Git")
207
493
  }
494
+
495
+ // Create GitHub repo if requested (M13 + M14)
496
+ if (config.github) {
497
+ s.start("Criando repositorio no GitHub...")
498
+ try {
499
+ const cwd = config.name
500
+ const repoName = config.githubOrg
501
+ ? `${config.githubOrg}/${config.name}`
502
+ : config.name
503
+
504
+ // Create repo with description (private by default for client projects)
505
+ const visibility = config.githubOrg === "fast-by-nimbuslab" ? "--private" : "--public"
506
+
507
+ // Criar repo sem push automático
508
+ await $`gh repo create ${repoName} ${visibility} --description ${config.githubDescription} --source . --remote origin`.cwd(cwd).quiet()
509
+
510
+ // Push todas as branches na ordem correta: main -> staging -> develop
511
+ await $`git checkout main`.cwd(cwd).quiet()
512
+ await $`git push -u origin main`.cwd(cwd).quiet()
513
+ await $`git checkout staging`.cwd(cwd).quiet()
514
+ await $`git push -u origin staging`.cwd(cwd).quiet()
515
+ await $`git checkout develop`.cwd(cwd).quiet()
516
+ await $`git push -u origin develop`.cwd(cwd).quiet()
517
+
518
+ s.stop(`GitHub: ${repoName} criado`)
519
+ } catch (error) {
520
+ s.stop("Erro ao criar repositorio GitHub")
521
+ console.log(pc.dim(" Voce pode criar manualmente com: gh repo create"))
522
+ }
523
+ }
524
+ }
525
+
526
+ // M23: Gerar .env se configs foram fornecidas
527
+ if (config.resendApiKey || config.stagingUrl) {
528
+ s.start("Gerando arquivo .env...")
529
+ try {
530
+ const envContent = generateEnvFile(config)
531
+ await Bun.write(`${config.name}/.env`, envContent)
532
+ s.stop("Arquivo .env criado")
533
+ } catch (error) {
534
+ s.stop("Erro ao criar .env")
535
+ }
208
536
  }
209
537
 
210
538
  // Install deps
@@ -219,6 +547,50 @@ async function createProject(config: ProjectConfig) {
219
547
  }
220
548
  }
221
549
 
550
+ // M23: Gerar conteudo do .env
551
+ function generateEnvFile(config: ProjectConfig): string {
552
+ const lines = [
553
+ "# Gerado automaticamente pelo nimbus-cli",
554
+ "# Nao commitar este arquivo!",
555
+ "",
556
+ "# App",
557
+ `NODE_ENV=development`,
558
+ "",
559
+ "# URLs",
560
+ `NEXT_PUBLIC_APP_URL=${config.productionUrl || "http://localhost:3000"}`,
561
+ `STAGING_URL=${config.stagingUrl || ""}`,
562
+ `PRODUCTION_URL=${config.productionUrl || ""}`,
563
+ "",
564
+ "# Resend (Email)",
565
+ `RESEND_API_KEY=${config.resendApiKey || ""}`,
566
+ `RESEND_FROM_EMAIL=${config.resendFromEmail || ""}`,
567
+ `CONTACT_EMAIL=${config.contactEmail || ""}`,
568
+ ]
569
+
570
+ // Railway (projeto e token)
571
+ if (config.railwayProject || config.railwayToken) {
572
+ lines.push("")
573
+ lines.push("# Railway")
574
+ if (config.railwayProject) {
575
+ lines.push(`# Projeto: ${config.railwayProject}`)
576
+ }
577
+ lines.push(`RAILWAY_TOKEN=${config.railwayToken || "# Configure com: railway link"}`)
578
+ }
579
+
580
+ // Fast+ specific vars
581
+ if (config.type === "fast+") {
582
+ lines.push("")
583
+ lines.push("# Database (fast+)")
584
+ lines.push("DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app")
585
+ lines.push("")
586
+ lines.push("# Auth (fast+)")
587
+ lines.push("BETTER_AUTH_SECRET=")
588
+ lines.push("BETTER_AUTH_URL=http://localhost:3000")
589
+ }
590
+
591
+ return lines.join("\n") + "\n"
592
+ }
593
+
222
594
  function showNextSteps(config: ProjectConfig) {
223
595
  console.log()
224
596
  console.log(pc.bold("Proximos passos:"))
@@ -237,18 +609,37 @@ function showNextSteps(config: ProjectConfig) {
237
609
  if (config.git) {
238
610
  console.log(pc.dim(" Git flow: main -> staging -> develop (branch atual)"))
239
611
  console.log()
612
+
613
+ // GitHub info
614
+ if (config.github) {
615
+ const repoUrl = config.githubOrg
616
+ ? `https://github.com/${config.githubOrg}/${config.name}`
617
+ : `https://github.com/${config.name}`
618
+ console.log(pc.green(` GitHub: ${repoUrl}`))
619
+ console.log()
620
+ } else {
621
+ console.log(pc.yellow(" Dica: Para criar repo GitHub, use 'gh repo create' ou 'bun setup'."))
622
+ console.log()
623
+ }
240
624
  }
241
625
 
242
626
  if (config.type === "fast+") {
243
- console.log(pc.dim(" Dica: Execute 'bun setup' e escolha fast+ para configurar backend"))
627
+ console.log(pc.dim(" Dica: Para fast+, configure DATABASE_URL e BETTER_AUTH_SECRET no .env"))
628
+ if (!config.railwayToken) {
629
+ console.log(pc.dim(" Railway: Crie um projeto em https://railway.app/new"))
630
+ }
244
631
  console.log()
245
632
  }
246
633
 
247
- // Dica para configurar GitHub no bun setup
248
- console.log(pc.yellow(" Dica: No 'bun setup', voce pode criar o repositorio GitHub."))
249
- console.log(pc.dim(" Copie a URL gerada para configurar o remote do projeto."))
250
- console.log()
634
+ // Info sobre .env
635
+ if (config.resendApiKey || config.stagingUrl) {
636
+ console.log(pc.green(" .env configurado com sucesso!"))
637
+ console.log()
638
+ } else {
639
+ console.log(pc.yellow(" Dica: Configure .env manualmente ou use 'bun setup'."))
640
+ console.log()
641
+ }
251
642
 
252
- console.log(pc.dim(" Documentacao: https://github.com/nimbuslab/fast-by-nimbuslab"))
643
+ console.log(pc.dim(" Documentacao: https://github.com/nimbuslab-templates"))
253
644
  console.log()
254
645
  }