fwinit 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -5
  2. package/dist/index.js +461 -16
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -25,6 +25,10 @@ fwinit
25
25
  fwinit fastify mi-api
26
26
  fwinit aspnet mi-app
27
27
  fwinit react-native mi-mobile
28
+ fwinit flutter mi-app
29
+
30
+ # Flutter: saltear la pregunta de organización (bundle id)
31
+ fwinit flutter mi-app -o ar.miempresa
28
32
 
29
33
  # Crear un proyecto completo: backend + frontend (+ db incluida)
30
34
  fwinit fullstack mi-proyecto
@@ -75,7 +79,7 @@ fwinit --help
75
79
  fwinit fullstack mi-proyecto
76
80
  ```
77
81
 
78
- Pregunta qué backend querés (ASP.NET, Express, Fastify, Node.js Vanilla) y qué frontend (React Native). Genera:
82
+ Pregunta qué backend querés (ASP.NET, Express, Fastify, Node.js Vanilla) y qué frontend (React Native, Flutter, Astro, Tauri). Genera:
79
83
 
80
84
  ```
81
85
  mi-proyecto/
@@ -104,13 +108,14 @@ fwinit express mi-api --no-backend
104
108
  fwinit express mi-api --backend
105
109
  ```
106
110
 
107
- Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
111
+ Igual para frontend: `fwinit react-native mi-app` o `fwinit flutter mi-app` pregunta por `frontend/`.
108
112
 
109
113
  ## Flags
110
114
 
111
115
  | Flag | Descripción |
112
116
  |------|-------------|
113
117
  | `-p, --pm <manager>` | Package manager: `npm`, `pnpm` o `bun` (saltea la pregunta) |
118
+ | `-o, --org <domain>` | Organización reverse-domain del bundle id. Solo aplica a Flutter (saltea la pregunta; default `com.example`) |
114
119
  | `--backend` | Empaquetar el código en `backend/` (solo modo single, saltea la pregunta) |
115
120
  | `--no-backend` | Dejar el código en la raíz del proyecto (saltea la pregunta) |
116
121
  | `-b, --backend-template <tpl>` | Backend a usar en fullstack (saltea la pregunta) |
@@ -129,8 +134,11 @@ Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
129
134
  | `astro` | frontend | Landing estática con Astro 7 y TypeScript | node |
130
135
  | `tauri` | frontend | Dashboard de escritorio con React, Vite y Tauri 2 | node |
131
136
  | `react-native` | frontend | App móvil con React Native, Expo y TypeScript | node |
137
+ | `flutter` | frontend | App multiplataforma con Flutter, Clean Architecture y Riverpod | flutter |
138
+
139
+ La capa y el runtime son metadata de cada template (`layer` y `runtime` en `templates/<TEMPLATE>/template.json`), pero **el template se registra en `cli/src/templates.ts`**: el `template.json` documenta y aporta los comandos de `postInit`, no resuelve el alias. Para sumar un template nuevo alcanzan la carpeta, su `template.json` y su entrada en `TEMPLATES`.
132
140
 
133
- La capa es metadata de cada template (`layer` en `templates/<TEMPLATE>/template.json`): para sumar un frontend nuevo (Astro, Next.js...), alcanza con agregar la carpeta del template y su `layer`. Los templates se clasifican por capa en la metadata, no por estructura de carpetas.
141
+ Flutter es el único template que pregunta una identidad propia: la organización reverse-domain (default `com.example`) para armar los bundle ids nativos. `-o, --org` la saltea; en modo scripting/CI se usa el default.
134
142
 
135
143
  El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si elegís npm o pnpm: scripts con `tsx`, tests con `vitest` y sin `bun-types`.
136
144
 
@@ -138,10 +146,10 @@ El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si
138
146
 
139
147
  1. Descarga el ZIP del repositorio de templates desde GitHub (una sola vez, también en fullstack)
140
148
  2. Extrae y copia el template (o los dos, `backend/` + `frontend/`) al proyecto
141
- 3. Sustituye el nombre del proyecto (package.json, app.json de Expo, proyectos ASP.NET con "Example")
149
+ 3. Sustituye el nombre del proyecto (package.json, app.json de Expo, proyectos ASP.NET con "Example", y en Flutter el paquete Dart, los imports, los bundle ids nativos y los nombres de binario)
142
150
  4. Si corresponde, porta el template de runtime bun a node (npm/pnpm)
143
151
  5. Instala `.opencode/`, `.pi/` y `.agents/` con las skills `create-specs` y `create-specs-from-code`, sus comandos (`/create-specs` rutea entre ambos según el estado del repo) y la biblioteca de diseño (mismo bundle para OpenCode, Pi/Gentle Shell y Agent Skills: elegí el agente que quieras)
144
- 6. Limpia los lock files (cada proyecto instala los suyos)
152
+ 6. Limpia los lock files (cada proyecto instala los suyos; incluye `pubspec.lock` en Flutter)
145
153
  7. Si lo elegís (o pasás `--git`), inicializa un repositorio git en la raíz
146
154
  8. Te muestra los próximos pasos (install y dev de cada carpeta)
147
155
 
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import ora from "ora";
6
6
  import chalk from "chalk";
7
7
  import { readFileSync } from "fs";
8
8
  import { mkdir as mkdir3, cp as cp3, readFile as readFile3 } from "fs/promises";
9
- import { join as join3, basename } from "path";
9
+ import { join as join3, basename as basename2 } from "path";
10
10
 
11
11
  // src/templates.ts
12
12
  var TEMPLATES = [
@@ -65,6 +65,14 @@ var TEMPLATES = [
65
65
  folder: "REACT-NATIVE",
66
66
  runtime: "node",
67
67
  layer: "frontend"
68
+ },
69
+ {
70
+ name: "Flutter",
71
+ alias: ["flutter", "dart", "fwiflu"],
72
+ description: "App multiplataforma con Flutter, Clean Architecture y Riverpod",
73
+ folder: "FLUTTER",
74
+ runtime: "flutter",
75
+ layer: "frontend"
68
76
  }
69
77
  ];
70
78
  function getTemplatesByLayer(layer) {
@@ -79,10 +87,191 @@ function findTemplate(input) {
79
87
 
80
88
  // src/downloader.ts
81
89
  import AdmZip from "adm-zip";
82
- import { mkdir, rm, cp, readdir, readFile, writeFile, rename } from "fs/promises";
83
- import { join, extname } from "path";
90
+ import { mkdir, rm, cp, readdir, readFile, writeFile, rename, stat } from "fs/promises";
91
+ import { join, extname, basename, dirname, relative, sep } from "path";
84
92
  import { tmpdir } from "os";
85
93
  import { randomBytes } from "crypto";
94
+
95
+ // src/flutter-naming.ts
96
+ var FLUTTER_ORG_DEFAULT = "com.example";
97
+ var DART_KEYWORDS = /* @__PURE__ */ new Set([
98
+ "abstract",
99
+ "as",
100
+ "assert",
101
+ "async",
102
+ "await",
103
+ "break",
104
+ "case",
105
+ "catch",
106
+ "class",
107
+ "const",
108
+ "continue",
109
+ "covariant",
110
+ "default",
111
+ "deferred",
112
+ "do",
113
+ "dynamic",
114
+ "else",
115
+ "enum",
116
+ "export",
117
+ "extends",
118
+ "extension",
119
+ "external",
120
+ "factory",
121
+ "final",
122
+ "finally",
123
+ "for",
124
+ "get",
125
+ "hide",
126
+ "if",
127
+ "implements",
128
+ "import",
129
+ "in",
130
+ "interface",
131
+ "is",
132
+ "late",
133
+ "library",
134
+ "mixin",
135
+ "new",
136
+ "on",
137
+ "operator",
138
+ "part",
139
+ "required",
140
+ "rethrow",
141
+ "return",
142
+ "set",
143
+ "show",
144
+ "static",
145
+ "super",
146
+ "switch",
147
+ "sync",
148
+ "this",
149
+ "throw",
150
+ "try",
151
+ "typedef",
152
+ "var",
153
+ "void",
154
+ "while",
155
+ "with",
156
+ "yield"
157
+ ]);
158
+ var FLUTTER_SDK_PACKAGES = /* @__PURE__ */ new Set([
159
+ "flutter",
160
+ "flutter_test",
161
+ "flutter_localizations",
162
+ "flutter_web_plugins",
163
+ "sky_engine"
164
+ ]);
165
+ var TEMPLATE_DEPENDENCIES = /* @__PURE__ */ new Set([
166
+ "cupertino_icons",
167
+ "flutter_riverpod",
168
+ "go_router",
169
+ "dio",
170
+ "freezed_annotation",
171
+ "json_annotation",
172
+ "shared_preferences",
173
+ "flutter_secure_storage",
174
+ "logger",
175
+ "intl",
176
+ "flutter_lints",
177
+ "build_runner",
178
+ "freezed",
179
+ "json_serializable",
180
+ "mocktail"
181
+ ]);
182
+ var RESERVED_DART_PACKAGE_NAMES = [
183
+ DART_KEYWORDS,
184
+ FLUTTER_SDK_PACKAGES,
185
+ TEMPLATE_DEPENDENCIES
186
+ ];
187
+ function isReservedDartPackageName(name) {
188
+ return RESERVED_DART_PACKAGE_NAMES.some((reserved) => reserved.has(name));
189
+ }
190
+ function toDartPackageName(projectName) {
191
+ let name = projectName.trim().toLowerCase();
192
+ name = name.replace(/[-_.\s]+/g, "_");
193
+ name = name.replace(/[^a-z0-9_]/g, "");
194
+ name = name.replace(/_+/g, "_").replace(/^_+|_+$/g, "");
195
+ if (name === "") {
196
+ return "app";
197
+ }
198
+ if (!/^[a-z]/.test(name)) {
199
+ name = `app_${name}`;
200
+ }
201
+ if (isReservedDartPackageName(name)) {
202
+ name = `${name}_app`;
203
+ }
204
+ return name;
205
+ }
206
+ function normalizeOrg(org) {
207
+ return org.trim().toLowerCase();
208
+ }
209
+ function isValidOrg(org) {
210
+ return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(org);
211
+ }
212
+ function checkOrg(value) {
213
+ if (isValidOrg(normalizeOrg(value))) {
214
+ return true;
215
+ }
216
+ return "Organizaci\xF3n inv\xE1lida. Us\xE1 formato reverse-domain en min\xFAsculas, por ejemplo com.example o ar.miempresa";
217
+ }
218
+
219
+ // src/dart-directives.ts
220
+ var LIBRARY_RE = /^library\s*(?:['"]([^'"]*)['"])?\s*;$/;
221
+ var DIRECTIVE_URI_RE = /^(import|export|part)\s+(['"])(.+?)\2/;
222
+ var GROUP_LIBRARY = 0;
223
+ var GROUP_DART = 1;
224
+ var GROUP_PACKAGE = 2;
225
+ var GROUP_OTHER = 3;
226
+ function parseDirective(line) {
227
+ const trimmed = line.trim();
228
+ const library = trimmed.match(LIBRARY_RE);
229
+ if (library) {
230
+ return { group: GROUP_LIBRARY, uri: library[1] ?? "" };
231
+ }
232
+ const directive = trimmed.match(DIRECTIVE_URI_RE);
233
+ if (!directive) return void 0;
234
+ const uri = directive[3];
235
+ let group = GROUP_OTHER;
236
+ if (uri.startsWith("dart:")) {
237
+ group = GROUP_DART;
238
+ } else if (uri.startsWith("package:")) {
239
+ group = GROUP_PACKAGE;
240
+ }
241
+ return { group, uri };
242
+ }
243
+ function compareKeys(a, b) {
244
+ if (a.group !== b.group) return a.group - b.group;
245
+ return a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0;
246
+ }
247
+ function sortDartDirectiveRuns(content) {
248
+ const lines = content.split("\n");
249
+ const result = lines.slice();
250
+ let i = 0;
251
+ while (i < lines.length) {
252
+ const key = parseDirective(lines[i]);
253
+ if (!key) {
254
+ i++;
255
+ continue;
256
+ }
257
+ let j = i;
258
+ const run = [];
259
+ while (j < lines.length) {
260
+ const runKey = parseDirective(lines[j]);
261
+ if (!runKey) break;
262
+ run.push({ line: lines[j], key: runKey });
263
+ j++;
264
+ }
265
+ run.sort((a, b) => compareKeys(a.key, b.key));
266
+ for (let k = 0; k < run.length; k++) {
267
+ result[i + k] = run[k].line;
268
+ }
269
+ i = j;
270
+ }
271
+ return result.join("\n");
272
+ }
273
+
274
+ // src/downloader.ts
86
275
  var GITHUB_REPO = "orlandotellez/fwinit";
87
276
  var GITHUB_BRANCH = "main";
88
277
  async function downloadAndExtractRepo() {
@@ -115,8 +304,25 @@ async function downloadAndExtract(templateFolder) {
115
304
  }
116
305
  return { tempDir, templatePath, repoRoot };
117
306
  }
307
+ var COPY_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
308
+ ".git",
309
+ ".dart_tool",
310
+ ".idea",
311
+ ".atl",
312
+ "node_modules",
313
+ "build"
314
+ ]);
118
315
  async function copyTemplate(source, destination) {
119
- await cp(source, destination, { recursive: true });
316
+ await cp(source, destination, {
317
+ recursive: true,
318
+ // Solo se descartan directorios con esos nombres: un archivo homónimo
319
+ // (p. ej. un script llamado `build`) se copia igual.
320
+ filter: async (src) => {
321
+ if (!COPY_EXCLUDED_DIRS.has(basename(src))) return true;
322
+ const stats = await stat(src).catch(() => void 0);
323
+ return stats === void 0 || !stats.isDirectory();
324
+ }
325
+ });
120
326
  await cleanTemplate(destination);
121
327
  }
122
328
  async function cleanTemplate(dir) {
@@ -124,7 +330,10 @@ async function cleanTemplate(dir) {
124
330
  "bun.lock",
125
331
  "pnpm-lock.yaml",
126
332
  "package-lock.json",
127
- "yarn.lock"
333
+ "yarn.lock",
334
+ // Cada proyecto Flutter instala su propio árbol de dependencias, igual
335
+ // que con los lock files de npm.
336
+ "pubspec.lock"
128
337
  ];
129
338
  for (const file of lockFiles) {
130
339
  const path = join(dir, file);
@@ -137,9 +346,15 @@ async function cleanTemplate(dir) {
137
346
  async function cleanup(tempDir) {
138
347
  await rm(tempDir, { recursive: true, force: true });
139
348
  }
140
- async function substituteTemplate(dir, projectName, templateFolder) {
349
+ async function substituteTemplate(dir, projectName, templateFolder, options = {}) {
141
350
  if (templateFolder === "ASPNET") {
142
351
  await substituteAspNet(dir, projectName);
352
+ } else if (templateFolder === "FLUTTER") {
353
+ await substituteFlutter(
354
+ dir,
355
+ projectName,
356
+ options.org ?? FLUTTER_ORG_DEFAULT
357
+ );
143
358
  } else {
144
359
  await substituteNodejs(dir, projectName);
145
360
  }
@@ -190,6 +405,188 @@ async function substituteAspNet(dir, projectName) {
190
405
  }
191
406
  await renameAll(dir, pascalName);
192
407
  }
408
+ var FLUTTER_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
409
+ ".dart",
410
+ ".arb",
411
+ ".yaml",
412
+ ".yml",
413
+ ".json",
414
+ ".kt",
415
+ ".kts",
416
+ ".gradle",
417
+ ".xml",
418
+ ".plist",
419
+ ".pbxproj",
420
+ ".xcconfig",
421
+ ".xcscheme",
422
+ ".xcsettings",
423
+ ".xcworkspacedata",
424
+ ".storyboard",
425
+ ".xib",
426
+ ".swift",
427
+ ".h",
428
+ ".cpp",
429
+ ".cc",
430
+ ".cmake",
431
+ ".txt",
432
+ ".rc",
433
+ ".properties",
434
+ ".manifest",
435
+ ".md",
436
+ ".html",
437
+ ".gitignore",
438
+ ".metadata",
439
+ ".iml",
440
+ ".sh"
441
+ ]);
442
+ var FLUTTER_TEXT_BASENAMES = /* @__PURE__ */ new Set([
443
+ "makefile",
444
+ "license",
445
+ "podfile",
446
+ ".gitignore",
447
+ ".metadata"
448
+ ]);
449
+ var FLUTTER_DISPLAY_FILES = /* @__PURE__ */ new Set([
450
+ "lib/l10n/arb/app_en.arb",
451
+ "lib/l10n/arb/app_es.arb",
452
+ "lib/l10n/gen/app_localizations.dart",
453
+ "lib/l10n/gen/app_localizations_en.dart",
454
+ "lib/l10n/gen/app_localizations_es.dart",
455
+ "lib/core/config/app_config.dart",
456
+ "android/app/src/main/AndroidManifest.xml",
457
+ "ios/Runner/Info.plist",
458
+ "web/index.html",
459
+ "web/manifest.json",
460
+ "linux/runner/my_application.cc",
461
+ "windows/runner/main.cpp"
462
+ ]);
463
+ var FLUTTER_RUNNER_RC = "windows/runner/Runner.rc";
464
+ var FLUTTER_NATIVE_ROOTS = ["kotlin", "java"];
465
+ function isFlutterTextFile(filePath) {
466
+ if (FLUTTER_TEXT_EXTENSIONS.has(extname(filePath).toLowerCase())) {
467
+ return true;
468
+ }
469
+ return FLUTTER_TEXT_BASENAMES.has(basename(filePath).toLowerCase());
470
+ }
471
+ function replaceToken(content, token, value) {
472
+ return content.split(token).join(value);
473
+ }
474
+ function matchPackageDeclaration(content) {
475
+ const match = content.match(
476
+ /^[ \t]*package[ \t]+([A-Za-z_][\w.]*)[ \t]*;?[ \t]*$/m
477
+ );
478
+ return match?.[1];
479
+ }
480
+ function substituteRunnerRc(content, dartName, displayName, org) {
481
+ return content.split("\n").map((line) => {
482
+ const isInternalName = line.includes("InternalName") || line.includes("OriginalFilename");
483
+ let replaced = replaceToken(
484
+ line,
485
+ "fwiflu",
486
+ isInternalName ? dartName : displayName
487
+ );
488
+ replaced = replaceToken(replaced, "mx.apto", org);
489
+ return replaced;
490
+ }).join("\n");
491
+ }
492
+ async function getAllFilesOrEmpty(dir) {
493
+ try {
494
+ return await getAllFiles(dir);
495
+ } catch {
496
+ return [];
497
+ }
498
+ }
499
+ async function removeEmptySubdirectories(dir) {
500
+ let entries;
501
+ try {
502
+ entries = await readdir(dir, { withFileTypes: true });
503
+ } catch {
504
+ return false;
505
+ }
506
+ let isEmpty = true;
507
+ for (const entry of entries) {
508
+ const childPath = join(dir, entry.name);
509
+ if (!entry.isDirectory()) {
510
+ isEmpty = false;
511
+ continue;
512
+ }
513
+ if (await removeEmptySubdirectories(childPath)) {
514
+ await rm(childPath, { recursive: true, force: true });
515
+ } else {
516
+ isEmpty = false;
517
+ }
518
+ }
519
+ return isEmpty;
520
+ }
521
+ async function substituteFlutter(dir, projectName, orgInput) {
522
+ const dartName = toDartPackageName(projectName);
523
+ const displayName = projectName.trim();
524
+ const org = normalizeOrg(orgInput);
525
+ const androidMainDir = join(dir, "android", "app", "src", "main");
526
+ const nativeFiles = [];
527
+ for (const root of FLUTTER_NATIVE_ROOTS) {
528
+ const files = await getAllFilesOrEmpty(join(androidMainDir, root));
529
+ for (const filePath of files) {
530
+ const ext = extname(filePath).toLowerCase();
531
+ if (ext !== ".kt" && ext !== ".java") continue;
532
+ try {
533
+ const content = await readFile(filePath, "utf-8");
534
+ if (matchPackageDeclaration(content) !== void 0) {
535
+ nativeFiles.push({ path: filePath, root });
536
+ }
537
+ } catch {
538
+ }
539
+ }
540
+ }
541
+ const allFiles = await getAllFiles(dir);
542
+ for (const filePath of allFiles) {
543
+ if (!isFlutterTextFile(filePath)) continue;
544
+ let content;
545
+ try {
546
+ content = await readFile(filePath, "utf-8");
547
+ } catch {
548
+ continue;
549
+ }
550
+ const relPath = relative(dir, filePath).split(sep).join("/");
551
+ let replaced;
552
+ if (relPath === FLUTTER_RUNNER_RC) {
553
+ replaced = substituteRunnerRc(content, dartName, displayName, org);
554
+ } else if (FLUTTER_DISPLAY_FILES.has(relPath)) {
555
+ replaced = replaceToken(content, "fwiflu", displayName);
556
+ replaced = replaceToken(replaced, "mx.apto", org);
557
+ } else {
558
+ replaced = replaceToken(content, "fwiflu", dartName);
559
+ replaced = replaceToken(replaced, "mx.apto", org);
560
+ }
561
+ if (extname(filePath).toLowerCase() === ".dart") {
562
+ replaced = sortDartDirectiveRuns(replaced);
563
+ }
564
+ if (replaced !== content) {
565
+ await writeFile(filePath, replaced);
566
+ }
567
+ }
568
+ for (const nativeFile of nativeFiles) {
569
+ let content;
570
+ try {
571
+ content = await readFile(nativeFile.path, "utf-8");
572
+ } catch {
573
+ continue;
574
+ }
575
+ const newPackage = matchPackageDeclaration(content);
576
+ if (newPackage === void 0) continue;
577
+ const expectedDir = join(
578
+ androidMainDir,
579
+ nativeFile.root,
580
+ ...newPackage.split(".")
581
+ );
582
+ if (dirname(nativeFile.path) === expectedDir) continue;
583
+ await mkdir(expectedDir, { recursive: true });
584
+ await rename(nativeFile.path, join(expectedDir, basename(nativeFile.path)));
585
+ }
586
+ for (const root of FLUTTER_NATIVE_ROOTS) {
587
+ await removeEmptySubdirectories(join(androidMainDir, root));
588
+ }
589
+ }
193
590
  async function getAllFiles(dir) {
194
591
  const results = [];
195
592
  const entries = await readdir(dir, { withFileTypes: true });
@@ -450,6 +847,18 @@ async function selectSkillsAction() {
450
847
  ]);
451
848
  return action;
452
849
  }
850
+ async function askFlutterOrg() {
851
+ const { org } = await inquirer.prompt([
852
+ {
853
+ type: "input",
854
+ name: "org",
855
+ message: "Organizaci\xF3n para el bundle id (reverse-domain, ej. com.example):",
856
+ default: FLUTTER_ORG_DEFAULT,
857
+ validate: checkOrg
858
+ }
859
+ ]);
860
+ return normalizeOrg(org);
861
+ }
453
862
  async function askGlobalSkillsToo() {
454
863
  const { global } = await inquirer.prompt([
455
864
  {
@@ -908,9 +1317,9 @@ async function readTemplatePostInit(dir) {
908
1317
  return void 0;
909
1318
  }
910
1319
  }
911
- async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
1320
+ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
912
1321
  const { tempDir, repoRoot } = await downloadAndExtractRepo();
913
- const projectName = basename(projectPath);
1322
+ const projectName = basename2(projectPath);
914
1323
  const backendPath = join3(projectPath, "backend");
915
1324
  const frontendPath = join3(projectPath, "frontend");
916
1325
  await copyTemplate(
@@ -921,8 +1330,12 @@ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm)
921
1330
  join3(repoRoot, "templates", frontendTpl.folder),
922
1331
  frontendPath
923
1332
  );
924
- await substituteTemplate(backendPath, projectName, backendTpl.folder);
925
- await substituteTemplate(frontendPath, projectName, frontendTpl.folder);
1333
+ await substituteTemplate(backendPath, projectName, backendTpl.folder, {
1334
+ org
1335
+ });
1336
+ await substituteTemplate(frontendPath, projectName, frontendTpl.folder, {
1337
+ org
1338
+ });
926
1339
  await installAgentSkills(repoRoot, projectPath);
927
1340
  if (backendTpl.runtime === "bun" && pm !== "bun") {
928
1341
  await adaptToNodeRuntime(backendPath);
@@ -1073,10 +1486,20 @@ program.command("skills").description(
1073
1486
  process.exit(1);
1074
1487
  }
1075
1488
  });
1076
- program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option("--backend", "Empaquetar el c\xF3digo en backend/ (saltea la pregunta)").option("--no-backend", "Dejar el c\xF3digo en la ra\xEDz del proyecto (saltea la pregunta)").option("-b, --backend-template <tpl>", "Backend a usar en fullstack (saltea la pregunta)").option("-f, --frontend-template <tpl>", "Frontend a usar en fullstack (saltea la pregunta)").option("--git", "Inicializar un repositorio git (saltea la pregunta)").option("--no-git", "No inicializar git (saltea la pregunta)").action(
1489
+ program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option(
1490
+ "-o, --org <domain>",
1491
+ "Organizaci\xF3n reverse-domain para el bundle id de Flutter (ej: ar.miempresa)"
1492
+ ).option("--backend", "Empaquetar el c\xF3digo en backend/ (saltea la pregunta)").option("--no-backend", "Dejar el c\xF3digo en la ra\xEDz del proyecto (saltea la pregunta)").option("-b, --backend-template <tpl>", "Backend a usar en fullstack (saltea la pregunta)").option("-f, --frontend-template <tpl>", "Frontend a usar en fullstack (saltea la pregunta)").option("--git", "Inicializar un repositorio git (saltea la pregunta)").option("--no-git", "No inicializar git (saltea la pregunta)").action(
1077
1493
  async (templateArg, projectNameArg) => {
1078
1494
  try {
1079
- const { pm: pmArg, backend, backendTemplate, frontendTemplate, git } = program.opts();
1495
+ const {
1496
+ pm: pmArg,
1497
+ org: orgArg,
1498
+ backend,
1499
+ backendTemplate,
1500
+ frontendTemplate,
1501
+ git
1502
+ } = program.opts();
1080
1503
  if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
1081
1504
  console.error(
1082
1505
  chalk.red(
@@ -1163,10 +1586,29 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1163
1586
  if (!projectName) {
1164
1587
  projectName = await askProjectName();
1165
1588
  }
1589
+ const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
1590
+ let org;
1591
+ if (runtimes.includes("flutter")) {
1592
+ if (orgArg) {
1593
+ const check = checkOrg(orgArg);
1594
+ if (check !== true) {
1595
+ console.error(chalk.red(`
1596
+ \u2716 ${check}
1597
+ `));
1598
+ process.exit(1);
1599
+ }
1600
+ org = normalizeOrg(orgArg);
1601
+ } else if (git === void 0) {
1602
+ org = await askFlutterOrg();
1603
+ } else {
1604
+ org = FLUTTER_ORG_DEFAULT;
1605
+ }
1606
+ }
1166
1607
  const layer = template?.layer;
1167
1608
  const useLayerLayout = !template ? false : backend === void 0 ? await askLayerLayout(layer) : backend;
1168
- const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
1169
- const needsPm = runtimes.some((r) => r !== "dotnet");
1609
+ const needsPm = runtimes.some(
1610
+ (r) => r !== "dotnet" && r !== "flutter"
1611
+ );
1170
1612
  if (!pm && needsPm) {
1171
1613
  pm = await selectPackageManager();
1172
1614
  }
@@ -1190,7 +1632,8 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1190
1632
  projectPath,
1191
1633
  backendTpl,
1192
1634
  frontendTpl,
1193
- pm ?? "npm"
1635
+ pm ?? "npm",
1636
+ org
1194
1637
  );
1195
1638
  spinner.succeed("Template descargado");
1196
1639
  } else {
@@ -1198,7 +1641,9 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1198
1641
  const codePath = useLayerLayout ? join3(projectPath, layer) : projectPath;
1199
1642
  spinner.text = "Creando proyecto...";
1200
1643
  await copyTemplate(templatePath, codePath);
1201
- await substituteTemplate(codePath, projectName, template.folder);
1644
+ await substituteTemplate(codePath, projectName, template.folder, {
1645
+ org
1646
+ });
1202
1647
  await installAgentSkills(repoRoot, projectPath);
1203
1648
  if (useLayerLayout) {
1204
1649
  await writeRootGitignore(projectPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fwinit",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "CLI para crear proyectos desde templates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "dev": "tsup src/index.ts --format esm --watch",
15
15
  "start": "node dist/index.js",
16
16
  "typecheck": "tsc --noEmit",
17
+ "test": "bun test",
17
18
  "prepublishOnly": "chmod +x dist/index.js"
18
19
  },
19
20
  "dependencies": {
@@ -25,6 +26,7 @@
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/adm-zip": "^0.5.7",
29
+ "@types/bun": "^1.3.0",
28
30
  "@types/node": "^22.13.10",
29
31
  "tsup": "^8.4.0",
30
32
  "typescript": "^5.8.2"