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.
- package/README.md +21 -11
- package/dist/index.js +473 -20
- 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 .
|
|
84
|
-
├── .opencode/ # Skills de
|
|
85
|
-
├── .pi/ # Mismo bundle
|
|
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
|
-
|
|
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 `.
|
|
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 `.
|
|
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, {
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
1161
|
-
|
|
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.
|
|
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"
|