@carlos-iso/versioner 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 <copyright holders>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ # Versioner
2
+
3
+ CLI que substitui o fluxo manual de release por um único comando.
4
+
5
+ Antes:
6
+
7
+ ```bash
8
+ # edita package.json
9
+ # edita app.json
10
+ git add .
11
+ git commit -m "v1.4.273 - Corrige login"
12
+ git push
13
+ ```
14
+
15
+ Depois:
16
+
17
+ ```bash
18
+ versioner build "Corrige login"
19
+ ```
20
+
21
+ Sem dependências externas. Só Node.js e Git.
22
+
23
+ ---
24
+
25
+ ## Instalação
26
+
27
+ No projeto:
28
+
29
+ ```bash
30
+ npm install -D @carloscoding/versioner
31
+ ```
32
+
33
+ Sem instalar:
34
+
35
+ ```bash
36
+ npx @carloscoding/versioner init
37
+ ```
38
+
39
+ Global:
40
+
41
+ ```bash
42
+ npm install -g @carloscoding/versioner
43
+ ```
44
+
45
+ Requer Node.js 16 ou superior.
46
+
47
+ ---
48
+
49
+ ## Início rápido
50
+
51
+ ```bash
52
+ cd meu-projeto
53
+ npx @carloscoding/versioner init
54
+ npx versioner build "primeira release"
55
+ ```
56
+
57
+ O `init` cria dois arquivos, detecta o `package.json` e o `app.json`, e pergunta a versão inicial.
58
+
59
+ Se estiver usando como dependência do projeto, adicione atalhos no `package.json`:
60
+
61
+ ```json
62
+ {
63
+ "scripts": {
64
+ "build": "versioner build",
65
+ "minor": "versioner minor",
66
+ "major": "versioner major"
67
+ }
68
+ }
69
+ ```
70
+
71
+ E use:
72
+
73
+ ```bash
74
+ npm run build -- "Corrige login"
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Modelo de versionamento
80
+
81
+ Formato:
82
+
83
+ ```
84
+ major.minor.build
85
+ ```
86
+
87
+ Exemplo: `2.14.583`
88
+
89
+ ### Build
90
+
91
+ Incrementa em **toda** release e **nunca** volta para zero. Representa o total de releases já publicadas.
92
+
93
+ ```
94
+ 2.14.583 → 2.14.584 → 2.14.585
95
+ ```
96
+
97
+ ### Minor
98
+
99
+ Pequenas evoluções dentro de uma mesma versão principal. Zera quando ocorre um Major.
100
+
101
+ ```
102
+ 2.14.583 → 2.15.584
103
+ ```
104
+
105
+ ### Major
106
+
107
+ Grandes marcos: primeira versão pública, reescrita, nova arquitetura. Definido manualmente.
108
+
109
+ ```
110
+ 2.14.583 → 3.0.584
111
+ ```
112
+
113
+ O Build continua incrementando normalmente mesmo em um Major.
114
+
115
+ ### Por que não SemVer
116
+
117
+ O SemVer descreve compatibilidade de API. Este modelo descreve o **histórico do projeto**: quantas grandes versões existiram, quantas evoluções cada uma recebeu e quantas releases foram publicadas desde o início.
118
+
119
+ O formato continua compatível com `x.y.z`, então o `package.json` permanece válido.
120
+
121
+ ---
122
+
123
+ ## Comandos
124
+
125
+ | Comando | O que faz |
126
+ |---|---|
127
+ | `versioner init` | Inicializa o Versioner no projeto |
128
+ | `versioner build "msg"` | Incrementa a Build e publica a release |
129
+ | `versioner minor "msg"` | Incrementa Minor e Build |
130
+ | `versioner major "msg"` | Incrementa Major, zera Minor, incrementa Build |
131
+ | `versioner version` | Mostra a versão atual |
132
+ | `versioner status` | Mostra versão, arquivos monitorados e estado do Git |
133
+ | `versioner help [comando]` | Ajuda |
134
+
135
+ Aliases: `b`, `m`, `M`, `i`, `v`, `s`, `h`.
136
+
137
+ ### Flags de release
138
+
139
+ | Flag | Efeito |
140
+ |---|---|
141
+ | `--no-push` | Faz commit mas não envia para o remoto |
142
+ | `--no-git` | Só versiona os arquivos, não toca no Git |
143
+ | `--tag` | Cria uma tag Git para a release |
144
+ | `--no-tag` | Desativa a tag mesmo se ligada na config |
145
+ | `--dry-run` | Simula tudo sem gravar nada |
146
+
147
+ ### Flags do init
148
+
149
+ | Flag | Efeito |
150
+ |---|---|
151
+ | `--yes`, `-y` | Usa os padrões sem perguntar |
152
+ | `--force`, `-f` | Sobrescreve arquivos existentes |
153
+
154
+ ---
155
+
156
+ ## Arquivos gerados
157
+
158
+ ### `.versioner.json`
159
+
160
+ O contador de versão do projeto.
161
+
162
+ ```json
163
+ {
164
+ "major": 1,
165
+ "minor": 4,
166
+ "build": 273
167
+ }
168
+ ```
169
+
170
+ ### `versioner.config.json`
171
+
172
+ Define o comportamento e quais arquivos recebem a versão.
173
+
174
+ ```json
175
+ {
176
+ "versionFile": ".versioner.json",
177
+ "files": [
178
+ {
179
+ "path": "package.json",
180
+ "field": "version"
181
+ },
182
+ {
183
+ "path": "app.json",
184
+ "field": "expo.version"
185
+ }
186
+ ],
187
+ "commit": {
188
+ "template": "v{version} - {message}",
189
+ "minLength": 3,
190
+ "maxLength": 100
191
+ },
192
+ "git": {
193
+ "enabled": true,
194
+ "add": true,
195
+ "commit": true,
196
+ "push": true,
197
+ "tag": false,
198
+ "tagPrefix": "v",
199
+ "tagMessage": "Release {version}"
200
+ }
201
+ }
202
+ ```
203
+
204
+ **`files`** — qualquer arquivo `.json`. O campo `field` aceita caminhos aninhados:
205
+
206
+ ```
207
+ version
208
+ expo.version
209
+ project.meta.version
210
+ ```
211
+
212
+ **`commit.template`** — aceita `{version}`, `{message}` e `{type}`.
213
+
214
+ Arquivos de código (`app.config.js`, `app.config.ts`) são detectados pelo `init` mas **não** são alterados automaticamente. Nesses casos, leia a versão do JSON:
215
+
216
+ ```js
217
+ const { version } = require("./package.json");
218
+
219
+ export default {
220
+ expo: { version },
221
+ };
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Segurança da release
227
+
228
+ Se qualquer etapa falhar no meio do caminho, o Versioner reverte o arquivo de versão e todos os arquivos alterados. Nada fica commitado pela metade.
229
+
230
+ O `push` é ignorado com um aviso quando não existe remoto configurado, em vez de derrubar a release. Se a branch ainda não tiver upstream, o Versioner usa `--set-upstream origin <branch>` automaticamente.
231
+
232
+ Mensagens de commit são passadas por `execFile` com array de argumentos, então aspas, `$` e crases não quebram nem executam nada.
233
+
234
+ ---
235
+
236
+ ## Uso programático
237
+
238
+ ```js
239
+ const { ReleaseManager, createContext } = require("@carloscoding/versioner");
240
+
241
+ const context = createContext({ type: "build", message: "Release automática" });
242
+
243
+ new ReleaseManager().run(context);
244
+ ```
245
+
246
+ Também são exportados `VersionManager`, `ConfigManager`, `FileManager`, `GitManager`, `CommandRouter`, `logger` e `constants`.
247
+
248
+ ---
249
+
250
+ ## Arquitetura
251
+
252
+ ```
253
+ bin/versioner.js Binário da CLI (shebang)
254
+ src/cli.js Bootstrap da CLI e tratamento de erro
255
+ src/index.js Entrada programática (require do pacote)
256
+ src/services/ CommandRouter (mapa nome → classe)
257
+ src/commands/ Um arquivo por comando
258
+ src/managers/ Release, Version, Config, File, Git
259
+ src/core/Context.js Objeto compartilhado da execução
260
+ src/utils/ file, object, time, args, logger
261
+ src/constants/ Valores fixos e metadados de ajuda
262
+ ```
263
+
264
+ Regra central: nenhum Manager conhece outro Manager. Todos leem e escrevem apenas no `Context`. O `ReleaseManager` só orquestra a sequência:
265
+
266
+ ```
267
+ validate → loadConfig → version → updateFiles → git → finish
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Desenvolvimento
273
+
274
+ ```bash
275
+ npm test # teste de fumaça em um repositório Git temporário
276
+ ```
277
+
278
+ Para logs completos de erro:
279
+
280
+ ```bash
281
+ VERSIONER_DEBUG=1 versioner build "teste"
282
+ ```
283
+
284
+ Cores são desativadas automaticamente com `NO_COLOR=1` ou fora de um TTY.
285
+
286
+ ---
287
+
288
+ ## Licença
289
+
290
+ MIT.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("../src/cli");
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@carlos-iso/versioner",
3
+ "version": "0.0.2",
4
+ "description": "CLI para automatizar versionamento e fluxo de Git usando um modelo de versionamento baseado em marcos do projeto (major.minor.build).",
5
+ "license": "MIT",
6
+ "author": "Carlos-iso",
7
+ "type": "commonjs",
8
+ "main": "src/index.js",
9
+ "bin": {
10
+ "versioner": "bin/versioner.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "start": "node ./bin/versioner.js",
23
+ "dev": "node ./bin/versioner.js",
24
+ "help": "node ./bin/versioner.js help",
25
+ "test": "node ./test/smoke.js",
26
+ "prepublishOnly": "node ./src/test/smoke.js"
27
+ },
28
+ "keywords": [
29
+ "version",
30
+ "versioning",
31
+ "release",
32
+ "git",
33
+ "automation",
34
+ "cli",
35
+ "build",
36
+ "semver",
37
+ "expo"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Carlos-iso/versioner.git"
45
+ },
46
+ "homepage": "https://github.com/Carlos-iso/versioner",
47
+ "bugs": {
48
+ "url": "https://github.com/Carlos-iso/versioner/issues"
49
+ }
50
+ }
package/src/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ const CommandRouter = require("./services/CommandRouter");
2
+ const logger = require("./utils/logger");
3
+
4
+ async function main() {
5
+ const router = new CommandRouter();
6
+
7
+ const code = await router.run(process.argv.slice(2));
8
+
9
+ process.exit(code || 0);
10
+ }
11
+
12
+ main().catch((error) => {
13
+ logger.break();
14
+ logger.error(error.message);
15
+
16
+ if (process.env.VERSIONER_DEBUG) {
17
+ console.error(error);
18
+ }
19
+
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,33 @@
1
+ const ReleaseManager = require("../managers/ReleaseManager");
2
+ const { createContext } = require("../core/Context");
3
+ const { parse, toMessage } = require("../utils/args");
4
+
5
+ /**
6
+ * Base dos comandos de release (build, minor, major).
7
+ * Monta o Context e delega tudo ao ReleaseManager.
8
+ */
9
+ class BaseCommand {
10
+ /**
11
+ * Sobrescrito pelas subclasses: "build" | "minor" | "major".
12
+ */
13
+ get type() {
14
+ throw new Error("Comando sem tipo definido.");
15
+ }
16
+
17
+ async run(args = []) {
18
+ const { values, flags } = parse(args);
19
+
20
+ const context = createContext({
21
+ type: this.type,
22
+ message: toMessage(values),
23
+ cwd: process.cwd(),
24
+ flags,
25
+ });
26
+
27
+ const release = new ReleaseManager(context.cwd);
28
+
29
+ return release.run(context);
30
+ }
31
+ }
32
+
33
+ module.exports = BaseCommand;
@@ -0,0 +1,9 @@
1
+ const BaseCommand = require("./BaseCommand");
2
+
3
+ class BuildCommand extends BaseCommand {
4
+ get type() {
5
+ return "build";
6
+ }
7
+ }
8
+
9
+ module.exports = BuildCommand;
@@ -0,0 +1,77 @@
1
+ const logger = require("../utils/logger");
2
+ const { parse } = require("../utils/args");
3
+ const { COMMANDS, ALIASES } = require("../constants");
4
+
5
+ class HelpCommand {
6
+ async run(args = []) {
7
+ const { values } = parse(args);
8
+
9
+ const target = values[0];
10
+
11
+ if (target) {
12
+ return this.detail(ALIASES[target] || target);
13
+ }
14
+
15
+ return this.overview();
16
+ }
17
+
18
+ overview() {
19
+ logger.title("Versioner");
20
+
21
+ logger.muted(" Automatiza versionamento e fluxo de Git em um único comando.");
22
+
23
+ logger.section("Uso");
24
+ logger.muted(" versioner <comando> [argumentos] [flags]");
25
+
26
+ logger.section("Comandos");
27
+
28
+ const width = Math.max(...COMMANDS.map((command) => command.name.length)) + 4;
29
+
30
+ for (const command of COMMANDS) {
31
+ logger.write(` ${logger.bold(command.name.padEnd(width))}${command.summary}`);
32
+ }
33
+
34
+ logger.section("Exemplos");
35
+ logger.muted(" versioner init");
36
+ logger.muted(' versioner build "Corrige o login"');
37
+ logger.muted(' versioner minor "Adiciona filtros na busca"');
38
+ logger.muted(' versioner major "Nova arquitetura" --tag');
39
+ logger.muted(" versioner status");
40
+
41
+ logger.section("Ajuda de um comando");
42
+ logger.muted(" versioner help build");
43
+ logger.break();
44
+
45
+ return 0;
46
+ }
47
+
48
+ detail(name) {
49
+ const command = COMMANDS.find((item) => item.name === name);
50
+
51
+ if (!command) {
52
+ logger.error(`Comando desconhecido: ${name}`);
53
+ logger.muted(' Use "versioner help" para ver a lista de comandos.');
54
+ logger.break();
55
+
56
+ return 1;
57
+ }
58
+
59
+ logger.title(`Versioner · ${command.name}`);
60
+
61
+ logger.muted(` ${command.summary}`);
62
+
63
+ logger.section("Uso");
64
+ logger.muted(` ${command.usage}`);
65
+
66
+ if (command.details.length) {
67
+ logger.section("Detalhes");
68
+ command.details.forEach((line) => logger.muted(` ${line}`));
69
+ }
70
+
71
+ logger.break();
72
+
73
+ return 0;
74
+ }
75
+ }
76
+
77
+ module.exports = HelpCommand;
@@ -0,0 +1,197 @@
1
+ const readline = require("readline");
2
+
3
+ const ConfigManager = require("../managers/ConfigManager");
4
+ const VersionManager = require("../managers/VersionManager");
5
+ const GitManager = require("../managers/GitManager");
6
+
7
+ const logger = require("../utils/logger");
8
+ const { resolve, exists, readJSON } = require("../utils/file");
9
+ const { parse, hasFlag } = require("../utils/args");
10
+ const {
11
+ CONFIG_FILE,
12
+ VERSION_FILE,
13
+ DEFAULT_CONFIG,
14
+ DEFAULT_VERSION,
15
+ DETECTABLE_FILES,
16
+ } = require("../constants");
17
+
18
+ class InitCommand {
19
+ async run(args = []) {
20
+ const { flags } = parse(args);
21
+
22
+ const cwd = process.cwd();
23
+
24
+ const auto = hasFlag(flags, "yes");
25
+ const force = hasFlag(flags, "force");
26
+
27
+ const configManager = new ConfigManager(cwd);
28
+ const versionManager = new VersionManager(cwd, VERSION_FILE);
29
+ const gitManager = new GitManager(cwd);
30
+
31
+ logger.title("Versioner · init");
32
+
33
+ // 1. Repositório Git
34
+ if (!gitManager.isInstalled()) {
35
+ logger.error("Git não encontrado no sistema.");
36
+ return 1;
37
+ }
38
+
39
+ if (!gitManager.isRepository()) {
40
+ logger.warn('Nenhum repositório Git encontrado. Rode "git init" antes de publicar.');
41
+ } else {
42
+ logger.success(`Repositório Git detectado (branch: ${gitManager.branch()}).`);
43
+ }
44
+
45
+ // 2. Arquivos já existentes
46
+ if ((configManager.exists() || versionManager.exists()) && !force) {
47
+ logger.error(
48
+ `O Versioner já está inicializado neste projeto. Use --force para sobrescrever.`,
49
+ );
50
+ return 1;
51
+ }
52
+
53
+ // 3. Detecção de arquivos
54
+ const detected = this.detect(cwd);
55
+
56
+ if (detected.supported.length) {
57
+ logger.success("Arquivos detectados:");
58
+ detected.supported.forEach((file) => logger.item(`${file.path} (${file.field})`));
59
+ } else {
60
+ logger.warn("Nenhum arquivo JSON de versão foi detectado.");
61
+ }
62
+
63
+ detected.unsupported.forEach((file) => {
64
+ logger.warn(
65
+ `${file.path} detectado, mas arquivos de código não são versionados automaticamente.`,
66
+ );
67
+ });
68
+
69
+ // 4. Versão inicial
70
+ const initial = auto
71
+ ? this.fromPackage(cwd) || { ...DEFAULT_VERSION }
72
+ : await this.ask(cwd);
73
+
74
+ // 5. Gravação
75
+ const config = {
76
+ ...DEFAULT_CONFIG,
77
+ versionFile: VERSION_FILE,
78
+ files: detected.supported.map((file) => ({
79
+ path: file.path,
80
+ field: file.field,
81
+ })),
82
+ };
83
+
84
+ configManager.save(config);
85
+ versionManager.save(initial);
86
+
87
+ logger.break();
88
+ logger.success(`${CONFIG_FILE} criado.`);
89
+ logger.success(`${VERSION_FILE} criado.`);
90
+
91
+ logger.section("Versão inicial");
92
+ logger.field("Versão", versionManager.toString(initial));
93
+
94
+ logger.section("Próximo passo");
95
+ logger.muted(' versioner build "primeira release"');
96
+ logger.break();
97
+
98
+ return 0;
99
+ }
100
+
101
+ /**
102
+ * Procura arquivos conhecidos no diretório atual.
103
+ */
104
+ detect(cwd) {
105
+ const supported = [];
106
+ const unsupported = [];
107
+
108
+ for (const file of DETECTABLE_FILES) {
109
+ if (!exists(resolve(cwd, file.path))) {
110
+ continue;
111
+ }
112
+
113
+ if (file.json) {
114
+ supported.push(file);
115
+ } else {
116
+ unsupported.push(file);
117
+ }
118
+ }
119
+
120
+ return { supported, unsupported };
121
+ }
122
+
123
+ /**
124
+ * Tenta reaproveitar a versão já presente no package.json.
125
+ */
126
+ fromPackage(cwd) {
127
+ const filepath = resolve(cwd, "package.json");
128
+
129
+ if (!exists(filepath)) {
130
+ return null;
131
+ }
132
+
133
+ try {
134
+ const version = readJSON(filepath).version;
135
+
136
+ return this.parseVersion(version);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ parseVersion(value) {
143
+ const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/);
144
+
145
+ if (!match) {
146
+ return null;
147
+ }
148
+
149
+ return {
150
+ major: Number(match[1]),
151
+ minor: Number(match[2]),
152
+ build: Number(match[3]),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Pergunta interativa da versão inicial.
158
+ */
159
+ async ask(cwd) {
160
+ const suggestion = this.fromPackage(cwd) || { ...DEFAULT_VERSION };
161
+
162
+ const suggested = `${suggestion.major}.${suggestion.minor}.${suggestion.build}`;
163
+
164
+ logger.break();
165
+
166
+ const answer = await this.prompt(`Versão inicial [${suggested}]: `);
167
+
168
+ if (!answer.trim()) {
169
+ return suggestion;
170
+ }
171
+
172
+ const parsed = this.parseVersion(answer.trim().replace(/^v/i, ""));
173
+
174
+ if (!parsed) {
175
+ logger.warn(`Formato inválido. Usando ${suggested}.`);
176
+ return suggestion;
177
+ }
178
+
179
+ return parsed;
180
+ }
181
+
182
+ prompt(question) {
183
+ const rl = readline.createInterface({
184
+ input: process.stdin,
185
+ output: process.stdout,
186
+ });
187
+
188
+ return new Promise((resolveAnswer) => {
189
+ rl.question(question, (answer) => {
190
+ rl.close();
191
+ resolveAnswer(answer);
192
+ });
193
+ });
194
+ }
195
+ }
196
+
197
+ module.exports = InitCommand;
@@ -0,0 +1,9 @@
1
+ const BaseCommand = require("./BaseCommand");
2
+
3
+ class MajorCommand extends BaseCommand {
4
+ get type() {
5
+ return "major";
6
+ }
7
+ }
8
+
9
+ module.exports = MajorCommand;