@struggler/cli 1.0.2 → 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.
@@ -0,0 +1,6 @@
1
+ # macOS / editor noise
2
+ .DS_Store
3
+ Thumbs.db
4
+
5
+ # source maps are often unnecessary on CDN refreshes
6
+ *.map
package/Makefile ADDED
@@ -0,0 +1,81 @@
1
+ SHELL := /bin/zsh
2
+
3
+ NODE ?= node
4
+ PNPM ?= pnpm
5
+ CLI_ENTRY ?= ./index.js
6
+ CONFIG ?= ./command/qiniu.json
7
+ DIR ?= ./dist
8
+ DRY_RUN ?=
9
+ CONCURRENCY ?= 5
10
+ EXCLUDE ?=
11
+ MANIFEST ?=
12
+ JSON ?=
13
+ SKIP_INIT ?=
14
+ SKIP_REFRESH ?=
15
+
16
+ .PHONY: help install reinstall link unlink init upload refresh deploy add-version example-upload test
17
+
18
+ help:
19
+ @echo "Available targets:"
20
+ @echo " make install Install dependencies"
21
+ @echo " make reinstall Reinstall dependencies from scratch"
22
+ @echo " make link Link the CLI globally for local usage"
23
+ @echo " make unlink Remove the global link"
24
+ @echo " make init Generate/update upload config metadata"
25
+ @echo " make upload Upload files from DIR using CONFIG"
26
+ @echo " make refresh Refresh CDN for files from DIR using CONFIG"
27
+ @echo " make deploy Run init, upload, and refresh in one go"
28
+ @echo " make add-version Run the package version bump script"
29
+ @echo " make example-upload Upload the sample test/dist3 directory"
30
+ @echo " make test Run the automated test suite"
31
+ @echo ""
32
+ @echo "Variables:"
33
+ @echo " CONFIG=$(CONFIG)"
34
+ @echo " DIR=$(DIR)"
35
+ @echo " CLI_ENTRY=$(CLI_ENTRY)"
36
+ @echo " CONCURRENCY=$(CONCURRENCY)"
37
+ @echo " DRY_RUN=$(DRY_RUN)"
38
+ @echo " EXCLUDE=$(EXCLUDE)"
39
+ @echo " MANIFEST=$(MANIFEST)"
40
+ @echo " JSON=$(JSON)"
41
+ @echo ""
42
+ @echo "Example:"
43
+ @echo " make upload DIR=./test/dist3 CONFIG=./test/command/qiniu.json DRY_RUN=--dry-run EXCLUDE=.DS_Store"
44
+
45
+ install:
46
+ $(PNPM) install
47
+
48
+ reinstall:
49
+ rm -rf node_modules
50
+ $(PNPM) install
51
+
52
+ link:
53
+ @if ! $(PNPM) bin --global >/dev/null 2>&1; then \
54
+ echo "pnpm global bin not configured, running 'pnpm setup' first..."; \
55
+ $(PNPM) setup; \
56
+ fi
57
+ @$(PNPM) link --global
58
+
59
+ unlink:
60
+ @$(PNPM) unlink --global @struggler/cli
61
+
62
+ init:
63
+ $(NODE) $(CLI_ENTRY) --config $(CONFIG) --dir $(DIR) $(DRY_RUN) $(JSON) init
64
+
65
+ upload:
66
+ $(NODE) $(CLI_ENTRY) --config $(CONFIG) --dir $(DIR) --concurrency $(CONCURRENCY) $(DRY_RUN) $(if $(EXCLUDE),--exclude $(EXCLUDE),) $(if $(MANIFEST),--manifest $(MANIFEST),) $(JSON) upload
67
+
68
+ refresh:
69
+ $(NODE) $(CLI_ENTRY) --config $(CONFIG) --dir $(DIR) $(DRY_RUN) $(if $(EXCLUDE),--exclude $(EXCLUDE),) $(if $(MANIFEST),--manifest $(MANIFEST),) $(JSON) refresh
70
+
71
+ deploy:
72
+ $(NODE) $(CLI_ENTRY) --config $(CONFIG) --dir $(DIR) --concurrency $(CONCURRENCY) $(DRY_RUN) $(if $(EXCLUDE),--exclude $(EXCLUDE),) $(if $(MANIFEST),--manifest $(MANIFEST),) $(JSON) $(SKIP_INIT) $(SKIP_REFRESH) deploy
73
+
74
+ add-version:
75
+ $(PNPM) add-version
76
+
77
+ example-upload:
78
+ $(NODE) $(CLI_ENTRY) --config ./test/command/qiniu.json --dir ./test/dist3 --concurrency $(CONCURRENCY) $(DRY_RUN) $(if $(EXCLUDE),--exclude $(EXCLUDE),) $(if $(MANIFEST),--manifest $(MANIFEST),) $(JSON) upload
79
+
80
+ test:
81
+ $(PNPM) test
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # struggler-cli
2
+
3
+ `struggler-cli` is a small deployment CLI for front-end build assets on Qiniu Cloud. It can generate versioned paths, upload build output, refresh CDN URLs, and now supports `dry-run`, concurrent uploads, ignore rules, manifest export, JSON output, and a one-shot `deploy` command.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm install
9
+ ```
10
+
11
+ For local command usage:
12
+
13
+ ```bash
14
+ pnpm link --global
15
+ ```
16
+
17
+ ## Config Files
18
+
19
+ The CLI expects two files in the same directory:
20
+
21
+ - `qiniu.json`: Qiniu credentials and bucket metadata
22
+ - `config.json`: generated deploy prefix metadata used by upload/refresh
23
+
24
+ Example `qiniu.json`:
25
+
26
+ ```json
27
+ {
28
+ "path": "your-project",
29
+ "accessKey": "",
30
+ "secretKey": "",
31
+ "Bucket": "",
32
+ "zone": "Zone_z1",
33
+ "domain": "https://cdn.example.com/"
34
+ }
35
+ ```
36
+
37
+ Optional ignore file:
38
+
39
+ ```text
40
+ .strugglerignore
41
+ ```
42
+
43
+ Example:
44
+
45
+ ```text
46
+ .DS_Store
47
+ *.map
48
+ legacy/**
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ Initialize versioned config:
54
+
55
+ ```bash
56
+ node ./index.js --config ./command/qiniu.json --dir ./dist init
57
+ ```
58
+
59
+ Preview upload work without changing files or calling Qiniu:
60
+
61
+ ```bash
62
+ node ./index.js --config ./command/qiniu.json --dir ./dist --dry-run upload
63
+ ```
64
+
65
+ Upload with concurrency:
66
+
67
+ ```bash
68
+ node ./index.js --config ./command/qiniu.json --dir ./dist --concurrency 8 upload
69
+ ```
70
+
71
+ Upload with ignore patterns and a manifest:
72
+
73
+ ```bash
74
+ node ./index.js --config ./command/qiniu.json --dir ./dist --exclude ".DS_Store,*.map" --manifest ./artifacts/upload-manifest.json upload
75
+ ```
76
+
77
+ Refresh CDN for generated URLs:
78
+
79
+ ```bash
80
+ node ./index.js --config ./command/qiniu.json --dir ./dist refresh
81
+ ```
82
+
83
+ Run the full flow:
84
+
85
+ ```bash
86
+ node ./index.js --config ./command/qiniu.json --dir ./dist --concurrency 8 deploy
87
+ ```
88
+
89
+ Skip parts of deploy:
90
+
91
+ ```bash
92
+ node ./index.js --config ./command/qiniu.json --dir ./dist --skip-refresh deploy
93
+ ```
94
+
95
+ Machine-readable output:
96
+
97
+ ```bash
98
+ node ./index.js --config ./command/qiniu.json --dir ./dist --json --dry-run deploy
99
+ ```
100
+
101
+ Version bump script:
102
+
103
+ ```bash
104
+ pnpm add-version
105
+ ```
106
+
107
+ ## Makefile Shortcuts
108
+
109
+ ```bash
110
+ make init
111
+ make upload
112
+ make refresh
113
+ make deploy
114
+ make upload DIR=./test/dist3 CONFIG=./test/command/qiniu.json DRY_RUN=--dry-run
115
+ make deploy MANIFEST=./artifacts/deploy.json JSON=--json SKIP_REFRESH=--skip-refresh
116
+ ```
117
+
118
+ ## Test
119
+
120
+ ```bash
121
+ pnpm test
122
+ ```
123
+
124
+ The automated tests cover config path resolution, init dry-run behavior, upload ignore handling, manifest generation, and deploy JSON summaries.
@@ -14,16 +14,22 @@ function getPackageJson() {
14
14
  function main(){
15
15
  let packageData = getPackageJson();//获取package的json
16
16
  let arr = packageData.version.split('.');//切割后的版本号数组
17
+ if (arr.length !== 3 || arr.some((item) => Number.isNaN(parseInt(item, 10)))) {
18
+ throw new Error(`Invalid semver version: ${packageData.version}`)
19
+ }
17
20
  arr[2] = parseInt(arr[2]) + 1;
18
21
  packageData.version = arr.join('.');//转换为以"."分割的字符串
19
- //用packageData覆盖package.json内容
20
- fs.writeFile(
22
+ fs.writeFileSync(
21
23
  packagePath,
22
24
  JSON.stringify(packageData, null, "\t"
23
- ),
24
- (err) => { }
25
+ )
25
26
  )
27
+ console.log(`Version updated to ${packageData.version}`)
26
28
  }
27
29
 
28
30
 
29
- module.exports = main
31
+ module.exports = main
32
+
33
+ if (require.main === module) {
34
+ main()
35
+ }
@@ -0,0 +1,75 @@
1
+ const init = require('./init');
2
+ const upload = require('./upload');
3
+ const refresh = require('./refresh');
4
+ const { collectDeployFiles, createSummary, finalizeOutput } = require('../lib/deploy');
5
+ const { getDir } = require('../lib/config');
6
+ const { createIgnoreMatcher } = require('../lib/ignore');
7
+ const { printMessage } = require('../lib/output');
8
+
9
+ async function main(options) {
10
+ const startedAt = Date.now();
11
+ const dir = getDir(options);
12
+ const ignoreMatcher = createIgnoreMatcher(dir, options);
13
+ const files = await collectDeployFiles(dir, options);
14
+ const manifestExtra = {
15
+ command: 'deploy',
16
+ skippedSteps: {
17
+ init: Boolean(options.skipInit),
18
+ refresh: Boolean(options.skipRefresh),
19
+ },
20
+ };
21
+
22
+ let initConfig = null;
23
+ if (!options.skipInit) {
24
+ initConfig = await init(options);
25
+ }
26
+
27
+ const prefix = initConfig ? initConfig.publicPath : undefined;
28
+ const uploadSummary = await upload(options, {
29
+ files,
30
+ prefix,
31
+ excludePatterns: ignoreMatcher.patterns,
32
+ suppressOutput: true,
33
+ });
34
+
35
+ let refreshSummary = createSummary('refresh', Boolean(options.dryRun), [], startedAt, {
36
+ skipped: Boolean(options.skipRefresh),
37
+ prefix: uploadSummary.prefix,
38
+ excludedPatterns: ignoreMatcher.patterns,
39
+ });
40
+
41
+ if (!options.skipRefresh) {
42
+ const refreshFiles = uploadSummary.succeeded.map((item) => item.localFile);
43
+ refreshSummary = await refresh(options, {
44
+ files: refreshFiles,
45
+ prefix: uploadSummary.prefix,
46
+ excludePatterns: ignoreMatcher.patterns,
47
+ suppressOutput: true,
48
+ });
49
+ } else {
50
+ printMessage(options, '[deploy] refresh step skipped');
51
+ }
52
+
53
+ const deploySummary = finalizeOutput(createSummary('deploy', Boolean(options.dryRun), [
54
+ {
55
+ ok: uploadSummary.failedCount === 0,
56
+ step: 'upload',
57
+ detail: uploadSummary,
58
+ },
59
+ {
60
+ ok: options.skipRefresh || refreshSummary.failedCount === 0,
61
+ step: 'refresh',
62
+ detail: refreshSummary,
63
+ },
64
+ ], startedAt, {
65
+ upload: uploadSummary,
66
+ refresh: refreshSummary,
67
+ prefix: uploadSummary.prefix,
68
+ excludedPatterns: ignoreMatcher.patterns,
69
+ }), options, manifestExtra);
70
+
71
+ printMessage(options, `[deploy] upload=${uploadSummary.succeededCount}/${uploadSummary.total} refresh=${refreshSummary.succeededCount}/${refreshSummary.total}`);
72
+ return deploySummary;
73
+ }
74
+
75
+ module.exports = main
package/command/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  var init = require('./init');
2
2
  var upload = require('./upload');
3
- var addVersion = require('./addVersion');
3
+ var refresh = require('./refresh');
4
+ var deploy = require('./deploy');
4
5
 
5
6
  exports.init = init;
6
7
  exports.upload = upload;
7
- exports.addVersion = addVersion;
8
+ exports.refresh = refresh;
9
+ exports.deploy = deploy;
package/command/init.js CHANGED
@@ -2,22 +2,17 @@ let { getConfig, getQiniuConfig } = require('../lib/config')
2
2
  const chalk = require('chalk');
3
3
  let { getJsonData, setJsonData, setSyncJsonData, directoryExists } = require('../lib/files')
4
4
  let path = require('path')
5
+ const { formatDate } = require('../lib/date');
6
+ const { printMessage } = require('../lib/output');
5
7
 
6
- function formatDate(date) {
7
- date = date || new Date()
8
- const year = date.getFullYear()
9
- const month = date.getMonth() + 1
10
- const day = date.getDate()
11
- const hours = date.getHours()
12
- const minutes = date.getMinutes()
13
-
14
- return `${year}${month < 10 ? `0${month}` : month}${day < 10 ? `0${day}` : day}${hours < 10 ? `0${hours}` : hours}${minutes < 10 ? `0${minutes}` : minutes}`
15
- }
16
-
17
- function init(qiniuConfigPath, configPath){
8
+ function init(qiniuConfigPath, options){
18
9
  if (!directoryExists(qiniuConfigPath)){
19
- console.log(chalk.red("七牛配置不存在 正在生成模版 请稍后在下面的文件里填写必要的信息!"))
20
- console.log(chalk.blue(qiniuConfigPath))
10
+ printMessage(options, chalk.red("七牛配置不存在 正在生成模版 请稍后在下面的文件里填写必要的信息!"))
11
+ printMessage(options, chalk.blue(qiniuConfigPath))
12
+ if (options.dryRun) {
13
+ printMessage(options, `[dry-run] init would create template ${qiniuConfigPath}`)
14
+ return
15
+ }
21
16
  setSyncJsonData(qiniuConfigPath, getJsonData(path.resolve(__dirname, '../def/qiniu.json')))
22
17
  }
23
18
 
@@ -26,16 +21,21 @@ function init(qiniuConfigPath, configPath){
26
21
  function main(options){
27
22
  let configPath = getConfig(options)
28
23
  let qiniuConfigPath = getQiniuConfig(options)
29
- init(qiniuConfigPath, configPath)
24
+ init(qiniuConfigPath, options)
30
25
  const qiniuConfig = getJsonData(qiniuConfigPath)
31
26
  let domain = qiniuConfig.domain
32
-
27
+ const versionPrefix = formatDate()
33
28
  let config = getJsonData(configPath)
34
- config.publicPath = qiniuConfig.path + '/' + formatDate() + '/'
35
- config.base = domain + qiniuConfig.path + '/' + formatDate() + '/'
36
- //用packageData覆盖package.json内容
37
- console.log(config)
29
+ config.publicPath = `${qiniuConfig.path || ''}/${versionPrefix}/`
30
+ config.base = `${domain || ''}${qiniuConfig.path || ''}/${versionPrefix}/`
31
+ if (options.dryRun) {
32
+ printMessage(options, `[dry-run] init would write ${configPath}`)
33
+ printMessage(options, JSON.stringify(config, null, 2))
34
+ return config
35
+ }
36
+ printMessage(options, JSON.stringify(config, null, 2))
38
37
  setJsonData(configPath, config)
38
+ return config
39
39
  }
40
40
 
41
41
  module.exports = main
@@ -0,0 +1,111 @@
1
+ var qiniu = require("qiniu");
2
+
3
+ var qiniuPrefix = require("../lib/prefix")
4
+
5
+ let { getQiniuConfig, getDir } = require('../lib/config')
6
+ let { getJsonData } = require('../lib/files')
7
+ const { createIgnoreMatcher } = require('../lib/ignore');
8
+ const {
9
+ collectDeployFiles,
10
+ createSummary,
11
+ ensureRequiredConfig,
12
+ finalizeOutput,
13
+ logPlan,
14
+ toRemoteKey,
15
+ } = require('../lib/deploy');
16
+ const { printMessage } = require('../lib/output');
17
+
18
+ async function main(options, runtime = {}) {
19
+ const qiniuConfig = getJsonData(getQiniuConfig(options))
20
+ const prefix = runtime.prefix || qiniuPrefix.prefix(options);
21
+ let dir = getDir(options)
22
+ const startedAt = Date.now();
23
+ const ignoreMatcher = createIgnoreMatcher(dir, options);
24
+ const excludedPatterns = runtime.excludePatterns || ignoreMatcher.patterns;
25
+ const files = runtime.files || await collectDeployFiles(dir, options);
26
+ const plans = files.map((localFile) => {
27
+ const key = toRemoteKey(prefix, dir, localFile);
28
+ return {
29
+ localFile,
30
+ key,
31
+ target: `${qiniuConfig.domain || ''}${key}`,
32
+ };
33
+ });
34
+
35
+ if (options.dryRun) {
36
+ if (!runtime.suppressOutput) {
37
+ logPlan('refresh', plans, options);
38
+ }
39
+ const summary = createSummary('refresh', true, plans.map((plan) => ({
40
+ ok: true,
41
+ localFile: plan.localFile,
42
+ key: plan.key,
43
+ target: plan.target,
44
+ })), startedAt, {
45
+ prefix,
46
+ excludedPatterns,
47
+ });
48
+ return runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
49
+ }
50
+
51
+ ensureRequiredConfig(
52
+ { ...qiniuConfig, 'publicPath(config.json)': prefix },
53
+ ['accessKey', 'secretKey', 'domain', 'publicPath(config.json)']
54
+ );
55
+
56
+ var accessKey = qiniuConfig.accessKey
57
+ var secretKey = qiniuConfig.secretKey;
58
+ var mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
59
+ var cdnManager = new qiniu.cdn.CdnManager(mac);
60
+
61
+ function refresh(plan) {
62
+ return new Promise((resolve, reject) => {
63
+ cdnManager.refreshUrls([plan.target], function (respErr, respBody, respInfo) {
64
+ if (respErr || respInfo.statusCode !== 200) {
65
+ const errorMessage = respErr || (respBody && respBody.error) || `statusCode=${respInfo && respInfo.statusCode}`;
66
+ reject(new Error(errorMessage));
67
+ return;
68
+ }
69
+
70
+ resolve(respBody);
71
+ });
72
+ });
73
+ }
74
+
75
+ const results = [];
76
+ for (const plan of plans) {
77
+ try {
78
+ await refresh(plan);
79
+ if (!runtime.suppressOutput) {
80
+ printMessage(options, `[refreshed] ${plan.target}`);
81
+ }
82
+ results.push({
83
+ ok: true,
84
+ localFile: plan.localFile,
85
+ key: plan.key,
86
+ target: plan.target,
87
+ });
88
+ } catch (error) {
89
+ results.push({
90
+ ok: false,
91
+ localFile: plan.localFile,
92
+ key: plan.key,
93
+ target: plan.target,
94
+ error: error.message,
95
+ });
96
+ }
97
+ }
98
+
99
+ const summary = createSummary('refresh', false, results, startedAt, {
100
+ prefix,
101
+ excludedPatterns,
102
+ });
103
+ const finalSummary = runtime.suppressOutput ? summary : finalizeOutput(summary, options, runtime.manifestExtra);
104
+ if (finalSummary.failedCount > 0) {
105
+ throw new Error(`Refresh finished with ${summary.failedCount} failures`);
106
+ }
107
+
108
+ return finalSummary;
109
+ }
110
+
111
+ module.exports = main