@struggler/cli 1.0.6 → 1.0.7

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/upload.js CHANGED
@@ -17,6 +17,7 @@ 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');
20
21
 
21
22
  async function main(options, runtime = {}) {
22
23
  const qiniuConfig = getJsonData(getQiniuConfig(options))
@@ -123,12 +124,20 @@ async function main(options, runtime = {}) {
123
124
  });
124
125
  }
125
126
 
127
+ const totalFiles = plans.length + skippedPlans.length;
128
+ const bar = createProgressBar(totalFiles, {
129
+ json: options.json,
130
+ suppressOutput: runtime.suppressOutput,
131
+ });
132
+
133
+ for (const plan of skippedPlans) {
134
+ bar.tick({ filename: plan.localFile, skipped: true });
135
+ }
136
+
126
137
  const results = await runWithConcurrency(plans, normalizeConcurrency(options.concurrency), async (plan) => {
127
138
  try {
128
139
  const response = await upload(plan);
129
- if (!runtime.suppressOutput) {
130
- printMessage(options, `[uploaded] ${plan.localFile} -> ${plan.target}`);
131
- }
140
+ bar.tick({ filename: plan.localFile });
132
141
  if (useCache && plan.localMd5) {
133
142
  updateCacheEntry(cache, plan.key, plan.localMd5, response.hash);
134
143
  }
@@ -140,6 +149,7 @@ async function main(options, runtime = {}) {
140
149
  hash: response.hash,
141
150
  };
142
151
  } catch (error) {
152
+ bar.tick({ filename: plan.localFile, failed: true });
143
153
  return {
144
154
  ok: false,
145
155
  localFile: plan.localFile,
@@ -150,6 +160,8 @@ async function main(options, runtime = {}) {
150
160
  }
151
161
  });
152
162
 
163
+ bar.finish();
164
+
153
165
  if (useCache) {
154
166
  writeCache(cachePath, cache);
155
167
  }
package/lib/deploy.js CHANGED
@@ -1,4 +1,5 @@
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');
@@ -70,13 +71,65 @@ function logPlan(action, items, options) {
70
71
  });
71
72
  }
72
73
 
74
+ function formatDuration(ms) {
75
+ if (ms < 1000) return `${ms}ms`;
76
+ return `${(ms / 1000).toFixed(1)}s`;
77
+ }
78
+
73
79
  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`);
80
+ const allOk = summary.failedCount === 0;
81
+ const isDryRun = summary.dryRun;
82
+ const skipped = summary.skippedCount || 0;
83
+ const uploaded = summary.succeededCount - skipped;
84
+
85
+ const statusIcon = allOk ? chalk.green('✓') : chalk.red('✗');
86
+ const actionLabel = summary.action === 'upload' ? '上传' : summary.action === 'refresh' ? '刷新' : summary.action;
87
+ const modeTag = isDryRun ? chalk.yellow(' [dry-run]') : '';
88
+
89
+ printMessage(options, '');
90
+ printMessage(options, ` ${statusIcon} ${chalk.bold(actionLabel + '完成')}${modeTag} ${chalk.dim(formatDuration(summary.durationMs))}`);
91
+ printMessage(options, '');
92
+
93
+ const rows = [];
94
+ if (summary.action === 'upload') {
95
+ rows.push([chalk.dim('上传'), chalk.green(`${uploaded} 个文件`)]);
96
+ if (skipped > 0) {
97
+ rows.push([chalk.dim('跳过'), chalk.cyan(`${skipped} 个文件`) + chalk.dim(' (缓存命中,无变更)')]);
98
+ }
99
+ if (summary.failedCount > 0) {
100
+ rows.push([chalk.dim('失败'), chalk.red(`${summary.failedCount} 个文件`)]);
101
+ }
102
+ rows.push([chalk.dim('合计'), chalk.white(`${summary.total} 个文件`)]);
103
+ } else {
104
+ rows.push([chalk.dim('成功'), chalk.green(`${summary.succeededCount} 个`)]);
105
+ if (summary.failedCount > 0) {
106
+ rows.push([chalk.dim('失败'), chalk.red(`${summary.failedCount} 个`)]);
107
+ }
108
+ rows.push([chalk.dim('合计'), chalk.white(`${summary.total} 个`)]);
109
+ }
110
+
111
+ const labelWidth = 4;
112
+ rows.forEach(([label, value]) => {
113
+ printMessage(options, ` ${label.padEnd ? label : label} ${value}`);
114
+ });
115
+ printMessage(options, '');
116
+
76
117
  if (summary.failedCount > 0) {
77
118
  summary.failed.forEach((item) => {
78
- printMessage(options, `[failed] ${item.localFile}: ${item.error}`);
119
+ printMessage(options, ` ${chalk.red('✗')} ${chalk.dim(item.localFile)}`);
120
+ printMessage(options, ` ${chalk.red(item.error)}`);
121
+ });
122
+ printMessage(options, '');
123
+ }
124
+
125
+ const uploadedItems = (summary.succeeded || []).filter((item) => !item.skipped && item.target);
126
+ if (uploadedItems.length > 0) {
127
+ printMessage(options, ` ${chalk.dim('─── 已上传文件链接 ───')}`);
128
+ printMessage(options, '');
129
+ uploadedItems.forEach((item) => {
130
+ printMessage(options, ` ${chalk.cyan(item.target)}`);
79
131
  });
132
+ printMessage(options, '');
80
133
  }
81
134
  }
82
135
 
@@ -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.7",
4
4
  "description": "CLI to Upload vite packaged files to Qiniu Cloud OSS.",
5
5
  "main": "index.js",
6
6
  "scripts": {