@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 +15 -3
- package/lib/deploy.js +56 -3
- package/lib/progress.js +82 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
75
|
-
|
|
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, `
|
|
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
|
|
package/lib/progress.js
ADDED
|
@@ -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 };
|