hyouji 0.0.8 → 0.0.9

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.
Files changed (3) hide show
  1. package/README.md +94 -8
  2. package/dist/index.js +171 -50
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -18,7 +18,6 @@ https://levelup.gitconnected.com/create-github-labels-from-terminal-158d4868fab
18
18
 
19
19
  https://github.com/user-attachments/assets/739f185a-1bd0-411b-8947-dd4600c452c8
20
20
 
21
-
22
21
  ### Labels API
23
22
 
24
23
  https://docs.github.com/en/rest/reference/issues#labels
@@ -60,7 +59,7 @@ This tool provides the following functionality:
60
59
  2. Create multiple labels on a specific repo
61
60
  3. Delete a single label from a specific repo
62
61
  4. Delete all labels from a specific repo
63
- 5. Import labels from JSON file
62
+ 5. Import labels from JSON or YAML file
64
63
  6. **Display your saved settings** - View your stored GitHub configuration
65
64
  7. **Persistent configuration** - Save your GitHub token and username for future use
66
65
 
@@ -77,9 +76,7 @@ hyouji
77
76
  On your first run, you'll be prompted to enter:
78
77
 
79
78
  - **GitHub Personal Token** - Generate one [here](https://github.com/settings/tokens) with `repo` scope
80
- <img width="792" height="564" alt="github_token" src="https://github.com/user-attachments/assets/e460738f-833a-4158-a8ba-61752beaad72" />
81
-
82
-
79
+ <img width="792" height="564" alt="github_token" src="https://github.com/user-attachments/assets/e460738f-833a-4158-a8ba-61752beaad72" />
83
80
  - **GitHub Username** - Your GitHub account name
84
81
 
85
82
  These credentials will be securely saved and reused for future sessions.
@@ -90,9 +87,11 @@ These credentials will be securely saved and reused for future sessions.
90
87
  2. **Create multiple labels on a specific repo**
91
88
  3. **Delete a single label from a specific repo**
92
89
  4. **Delete all labels from a specific repo**
93
- 5. **Import labels from JSON file**
94
- 6. **Display your settings** - View your saved configuration
95
- 7. **Exit**
90
+ 5. **Import labels from JSON or YAML file**
91
+ 6. **Generate sample JSON** - Create a sample JSON file with predefined labels
92
+ 7. **Generate sample YAML** - Create a sample YAML file with predefined labels
93
+ 8. **Display your settings** - View your saved configuration
94
+ 9. **Exit**
96
95
 
97
96
  ### Settings Management
98
97
 
@@ -200,6 +199,93 @@ npm start
200
199
 
201
200
  You can use `pnpm`, `yarn` or `bun`.
202
201
 
202
+ ### File Import Support
203
+
204
+ The "Import labels from JSON or YAML file" option allows you to import multiple labels from external files. Both JSON and YAML formats are supported.
205
+
206
+ #### Supported File Formats
207
+
208
+ - **JSON files** (`.json` extension)
209
+ - **YAML files** (`.yaml` or `.yml` extension)
210
+
211
+ #### Label Structure
212
+
213
+ Both formats support the same label structure:
214
+
215
+ - `name` (required): The label name
216
+ - `color` (optional): Hex color code without the `#` symbol
217
+ - `description` (optional): Label description
218
+
219
+ #### JSON Example
220
+
221
+ ```json
222
+ [
223
+ {
224
+ "name": "bug",
225
+ "color": "d73a4a",
226
+ "description": "Something isn't working"
227
+ },
228
+ {
229
+ "name": "enhancement",
230
+ "color": "a2eeef",
231
+ "description": "New feature or request"
232
+ },
233
+ {
234
+ "name": "documentation",
235
+ "color": "0075ca",
236
+ "description": "Improvements or additions to documentation"
237
+ }
238
+ ]
239
+ ```
240
+
241
+ #### YAML Example
242
+
243
+ ```yaml
244
+ # Sample YAML file for importing GitHub labels
245
+ - name: 'bug'
246
+ color: 'd73a4a'
247
+ description: "Something isn't working"
248
+
249
+ - name: 'enhancement'
250
+ color: 'a2eeef'
251
+ description: 'New feature or request'
252
+
253
+ - name: 'documentation'
254
+ color: '0075ca'
255
+ description: 'Improvements or additions to documentation'
256
+
257
+ # Labels with minimal configuration (name only)
258
+ - name: 'good first issue'
259
+ color: '7057ff'
260
+ description: 'Good for newcomers'
261
+
262
+ # Labels without description (optional field)
263
+ - name: 'wontfix'
264
+ color: 'ffffff'
265
+
266
+ # Labels without color (will use GitHub default)
267
+ - name: 'question'
268
+ description: 'Further information is requested'
269
+ ```
270
+
271
+ #### Sample Files
272
+
273
+ You can find complete example files in the `examples/` directory:
274
+
275
+ - `examples/labels.json` - Basic label examples in JSON format
276
+ - `examples/labels.yaml` - Basic label examples in YAML format
277
+ - `examples/project-labels.json` - Project management labels in JSON format
278
+ - `examples/project-labels.yaml` - Project management labels in YAML format
279
+
280
+ #### Generate Sample Files
281
+
282
+ The tool can generate sample files for you:
283
+
284
+ - **Generate sample JSON** - Creates `hyouji.json` with predefined labels
285
+ - **Generate sample YAML** - Creates `hyouji.yaml` with predefined labels
286
+
287
+ Both generated files contain the same predefined labels and can be used as starting points for your own label configurations.
288
+
203
289
  ### Predefined Labels
204
290
 
205
291
  The "Create multiple labels" option uses predefined labels from `src/constant.ts`. These include common labels for project management:
package/dist/index.js CHANGED
@@ -4,9 +4,12 @@ import { renderFilled } from "oh-my-logo";
4
4
  import * as fs from "fs";
5
5
  import { promises, existsSync } from "fs";
6
6
  import { homedir } from "os";
7
+ import * as path from "path";
7
8
  import { join, dirname } from "path";
8
9
  import { createHash, randomBytes, createCipheriv, createDecipheriv } from "crypto";
9
10
  import prompts from "prompts";
11
+ import * as yaml from "js-yaml";
12
+ import yaml__default from "js-yaml";
10
13
  import { Octokit } from "@octokit/core";
11
14
  import { exec } from "child_process";
12
15
  import { promisify } from "util";
@@ -49,10 +52,10 @@ const deleteLabel$1 = {
49
52
  name: "name",
50
53
  message: "Please type label name you want to delete"
51
54
  };
52
- const jsonFilePath = {
55
+ const labelFilePath = {
53
56
  type: "text",
54
57
  name: "filePath",
55
- message: "Please type the path to your JSON file"
58
+ message: "Please type the path to your JSON or YAML file"
56
59
  };
57
60
  const actionSelector = {
58
61
  type: "multiselect",
@@ -63,10 +66,11 @@ const actionSelector = {
63
66
  { title: "create multiple labels", value: 1 },
64
67
  { title: "delete a label", value: 2 },
65
68
  { title: "delete all labels", value: 3 },
66
- { title: "import JSON", value: 4 },
69
+ { title: "import labels from JSON or YAML", value: 4 },
67
70
  { title: "Generate sample JSON", value: 5 },
68
- { title: "Display your settings", value: 6 },
69
- { title: "exit", value: 7 }
71
+ { title: "Generate sample YAML", value: 6 },
72
+ { title: "Display your settings", value: 7 },
73
+ { title: "exit", value: 8 }
70
74
  ]
71
75
  };
72
76
  const holdToken = {
@@ -265,7 +269,7 @@ Thank you!
265
269
  };
266
270
  const extraGuideText = `If you don't see action selector, please hit space key.`;
267
271
  const linkToPersonalToken = "https://github.com/settings/tokens";
268
- const log$3 = console.log;
272
+ const log$4 = console.log;
269
273
  const createLabel = async (configs2, label) => {
270
274
  const resp = await configs2.octokit.request(
271
275
  "POST /repos/{owner}/{repo}/labels",
@@ -280,16 +284,16 @@ const createLabel = async (configs2, label) => {
280
284
  const status = resp.status;
281
285
  switch (status) {
282
286
  case 201:
283
- log$3(chalk.green(`${resp.status}: Created ${label.name}`));
287
+ log$4(chalk.green(`${resp.status}: Created ${label.name}`));
284
288
  break;
285
289
  case 404:
286
- log$3(chalk.red(`${resp.status}: Resource not found`));
290
+ log$4(chalk.red(`${resp.status}: Resource not found`));
287
291
  break;
288
292
  case 422:
289
- log$3(chalk.red(`${resp.status}: Validation failed`));
293
+ log$4(chalk.red(`${resp.status}: Validation failed`));
290
294
  break;
291
295
  default:
292
- log$3(chalk.yellow(`${resp.status}: Something wrong`));
296
+ log$4(chalk.yellow(`${resp.status}: Something wrong`));
293
297
  break;
294
298
  }
295
299
  };
@@ -297,8 +301,8 @@ const createLabels = async (configs2) => {
297
301
  labels.forEach(async (label) => {
298
302
  createLabel(configs2, label);
299
303
  });
300
- log$3("Created all labels");
301
- log$3(chalk.bgBlueBright(extraGuideText));
304
+ log$4("Created all labels");
305
+ log$4(chalk.bgBlueBright(extraGuideText));
302
306
  };
303
307
  const deleteLabel = async (configs2, labelNames) => {
304
308
  for (const labelName of labelNames) {
@@ -312,15 +316,15 @@ const deleteLabel = async (configs2, labelNames) => {
312
316
  }
313
317
  );
314
318
  if (resp.status === 204) {
315
- log$3(chalk.green(`${resp.status}: Deleted ${labelName}`));
319
+ log$4(chalk.green(`${resp.status}: Deleted ${labelName}`));
316
320
  } else {
317
- log$3(chalk.yellow(`${resp.status}: Something wrong with ${labelName}`));
321
+ log$4(chalk.yellow(`${resp.status}: Something wrong with ${labelName}`));
318
322
  }
319
323
  } catch (error) {
320
324
  if (error && typeof error === "object" && "status" in error && error.status === 404) {
321
- log$3(chalk.red(`404: Label "${labelName}" not found`));
325
+ log$4(chalk.red(`404: Label "${labelName}" not found`));
322
326
  } else {
323
- log$3(
327
+ log$4(
324
328
  chalk.red(
325
329
  `Error deleting label "${labelName}": ${error instanceof Error ? error.message : "Unknown error"}`
326
330
  )
@@ -341,17 +345,17 @@ const getLabels = async (configs2) => {
341
345
  const names = await resp.data.map((label) => label.name);
342
346
  return names;
343
347
  } else {
344
- log$3(chalk.red("something wrong"));
348
+ log$4(chalk.red("something wrong"));
345
349
  return [];
346
350
  }
347
351
  };
348
352
  const deleteLabels = async (configs2) => {
349
353
  const names = await getLabels(configs2);
350
354
  if (names.length === 0) {
351
- log$3(chalk.yellow("No labels found to delete"));
355
+ log$4(chalk.yellow("No labels found to delete"));
352
356
  return;
353
357
  }
354
- log$3(chalk.blue(`Deleting ${names.length} labels...`));
358
+ log$4(chalk.blue(`Deleting ${names.length} labels...`));
355
359
  for (const name of names) {
356
360
  try {
357
361
  const resp = await configs2.octokit.request(
@@ -363,15 +367,15 @@ const deleteLabels = async (configs2) => {
363
367
  }
364
368
  );
365
369
  if (resp.status === 204) {
366
- log$3(chalk.green(`${resp.status}: Deleted ${name}`));
370
+ log$4(chalk.green(`${resp.status}: Deleted ${name}`));
367
371
  } else {
368
- log$3(chalk.yellow(`${resp.status}: Something wrong with ${name}`));
372
+ log$4(chalk.yellow(`${resp.status}: Something wrong with ${name}`));
369
373
  }
370
374
  } catch (error) {
371
375
  if (error && typeof error === "object" && "status" in error && error.status === 404) {
372
- log$3(chalk.red(`404: Label "${name}" not found`));
376
+ log$4(chalk.red(`404: Label "${name}" not found`));
373
377
  } else {
374
- log$3(
378
+ log$4(
375
379
  chalk.red(
376
380
  `Error deleting label "${name}": ${error instanceof Error ? error.message : "Unknown error"}`
377
381
  )
@@ -379,8 +383,8 @@ const deleteLabels = async (configs2) => {
379
383
  }
380
384
  }
381
385
  }
382
- log$3(chalk.blue("Finished deleting labels"));
383
- log$3(chalk.bgBlueBright(extraGuideText));
386
+ log$4(chalk.blue("Finished deleting labels"));
387
+ log$4(chalk.bgBlueBright(extraGuideText));
384
388
  };
385
389
  const _CryptoUtils = class _CryptoUtils {
386
390
  /**
@@ -930,9 +934,9 @@ class ConfigManager {
930
934
  /**
931
935
  * Check if file exists
932
936
  */
933
- async fileExists(path) {
937
+ async fileExists(path2) {
934
938
  try {
935
- await promises.access(path);
939
+ await promises.access(path2);
936
940
  return true;
937
941
  } catch {
938
942
  return false;
@@ -974,14 +978,14 @@ const getConfirmation = async () => {
974
978
  const response = await prompts(holdToken);
975
979
  return response.value;
976
980
  };
977
- const log$2 = console.log;
981
+ const log$3 = console.log;
978
982
  const generateSampleJson = async () => {
979
983
  try {
980
984
  const outputPath = "./hyouji.json";
981
985
  const jsonContent = JSON.stringify(sampleData, null, 2);
982
- log$2(chalk.blue("Generating sample JSON file..."));
986
+ log$3(chalk.blue("Generating sample JSON file..."));
983
987
  fs.writeFileSync(outputPath, jsonContent, "utf8");
984
- log$2(
988
+ log$3(
985
989
  chalk.green(
986
990
  "✅ Sample JSON file generated successfully at ./hyouji.json"
987
991
  )
@@ -990,30 +994,30 @@ const generateSampleJson = async () => {
990
994
  if (error instanceof Error) {
991
995
  const nodeError = error;
992
996
  if (nodeError.code === "EACCES") {
993
- log$2(
997
+ log$3(
994
998
  chalk.red(
995
999
  "❌ Error generating sample JSON file: Permission denied. Please check write permissions for the current directory."
996
1000
  )
997
1001
  );
998
1002
  } else if (nodeError.code === "ENOSPC") {
999
- log$2(
1003
+ log$3(
1000
1004
  chalk.red(
1001
1005
  "❌ Error generating sample JSON file: Insufficient disk space."
1002
1006
  )
1003
1007
  );
1004
1008
  } else if (nodeError.code === "EROFS") {
1005
- log$2(
1009
+ log$3(
1006
1010
  chalk.red(
1007
1011
  "❌ Error generating sample JSON file: Read-only file system."
1008
1012
  )
1009
1013
  );
1010
1014
  } else {
1011
- log$2(
1015
+ log$3(
1012
1016
  chalk.red(`❌ Error generating sample JSON file: ${error.message}`)
1013
1017
  );
1014
1018
  }
1015
1019
  } else {
1016
- log$2(
1020
+ log$3(
1017
1021
  chalk.red(
1018
1022
  "❌ An unexpected error occurred while generating the sample JSON file"
1019
1023
  )
@@ -1021,19 +1025,123 @@ const generateSampleJson = async () => {
1021
1025
  }
1022
1026
  }
1023
1027
  };
1028
+ const log$2 = console.log;
1029
+ const generateSampleYaml = async () => {
1030
+ try {
1031
+ const outputPath = "./hyouji.yaml";
1032
+ const yamlContent = yaml__default.dump(sampleData, {
1033
+ indent: 2,
1034
+ lineWidth: -1,
1035
+ // Disable line wrapping
1036
+ noRefs: true,
1037
+ // Disable references
1038
+ quotingType: '"',
1039
+ // Use double quotes for strings
1040
+ forceQuotes: false
1041
+ // Only quote when necessary
1042
+ });
1043
+ log$2(chalk.blue("Generating sample YAML file..."));
1044
+ fs.writeFileSync(outputPath, yamlContent, "utf8");
1045
+ log$2(
1046
+ chalk.green(
1047
+ "✅ Sample YAML file generated successfully at ./hyouji.yaml"
1048
+ )
1049
+ );
1050
+ } catch (error) {
1051
+ if (error instanceof Error) {
1052
+ const nodeError = error;
1053
+ if (nodeError.code === "EACCES") {
1054
+ log$2(
1055
+ chalk.red(
1056
+ "❌ Error generating sample YAML file: Permission denied. Please check write permissions for the current directory."
1057
+ )
1058
+ );
1059
+ } else if (nodeError.code === "ENOSPC") {
1060
+ log$2(
1061
+ chalk.red(
1062
+ "❌ Error generating sample YAML file: Insufficient disk space."
1063
+ )
1064
+ );
1065
+ } else if (nodeError.code === "EROFS") {
1066
+ log$2(
1067
+ chalk.red(
1068
+ "❌ Error generating sample YAML file: Read-only file system."
1069
+ )
1070
+ );
1071
+ } else {
1072
+ log$2(
1073
+ chalk.red(`❌ Error generating sample YAML file: ${error.message}`)
1074
+ );
1075
+ }
1076
+ } else {
1077
+ log$2(
1078
+ chalk.red(
1079
+ "❌ An unexpected error occurred while generating the sample YAML file"
1080
+ )
1081
+ );
1082
+ }
1083
+ }
1084
+ };
1085
+ const detectFileFormat = (filePath) => {
1086
+ const extension = path.extname(filePath).toLowerCase();
1087
+ switch (extension) {
1088
+ case ".json":
1089
+ return "json";
1090
+ case ".yaml":
1091
+ case ".yml":
1092
+ return "yaml";
1093
+ default:
1094
+ return null;
1095
+ }
1096
+ };
1097
+ const parseJsonContent = (content) => {
1098
+ return JSON.parse(content);
1099
+ };
1100
+ const parseYamlContent = (content) => {
1101
+ try {
1102
+ return yaml.load(content);
1103
+ } catch (error) {
1104
+ if (error instanceof yaml.YAMLException) {
1105
+ throw new Error(`YAMLException: ${error.message}`);
1106
+ }
1107
+ throw error;
1108
+ }
1109
+ };
1110
+ const getSupportedExtensions = () => {
1111
+ return [".json", ".yaml", ".yml"];
1112
+ };
1113
+ const formatSupportedExtensions = () => {
1114
+ return getSupportedExtensions().join(", ");
1115
+ };
1024
1116
  const log$1 = console.log;
1025
- const importLabelsFromJson = async (configs2, filePath) => {
1117
+ const importLabelsFromFile = async (configs2, filePath) => {
1026
1118
  try {
1027
1119
  if (!fs.existsSync(filePath)) {
1028
1120
  log$1(chalk.red(`Error: File not found at path: ${filePath}`));
1029
1121
  return;
1030
1122
  }
1123
+ const format = detectFileFormat(filePath);
1124
+ if (!format) {
1125
+ log$1(
1126
+ chalk.red(
1127
+ `Error: Unsupported file format. Supported formats: ${formatSupportedExtensions()}`
1128
+ )
1129
+ );
1130
+ return;
1131
+ }
1031
1132
  const fileContent = fs.readFileSync(filePath, "utf8");
1032
- let jsonData;
1133
+ let parsedData;
1033
1134
  try {
1034
- jsonData = JSON.parse(fileContent);
1135
+ if (format === "json") {
1136
+ parsedData = parseJsonContent(fileContent);
1137
+ } else if (format === "yaml") {
1138
+ parsedData = parseYamlContent(fileContent);
1139
+ }
1035
1140
  } catch (parseError) {
1036
- log$1(chalk.red(`Error: Invalid JSON syntax in file: ${filePath}`));
1141
+ const formatName = format.toUpperCase();
1142
+ log$1(
1143
+ chalk.red(`Error: Invalid ${formatName} syntax in file: ${filePath}`)
1144
+ );
1037
1145
  log$1(
1038
1146
  chalk.red(
1039
1147
  `Parse error: ${parseError instanceof Error ? parseError.message : "Unknown error"}`
@@ -1041,13 +1149,13 @@ const importLabelsFromJson = async (configs2, filePath) => {
1041
1149
  );
1042
1150
  return;
1043
1151
  }
1044
- if (!Array.isArray(jsonData)) {
1045
- log$1(chalk.red("Error: JSON file must contain an array of label objects"));
1152
+ if (!Array.isArray(parsedData)) {
1153
+ log$1(chalk.red("Error: File must contain an array of label objects"));
1046
1154
  return;
1047
1155
  }
1048
1156
  const validLabels = [];
1049
- for (let i = 0; i < jsonData.length; i++) {
1050
- const item = jsonData[i];
1157
+ for (let i = 0; i < parsedData.length; i++) {
1158
+ const item = parsedData[i];
1051
1159
  if (typeof item !== "object" || item === null) {
1052
1160
  log$1(chalk.red(`Error: Item at index ${i} is not a valid object`));
1053
1161
  continue;
@@ -1128,7 +1236,7 @@ const importLabelsFromJson = async (configs2, filePath) => {
1128
1236
  validLabels.push(validLabel);
1129
1237
  }
1130
1238
  if (validLabels.length === 0) {
1131
- log$1(chalk.red("Error: No valid labels found in JSON file"));
1239
+ log$1(chalk.red("Error: No valid labels found in file"));
1132
1240
  return;
1133
1241
  }
1134
1242
  log$1(chalk.blue(`Starting import of ${validLabels.length} labels...`));
@@ -1485,8 +1593,8 @@ const getGitHubConfigs = async () => {
1485
1593
  detectionMethod: "manual"
1486
1594
  };
1487
1595
  };
1488
- const getJsonFilePath = async () => {
1489
- const response = await prompts(jsonFilePath);
1596
+ const getLabelFilePath = async () => {
1597
+ const response = await prompts(labelFilePath);
1490
1598
  return response.filePath;
1491
1599
  };
1492
1600
  const getNewLabel = async () => {
@@ -1665,16 +1773,16 @@ const main = async () => {
1665
1773
  }
1666
1774
  case 4: {
1667
1775
  try {
1668
- const filePath = await getJsonFilePath();
1776
+ const filePath = await getLabelFilePath();
1669
1777
  if (filePath) {
1670
- await importLabelsFromJson(configs, filePath);
1778
+ await importLabelsFromFile(configs, filePath);
1671
1779
  } else {
1672
1780
  log(chalk.yellow("No file path provided. Returning to main menu."));
1673
1781
  }
1674
1782
  } catch (error) {
1675
1783
  log(
1676
1784
  chalk.red(
1677
- `Error during JSON import: ${error instanceof Error ? error.message : "Unknown error"}`
1785
+ `Error during label import: ${error instanceof Error ? error.message : "Unknown error"}`
1678
1786
  )
1679
1787
  );
1680
1788
  }
@@ -1695,11 +1803,24 @@ const main = async () => {
1695
1803
  break;
1696
1804
  }
1697
1805
  case 6: {
1698
- await displaySettings();
1806
+ try {
1807
+ await generateSampleYaml();
1808
+ } catch (error) {
1809
+ log(
1810
+ chalk.red(
1811
+ `Error generating sample YAML: ${error instanceof Error ? error.message : "Unknown error"}`
1812
+ )
1813
+ );
1814
+ }
1699
1815
  firstStart = firstStart && false;
1700
1816
  break;
1701
1817
  }
1702
1818
  case 7: {
1819
+ await displaySettings();
1820
+ firstStart = firstStart && false;
1821
+ break;
1822
+ }
1823
+ case 8: {
1703
1824
  console.log("exit");
1704
1825
  process.exit(0);
1705
1826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyouji",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Hyouji (表示) — A command-line tool for organizing and displaying GitHub labels with clarity and harmony.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -20,7 +20,10 @@
20
20
  "hyouji",
21
21
  "cli",
22
22
  "表示",
23
- "Hyouji(表示)"
23
+ "Hyouji(表示)",
24
+ "JSON",
25
+ "yaml",
26
+ "yml"
24
27
  ],
25
28
  "scripts": {
26
29
  "start": "node dist/index.js",
@@ -60,6 +63,7 @@
60
63
  "prompts": "^2.4.2"
61
64
  },
62
65
  "devDependencies": {
66
+ "@types/js-yaml": "^4.0.9",
63
67
  "@types/node": "^24.0.13",
64
68
  "@vitest/coverage-v8": "^3.2.4",
65
69
  "@vitest/ui": "^3.2.4",