fwinit 1.5.4 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +15 -7
  2. package/dist/index.js +478 -26
  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,14 +79,14 @@ 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/
82
86
  ├── backend/ # API (template elegido — trae su DB: Prisma o EF)
83
87
  ├── frontend/ # App (template frontend elegido)
84
88
  ├── .gitignore # .atl/ · odd (.opencode/, .pi/ y .agents/ SÍ se versionan)
85
- ├── .opencode/ # Skills de OpenCode (create-specs, create-specs-from-code, create-prompt + biblioteca de diseño)
89
+ ├── .opencode/ # Skills de OpenCode (create-specs, create-specs-from-code, create-prompt, biblioteca de diseño y catálogo ui-ux-pro-max)
86
90
  ├── .pi/ # Mismo bundle para Pi y Gentle Shell (skills + prompt templates)
87
91
  ├── .agents/ # Mismo bundle para Freebuff y agentes Agent Skills
88
92
  ├── README.md # Cómo correr backend y frontend
@@ -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
- 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)
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), la biblioteca de diseño y el catálogo `ui-ux-pro-max` (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)
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 });
@@ -259,6 +656,13 @@ var AGENT_BUNDLE = [
259
656
  {
260
657
  // Biblioteca de estilos: skills/design/ con una subskill por estilo y sus
261
658
  // commands (/design, /design-<estilo>) que leen specs/frontend/02-design.md.
659
+ // Adentro vive también el catálogo search-driven ui-ux-pro-max (Python
660
+ // stdlib, offline): no es un estilo fijo sino una base consultable que
661
+ // genera un design system, pero comparte familia, carpeta y comando
662
+ // (/design-ui-ux-pro-max). Como la biblioteca se copia entera, el catálogo
663
+ // viaja con su data/ y sus scripts/ — si sólo viajara su SKILL.md, cada
664
+ // búsqueda devolvería 0 resultados y el agente caería a defaults genéricos
665
+ // sin avisar.
262
666
  kind: "library",
263
667
  dir: "design",
264
668
  required: false
@@ -315,13 +719,12 @@ async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
315
719
  }
316
720
  for (const extra of entry.extras ?? []) {
317
721
  const from = join4(src, extra);
318
- if (await exists(from)) {
319
- installs.push({
320
- from,
321
- to: `skills/${entry.dir}/${extra}`,
322
- recursive: true
323
- });
324
- }
722
+ if (!await exists(from)) continue;
723
+ installs.push({
724
+ from,
725
+ to: `skills/${entry.dir}/${extra}`,
726
+ recursive: true
727
+ });
325
728
  }
326
729
  }
327
730
  return { installs, skillDirs, commandDirs: ["commands"], missingRequired };
@@ -450,6 +853,18 @@ async function selectSkillsAction() {
450
853
  ]);
451
854
  return action;
452
855
  }
856
+ async function askFlutterOrg() {
857
+ const { org } = await inquirer.prompt([
858
+ {
859
+ type: "input",
860
+ name: "org",
861
+ message: "Organizaci\xF3n para el bundle id (reverse-domain, ej. com.example):",
862
+ default: FLUTTER_ORG_DEFAULT,
863
+ validate: checkOrg
864
+ }
865
+ ]);
866
+ return normalizeOrg(org);
867
+ }
453
868
  async function askGlobalSkillsToo() {
454
869
  const { global } = await inquirer.prompt([
455
870
  {
@@ -817,7 +1232,7 @@ async function writeRootFiles(info) {
817
1232
  "",
818
1233
  `- \`backend/\` \u2014 ${info.backend.description}`,
819
1234
  `- \`frontend/\` \u2014 ${info.frontend.description}`,
820
- "- `.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/`",
1235
+ "- `.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 con `/design` (estilos dark-luxury, docs-starship, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism) y el cat\xE1logo search-driven `/design-ui-ux-pro-max` (requiere Python 3). Mismo bundle en las 3 ubicaciones: OpenCode lee `.opencode/`; Pi y Gentle Shell leen `.pi/` (skills + prompt templates); los agentes Agent Skills leen `.agents/`",
821
1236
  "- `specs/` \u2014 especificaciones del proyecto",
822
1237
  "",
823
1238
  "## Backend",
@@ -908,9 +1323,9 @@ async function readTemplatePostInit(dir) {
908
1323
  return void 0;
909
1324
  }
910
1325
  }
911
- async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
1326
+ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
912
1327
  const { tempDir, repoRoot } = await downloadAndExtractRepo();
913
- const projectName = basename(projectPath);
1328
+ const projectName = basename2(projectPath);
914
1329
  const backendPath = join3(projectPath, "backend");
915
1330
  const frontendPath = join3(projectPath, "frontend");
916
1331
  await copyTemplate(
@@ -921,8 +1336,12 @@ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm)
921
1336
  join3(repoRoot, "templates", frontendTpl.folder),
922
1337
  frontendPath
923
1338
  );
924
- await substituteTemplate(backendPath, projectName, backendTpl.folder);
925
- await substituteTemplate(frontendPath, projectName, frontendTpl.folder);
1339
+ await substituteTemplate(backendPath, projectName, backendTpl.folder, {
1340
+ org
1341
+ });
1342
+ await substituteTemplate(frontendPath, projectName, frontendTpl.folder, {
1343
+ org
1344
+ });
926
1345
  await installAgentSkills(repoRoot, projectPath);
927
1346
  if (backendTpl.runtime === "bun" && pm !== "bun") {
928
1347
  await adaptToNodeRuntime(backendPath);
@@ -986,7 +1405,8 @@ async function runSkillsFlow(opts = {}) {
986
1405
  console.log(chalk.bold("\nSkills disponibles:\n"));
987
1406
  console.log(chalk.dim(" create-specs \u2014 specs para proyecto nuevo (/create-specs)"));
988
1407
  console.log(chalk.dim(" create-specs-from-code \u2014 specs desde c\xF3digo existente (/create-specs-from-code)"));
989
- console.log(chalk.dim(" design/ \u2014 dark-luxury, glassmorphism, minimal-dashboard, minimal-light, neo-brutalist (/design-<estilo>)\n"));
1408
+ console.log(chalk.dim(" create-prompt \u2014 detalla una idea en specs/prompt.md (/create-prompt)"));
1409
+ console.log(chalk.dim(" design/ \u2014 dark-luxury, glassmorphism, minimal-dashboard, minimal-light, neo-brutalist, docs-starship + cat\xE1logo ui-ux-pro-max (/design-<estilo>; el cat\xE1logo requiere Python 3)\n"));
990
1410
  let bundle;
991
1411
  if (opts.only) {
992
1412
  const names = opts.only.split(",").map((s) => s.trim()).filter(Boolean);
@@ -1059,7 +1479,7 @@ program.command("skills").description(
1059
1479
  "Destinos separados por coma: opencode, pi, gentle-shell, agents (o 'all'). Saltea la pregunta"
1060
1480
  ).option(
1061
1481
  "-o, --only <skills>",
1062
- "Solo estas skills, separadas por coma: create-specs, create-specs-from-code, design"
1482
+ "Solo estas skills, separadas por coma: create-specs, create-specs-from-code, create-prompt, design"
1063
1483
  ).option(
1064
1484
  "-r, --remove",
1065
1485
  "Eliminar en vez de instalar (combinar con --global y opcionalmente --only)"
@@ -1073,10 +1493,20 @@ program.command("skills").description(
1073
1493
  process.exit(1);
1074
1494
  }
1075
1495
  });
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(
1496
+ 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(
1497
+ "-o, --org <domain>",
1498
+ "Organizaci\xF3n reverse-domain para el bundle id de Flutter (ej: ar.miempresa)"
1499
+ ).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
1500
  async (templateArg, projectNameArg) => {
1078
1501
  try {
1079
- const { pm: pmArg, backend, backendTemplate, frontendTemplate, git } = program.opts();
1502
+ const {
1503
+ pm: pmArg,
1504
+ org: orgArg,
1505
+ backend,
1506
+ backendTemplate,
1507
+ frontendTemplate,
1508
+ git
1509
+ } = program.opts();
1080
1510
  if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
1081
1511
  console.error(
1082
1512
  chalk.red(
@@ -1163,10 +1593,29 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1163
1593
  if (!projectName) {
1164
1594
  projectName = await askProjectName();
1165
1595
  }
1596
+ const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
1597
+ let org;
1598
+ if (runtimes.includes("flutter")) {
1599
+ if (orgArg) {
1600
+ const check = checkOrg(orgArg);
1601
+ if (check !== true) {
1602
+ console.error(chalk.red(`
1603
+ \u2716 ${check}
1604
+ `));
1605
+ process.exit(1);
1606
+ }
1607
+ org = normalizeOrg(orgArg);
1608
+ } else if (git === void 0) {
1609
+ org = await askFlutterOrg();
1610
+ } else {
1611
+ org = FLUTTER_ORG_DEFAULT;
1612
+ }
1613
+ }
1166
1614
  const layer = template?.layer;
1167
1615
  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");
1616
+ const needsPm = runtimes.some(
1617
+ (r) => r !== "dotnet" && r !== "flutter"
1618
+ );
1170
1619
  if (!pm && needsPm) {
1171
1620
  pm = await selectPackageManager();
1172
1621
  }
@@ -1190,7 +1639,8 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1190
1639
  projectPath,
1191
1640
  backendTpl,
1192
1641
  frontendTpl,
1193
- pm ?? "npm"
1642
+ pm ?? "npm",
1643
+ org
1194
1644
  );
1195
1645
  spinner.succeed("Template descargado");
1196
1646
  } else {
@@ -1198,7 +1648,9 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
1198
1648
  const codePath = useLayerLayout ? join3(projectPath, layer) : projectPath;
1199
1649
  spinner.text = "Creando proyecto...";
1200
1650
  await copyTemplate(templatePath, codePath);
1201
- await substituteTemplate(codePath, projectName, template.folder);
1651
+ await substituteTemplate(codePath, projectName, template.folder, {
1652
+ org
1653
+ });
1202
1654
  await installAgentSkills(repoRoot, projectPath);
1203
1655
  if (useLayerLayout) {
1204
1656
  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.2",
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"