specifica-br 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -2
- package/dist/commands/help.js +38 -40
- package/dist/commands/init.js +67 -45
- package/dist/commands/upgrade.d.ts +0 -3
- package/dist/commands/upgrade.js +92 -21
- package/dist/index.d.ts +0 -1
- package/dist/index.js +13 -16
- package/dist/settings.json +4 -0
- package/dist/types/init.d.ts +3 -2
- package/dist/types/init.js +1 -2
- package/dist/types/settings.d.ts +9 -0
- package/dist/types/settings.js +1 -0
- package/dist/utils/file-service.d.ts +5 -2
- package/dist/utils/file-service.js +65 -98
- package/dist/utils/log-service.d.ts +7 -0
- package/dist/utils/log-service.js +48 -0
- package/dist/utils/message-formatter.d.ts +1 -1
- package/dist/utils/message-formatter.js +47 -50
- package/dist/utils/settings-service.d.ts +6 -0
- package/dist/utils/settings-service.js +26 -0
- package/dist/utils/update-notifier-middleware.d.ts +59 -0
- package/dist/utils/update-notifier-middleware.js +276 -0
- package/package.json +11 -5
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { settingsService } from './settings-service.js';
|
|
6
|
+
import { logService } from './log-service.js';
|
|
7
|
+
import latestVersion from 'latest-version';
|
|
8
|
+
import semver from 'semver';
|
|
9
|
+
import { exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import prompts from 'prompts';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PACKAGE_JSON_PATH = path.join(__dirname, '..', '..', 'package.json');
|
|
15
|
+
class UpdateNotifierMiddleware {
|
|
16
|
+
async shouldNotify(commandName) {
|
|
17
|
+
const settings = await settingsService.getSettings();
|
|
18
|
+
return settings.enabledUpgradeCommands.includes(commandName);
|
|
19
|
+
}
|
|
20
|
+
async getLatestVersion(packageName) {
|
|
21
|
+
try {
|
|
22
|
+
const version = await latestVersion(packageName);
|
|
23
|
+
return version;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execAsync(`npm view ${packageName} version`);
|
|
28
|
+
const version = stdout.trim();
|
|
29
|
+
return version;
|
|
30
|
+
}
|
|
31
|
+
catch (fallbackError) {
|
|
32
|
+
console.error('Erro ao obter versão mais recente:', fallbackError);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Obtém a versão mais recente com timeout configurável
|
|
39
|
+
* @param packageName Nome do pacote
|
|
40
|
+
* @param timeoutMs Timeout em milissegundos
|
|
41
|
+
* @returns Promise com a versão ou null em caso de timeout/erro
|
|
42
|
+
*/
|
|
43
|
+
async getLatestVersionWithTimeout(packageName, timeoutMs) {
|
|
44
|
+
try {
|
|
45
|
+
// Criar uma Promise de timeout
|
|
46
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
47
|
+
setTimeout(() => reject(new Error('Timeout na verificação de versão')), timeoutMs);
|
|
48
|
+
});
|
|
49
|
+
// Correr as duas promises em paralelo - a primeira a resolver/rejeitar vence
|
|
50
|
+
const version = await Promise.race([
|
|
51
|
+
this.getLatestVersion(packageName),
|
|
52
|
+
timeoutPromise
|
|
53
|
+
]);
|
|
54
|
+
return version;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// Em caso de timeout ou erro, retornar null silenciosamente
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verifica se há uma nova versão disponível
|
|
63
|
+
* @param currentVersion Versão atual
|
|
64
|
+
* @param latestVersion Versão mais recente
|
|
65
|
+
* @returns boolean true se houver nova versão
|
|
66
|
+
*/
|
|
67
|
+
isNewVersionAvailable(currentVersion, latestVersion) {
|
|
68
|
+
// Validar versões
|
|
69
|
+
if (!this.isValidVersion(currentVersion) || !this.isValidVersion(latestVersion)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Verificar se a versão atual é menor que a mais recente
|
|
73
|
+
return semver.lt(currentVersion, latestVersion);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Obtém a versão atual do package.json
|
|
77
|
+
* @returns string com a versão atual
|
|
78
|
+
*/
|
|
79
|
+
getCurrentVersion() {
|
|
80
|
+
try {
|
|
81
|
+
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
|
|
82
|
+
return packageJson.version;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error('Não foi possível ler a versão atual do package.json');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
isValidVersion(version) {
|
|
89
|
+
if (!version) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const isValid = semver.valid(version);
|
|
93
|
+
return isValid !== null;
|
|
94
|
+
}
|
|
95
|
+
getUpdateType(currentVersion, latestVersion) {
|
|
96
|
+
const diff = semver.diff(currentVersion, latestVersion);
|
|
97
|
+
if (!diff) {
|
|
98
|
+
return 'unknown';
|
|
99
|
+
}
|
|
100
|
+
return diff;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Exibe prompt interativo perguntando se o usuário deseja atualizar
|
|
104
|
+
* @param latestVersion Versão mais recente disponível
|
|
105
|
+
* @returns Promise<boolean> true se o usuário aceitar atualizar
|
|
106
|
+
*/
|
|
107
|
+
async promptUserForUpdate(latestVersion) {
|
|
108
|
+
try {
|
|
109
|
+
const response = await prompts({
|
|
110
|
+
type: 'confirm',
|
|
111
|
+
name: 'shouldUpdate',
|
|
112
|
+
message: `Deseja atualizar para a versão ${latestVersion} agora? (Y=Sim, n=Não)`,
|
|
113
|
+
initial: true
|
|
114
|
+
});
|
|
115
|
+
// Se o usuário pressionar Ctrl+C, response será undefined
|
|
116
|
+
if (response === undefined) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return response.shouldUpdate;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
// Em caso de erro no prompt, assumir que o usuário não quer atualizar
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Exibe prompt de retry após falha na atualização
|
|
128
|
+
* @returns Promise<boolean> true se o usuário quiser tentar novamente
|
|
129
|
+
*/
|
|
130
|
+
async promptForRetry() {
|
|
131
|
+
try {
|
|
132
|
+
const response = await prompts({
|
|
133
|
+
type: 'confirm',
|
|
134
|
+
name: 'shouldRetry',
|
|
135
|
+
message: 'Erro ao atualizar. Deseja tentar novamente?',
|
|
136
|
+
initial: true
|
|
137
|
+
});
|
|
138
|
+
// Se o usuário pressionar Ctrl+C, response será undefined
|
|
139
|
+
if (response === undefined) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return response.shouldRetry;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Em caso de erro no prompt, assumir que o usuário não quer tentar novamente
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Executa a atualização do pacote via npm install -g
|
|
151
|
+
* @param retryCount Número de tentativas já realizadas
|
|
152
|
+
* @returns Promise<void>
|
|
153
|
+
*/
|
|
154
|
+
async executeUpdate(retryCount = 0) {
|
|
155
|
+
try {
|
|
156
|
+
// Executar comando de atualização
|
|
157
|
+
await execAsync('npm install -g specifica-br@latest', {
|
|
158
|
+
timeout: 60000, // Timeout de 60 segundos para o npm install
|
|
159
|
+
});
|
|
160
|
+
// Exibir mensagem de sucesso
|
|
161
|
+
console.log('specifica-br atualizado com sucesso!');
|
|
162
|
+
// Encerrar processo após atualização bem-sucedida
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
// Log de erro
|
|
167
|
+
logService.logError(error instanceof Error ? error : new Error(String(error)), 'UpdateInterceptorMiddleware - Falha na atualização');
|
|
168
|
+
// Exibir mensagem de erro para o usuário
|
|
169
|
+
console.error('Erro ao atualizar specifica-br:');
|
|
170
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
171
|
+
// Perguntar se deseja tentar novamente
|
|
172
|
+
const shouldRetry = await this.promptForRetry();
|
|
173
|
+
if (shouldRetry) {
|
|
174
|
+
// Limitar a 3 tentativas para evitar loop infinito
|
|
175
|
+
if (retryCount < 2) {
|
|
176
|
+
console.log('Tentando novamente...');
|
|
177
|
+
await this.executeUpdate(retryCount + 1);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.error('Número máximo de tentativas atingido. Por favor, tente atualizar manualmente.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Se não quiser tentar novamente ou atingir limite de tentativas, lançar erro
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Exibe notificação e prompt interativo para atualização
|
|
189
|
+
* @param latestVersion Versão mais recente disponível
|
|
190
|
+
* @param originalAction Função original a ser executada se recusar atualização
|
|
191
|
+
* @returns Promise<void>
|
|
192
|
+
*/
|
|
193
|
+
async displayNotificationWithPrompt(latestVersion, originalAction) {
|
|
194
|
+
const currentVersion = this.getCurrentVersion();
|
|
195
|
+
const type = this.getUpdateType(currentVersion, latestVersion);
|
|
196
|
+
this.displayNotification({
|
|
197
|
+
name: 'specifica-br',
|
|
198
|
+
currentVersion,
|
|
199
|
+
latestVersion,
|
|
200
|
+
type
|
|
201
|
+
});
|
|
202
|
+
const shouldUpdate = await this.promptUserForUpdate(latestVersion);
|
|
203
|
+
if (shouldUpdate) {
|
|
204
|
+
await this.executeUpdate();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
await originalAction();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Envelopa uma função com verificação de atualização
|
|
212
|
+
* @param commandName Nome do comando sendo executado
|
|
213
|
+
* @param originalAction Função original a ser executada
|
|
214
|
+
* @returns Promise com o resultado da função original
|
|
215
|
+
*/
|
|
216
|
+
async wrap(commandName, originalAction) {
|
|
217
|
+
// Verificar se o comando está na lista de comandos habilitados para verificação
|
|
218
|
+
const settings = await settingsService.getSettings();
|
|
219
|
+
const enabledCommands = settings.enabledUpgradeCommands || [];
|
|
220
|
+
if (!enabledCommands.includes(commandName)) {
|
|
221
|
+
// Se não estiver na lista, executar ação original sem verificação
|
|
222
|
+
await originalAction();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// Obter configuração de timeout
|
|
226
|
+
const timeoutMs = settings.versionCheckTimeoutMs || 1500;
|
|
227
|
+
try {
|
|
228
|
+
// Verificar versão mais recente com timeout
|
|
229
|
+
const latestVersion = await this.getLatestVersionWithTimeout('specifica-br', timeoutMs);
|
|
230
|
+
// Se não conseguiu obter versão (timeout, erro, etc.), executar ação original
|
|
231
|
+
if (!latestVersion) {
|
|
232
|
+
await originalAction();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Obter versão atual
|
|
236
|
+
const currentVersion = this.getCurrentVersion();
|
|
237
|
+
// Verificar se há nova versão disponível
|
|
238
|
+
if (!this.isNewVersionAvailable(currentVersion, latestVersion)) {
|
|
239
|
+
// Se não há nova versão, executar ação original
|
|
240
|
+
await originalAction();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Se chegou aqui, há nova versão disponível - iniciar fluxo de atualização
|
|
244
|
+
await this.displayNotificationWithPrompt(latestVersion, originalAction);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// Em caso de qualquer erro no fluxo de interceptação, executar ação original
|
|
248
|
+
logService.logError(error instanceof Error ? error : new Error(String(error)), `UpdateInterceptorMiddleware - Erro no comando ${commandName}`);
|
|
249
|
+
await originalAction();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
displayNotification(update) {
|
|
253
|
+
const { name, currentVersion, latestVersion, type } = update;
|
|
254
|
+
const lines = [
|
|
255
|
+
'Update disponível',
|
|
256
|
+
'',
|
|
257
|
+
name,
|
|
258
|
+
`${currentVersion} → ${latestVersion}`
|
|
259
|
+
];
|
|
260
|
+
const boxWidth = 60;
|
|
261
|
+
const horizontalBorder = '─'.repeat(boxWidth);
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(chalk.bgYellow.black('┌' + horizontalBorder + '┐'));
|
|
264
|
+
lines.forEach(line => {
|
|
265
|
+
const content = line.trim() === '' ? '' : line;
|
|
266
|
+
const padding = Math.max(0, boxWidth - content.length);
|
|
267
|
+
const leftPadding = Math.floor(padding / 2);
|
|
268
|
+
const rightPadding = padding - leftPadding;
|
|
269
|
+
const paddedLine = ' '.repeat(leftPadding) + content + ' '.repeat(rightPadding);
|
|
270
|
+
console.log(chalk.bgYellow.black('│' + paddedLine + '│'));
|
|
271
|
+
});
|
|
272
|
+
console.log(chalk.bgYellow.black('└' + horizontalBorder + '┘'));
|
|
273
|
+
console.log('');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export const updateNotifierMiddleware = new UpdateNotifierMiddleware();
|
package/package.json
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specifica-br",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Ferramenta de automação para desenvolvimento guiado por especificações (Spec Driven Development - SDD) com IA. Otimizado para o ecossistema brasileiro.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"bin": {
|
|
7
8
|
"specifica-br": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
|
-
"build": "tsc && npm run copy:assets",
|
|
11
|
+
"build": "tsc && npm run copy:assets && npm run add:shebang",
|
|
11
12
|
"copy:assets": "npx copyfiles -u 2 \"src/assets/**/*\" dist",
|
|
13
|
+
"add:shebang": "sed -i '1i#!/usr/bin/env node' dist/index.js",
|
|
12
14
|
"start": "node dist/index.js",
|
|
13
|
-
"dev": "tsc && npm run copy:assets && node dist/index.js"
|
|
15
|
+
"dev": "tsc && npm run copy:assets && npm run add:shebang && node dist/index.js"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
16
18
|
"spec driven development",
|
|
19
|
+
"spec-driven-development",
|
|
20
|
+
"spec-driven",
|
|
17
21
|
"SSD",
|
|
18
22
|
"spec",
|
|
19
23
|
"especificar",
|
|
20
24
|
"ai",
|
|
21
25
|
"ia",
|
|
22
|
-
"spec-driven-development",
|
|
23
26
|
"automation",
|
|
24
27
|
"opencode",
|
|
25
28
|
"brazil",
|
|
@@ -38,11 +41,14 @@
|
|
|
38
41
|
"chalk": "^4.1.2",
|
|
39
42
|
"commander": "^14.0.3",
|
|
40
43
|
"fs-extra": "^11.3.3",
|
|
41
|
-
"
|
|
44
|
+
"latest-version": "^9.0.0",
|
|
45
|
+
"prompts": "^2.4.2",
|
|
46
|
+
"semver": "^7.7.4"
|
|
42
47
|
},
|
|
43
48
|
"devDependencies": {
|
|
44
49
|
"@types/fs-extra": "^11.0.4",
|
|
45
50
|
"@types/node": "^25.2.3",
|
|
51
|
+
"@types/semver": "^7.7.1",
|
|
46
52
|
"copyfiles": "^2.4.1",
|
|
47
53
|
"typescript": "^5.3.3"
|
|
48
54
|
}
|