opencode-conductor-cdd-plugin 1.0.0-beta.19 → 1.0.0-beta.21

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.
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { validateSelection, parseSelection } from './interactiveMenu.js';
4
+ import { translateQuestion, detectLanguage } from './languageSupport.js';
4
5
  // Constants
5
6
  const MAX_QUESTIONS = 5;
6
7
  const MAX_REVISION_ATTEMPTS = 3;
@@ -18,6 +19,7 @@ const CHECKPOINT_MAP = {
18
19
  */
19
20
  export async function presentQuestionsSequentially(questions, responder, options = {}) {
20
21
  const { maxQuestions = MAX_QUESTIONS, customInputPrompt } = options;
22
+ let { language = 'en' } = options;
21
23
  const session = {
22
24
  section: questions[0]?.section || 'product',
23
25
  questionsAsked: [],
@@ -28,30 +30,47 @@ export async function presentQuestionsSequentially(questions, responder, options
28
30
  };
29
31
  for (let i = 0; i < Math.min(questions.length, maxQuestions); i++) {
30
32
  const question = questions[i];
31
- session.questionsAsked.push(question);
33
+ // Translate question if language is Spanish
34
+ const translatedQuestion = language === 'es' ? translateQuestion(question, 'es') : question;
35
+ session.questionsAsked.push(translatedQuestion);
32
36
  // Get user response
33
- let selections;
37
+ let selections = [];
34
38
  let isValid = false;
35
39
  // Keep asking until valid response
36
40
  while (!isValid) {
37
- selections = await responder(question, i + 1);
38
- isValid = validateSelection(selections.join(','), question.type);
41
+ selections = await responder(translatedQuestion, i + 1);
42
+ isValid = validateSelection(selections.join(','), translatedQuestion.type);
39
43
  }
40
- const parsedSelections = parseSelection(selections.join(','), question.type);
41
- // Map selections to actual option text
44
+ // Auto-detect language from first user response if custom input provided
45
+ if (i === 0 && selections.length > 0 && selections[0].length > 5) {
46
+ const detectedLang = detectLanguage(selections.join(' '));
47
+ if (detectedLang !== language) {
48
+ language = detectedLang;
49
+ }
50
+ }
51
+ const parsedSelections = parseSelection(selections.join(','), translatedQuestion.type);
52
+ // Map selections to actual option text (from translated question)
42
53
  const selectedOptions = parsedSelections.map(letter => {
43
54
  const index = letter.charCodeAt(0) - 'A'.charCodeAt(0);
44
- return question.options[index];
55
+ return translatedQuestion.options[index];
45
56
  });
46
57
  const answer = {
47
- questionId: question.id,
58
+ questionId: translatedQuestion.id,
48
59
  selections: parsedSelections,
49
60
  selectedOptions,
50
61
  timestamp: new Date().toISOString(),
51
62
  };
52
63
  // Handle option D (custom input)
53
64
  if (parsedSelections.includes('D') && customInputPrompt) {
54
- answer.customText = await customInputPrompt(question);
65
+ const customInput = await customInputPrompt(translatedQuestion);
66
+ answer.customText = customInput;
67
+ // Detect language from custom text input
68
+ if (customInput && customInput.length > 5) {
69
+ const detectedLang = detectLanguage(customInput);
70
+ if (detectedLang !== language) {
71
+ language = detectedLang;
72
+ }
73
+ }
55
74
  }
56
75
  session.answers.push(answer);
57
76
  // Handle option E (auto-generate) - stop asking questions
@@ -242,6 +261,7 @@ export async function generateDocument(options) {
242
261
  // Step 1: Present questions sequentially
243
262
  const session = await presentQuestionsSequentially(questions, responder, {
244
263
  customInputPrompt,
264
+ language: options.language || 'en',
245
265
  });
246
266
  // Step 2: Draft document from answers
247
267
  let draft = draftDocumentFromAnswers(session, analysis);
@@ -121,6 +121,11 @@ describe('interactiveMenu', () => {
121
121
  expect(validateSelection('', 'additive')).toBe(false);
122
122
  expect(validateSelection(' ', 'exclusive')).toBe(false);
123
123
  });
124
+ it('should reject input with only commas or whitespace', () => {
125
+ expect(validateSelection(',,,', 'exclusive')).toBe(false);
126
+ expect(validateSelection(', , ,', 'additive')).toBe(false);
127
+ expect(validateSelection(' , ', 'exclusive')).toBe(false);
128
+ });
124
129
  it('should reject option E in combination with others for additive', () => {
125
130
  expect(validateSelection('A,E', 'additive')).toBe(false);
126
131
  expect(validateSelection('E,A', 'additive')).toBe(false);
@@ -0,0 +1,5 @@
1
+ import { Question } from './questionGenerator.js';
2
+ export type Language = 'en' | 'es';
3
+ export declare function detectLanguage(text: string): Language;
4
+ export declare function translateQuestion(question: Question, language: Language): Question;
5
+ export declare function translateInstruction(key: string, language: Language): string;
@@ -0,0 +1,163 @@
1
+ const SPANISH_INDICATORS = [
2
+ '¿', '¡', 'á', 'é', 'í', 'ó', 'ú', 'ñ', 'ü',
3
+ 'cuál', 'qué', 'cómo', 'dónde', 'cuándo', 'quién',
4
+ 'los', 'las', 'una', 'del', 'con', 'para', 'por',
5
+ 'necesito', 'quiero', 'aplicación', 'proyecto', 'crear',
6
+ 'objetivo', 'principal', 'usuarios', 'principales',
7
+ 'añadir', 'configuración', 'nuevo', 'algo',
8
+ ];
9
+ const ENGLISH_INDICATORS = [
10
+ 'what', 'how', 'where', 'when', 'who', 'which',
11
+ 'the', 'a', 'an', 'of', 'with', 'for', 'by',
12
+ 'need', 'want', 'application', 'project', 'create',
13
+ 'main', 'primary', 'users', 'add', 'something', 'new',
14
+ ];
15
+ const TRANSLATIONS = {
16
+ 'menu.select_option': {
17
+ en: 'Select your option (e.g., "A", "B", "C")',
18
+ es: 'Selecciona tu opción (ej: "A", "B", "C")',
19
+ },
20
+ 'menu.custom_input': {
21
+ en: 'Enter custom text',
22
+ es: 'Ingresa texto personalizado',
23
+ },
24
+ 'menu.auto_generate': {
25
+ en: 'Auto-generate from context',
26
+ es: 'Auto-generar desde contexto',
27
+ },
28
+ 'approval.approve': {
29
+ en: 'Approve this document',
30
+ es: 'Aprobar este documento',
31
+ },
32
+ 'approval.revise': {
33
+ en: 'Request revisions',
34
+ es: 'Solicitar revisiones',
35
+ },
36
+ 'question.primary_purpose': {
37
+ en: 'What is the primary purpose of this project?',
38
+ es: '¿Cuál es el propósito principal de este proyecto?',
39
+ },
40
+ 'option.web_application': {
41
+ en: 'Build a web application',
42
+ es: 'Construir una aplicación web',
43
+ },
44
+ 'option.cli_tool': {
45
+ en: 'Build a CLI tool',
46
+ es: 'Construir una herramienta CLI',
47
+ },
48
+ 'option.library': {
49
+ en: 'Create a library',
50
+ es: 'Crear una biblioteca',
51
+ },
52
+ 'option.custom': {
53
+ en: 'Enter custom text',
54
+ es: 'Ingresar texto personalizado',
55
+ },
56
+ 'option.auto_generate': {
57
+ en: 'Auto-generate from context',
58
+ es: 'Auto-generar desde contexto',
59
+ },
60
+ };
61
+ export function detectLanguage(text) {
62
+ if (!text || text.trim().length === 0) {
63
+ return 'en';
64
+ }
65
+ const lowerText = text.toLowerCase();
66
+ const words = lowerText.split(/\s+/);
67
+ let spanishScore = 0;
68
+ let englishScore = 0;
69
+ for (let i = 0; i < words.length; i++) {
70
+ const word = words[i];
71
+ // First word gets significantly more weight (10x) to dominate language detection
72
+ const weight = i === 0 ? 10 : 1;
73
+ if (SPANISH_INDICATORS.includes(word)) {
74
+ spanishScore += weight;
75
+ }
76
+ if (ENGLISH_INDICATORS.includes(word)) {
77
+ englishScore += weight;
78
+ }
79
+ }
80
+ // Special characters strongly indicate Spanish
81
+ for (const char of ['¿', '¡', 'á', 'é', 'í', 'ó', 'ú', 'ñ', 'ü']) {
82
+ if (lowerText.includes(char)) {
83
+ spanishScore += 5;
84
+ }
85
+ }
86
+ return spanishScore > englishScore ? 'es' : 'en';
87
+ }
88
+ export function translateQuestion(question, language) {
89
+ if (language === 'en') {
90
+ return question;
91
+ }
92
+ const translated = {
93
+ ...question,
94
+ text: translateQuestionText(question.text, question.id),
95
+ options: question.options.map((opt, idx) => translateOption(opt, idx)),
96
+ };
97
+ return translated;
98
+ }
99
+ function translateQuestionText(text, questionId) {
100
+ const translationKey = `question.${questionId}`;
101
+ if (TRANSLATIONS[translationKey]) {
102
+ return TRANSLATIONS[translationKey].es;
103
+ }
104
+ if (text.includes('primary purpose')) {
105
+ return '¿Cuál es el propósito principal de este proyecto?';
106
+ }
107
+ if (text.includes('target users')) {
108
+ return '¿Quiénes son los usuarios objetivo?';
109
+ }
110
+ if (text.includes('key features')) {
111
+ return '¿Cuáles son las características principales?';
112
+ }
113
+ if (text.includes('programming language')) {
114
+ return '¿Qué lenguajes de programación usarás?';
115
+ }
116
+ if (text.includes('framework')) {
117
+ return '¿Qué frameworks usarás?';
118
+ }
119
+ return text;
120
+ }
121
+ function translateOption(option, index) {
122
+ const optionLower = option.toLowerCase();
123
+ if (optionLower.includes('web application')) {
124
+ return 'Construir una aplicación web';
125
+ }
126
+ if (optionLower.includes('cli tool')) {
127
+ return 'Construir una herramienta CLI';
128
+ }
129
+ if (optionLower.includes('library')) {
130
+ return 'Crear una biblioteca';
131
+ }
132
+ if (optionLower.includes('enter custom') || optionLower.includes('custom text')) {
133
+ return 'Ingresar texto personalizado';
134
+ }
135
+ if (optionLower.includes('auto-generate')) {
136
+ return 'Auto-generar desde contexto';
137
+ }
138
+ if (optionLower.includes('developers')) {
139
+ return 'Desarrolladores';
140
+ }
141
+ if (optionLower.includes('end users')) {
142
+ return 'Usuarios finales';
143
+ }
144
+ if (optionLower.includes('businesses')) {
145
+ return 'Empresas';
146
+ }
147
+ if (optionLower.includes('typescript')) {
148
+ return 'TypeScript';
149
+ }
150
+ if (optionLower.includes('python')) {
151
+ return 'Python';
152
+ }
153
+ if (optionLower.includes('javascript')) {
154
+ return 'JavaScript';
155
+ }
156
+ return option;
157
+ }
158
+ export function translateInstruction(key, language) {
159
+ if (TRANSLATIONS[key] && TRANSLATIONS[key][language]) {
160
+ return TRANSLATIONS[key][language];
161
+ }
162
+ return key;
163
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectLanguage, translateQuestion, translateInstruction, } from './languageSupport.js';
3
+ describe('languageSupport', () => {
4
+ describe('detectLanguage', () => {
5
+ it('should detect Spanish from common Spanish words', () => {
6
+ expect(detectLanguage('¿Cuál es el objetivo principal?')).toBe('es');
7
+ expect(detectLanguage('Los usuarios principales son')).toBe('es');
8
+ expect(detectLanguage('Necesito crear una aplicación')).toBe('es');
9
+ });
10
+ it('should detect Spanish from Spanish-specific characters', () => {
11
+ expect(detectLanguage('Añadir funcionalidad')).toBe('es');
12
+ expect(detectLanguage('Configuración básica')).toBe('es');
13
+ });
14
+ it('should detect English from common English words', () => {
15
+ expect(detectLanguage('What is the primary goal?')).toBe('en');
16
+ expect(detectLanguage('The main users are')).toBe('en');
17
+ expect(detectLanguage('I need to create an application')).toBe('en');
18
+ });
19
+ it('should default to English for ambiguous input', () => {
20
+ expect(detectLanguage('123')).toBe('en');
21
+ expect(detectLanguage('React TypeScript')).toBe('en');
22
+ expect(detectLanguage('')).toBe('en');
23
+ });
24
+ it('should handle mixed language input', () => {
25
+ // Should detect based on predominant language
26
+ expect(detectLanguage('Create a proyecto nuevo')).toBe('en');
27
+ expect(detectLanguage('Necesito create something')).toBe('es');
28
+ });
29
+ });
30
+ describe('translateQuestion', () => {
31
+ const englishQuestion = {
32
+ id: 'product-1',
33
+ text: 'What is the primary purpose of this project?',
34
+ type: 'exclusive',
35
+ section: 'product',
36
+ options: [
37
+ 'Build a web application',
38
+ 'Build a CLI tool',
39
+ 'Create a library',
40
+ 'Enter custom text',
41
+ 'Auto-generate from context',
42
+ ],
43
+ };
44
+ it('should translate question text to Spanish', () => {
45
+ const translated = translateQuestion(englishQuestion, 'es');
46
+ expect(translated.text).toContain('propósito');
47
+ expect(translated.text).toContain('proyecto');
48
+ });
49
+ it('should translate options to Spanish', () => {
50
+ const translated = translateQuestion(englishQuestion, 'es');
51
+ expect(translated.options[0]).toContain('aplicación web');
52
+ expect(translated.options[3]).toContain('texto personalizado');
53
+ expect(translated.options[4]).toContain('Auto-generar');
54
+ });
55
+ it('should keep English text when language is English', () => {
56
+ const translated = translateQuestion(englishQuestion, 'en');
57
+ expect(translated.text).toBe(englishQuestion.text);
58
+ expect(translated.options).toEqual(englishQuestion.options);
59
+ });
60
+ it('should preserve question metadata (id, type, section)', () => {
61
+ const translated = translateQuestion(englishQuestion, 'es');
62
+ expect(translated.id).toBe(englishQuestion.id);
63
+ expect(translated.type).toBe(englishQuestion.type);
64
+ expect(translated.section).toBe(englishQuestion.section);
65
+ });
66
+ it('should use fallback translation for target users question', () => {
67
+ const question = {
68
+ id: 'users-1',
69
+ text: 'Who are the target users?',
70
+ type: 'exclusive',
71
+ section: 'product',
72
+ options: ['Developers', 'End users', 'Businesses'],
73
+ };
74
+ const translated = translateQuestion(question, 'es');
75
+ expect(translated.text).toContain('usuarios objetivo');
76
+ expect(translated.options[0]).toBe('Desarrolladores');
77
+ expect(translated.options[1]).toContain('Usuarios finales');
78
+ expect(translated.options[2]).toBe('Empresas');
79
+ });
80
+ it('should use fallback translation for key features question', () => {
81
+ const question = {
82
+ id: 'features-1',
83
+ text: 'What are the key features?',
84
+ type: 'exclusive',
85
+ section: 'product',
86
+ options: ['Feature 1', 'Feature 2'],
87
+ };
88
+ const translated = translateQuestion(question, 'es');
89
+ expect(translated.text).toContain('características principales');
90
+ });
91
+ it('should use fallback translation for programming language question', () => {
92
+ const question = {
93
+ id: 'tech-1',
94
+ text: 'What programming language will you use?',
95
+ type: 'exclusive',
96
+ section: 'tech-stack',
97
+ options: ['TypeScript', 'Python', 'JavaScript'],
98
+ };
99
+ const translated = translateQuestion(question, 'es');
100
+ expect(translated.text).toContain('lenguajes de programación');
101
+ expect(translated.options[0]).toBe('TypeScript');
102
+ expect(translated.options[1]).toBe('Python');
103
+ expect(translated.options[2]).toBe('JavaScript');
104
+ });
105
+ it('should use fallback translation for framework question', () => {
106
+ const question = {
107
+ id: 'tech-2',
108
+ text: 'What framework will you use?',
109
+ type: 'exclusive',
110
+ section: 'tech-stack',
111
+ options: ['React', 'Vue', 'Angular'],
112
+ };
113
+ const translated = translateQuestion(question, 'es');
114
+ expect(translated.text).toContain('frameworks');
115
+ });
116
+ it('should return original text if no translation matches', () => {
117
+ const question = {
118
+ id: 'custom-question',
119
+ text: 'Some custom question text without matches',
120
+ type: 'exclusive',
121
+ section: 'product',
122
+ options: ['Option A', 'Option B'],
123
+ };
124
+ const translated = translateQuestion(question, 'es');
125
+ expect(translated.text).toBe(question.text);
126
+ expect(translated.options).toEqual(question.options);
127
+ });
128
+ });
129
+ describe('translateInstruction', () => {
130
+ it('should translate menu instructions to Spanish', () => {
131
+ const key = 'menu.select_option';
132
+ const translated = translateInstruction(key, 'es');
133
+ expect(translated).toContain('Selecciona');
134
+ expect(translated).toContain('opción');
135
+ });
136
+ it('should translate menu instructions to English', () => {
137
+ const key = 'menu.select_option';
138
+ const translated = translateInstruction(key, 'en');
139
+ expect(translated).toContain('Select');
140
+ expect(translated).toContain('option');
141
+ });
142
+ it('should handle approval instructions in Spanish', () => {
143
+ const key = 'approval.approve';
144
+ const translated = translateInstruction(key, 'es');
145
+ expect(translated.toLowerCase()).toContain('aprobar');
146
+ });
147
+ it('should handle approval instructions in English', () => {
148
+ const key = 'approval.approve';
149
+ const translated = translateInstruction(key, 'en');
150
+ expect(translated.toLowerCase()).toContain('approve');
151
+ });
152
+ it('should return key if translation not found', () => {
153
+ const key = 'unknown.key';
154
+ const translated = translateInstruction(key, 'es');
155
+ expect(translated).toBe(key);
156
+ });
157
+ });
158
+ });
@@ -15,6 +15,7 @@ describe("synergyState", () => {
15
15
  vi.mocked(detectCDDConfig).mockReturnValue({
16
16
  hasCDDInOpenCode: false,
17
17
  hasCDDInOMO: false,
18
+ hasCDDInSlim: true,
18
19
  synergyActive: true,
19
20
  synergyFramework: 'oh-my-opencode-slim',
20
21
  slimAgents: ['explorer', 'librarian', 'oracle', 'designer'],
@@ -28,6 +29,7 @@ describe("synergyState", () => {
28
29
  vi.mocked(detectCDDConfig).mockReturnValue({
29
30
  hasCDDInOpenCode: false,
30
31
  hasCDDInOMO: false,
32
+ hasCDDInSlim: true,
31
33
  synergyActive: true,
32
34
  synergyFramework: 'oh-my-opencode-slim',
33
35
  slimAgents: ['explorer', 'librarian'],
@@ -42,6 +44,7 @@ describe("synergyState", () => {
42
44
  .mockReturnValueOnce({
43
45
  hasCDDInOpenCode: false,
44
46
  hasCDDInOMO: false,
47
+ hasCDDInSlim: true,
45
48
  synergyActive: true,
46
49
  synergyFramework: 'oh-my-opencode-slim',
47
50
  slimAgents: ['explorer'],
@@ -49,6 +52,7 @@ describe("synergyState", () => {
49
52
  .mockReturnValueOnce({
50
53
  hasCDDInOpenCode: false,
51
54
  hasCDDInOMO: true,
55
+ hasCDDInSlim: false,
52
56
  synergyActive: true,
53
57
  synergyFramework: 'oh-my-opencode',
54
58
  cddModel: 'model-1',
@@ -64,13 +68,15 @@ describe("synergyState", () => {
64
68
  .mockReturnValueOnce({
65
69
  hasCDDInOpenCode: false,
66
70
  hasCDDInOMO: false,
71
+ hasCDDInSlim: true,
67
72
  synergyActive: true,
68
73
  synergyFramework: 'oh-my-opencode-slim',
69
74
  slimAgents: ['explorer'],
70
75
  })
71
76
  .mockReturnValueOnce({
72
77
  hasCDDInOpenCode: false,
73
- hasCDDInOMO: false,
78
+ hasCDDInOMO: true,
79
+ hasCDDInSlim: false,
74
80
  synergyActive: true,
75
81
  synergyFramework: 'oh-my-opencode',
76
82
  cddModel: 'model-changed',
@@ -85,6 +91,7 @@ describe("synergyState", () => {
85
91
  vi.mocked(detectCDDConfig).mockReturnValue({
86
92
  hasCDDInOpenCode: true,
87
93
  hasCDDInOMO: false,
94
+ hasCDDInSlim: false,
88
95
  synergyActive: false,
89
96
  synergyFramework: 'none',
90
97
  cddModel: 'model-1',
@@ -99,13 +106,15 @@ describe("synergyState", () => {
99
106
  .mockReturnValueOnce({
100
107
  hasCDDInOpenCode: false,
101
108
  hasCDDInOMO: false,
109
+ hasCDDInSlim: true,
102
110
  synergyActive: true,
103
111
  synergyFramework: 'oh-my-opencode-slim',
104
112
  slimAgents: ['explorer'],
105
113
  })
106
114
  .mockReturnValueOnce({
107
115
  hasCDDInOpenCode: false,
108
- hasCDDInOMO: false,
116
+ hasCDDInOMO: true,
117
+ hasCDDInSlim: false,
109
118
  synergyActive: true,
110
119
  synergyFramework: 'oh-my-opencode',
111
120
  cddModel: 'model-1',
@@ -124,6 +133,7 @@ describe("synergyState", () => {
124
133
  vi.mocked(detectCDDConfig).mockReturnValue({
125
134
  hasCDDInOpenCode: false,
126
135
  hasCDDInOMO: false,
136
+ hasCDDInSlim: false,
127
137
  synergyActive: false,
128
138
  synergyFramework: 'none',
129
139
  });
@@ -136,6 +146,7 @@ describe("synergyState", () => {
136
146
  vi.mocked(detectCDDConfig).mockReturnValue({
137
147
  hasCDDInOpenCode: false,
138
148
  hasCDDInOMO: false,
149
+ hasCDDInSlim: true,
139
150
  synergyActive: true,
140
151
  synergyFramework: 'oh-my-opencode-slim',
141
152
  slimAgents: ['explorer', 'oracle'],
@@ -147,6 +158,7 @@ describe("synergyState", () => {
147
158
  vi.mocked(detectCDDConfig).mockReturnValue({
148
159
  hasCDDInOpenCode: false,
149
160
  hasCDDInOMO: true,
161
+ hasCDDInSlim: false,
150
162
  synergyActive: true,
151
163
  synergyFramework: 'oh-my-opencode',
152
164
  cddModel: 'model-1',
@@ -163,13 +175,15 @@ describe("synergyState", () => {
163
175
  .mockReturnValueOnce({
164
176
  hasCDDInOpenCode: false,
165
177
  hasCDDInOMO: false,
178
+ hasCDDInSlim: true,
166
179
  synergyActive: true,
167
180
  synergyFramework: 'oh-my-opencode-slim',
168
181
  slimAgents: ['explorer'],
169
182
  })
170
183
  .mockReturnValueOnce({
171
184
  hasCDDInOpenCode: false,
172
- hasCDDInOMO: false,
185
+ hasCDDInOMO: true,
186
+ hasCDDInSlim: false,
173
187
  synergyActive: true,
174
188
  synergyFramework: 'oh-my-opencode',
175
189
  cddModel: 'changed',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-conductor-cdd-plugin",
3
- "version": "1.0.0-beta.19",
3
+ "version": "1.0.0-beta.21",
4
4
  "description": "Context-Driven Development (CDD) plugin for OpenCode - Transform your AI coding workflow with structured specifications, plans, and implementation tracking",
5
5
  "type": "module",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "dependencies": {
45
- "@opencode-ai/plugin": "1.0.223",
45
+ "@opencode-ai/plugin": "^1.1.27",
46
46
  "smol-toml": "^1.6.0"
47
47
  },
48
48
  "devDependencies": {
@@ -53,6 +53,7 @@
53
53
  "@semantic-release/npm": "^12.0.1",
54
54
  "@semantic-release/release-notes-generator": "^14.0.0",
55
55
  "@types/node": "^20.0.0",
56
+ "@vitest/coverage-v8": "^4.0.17",
56
57
  "semantic-release": "^24.2.1",
57
58
  "typescript": "^5.0.0",
58
59
  "vitest": "^4.0.16"