@struggler/cli 1.0.3 → 1.0.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.
package/lib/i18n.js ADDED
@@ -0,0 +1,189 @@
1
+ const LANGUAGES = {
2
+ zh: {
3
+ appDescription: '用于将前端打包产物上传到七牛云 OSS 的命令行工具。',
4
+ options: {
5
+ version: '显示版本号。',
6
+ config: '指定上传配置文件路径。',
7
+ dir: '指定要上传的目录。',
8
+ dryRun: '仅预览执行计划,不写文件也不调用七牛接口。',
9
+ concurrency: '设置上传并发数。',
10
+ exclude: '排除文件匹配规则,支持逗号分隔多个模式。',
11
+ ignoreFile: '指定自定义忽略文件。',
12
+ manifest: '将命令执行结果写入 manifest JSON 文件。',
13
+ json: '输出机器可读的 JSON 结果。',
14
+ skipInit: '在 deploy 时跳过 init 步骤。',
15
+ skipRefresh: '在 deploy 时跳过 refresh 步骤。',
16
+ lang: '切换菜单语言,可选 zh / en。',
17
+ help: '显示帮助信息。',
18
+ },
19
+ commands: {
20
+ init: '为指定路径生成上传配置。',
21
+ upload: '将指定目录下文件上传到七牛云。',
22
+ refresh: '刷新七牛云 CDN 文件。',
23
+ deploy: '一次执行 init、upload 和 refresh。',
24
+ },
25
+ help: {
26
+ helpCommandDescription: '显示指定命令的帮助信息',
27
+ commandUsage: '用法:',
28
+ commandDescription: '说明:',
29
+ commandOptions: '参数:',
30
+ commandCommands: '命令:',
31
+ subcommandTerm: '命令',
32
+ optionTerm: '参数',
33
+ argumentTerm: '参数',
34
+ usage: '用法',
35
+ options: '参数',
36
+ commands: '命令',
37
+ arguments: '参数',
38
+ },
39
+ },
40
+ en: {
41
+ appDescription: 'CLI to upload front-end build files to Qiniu Cloud OSS.',
42
+ options: {
43
+ version: 'Display the version number.',
44
+ config: 'Specify the path to the upload configuration file.',
45
+ dir: 'Specify the directory to upload.',
46
+ dryRun: 'Preview actions without writing files or calling Qiniu APIs.',
47
+ concurrency: 'Set upload concurrency.',
48
+ exclude: 'Exclude file glob patterns, supports comma-separated values.',
49
+ ignoreFile: 'Specify a custom ignore file.',
50
+ manifest: 'Write the command summary to a manifest JSON file.',
51
+ json: 'Print machine-readable JSON output.',
52
+ skipInit: 'Skip init during deploy.',
53
+ skipRefresh: 'Skip refresh during deploy.',
54
+ lang: 'Switch menu language, supported values: zh / en.',
55
+ help: 'Display help information.',
56
+ },
57
+ commands: {
58
+ init: 'Create upload configuration for the specified path.',
59
+ upload: 'Upload files under the specified directory to Qiniu Cloud.',
60
+ refresh: 'Refresh Qiniu Cloud CDN files.',
61
+ deploy: 'Run init, upload, and refresh in one command.',
62
+ },
63
+ help: {
64
+ helpCommandDescription: 'display help for command',
65
+ commandUsage: 'Usage:',
66
+ commandDescription: 'Description:',
67
+ commandOptions: 'Options:',
68
+ commandCommands: 'Commands:',
69
+ subcommandTerm: 'command',
70
+ optionTerm: 'option',
71
+ argumentTerm: 'argument',
72
+ usage: 'Usage',
73
+ options: 'Options',
74
+ commands: 'Commands',
75
+ arguments: 'Arguments',
76
+ },
77
+ },
78
+ };
79
+
80
+ function normalizeLocaleTag(value) {
81
+ if (!value) {
82
+ return '';
83
+ }
84
+
85
+ return value.toLowerCase().replace('.', '_');
86
+ }
87
+
88
+ function detectLangFromLocale(localeValue) {
89
+ const normalized = normalizeLocaleTag(localeValue);
90
+ if (!normalized) {
91
+ return null;
92
+ }
93
+
94
+ if (normalized.startsWith('en')) {
95
+ return 'en';
96
+ }
97
+
98
+ if (normalized.startsWith('zh')) {
99
+ return 'zh';
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ function detectLangFromVSLang(value) {
106
+ if (!value) {
107
+ return null;
108
+ }
109
+
110
+ const normalized = String(value).trim();
111
+ const englishCodes = new Set(['1033', '2057', '4105', '5129', '6153']);
112
+ const chineseCodes = new Set(['2052', '1028', '3076', '4100']);
113
+
114
+ if (englishCodes.has(normalized)) {
115
+ return 'en';
116
+ }
117
+
118
+ if (chineseCodes.has(normalized)) {
119
+ return 'zh';
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ function detectLangFromWindowsValue(value) {
126
+ return detectLangFromLocale(value);
127
+ }
128
+
129
+ function detectSystemLang(env = process.env) {
130
+ const localeCandidates = [
131
+ env.LC_ALL,
132
+ env.LC_MESSAGES,
133
+ env.LANG,
134
+ env.PreferredUILanguages,
135
+ env.UILANG,
136
+ env.Culture,
137
+ env.UserLanguage,
138
+ ];
139
+
140
+ for (const candidate of localeCandidates) {
141
+ const detected = detectLangFromLocale(candidate);
142
+ if (detected) {
143
+ return detected;
144
+ }
145
+ }
146
+
147
+ const windowsSpecificCandidates = [
148
+ env.VSLANG,
149
+ env.PREFERRED_UI_LANGUAGES,
150
+ env.UI_LANG,
151
+ ];
152
+
153
+ for (const candidate of windowsSpecificCandidates) {
154
+ const detected = detectLangFromVSLang(candidate) || detectLangFromWindowsValue(candidate);
155
+ if (detected) {
156
+ return detected;
157
+ }
158
+ }
159
+
160
+ return 'zh';
161
+ }
162
+
163
+ function resolveLang(argv) {
164
+ const langFlagIndex = argv.findIndex((item) => item === '--lang');
165
+ if (langFlagIndex >= 0 && argv[langFlagIndex + 1] && LANGUAGES[argv[langFlagIndex + 1]]) {
166
+ return argv[langFlagIndex + 1];
167
+ }
168
+
169
+ const inlineFlag = argv.find((item) => item.startsWith('--lang='));
170
+ if (inlineFlag) {
171
+ const [, value] = inlineFlag.split('=');
172
+ if (LANGUAGES[value]) {
173
+ return value;
174
+ }
175
+ }
176
+
177
+ return detectSystemLang();
178
+ }
179
+
180
+ function getLocale(lang) {
181
+ return LANGUAGES[lang] || LANGUAGES.zh;
182
+ }
183
+
184
+ module.exports = {
185
+ LANGUAGES,
186
+ detectSystemLang,
187
+ resolveLang,
188
+ getLocale,
189
+ };
package/lib/ignore.js ADDED
@@ -0,0 +1,69 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_IGNORE_FILE = '.strugglerignore';
5
+
6
+ function escapeRegExp(value) {
7
+ return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
8
+ }
9
+
10
+ function patternToRegExp(pattern) {
11
+ const normalizedPattern = pattern.replace(/\\/g, '/');
12
+ const escaped = escapeRegExp(normalizedPattern)
13
+ .replace(/\\\*\\\*/g, '.*')
14
+ .replace(/\\\*/g, '[^/]*');
15
+ return new RegExp(`(^|/)${escaped}$`);
16
+ }
17
+
18
+ function parseIgnoreFile(ignoreFilePath) {
19
+ if (!fs.existsSync(ignoreFilePath)) {
20
+ return [];
21
+ }
22
+
23
+ return fs.readFileSync(ignoreFilePath, 'utf8')
24
+ .split(/\r?\n/)
25
+ .map((line) => line.trim())
26
+ .filter((line) => line && !line.startsWith('#'));
27
+ }
28
+
29
+ function normalizeExcludePatterns(value) {
30
+ if (!value) {
31
+ return [];
32
+ }
33
+
34
+ if (Array.isArray(value)) {
35
+ return value.flatMap((item) => normalizeExcludePatterns(item));
36
+ }
37
+
38
+ return String(value)
39
+ .split(',')
40
+ .map((item) => item.trim())
41
+ .filter(Boolean);
42
+ }
43
+
44
+ function createIgnoreMatcher(rootDir, options = {}) {
45
+ const ignoreFilePath = path.resolve(process.cwd(), options.ignoreFile || DEFAULT_IGNORE_FILE);
46
+ const patterns = [
47
+ ...parseIgnoreFile(ignoreFilePath),
48
+ ...normalizeExcludePatterns(options.exclude),
49
+ ];
50
+ const matchers = patterns.map((pattern) => patternToRegExp(pattern));
51
+
52
+ return {
53
+ patterns,
54
+ shouldIgnore(filePath) {
55
+ if (matchers.length === 0) {
56
+ return false;
57
+ }
58
+
59
+ const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/');
60
+ return matchers.some((matcher) => matcher.test(relativePath));
61
+ },
62
+ };
63
+ }
64
+
65
+ module.exports = {
66
+ DEFAULT_IGNORE_FILE,
67
+ normalizeExcludePatterns,
68
+ createIgnoreMatcher,
69
+ };
package/lib/output.js ADDED
@@ -0,0 +1,37 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function shouldUseJson(options = {}) {
5
+ return Boolean(options.json);
6
+ }
7
+
8
+ function printMessage(options, message) {
9
+ if (!shouldUseJson(options)) {
10
+ console.log(message);
11
+ }
12
+ }
13
+
14
+ function printError(options, message) {
15
+ if (!shouldUseJson(options)) {
16
+ console.error(message);
17
+ }
18
+ }
19
+
20
+ function printJson(payload) {
21
+ console.log(JSON.stringify(payload, null, 2));
22
+ }
23
+
24
+ function writeManifest(filePath, payload) {
25
+ const resolvedPath = path.resolve(process.cwd(), filePath);
26
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
27
+ fs.writeFileSync(resolvedPath, JSON.stringify(payload, null, 2));
28
+ return resolvedPath;
29
+ }
30
+
31
+ module.exports = {
32
+ shouldUseJson,
33
+ printMessage,
34
+ printError,
35
+ printJson,
36
+ writeManifest,
37
+ };
package/lib/prefix.js CHANGED
@@ -1,24 +1,10 @@
1
1
  let { getConfig } = require('./config')
2
2
  let { getJsonData } = require('./files')
3
3
 
4
- //七牛文件上传前缀,使用时间戳作为文件上传前缀
5
- function formatDate(date) {
6
- date = date || new Date()
7
- const year = date.getFullYear()
8
- const month = date.getMonth() + 1
9
- const day = date.getDate()
10
- const hours = date.getHours()
11
- const minutes = date.getMinutes()
12
-
13
- return `${year}${month < 10 ? `0${month}` : month}${day < 10 ? `0${day}` : day}${hours < 10 ? `0${hours}` : hours}${minutes < 10 ? `0${minutes}` : minutes}`
14
- }
15
-
16
-
17
-
18
4
  module.exports = {
19
5
  prefix: (options)=>{
20
6
  const { publicPath } = getJsonData(getConfig(options))
21
- return publicPath
7
+ return publicPath || ''
22
8
  }
23
9
 
24
- }
10
+ }
package/package.json CHANGED
@@ -1,28 +1,29 @@
1
1
  {
2
- "name": "@struggler/cli",
3
- "version": "1.0.3",
4
- "description": "CLI to Upload vite packaged files to Qiniu Cloud OSS.",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "bin": {
10
- "struggler-cli": "./index.js"
11
- },
12
- "keywords": [
13
- "Qiniu",
14
- "OSS"
15
- ],
16
- "author": "moqi(str@li.cm)",
17
- "license": "ISC",
18
- "dependencies": {
19
- "chalk": "^v4.1.2",
20
- "clear": "^0.1.0",
21
- "commander": "^11.0.0",
22
- "figlet": "^1.6.0",
23
- "qiniu": "^7.8.0"
24
- },
25
- "publicConfig": {
26
- "registry": "http://registry.npmjs.org/"
27
- }
28
- }
2
+ "name": "@struggler/cli",
3
+ "version": "1.0.5",
4
+ "description": "CLI to Upload vite packaged files to Qiniu Cloud OSS.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "add-version": "node ./command/addVersion.js",
8
+ "test": "node --test"
9
+ },
10
+ "bin": {
11
+ "struggler-cli": "./index.js"
12
+ },
13
+ "keywords": [
14
+ "Qiniu",
15
+ "OSS"
16
+ ],
17
+ "author": "moqi(str@li.cm)",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "chalk": "^v4.1.2",
21
+ "clear": "^0.1.0",
22
+ "commander": "^11.0.0",
23
+ "figlet": "^1.6.0",
24
+ "qiniu": "^7.8.0"
25
+ },
26
+ "publicConfig": {
27
+ "registry": "http://registry.npmjs.org/"
28
+ }
29
+ }
@@ -0,0 +1,41 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const { getConfig, getQiniuConfig, getDir } = require('../lib/config');
4
+ const { normalizeConcurrency, toRemoteKey } = require('../lib/deploy');
5
+ const { normalizeExcludePatterns } = require('../lib/ignore');
6
+
7
+ test('config helpers resolve qiniu and derived config paths together', () => {
8
+ const options = {
9
+ config: './test/command/qiniu.json',
10
+ dir: './test/dist3',
11
+ };
12
+
13
+ assert.equal(
14
+ getQiniuConfig(options),
15
+ `${process.cwd()}/test/command/qiniu.json`
16
+ );
17
+ assert.equal(
18
+ getConfig(options),
19
+ `${process.cwd()}/test/command/config.json`
20
+ );
21
+ assert.equal(
22
+ getDir(options),
23
+ `${process.cwd()}/test/dist3`
24
+ );
25
+ });
26
+
27
+ test('deploy helpers normalize concurrency and build remote keys', () => {
28
+ assert.equal(normalizeConcurrency('0'), 5);
29
+ assert.equal(normalizeConcurrency('3'), 3);
30
+ assert.equal(
31
+ toRemoteKey('release/20260313/', '/tmp/dist', '/tmp/dist/assets/app.js'),
32
+ 'release/20260313/assets/app.js'
33
+ );
34
+ });
35
+
36
+ test('ignore helpers split exclude patterns consistently', () => {
37
+ assert.deepEqual(
38
+ normalizeExcludePatterns('*.map,.DS_Store, dist/**'),
39
+ ['*.map', '.DS_Store', 'dist/**']
40
+ );
41
+ });
@@ -0,0 +1,52 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const init = require('../command/init');
7
+
8
+ function createTempWorkspace() {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'struggler-cli-init-'));
10
+ }
11
+
12
+ test('init dry-run does not create files and returns generated config', async () => {
13
+ const previousCwd = process.cwd();
14
+ const workspace = createTempWorkspace();
15
+
16
+ try {
17
+ process.chdir(workspace);
18
+
19
+ const config = await init({
20
+ config: './command/qiniu.json',
21
+ dryRun: true,
22
+ });
23
+
24
+ assert.equal(fs.existsSync(path.join(workspace, 'command/qiniu.json')), false);
25
+ assert.match(config.publicPath, /^\/\d{12}\/$/);
26
+ assert.match(config.base, /^\/\d{12}\/$/);
27
+ } finally {
28
+ process.chdir(previousCwd);
29
+ fs.rmSync(workspace, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test('init writes qiniu template and deploy config when not in dry-run mode', async () => {
34
+ const previousCwd = process.cwd();
35
+ const workspace = createTempWorkspace();
36
+
37
+ try {
38
+ process.chdir(workspace);
39
+
40
+ const config = await init({
41
+ config: './command/qiniu.json',
42
+ });
43
+
44
+ assert.equal(fs.existsSync(path.join(workspace, 'command/qiniu.json')), true);
45
+ assert.equal(fs.existsSync(path.join(workspace, 'command/config.json')), true);
46
+ assert.equal(config.publicPath.endsWith('/'), true);
47
+ assert.equal(config.base.endsWith('/'), true);
48
+ } finally {
49
+ process.chdir(previousCwd);
50
+ fs.rmSync(workspace, { recursive: true, force: true });
51
+ }
52
+ });
package/test/test.sh CHANGED
@@ -1,5 +1,5 @@
1
1
  struggler-cli upload -d ./dist3
2
2
 
3
- struggler-cli addVersion
3
+ pnpm add-version
4
4
 
5
- struggler-cli init
5
+ struggler-cli init
@@ -0,0 +1,113 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const upload = require('../command/upload');
7
+ const deploy = require('../command/deploy');
8
+
9
+ function createWorkspace() {
10
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'struggler-cli-upload-'));
11
+ fs.mkdirSync(path.join(workspace, 'command'), { recursive: true });
12
+ fs.mkdirSync(path.join(workspace, 'dist/assets'), { recursive: true });
13
+
14
+ fs.writeFileSync(path.join(workspace, 'command/qiniu.json'), JSON.stringify({
15
+ path: 'demo-app',
16
+ domain: 'https://cdn.example.com/',
17
+ }, null, 2));
18
+ fs.writeFileSync(path.join(workspace, 'command/config.json'), JSON.stringify({
19
+ publicPath: 'demo-app/202603131600/',
20
+ base: 'https://cdn.example.com/demo-app/202603131600/',
21
+ }, null, 2));
22
+ fs.writeFileSync(path.join(workspace, 'dist/index.html'), '<html></html>');
23
+ fs.writeFileSync(path.join(workspace, 'dist/assets/app.js'), 'console.log("ok")');
24
+ fs.writeFileSync(path.join(workspace, 'dist/assets/app.js.gz'), 'gzip');
25
+ fs.writeFileSync(path.join(workspace, 'dist/.DS_Store'), 'noise');
26
+ fs.writeFileSync(path.join(workspace, '.strugglerignore'), '.DS_Store\n');
27
+
28
+ return workspace;
29
+ }
30
+
31
+ test('upload dry-run plans files, skips gz assets, and returns summary', async () => {
32
+ const previousCwd = process.cwd();
33
+ const workspace = createWorkspace();
34
+
35
+ try {
36
+ process.chdir(workspace);
37
+
38
+ const summary = await upload({
39
+ config: './command/qiniu.json',
40
+ dir: './dist',
41
+ dryRun: true,
42
+ concurrency: '2',
43
+ });
44
+
45
+ assert.equal(summary.action, 'upload');
46
+ assert.equal(summary.dryRun, true);
47
+ assert.equal(summary.total, 2);
48
+ assert.equal(summary.failedCount, 0);
49
+ assert.deepEqual(
50
+ summary.succeeded.map((item) => item.key),
51
+ [
52
+ 'demo-app/202603131600/assets/app.js',
53
+ 'demo-app/202603131600/index.html',
54
+ ]
55
+ );
56
+ assert.equal(summary.excludedPatterns.includes('.DS_Store'), true);
57
+ } finally {
58
+ process.chdir(previousCwd);
59
+ fs.rmSync(workspace, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ test('upload dry-run can write a manifest file', async () => {
64
+ const previousCwd = process.cwd();
65
+ const workspace = createWorkspace();
66
+
67
+ try {
68
+ process.chdir(workspace);
69
+
70
+ const summary = await upload({
71
+ config: './command/qiniu.json',
72
+ dir: './dist',
73
+ dryRun: true,
74
+ manifest: './artifacts/upload.json',
75
+ });
76
+
77
+ const manifestPath = path.resolve(workspace, 'artifacts/upload.json');
78
+ assert.equal(fs.existsSync(manifestPath), true);
79
+ assert.equal(summary.manifestPath, fs.realpathSync(manifestPath));
80
+
81
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
82
+ assert.equal(manifest.action, 'upload');
83
+ assert.equal(manifest.total, 2);
84
+ } finally {
85
+ process.chdir(previousCwd);
86
+ fs.rmSync(workspace, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test('deploy dry-run supports skip-refresh and json-friendly summary data', async () => {
91
+ const previousCwd = process.cwd();
92
+ const workspace = createWorkspace();
93
+
94
+ try {
95
+ process.chdir(workspace);
96
+
97
+ const summary = await deploy({
98
+ config: './command/qiniu.json',
99
+ dir: './dist',
100
+ dryRun: true,
101
+ json: true,
102
+ skipRefresh: true,
103
+ });
104
+
105
+ assert.equal(summary.action, 'deploy');
106
+ assert.equal(summary.upload.total, 2);
107
+ assert.equal(summary.refresh.skipped, true);
108
+ assert.equal(summary.excludedPatterns.includes('.DS_Store'), true);
109
+ } finally {
110
+ process.chdir(previousCwd);
111
+ fs.rmSync(workspace, { recursive: true, force: true });
112
+ }
113
+ });