@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.
@@ -0,0 +1,9 @@
1
+ const BaseCommand = require("./BaseCommand");
2
+
3
+ class MinorCommand extends BaseCommand {
4
+ get type() {
5
+ return "minor";
6
+ }
7
+ }
8
+
9
+ module.exports = MinorCommand;
@@ -0,0 +1,118 @@
1
+ const VersionManager = require("../managers/VersionManager");
2
+ const ConfigManager = require("../managers/ConfigManager");
3
+ const GitManager = require("../managers/GitManager");
4
+
5
+ const logger = require("../utils/logger");
6
+ const { resolve, exists, readJSON } = require("../utils/file");
7
+ const { getValue } = require("../utils/object");
8
+ const { CONFIG_FILE } = require("../constants");
9
+
10
+ class StatusCommand {
11
+ async run() {
12
+ const cwd = process.cwd();
13
+
14
+ const configManager = new ConfigManager(cwd);
15
+ const gitManager = new GitManager(cwd);
16
+
17
+ logger.title("Versioner · status");
18
+
19
+ if (!configManager.exists()) {
20
+ logger.error(`${CONFIG_FILE} não encontrado. Execute "versioner init".`);
21
+ return 1;
22
+ }
23
+
24
+ const config = configManager.load();
25
+
26
+ const versionManager = new VersionManager(cwd, config.versionFile);
27
+
28
+ // ── Versão ────────────────────────────────────────────
29
+ logger.section("Versão");
30
+
31
+ if (versionManager.exists()) {
32
+ const version = versionManager.load();
33
+
34
+ logger.field("Atual", logger.bold(versionManager.toString(version)));
35
+ logger.field("Arquivo", config.versionFile);
36
+ } else {
37
+ logger.warn("Arquivo de versão ainda não criado.");
38
+ }
39
+
40
+ // ── Arquivos monitorados ──────────────────────────────
41
+ logger.section("Arquivos monitorados");
42
+
43
+ if (!config.files.length) {
44
+ logger.muted(" Nenhum arquivo configurado.");
45
+ }
46
+
47
+ for (const file of config.files) {
48
+ const filepath = resolve(cwd, file.path);
49
+
50
+ if (!exists(filepath)) {
51
+ logger.item(`${file.path} ${logger.dim("(não encontrado)")}`);
52
+ continue;
53
+ }
54
+
55
+ let current = "?";
56
+
57
+ try {
58
+ current = String(getValue(readJSON(filepath), file.field) ?? "não definido");
59
+ } catch {
60
+ current = "erro ao ler";
61
+ }
62
+
63
+ logger.item(`${file.path} ${logger.dim(`(${file.field} = ${current})`)}`);
64
+ }
65
+
66
+ // ── Configuração ──────────────────────────────────────
67
+ logger.section("Configuração");
68
+
69
+ logger.field("Commit", config.commit.template);
70
+ logger.field("Mensagem", `${config.commit.minLength}–${config.commit.maxLength} caracteres`);
71
+ logger.field("Git", config.git.enabled === false ? "desativado" : "ativado");
72
+ logger.field("Push automático", config.git.push ? "sim" : "não");
73
+ logger.field("Tag automática", config.git.tag ? `sim (${config.git.tagPrefix})` : "não");
74
+
75
+ // ── Git ───────────────────────────────────────────────
76
+ logger.section("Git");
77
+
78
+ if (!gitManager.isInstalled()) {
79
+ logger.warn("Git não encontrado no sistema.");
80
+ logger.break();
81
+ return 0;
82
+ }
83
+
84
+ if (!gitManager.isRepository()) {
85
+ logger.warn("Este diretório não é um repositório Git.");
86
+ logger.break();
87
+ return 0;
88
+ }
89
+
90
+ const changes = gitManager.changes();
91
+
92
+ logger.field("Branch", gitManager.branch() || "desconhecida");
93
+ logger.field("Remoto", gitManager.hasRemote() ? "configurado" : "nenhum");
94
+ logger.field("Upstream", gitManager.hasUpstream() ? "sim" : "não");
95
+ logger.field("Alterações", changes.length ? `${changes.length} arquivo(s)` : "nenhuma");
96
+
97
+ const last = gitManager.lastCommit();
98
+
99
+ if (last) {
100
+ logger.field("Último commit", last);
101
+ }
102
+
103
+ if (changes.length) {
104
+ logger.break();
105
+ changes.slice(0, 10).forEach((line) => logger.muted(` ${line}`));
106
+
107
+ if (changes.length > 10) {
108
+ logger.muted(` ... e mais ${changes.length - 10}`);
109
+ }
110
+ }
111
+
112
+ logger.break();
113
+
114
+ return 0;
115
+ }
116
+ }
117
+
118
+ module.exports = StatusCommand;
@@ -0,0 +1,63 @@
1
+ const path = require("path");
2
+
3
+ const VersionManager = require("../managers/VersionManager");
4
+ const ConfigManager = require("../managers/ConfigManager");
5
+
6
+ const logger = require("../utils/logger");
7
+ const { readJSON } = require("../utils/file");
8
+ const { parse, hasFlag } = require("../utils/args");
9
+ const { VERSION_FILE } = require("../constants");
10
+
11
+ class VersionCommand {
12
+ async run(args = []) {
13
+ const { flags } = parse(args);
14
+
15
+ // Versão do próprio Versioner
16
+ if (hasFlag(flags, "self")) {
17
+ const pkg = readJSON(path.join(__dirname, "..", "..", "package.json"));
18
+
19
+ console.log(pkg.version);
20
+
21
+ return 0;
22
+ }
23
+
24
+ const cwd = process.cwd();
25
+
26
+ const configManager = new ConfigManager(cwd);
27
+
28
+ const versionFile = configManager.exists()
29
+ ? configManager.load().versionFile
30
+ : VERSION_FILE;
31
+
32
+ const versionManager = new VersionManager(cwd, versionFile);
33
+
34
+ if (!versionManager.exists()) {
35
+ logger.error('Projeto não inicializado. Execute "versioner init".');
36
+ return 1;
37
+ }
38
+
39
+ const version = versionManager.load();
40
+ const text = versionManager.toString(version);
41
+
42
+ if (hasFlag(flags, "json")) {
43
+ console.log(JSON.stringify({ ...version, version: text }, null, 2));
44
+ return 0;
45
+ }
46
+
47
+ if (hasFlag(flags, "raw")) {
48
+ console.log(text);
49
+ return 0;
50
+ }
51
+
52
+ logger.title("Versioner · version");
53
+ logger.field("Versão", logger.bold(text));
54
+ logger.field("Major", String(version.major));
55
+ logger.field("Minor", String(version.minor));
56
+ logger.field("Build", String(version.build));
57
+ logger.break();
58
+
59
+ return 0;
60
+ }
61
+ }
62
+
63
+ module.exports = VersionCommand;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Constantes globais do Versioner.
3
+ * Nenhuma regra de negócio aqui, apenas valores fixos.
4
+ */
5
+
6
+ const VERSION_FILE = ".versioner.json";
7
+
8
+ const CONFIG_FILE = "versioner.config.json";
9
+
10
+ const VERSION_TYPES = ["build", "minor", "major"];
11
+
12
+ const DEFAULT_VERSION = {
13
+ major: 0,
14
+ minor: 0,
15
+ build: 0,
16
+ };
17
+
18
+ const DEFAULT_COMMIT = {
19
+ template: "v{version} - {message}",
20
+ minLength: 3,
21
+ maxLength: 100,
22
+ };
23
+
24
+ const DEFAULT_GIT = {
25
+ enabled: true,
26
+ add: true,
27
+ commit: true,
28
+ push: true,
29
+ tag: false,
30
+ tagPrefix: "v",
31
+ tagMessage: "Release {version}",
32
+ };
33
+
34
+ const DEFAULT_CONFIG = {
35
+ versionFile: VERSION_FILE,
36
+ files: [],
37
+ commit: DEFAULT_COMMIT,
38
+ git: DEFAULT_GIT,
39
+ };
40
+
41
+ /**
42
+ * Arquivos que o `init` tenta detectar automaticamente.
43
+ * `json: false` significa que o arquivo é código e não pode ser
44
+ * versionado automaticamente (apenas avisamos o usuário).
45
+ */
46
+ const DETECTABLE_FILES = [
47
+ { path: "package.json", field: "version", json: true },
48
+ { path: "app.json", field: "expo.version", json: true },
49
+ { path: "manifest.json", field: "version", json: true },
50
+ { path: "app.config.js", json: false },
51
+ { path: "app.config.ts", json: false },
52
+ ];
53
+
54
+ /**
55
+ * Metadados usados pelo HelpCommand e pelo CommandRouter.
56
+ */
57
+ const COMMANDS = [
58
+ {
59
+ name: "init",
60
+ usage: "versioner init [--yes] [--force]",
61
+ summary: "Inicializa o Versioner no projeto atual.",
62
+ details: [
63
+ "Verifica se existe um repositório Git.",
64
+ `Cria o ${VERSION_FILE} (contador de versão).`,
65
+ `Cria o ${CONFIG_FILE} (arquivos monitorados).`,
66
+ "Detecta package.json, app.json, app.config.js e app.config.ts.",
67
+ "Pergunta a versão inicial do projeto.",
68
+ "",
69
+ "Flags:",
70
+ " --yes, -y Usa os valores padrão sem perguntar nada.",
71
+ " --force, -f Sobrescreve arquivos já existentes.",
72
+ ],
73
+ },
74
+ {
75
+ name: "build",
76
+ usage: 'versioner build "mensagem do commit"',
77
+ summary: "Incrementa a Build e publica a release.",
78
+ details: [
79
+ "Exemplo: 1.4.272 → 1.4.273",
80
+ "",
81
+ "Flags:",
82
+ " --no-push Faz commit mas não envia para o remoto.",
83
+ " --no-git Apenas versiona os arquivos, sem tocar no Git.",
84
+ " --tag Cria uma tag Git para essa release.",
85
+ " --dry-run Simula tudo sem gravar nada.",
86
+ ],
87
+ },
88
+ {
89
+ name: "minor",
90
+ usage: 'versioner minor "mensagem do commit"',
91
+ summary: "Incrementa Minor e Build e publica a release.",
92
+ details: ["Exemplo: 1.4.272 → 1.5.273", "", "Aceita as mesmas flags do build."],
93
+ },
94
+ {
95
+ name: "major",
96
+ usage: 'versioner major "mensagem do commit"',
97
+ summary: "Incrementa Major, zera Minor, incrementa Build.",
98
+ details: ["Exemplo: 1.4.272 → 2.0.273", "", "Aceita as mesmas flags do build."],
99
+ },
100
+ {
101
+ name: "version",
102
+ usage: "versioner version [--json]",
103
+ summary: "Mostra a versão atual do projeto.",
104
+ details: [
105
+ "Flags:",
106
+ " --json Retorna a versão em JSON.",
107
+ " --raw Retorna apenas a string da versão.",
108
+ " --self Mostra a versão do próprio Versioner.",
109
+ ],
110
+ },
111
+ {
112
+ name: "status",
113
+ usage: "versioner status",
114
+ summary: "Mostra versão, arquivos monitorados e estado do Git.",
115
+ details: [],
116
+ },
117
+ {
118
+ name: "help",
119
+ usage: "versioner help [comando]",
120
+ summary: "Lista os comandos disponíveis.",
121
+ details: [],
122
+ },
123
+ ];
124
+
125
+ const ALIASES = {
126
+ b: "build",
127
+ m: "minor",
128
+ M: "major",
129
+ i: "init",
130
+ h: "help",
131
+ v: "version",
132
+ s: "status",
133
+ "--help": "help",
134
+ "-h": "help",
135
+ "--version": "version",
136
+ "-v": "version",
137
+ };
138
+
139
+ module.exports = {
140
+ VERSION_FILE,
141
+ CONFIG_FILE,
142
+ VERSION_TYPES,
143
+ DEFAULT_VERSION,
144
+ DEFAULT_COMMIT,
145
+ DEFAULT_GIT,
146
+ DEFAULT_CONFIG,
147
+ DETECTABLE_FILES,
148
+ COMMANDS,
149
+ ALIASES,
150
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Objeto compartilhado por toda a execução.
3
+ *
4
+ * Regra da arquitetura: nenhum Manager conhece outro Manager.
5
+ * Todos leem e escrevem apenas no Context.
6
+ */
7
+ function createContext({ type, message = "", cwd = process.cwd(), flags = {} } = {}) {
8
+ return {
9
+ // Argumentos da CLI
10
+ type,
11
+ message,
12
+ flags,
13
+
14
+ // Diretório onde a ferramenta foi executada
15
+ cwd,
16
+
17
+ // Configuração do projeto (versioner.config.json)
18
+ config: null,
19
+
20
+ // Versão anterior (string)
21
+ previousVersion: null,
22
+
23
+ // Versão atual (string)
24
+ version: null,
25
+
26
+ // Objeto da versão anterior
27
+ previous: {
28
+ major: null,
29
+ minor: null,
30
+ build: null,
31
+ },
32
+
33
+ // Objeto da versão atual
34
+ current: {
35
+ major: null,
36
+ minor: null,
37
+ build: null,
38
+ },
39
+
40
+ // Arquivos atualizados durante a release
41
+ files: [],
42
+
43
+ // Arquivos configurados que não foram encontrados
44
+ skipped: [],
45
+
46
+ // Informações do Git
47
+ git: {
48
+ add: false,
49
+ commit: false,
50
+ push: false,
51
+ tag: false,
52
+ tagName: null,
53
+ branch: null,
54
+ },
55
+
56
+ // Modo simulação
57
+ dryRun: false,
58
+
59
+ // Controle de execução
60
+ startedAt: new Date(),
61
+ finishedAt: null,
62
+ };
63
+ }
64
+
65
+ module.exports = { createContext };
package/src/index.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Entrada programática do Versioner.
3
+ *
4
+ * const { ReleaseManager, createContext } = require("@carloscoding/versioner");
5
+ */
6
+
7
+ const ReleaseManager = require("./managers/ReleaseManager");
8
+ const VersionManager = require("./managers/VersionManager");
9
+ const ConfigManager = require("./managers/ConfigManager");
10
+ const FileManager = require("./managers/FileManager");
11
+ const GitManager = require("./managers/GitManager");
12
+ const CommandRouter = require("./services/CommandRouter");
13
+
14
+ const { createContext } = require("./core/Context");
15
+ const logger = require("./utils/logger");
16
+ const constants = require("./constants");
17
+
18
+ module.exports = {
19
+ ReleaseManager,
20
+ VersionManager,
21
+ ConfigManager,
22
+ FileManager,
23
+ GitManager,
24
+ CommandRouter,
25
+ createContext,
26
+ logger,
27
+ constants,
28
+ };
@@ -0,0 +1,70 @@
1
+ const { resolve, exists, readJSON, writeJSON } = require("../utils/file");
2
+ const { merge } = require("../utils/object");
3
+ const { CONFIG_FILE, DEFAULT_CONFIG } = require("../constants");
4
+
5
+ class ConfigManager {
6
+ constructor(cwd = process.cwd()) {
7
+ this.cwd = cwd;
8
+ this.file = resolve(cwd, CONFIG_FILE);
9
+ }
10
+
11
+ exists() {
12
+ return exists(this.file);
13
+ }
14
+
15
+ /**
16
+ * Carrega a configuração, aplica os defaults e valida o formato.
17
+ */
18
+ load() {
19
+ if (!this.exists()) {
20
+ throw new Error(
21
+ `Arquivo ${CONFIG_FILE} não encontrado. Execute "versioner init" neste projeto.`,
22
+ );
23
+ }
24
+
25
+ const raw = readJSON(this.file);
26
+
27
+ const config = merge(DEFAULT_CONFIG, raw);
28
+
29
+ this.validate(config);
30
+
31
+ return config;
32
+ }
33
+
34
+ validate(config) {
35
+ if (!Array.isArray(config.files)) {
36
+ throw new Error(`"files" deve ser um array em ${CONFIG_FILE}.`);
37
+ }
38
+
39
+ config.files.forEach((file, index) => {
40
+ if (!file || typeof file !== "object") {
41
+ throw new Error(`files[${index}] deve ser um objeto em ${CONFIG_FILE}.`);
42
+ }
43
+
44
+ if (!file.path) {
45
+ throw new Error(`files[${index}] está sem a propriedade "path".`);
46
+ }
47
+
48
+ if (!file.field) {
49
+ throw new Error(`files[${index}] (${file.path}) está sem a propriedade "field".`);
50
+ }
51
+ });
52
+
53
+ if (!config.versionFile) {
54
+ throw new Error(`"versionFile" não pode ser vazio em ${CONFIG_FILE}.`);
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Cria/sobrescreve o arquivo de configuração.
62
+ */
63
+ save(config) {
64
+ writeJSON(this.file, config);
65
+
66
+ return this.file;
67
+ }
68
+ }
69
+
70
+ module.exports = ConfigManager;
@@ -0,0 +1,75 @@
1
+ const { resolve, exists, readJSON, writeJSON, readText, writeText } = require("../utils/file");
2
+ const { setValue, getValue } = require("../utils/object");
3
+
4
+ class FileManager {
5
+ constructor(cwd = process.cwd()) {
6
+ this.cwd = cwd;
7
+
8
+ // Guarda o conteúdo original para permitir rollback.
9
+ this.backups = [];
10
+ }
11
+
12
+ /**
13
+ * Atualiza a versão dentro de um arquivo JSON.
14
+ *
15
+ * @returns {{ updated: boolean, reason?: string, from?: string }}
16
+ */
17
+ update(file, version, { dryRun = false } = {}) {
18
+ const filepath = resolve(this.cwd, file.path);
19
+
20
+ if (!exists(filepath)) {
21
+ return { updated: false, reason: "arquivo não encontrado" };
22
+ }
23
+
24
+ if (!filepath.endsWith(".json")) {
25
+ return { updated: false, reason: "apenas arquivos .json são suportados" };
26
+ }
27
+
28
+ const json = readJSON(filepath);
29
+
30
+ const from = getValue(json, file.field);
31
+
32
+ if (from === version) {
33
+ return { updated: false, reason: "já está na versão atual", from };
34
+ }
35
+
36
+ if (dryRun) {
37
+ return { updated: true, from, dryRun: true };
38
+ }
39
+
40
+ this.backups.push({ filepath, content: readText(filepath) });
41
+
42
+ setValue(json, file.field, version);
43
+
44
+ writeJSON(filepath, json);
45
+
46
+ return { updated: true, from };
47
+ }
48
+
49
+ /**
50
+ * Restaura todos os arquivos alterados nesta execução.
51
+ */
52
+ rollback() {
53
+ let restored = 0;
54
+
55
+ while (this.backups.length) {
56
+ const backup = this.backups.pop();
57
+
58
+ try {
59
+ writeText(backup.filepath, backup.content);
60
+ restored += 1;
61
+ } catch {
62
+ // Um arquivo que não pode ser restaurado não deve
63
+ // impedir a restauração dos demais.
64
+ }
65
+ }
66
+
67
+ return restored;
68
+ }
69
+
70
+ clearBackups() {
71
+ this.backups = [];
72
+ }
73
+ }
74
+
75
+ module.exports = FileManager;