@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 +19 -0
- package/README.md +290 -0
- package/bin/versioner.js +3 -0
- package/package.json +50 -0
- package/src/cli.js +21 -0
- package/src/commands/BaseCommand.js +33 -0
- package/src/commands/BuildCommand.js +9 -0
- package/src/commands/HelpCommand.js +77 -0
- package/src/commands/InitCommand.js +197 -0
- package/src/commands/MajorCommand.js +9 -0
- package/src/commands/MinorCommand.js +9 -0
- package/src/commands/StatusCommand.js +118 -0
- package/src/commands/VersionCommand.js +63 -0
- package/src/constants/index.js +150 -0
- package/src/core/Context.js +65 -0
- package/src/index.js +28 -0
- package/src/managers/ConfigManager.js +70 -0
- package/src/managers/FileManager.js +75 -0
- package/src/managers/GitManager.js +135 -0
- package/src/managers/ReleaseManager.js +301 -0
- package/src/managers/VersionManager.js +126 -0
- package/src/services/CommandRouter.js +107 -0
- package/src/test/smoke.js +156 -0
- package/src/utils/args.js +60 -0
- package/src/utils/file.js +80 -0
- package/src/utils/logger.js +103 -0
- package/src/utils/object.js +79 -0
- package/src/utils/time.js +36 -0
- package/src/utils/utils.js +20 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { execFileSync } = require("child_process");
|
|
2
|
+
|
|
3
|
+
class GitManager {
|
|
4
|
+
constructor(cwd = process.cwd()) {
|
|
5
|
+
this.cwd = cwd;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Executa um comando git.
|
|
10
|
+
* Usa execFileSync com array de argumentos: a mensagem de commit
|
|
11
|
+
* nunca passa pelo shell, então aspas e $ não quebram nada.
|
|
12
|
+
*/
|
|
13
|
+
run(args, { silent = false } = {}) {
|
|
14
|
+
return execFileSync("git", args, {
|
|
15
|
+
cwd: this.cwd,
|
|
16
|
+
stdio: silent ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Executa e devolve a saída, sem lançar erro.
|
|
23
|
+
*/
|
|
24
|
+
query(args) {
|
|
25
|
+
try {
|
|
26
|
+
return String(this.run(args, { silent: true }) || "").trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isInstalled() {
|
|
33
|
+
return this.query(["--version"]) !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isRepository() {
|
|
37
|
+
return this.query(["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
branch() {
|
|
41
|
+
return this.query(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hasRemote() {
|
|
45
|
+
const remotes = this.query(["remote"]);
|
|
46
|
+
|
|
47
|
+
return Boolean(remotes && remotes.length);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
hasUpstream() {
|
|
51
|
+
return this.query(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) !== null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lista de arquivos modificados (porcelain).
|
|
56
|
+
*/
|
|
57
|
+
changes() {
|
|
58
|
+
const output = this.query(["status", "--porcelain"]);
|
|
59
|
+
|
|
60
|
+
if (!output) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return output
|
|
65
|
+
.split("\n")
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hasChanges() {
|
|
71
|
+
return this.changes().length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lastCommit() {
|
|
75
|
+
return this.query(["log", "-1", "--pretty=%s"]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
add() {
|
|
79
|
+
this.run(["add", "."]);
|
|
80
|
+
|
|
81
|
+
return { success: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
commit(message) {
|
|
85
|
+
this.run(["commit", "-m", message]);
|
|
86
|
+
|
|
87
|
+
return { success: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
push() {
|
|
91
|
+
if (!this.hasRemote()) {
|
|
92
|
+
return { success: false, skipped: true, reason: "nenhum remoto configurado" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!this.hasUpstream()) {
|
|
96
|
+
const branch = this.branch();
|
|
97
|
+
|
|
98
|
+
this.run(["push", "--set-upstream", "origin", branch]);
|
|
99
|
+
|
|
100
|
+
return { success: true, upstream: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.run(["push"]);
|
|
104
|
+
|
|
105
|
+
return { success: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
tagExists(name) {
|
|
109
|
+
const output = this.query(["tag", "--list", name]);
|
|
110
|
+
|
|
111
|
+
return Boolean(output && output.length);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
tag(name, message) {
|
|
115
|
+
if (this.tagExists(name)) {
|
|
116
|
+
return { success: false, skipped: true, reason: "tag já existe" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.run(["tag", "-a", name, "-m", message || name]);
|
|
120
|
+
|
|
121
|
+
return { success: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pushTags() {
|
|
125
|
+
if (!this.hasRemote()) {
|
|
126
|
+
return { success: false, skipped: true, reason: "nenhum remoto configurado" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.run(["push", "--tags"]);
|
|
130
|
+
|
|
131
|
+
return { success: true };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = GitManager;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const VersionManager = require("./VersionManager");
|
|
2
|
+
const ConfigManager = require("./ConfigManager");
|
|
3
|
+
const FileManager = require("./FileManager");
|
|
4
|
+
const GitManager = require("./GitManager");
|
|
5
|
+
|
|
6
|
+
const logger = require("../utils/logger");
|
|
7
|
+
const { formatDuration } = require("../utils/time");
|
|
8
|
+
const { hasFlag } = require("../utils/args");
|
|
9
|
+
const { VERSION_TYPES } = require("../constants");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Orquestra o fluxo de release.
|
|
13
|
+
* Nenhuma regra de negócio fica no run(): apenas a sequência de etapas.
|
|
14
|
+
*/
|
|
15
|
+
class ReleaseManager {
|
|
16
|
+
constructor(cwd = process.cwd()) {
|
|
17
|
+
this.cwd = cwd;
|
|
18
|
+
|
|
19
|
+
this.configManager = new ConfigManager(cwd);
|
|
20
|
+
this.fileManager = new FileManager(cwd);
|
|
21
|
+
this.gitManager = new GitManager(cwd);
|
|
22
|
+
|
|
23
|
+
// Criado em loadConfig(), pois depende de config.versionFile.
|
|
24
|
+
this.versionManager = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async run(context) {
|
|
28
|
+
try {
|
|
29
|
+
logger.title(`Versioner · ${context.type}`);
|
|
30
|
+
|
|
31
|
+
this.validate(context);
|
|
32
|
+
this.loadConfig(context);
|
|
33
|
+
this.version(context);
|
|
34
|
+
this.updateFiles(context);
|
|
35
|
+
this.git(context);
|
|
36
|
+
this.finish(context);
|
|
37
|
+
|
|
38
|
+
return 0;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.rollback(context, error);
|
|
41
|
+
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Etapas ────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
validate(context) {
|
|
49
|
+
if (!VERSION_TYPES.includes(context.type)) {
|
|
50
|
+
throw new Error("Tipo de release inválido. Utilize: build, minor ou major.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
context.dryRun = hasFlag(context.flags, "dry-run");
|
|
54
|
+
|
|
55
|
+
if (!this.gitManager.isInstalled()) {
|
|
56
|
+
throw new Error("Git não encontrado no sistema.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.useGit(context) && !this.gitManager.isRepository()) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Este diretório não é um repositório Git. Use "git init" ou rode com --no-git.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loadConfig(context) {
|
|
67
|
+
context.config = this.configManager.load();
|
|
68
|
+
|
|
69
|
+
this.versionManager = new VersionManager(this.cwd, context.config.versionFile);
|
|
70
|
+
|
|
71
|
+
this.validateMessage(context);
|
|
72
|
+
|
|
73
|
+
logger.success("Configuração carregada.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
validateMessage(context) {
|
|
77
|
+
const { minLength, maxLength } = context.config.commit;
|
|
78
|
+
|
|
79
|
+
const message = String(context.message || "").trim();
|
|
80
|
+
|
|
81
|
+
if (!message) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Informe uma mensagem para o commit. Exemplo: versioner ${context.type} "Corrige login"`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (message.length < minLength) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`A mensagem do commit deve ter no mínimo ${minLength} caracteres.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (message.length > maxLength) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`A mensagem do commit deve ter no máximo ${maxLength} caracteres.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
context.message = message;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
version(context) {
|
|
103
|
+
const result = this.versionManager.increment(context.type, {
|
|
104
|
+
dryRun: context.dryRun,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
context.previous = result.previous;
|
|
108
|
+
context.current = result.current;
|
|
109
|
+
context.previousVersion = result.previousVersion;
|
|
110
|
+
context.version = result.version;
|
|
111
|
+
|
|
112
|
+
logger.success(`Versão ${context.previousVersion} → ${logger.bold(context.version)}`);
|
|
113
|
+
|
|
114
|
+
logger.info(`Mensagem: ${context.message}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateFiles(context) {
|
|
118
|
+
const files = context.config.files;
|
|
119
|
+
|
|
120
|
+
if (!files.length) {
|
|
121
|
+
logger.warn("Nenhum arquivo monitorado na configuração.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
logger.info("Atualizando arquivos...");
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const result = this.fileManager.update(file, context.version, {
|
|
129
|
+
dryRun: context.dryRun,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!result.updated) {
|
|
133
|
+
context.skipped.push({ path: file.path, reason: result.reason });
|
|
134
|
+
|
|
135
|
+
logger.muted(` ${file.path} ignorado (${result.reason})`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
context.files.push(file.path);
|
|
140
|
+
|
|
141
|
+
logger.success(` ${file.path} → ${file.field}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
git(context) {
|
|
146
|
+
if (!this.useGit(context)) {
|
|
147
|
+
logger.warn("Etapa do Git ignorada.");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const git = context.config.git;
|
|
152
|
+
|
|
153
|
+
logger.info("Executando Git...");
|
|
154
|
+
|
|
155
|
+
context.git.branch = this.gitManager.branch();
|
|
156
|
+
|
|
157
|
+
if (context.dryRun) {
|
|
158
|
+
logger.muted(" dry-run: nenhum comando Git foi executado.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (git.add) {
|
|
163
|
+
this.gitManager.add();
|
|
164
|
+
context.git.add = true;
|
|
165
|
+
logger.success(" git add");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (git.commit) {
|
|
169
|
+
const message = this.buildCommitMessage(context);
|
|
170
|
+
|
|
171
|
+
this.gitManager.commit(message);
|
|
172
|
+
context.git.commit = true;
|
|
173
|
+
logger.success(` git commit -m "${message}"`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this.shouldTag(context)) {
|
|
177
|
+
const name = `${git.tagPrefix}${context.version}`;
|
|
178
|
+
const message = git.tagMessage.replace("{version}", context.version);
|
|
179
|
+
|
|
180
|
+
const result = this.gitManager.tag(name, message);
|
|
181
|
+
|
|
182
|
+
context.git.tag = result.success;
|
|
183
|
+
context.git.tagName = result.success ? name : null;
|
|
184
|
+
|
|
185
|
+
if (result.success) {
|
|
186
|
+
logger.success(` git tag ${name}`);
|
|
187
|
+
} else {
|
|
188
|
+
logger.warn(` tag ignorada (${result.reason})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.shouldPush(context)) {
|
|
193
|
+
const result = this.gitManager.push();
|
|
194
|
+
|
|
195
|
+
context.git.push = result.success;
|
|
196
|
+
|
|
197
|
+
if (result.success) {
|
|
198
|
+
logger.success(" git push");
|
|
199
|
+
} else {
|
|
200
|
+
logger.warn(` push ignorado (${result.reason})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (context.git.tag && result.success) {
|
|
204
|
+
this.gitManager.pushTags();
|
|
205
|
+
logger.success(" git push --tags");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
finish(context) {
|
|
211
|
+
context.finishedAt = new Date();
|
|
212
|
+
|
|
213
|
+
const duration = context.finishedAt - context.startedAt;
|
|
214
|
+
|
|
215
|
+
this.fileManager.clearBackups();
|
|
216
|
+
|
|
217
|
+
logger.title(context.dryRun ? "Simulação concluída" : "Release concluída");
|
|
218
|
+
|
|
219
|
+
logger.field("Versão", `${context.previousVersion} → ${context.version}`);
|
|
220
|
+
logger.field("Arquivos", String(context.files.length));
|
|
221
|
+
|
|
222
|
+
if (context.git.branch) {
|
|
223
|
+
logger.field("Branch", context.git.branch);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
logger.field("Git add", this.mark(context.git.add));
|
|
227
|
+
logger.field("Git commit", this.mark(context.git.commit));
|
|
228
|
+
logger.field("Git push", this.mark(context.git.push));
|
|
229
|
+
|
|
230
|
+
if (context.git.tagName) {
|
|
231
|
+
logger.field("Tag", context.git.tagName);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
logger.field("Tempo", formatDuration(duration));
|
|
235
|
+
|
|
236
|
+
logger.break();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Suporte ───────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
rollback(context, error) {
|
|
242
|
+
logger.break();
|
|
243
|
+
logger.error(error.message);
|
|
244
|
+
|
|
245
|
+
const restoredFiles = this.fileManager.rollback();
|
|
246
|
+
|
|
247
|
+
const restoredVersion =
|
|
248
|
+
!context.dryRun && this.versionManager && context.previous?.build !== null
|
|
249
|
+
? this.versionManager.rollback(context.previous)
|
|
250
|
+
: false;
|
|
251
|
+
|
|
252
|
+
if (restoredFiles || restoredVersion) {
|
|
253
|
+
logger.warn("Alterações revertidas. Nada foi commitado.");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (process.env.VERSIONER_DEBUG) {
|
|
257
|
+
console.error(error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
buildCommitMessage(context) {
|
|
262
|
+
return context.config.commit.template
|
|
263
|
+
.replace("{version}", context.version)
|
|
264
|
+
.replace("{message}", context.message)
|
|
265
|
+
.replace("{type}", context.type);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
useGit(context) {
|
|
269
|
+
if (hasFlag(context.flags, "no-git")) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return context.config ? context.config.git.enabled !== false : true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
shouldPush(context) {
|
|
277
|
+
if (hasFlag(context.flags, "no-push")) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return context.config.git.push !== false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
shouldTag(context) {
|
|
285
|
+
if (hasFlag(context.flags, "tag")) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (hasFlag(context.flags, "no-tag")) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return context.config.git.tag === true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
mark(value) {
|
|
297
|
+
return value ? "✔" : "✖";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = ReleaseManager;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { resolve, exists, readJSON, writeJSON } = require("../utils/file");
|
|
2
|
+
const { VERSION_FILE, DEFAULT_VERSION, VERSION_TYPES } = require("../constants");
|
|
3
|
+
|
|
4
|
+
class VersionManager {
|
|
5
|
+
constructor(cwd = process.cwd(), versionFile = VERSION_FILE) {
|
|
6
|
+
this.cwd = cwd;
|
|
7
|
+
this.file = resolve(cwd, versionFile || VERSION_FILE);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
exists() {
|
|
11
|
+
return exists(this.file);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lê o arquivo de versão. Cria com os valores padrão se não existir.
|
|
16
|
+
*/
|
|
17
|
+
load() {
|
|
18
|
+
if (!this.exists()) {
|
|
19
|
+
this.save(DEFAULT_VERSION);
|
|
20
|
+
|
|
21
|
+
return { ...DEFAULT_VERSION };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const version = readJSON(this.file);
|
|
25
|
+
|
|
26
|
+
return this.normalize(version);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Garante que major/minor/build existam e sejam números inteiros.
|
|
31
|
+
*/
|
|
32
|
+
normalize(version) {
|
|
33
|
+
const result = { ...DEFAULT_VERSION };
|
|
34
|
+
|
|
35
|
+
for (const key of Object.keys(DEFAULT_VERSION)) {
|
|
36
|
+
const value = Number(version?.[key]);
|
|
37
|
+
|
|
38
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Valor inválido para "${key}" no arquivo de versão. Esperado um inteiro >= 0.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
result[key] = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
save(version) {
|
|
51
|
+
writeJSON(this.file, {
|
|
52
|
+
major: version.major,
|
|
53
|
+
minor: version.minor,
|
|
54
|
+
build: version.build,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return this.file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toString(version) {
|
|
61
|
+
return `${version.major}.${version.minor}.${version.build}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calcula a próxima versão sem gravar nada.
|
|
66
|
+
*/
|
|
67
|
+
next(type, version) {
|
|
68
|
+
if (!VERSION_TYPES.includes(type)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Tipo de versão inválido: "${type}". Utilize build, minor ou major.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = { ...version };
|
|
75
|
+
|
|
76
|
+
// A Build sempre incrementa. Ela nunca volta para zero.
|
|
77
|
+
result.build += 1;
|
|
78
|
+
|
|
79
|
+
if (type === "minor") {
|
|
80
|
+
result.minor += 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (type === "major") {
|
|
84
|
+
result.major += 1;
|
|
85
|
+
result.minor = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Incrementa e persiste (a menos que dryRun seja true).
|
|
93
|
+
*/
|
|
94
|
+
increment(type, { dryRun = false } = {}) {
|
|
95
|
+
const previous = this.load();
|
|
96
|
+
|
|
97
|
+
const current = this.next(type, previous);
|
|
98
|
+
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
this.save(current);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
previous,
|
|
105
|
+
current,
|
|
106
|
+
previousVersion: this.toString(previous),
|
|
107
|
+
version: this.toString(current),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Volta o arquivo de versão para um estado anterior.
|
|
113
|
+
* Usado quando o Git falha no meio da release.
|
|
114
|
+
*/
|
|
115
|
+
rollback(version) {
|
|
116
|
+
if (!version) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.save(version);
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = VersionManager;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const BuildCommand = require("../commands/BuildCommand");
|
|
2
|
+
const MinorCommand = require("../commands/MinorCommand");
|
|
3
|
+
const MajorCommand = require("../commands/MajorCommand");
|
|
4
|
+
const InitCommand = require("../commands/InitCommand");
|
|
5
|
+
const HelpCommand = require("../commands/HelpCommand");
|
|
6
|
+
const VersionCommand = require("../commands/VersionCommand");
|
|
7
|
+
const StatusCommand = require("../commands/StatusCommand");
|
|
8
|
+
|
|
9
|
+
const logger = require("../utils/logger");
|
|
10
|
+
const { ALIASES } = require("../constants");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Despacha o comando digitado na CLI.
|
|
14
|
+
* Sem switch: usa um mapa nome → classe.
|
|
15
|
+
*/
|
|
16
|
+
class CommandRouter {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.commands = {
|
|
19
|
+
build: BuildCommand,
|
|
20
|
+
minor: MinorCommand,
|
|
21
|
+
major: MajorCommand,
|
|
22
|
+
init: InitCommand,
|
|
23
|
+
help: HelpCommand,
|
|
24
|
+
version: VersionCommand,
|
|
25
|
+
status: StatusCommand,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolve(name) {
|
|
30
|
+
if (!name) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.commands[ALIASES[name] || name] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async run(args = []) {
|
|
38
|
+
const [name, ...rest] = args;
|
|
39
|
+
|
|
40
|
+
// Sem argumentos: mostra a ajuda.
|
|
41
|
+
if (!name) {
|
|
42
|
+
return new HelpCommand().run([]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const Command = this.resolve(name);
|
|
46
|
+
|
|
47
|
+
if (!Command) {
|
|
48
|
+
logger.error(`Comando desconhecido: ${name}`);
|
|
49
|
+
|
|
50
|
+
const suggestion = this.suggest(name);
|
|
51
|
+
|
|
52
|
+
if (suggestion) {
|
|
53
|
+
logger.muted(` Você quis dizer "${suggestion}"?`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.muted(' Use "versioner help" para ver a lista de comandos.');
|
|
57
|
+
logger.break();
|
|
58
|
+
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Command().run(rest);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sugestão simples por distância de Levenshtein.
|
|
67
|
+
*/
|
|
68
|
+
suggest(name) {
|
|
69
|
+
let best = null;
|
|
70
|
+
let bestDistance = Infinity;
|
|
71
|
+
|
|
72
|
+
for (const command of Object.keys(this.commands)) {
|
|
73
|
+
const distance = this.distance(name.toLowerCase(), command);
|
|
74
|
+
|
|
75
|
+
if (distance < bestDistance) {
|
|
76
|
+
bestDistance = distance;
|
|
77
|
+
best = command;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return bestDistance <= 2 ? best : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
distance(a, b) {
|
|
85
|
+
const matrix = Array.from({ length: a.length + 1 }, (_, i) => [i]);
|
|
86
|
+
|
|
87
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
88
|
+
matrix[0][j] = j;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
92
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
93
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
94
|
+
|
|
95
|
+
matrix[i][j] = Math.min(
|
|
96
|
+
matrix[i - 1][j] + 1,
|
|
97
|
+
matrix[i][j - 1] + 1,
|
|
98
|
+
matrix[i - 1][j - 1] + cost,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return matrix[a.length][b.length];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = CommandRouter;
|