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.
@@ -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.2",
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
- "prompts": "^2.4.2"
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
  }