fwinit 1.5.3 → 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 +21 -11
  2. package/dist/index.js +473 -20
  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
@@ -35,11 +39,12 @@ fwinit fullstack mi-proyecto -b fastify -f react-native -p pnpm
35
39
  # Ver templates disponibles (agrupados por capa)
36
40
  fwinit list
37
41
 
38
- # Instalar skills globalmente (OpenCode, Pi, Agent Skills) — interactivo
42
+ # Instalar skills globalmente (OpenCode, Pi, Gentle Shell, Agent Skills) — interactivo
39
43
  fwinit skills
40
44
 
41
45
  # Instalar skills globalmente sin preguntas (modo scripting/CI)
42
46
  fwinit skills --global opencode,pi # o: --global all
47
+ fwinit skills --global gentle-shell
43
48
 
44
49
  # Instalar solo algunas skills
45
50
  fwinit skills --global all --only create-specs,design
@@ -74,15 +79,16 @@ fwinit --help
74
79
  fwinit fullstack mi-proyecto
75
80
  ```
76
81
 
77
- 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:
78
83
 
79
84
  ```
80
85
  mi-proyecto/
81
86
  ├── backend/ # API (template elegido — trae su DB: Prisma o EF)
82
87
  ├── frontend/ # App (template frontend elegido)
83
- ├── .gitignore # .atl/ · odd (.opencode/ y .pi/ SÍ se versionan)
84
- ├── .opencode/ # Skills de opencode (create-specs, create-specs-from-code y biblioteca de diseño)
85
- ├── .pi/ # Mismo bundle de skills para Pi (badlogic)
88
+ ├── .gitignore # .atl/ · odd (.opencode/, .pi/ y .agents/ SÍ se versionan)
89
+ ├── .opencode/ # Skills de OpenCode (create-specs, create-specs-from-code, create-prompt + biblioteca de diseño)
90
+ ├── .pi/ # Mismo bundle para Pi y Gentle Shell (skills + prompt templates)
91
+ ├── .agents/ # Mismo bundle para Freebuff y agentes Agent Skills
86
92
  ├── README.md # Cómo correr backend y frontend
87
93
  └── specs/ # La creás con /create-specs
88
94
  ```
@@ -102,13 +108,14 @@ fwinit express mi-api --no-backend
102
108
  fwinit express mi-api --backend
103
109
  ```
104
110
 
105
- 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/`.
106
112
 
107
113
  ## Flags
108
114
 
109
115
  | Flag | Descripción |
110
116
  |------|-------------|
111
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`) |
112
119
  | `--backend` | Empaquetar el código en `backend/` (solo modo single, saltea la pregunta) |
113
120
  | `--no-backend` | Dejar el código en la raíz del proyecto (saltea la pregunta) |
114
121
  | `-b, --backend-template <tpl>` | Backend a usar en fullstack (saltea la pregunta) |
@@ -127,8 +134,11 @@ Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
127
134
  | `astro` | frontend | Landing estática con Astro 7 y TypeScript | node |
128
135
  | `tauri` | frontend | Dashboard de escritorio con React, Vite y Tauri 2 | node |
129
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`.
130
140
 
131
- 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.
132
142
 
133
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`.
134
144
 
@@ -136,14 +146,14 @@ El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si
136
146
 
137
147
  1. Descarga el ZIP del repositorio de templates desde GitHub (una sola vez, también en fullstack)
138
148
  2. Extrae y copia el template (o los dos, `backend/` + `frontend/`) al proyecto
139
- 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)
140
150
  4. Si corresponde, porta el template de runtime bun a node (npm/pnpm)
141
- 5. Instala `.opencode/` y `.pi/` 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 y Pi: elegí el agente que quieras)
142
- 6. Limpia los lock files (cada proyecto instala los suyos)
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)
152
+ 6. Limpia los lock files (cada proyecto instala los suyos; incluye `pubspec.lock` en Flutter)
143
153
  7. Si lo elegís (o pasás `--git`), inicializa un repositorio git en la raíz
144
154
  8. Te muestra los próximos pasos (install y dev de cada carpeta)
145
155
 
146
- En modo fullstack (y single con layout de capa) crea además `.gitignore` y `README.md` en la raíz: el `.gitignore` excluye solo el estado local de AI (`.atl/` y `odd`). `.opencode/` y `.pi/` NO se ignoran — los bundles de skills se versionan con el proyecto.
156
+ En modo fullstack (y single con layout de capa) crea además `.gitignore` y `README.md` en la raíz: el `.gitignore` excluye solo el estado local de AI (`.atl/` y `odd`). `.opencode/`, `.pi/` y `.agents/` NO se ignoran — los bundles de skills se versionan con el proyecto.
147
157
 
148
158
  ## Desarrollo
149
159
 
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,12 +847,24 @@ 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
  {
456
865
  type: "confirm",
457
866
  name: "global",
458
- message: "\xBFInstalar las skills tambi\xE9n globalmente (OpenCode/Pi/Agent Skills)?",
867
+ message: "\xBFInstalar las skills tambi\xE9n globalmente (OpenCode/Pi/Gentle Shell/Agent Skills)?",
459
868
  default: false
460
869
  }
461
870
  ]);
@@ -468,6 +877,7 @@ import { join as join2 } from "path";
468
877
  function resolveGlobalTargets() {
469
878
  const home = process.env.HOME;
470
879
  if (!home) return [];
880
+ const gentleShellRoot = process.env.GENTLE_SHELL_HOME || join2(home, ".gentle-shell", "agent");
471
881
  return [
472
882
  {
473
883
  id: "opencode",
@@ -483,6 +893,13 @@ function resolveGlobalTargets() {
483
893
  skillsDir: "skills",
484
894
  commandsDir: "prompts"
485
895
  },
896
+ {
897
+ id: "gentle-shell",
898
+ label: `Gentle Shell global (${gentleShellRoot})`,
899
+ root: gentleShellRoot,
900
+ skillsDir: "skills",
901
+ commandsDir: "prompts"
902
+ },
486
903
  {
487
904
  id: "agents",
488
905
  label: `Agent Skills global (${home}/.agents/skills)`,
@@ -809,7 +1226,7 @@ async function writeRootFiles(info) {
809
1226
  "",
810
1227
  `- \`backend/\` \u2014 ${info.backend.description}`,
811
1228
  `- \`frontend/\` \u2014 ${info.frontend.description}`,
812
- "- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-prompt` para detallar tu idea, `/create-specs` para generar las specs, `/create-specs-from-code` para documentar un proyecto que ya existe, + biblioteca de dise\xF1o (dark-luxury, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism). Mismo bundle en las 3 ubicaciones",
1229
+ "- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-prompt` para detallar tu idea, `/create-specs` para generar las specs, `/create-specs-from-code` para documentar un proyecto que ya existe, + biblioteca de dise\xF1o (dark-luxury, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism). Mismo bundle en las 3 ubicaciones: OpenCode lee `.opencode/`; Pi y Gentle Shell leen `.pi/` (skills + prompt templates); los agentes Agent Skills leen `.agents/`",
813
1230
  "- `specs/` \u2014 especificaciones del proyecto",
814
1231
  "",
815
1232
  "## Backend",
@@ -900,9 +1317,9 @@ async function readTemplatePostInit(dir) {
900
1317
  return void 0;
901
1318
  }
902
1319
  }
903
- async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
1320
+ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
904
1321
  const { tempDir, repoRoot } = await downloadAndExtractRepo();
905
- const projectName = basename(projectPath);
1322
+ const projectName = basename2(projectPath);
906
1323
  const backendPath = join3(projectPath, "backend");
907
1324
  const frontendPath = join3(projectPath, "frontend");
908
1325
  await copyTemplate(
@@ -913,8 +1330,12 @@ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm)
913
1330
  join3(repoRoot, "templates", frontendTpl.folder),
914
1331
  frontendPath
915
1332
  );
916
- await substituteTemplate(backendPath, projectName, backendTpl.folder);
917
- 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
+ });
918
1339
  await installAgentSkills(repoRoot, projectPath);
919
1340
  if (backendTpl.runtime === "bun" && pm !== "bun") {
920
1341
  await adaptToNodeRuntime(backendPath);
@@ -1045,10 +1466,10 @@ async function runSkillsFlow(opts = {}) {
1045
1466
  return true;
1046
1467
  }
1047
1468
  program.command("skills").description(
1048
- "Instalar o eliminar las skills del repo en ubicaciones globales (OpenCode, Pi, Agent Skills)"
1469
+ "Instalar o eliminar las skills del repo en ubicaciones globales (OpenCode, Pi, Gentle Shell, Agent Skills)"
1049
1470
  ).option(
1050
1471
  "-g, --global <targets>",
1051
- "Destinos separados por coma: opencode, pi, agents (o 'all'). Saltea la pregunta"
1472
+ "Destinos separados por coma: opencode, pi, gentle-shell, agents (o 'all'). Saltea la pregunta"
1052
1473
  ).option(
1053
1474
  "-o, --only <skills>",
1054
1475
  "Solo estas skills, separadas por coma: create-specs, create-specs-from-code, design"
@@ -1065,10 +1486,20 @@ program.command("skills").description(
1065
1486
  process.exit(1);
1066
1487
  }
1067
1488
  });
1068
- 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(
1069
1493
  async (templateArg, projectNameArg) => {
1070
1494
  try {
1071
- 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();
1072
1503
  if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
1073
1504
  console.error(
1074
1505
  chalk.red(
@@ -1155,10 +1586,29 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1155
1586
  if (!projectName) {
1156
1587
  projectName = await askProjectName();
1157
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
+ }
1158
1607
  const layer = template?.layer;
1159
1608
  const useLayerLayout = !template ? false : backend === void 0 ? await askLayerLayout(layer) : backend;
1160
- const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
1161
- const needsPm = runtimes.some((r) => r !== "dotnet");
1609
+ const needsPm = runtimes.some(
1610
+ (r) => r !== "dotnet" && r !== "flutter"
1611
+ );
1162
1612
  if (!pm && needsPm) {
1163
1613
  pm = await selectPackageManager();
1164
1614
  }
@@ -1182,7 +1632,8 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1182
1632
  projectPath,
1183
1633
  backendTpl,
1184
1634
  frontendTpl,
1185
- pm ?? "npm"
1635
+ pm ?? "npm",
1636
+ org
1186
1637
  );
1187
1638
  spinner.succeed("Template descargado");
1188
1639
  } else {
@@ -1190,7 +1641,9 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1190
1641
  const codePath = useLayerLayout ? join3(projectPath, layer) : projectPath;
1191
1642
  spinner.text = "Creando proyecto...";
1192
1643
  await copyTemplate(templatePath, codePath);
1193
- await substituteTemplate(codePath, projectName, template.folder);
1644
+ await substituteTemplate(codePath, projectName, template.folder, {
1645
+ org
1646
+ });
1194
1647
  await installAgentSkills(repoRoot, projectPath);
1195
1648
  if (useLayerLayout) {
1196
1649
  await writeRootGitignore(projectPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fwinit",
3
- "version": "1.5.3",
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"