@oinone/cli 7.2.3 → 7.2.5

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/bin/cli.js +106 -44
  2. package/bin/i18n.js +134 -0
  3. package/package.json +5 -2
package/bin/cli.js CHANGED
@@ -11,12 +11,16 @@ const os = require('os');
11
11
  const AdmZip = require('adm-zip');
12
12
  const cliProgress = require('cli-progress');
13
13
  const packageJson = require('../package.json');
14
+ const { t } = require('./i18n');
14
15
 
15
16
  const downloadAndExtractZip = (url, targetDir) => {
16
17
  return new Promise((resolve, reject) => {
17
18
  const req = https.request(url, { method: 'HEAD' }, (headRes) => {
19
+ // 必须调用 resume() 消费响应数据,否则 Node 19+ 默认开启 keepAlive 会导致 socket 保持连接,进程卡住 60 秒
20
+ headRes.resume();
21
+
18
22
  if (headRes.statusCode !== 200) {
19
- reject(new Error(`Failed to fetch file info: ${headRes.statusCode}`));
23
+ reject(new Error(t('fetchFileInfoFailed', { status: headRes.statusCode })));
20
24
  return;
21
25
  }
22
26
 
@@ -33,7 +37,7 @@ const downloadAndExtractZip = (url, targetDir) => {
33
37
  const cachedZipPath = path.join(cacheDir, `static-resources-${etag}.zip`);
34
38
 
35
39
  if (fs.existsSync(cachedZipPath)) {
36
- console.log(chalk.cyan('⚡ Zap! Using cached static resources...'));
40
+ console.log(chalk.cyan(t('usingCached')));
37
41
  try {
38
42
  const zip = new AdmZip(cachedZipPath);
39
43
  zip.extractAllTo(targetDir, true);
@@ -49,7 +53,8 @@ const downloadAndExtractZip = (url, targetDir) => {
49
53
 
50
54
  https.get(url, (response) => {
51
55
  if (response.statusCode !== 200) {
52
- reject(new Error(`Failed to download: ${response.statusCode}`));
56
+ response.resume(); // 消费响应数据释放 socket
57
+ reject(new Error(t('downloadFailed', { status: response.statusCode })));
53
58
  return;
54
59
  }
55
60
 
@@ -70,7 +75,7 @@ const downloadAndExtractZip = (url, targetDir) => {
70
75
  let startTime = Date.now();
71
76
 
72
77
  const progressBar = new cliProgress.SingleBar({
73
- format: chalk.cyan('🚀 Fetching awesomeness ') + `[{bar}] {percentage}% | ⏳ {eta}s left | 📦 {value}/{total} ${unit} | 💨 {speed} {speedUnit}/s`,
78
+ format: chalk.cyan(t('fetchingAwesomeness')) + `[{bar}] {percentage}% | ⏳ {eta}s ${t('progressLeft')} | 📦 {value}/{total} ${unit} | 💨 {speed} {speedUnit}/s`,
74
79
  barCompleteChar: '\u2588',
75
80
  barIncompleteChar: '\u2591',
76
81
  hideCursor: true,
@@ -135,14 +140,14 @@ const downloadAndExtractZip = (url, targetDir) => {
135
140
  };
136
141
 
137
142
  program
138
- .version(packageJson.version)
139
- .description('Oinone Frontend Template CLI')
140
- .helpOption('-h, --help', 'display help for command');
143
+ .version(packageJson.version, '-V, --version', t('versionOption'))
144
+ .description(t('description'))
145
+ .helpOption('-h, --help', t('helpOption'));
141
146
 
142
147
  // Cache Command
143
148
  const cacheCmd = program
144
149
  .command('cache')
145
- .description('Manage the local static resources cache')
150
+ .description(t('cacheCmdDesc'))
146
151
  .action(() => {
147
152
  // If just "cache" is typed without subcommands, show cache help
148
153
  cacheCmd.help();
@@ -150,29 +155,29 @@ const cacheCmd = program
150
155
 
151
156
  cacheCmd
152
157
  .command('clean')
153
- .description('Clean the local static resources cache')
158
+ .description(t('cacheCleanDesc'))
154
159
  .action(() => {
155
160
  const cacheDir = path.join(os.tmpdir(), 'oinone-cli-cache');
156
161
  if (fs.existsSync(cacheDir)) {
157
162
  fs.removeSync(cacheDir);
158
- console.log(chalk.green(`✨ Whoosh! Cache swept clean at ${cacheDir}`));
163
+ console.log(chalk.green(t('cacheCleaned', { dir: cacheDir })));
159
164
  } else {
160
- console.log(chalk.yellow(`🤷 Cache directory already vanished (or never existed): ${cacheDir}`));
165
+ console.log(chalk.yellow(t('cacheAlreadyEmpty', { dir: cacheDir })));
161
166
  }
162
167
  });
163
168
 
164
169
  cacheCmd
165
170
  .command('info')
166
- .description('Print cache directory information')
171
+ .description(t('cacheInfoDesc'))
167
172
  .action(async () => {
168
173
  const cacheDir = path.join(os.tmpdir(), 'oinone-cli-cache');
169
- console.log(chalk.cyan('📂 Cache Directory: ') + cacheDir);
174
+ console.log(chalk.cyan(t('cacheDir')) + cacheDir);
170
175
 
171
176
  if (fs.existsSync(cacheDir)) {
172
177
  const files = await fs.readdir(cacheDir);
173
178
  let totalSize = 0;
174
179
 
175
- console.log(chalk.cyan('📦 Cached Files:'));
180
+ console.log(chalk.cyan(t('cachedFiles')));
176
181
  for (const file of files) {
177
182
  const filePath = path.join(cacheDir, file);
178
183
  const stat = await fs.stat(filePath);
@@ -195,23 +200,47 @@ cacheCmd
195
200
  totalDisplaySize = `${Math.round(totalSize / 1024)} KB`;
196
201
  }
197
202
 
198
- console.log(chalk.cyan(`\n📊 Total Cache Size: `) + totalDisplaySize);
203
+ console.log(chalk.cyan(t('totalCacheSize')) + totalDisplaySize);
199
204
  } else {
200
- console.log(chalk.yellow('🍃 Cache is currently as empty as space.'));
205
+ console.log(chalk.yellow(t('cacheEmpty')));
201
206
  }
202
207
  });
203
208
 
204
209
  // Create Command (Default Behavior)
205
210
  program
206
- .argument('[project-name]', 'Name of the project to create silently')
207
- .option('--download-static-resource <value>', 'Download static resources (true/false)', 'true')
208
- .option('--oinone-version <value>', 'Oinone version to use (e.g. 7.2.0, 6.4.0)', '7.2.0')
209
- .option('--edition <value>', 'Oinone edition to use (community/enterprise)', 'community')
211
+ .argument('[project-name]', t('projectNameArg'))
212
+ .option('--download-static-resource <value>', t('downloadOpt'), 'true')
213
+ .option('--oinone-version <value>', t('versionOpt'), '7.2.0')
214
+ .option('--edition <value>', t('editionOpt'), 'community')
210
215
  .action(async (projectNameArg, options) => {
211
216
  // If command is 'cache', skip create logic
212
217
  if (projectNameArg === 'cache') {
213
218
  return;
214
219
  }
220
+
221
+ // Trigger version check without blocking
222
+ let checkReq = null;
223
+ const versionCheckPromise = new Promise((resolve) => {
224
+ checkReq = https.get('https://registry.npmjs.org/-/package/@oinone/cli/dist-tags', { timeout: 3000 }, (res) => {
225
+ let data = '';
226
+ res.on('data', chunk => data += chunk);
227
+ res.on('end', () => {
228
+ try {
229
+ const parsed = JSON.parse(data);
230
+ resolve(parsed.latest);
231
+ } catch (e) {
232
+ resolve(null);
233
+ }
234
+ });
235
+ });
236
+ checkReq.on('timeout', () => {
237
+ checkReq.destroy();
238
+ resolve(null);
239
+ });
240
+ checkReq.on('error', () => {
241
+ resolve(null);
242
+ });
243
+ });
215
244
 
216
245
  try {
217
246
  let prefix;
@@ -238,47 +267,47 @@ program
238
267
  {
239
268
  type: 'input',
240
269
  name: 'companyName',
241
- message: '🏢 What is your awesome company/org name? (e.g. ss):',
270
+ message: t('promptCompany'),
242
271
  default: 'ss',
243
272
  validate: (input) => {
244
- if (input.trim() === '') return 'Oops! Company name cannot be empty 🙈';
273
+ if (input.trim() === '') return t('errorCompanyEmpty');
245
274
  return true;
246
275
  },
247
276
  },
248
277
  {
249
278
  type: 'input',
250
279
  name: 'projectName',
251
- message: '🚀 Name your epic project (e.g. oms):',
280
+ message: t('promptProject'),
252
281
  default: 'oms',
253
282
  validate: (input) => {
254
- if (input.trim() === '') return 'Hold on! Project name cannot be empty 🛑';
283
+ if (input.trim() === '') return t('errorProjectEmpty');
255
284
  return true;
256
285
  },
257
286
  },
258
287
  {
259
288
  type: 'list',
260
289
  name: 'edition',
261
- message: '💎 Choose your Oinone flavor:',
290
+ message: t('promptEdition'),
262
291
  choices: [
263
- { name: '🌱 Community Edition (Default)', value: 'community' },
264
- { name: '👑 Enterprise Edition', value: 'enterprise' }
292
+ { name: t('editionCommunity'), value: 'community' },
293
+ { name: t('editionEnterprise'), value: 'enterprise' }
265
294
  ],
266
295
  default: 'community'
267
296
  },
268
297
  {
269
298
  type: 'list',
270
299
  name: 'oinoneVersion',
271
- message: '📦 Which Oinone version are we rocking today?',
300
+ message: t('promptVersion'),
272
301
  choices: [
273
- { name: '✨ 7.2.0 (Default)', value: '~7.2.0' },
274
- { name: '🕰️ 6.4.0', value: '~6.4.0' }
302
+ { name: t('version720'), value: '~7.2.0' },
303
+ { name: t('version640'), value: '~6.4.0' }
275
304
  ],
276
305
  default: '~7.2.0'
277
306
  },
278
307
  {
279
308
  type: 'confirm',
280
309
  name: 'downloadStaticResources',
281
- message: '📥 Shall we fetch the latest static resources for you? (Highly recommended! 🌟)',
310
+ message: t('promptDownload'),
282
311
  default: true
283
312
  }
284
313
  ]);
@@ -293,17 +322,17 @@ program
293
322
  const targetPath = path.resolve(process.cwd(), fullProjectName);
294
323
 
295
324
  if (fs.existsSync(targetPath)) {
296
- console.log(chalk.red(`\n🚫 Yikes! The directory ${fullProjectName} already exists. Try a different name or clear the path.\n`));
325
+ console.log(chalk.red(t('errorDirExists', { name: fullProjectName })));
297
326
  process.exit(1);
298
327
  }
299
328
 
300
- const spinner = ora('✨ Weaving magic to create your project...').start();
329
+ const spinner = ora(t('weavingMagic')).start();
301
330
 
302
331
  const versionDir = oinoneVersion.replace(/[^0-9.]/g, ''); // Extract '7.2.0' or '6.4.0'
303
332
  const templatePath = path.resolve(__dirname, `../template-${versionDir}-${edition}`);
304
333
 
305
334
  if (!fs.existsSync(templatePath)) {
306
- spinner.fail(chalk.red(`😢 Uh-oh! Template for version ${oinoneVersion} (${edition} edition) went missing in action.`));
335
+ spinner.fail(chalk.red(t('errorTemplateMissing', { version: oinoneVersion, edition })));
307
336
  process.exit(1);
308
337
  }
309
338
 
@@ -394,35 +423,68 @@ program
394
423
  }
395
424
 
396
425
  if (downloadStaticResources) {
397
- spinner.succeed(chalk.green('🎉 Boom! Project structure is ready!'));
398
- console.log(chalk.cyan('🚚 Hauling in the static resources...'));
426
+ spinner.succeed(chalk.green(t('structureReady')));
427
+ console.log(chalk.cyan(t('haulingStatic')));
399
428
  try {
400
429
  const ossDir = path.join(targetPath, 'packages', `${prefix}-boot`, 'public', 'oss');
401
430
  await fs.ensureDir(ossDir);
402
431
  await downloadAndExtractZip('https://pamirs.oss-cn-hangzhou.aliyuncs.com/oinone/installer/static-resources.zip', ossDir);
403
- console.log(chalk.green('🎯 Bullseye! Static resources downloaded and ready to roll!'));
432
+ console.log(chalk.green(t('staticDownloaded')));
404
433
  } catch (err) {
405
- console.log(chalk.yellow(`⚠️ Project is ready, but the static resources had a hiccup: ${err.message}`));
434
+ console.log(chalk.yellow(t('staticHiccup', { msg: err.message })));
406
435
  }
407
436
  } else {
408
- spinner.succeed(chalk.green('🎉 Boom! Project created successfully!'));
437
+ spinner.succeed(chalk.green(t('projectCreated')));
409
438
  const relativeOssDir = `packages/${prefix}-boot/public/oss`;
410
- console.log(chalk.yellow(`\n⚠️ You chose to skip the static resources download.`));
411
- console.log(chalk.yellow(`Don't forget to grab them manually before you fire up the project:`));
439
+ console.log(chalk.yellow(t('skipStaticWarning')));
440
+ console.log(chalk.yellow(t('manualStaticInstructions')));
412
441
  console.log(chalk.cyan(` mkdir -p ${fullProjectName}/${relativeOssDir}`));
413
442
  console.log(chalk.cyan(` curl -L https://pamirs.oss-cn-hangzhou.aliyuncs.com/oinone/installer/static-resources.zip -o static-resources.zip`));
414
443
  console.log(chalk.cyan(` unzip static-resources.zip -d ${fullProjectName}/${relativeOssDir}`));
415
444
  console.log(chalk.cyan(` rm static-resources.zip`));
416
445
  }
417
446
 
418
- console.log(`\n🚀 Let's get this party started:\n`);
447
+ console.log(t('partyStarted'));
419
448
  console.log(chalk.cyan(` cd ${fullProjectName}`));
420
449
  console.log(chalk.cyan(` pnpm install`));
421
450
  console.log(chalk.cyan(` pnpm run dev\n`));
422
- console.log(chalk.magenta(`Happy coding! 💻☕\n`));
451
+ console.log(chalk.magenta(t('happyCoding')));
452
+
453
+ try {
454
+ const latestVersion = await Promise.race([
455
+ versionCheckPromise,
456
+ new Promise(resolve => setTimeout(() => {
457
+ if (checkReq) checkReq.destroy();
458
+ resolve(null);
459
+ }, 1000))
460
+ ]);
461
+
462
+ if (latestVersion) {
463
+ const isNewerVersion = (current, latest) => {
464
+ const c = current.replace(/[^0-9.]/g, '').split('.').map(Number);
465
+ const l = latest.replace(/[^0-9.]/g, '').split('.').map(Number);
466
+ for (let i = 0; i < Math.max(c.length, l.length); i++) {
467
+ const numC = c[i] || 0;
468
+ const numL = l[i] || 0;
469
+ if (numL > numC) return true;
470
+ if (numL < numC) return false;
471
+ }
472
+ return false;
473
+ };
474
+
475
+ if (isNewerVersion(packageJson.version, latestVersion)) {
476
+ console.log(chalk.yellow(`====================================================`));
477
+ console.log(chalk.yellow(t('updateAvailable', { current: chalk.red(packageJson.version), latest: chalk.green(latestVersion) })));
478
+ console.log(chalk.yellow(t('runUpdate', { cmd: chalk.cyan('npm install -g @oinone/cli') })));
479
+ console.log(chalk.yellow(`====================================================\n`));
480
+ }
481
+ }
482
+ } catch (e) {
483
+ // ignore errors
484
+ }
423
485
 
424
486
  } catch (error) {
425
- console.error(chalk.red(`\n💥 Oops! Something broke: ${error.message}\n`));
487
+ console.error(chalk.red(t('somethingBroke', { msg: error.message })));
426
488
  process.exit(1);
427
489
  }
428
490
  });
package/bin/i18n.js ADDED
@@ -0,0 +1,134 @@
1
+ const detectLanguage = () => {
2
+ try {
3
+ const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
4
+ if (locale.toLowerCase().startsWith('zh')) {
5
+ return 'zh-CN';
6
+ }
7
+ } catch (e) {
8
+ // Ignore error
9
+ }
10
+
11
+ // Also check environment variables for language
12
+ const envLang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || '';
13
+ if (envLang.toLowerCase().startsWith('zh')) {
14
+ return 'zh-CN';
15
+ }
16
+
17
+ return 'en-US';
18
+ };
19
+
20
+ const currentLang = detectLanguage();
21
+
22
+ const locales = {
23
+ 'en-US': {
24
+ usingCached: '⚡ Zap! Using cached static resources...',
25
+ fetchFileInfoFailed: 'Failed to fetch file info: {status}',
26
+ downloadFailed: 'Failed to download: {status}',
27
+ fetchingAwesomeness: '🚀 Fetching awesomeness ',
28
+ progressLeft: 'left',
29
+ cacheCleaned: '✨ Whoosh! Cache swept clean at {dir}',
30
+ cacheAlreadyEmpty: '🤷 Cache directory already vanished (or never existed): {dir}',
31
+ cacheDir: '📂 Cache Directory: ',
32
+ cachedFiles: '📦 Cached Files:',
33
+ totalCacheSize: '\n📊 Total Cache Size: ',
34
+ cacheEmpty: '🍃 Cache is currently as empty as space.',
35
+ description: 'Oinone Frontend Template CLI',
36
+ cacheCmdDesc: 'Manage the local static resources cache',
37
+ cacheCleanDesc: 'Clean the local static resources cache',
38
+ cacheInfoDesc: 'Print cache directory information',
39
+ versionOption: 'output the version number',
40
+ helpOption: 'display help for command',
41
+ projectNameArg: 'Name of the project to create silently',
42
+ downloadOpt: 'Download static resources (true/false)',
43
+ versionOpt: 'Oinone version to use (e.g. 7.2.0, 6.4.0)',
44
+ editionOpt: 'Oinone edition to use (community/enterprise)',
45
+ promptCompany: '🏢 What is your awesome company/org name? (e.g. ss):',
46
+ errorCompanyEmpty: 'Oops! Company name cannot be empty 🙈',
47
+ promptProject: '🚀 Name your epic project (e.g. oms):',
48
+ errorProjectEmpty: 'Hold on! Project name cannot be empty 🛑',
49
+ promptEdition: '💎 Choose your Oinone flavor:',
50
+ editionCommunity: '🌱 Community Edition (Default)',
51
+ editionEnterprise: '👑 Enterprise Edition',
52
+ promptVersion: '📦 Which Oinone version are we rocking today?',
53
+ version720: '✨ 7.2.0 (Default)',
54
+ version640: '🕰️ 6.4.0',
55
+ promptDownload: '📥 Shall we fetch the latest static resources for you? (Highly recommended! 🌟)',
56
+ errorDirExists: '\n🚫 Yikes! The directory {name} already exists. Try a different name or clear the path.\n',
57
+ weavingMagic: '✨ Weaving magic to create your project...',
58
+ errorTemplateMissing: '😢 Uh-oh! Template for version {version} ({edition} edition) went missing in action.',
59
+ structureReady: '🎉 Boom! Project structure is ready!',
60
+ haulingStatic: '🚚 Hauling in the static resources...',
61
+ staticDownloaded: '🎯 Bullseye! Static resources downloaded and ready to roll!',
62
+ staticHiccup: '⚠️ Project is ready, but the static resources had a hiccup: {msg}',
63
+ projectCreated: '🎉 Boom! Project created successfully!',
64
+ skipStaticWarning: '\n⚠️ You chose to skip the static resources download.',
65
+ manualStaticInstructions: 'Don\'t forget to grab them manually before you fire up the project:',
66
+ partyStarted: '\n🚀 Let\'s get this party started:\n',
67
+ happyCoding: 'Happy coding! 💻☕\n',
68
+ updateAvailable: '📦 Update available! {current} → {latest}',
69
+ runUpdate: 'Run {cmd} to update.',
70
+ somethingBroke: '\n💥 Oops! Something broke: {msg}\n'
71
+ },
72
+ 'zh-CN': {
73
+ usingCached: '⚡ 嗖!正在使用本地缓存的静态资源...',
74
+ fetchFileInfoFailed: '获取文件信息失败: {status}',
75
+ downloadFailed: '下载失败: {status}',
76
+ fetchingAwesomeness: '🚀 正在获取静态资源 ',
77
+ progressLeft: '剩余',
78
+ cacheCleaned: '✨ 呼!缓存清理完毕,位置:{dir}',
79
+ cacheAlreadyEmpty: '🤷 缓存目录已消失(或从未存在):{dir}',
80
+ cacheDir: '📂 缓存目录: ',
81
+ cachedFiles: '📦 缓存文件:',
82
+ totalCacheSize: '\n📊 总缓存大小: ',
83
+ cacheEmpty: '🍃 当前缓存空空如也。',
84
+ description: 'Oinone 前端脚手架',
85
+ cacheCmdDesc: '管理本地静态资源缓存',
86
+ cacheCleanDesc: '清理本地静态资源缓存',
87
+ cacheInfoDesc: '打印缓存目录信息',
88
+ versionOption: '输出版本号',
89
+ helpOption: '显示命令帮助',
90
+ projectNameArg: '静默创建时的项目名称',
91
+ downloadOpt: '是否下载静态资源 (true/false)',
92
+ versionOpt: '使用的 Oinone 版本 (例如 7.2.0, 6.4.0)',
93
+ editionOpt: '使用的 Oinone 版本类型 (community/enterprise)',
94
+ promptCompany: '🏢 您的公司/组织名称是?(例如 ss):',
95
+ errorCompanyEmpty: '哎呀!公司名称不能为空 🙈',
96
+ promptProject: '🚀 给您的项目起个名字吧 (例如 oms):',
97
+ errorProjectEmpty: '稍等!项目名称不能为空 🛑',
98
+ promptEdition: '💎 请选择 Oinone 版本类型:',
99
+ editionCommunity: '🌱 社区版 (默认)',
100
+ editionEnterprise: '👑 企业版',
101
+ promptVersion: '📦 今天我们要使用哪个 Oinone 版本?',
102
+ version720: '✨ 7.2.0 (默认)',
103
+ version640: '🕰️ 6.4.0',
104
+ promptDownload: '📥 是否为您获取最新的静态资源?(强烈推荐!🌟)',
105
+ errorDirExists: '\n🚫 哎呀!目录 {name} 已存在。请尝试其他名称或清理该路径。\n',
106
+ weavingMagic: '✨ 正在施展魔法为您创建项目...',
107
+ errorTemplateMissing: '😢 糟糕!未找到 {version} 版本 ({edition}版) 的模板。',
108
+ structureReady: '🎉 棒!项目结构已准备就绪!',
109
+ haulingStatic: '🚚 正在搬运静态资源...',
110
+ staticDownloaded: '🎯 太准了!静态资源下载完毕,随时可以起飞!',
111
+ staticHiccup: '⚠️ 项目已就绪,但静态资源遇到点小麻烦: {msg}',
112
+ projectCreated: '🎉 棒!项目创建成功!',
113
+ skipStaticWarning: '\n⚠️ 您选择了跳过静态资源下载。',
114
+ manualStaticInstructions: '启动项目前,别忘了手动获取它们:',
115
+ partyStarted: '\n🚀 让我们开始狂欢吧:\n',
116
+ happyCoding: '编码愉快!💻☕\n',
117
+ updateAvailable: '📦 发现新版本!{current} → {latest}',
118
+ runUpdate: '运行 {cmd} 进行更新。',
119
+ somethingBroke: '\n💥 糟糕!出了点问题: {msg}\n'
120
+ }
121
+ };
122
+
123
+ const t = (key, params = {}) => {
124
+ let str = locales[currentLang][key] || locales['en-US'][key] || key;
125
+ Object.keys(params).forEach((k) => {
126
+ str = str.replace(new RegExp(`{${k}}`, 'g'), params[k]);
127
+ });
128
+ return str;
129
+ };
130
+
131
+ module.exports = {
132
+ currentLang,
133
+ t
134
+ };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@oinone/cli",
3
- "version": "7.2.3",
3
+ "version": "7.2.5",
4
4
  "description": "CLI tool to generate Oinone Frontend Template",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "oinone-frontend": "./bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "test": "vitest run"
11
11
  },
12
12
  "keywords": [
13
13
  "oinone",
@@ -25,5 +25,8 @@
25
25
  "fs-extra": "^11.3.4",
26
26
  "inquirer": "^8.2.6",
27
27
  "ora": "^5.4.1"
28
+ },
29
+ "devDependencies": {
30
+ "vitest": "^4.1.4"
28
31
  }
29
32
  }