@nimbuslab/cli 0.8.0 → 0.9.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/MIGRATION-ROADMAP.md +201 -0
- package/dist/index.js +429 -20
- package/package.json +1 -1
- package/src/commands/analyze.ts +210 -0
- package/src/commands/upgrade.ts +251 -0
- package/src/index.ts +19 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Roadmap - Sistema de Migracao nimbuslab
|
|
2
|
+
|
|
3
|
+
> Lola Migration Assistant - Facilitar migracoes de projetos para o ecossistema nimbuslab
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Visao Geral
|
|
8
|
+
|
|
9
|
+
O sistema de migracao permite:
|
|
10
|
+
1. **Analisar** projetos existentes (detectar stack)
|
|
11
|
+
2. **Planejar** caminho de migracao
|
|
12
|
+
3. **Executar** transformacoes automaticas (codemods)
|
|
13
|
+
4. **Verificar** resultado final
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Fase 1: Fundacao (v0.9.0)
|
|
18
|
+
|
|
19
|
+
### M80: Estrutura base do sistema de migracao
|
|
20
|
+
- [ ] Criar `src/commands/migrate.ts`
|
|
21
|
+
- [ ] Criar `src/commands/analyze.ts`
|
|
22
|
+
- [ ] Criar `src/commands/upgrade.ts`
|
|
23
|
+
- [ ] Estrutura de pastas para codemods
|
|
24
|
+
|
|
25
|
+
### M81: Comando `nimbus analyze`
|
|
26
|
+
- [ ] Detectar package.json (name, version, dependencies)
|
|
27
|
+
- [ ] Detectar framework (Next.js, React, Angular, Vue, etc)
|
|
28
|
+
- [ ] Detectar styling (Tailwind, CSS Modules, styled-components)
|
|
29
|
+
- [ ] Detectar package manager (bun, pnpm, npm, yarn)
|
|
30
|
+
- [ ] Detectar monorepo (Turborepo, Nx, Lerna)
|
|
31
|
+
- [ ] Detectar auth (Better Auth, NextAuth, Clerk, etc)
|
|
32
|
+
- [ ] Detectar DB (Drizzle, Prisma, TypeORM, etc)
|
|
33
|
+
- [ ] Output: JSON com analise completa
|
|
34
|
+
|
|
35
|
+
### M82: Comando `nimbus upgrade --plan`
|
|
36
|
+
- [ ] Comparar versoes atuais com recomendadas
|
|
37
|
+
- [ ] Listar breaking changes conhecidos
|
|
38
|
+
- [ ] Gerar plano de upgrade
|
|
39
|
+
- [ ] Estimar complexidade (low/medium/high)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Fase 2: Codemods (v0.10.0)
|
|
44
|
+
|
|
45
|
+
### M83: Infraestrutura de codemods
|
|
46
|
+
- [ ] Criar `src/codemods/` estrutura
|
|
47
|
+
- [ ] Runner de codemods (jscodeshift ou ts-morph)
|
|
48
|
+
- [ ] Sistema de dry-run (preview)
|
|
49
|
+
- [ ] Rollback automatico em caso de erro
|
|
50
|
+
|
|
51
|
+
### M84: Codemod Tailwind 3 -> 4
|
|
52
|
+
- [ ] Migrar classes depreciadas
|
|
53
|
+
- [ ] Atualizar config (tailwind.config.js -> CSS)
|
|
54
|
+
- [ ] Converter @apply para novo formato
|
|
55
|
+
- [ ] Atualizar imports
|
|
56
|
+
|
|
57
|
+
### M85: Codemod React 18 -> 19
|
|
58
|
+
- [ ] Remover forwardRef (nao mais necessario)
|
|
59
|
+
- [ ] Atualizar tipos (ref como prop)
|
|
60
|
+
- [ ] Ajustar Suspense boundaries
|
|
61
|
+
- [ ] Atualizar async components
|
|
62
|
+
|
|
63
|
+
### M86: Codemod pnpm -> bun
|
|
64
|
+
- [ ] Converter pnpm-lock.yaml para bun.lockb
|
|
65
|
+
- [ ] Atualizar scripts no package.json
|
|
66
|
+
- [ ] Remover .npmrc especifico pnpm
|
|
67
|
+
- [ ] Atualizar CI/CD configs
|
|
68
|
+
|
|
69
|
+
### M87: Codemod Prisma -> Drizzle
|
|
70
|
+
- [ ] Converter schema.prisma para drizzle schema
|
|
71
|
+
- [ ] Gerar migrations Drizzle
|
|
72
|
+
- [ ] Atualizar queries (findMany -> select, etc)
|
|
73
|
+
- [ ] Atualizar auth config
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Fase 3: Templates Modulares (v0.11.0)
|
|
78
|
+
|
|
79
|
+
### M88: Refatorar templates em camadas
|
|
80
|
+
- [ ] Extrair `layers/base` (tsconfig, eslint, prettier)
|
|
81
|
+
- [ ] Extrair `layers/nextjs` (next.config, app structure)
|
|
82
|
+
- [ ] Extrair `layers/tailwind` (tailwind config, globals.css)
|
|
83
|
+
- [ ] Extrair `layers/shadcn` (components.json, ui/)
|
|
84
|
+
- [ ] Extrair `layers/auth` (Better Auth setup)
|
|
85
|
+
- [ ] Extrair `layers/db` (Drizzle setup)
|
|
86
|
+
- [ ] Extrair `layers/monorepo` (Turborepo config)
|
|
87
|
+
|
|
88
|
+
### M89: Comando `nimbus add <layer>`
|
|
89
|
+
- [ ] `nimbus add auth` - Adiciona Better Auth
|
|
90
|
+
- [ ] `nimbus add db` - Adiciona Drizzle
|
|
91
|
+
- [ ] `nimbus add shadcn` - Adiciona shadcn/ui
|
|
92
|
+
- [ ] `nimbus add monorepo` - Converte para Turborepo
|
|
93
|
+
- [ ] Detectar conflitos antes de aplicar
|
|
94
|
+
- [ ] Merge inteligente de configs
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Fase 4: Migracao Assistida (v0.12.0)
|
|
99
|
+
|
|
100
|
+
### M90: Lola Migration Assistant
|
|
101
|
+
- [ ] Criar `.claude/agents/lola-migrate.md`
|
|
102
|
+
- [ ] Criar `.gemini/lola-migrate.md`
|
|
103
|
+
- [ ] Prompts especializados para migracao
|
|
104
|
+
- [ ] Checklist interativo
|
|
105
|
+
- [ ] Documentacao de decisoes
|
|
106
|
+
|
|
107
|
+
### M91: Comando `nimbus migrate`
|
|
108
|
+
- [ ] `nimbus migrate --from=angular` - Plano Angular -> Next.js
|
|
109
|
+
- [ ] `nimbus migrate --from=php` - Plano PHP -> Next.js
|
|
110
|
+
- [ ] `nimbus migrate --from=vue` - Plano Vue -> React
|
|
111
|
+
- [ ] Gerar plano detalhado (markdown)
|
|
112
|
+
- [ ] Estimar esforco (horas/dias)
|
|
113
|
+
|
|
114
|
+
### M92: Migration Guides
|
|
115
|
+
- [ ] `knowledge/migrations/nextjs-15-to-16.md`
|
|
116
|
+
- [ ] `knowledge/migrations/tailwind-3-to-4.md`
|
|
117
|
+
- [ ] `knowledge/migrations/prisma-to-drizzle.md`
|
|
118
|
+
- [ ] `knowledge/migrations/pnpm-to-bun.md`
|
|
119
|
+
- [ ] `knowledge/migrations/angular-to-nextjs.md`
|
|
120
|
+
- [ ] `knowledge/migrations/php-laravel-to-nextjs.md`
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Fase 5: Automacao Completa (v1.0.0)
|
|
125
|
+
|
|
126
|
+
### M93: Pipeline de migracao
|
|
127
|
+
- [ ] `nimbus migrate --execute` (aplica codemods)
|
|
128
|
+
- [ ] `nimbus migrate --verify` (roda build + lint + tests)
|
|
129
|
+
- [ ] Relatorio de migracao (o que mudou, o que revisar)
|
|
130
|
+
- [ ] Integracao com CI/CD
|
|
131
|
+
|
|
132
|
+
### M94: Atualizacao automatica
|
|
133
|
+
- [ ] `nimbus upgrade` detecta atualizacoes disponiveis
|
|
134
|
+
- [ ] `nimbus upgrade --all` aplica todas seguras
|
|
135
|
+
- [ ] Notificacao de breaking changes
|
|
136
|
+
- [ ] Changelog automatico
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Prioridade de Implementacao
|
|
141
|
+
|
|
142
|
+
| Milestone | Prioridade | Dependencia |
|
|
143
|
+
|-----------|------------|-------------|
|
|
144
|
+
| M80 | Alta | - |
|
|
145
|
+
| M81 | Alta | M80 |
|
|
146
|
+
| M82 | Alta | M81 |
|
|
147
|
+
| M83 | Alta | M80 |
|
|
148
|
+
| M84 | Media | M83 |
|
|
149
|
+
| M85 | Media | M83 |
|
|
150
|
+
| M86 | Media | M83 |
|
|
151
|
+
| M87 | Media | M83 |
|
|
152
|
+
| M88 | Media | M80 |
|
|
153
|
+
| M89 | Media | M88 |
|
|
154
|
+
| M90 | Alta | M80 |
|
|
155
|
+
| M91 | Media | M81, M83 |
|
|
156
|
+
| M92 | Media | - |
|
|
157
|
+
| M93 | Baixa | M91 |
|
|
158
|
+
| M94 | Baixa | M93 |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Stack dos Codemods
|
|
163
|
+
|
|
164
|
+
- **Parser:** ts-morph (TypeScript AST)
|
|
165
|
+
- **Runner:** Custom (baseado em jscodeshift patterns)
|
|
166
|
+
- **Configs:** JSON transforms
|
|
167
|
+
- **CSS:** PostCSS para Tailwind
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Exemplo de Uso Final
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Analisar projeto
|
|
175
|
+
nimbus analyze ./meu-projeto
|
|
176
|
+
# Output: Next.js 15, React 18, Tailwind 3, pnpm, Prisma
|
|
177
|
+
|
|
178
|
+
# Ver plano de upgrade
|
|
179
|
+
nimbus upgrade --plan
|
|
180
|
+
# Output: Recomendado: Next 16, React 19, Tailwind 4, bun, Drizzle
|
|
181
|
+
|
|
182
|
+
# Upgrade especifico
|
|
183
|
+
nimbus upgrade tailwind
|
|
184
|
+
# Executa codemod Tailwind 3 -> 4
|
|
185
|
+
|
|
186
|
+
# Adicionar camada
|
|
187
|
+
nimbus add auth
|
|
188
|
+
# Adiciona Better Auth ao projeto
|
|
189
|
+
|
|
190
|
+
# Migracao completa
|
|
191
|
+
nimbus migrate --from=angular --plan
|
|
192
|
+
# Gera plano de migracao Angular -> Next.js
|
|
193
|
+
|
|
194
|
+
# Usar Lola para assistir
|
|
195
|
+
claude --agent lola-migrate
|
|
196
|
+
# Lola especializada em migracoes
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
*Ultima atualizacao: Janeiro 2026*
|
package/dist/index.js
CHANGED
|
@@ -147,7 +147,7 @@ var require_src = __commonJS((exports, module) => {
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
// src/index.ts
|
|
150
|
-
var
|
|
150
|
+
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
151
151
|
|
|
152
152
|
// node_modules/@clack/core/dist/index.mjs
|
|
153
153
|
var import_sisteransi = __toESM(require_src(), 1);
|
|
@@ -1888,9 +1888,404 @@ function showNextSteps(config) {
|
|
|
1888
1888
|
console.log();
|
|
1889
1889
|
}
|
|
1890
1890
|
|
|
1891
|
+
// src/commands/analyze.ts
|
|
1892
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
1893
|
+
import { existsSync, readFileSync } from "fs";
|
|
1894
|
+
import { join as join2 } from "path";
|
|
1895
|
+
function detectPackageManager(dir) {
|
|
1896
|
+
if (existsSync(join2(dir, "bun.lockb")))
|
|
1897
|
+
return "bun";
|
|
1898
|
+
if (existsSync(join2(dir, "pnpm-lock.yaml")))
|
|
1899
|
+
return "pnpm";
|
|
1900
|
+
if (existsSync(join2(dir, "yarn.lock")))
|
|
1901
|
+
return "yarn";
|
|
1902
|
+
if (existsSync(join2(dir, "package-lock.json")))
|
|
1903
|
+
return "npm";
|
|
1904
|
+
return "unknown";
|
|
1905
|
+
}
|
|
1906
|
+
function detectMonorepo(dir, pkg) {
|
|
1907
|
+
if (existsSync(join2(dir, "turbo.json")))
|
|
1908
|
+
return "turborepo";
|
|
1909
|
+
if (existsSync(join2(dir, "nx.json")))
|
|
1910
|
+
return "nx";
|
|
1911
|
+
if (existsSync(join2(dir, "lerna.json")))
|
|
1912
|
+
return "lerna";
|
|
1913
|
+
if (pkg.workspaces)
|
|
1914
|
+
return "workspaces";
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
function detectFramework(deps) {
|
|
1918
|
+
if (deps["next"])
|
|
1919
|
+
return { name: "nextjs", version: deps["next"] };
|
|
1920
|
+
if (deps["@angular/core"])
|
|
1921
|
+
return { name: "angular", version: deps["@angular/core"] };
|
|
1922
|
+
if (deps["vue"])
|
|
1923
|
+
return { name: "vue", version: deps["vue"] };
|
|
1924
|
+
if (deps["svelte"])
|
|
1925
|
+
return { name: "svelte", version: deps["svelte"] };
|
|
1926
|
+
if (deps["react"] && !deps["next"])
|
|
1927
|
+
return { name: "react", version: deps["react"] };
|
|
1928
|
+
return { name: null, version: null };
|
|
1929
|
+
}
|
|
1930
|
+
function detectStyling(deps, devDeps) {
|
|
1931
|
+
const styling = [];
|
|
1932
|
+
const allDeps = { ...deps, ...devDeps };
|
|
1933
|
+
if (allDeps["tailwindcss"])
|
|
1934
|
+
styling.push(`tailwind@${allDeps["tailwindcss"]}`);
|
|
1935
|
+
if (allDeps["styled-components"])
|
|
1936
|
+
styling.push("styled-components");
|
|
1937
|
+
if (allDeps["@emotion/react"])
|
|
1938
|
+
styling.push("emotion");
|
|
1939
|
+
if (allDeps["sass"])
|
|
1940
|
+
styling.push("sass");
|
|
1941
|
+
if (allDeps["less"])
|
|
1942
|
+
styling.push("less");
|
|
1943
|
+
return styling.length > 0 ? styling : ["css"];
|
|
1944
|
+
}
|
|
1945
|
+
function detectAuth(deps) {
|
|
1946
|
+
if (deps["better-auth"])
|
|
1947
|
+
return "better-auth";
|
|
1948
|
+
if (deps["next-auth"])
|
|
1949
|
+
return "next-auth";
|
|
1950
|
+
if (deps["@clerk/nextjs"])
|
|
1951
|
+
return "clerk";
|
|
1952
|
+
if (deps["@auth0/nextjs-auth0"])
|
|
1953
|
+
return "auth0";
|
|
1954
|
+
if (deps["@supabase/supabase-js"])
|
|
1955
|
+
return "supabase";
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
function detectDatabase(deps) {
|
|
1959
|
+
if (deps["drizzle-orm"])
|
|
1960
|
+
return "drizzle";
|
|
1961
|
+
if (deps["@prisma/client"])
|
|
1962
|
+
return "prisma";
|
|
1963
|
+
if (deps["typeorm"])
|
|
1964
|
+
return "typeorm";
|
|
1965
|
+
if (deps["mongoose"])
|
|
1966
|
+
return "mongoose";
|
|
1967
|
+
if (deps["pg"])
|
|
1968
|
+
return "pg";
|
|
1969
|
+
if (deps["mysql2"])
|
|
1970
|
+
return "mysql";
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
function generateRecommendations(result) {
|
|
1974
|
+
const recs = [];
|
|
1975
|
+
if (result.packageManager !== "bun") {
|
|
1976
|
+
recs.push(`Migrar ${result.packageManager} -> bun (nimbus codemod bun)`);
|
|
1977
|
+
}
|
|
1978
|
+
if (result.framework === "nextjs" && result.frameworkVersion) {
|
|
1979
|
+
const majorVersion = parseInt(result.frameworkVersion.replace(/[^0-9]/g, "").slice(0, 2));
|
|
1980
|
+
if (majorVersion < 16) {
|
|
1981
|
+
recs.push(`Atualizar Next.js ${result.frameworkVersion} -> 16 (nimbus upgrade next)`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const tailwind = result.styling.find((s) => s.startsWith("tailwind"));
|
|
1985
|
+
if (tailwind) {
|
|
1986
|
+
const version = tailwind.split("@")[1] || "";
|
|
1987
|
+
if (version.startsWith("3")) {
|
|
1988
|
+
recs.push(`Atualizar Tailwind 3 -> 4 (nimbus upgrade tailwind)`);
|
|
1989
|
+
}
|
|
1990
|
+
} else if (!result.styling.includes("tailwind")) {
|
|
1991
|
+
recs.push(`Considerar adicionar Tailwind CSS (nimbus add tailwind)`);
|
|
1992
|
+
}
|
|
1993
|
+
if (result.dependencies["react"]) {
|
|
1994
|
+
const reactVersion = result.dependencies["react"];
|
|
1995
|
+
if (reactVersion.startsWith("18") || reactVersion.startsWith("^18")) {
|
|
1996
|
+
recs.push(`Atualizar React 18 -> 19 (nimbus upgrade react)`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (result.database === "prisma") {
|
|
2000
|
+
recs.push(`Considerar migrar Prisma -> Drizzle (nimbus codemod drizzle)`);
|
|
2001
|
+
} else if (!result.database && result.framework === "nextjs") {
|
|
2002
|
+
recs.push(`Considerar adicionar banco de dados (nimbus add db)`);
|
|
2003
|
+
}
|
|
2004
|
+
if (!result.auth && result.framework === "nextjs") {
|
|
2005
|
+
recs.push(`Considerar adicionar autenticacao (nimbus add auth)`);
|
|
2006
|
+
} else if (result.auth === "next-auth") {
|
|
2007
|
+
recs.push(`Considerar migrar NextAuth -> Better Auth`);
|
|
2008
|
+
}
|
|
2009
|
+
if (result.monorepo === "workspaces" && !result.monorepo) {
|
|
2010
|
+
recs.push(`Considerar usar Turborepo para monorepo (nimbus add monorepo)`);
|
|
2011
|
+
}
|
|
2012
|
+
return recs;
|
|
2013
|
+
}
|
|
2014
|
+
async function analyze(args) {
|
|
2015
|
+
const targetDir = args[0] || ".";
|
|
2016
|
+
const absoluteDir = targetDir.startsWith("/") ? targetDir : join2(process.cwd(), targetDir);
|
|
2017
|
+
console.log();
|
|
2018
|
+
console.log(import_picocolors4.default.cyan(" Analisando projeto..."));
|
|
2019
|
+
console.log();
|
|
2020
|
+
const pkgPath = join2(absoluteDir, "package.json");
|
|
2021
|
+
if (!existsSync(pkgPath)) {
|
|
2022
|
+
console.log(import_picocolors4.default.red(" Erro: package.json nao encontrado"));
|
|
2023
|
+
console.log(import_picocolors4.default.dim(` Diretorio: ${absoluteDir}`));
|
|
2024
|
+
process.exit(1);
|
|
2025
|
+
}
|
|
2026
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2027
|
+
const deps = pkg.dependencies || {};
|
|
2028
|
+
const devDeps = pkg.devDependencies || {};
|
|
2029
|
+
const framework = detectFramework(deps);
|
|
2030
|
+
const result = {
|
|
2031
|
+
name: pkg.name || "unknown",
|
|
2032
|
+
version: pkg.version || "0.0.0",
|
|
2033
|
+
framework: framework.name,
|
|
2034
|
+
frameworkVersion: framework.version,
|
|
2035
|
+
styling: detectStyling(deps, devDeps),
|
|
2036
|
+
packageManager: detectPackageManager(absoluteDir),
|
|
2037
|
+
monorepo: detectMonorepo(absoluteDir, pkg),
|
|
2038
|
+
auth: detectAuth(deps),
|
|
2039
|
+
database: detectDatabase(deps),
|
|
2040
|
+
typescript: existsSync(join2(absoluteDir, "tsconfig.json")),
|
|
2041
|
+
dependencies: deps,
|
|
2042
|
+
devDependencies: devDeps,
|
|
2043
|
+
recommendations: []
|
|
2044
|
+
};
|
|
2045
|
+
result.recommendations = generateRecommendations(result);
|
|
2046
|
+
console.log(import_picocolors4.default.bold(" Projeto: ") + import_picocolors4.default.cyan(result.name) + import_picocolors4.default.dim(` v${result.version}`));
|
|
2047
|
+
console.log();
|
|
2048
|
+
console.log(import_picocolors4.default.bold(" Stack Detectada:"));
|
|
2049
|
+
console.log(` Framework: ${result.framework ? import_picocolors4.default.green(result.framework + "@" + result.frameworkVersion) : import_picocolors4.default.dim("nenhum")}`);
|
|
2050
|
+
console.log(` Styling: ${result.styling.map((s) => import_picocolors4.default.green(s)).join(", ")}`);
|
|
2051
|
+
console.log(` Package Manager: ${result.packageManager === "bun" ? import_picocolors4.default.green(result.packageManager) : import_picocolors4.default.yellow(result.packageManager)}`);
|
|
2052
|
+
console.log(` TypeScript: ${result.typescript ? import_picocolors4.default.green("sim") : import_picocolors4.default.dim("nao")}`);
|
|
2053
|
+
console.log(` Monorepo: ${result.monorepo ? import_picocolors4.default.green(result.monorepo) : import_picocolors4.default.dim("nao")}`);
|
|
2054
|
+
console.log(` Auth: ${result.auth ? import_picocolors4.default.green(result.auth) : import_picocolors4.default.dim("nenhum")}`);
|
|
2055
|
+
console.log(` Database: ${result.database ? import_picocolors4.default.green(result.database) : import_picocolors4.default.dim("nenhum")}`);
|
|
2056
|
+
console.log();
|
|
2057
|
+
if (result.recommendations.length > 0) {
|
|
2058
|
+
console.log(import_picocolors4.default.bold(" Recomendacoes:"));
|
|
2059
|
+
result.recommendations.forEach((rec, i) => {
|
|
2060
|
+
console.log(import_picocolors4.default.yellow(` ${i + 1}. ${rec}`));
|
|
2061
|
+
});
|
|
2062
|
+
console.log();
|
|
2063
|
+
} else {
|
|
2064
|
+
console.log(import_picocolors4.default.green(" Projeto ja esta na stack recomendada!"));
|
|
2065
|
+
console.log();
|
|
2066
|
+
}
|
|
2067
|
+
if (args.includes("--json")) {
|
|
2068
|
+
console.log(import_picocolors4.default.dim(" JSON:"));
|
|
2069
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2070
|
+
}
|
|
2071
|
+
return result;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/commands/upgrade.ts
|
|
2075
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
2076
|
+
var UPGRADE_PLANS = {
|
|
2077
|
+
next: (current) => {
|
|
2078
|
+
const major = parseInt(current.replace(/[^0-9]/g, "").slice(0, 2));
|
|
2079
|
+
if (major >= 16)
|
|
2080
|
+
return null;
|
|
2081
|
+
return {
|
|
2082
|
+
current,
|
|
2083
|
+
target: "16.x",
|
|
2084
|
+
complexity: major < 15 ? "high" : "medium",
|
|
2085
|
+
breakingChanges: [
|
|
2086
|
+
"next/image: Mudancas na API de otimizacao",
|
|
2087
|
+
"Middleware: Novo formato de config",
|
|
2088
|
+
"next.config: Algumas opcoes depreciadas",
|
|
2089
|
+
"Turbopack: Agora e o bundler padrao"
|
|
2090
|
+
],
|
|
2091
|
+
steps: [
|
|
2092
|
+
"Atualizar next para ^16.0.0",
|
|
2093
|
+
"Atualizar react para ^19.0.0",
|
|
2094
|
+
"Atualizar react-dom para ^19.0.0",
|
|
2095
|
+
"Revisar next.config.ts",
|
|
2096
|
+
"Testar build: bun run build",
|
|
2097
|
+
"Testar dev: bun dev"
|
|
2098
|
+
]
|
|
2099
|
+
};
|
|
2100
|
+
},
|
|
2101
|
+
react: (current) => {
|
|
2102
|
+
if (current.startsWith("19") || current.startsWith("^19"))
|
|
2103
|
+
return null;
|
|
2104
|
+
return {
|
|
2105
|
+
current,
|
|
2106
|
+
target: "19.x",
|
|
2107
|
+
complexity: "medium",
|
|
2108
|
+
breakingChanges: [
|
|
2109
|
+
"forwardRef: Nao mais necessario, ref e prop regular",
|
|
2110
|
+
"useContext: Pode ser substituido por use(Context)",
|
|
2111
|
+
"Suspense: Mudancas em fallback behavior",
|
|
2112
|
+
"Async components: Novo suporte nativo"
|
|
2113
|
+
],
|
|
2114
|
+
steps: [
|
|
2115
|
+
"Atualizar react para ^19.0.0",
|
|
2116
|
+
"Atualizar react-dom para ^19.0.0",
|
|
2117
|
+
"Atualizar @types/react para ^19.0.0",
|
|
2118
|
+
"Remover forwardRef (usar ref como prop)",
|
|
2119
|
+
"Revisar Suspense boundaries",
|
|
2120
|
+
"Testar todos os componentes"
|
|
2121
|
+
]
|
|
2122
|
+
};
|
|
2123
|
+
},
|
|
2124
|
+
tailwind: (current) => {
|
|
2125
|
+
if (current.startsWith("4") || current.startsWith("^4"))
|
|
2126
|
+
return null;
|
|
2127
|
+
return {
|
|
2128
|
+
current,
|
|
2129
|
+
target: "4.x",
|
|
2130
|
+
complexity: "medium",
|
|
2131
|
+
breakingChanges: [
|
|
2132
|
+
"Config: Agora e CSS-first (nao mais tailwind.config.js)",
|
|
2133
|
+
"@apply: Sintaxe mudou",
|
|
2134
|
+
"Cores: Novo sistema de tokens",
|
|
2135
|
+
"Plugins: API diferente"
|
|
2136
|
+
],
|
|
2137
|
+
steps: [
|
|
2138
|
+
"Atualizar tailwindcss para ^4.0.0",
|
|
2139
|
+
"Converter tailwind.config.js para CSS",
|
|
2140
|
+
"Atualizar globals.css com @import 'tailwindcss'",
|
|
2141
|
+
"Revisar @apply usages",
|
|
2142
|
+
"Atualizar plugins para v4",
|
|
2143
|
+
"Testar todas as paginas"
|
|
2144
|
+
]
|
|
2145
|
+
};
|
|
2146
|
+
},
|
|
2147
|
+
bun: () => ({
|
|
2148
|
+
current: "pnpm/npm/yarn",
|
|
2149
|
+
target: "bun",
|
|
2150
|
+
complexity: "low",
|
|
2151
|
+
breakingChanges: [
|
|
2152
|
+
"Lockfile: Formato diferente (bun.lockb)",
|
|
2153
|
+
"Scripts: Alguns podem precisar ajuste",
|
|
2154
|
+
"Workspaces: Sintaxe levemente diferente"
|
|
2155
|
+
],
|
|
2156
|
+
steps: [
|
|
2157
|
+
"Remover node_modules",
|
|
2158
|
+
"Remover pnpm-lock.yaml / package-lock.json / yarn.lock",
|
|
2159
|
+
"Executar: bun install",
|
|
2160
|
+
"Atualizar scripts no package.json (npx -> bunx)",
|
|
2161
|
+
"Atualizar CI/CD configs",
|
|
2162
|
+
"Testar: bun dev, bun build"
|
|
2163
|
+
]
|
|
2164
|
+
}),
|
|
2165
|
+
drizzle: () => ({
|
|
2166
|
+
current: "prisma",
|
|
2167
|
+
target: "drizzle",
|
|
2168
|
+
complexity: "high",
|
|
2169
|
+
breakingChanges: [
|
|
2170
|
+
"Schema: Formato TypeScript (nao mais .prisma)",
|
|
2171
|
+
"Queries: API completamente diferente",
|
|
2172
|
+
"Migrations: Sistema diferente",
|
|
2173
|
+
"Relations: Declaracao diferente"
|
|
2174
|
+
],
|
|
2175
|
+
steps: [
|
|
2176
|
+
"Instalar drizzle-orm e drizzle-kit",
|
|
2177
|
+
"Converter schema.prisma para drizzle/schema.ts",
|
|
2178
|
+
"Configurar drizzle.config.ts",
|
|
2179
|
+
"Gerar migrations: bunx drizzle-kit generate",
|
|
2180
|
+
"Atualizar todas as queries",
|
|
2181
|
+
"Atualizar auth config (se usar)",
|
|
2182
|
+
"Remover @prisma/client e prisma",
|
|
2183
|
+
"Testar todas as operacoes de banco"
|
|
2184
|
+
]
|
|
2185
|
+
})
|
|
2186
|
+
};
|
|
2187
|
+
async function upgrade(args) {
|
|
2188
|
+
const showPlan = args.includes("--plan");
|
|
2189
|
+
const target = args.find((a) => !a.startsWith("-"));
|
|
2190
|
+
console.log();
|
|
2191
|
+
if (showPlan || !target) {
|
|
2192
|
+
console.log(import_picocolors5.default.cyan(" Analisando projeto para plano de upgrade..."));
|
|
2193
|
+
console.log();
|
|
2194
|
+
const analysis = await analyze([".", "--quiet"]);
|
|
2195
|
+
console.log(import_picocolors5.default.bold(" Upgrades Disponiveis:"));
|
|
2196
|
+
console.log();
|
|
2197
|
+
let hasUpgrades = false;
|
|
2198
|
+
if (analysis.frameworkVersion && analysis.framework === "nextjs") {
|
|
2199
|
+
const planFn = UPGRADE_PLANS["next"];
|
|
2200
|
+
if (planFn) {
|
|
2201
|
+
const plan = planFn(analysis.frameworkVersion);
|
|
2202
|
+
if (plan) {
|
|
2203
|
+
hasUpgrades = true;
|
|
2204
|
+
printUpgradePlan("Next.js", plan);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (analysis.dependencies["react"]) {
|
|
2209
|
+
const planFn = UPGRADE_PLANS["react"];
|
|
2210
|
+
if (planFn) {
|
|
2211
|
+
const plan = planFn(analysis.dependencies["react"]);
|
|
2212
|
+
if (plan) {
|
|
2213
|
+
hasUpgrades = true;
|
|
2214
|
+
printUpgradePlan("React", plan);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const tailwindDep = analysis.dependencies["tailwindcss"] || analysis.devDependencies["tailwindcss"];
|
|
2219
|
+
if (tailwindDep) {
|
|
2220
|
+
const planFn = UPGRADE_PLANS["tailwind"];
|
|
2221
|
+
if (planFn) {
|
|
2222
|
+
const plan = planFn(tailwindDep);
|
|
2223
|
+
if (plan) {
|
|
2224
|
+
hasUpgrades = true;
|
|
2225
|
+
printUpgradePlan("Tailwind CSS", plan);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (analysis.packageManager !== "bun") {
|
|
2230
|
+
const planFn = UPGRADE_PLANS["bun"];
|
|
2231
|
+
if (planFn) {
|
|
2232
|
+
const plan = planFn("");
|
|
2233
|
+
if (plan) {
|
|
2234
|
+
hasUpgrades = true;
|
|
2235
|
+
printUpgradePlan("Package Manager", plan);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (analysis.database === "prisma") {
|
|
2240
|
+
const planFn = UPGRADE_PLANS["drizzle"];
|
|
2241
|
+
if (planFn) {
|
|
2242
|
+
const plan = planFn("");
|
|
2243
|
+
if (plan) {
|
|
2244
|
+
hasUpgrades = true;
|
|
2245
|
+
printUpgradePlan("Database", plan);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (!hasUpgrades) {
|
|
2250
|
+
console.log(import_picocolors5.default.green(" Projeto ja esta atualizado!"));
|
|
2251
|
+
}
|
|
2252
|
+
console.log();
|
|
2253
|
+
console.log(import_picocolors5.default.dim(" Para executar um upgrade especifico:"));
|
|
2254
|
+
console.log(import_picocolors5.default.dim(" nimbus upgrade next"));
|
|
2255
|
+
console.log(import_picocolors5.default.dim(" nimbus upgrade tailwind"));
|
|
2256
|
+
console.log(import_picocolors5.default.dim(" nimbus upgrade bun"));
|
|
2257
|
+
console.log();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
console.log(import_picocolors5.default.yellow(` Upgrade ${target} ainda nao implementado.`));
|
|
2261
|
+
console.log(import_picocolors5.default.dim(" Por enquanto, siga os passos do --plan manualmente."));
|
|
2262
|
+
console.log();
|
|
2263
|
+
}
|
|
2264
|
+
function printUpgradePlan(name, plan) {
|
|
2265
|
+
const complexityColor = {
|
|
2266
|
+
low: import_picocolors5.default.green,
|
|
2267
|
+
medium: import_picocolors5.default.yellow,
|
|
2268
|
+
high: import_picocolors5.default.red
|
|
2269
|
+
};
|
|
2270
|
+
console.log(` ${import_picocolors5.default.bold(name)}`);
|
|
2271
|
+
console.log(` ${import_picocolors5.default.dim("Atual:")} ${plan.current} ${import_picocolors5.default.dim("->")} ${import_picocolors5.default.cyan(plan.target)}`);
|
|
2272
|
+
console.log(` ${import_picocolors5.default.dim("Complexidade:")} ${complexityColor[plan.complexity](plan.complexity)}`);
|
|
2273
|
+
console.log();
|
|
2274
|
+
console.log(` ${import_picocolors5.default.dim("Breaking Changes:")}`);
|
|
2275
|
+
plan.breakingChanges.forEach((bc) => {
|
|
2276
|
+
console.log(` ${import_picocolors5.default.yellow("!")} ${bc}`);
|
|
2277
|
+
});
|
|
2278
|
+
console.log();
|
|
2279
|
+
console.log(` ${import_picocolors5.default.dim("Passos:")}`);
|
|
2280
|
+
plan.steps.forEach((step, i) => {
|
|
2281
|
+
console.log(` ${import_picocolors5.default.dim(`${i + 1}.`)} ${step}`);
|
|
2282
|
+
});
|
|
2283
|
+
console.log();
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1891
2286
|
// src/index.ts
|
|
1892
2287
|
var PACKAGE_NAME = "@nimbuslab/cli";
|
|
1893
|
-
var CURRENT_VERSION = "0.
|
|
2288
|
+
var CURRENT_VERSION = "0.9.0";
|
|
1894
2289
|
var LOGO = `
|
|
1895
2290
|
\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
1896
2291
|
\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
@@ -1926,18 +2321,18 @@ function showUpdateNotice(latestVersion) {
|
|
|
1926
2321
|
const line2 = ` Update with: ${command}`;
|
|
1927
2322
|
const maxLen = Math.max(line1.length, line2.length);
|
|
1928
2323
|
const border = "\u2500".repeat(maxLen + 2);
|
|
1929
|
-
console.log(
|
|
1930
|
-
console.log(
|
|
1931
|
-
console.log(
|
|
1932
|
-
console.log(
|
|
2324
|
+
console.log(import_picocolors6.default.yellow(` \u250C${border}\u2510`));
|
|
2325
|
+
console.log(import_picocolors6.default.yellow(` \u2502`) + import_picocolors6.default.white(line1.padEnd(maxLen + 1)) + import_picocolors6.default.yellow(`\u2502`));
|
|
2326
|
+
console.log(import_picocolors6.default.yellow(` \u2502`) + import_picocolors6.default.cyan(line2.padEnd(maxLen + 1)) + import_picocolors6.default.yellow(`\u2502`));
|
|
2327
|
+
console.log(import_picocolors6.default.yellow(` \u2514${border}\u2518`));
|
|
1933
2328
|
console.log();
|
|
1934
2329
|
}
|
|
1935
2330
|
async function main() {
|
|
1936
2331
|
const args = process.argv.slice(2);
|
|
1937
2332
|
const command = args[0];
|
|
1938
|
-
console.log(
|
|
1939
|
-
console.log(
|
|
1940
|
-
console.log(
|
|
2333
|
+
console.log(import_picocolors6.default.cyan(LOGO));
|
|
2334
|
+
console.log(import_picocolors6.default.white(" nimbuslab CLI"));
|
|
2335
|
+
console.log(import_picocolors6.default.dim(" Create awesome projects"));
|
|
1941
2336
|
console.log();
|
|
1942
2337
|
const latestVersion = await checkForUpdates();
|
|
1943
2338
|
if (latestVersion) {
|
|
@@ -1945,46 +2340,60 @@ async function main() {
|
|
|
1945
2340
|
}
|
|
1946
2341
|
if (!command || command === "create") {
|
|
1947
2342
|
await create(args.slice(1));
|
|
2343
|
+
} else if (command === "analyze") {
|
|
2344
|
+
await analyze(args.slice(1));
|
|
2345
|
+
} else if (command === "upgrade") {
|
|
2346
|
+
await upgrade(args.slice(1));
|
|
1948
2347
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
1949
2348
|
showHelp();
|
|
1950
2349
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
1951
2350
|
showVersion();
|
|
1952
2351
|
} else {
|
|
1953
|
-
console.log(
|
|
2352
|
+
console.log(import_picocolors6.default.red(`Unknown command: ${command}`));
|
|
1954
2353
|
showHelp();
|
|
1955
2354
|
process.exit(1);
|
|
1956
2355
|
}
|
|
1957
2356
|
}
|
|
1958
2357
|
function showHelp() {
|
|
1959
2358
|
console.log(`
|
|
1960
|
-
${
|
|
2359
|
+
${import_picocolors6.default.bold("Usage:")} nimbus [command] [options]
|
|
1961
2360
|
|
|
1962
|
-
${
|
|
2361
|
+
${import_picocolors6.default.bold("Commands:")}
|
|
1963
2362
|
create [name] Create a new project
|
|
2363
|
+
analyze [dir] Analyze project stack
|
|
2364
|
+
upgrade [target] Upgrade dependencies
|
|
1964
2365
|
help Show this help
|
|
1965
2366
|
version Show version
|
|
1966
2367
|
|
|
1967
|
-
${
|
|
2368
|
+
${import_picocolors6.default.bold("Templates:")}
|
|
1968
2369
|
--landing Landing page (Next.js 16 + Tailwind 4 + shadcn)
|
|
1969
|
-
--app Web app (Landing + Better Auth +
|
|
2370
|
+
--app Web app (Landing + Better Auth + Drizzle)
|
|
1970
2371
|
--turborepo Monorepo (Turborepo + apps/packages)
|
|
1971
2372
|
|
|
1972
|
-
${
|
|
2373
|
+
${import_picocolors6.default.bold("Analyze & Upgrade:")}
|
|
2374
|
+
analyze . Detect stack and show recommendations
|
|
2375
|
+
analyze --json Output as JSON
|
|
2376
|
+
upgrade --plan Show upgrade plan
|
|
2377
|
+
upgrade next Upgrade Next.js
|
|
2378
|
+
upgrade tailwind Upgrade Tailwind CSS
|
|
2379
|
+
|
|
2380
|
+
${import_picocolors6.default.bold("Options:")}
|
|
1973
2381
|
-y, --yes Accept defaults
|
|
1974
2382
|
--no-git Don't initialize Git
|
|
1975
2383
|
--no-install Don't install dependencies
|
|
1976
2384
|
--template <url> Use custom template
|
|
1977
2385
|
|
|
1978
|
-
${
|
|
1979
|
-
${
|
|
1980
|
-
${
|
|
1981
|
-
${
|
|
2386
|
+
${import_picocolors6.default.bold("Examples:")}
|
|
2387
|
+
${import_picocolors6.default.dim("$")} nimbus create my-landing --landing
|
|
2388
|
+
${import_picocolors6.default.dim("$")} nimbus create my-app --app
|
|
2389
|
+
${import_picocolors6.default.dim("$")} nimbus analyze ./my-project
|
|
2390
|
+
${import_picocolors6.default.dim("$")} nimbus upgrade --plan
|
|
1982
2391
|
`);
|
|
1983
2392
|
}
|
|
1984
2393
|
function showVersion() {
|
|
1985
2394
|
console.log(`${PACKAGE_NAME} v${CURRENT_VERSION}`);
|
|
1986
2395
|
}
|
|
1987
2396
|
main().catch((err) => {
|
|
1988
|
-
console.error(
|
|
2397
|
+
console.error(import_picocolors6.default.red("Erro:"), err.message);
|
|
1989
2398
|
process.exit(1);
|
|
1990
2399
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as p from "@clack/prompts"
|
|
2
|
+
import pc from "picocolors"
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
interface AnalysisResult {
|
|
7
|
+
name: string
|
|
8
|
+
version: string
|
|
9
|
+
framework: string | null
|
|
10
|
+
frameworkVersion: string | null
|
|
11
|
+
styling: string[]
|
|
12
|
+
packageManager: string
|
|
13
|
+
monorepo: string | null
|
|
14
|
+
auth: string | null
|
|
15
|
+
database: string | null
|
|
16
|
+
typescript: boolean
|
|
17
|
+
dependencies: Record<string, string>
|
|
18
|
+
devDependencies: Record<string, string>
|
|
19
|
+
recommendations: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function detectPackageManager(dir: string): string {
|
|
23
|
+
if (existsSync(join(dir, "bun.lockb"))) return "bun"
|
|
24
|
+
if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm"
|
|
25
|
+
if (existsSync(join(dir, "yarn.lock"))) return "yarn"
|
|
26
|
+
if (existsSync(join(dir, "package-lock.json"))) return "npm"
|
|
27
|
+
return "unknown"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detectMonorepo(dir: string, pkg: any): string | null {
|
|
31
|
+
if (existsSync(join(dir, "turbo.json"))) return "turborepo"
|
|
32
|
+
if (existsSync(join(dir, "nx.json"))) return "nx"
|
|
33
|
+
if (existsSync(join(dir, "lerna.json"))) return "lerna"
|
|
34
|
+
if (pkg.workspaces) return "workspaces"
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectFramework(deps: Record<string, string>): { name: string | null; version: string | null } {
|
|
39
|
+
if (deps["next"]) return { name: "nextjs", version: deps["next"] }
|
|
40
|
+
if (deps["@angular/core"]) return { name: "angular", version: deps["@angular/core"] }
|
|
41
|
+
if (deps["vue"]) return { name: "vue", version: deps["vue"] }
|
|
42
|
+
if (deps["svelte"]) return { name: "svelte", version: deps["svelte"] }
|
|
43
|
+
if (deps["react"] && !deps["next"]) return { name: "react", version: deps["react"] }
|
|
44
|
+
return { name: null, version: null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function detectStyling(deps: Record<string, string>, devDeps: Record<string, string>): string[] {
|
|
48
|
+
const styling: string[] = []
|
|
49
|
+
const allDeps = { ...deps, ...devDeps }
|
|
50
|
+
|
|
51
|
+
if (allDeps["tailwindcss"]) styling.push(`tailwind@${allDeps["tailwindcss"]}`)
|
|
52
|
+
if (allDeps["styled-components"]) styling.push("styled-components")
|
|
53
|
+
if (allDeps["@emotion/react"]) styling.push("emotion")
|
|
54
|
+
if (allDeps["sass"]) styling.push("sass")
|
|
55
|
+
if (allDeps["less"]) styling.push("less")
|
|
56
|
+
|
|
57
|
+
return styling.length > 0 ? styling : ["css"]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectAuth(deps: Record<string, string>): string | null {
|
|
61
|
+
if (deps["better-auth"]) return "better-auth"
|
|
62
|
+
if (deps["next-auth"]) return "next-auth"
|
|
63
|
+
if (deps["@clerk/nextjs"]) return "clerk"
|
|
64
|
+
if (deps["@auth0/nextjs-auth0"]) return "auth0"
|
|
65
|
+
if (deps["@supabase/supabase-js"]) return "supabase"
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectDatabase(deps: Record<string, string>): string | null {
|
|
70
|
+
if (deps["drizzle-orm"]) return "drizzle"
|
|
71
|
+
if (deps["@prisma/client"]) return "prisma"
|
|
72
|
+
if (deps["typeorm"]) return "typeorm"
|
|
73
|
+
if (deps["mongoose"]) return "mongoose"
|
|
74
|
+
if (deps["pg"]) return "pg"
|
|
75
|
+
if (deps["mysql2"]) return "mysql"
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function generateRecommendations(result: AnalysisResult): string[] {
|
|
80
|
+
const recs: string[] = []
|
|
81
|
+
|
|
82
|
+
// Package manager
|
|
83
|
+
if (result.packageManager !== "bun") {
|
|
84
|
+
recs.push(`Migrar ${result.packageManager} -> bun (nimbus codemod bun)`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Framework version
|
|
88
|
+
if (result.framework === "nextjs" && result.frameworkVersion) {
|
|
89
|
+
const majorVersion = parseInt(result.frameworkVersion.replace(/[^0-9]/g, "").slice(0, 2))
|
|
90
|
+
if (majorVersion < 16) {
|
|
91
|
+
recs.push(`Atualizar Next.js ${result.frameworkVersion} -> 16 (nimbus upgrade next)`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tailwind
|
|
96
|
+
const tailwind = result.styling.find(s => s.startsWith("tailwind"))
|
|
97
|
+
if (tailwind) {
|
|
98
|
+
const version = tailwind.split("@")[1] || ""
|
|
99
|
+
if (version.startsWith("3")) {
|
|
100
|
+
recs.push(`Atualizar Tailwind 3 -> 4 (nimbus upgrade tailwind)`)
|
|
101
|
+
}
|
|
102
|
+
} else if (!result.styling.includes("tailwind")) {
|
|
103
|
+
recs.push(`Considerar adicionar Tailwind CSS (nimbus add tailwind)`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// React version
|
|
107
|
+
if (result.dependencies["react"]) {
|
|
108
|
+
const reactVersion = result.dependencies["react"]
|
|
109
|
+
if (reactVersion.startsWith("18") || reactVersion.startsWith("^18")) {
|
|
110
|
+
recs.push(`Atualizar React 18 -> 19 (nimbus upgrade react)`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Database
|
|
115
|
+
if (result.database === "prisma") {
|
|
116
|
+
recs.push(`Considerar migrar Prisma -> Drizzle (nimbus codemod drizzle)`)
|
|
117
|
+
} else if (!result.database && result.framework === "nextjs") {
|
|
118
|
+
recs.push(`Considerar adicionar banco de dados (nimbus add db)`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auth
|
|
122
|
+
if (!result.auth && result.framework === "nextjs") {
|
|
123
|
+
recs.push(`Considerar adicionar autenticacao (nimbus add auth)`)
|
|
124
|
+
} else if (result.auth === "next-auth") {
|
|
125
|
+
recs.push(`Considerar migrar NextAuth -> Better Auth`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Monorepo
|
|
129
|
+
if (result.monorepo === "workspaces" && !result.monorepo) {
|
|
130
|
+
recs.push(`Considerar usar Turborepo para monorepo (nimbus add monorepo)`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return recs
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function analyze(args: string[]) {
|
|
137
|
+
const targetDir = args[0] || "."
|
|
138
|
+
const absoluteDir = targetDir.startsWith("/") ? targetDir : join(process.cwd(), targetDir)
|
|
139
|
+
|
|
140
|
+
console.log()
|
|
141
|
+
console.log(pc.cyan(" Analisando projeto..."))
|
|
142
|
+
console.log()
|
|
143
|
+
|
|
144
|
+
// Check if package.json exists
|
|
145
|
+
const pkgPath = join(absoluteDir, "package.json")
|
|
146
|
+
if (!existsSync(pkgPath)) {
|
|
147
|
+
console.log(pc.red(" Erro: package.json nao encontrado"))
|
|
148
|
+
console.log(pc.dim(` Diretorio: ${absoluteDir}`))
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Read package.json
|
|
153
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
154
|
+
const deps = pkg.dependencies || {}
|
|
155
|
+
const devDeps = pkg.devDependencies || {}
|
|
156
|
+
|
|
157
|
+
// Detect everything
|
|
158
|
+
const framework = detectFramework(deps)
|
|
159
|
+
const result: AnalysisResult = {
|
|
160
|
+
name: pkg.name || "unknown",
|
|
161
|
+
version: pkg.version || "0.0.0",
|
|
162
|
+
framework: framework.name,
|
|
163
|
+
frameworkVersion: framework.version,
|
|
164
|
+
styling: detectStyling(deps, devDeps),
|
|
165
|
+
packageManager: detectPackageManager(absoluteDir),
|
|
166
|
+
monorepo: detectMonorepo(absoluteDir, pkg),
|
|
167
|
+
auth: detectAuth(deps),
|
|
168
|
+
database: detectDatabase(deps),
|
|
169
|
+
typescript: existsSync(join(absoluteDir, "tsconfig.json")),
|
|
170
|
+
dependencies: deps,
|
|
171
|
+
devDependencies: devDeps,
|
|
172
|
+
recommendations: [],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Generate recommendations
|
|
176
|
+
result.recommendations = generateRecommendations(result)
|
|
177
|
+
|
|
178
|
+
// Display results
|
|
179
|
+
console.log(pc.bold(" Projeto: ") + pc.cyan(result.name) + pc.dim(` v${result.version}`))
|
|
180
|
+
console.log()
|
|
181
|
+
|
|
182
|
+
console.log(pc.bold(" Stack Detectada:"))
|
|
183
|
+
console.log(` Framework: ${result.framework ? pc.green(result.framework + "@" + result.frameworkVersion) : pc.dim("nenhum")}`)
|
|
184
|
+
console.log(` Styling: ${result.styling.map(s => pc.green(s)).join(", ")}`)
|
|
185
|
+
console.log(` Package Manager: ${result.packageManager === "bun" ? pc.green(result.packageManager) : pc.yellow(result.packageManager)}`)
|
|
186
|
+
console.log(` TypeScript: ${result.typescript ? pc.green("sim") : pc.dim("nao")}`)
|
|
187
|
+
console.log(` Monorepo: ${result.monorepo ? pc.green(result.monorepo) : pc.dim("nao")}`)
|
|
188
|
+
console.log(` Auth: ${result.auth ? pc.green(result.auth) : pc.dim("nenhum")}`)
|
|
189
|
+
console.log(` Database: ${result.database ? pc.green(result.database) : pc.dim("nenhum")}`)
|
|
190
|
+
console.log()
|
|
191
|
+
|
|
192
|
+
if (result.recommendations.length > 0) {
|
|
193
|
+
console.log(pc.bold(" Recomendacoes:"))
|
|
194
|
+
result.recommendations.forEach((rec, i) => {
|
|
195
|
+
console.log(pc.yellow(` ${i + 1}. ${rec}`))
|
|
196
|
+
})
|
|
197
|
+
console.log()
|
|
198
|
+
} else {
|
|
199
|
+
console.log(pc.green(" Projeto ja esta na stack recomendada!"))
|
|
200
|
+
console.log()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// JSON output option
|
|
204
|
+
if (args.includes("--json")) {
|
|
205
|
+
console.log(pc.dim(" JSON:"))
|
|
206
|
+
console.log(JSON.stringify(result, null, 2))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import * as p from "@clack/prompts"
|
|
2
|
+
import pc from "picocolors"
|
|
3
|
+
import { analyze } from "./analyze"
|
|
4
|
+
|
|
5
|
+
interface UpgradePlan {
|
|
6
|
+
current: string
|
|
7
|
+
target: string
|
|
8
|
+
breakingChanges: string[]
|
|
9
|
+
steps: string[]
|
|
10
|
+
complexity: "low" | "medium" | "high"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const UPGRADE_PLANS: Record<string, (currentVersion: string) => UpgradePlan | null> = {
|
|
14
|
+
next: (current) => {
|
|
15
|
+
const major = parseInt(current.replace(/[^0-9]/g, "").slice(0, 2))
|
|
16
|
+
if (major >= 16) return null
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
current: current,
|
|
20
|
+
target: "16.x",
|
|
21
|
+
complexity: major < 15 ? "high" : "medium",
|
|
22
|
+
breakingChanges: [
|
|
23
|
+
"next/image: Mudancas na API de otimizacao",
|
|
24
|
+
"Middleware: Novo formato de config",
|
|
25
|
+
"next.config: Algumas opcoes depreciadas",
|
|
26
|
+
"Turbopack: Agora e o bundler padrao",
|
|
27
|
+
],
|
|
28
|
+
steps: [
|
|
29
|
+
"Atualizar next para ^16.0.0",
|
|
30
|
+
"Atualizar react para ^19.0.0",
|
|
31
|
+
"Atualizar react-dom para ^19.0.0",
|
|
32
|
+
"Revisar next.config.ts",
|
|
33
|
+
"Testar build: bun run build",
|
|
34
|
+
"Testar dev: bun dev",
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
react: (current) => {
|
|
40
|
+
if (current.startsWith("19") || current.startsWith("^19")) return null
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
current: current,
|
|
44
|
+
target: "19.x",
|
|
45
|
+
complexity: "medium",
|
|
46
|
+
breakingChanges: [
|
|
47
|
+
"forwardRef: Nao mais necessario, ref e prop regular",
|
|
48
|
+
"useContext: Pode ser substituido por use(Context)",
|
|
49
|
+
"Suspense: Mudancas em fallback behavior",
|
|
50
|
+
"Async components: Novo suporte nativo",
|
|
51
|
+
],
|
|
52
|
+
steps: [
|
|
53
|
+
"Atualizar react para ^19.0.0",
|
|
54
|
+
"Atualizar react-dom para ^19.0.0",
|
|
55
|
+
"Atualizar @types/react para ^19.0.0",
|
|
56
|
+
"Remover forwardRef (usar ref como prop)",
|
|
57
|
+
"Revisar Suspense boundaries",
|
|
58
|
+
"Testar todos os componentes",
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
tailwind: (current) => {
|
|
64
|
+
if (current.startsWith("4") || current.startsWith("^4")) return null
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
current: current,
|
|
68
|
+
target: "4.x",
|
|
69
|
+
complexity: "medium",
|
|
70
|
+
breakingChanges: [
|
|
71
|
+
"Config: Agora e CSS-first (nao mais tailwind.config.js)",
|
|
72
|
+
"@apply: Sintaxe mudou",
|
|
73
|
+
"Cores: Novo sistema de tokens",
|
|
74
|
+
"Plugins: API diferente",
|
|
75
|
+
],
|
|
76
|
+
steps: [
|
|
77
|
+
"Atualizar tailwindcss para ^4.0.0",
|
|
78
|
+
"Converter tailwind.config.js para CSS",
|
|
79
|
+
"Atualizar globals.css com @import 'tailwindcss'",
|
|
80
|
+
"Revisar @apply usages",
|
|
81
|
+
"Atualizar plugins para v4",
|
|
82
|
+
"Testar todas as paginas",
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
bun: () => ({
|
|
88
|
+
current: "pnpm/npm/yarn",
|
|
89
|
+
target: "bun",
|
|
90
|
+
complexity: "low",
|
|
91
|
+
breakingChanges: [
|
|
92
|
+
"Lockfile: Formato diferente (bun.lockb)",
|
|
93
|
+
"Scripts: Alguns podem precisar ajuste",
|
|
94
|
+
"Workspaces: Sintaxe levemente diferente",
|
|
95
|
+
],
|
|
96
|
+
steps: [
|
|
97
|
+
"Remover node_modules",
|
|
98
|
+
"Remover pnpm-lock.yaml / package-lock.json / yarn.lock",
|
|
99
|
+
"Executar: bun install",
|
|
100
|
+
"Atualizar scripts no package.json (npx -> bunx)",
|
|
101
|
+
"Atualizar CI/CD configs",
|
|
102
|
+
"Testar: bun dev, bun build",
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
drizzle: () => ({
|
|
107
|
+
current: "prisma",
|
|
108
|
+
target: "drizzle",
|
|
109
|
+
complexity: "high",
|
|
110
|
+
breakingChanges: [
|
|
111
|
+
"Schema: Formato TypeScript (nao mais .prisma)",
|
|
112
|
+
"Queries: API completamente diferente",
|
|
113
|
+
"Migrations: Sistema diferente",
|
|
114
|
+
"Relations: Declaracao diferente",
|
|
115
|
+
],
|
|
116
|
+
steps: [
|
|
117
|
+
"Instalar drizzle-orm e drizzle-kit",
|
|
118
|
+
"Converter schema.prisma para drizzle/schema.ts",
|
|
119
|
+
"Configurar drizzle.config.ts",
|
|
120
|
+
"Gerar migrations: bunx drizzle-kit generate",
|
|
121
|
+
"Atualizar todas as queries",
|
|
122
|
+
"Atualizar auth config (se usar)",
|
|
123
|
+
"Remover @prisma/client e prisma",
|
|
124
|
+
"Testar todas as operacoes de banco",
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function upgrade(args: string[]) {
|
|
130
|
+
const showPlan = args.includes("--plan")
|
|
131
|
+
const target = args.find(a => !a.startsWith("-"))
|
|
132
|
+
|
|
133
|
+
console.log()
|
|
134
|
+
|
|
135
|
+
if (showPlan || !target) {
|
|
136
|
+
// Analyze first
|
|
137
|
+
console.log(pc.cyan(" Analisando projeto para plano de upgrade..."))
|
|
138
|
+
console.log()
|
|
139
|
+
|
|
140
|
+
const analysis = await analyze([".", "--quiet"])
|
|
141
|
+
|
|
142
|
+
console.log(pc.bold(" Upgrades Disponiveis:"))
|
|
143
|
+
console.log()
|
|
144
|
+
|
|
145
|
+
let hasUpgrades = false
|
|
146
|
+
|
|
147
|
+
// Check Next.js
|
|
148
|
+
if (analysis.frameworkVersion && analysis.framework === "nextjs") {
|
|
149
|
+
const planFn = UPGRADE_PLANS["next"]
|
|
150
|
+
if (planFn) {
|
|
151
|
+
const plan = planFn(analysis.frameworkVersion)
|
|
152
|
+
if (plan) {
|
|
153
|
+
hasUpgrades = true
|
|
154
|
+
printUpgradePlan("Next.js", plan)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check React
|
|
160
|
+
if (analysis.dependencies["react"]) {
|
|
161
|
+
const planFn = UPGRADE_PLANS["react"]
|
|
162
|
+
if (planFn) {
|
|
163
|
+
const plan = planFn(analysis.dependencies["react"])
|
|
164
|
+
if (plan) {
|
|
165
|
+
hasUpgrades = true
|
|
166
|
+
printUpgradePlan("React", plan)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check Tailwind
|
|
172
|
+
const tailwindDep = analysis.dependencies["tailwindcss"] || analysis.devDependencies["tailwindcss"]
|
|
173
|
+
if (tailwindDep) {
|
|
174
|
+
const planFn = UPGRADE_PLANS["tailwind"]
|
|
175
|
+
if (planFn) {
|
|
176
|
+
const plan = planFn(tailwindDep)
|
|
177
|
+
if (plan) {
|
|
178
|
+
hasUpgrades = true
|
|
179
|
+
printUpgradePlan("Tailwind CSS", plan)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check package manager
|
|
185
|
+
if (analysis.packageManager !== "bun") {
|
|
186
|
+
const planFn = UPGRADE_PLANS["bun"]
|
|
187
|
+
if (planFn) {
|
|
188
|
+
const plan = planFn("")
|
|
189
|
+
if (plan) {
|
|
190
|
+
hasUpgrades = true
|
|
191
|
+
printUpgradePlan("Package Manager", plan)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check Prisma -> Drizzle
|
|
197
|
+
if (analysis.database === "prisma") {
|
|
198
|
+
const planFn = UPGRADE_PLANS["drizzle"]
|
|
199
|
+
if (planFn) {
|
|
200
|
+
const plan = planFn("")
|
|
201
|
+
if (plan) {
|
|
202
|
+
hasUpgrades = true
|
|
203
|
+
printUpgradePlan("Database", plan)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!hasUpgrades) {
|
|
209
|
+
console.log(pc.green(" Projeto ja esta atualizado!"))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log()
|
|
213
|
+
console.log(pc.dim(" Para executar um upgrade especifico:"))
|
|
214
|
+
console.log(pc.dim(" nimbus upgrade next"))
|
|
215
|
+
console.log(pc.dim(" nimbus upgrade tailwind"))
|
|
216
|
+
console.log(pc.dim(" nimbus upgrade bun"))
|
|
217
|
+
console.log()
|
|
218
|
+
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Execute specific upgrade
|
|
223
|
+
console.log(pc.yellow(` Upgrade ${target} ainda nao implementado.`))
|
|
224
|
+
console.log(pc.dim(" Por enquanto, siga os passos do --plan manualmente."))
|
|
225
|
+
console.log()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function printUpgradePlan(name: string, plan: UpgradePlan) {
|
|
229
|
+
const complexityColor = {
|
|
230
|
+
low: pc.green,
|
|
231
|
+
medium: pc.yellow,
|
|
232
|
+
high: pc.red,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(` ${pc.bold(name)}`)
|
|
236
|
+
console.log(` ${pc.dim("Atual:")} ${plan.current} ${pc.dim("->")} ${pc.cyan(plan.target)}`)
|
|
237
|
+
console.log(` ${pc.dim("Complexidade:")} ${complexityColor[plan.complexity](plan.complexity)}`)
|
|
238
|
+
console.log()
|
|
239
|
+
|
|
240
|
+
console.log(` ${pc.dim("Breaking Changes:")}`)
|
|
241
|
+
plan.breakingChanges.forEach(bc => {
|
|
242
|
+
console.log(` ${pc.yellow("!")} ${bc}`)
|
|
243
|
+
})
|
|
244
|
+
console.log()
|
|
245
|
+
|
|
246
|
+
console.log(` ${pc.dim("Passos:")}`)
|
|
247
|
+
plan.steps.forEach((step, i) => {
|
|
248
|
+
console.log(` ${pc.dim(`${i + 1}.`)} ${step}`)
|
|
249
|
+
})
|
|
250
|
+
console.log()
|
|
251
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import * as p from "@clack/prompts"
|
|
4
4
|
import pc from "picocolors"
|
|
5
5
|
import { create } from "./commands/create"
|
|
6
|
+
import { analyze } from "./commands/analyze"
|
|
7
|
+
import { upgrade } from "./commands/upgrade"
|
|
6
8
|
|
|
7
9
|
const PACKAGE_NAME = "@nimbuslab/cli"
|
|
8
|
-
const CURRENT_VERSION = "0.
|
|
10
|
+
const CURRENT_VERSION = "0.9.0"
|
|
9
11
|
|
|
10
12
|
const LOGO = `
|
|
11
13
|
███╗ ██╗██╗███╗ ███╗██████╗ ██╗ ██╗███████╗
|
|
@@ -74,6 +76,10 @@ async function main() {
|
|
|
74
76
|
|
|
75
77
|
if (!command || command === "create") {
|
|
76
78
|
await create(args.slice(1))
|
|
79
|
+
} else if (command === "analyze") {
|
|
80
|
+
await analyze(args.slice(1))
|
|
81
|
+
} else if (command === "upgrade") {
|
|
82
|
+
await upgrade(args.slice(1))
|
|
77
83
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
78
84
|
showHelp()
|
|
79
85
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
@@ -91,14 +97,23 @@ ${pc.bold("Usage:")} nimbus [command] [options]
|
|
|
91
97
|
|
|
92
98
|
${pc.bold("Commands:")}
|
|
93
99
|
create [name] Create a new project
|
|
100
|
+
analyze [dir] Analyze project stack
|
|
101
|
+
upgrade [target] Upgrade dependencies
|
|
94
102
|
help Show this help
|
|
95
103
|
version Show version
|
|
96
104
|
|
|
97
105
|
${pc.bold("Templates:")}
|
|
98
106
|
--landing Landing page (Next.js 16 + Tailwind 4 + shadcn)
|
|
99
|
-
--app Web app (Landing + Better Auth +
|
|
107
|
+
--app Web app (Landing + Better Auth + Drizzle)
|
|
100
108
|
--turborepo Monorepo (Turborepo + apps/packages)
|
|
101
109
|
|
|
110
|
+
${pc.bold("Analyze & Upgrade:")}
|
|
111
|
+
analyze . Detect stack and show recommendations
|
|
112
|
+
analyze --json Output as JSON
|
|
113
|
+
upgrade --plan Show upgrade plan
|
|
114
|
+
upgrade next Upgrade Next.js
|
|
115
|
+
upgrade tailwind Upgrade Tailwind CSS
|
|
116
|
+
|
|
102
117
|
${pc.bold("Options:")}
|
|
103
118
|
-y, --yes Accept defaults
|
|
104
119
|
--no-git Don't initialize Git
|
|
@@ -108,7 +123,8 @@ ${pc.bold("Options:")}
|
|
|
108
123
|
${pc.bold("Examples:")}
|
|
109
124
|
${pc.dim("$")} nimbus create my-landing --landing
|
|
110
125
|
${pc.dim("$")} nimbus create my-app --app
|
|
111
|
-
${pc.dim("$")} nimbus
|
|
126
|
+
${pc.dim("$")} nimbus analyze ./my-project
|
|
127
|
+
${pc.dim("$")} nimbus upgrade --plan
|
|
112
128
|
`)
|
|
113
129
|
}
|
|
114
130
|
|