@struggler/cli 1.0.6 → 1.0.8

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/command/deploy.js CHANGED
@@ -1,10 +1,12 @@
1
1
  const init = require('./init');
2
2
  const upload = require('./upload');
3
3
  const refresh = require('./refresh');
4
+ const chalk = require('chalk');
4
5
  const { collectDeployFiles, createSummary, finalizeOutput } = require('../lib/deploy');
5
6
  const { getDir } = require('../lib/config');
6
7
  const { createIgnoreMatcher } = require('../lib/ignore');
7
8
  const { printMessage } = require('../lib/output');
9
+ const { getLocale } = require('../lib/i18n');
8
10
 
9
11
  async function main(options) {
10
12
  const startedAt = Date.now();
@@ -47,7 +49,8 @@ async function main(options) {
47
49
  suppressOutput: true,
48
50
  });
49
51
  } else {
50
- printMessage(options, '[deploy] refresh step skipped');
52
+ const { messages } = getLocale(options && options.lang);
53
+ printMessage(options, ` ${chalk.dim(`↩ ${messages.deploySkipRefresh}`)}`);
51
54
  }
52
55
 
53
56
  const deploySummary = finalizeOutput(createSummary('deploy', Boolean(options.dryRun), [
@@ -68,7 +71,18 @@ async function main(options) {
68
71
  excludedPatterns: ignoreMatcher.patterns,
69
72
  }), options, manifestExtra);
70
73
 
71
- printMessage(options, `[deploy] upload=${uploadSummary.succeededCount}/${uploadSummary.total} refresh=${refreshSummary.succeededCount}/${refreshSummary.total}`);
74
+ const { messages } = getLocale(options && options.lang);
75
+ const uploadOk = uploadSummary.failedCount === 0;
76
+ const refreshOk = options.skipRefresh || refreshSummary.failedCount === 0;
77
+ const allOk = uploadOk && refreshOk;
78
+ printMessage(options, '');
79
+ printMessage(options, ` ${allOk ? chalk.green('✓') : chalk.red('✗')} ${chalk.bold(messages.deployDone)}`);
80
+ printMessage(options, '');
81
+ printMessage(options, ` ${chalk.dim(messages.deployUploadLabel)} ${chalk.green(uploadSummary.succeededCount)} / ${uploadSummary.total} ${messages.deployFileUnit}`);
82
+ if (!options.skipRefresh) {
83
+ printMessage(options, ` ${chalk.dim(messages.deployRefreshLabel)} ${chalk.green(refreshSummary.succeededCount)} / ${refreshSummary.total} ${messages.deployUnit}`);
84
+ }
85
+ printMessage(options, '');
72
86
  return deploySummary;
73
87
  }
74
88
 
package/command/init.js CHANGED
@@ -4,21 +4,23 @@ let { getJsonData, setJsonData, setSyncJsonData, directoryExists } = require('..
4
4
  let path = require('path')
5
5
  const { formatDate } = require('../lib/date');
6
6
  const { printMessage } = require('../lib/output');
7
+ const { getLocale } = require('../lib/i18n');
7
8
 
8
9
  function init(qiniuConfigPath, options){
10
+ const { messages } = getLocale(options && options.lang);
9
11
  if (!directoryExists(qiniuConfigPath)){
10
- printMessage(options, chalk.red("七牛配置不存在 正在生成模版 请稍后在下面的文件里填写必要的信息!"))
11
- printMessage(options, chalk.blue(qiniuConfigPath))
12
+ printMessage(options, chalk.yellow(` ⚠ ${messages.initTemplateCreated}`));
13
+ printMessage(options, ` ${chalk.dim(qiniuConfigPath)}`);
12
14
  if (options.dryRun) {
13
- printMessage(options, `[dry-run] init would create template ${qiniuConfigPath}`)
15
+ printMessage(options, chalk.dim(messages.initDryRunCreate(qiniuConfigPath)));
14
16
  return
15
17
  }
16
18
  setSyncJsonData(qiniuConfigPath, getJsonData(path.resolve(__dirname, '../def/qiniu.json')))
17
19
  }
18
-
19
20
  }
20
21
 
21
22
  function main(options){
23
+ const { messages } = getLocale(options && options.lang);
22
24
  let configPath = getConfig(options)
23
25
  let qiniuConfigPath = getQiniuConfig(options)
24
26
  init(qiniuConfigPath, options)
@@ -29,11 +31,12 @@ function main(options){
29
31
  config.publicPath = `${qiniuConfig.path || ''}/${versionPrefix}/`
30
32
  config.base = `${domain || ''}${qiniuConfig.path || ''}/${versionPrefix}/`
31
33
  if (options.dryRun) {
32
- printMessage(options, `[dry-run] init would write ${configPath}`)
33
- printMessage(options, JSON.stringify(config, null, 2))
34
+ printMessage(options, chalk.dim(messages.initDryRunWrite(configPath)));
35
+ printMessage(options, chalk.dim(JSON.stringify(config, null, 2)));
34
36
  return config
35
37
  }
36
- printMessage(options, JSON.stringify(config, null, 2))
38
+ printMessage(options, ` ${chalk.green('✓')} ${messages.initConfigUpdated}`);
39
+ printMessage(options, ` ${chalk.dim(configPath)}`);
37
40
  setJsonData(configPath, config)
38
41
  return config
39
42
  }
@@ -14,6 +14,8 @@ const {
14
14
  toRemoteKey,
15
15
  } = require('../lib/deploy');
16
16
  const { printMessage } = require('../lib/output');
17
+ const { createProgressBar } = require('../lib/progress');
18
+ const { getLocale } = require('../lib/i18n');
17
19
 
18
20
  async function main(options, runtime = {}) {
19
21
  const qiniuConfig = getJsonData(getQiniuConfig(options))
@@ -50,7 +52,8 @@ async function main(options, runtime = {}) {
50
52
 
51
53
  ensureRequiredConfig(
52
54
  { ...qiniuConfig, 'publicPath(config.json)': prefix },
53
- ['accessKey', 'secretKey', 'domain', 'publicPath(config.json)']
55
+ ['accessKey', 'secretKey', 'domain', 'publicPath(config.json)'],
56
+ options
54
57
  );
55
58
 
56
59
  var accessKey = qiniuConfig.accessKey
@@ -72,13 +75,16 @@ async function main(options, runtime = {}) {
72
75
  });
73
76
  }
74
77
 
78
+ const bar = createProgressBar(plans.length, {
79
+ json: options.json,
80
+ suppressOutput: runtime.suppressOutput,
81
+ });
82
+
75
83
  const results = [];
76
84
  for (const plan of plans) {
77
85
  try {
78
86
  await refresh(plan);
79
- if (!runtime.suppressOutput) {
80
- printMessage(options, `[refreshed] ${plan.target}`);
81
- }
87
+ bar.tick({ filename: plan.target });
82
88
  results.push({
83
89
  ok: true,
84
90
  localFile: plan.localFile,
@@ -86,6 +92,7 @@ async function main(options, runtime = {}) {
86
92
  target: plan.target,
87
93
  });
88
94
  } catch (error) {
95
+ bar.tick({ filename: plan.target, failed: true });
89
96
  results.push({
90
97
  ok: false,
91
98
  localFile: plan.localFile,
@@ -96,13 +103,16 @@ async function main(options, runtime = {}) {
96
103
  }
97
104
  }
98
105
 
106
+ bar.finish();
107
+
99
108
  const summary = createSummary('refresh', false, results, startedAt, {
100
109
  prefix,
101
110
  excludedPatterns,
102
111
  });
103
112
  const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
104
113
  if (finalSummary.failedCount > 0) {
105
- throw new Error(`Refresh finished with ${summary.failedCount} failures`);
114
+ const { messages } = getLocale(options && options.lang);
115
+ throw new Error(messages.refreshFailed(summary.failedCount));
106
116
  }
107
117
 
108
118
  return finalSummary;
package/command/upload.js CHANGED
@@ -17,6 +17,8 @@ const {
17
17
  } = require('../lib/deploy');
18
18
  const { printMessage } = require('../lib/output');
19
19
  const { computeFileMd5, readCache, writeCache, isCacheHit, updateCacheEntry } = require('../lib/cache');
20
+ const { createProgressBar } = require('../lib/progress');
21
+ const { getLocale } = require('../lib/i18n');
20
22
 
21
23
  async function main(options, runtime = {}) {
22
24
  const qiniuConfig = getJsonData(getQiniuConfig(options))
@@ -74,7 +76,8 @@ async function main(options, runtime = {}) {
74
76
 
75
77
  ensureRequiredConfig(
76
78
  { ...qiniuConfig, 'publicPath(config.json)': prefix },
77
- ['accessKey', 'secretKey', 'Bucket', 'zone', 'domain', 'publicPath(config.json)']
79
+ ['accessKey', 'secretKey', 'Bucket', 'zone', 'domain', 'publicPath(config.json)'],
80
+ options
78
81
  );
79
82
 
80
83
  var accessKey = qiniuConfig.accessKey
@@ -99,7 +102,8 @@ async function main(options, runtime = {}) {
99
102
  formUploader.putFile(uploadToken, plan.key, plan.localFile, putExtra, async function (respErr, respBody, respInfo) {
100
103
  if (respErr || respInfo.statusCode !== 200) {
101
104
  if (attempt < 3) {
102
- console.log(`${plan.localFile} 上传失败,正在进行第 ${attempt + 1} 次重试`);
105
+ const { messages } = getLocale(options && options.lang);
106
+ printMessage(options, messages.uploadRetrying(plan.localFile, attempt + 1));
103
107
  try {
104
108
  const retryResult = await upload(plan, attempt + 1);
105
109
  resolve(retryResult);
@@ -123,12 +127,20 @@ async function main(options, runtime = {}) {
123
127
  });
124
128
  }
125
129
 
130
+ const totalFiles = plans.length + skippedPlans.length;
131
+ const bar = createProgressBar(totalFiles, {
132
+ json: options.json,
133
+ suppressOutput: runtime.suppressOutput,
134
+ });
135
+
136
+ for (const plan of skippedPlans) {
137
+ bar.tick({ filename: plan.localFile, skipped: true });
138
+ }
139
+
126
140
  const results = await runWithConcurrency(plans, normalizeConcurrency(options.concurrency), async (plan) => {
127
141
  try {
128
142
  const response = await upload(plan);
129
- if (!runtime.suppressOutput) {
130
- printMessage(options, `[uploaded] ${plan.localFile} -> ${plan.target}`);
131
- }
143
+ bar.tick({ filename: plan.localFile });
132
144
  if (useCache && plan.localMd5) {
133
145
  updateCacheEntry(cache, plan.key, plan.localMd5, response.hash);
134
146
  }
@@ -140,6 +152,7 @@ async function main(options, runtime = {}) {
140
152
  hash: response.hash,
141
153
  };
142
154
  } catch (error) {
155
+ bar.tick({ filename: plan.localFile, failed: true });
143
156
  return {
144
157
  ok: false,
145
158
  localFile: plan.localFile,
@@ -150,6 +163,8 @@ async function main(options, runtime = {}) {
150
163
  }
151
164
  });
152
165
 
166
+ bar.finish();
167
+
153
168
  if (useCache) {
154
169
  writeCache(cachePath, cache);
155
170
  }
@@ -172,7 +187,8 @@ async function main(options, runtime = {}) {
172
187
  });
173
188
  const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
174
189
  if (finalSummary.failedCount > 0) {
175
- throw new Error(`Upload finished with ${summary.failedCount} failures`);
190
+ const { messages } = getLocale(options && options.lang);
191
+ throw new Error(messages.uploadFailed(summary.failedCount));
176
192
  }
177
193
 
178
194
  return finalSummary;
package/index.js CHANGED
@@ -19,7 +19,7 @@ if (!shouldUseJson({ json: isJsonMode })) {
19
19
 
20
20
  // 输出Logo
21
21
  if (!isJsonMode) {
22
- console.log(magentaBright(figlet.textSync("struggler-cli", { horizontalLayout: "full" })), "\n\n")
22
+ console.log(magentaBright(figlet.textSync("struggler-cli", { font: "Small" })), "\n")
23
23
  }
24
24
 
25
25
  function formatItems(items, getLeft, getRight) {
@@ -97,6 +97,13 @@ program.parseAsync().catch(error => {
97
97
  printJson({ ok: false, error: error.message || String(error) })
98
98
  return
99
99
  }
100
- printError({}, error.message || error)
100
+ const { redBright, dim } = require('chalk')
101
+ const { messages } = getLocale(lang)
102
+ const msg = error.message || String(error)
103
+ console.error('')
104
+ console.error(redBright(` ✗ ${messages.errorTitle}`))
105
+ console.error('')
106
+ msg.split('\n').forEach(line => console.error(` ${dim(line)}`))
107
+ console.error('')
101
108
  process.exitCode = 1
102
109
  })
package/lib/deploy.js CHANGED
@@ -1,7 +1,9 @@
1
1
  const path = require('path');
2
+ const chalk = require('chalk');
2
3
  const { listFiles } = require('./files');
3
4
  const { createIgnoreMatcher } = require('./ignore');
4
5
  const { printMessage, writeManifest, shouldUseJson, printJson } = require('./output');
6
+ const { getLocale } = require('./i18n');
5
7
 
6
8
  const DEFAULT_CONCURRENCY = 5;
7
9
 
@@ -64,19 +66,76 @@ function createSummary(action, dryRun, results, startedAt, extra = {}) {
64
66
  }
65
67
 
66
68
  function logPlan(action, items, options) {
67
- printMessage(options, `[dry-run] ${action} plan (${items.length} files)`);
69
+ const { messages } = getLocale(options && options.lang);
70
+ printMessage(options, messages.dryRunPlan(action, items.length));
68
71
  items.forEach((item) => {
69
- printMessage(options, `- ${item.localFile} -> ${item.target}`);
72
+ printMessage(options, ` ${item.localFile} -> ${item.target}`);
70
73
  });
71
74
  }
72
75
 
76
+ function formatDuration(ms) {
77
+ if (ms < 1000) return `${ms}ms`;
78
+ return `${(ms / 1000).toFixed(1)}s`;
79
+ }
80
+
73
81
  function logSummary(summary, options) {
74
- const mode = summary.dryRun ? 'dry-run' : 'live';
75
- printMessage(options, `[summary] ${summary.action} mode=${mode} total=${summary.total} succeeded=${summary.succeededCount} failed=${summary.failedCount} duration=${summary.durationMs}ms`);
82
+ const { messages } = getLocale(options && options.lang);
83
+ const allOk = summary.failedCount === 0;
84
+ const isDryRun = summary.dryRun;
85
+ const skipped = summary.skippedCount || 0;
86
+ const uploaded = summary.succeededCount - skipped;
87
+
88
+ const statusIcon = allOk ? chalk.green('✓') : chalk.red('✗');
89
+ const actionDoneLabel = summary.action === 'upload'
90
+ ? messages.uploadDone
91
+ : summary.action === 'refresh'
92
+ ? messages.refreshDone
93
+ : messages.deployDone;
94
+ const modeTag = isDryRun ? chalk.yellow(` [${messages.dryRunLabel}]`) : '';
95
+
96
+ printMessage(options, '');
97
+ printMessage(options, ` ${statusIcon} ${chalk.bold(actionDoneLabel)}${modeTag} ${chalk.dim(formatDuration(summary.durationMs))}`);
98
+ printMessage(options, '');
99
+
100
+ const rows = [];
101
+ if (summary.action === 'upload') {
102
+ rows.push([chalk.dim(messages.uploadLabel), chalk.green(`${uploaded} ${messages.uploadFileUnit}`)]);
103
+ if (skipped > 0) {
104
+ rows.push([chalk.dim(messages.uploadSkippedLabel), chalk.cyan(`${skipped} ${messages.uploadFileUnit}`) + chalk.dim(` (${messages.uploadCacheHint})`)]);
105
+ }
106
+ if (summary.failedCount > 0) {
107
+ rows.push([chalk.dim(messages.uploadFailedLabel), chalk.red(`${summary.failedCount} ${messages.uploadFileUnit}`)]);
108
+ }
109
+ rows.push([chalk.dim(messages.uploadTotalLabel), chalk.white(`${summary.total} ${messages.uploadFileUnit}`)]);
110
+ } else {
111
+ rows.push([chalk.dim(messages.refreshSucceededLabel), chalk.green(`${summary.succeededCount} ${messages.refreshUnit}`)]);
112
+ if (summary.failedCount > 0) {
113
+ rows.push([chalk.dim(messages.refreshFailedLabel), chalk.red(`${summary.failedCount} ${messages.refreshUnit}`)]);
114
+ }
115
+ rows.push([chalk.dim(messages.refreshTotalLabel), chalk.white(`${summary.total} ${messages.refreshUnit}`)]);
116
+ }
117
+
118
+ rows.forEach(([label, value]) => {
119
+ printMessage(options, ` ${label} ${value}`);
120
+ });
121
+ printMessage(options, '');
122
+
76
123
  if (summary.failedCount > 0) {
77
124
  summary.failed.forEach((item) => {
78
- printMessage(options, `[failed] ${item.localFile}: ${item.error}`);
125
+ printMessage(options, ` ${chalk.red('✗')} ${chalk.dim(item.localFile)}`);
126
+ printMessage(options, ` ${chalk.red(item.error)}`);
127
+ });
128
+ printMessage(options, '');
129
+ }
130
+
131
+ const uploadedItems = (summary.succeeded || []).filter((item) => !item.skipped && item.target);
132
+ if (uploadedItems.length > 0) {
133
+ printMessage(options, ` ${chalk.dim(`─── ${messages.uploadLinksTitle} ───`)}`);
134
+ printMessage(options, '');
135
+ uploadedItems.forEach((item) => {
136
+ printMessage(options, ` ${chalk.cyan(item.target)}`);
79
137
  });
138
+ printMessage(options, '');
80
139
  }
81
140
  }
82
141
 
@@ -103,17 +162,25 @@ function finalizeOutput(summary, options, extra = {}) {
103
162
  } else {
104
163
  logSummary(finalSummary, options);
105
164
  if (finalSummary.manifestPath) {
106
- printMessage(options, `[manifest] ${finalSummary.manifestPath}`);
165
+ const { messages } = getLocale(options && options.lang);
166
+ printMessage(options, ` ${chalk.dim(messages.manifestWritten)} ${finalSummary.manifestPath}`);
107
167
  }
108
168
  }
109
169
 
110
170
  return finalSummary;
111
171
  }
112
172
 
113
- function ensureRequiredConfig(config, requiredFields) {
173
+ function ensureRequiredConfig(config, requiredFields, options) {
114
174
  const missingFields = requiredFields.filter((field) => !config[field]);
115
175
  if (missingFields.length > 0) {
116
- throw new Error(`Missing required config: ${missingFields.join(', ')}`);
176
+ const { messages } = getLocale(options && options.lang);
177
+ const lines = [
178
+ messages.errorMissingConfig,
179
+ ...missingFields.map((f) => ` · ${f}`),
180
+ '',
181
+ messages.errorMissingConfigHint,
182
+ ];
183
+ throw new Error(lines.join('\n'));
117
184
  }
118
185
  }
119
186
 
package/lib/i18n.js CHANGED
@@ -1,6 +1,40 @@
1
1
  const LANGUAGES = {
2
2
  zh: {
3
3
  appDescription: '用于将前端打包产物上传到七牛云 OSS 的命令行工具。',
4
+ messages: {
5
+ initTemplateCreated: '七牛配置不存在,已生成模版,请填写必要信息:',
6
+ initConfigUpdated: '配置已更新:',
7
+ initDryRunCreate: (p) => `[预览] 将创建配置模版:${p}`,
8
+ initDryRunWrite: (p) => `[预览] 将写入配置:${p}`,
9
+ uploadRetrying: (file, attempt) => `${file} 上传失败,正在进行第 ${attempt} 次重试`,
10
+ uploadDone: '上传完成',
11
+ uploadLabel: '上传',
12
+ uploadSkippedLabel: '跳过',
13
+ uploadFailedLabel: '失败',
14
+ uploadTotalLabel: '合计',
15
+ uploadCacheHint: '缓存命中,无变更',
16
+ uploadLinksTitle: '已上传文件链接',
17
+ uploadFileUnit: '个文件',
18
+ uploadFailed: (n) => `上传完成,${n} 个失败`,
19
+ refreshDone: '刷新完成',
20
+ refreshSucceededLabel: '成功',
21
+ refreshFailedLabel: '失败',
22
+ refreshTotalLabel: '合计',
23
+ refreshUnit: '个',
24
+ refreshFailed: (n) => `刷新完成,${n} 个失败`,
25
+ deployDone: '部署完成',
26
+ deploySkipRefresh: '刷新步骤已跳过 (--skip-refresh)',
27
+ deployUploadLabel: '上传',
28
+ deployRefreshLabel: '刷新',
29
+ deployFileUnit: '个文件',
30
+ deployUnit: '个',
31
+ errorTitle: '出错了',
32
+ errorMissingConfig: '配置不完整,以下字段缺失或为空:',
33
+ errorMissingConfigHint: '请检查 command/qiniu.json 和 command/config.json,或运行 struggler-cli init 初始化配置。',
34
+ dryRunLabel: '预览',
35
+ dryRunPlan: (action, n) => `[预览] ${action} 计划 (${n} 个文件)`,
36
+ manifestWritten: '清单已写入:',
37
+ },
4
38
  options: {
5
39
  version: '显示版本号。',
6
40
  config: '指定上传配置文件路径。',
@@ -40,6 +74,40 @@ const LANGUAGES = {
40
74
  },
41
75
  en: {
42
76
  appDescription: 'CLI to upload front-end build files to Qiniu Cloud OSS.',
77
+ messages: {
78
+ initTemplateCreated: 'Qiniu config not found, template created. Please fill in the required fields:',
79
+ initConfigUpdated: 'Config updated:',
80
+ initDryRunCreate: (p) => `[dry-run] would create template: ${p}`,
81
+ initDryRunWrite: (p) => `[dry-run] would write config: ${p}`,
82
+ uploadRetrying: (file, attempt) => `${file} upload failed, retrying (attempt ${attempt})`,
83
+ uploadDone: 'Upload complete',
84
+ uploadLabel: 'Uploaded',
85
+ uploadSkippedLabel: 'Skipped',
86
+ uploadFailedLabel: 'Failed',
87
+ uploadTotalLabel: 'Total',
88
+ uploadCacheHint: 'cached, no changes',
89
+ uploadLinksTitle: 'Uploaded file links',
90
+ uploadFileUnit: 'files',
91
+ uploadFailed: (n) => `Upload finished with ${n} failures`,
92
+ refreshDone: 'Refresh complete',
93
+ refreshSucceededLabel: 'Succeeded',
94
+ refreshFailedLabel: 'Failed',
95
+ refreshTotalLabel: 'Total',
96
+ refreshUnit: '',
97
+ refreshFailed: (n) => `Refresh finished with ${n} failures`,
98
+ deployDone: 'Deploy complete',
99
+ deploySkipRefresh: 'Refresh step skipped (--skip-refresh)',
100
+ deployUploadLabel: 'Upload',
101
+ deployRefreshLabel: 'Refresh',
102
+ deployFileUnit: 'files',
103
+ deployUnit: '',
104
+ errorTitle: 'Error',
105
+ errorMissingConfig: 'Incomplete config, the following fields are missing or empty:',
106
+ errorMissingConfigHint: 'Check command/qiniu.json and command/config.json, or run struggler-cli init.',
107
+ dryRunLabel: 'dry-run',
108
+ dryRunPlan: (action, n) => `[dry-run] ${action} plan (${n} files)`,
109
+ manifestWritten: 'Manifest written:',
110
+ },
43
111
  options: {
44
112
  version: 'Display the version number.',
45
113
  config: 'Specify the path to the upload configuration file.',
@@ -0,0 +1,82 @@
1
+ const chalk = require('chalk');
2
+
3
+ const BAR_WIDTH = 30;
4
+
5
+ function buildBar(completed, total) {
6
+ const ratio = total === 0 ? 1 : completed / total;
7
+ const filled = Math.round(BAR_WIDTH * ratio);
8
+ const empty = BAR_WIDTH - filled;
9
+ return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
10
+ }
11
+
12
+ function truncateFilename(filename, maxLen) {
13
+ if (!filename || filename.length <= maxLen) {
14
+ return (filename || '').padEnd(maxLen);
15
+ }
16
+ return '...' + filename.slice(-(maxLen - 3));
17
+ }
18
+
19
+ function createProgressBar(total, options = {}) {
20
+ if (options.json || options.suppressOutput || total === 0) {
21
+ return { tick: () => {}, finish: () => {} };
22
+ }
23
+
24
+ let completed = 0;
25
+ let failed = 0;
26
+ let skipped = 0;
27
+ let lastLine = '';
28
+
29
+ const isTTY = process.stdout.isTTY;
30
+
31
+ function render(currentFile, status) {
32
+ const bar = buildBar(completed, total);
33
+ const pct = total === 0 ? 100 : Math.round((completed / total) * 100);
34
+ const cols = (process.stdout.columns || 80) - BAR_WIDTH - 30;
35
+ const nameWidth = Math.max(10, Math.min(40, cols));
36
+ const name = truncateFilename(currentFile, nameWidth);
37
+
38
+ let statusIcon;
39
+ if (status === 'skipped') {
40
+ statusIcon = chalk.cyan('↩');
41
+ } else if (status === 'failed') {
42
+ statusIcon = chalk.red('✗');
43
+ } else {
44
+ statusIcon = chalk.green('✓');
45
+ }
46
+
47
+ const countStr = chalk.white(`${completed}/${total}`);
48
+ const pctStr = chalk.yellow(`${pct}%`);
49
+ const line = ` ${bar} ${countStr} ${pctStr} ${statusIcon} ${chalk.dim(name)}`;
50
+
51
+ if (isTTY) {
52
+ process.stdout.write('\r' + line.padEnd(lastLine.length));
53
+ } else {
54
+ console.log(line);
55
+ }
56
+ lastLine = line;
57
+ }
58
+
59
+ function tick({ filename = '', skipped: isSkipped = false, failed: isFailed = false } = {}) {
60
+ completed += 1;
61
+ if (isSkipped) skipped += 1;
62
+ if (isFailed) failed += 1;
63
+ render(filename, isSkipped ? 'skipped' : isFailed ? 'failed' : 'uploaded');
64
+ }
65
+
66
+ function finish() {
67
+ if (!isTTY) return;
68
+ const bar = buildBar(total, total);
69
+ const parts = [
70
+ ` ${bar}`,
71
+ chalk.white(`${total}/${total}`),
72
+ chalk.green('100%'),
73
+ ];
74
+ if (skipped > 0) parts.push(chalk.cyan(`${skipped} skipped`));
75
+ if (failed > 0) parts.push(chalk.red(`${failed} failed`));
76
+ process.stdout.write('\r' + parts.join(' ').padEnd(lastLine.length) + '\n');
77
+ }
78
+
79
+ return { tick, finish };
80
+ }
81
+
82
+ module.exports = { createProgressBar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@struggler/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "CLI to Upload vite packaged files to Qiniu Cloud OSS.",
5
5
  "main": "index.js",
6
6
  "scripts": {