@struggler/cli 1.0.12 → 1.0.13
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/Makefile +6 -1
- package/README.md +125 -46
- package/command/index.js +2 -0
- package/command/profile.js +84 -0
- package/index.js +38 -1
- package/lib/config.js +8 -8
- package/lib/i18n.js +36 -6
- package/lib/profile.js +148 -0
- package/package.json +1 -1
- package/test/config.test.js +12 -1
- package/test/profile.test.js +129 -0
package/Makefile
CHANGED
|
@@ -55,9 +55,14 @@ link:
|
|
|
55
55
|
$(PNPM) setup; \
|
|
56
56
|
fi
|
|
57
57
|
@$(PNPM) link --global
|
|
58
|
+
@hash -r
|
|
59
|
+
@which struggler-cli || true
|
|
60
|
+
@struggler-cli -v || true
|
|
58
61
|
|
|
59
62
|
unlink:
|
|
60
|
-
@$(PNPM) unlink --global @struggler/cli
|
|
63
|
+
@$(PNPM) unlink --global @struggler/cli || true
|
|
64
|
+
@npm uninstall -g @struggler/cli || true
|
|
65
|
+
@hash -r
|
|
61
66
|
|
|
62
67
|
init:
|
|
63
68
|
$(NODE) $(CLI_ENTRY) --config $(CONFIG) --dir $(DIR) $(DRY_RUN) $(JSON) init
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# struggler-cli
|
|
2
2
|
|
|
3
|
-
`struggler-cli` is a small deployment CLI for front-end build assets on Qiniu Cloud. It
|
|
3
|
+
`struggler-cli` is a small deployment CLI for front-end build assets on Qiniu Cloud. It supports versioned paths, upload, CDN refresh, upload cache, profiles for multiple Qiniu accounts, `dry-run`, concurrency, ignore rules, manifest export, JSON output, and a one-shot `deploy` command.
|
|
4
|
+
|
|
5
|
+
Works on **macOS, Linux, and Windows** (Node.js `path` / `fs` APIs are cross-platform).
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -14,105 +16,182 @@ For local command usage:
|
|
|
14
16
|
pnpm link --global
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
##
|
|
19
|
+
## First-time setup (recommended: profiles)
|
|
20
|
+
|
|
21
|
+
Use **profiles** to store Qiniu credentials under your user home: `~/.struggler-cli/` (Windows: `%USERPROFILE%\\.struggler-cli\\`). This folder is created automatically; it is **not** shipped with the npm package.
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
```text
|
|
24
|
+
~/.struggler-cli/ # created on first profile add/import (Windows/macOS/Linux)
|
|
25
|
+
profiles/
|
|
26
|
+
prod.json # Qiniu credentials for production
|
|
27
|
+
staging.json
|
|
28
|
+
current # one line: active profile name, e.g. prod
|
|
20
29
|
|
|
21
|
-
-
|
|
22
|
-
|
|
30
|
+
your-project/
|
|
31
|
+
command/
|
|
32
|
+
config.json # deploy prefix (publicPath / base), from init
|
|
33
|
+
upload-cache.json # upload cache (optional)
|
|
34
|
+
dist/ # build output to upload (-d)
|
|
35
|
+
```
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
### Step 1 — Create a profile
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"secretKey": "",
|
|
31
|
-
"Bucket": "",
|
|
32
|
-
"zone": "Zone_z1",
|
|
33
|
-
"domain": "https://cdn.example.com/"
|
|
34
|
-
}
|
|
39
|
+
From your project root:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
struggler-cli profile add prod
|
|
35
43
|
```
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
This creates `~/.struggler-cli/profiles/prod.json` from the built-in template. Open it and fill in:
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
| Field | Description |
|
|
48
|
+
|-------|-------------|
|
|
49
|
+
| `path` | CDN path prefix segment for this app |
|
|
50
|
+
| `accessKey` | Qiniu access key |
|
|
51
|
+
| `secretKey` | Qiniu secret key |
|
|
52
|
+
| `Bucket` | Bucket name |
|
|
53
|
+
| `zone` | e.g. `Zone_z0`, `Zone_z1`, `Zone_z2` |
|
|
54
|
+
| `domain` | CDN domain, e.g. `https://cdn.example.com/` |
|
|
55
|
+
|
|
56
|
+
### Step 2 — Activate the profile
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
struggler-cli profile use prod
|
|
41
60
|
```
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
Or pass the profile name per command: `-c prod`.
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
### Step 3 — Generate deploy metadata
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
struggler-cli init -d ./dist
|
|
49
68
|
```
|
|
50
69
|
|
|
51
|
-
|
|
70
|
+
Writes `command/config.json` with a timestamped `publicPath` and `base` URL (always under `./command/`, same as older versions).
|
|
52
71
|
|
|
53
|
-
|
|
72
|
+
### Step 4 — Upload
|
|
54
73
|
|
|
55
74
|
```bash
|
|
56
|
-
|
|
75
|
+
struggler-cli upload -d ./dist
|
|
57
76
|
```
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
### More environments
|
|
60
79
|
|
|
61
80
|
```bash
|
|
62
|
-
|
|
81
|
+
struggler-cli profile add staging
|
|
82
|
+
# edit ~/.struggler-cli/profiles/staging.json
|
|
83
|
+
|
|
84
|
+
struggler-cli profile import dev ./path/to/existing-qiniu.json
|
|
85
|
+
|
|
86
|
+
struggler-cli profile list
|
|
87
|
+
struggler-cli profile current
|
|
88
|
+
struggler-cli profile use staging
|
|
89
|
+
struggler-cli init
|
|
90
|
+
struggler-cli upload -d ./dist
|
|
63
91
|
```
|
|
64
92
|
|
|
65
|
-
|
|
93
|
+
Because profiles are user-level, they are outside your project repo by default.
|
|
94
|
+
|
|
95
|
+
## Legacy setup (single `command/qiniu.json`)
|
|
96
|
+
|
|
97
|
+
Still supported for existing projects:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
command/
|
|
101
|
+
qiniu.json
|
|
102
|
+
config.json
|
|
103
|
+
```
|
|
66
104
|
|
|
67
105
|
```bash
|
|
68
|
-
|
|
106
|
+
struggler-cli init -c ./command/qiniu.json -d ./dist
|
|
107
|
+
struggler-cli upload -c ./command/qiniu.json -d ./dist
|
|
69
108
|
```
|
|
70
109
|
|
|
71
|
-
|
|
110
|
+
If no profile is active and you omit `-c`, the CLI defaults to `./command/qiniu.json`.
|
|
111
|
+
|
|
112
|
+
## How `-c` resolves config
|
|
113
|
+
|
|
114
|
+
| You pass | Resolved to |
|
|
115
|
+
|----------|-------------|
|
|
116
|
+
| *(omit)* | Active profile in `~/.struggler-cli/current`, else `./command/qiniu.json` |
|
|
117
|
+
| `-c prod` | `~/.struggler-cli/profiles/prod.json` |
|
|
118
|
+
| `-c ./command/qiniu.json` | That file path (legacy) |
|
|
119
|
+
|
|
120
|
+
`config.json` and upload cache stay in `./command/` unless you pass `--config-dir`.
|
|
121
|
+
|
|
122
|
+
## Profile commands
|
|
72
123
|
|
|
73
124
|
```bash
|
|
74
|
-
|
|
125
|
+
struggler-cli profile list
|
|
126
|
+
struggler-cli profile use <name>
|
|
127
|
+
struggler-cli profile current
|
|
128
|
+
struggler-cli profile add <name>
|
|
129
|
+
struggler-cli profile import <name> <file>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Other config files
|
|
133
|
+
|
|
134
|
+
Optional ignore file:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
.strugglerignore
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
.DS_Store
|
|
144
|
+
*.map
|
|
145
|
+
legacy/**
|
|
75
146
|
```
|
|
76
147
|
|
|
77
|
-
|
|
148
|
+
## Commands
|
|
149
|
+
|
|
150
|
+
Initialize versioned config:
|
|
78
151
|
|
|
79
152
|
```bash
|
|
80
|
-
|
|
153
|
+
struggler-cli init -d ./dist
|
|
81
154
|
```
|
|
82
155
|
|
|
83
|
-
|
|
156
|
+
Preview upload:
|
|
84
157
|
|
|
85
158
|
```bash
|
|
86
|
-
|
|
159
|
+
struggler-cli --dry-run upload -d ./dist
|
|
87
160
|
```
|
|
88
161
|
|
|
89
|
-
|
|
162
|
+
Upload with concurrency:
|
|
90
163
|
|
|
91
164
|
```bash
|
|
92
|
-
|
|
165
|
+
struggler-cli upload -d ./dist --concurrency 8
|
|
93
166
|
```
|
|
94
167
|
|
|
95
|
-
|
|
168
|
+
Full deploy:
|
|
96
169
|
|
|
97
170
|
```bash
|
|
98
|
-
|
|
171
|
+
struggler-cli deploy -d ./dist --concurrency 8
|
|
99
172
|
```
|
|
100
173
|
|
|
101
|
-
|
|
174
|
+
Machine-readable output:
|
|
102
175
|
|
|
103
176
|
```bash
|
|
104
|
-
|
|
177
|
+
struggler-cli deploy -d ./dist --json --dry-run
|
|
105
178
|
```
|
|
106
179
|
|
|
107
|
-
##
|
|
180
|
+
## Windows notes
|
|
181
|
+
|
|
182
|
+
- The folder name `.struggler-cli` is valid on Windows; Node resolves paths with `\` automatically.
|
|
183
|
+
- User-level profile root on Windows is `%USERPROFILE%\\.struggler-cli\\`.
|
|
184
|
+
- In **Cmd** or **PowerShell**, run the same commands as on Unix.
|
|
185
|
+
- Avoid spaces in profile names; use `prod`, `staging`, `dev`.
|
|
186
|
+
- When scripting, quote paths: `--config-dir "./command"`.
|
|
187
|
+
|
|
188
|
+
## Makefile shortcuts
|
|
108
189
|
|
|
109
190
|
```bash
|
|
110
191
|
make init
|
|
111
192
|
make upload
|
|
112
193
|
make refresh
|
|
113
194
|
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
195
|
```
|
|
117
196
|
|
|
118
197
|
## Test
|
|
@@ -121,4 +200,4 @@ make deploy MANIFEST=./artifacts/deploy.json JSON=--json SKIP_REFRESH=--skip-ref
|
|
|
121
200
|
pnpm test
|
|
122
201
|
```
|
|
123
202
|
|
|
124
|
-
|
|
203
|
+
Tests cover profile resolution, config paths, init, upload, and deploy.
|
package/command/index.js
CHANGED
|
@@ -2,8 +2,10 @@ var init = require('./init');
|
|
|
2
2
|
var upload = require('./upload');
|
|
3
3
|
var refresh = require('./refresh');
|
|
4
4
|
var deploy = require('./deploy');
|
|
5
|
+
var profile = require('./profile');
|
|
5
6
|
|
|
6
7
|
exports.init = init;
|
|
7
8
|
exports.upload = upload;
|
|
8
9
|
exports.refresh = refresh;
|
|
9
10
|
exports.deploy = deploy;
|
|
11
|
+
exports.profile = profile;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const {
|
|
4
|
+
listProfiles,
|
|
5
|
+
readCurrentProfile,
|
|
6
|
+
writeCurrentProfile,
|
|
7
|
+
importProfile,
|
|
8
|
+
addProfile,
|
|
9
|
+
getProfilePath,
|
|
10
|
+
getProfilesDir,
|
|
11
|
+
} = require('../lib/profile');
|
|
12
|
+
const { printMessage } = require('../lib/output');
|
|
13
|
+
const { getLocale } = require('../lib/i18n');
|
|
14
|
+
|
|
15
|
+
const TEMPLATE_PATH = path.resolve(__dirname, '../def/qiniu.json');
|
|
16
|
+
|
|
17
|
+
function listAction(options) {
|
|
18
|
+
const { messages } = getLocale(options.lang);
|
|
19
|
+
const profiles = listProfiles();
|
|
20
|
+
const current = readCurrentProfile();
|
|
21
|
+
|
|
22
|
+
if (profiles.length === 0) {
|
|
23
|
+
printMessage(options, chalk.yellow(` ${messages.profileListEmpty}`));
|
|
24
|
+
printMessage(options, chalk.dim(` ${getProfilesDir()}`));
|
|
25
|
+
return { profiles: [], current: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
printMessage(options, messages.profileListTitle);
|
|
29
|
+
profiles.forEach((name) => {
|
|
30
|
+
const marker = name === current ? chalk.green(' *') : ' ';
|
|
31
|
+
const file = getProfilePath(name);
|
|
32
|
+
printMessage(options, `${marker} ${name}${chalk.dim(` → ${file}`)}`);
|
|
33
|
+
});
|
|
34
|
+
if (current) {
|
|
35
|
+
printMessage(options, chalk.dim(messages.profileListCurrentHint));
|
|
36
|
+
}
|
|
37
|
+
return { profiles, current };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function useAction(name, options) {
|
|
41
|
+
const { messages } = getLocale(options.lang);
|
|
42
|
+
const profilePath = writeCurrentProfile(name);
|
|
43
|
+
printMessage(options, chalk.green(` ✓ ${messages.profileUseDone(name)}`));
|
|
44
|
+
printMessage(options, chalk.dim(` ${profilePath}`));
|
|
45
|
+
return { name, path: profilePath };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function currentAction(options) {
|
|
49
|
+
const { messages } = getLocale(options.lang);
|
|
50
|
+
const current = readCurrentProfile();
|
|
51
|
+
if (!current) {
|
|
52
|
+
printMessage(options, chalk.yellow(` ${messages.profileNoCurrent}`));
|
|
53
|
+
return { current: null };
|
|
54
|
+
}
|
|
55
|
+
const profilePath = getProfilePath(current);
|
|
56
|
+
printMessage(options, `${messages.profileCurrentLabel} ${chalk.cyan(current)}`);
|
|
57
|
+
printMessage(options, chalk.dim(` ${profilePath}`));
|
|
58
|
+
return { current, path: profilePath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addAction(name, options) {
|
|
62
|
+
const { messages } = getLocale(options.lang);
|
|
63
|
+
const profilePath = addProfile(name, TEMPLATE_PATH);
|
|
64
|
+
printMessage(options, chalk.green(` ✓ ${messages.profileAddDone(name)}`));
|
|
65
|
+
printMessage(options, chalk.dim(` ${profilePath}`));
|
|
66
|
+
printMessage(options, chalk.dim(messages.profileEditHint));
|
|
67
|
+
return { name, path: profilePath };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function importAction(name, fromPath, options) {
|
|
71
|
+
const { messages } = getLocale(options.lang);
|
|
72
|
+
const profilePath = importProfile(name, fromPath);
|
|
73
|
+
printMessage(options, chalk.green(` ✓ ${messages.profileImportDone(name)}`));
|
|
74
|
+
printMessage(options, chalk.dim(` ${profilePath}`));
|
|
75
|
+
return { name, path: profilePath, from: path.resolve(fromPath) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
list: listAction,
|
|
80
|
+
use: useAction,
|
|
81
|
+
current: currentAction,
|
|
82
|
+
add: addAction,
|
|
83
|
+
import: importAction,
|
|
84
|
+
};
|
package/index.js
CHANGED
|
@@ -58,7 +58,7 @@ program.configureHelp({
|
|
|
58
58
|
},
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
program.name("struggler-cli").description(locale.appDescription).version(packageJson.version, "-v, --version", locale.options.version).helpOption("-h, --help", locale.options.help).option("-c, --config <path>", locale.options.config
|
|
61
|
+
program.name("struggler-cli").description(locale.appDescription).version(packageJson.version, "-v, --version", locale.options.version).helpOption("-h, --help", locale.options.help).option("-c, --config <path>", locale.options.config).option("--config-dir [path]", locale.options.configDir).option("-d, --dir <path>", locale.options.dir, "./dist").option("--dry-run", locale.options.dryRun).option("--concurrency <number>", locale.options.concurrency, "5").option("--exclude <pattern>", locale.options.exclude).option("--ignore-file <path>", locale.options.ignoreFile, ".strugglerignore").option("--manifest <path>", locale.options.manifest).option("--json", locale.options.json).option("--skip-init", locale.options.skipInit).option("--skip-refresh", locale.options.skipRefresh).option("--no-cache", locale.options.noCache).option("--lang <lang>", locale.options.lang, lang)
|
|
62
62
|
|
|
63
63
|
program
|
|
64
64
|
.command("init")
|
|
@@ -88,6 +88,43 @@ program
|
|
|
88
88
|
await command.deploy(program.opts())
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
const profileCmd = program.command("profile").description(locale.commands.profile)
|
|
92
|
+
|
|
93
|
+
profileCmd
|
|
94
|
+
.command("list")
|
|
95
|
+
.description(locale.commands.profileList)
|
|
96
|
+
.action(() => {
|
|
97
|
+
command.profile.list(program.opts())
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
profileCmd
|
|
101
|
+
.command("use <name>")
|
|
102
|
+
.description(locale.commands.profileUse)
|
|
103
|
+
.action((name) => {
|
|
104
|
+
command.profile.use(name, program.opts())
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
profileCmd
|
|
108
|
+
.command("current")
|
|
109
|
+
.description(locale.commands.profileCurrent)
|
|
110
|
+
.action(() => {
|
|
111
|
+
command.profile.current(program.opts())
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
profileCmd
|
|
115
|
+
.command("add <name>")
|
|
116
|
+
.description(locale.commands.profileAdd)
|
|
117
|
+
.action((name) => {
|
|
118
|
+
command.profile.add(name, program.opts())
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
profileCmd
|
|
122
|
+
.command("import <name> <file>")
|
|
123
|
+
.description(locale.commands.profileImport)
|
|
124
|
+
.action((name, file) => {
|
|
125
|
+
command.profile.import(name, file, program.opts())
|
|
126
|
+
})
|
|
127
|
+
|
|
91
128
|
program.addHelpCommand(true, locale.help.helpCommandDescription)
|
|
92
129
|
|
|
93
130
|
program.parseAsync().catch(error => {
|
package/lib/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
let path = require('path')
|
|
2
2
|
const { getJsonData } = require('./files');
|
|
3
|
+
const { resolveProfileConfigPath } = require('./profile');
|
|
3
4
|
|
|
4
5
|
const DEFAULT_QINIU_CONFIG = './command/qiniu.json';
|
|
5
6
|
const DEFAULT_META_DIR = './command';
|
|
@@ -11,6 +12,10 @@ function resolveFromCwd(targetPath) {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function getQiniuConfigPath(options = {}) {
|
|
15
|
+
const profilePath = resolveProfileConfigPath(options);
|
|
16
|
+
if (profilePath) {
|
|
17
|
+
return profilePath;
|
|
18
|
+
}
|
|
14
19
|
return resolveFromCwd(options.config || DEFAULT_QINIU_CONFIG);
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -25,24 +30,19 @@ function resolveConfigDirOption(options = {}) {
|
|
|
25
30
|
return null;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
/** config.json / upload-cache.json 所在目录;优先级:CLI --config-dir > qiniu.json configDir >
|
|
33
|
+
/** config.json / upload-cache.json 所在目录;优先级:CLI --config-dir > qiniu.json configDir > 默认 ./command(与旧版一致,不随 -c 目录变化) */
|
|
29
34
|
function getMetaDir(options = {}) {
|
|
30
35
|
const cliConfigDir = resolveConfigDirOption(options);
|
|
31
36
|
if (cliConfigDir) {
|
|
32
37
|
return resolveFromCwd(cliConfigDir);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
const
|
|
36
|
-
const qiniuConfig = getJsonData(qiniuConfigPath);
|
|
40
|
+
const qiniuConfig = getJsonData(getQiniuConfigPath(options));
|
|
37
41
|
if (qiniuConfig.configDir) {
|
|
38
42
|
return resolveFromCwd(qiniuConfig.configDir);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
return resolveFromCwd(DEFAULT_META_DIR);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return path.dirname(qiniuConfigPath);
|
|
45
|
+
return resolveFromCwd(DEFAULT_META_DIR);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function getConfigPath(options = {}) {
|
package/lib/i18n.js
CHANGED
|
@@ -30,15 +30,24 @@ const LANGUAGES = {
|
|
|
30
30
|
deployUnit: '个',
|
|
31
31
|
errorTitle: '出错了',
|
|
32
32
|
errorMissingConfig: '配置不完整,以下字段缺失或为空:',
|
|
33
|
-
errorMissingConfigHint: '
|
|
33
|
+
errorMissingConfigHint: '请检查七牛配置(~/.struggler-cli/profiles,Windows 为 %USERPROFILE%\\.struggler-cli\\profiles)或 command/qiniu.json,以及 command/config.json;也可先运行 struggler-cli init。',
|
|
34
|
+
profileListEmpty: '尚无 profile,请先执行:struggler-cli profile add <name>',
|
|
35
|
+
profileListTitle: '可用 profile:',
|
|
36
|
+
profileListCurrentHint: '(* 为当前激活)',
|
|
37
|
+
profileUseDone: (name) => `已切换 profile:${name}`,
|
|
38
|
+
profileNoCurrent: '未设置当前 profile。可执行:struggler-cli profile use <name>',
|
|
39
|
+
profileCurrentLabel: '当前 profile:',
|
|
40
|
+
profileAddDone: (name) => `已创建 profile:${name}`,
|
|
41
|
+
profileImportDone: (name) => `已导入 profile:${name}`,
|
|
42
|
+
profileEditHint: '请编辑该文件,填写 accessKey、secretKey、Bucket、zone、domain 等字段。',
|
|
34
43
|
dryRunLabel: '预览',
|
|
35
44
|
dryRunPlan: (action, n) => `[预览] ${action} 计划 (${n} 个文件)`,
|
|
36
45
|
manifestWritten: '清单已写入:',
|
|
37
46
|
},
|
|
38
47
|
options: {
|
|
39
48
|
version: '显示版本号。',
|
|
40
|
-
config: '
|
|
41
|
-
configDir: '指定 config.json、upload-cache.json
|
|
49
|
+
config: '七牛配置:profile 名(如 prod)、文件路径(如 ./command/qiniu.json);不传则优先用 ~/.struggler-cli/current(Windows 为 %USERPROFILE%\\.struggler-cli\\current),否则回退 ./command/qiniu.json。',
|
|
50
|
+
configDir: '指定 config.json、upload-cache.json 所在目录;不传时固定为 ./command(与旧版一致);可写 --config-dir ./command。',
|
|
42
51
|
dir: '指定要上传的目录。',
|
|
43
52
|
dryRun: '仅预览执行计划,不写文件也不调用七牛接口。',
|
|
44
53
|
concurrency: '设置上传并发数。',
|
|
@@ -57,6 +66,12 @@ const LANGUAGES = {
|
|
|
57
66
|
upload: '将指定目录下文件上传到七牛云。',
|
|
58
67
|
refresh: '刷新七牛云 CDN 文件。',
|
|
59
68
|
deploy: '一次执行 init、upload 和 refresh。',
|
|
69
|
+
profile: '管理多套七牛配置(profile)。',
|
|
70
|
+
profileList: '列出所有 profile',
|
|
71
|
+
profileUse: '切换当前 profile',
|
|
72
|
+
profileCurrent: '显示当前 profile',
|
|
73
|
+
profileAdd: '从模版新建 profile',
|
|
74
|
+
profileImport: '从已有文件导入 profile',
|
|
60
75
|
},
|
|
61
76
|
help: {
|
|
62
77
|
helpCommandDescription: '显示指定命令的帮助信息',
|
|
@@ -104,15 +119,24 @@ const LANGUAGES = {
|
|
|
104
119
|
deployUnit: '',
|
|
105
120
|
errorTitle: 'Error',
|
|
106
121
|
errorMissingConfig: 'Incomplete config, the following fields are missing or empty:',
|
|
107
|
-
errorMissingConfigHint: 'Check command/qiniu.json
|
|
122
|
+
errorMissingConfigHint: 'Check Qiniu config (~/.struggler-cli/profiles, Windows: %USERPROFILE%\\.struggler-cli\\profiles) or command/qiniu.json, plus command/config.json; you can also run struggler-cli init first.',
|
|
123
|
+
profileListEmpty: 'No profiles yet. Run: struggler-cli profile add <name>',
|
|
124
|
+
profileListTitle: 'Profiles:',
|
|
125
|
+
profileListCurrentHint: '(* = active)',
|
|
126
|
+
profileUseDone: (name) => `Active profile: ${name}`,
|
|
127
|
+
profileNoCurrent: 'No active profile. Run: struggler-cli profile use <name>',
|
|
128
|
+
profileCurrentLabel: 'Active profile:',
|
|
129
|
+
profileAddDone: (name) => `Created profile: ${name}`,
|
|
130
|
+
profileImportDone: (name) => `Imported profile: ${name}`,
|
|
131
|
+
profileEditHint: 'Edit the file and fill accessKey, secretKey, Bucket, zone, domain.',
|
|
108
132
|
dryRunLabel: 'dry-run',
|
|
109
133
|
dryRunPlan: (action, n) => `[dry-run] ${action} plan (${n} files)`,
|
|
110
134
|
manifestWritten: 'Manifest written:',
|
|
111
135
|
},
|
|
112
136
|
options: {
|
|
113
137
|
version: 'Display the version number.',
|
|
114
|
-
config: '
|
|
115
|
-
configDir: 'Directory for config.json and upload-cache.json;
|
|
138
|
+
config: 'Qiniu config: profile name (e.g. prod) or file path (e.g. ./command/qiniu.json); when omitted, use ~/.struggler-cli/current first (Windows: %USERPROFILE%\\.struggler-cli\\current), then fallback to ./command/qiniu.json.',
|
|
139
|
+
configDir: 'Directory for config.json and upload-cache.json; defaults to ./command when omitted (legacy behavior).',
|
|
116
140
|
dir: 'Specify the directory to upload.',
|
|
117
141
|
dryRun: 'Preview actions without writing files or calling Qiniu APIs.',
|
|
118
142
|
concurrency: 'Set upload concurrency.',
|
|
@@ -131,6 +155,12 @@ const LANGUAGES = {
|
|
|
131
155
|
upload: 'Upload files under the specified directory to Qiniu Cloud.',
|
|
132
156
|
refresh: 'Refresh Qiniu Cloud CDN files.',
|
|
133
157
|
deploy: 'Run init, upload, and refresh in one command.',
|
|
158
|
+
profile: 'Manage multiple Qiniu configs (profiles).',
|
|
159
|
+
profileList: 'List profiles',
|
|
160
|
+
profileUse: 'Set active profile',
|
|
161
|
+
profileCurrent: 'Show active profile',
|
|
162
|
+
profileAdd: 'Create profile from template',
|
|
163
|
+
profileImport: 'Import profile from file',
|
|
134
164
|
},
|
|
135
165
|
help: {
|
|
136
166
|
helpCommandDescription: 'display help for command',
|
package/lib/profile.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { setSyncJsonData, getJsonData } = require('./files');
|
|
5
|
+
|
|
6
|
+
const PROFILE_DIR_NAME = '.struggler-cli';
|
|
7
|
+
const PROFILES_SUBDIR = 'profiles';
|
|
8
|
+
const CURRENT_FILE = 'current';
|
|
9
|
+
const PROFILE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
10
|
+
|
|
11
|
+
function getStoreRoot() {
|
|
12
|
+
const customHome = process.env.STRUGGLER_CLI_HOME;
|
|
13
|
+
if (customHome && customHome.trim()) {
|
|
14
|
+
return path.join(path.resolve(customHome), PROFILE_DIR_NAME);
|
|
15
|
+
}
|
|
16
|
+
return path.join(os.homedir(), PROFILE_DIR_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getProfilesDir() {
|
|
20
|
+
return path.join(getStoreRoot(), PROFILES_SUBDIR);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCurrentFile() {
|
|
24
|
+
return path.join(getStoreRoot(), CURRENT_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertProfileName(name) {
|
|
28
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid profile name "${name}". Use letters, numbers, _ or -, and start with a letter.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getProfilePath(name) {
|
|
36
|
+
assertProfileName(name);
|
|
37
|
+
return path.join(getProfilesDir(), `${name}.json`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureProfilesDir() {
|
|
41
|
+
fs.mkdirSync(getProfilesDir(), { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readCurrentProfile() {
|
|
45
|
+
const currentFile = getCurrentFile();
|
|
46
|
+
if (!fs.existsSync(currentFile)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const name = fs.readFileSync(currentFile, 'utf8').trim();
|
|
50
|
+
return name || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeCurrentProfile(name) {
|
|
54
|
+
assertProfileName(name);
|
|
55
|
+
const profilePath = getProfilePath(name);
|
|
56
|
+
if (!fs.existsSync(profilePath)) {
|
|
57
|
+
throw new Error(`Profile "${name}" does not exist. Run: struggler-cli profile list`);
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(getStoreRoot(), { recursive: true });
|
|
60
|
+
fs.writeFileSync(getCurrentFile(), `${name}\n`, 'utf8');
|
|
61
|
+
return profilePath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function listProfiles() {
|
|
65
|
+
const dir = getProfilesDir();
|
|
66
|
+
if (!fs.existsSync(dir)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return fs.readdirSync(dir)
|
|
70
|
+
.filter((file) => file.endsWith('.json'))
|
|
71
|
+
.map((file) => path.basename(file, '.json'))
|
|
72
|
+
.sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isFileConfigPath(configValue, cwd = process.cwd()) {
|
|
76
|
+
if (!configValue) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (configValue.includes('/') || configValue.includes('\\')) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const resolved = path.resolve(cwd, configValue);
|
|
83
|
+
return fs.existsSync(resolved) && fs.statSync(resolved).isFile();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveProfileConfigPath(options = {}, cwd = process.cwd()) {
|
|
87
|
+
const configValue = options.config;
|
|
88
|
+
|
|
89
|
+
if (!configValue) {
|
|
90
|
+
const current = readCurrentProfile();
|
|
91
|
+
if (current) {
|
|
92
|
+
return getProfilePath(current);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isFileConfigPath(configValue, cwd)) {
|
|
98
|
+
return path.resolve(cwd, configValue);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
assertProfileName(configValue);
|
|
102
|
+
const profilePath = getProfilePath(configValue);
|
|
103
|
+
if (!fs.existsSync(profilePath)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Profile "${configValue}" not found at ${profilePath}. Run: struggler-cli profile list`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return profilePath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function importProfile(name, fromPath, cwd = process.cwd()) {
|
|
112
|
+
assertProfileName(name);
|
|
113
|
+
const source = path.resolve(cwd, fromPath);
|
|
114
|
+
if (!fs.existsSync(source)) {
|
|
115
|
+
throw new Error(`Config file not found: ${source}`);
|
|
116
|
+
}
|
|
117
|
+
ensureProfilesDir();
|
|
118
|
+
const target = getProfilePath(name);
|
|
119
|
+
fs.copyFileSync(source, target);
|
|
120
|
+
return target;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addProfile(name, templatePath) {
|
|
124
|
+
assertProfileName(name);
|
|
125
|
+
ensureProfilesDir();
|
|
126
|
+
const target = getProfilePath(name);
|
|
127
|
+
if (fs.existsSync(target)) {
|
|
128
|
+
throw new Error(`Profile "${name}" already exists.`);
|
|
129
|
+
}
|
|
130
|
+
setSyncJsonData(target, getJsonData(templatePath));
|
|
131
|
+
return target;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
PROFILE_DIR_NAME,
|
|
136
|
+
getStoreRoot,
|
|
137
|
+
getProfilesDir,
|
|
138
|
+
getProfilePath,
|
|
139
|
+
getCurrentFile,
|
|
140
|
+
assertProfileName,
|
|
141
|
+
readCurrentProfile,
|
|
142
|
+
writeCurrentProfile,
|
|
143
|
+
listProfiles,
|
|
144
|
+
isFileConfigPath,
|
|
145
|
+
resolveProfileConfigPath,
|
|
146
|
+
importProfile,
|
|
147
|
+
addProfile,
|
|
148
|
+
};
|
package/package.json
CHANGED
package/test/config.test.js
CHANGED
|
@@ -16,7 +16,7 @@ test('config helpers resolve qiniu and derived config paths together', () => {
|
|
|
16
16
|
);
|
|
17
17
|
assert.equal(
|
|
18
18
|
getConfig(options),
|
|
19
|
-
`${process.cwd()}/
|
|
19
|
+
`${process.cwd()}/command/config.json`
|
|
20
20
|
);
|
|
21
21
|
assert.equal(
|
|
22
22
|
getDir(options),
|
|
@@ -48,6 +48,17 @@ test('config-dir flag without path defaults to ./command', () => {
|
|
|
48
48
|
);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test('custom -c without config-dir keeps meta files in ./command', () => {
|
|
52
|
+
const options = {
|
|
53
|
+
config: './def/qiniu.json',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
assert.equal(
|
|
57
|
+
getConfig(options),
|
|
58
|
+
`${process.cwd()}/command/config.json`
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
51
62
|
test('deploy helpers normalize concurrency and build remote keys', () => {
|
|
52
63
|
assert.equal(normalizeConcurrency('0'), 5);
|
|
53
64
|
assert.equal(normalizeConcurrency('3'), 3);
|
|
@@ -0,0 +1,129 @@
|
|
|
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 {
|
|
7
|
+
addProfile,
|
|
8
|
+
importProfile,
|
|
9
|
+
writeCurrentProfile,
|
|
10
|
+
readCurrentProfile,
|
|
11
|
+
listProfiles,
|
|
12
|
+
resolveProfileConfigPath,
|
|
13
|
+
isFileConfigPath,
|
|
14
|
+
} = require('../lib/profile');
|
|
15
|
+
const { getQiniuConfig } = require('../lib/config');
|
|
16
|
+
|
|
17
|
+
function makeWorkspace() {
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'struggler-cli-profile-'));
|
|
19
|
+
return fs.realpathSync(dir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function withTempProfileHome(workspace, run) {
|
|
23
|
+
const prevHome = process.env.STRUGGLER_CLI_HOME;
|
|
24
|
+
process.env.STRUGGLER_CLI_HOME = path.join(workspace, '.home');
|
|
25
|
+
try {
|
|
26
|
+
return run();
|
|
27
|
+
} finally {
|
|
28
|
+
if (prevHome === undefined) {
|
|
29
|
+
delete process.env.STRUGGLER_CLI_HOME;
|
|
30
|
+
} else {
|
|
31
|
+
process.env.STRUGGLER_CLI_HOME = prevHome;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function samePath(a, b) {
|
|
37
|
+
const norm = (p) => {
|
|
38
|
+
try {
|
|
39
|
+
return fs.realpathSync(p);
|
|
40
|
+
} catch {
|
|
41
|
+
const dir = path.dirname(p);
|
|
42
|
+
const base = path.basename(p);
|
|
43
|
+
try {
|
|
44
|
+
return path.join(fs.realpathSync(dir), base);
|
|
45
|
+
} catch {
|
|
46
|
+
return path.resolve(p);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return norm(a) === norm(b);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('profile paths work on any platform', () => {
|
|
54
|
+
const workspace = makeWorkspace();
|
|
55
|
+
const prev = process.cwd();
|
|
56
|
+
process.chdir(workspace);
|
|
57
|
+
try {
|
|
58
|
+
withTempProfileHome(workspace, () => {
|
|
59
|
+
addProfile('prod', path.resolve(__dirname, '../def/qiniu.json'));
|
|
60
|
+
const profileFile = path.join(workspace, '.home', '.struggler-cli', 'profiles', 'prod.json');
|
|
61
|
+
assert.equal(fs.existsSync(profileFile), true);
|
|
62
|
+
assert.match(profileFile, /profiles[\\/]prod\.json$/);
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
process.chdir(prev);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('resolve current profile when -c is omitted', () => {
|
|
70
|
+
const workspace = makeWorkspace();
|
|
71
|
+
const prev = process.cwd();
|
|
72
|
+
process.chdir(workspace);
|
|
73
|
+
try {
|
|
74
|
+
withTempProfileHome(workspace, () => {
|
|
75
|
+
addProfile('staging', path.resolve(__dirname, '../def/qiniu.json'));
|
|
76
|
+
writeCurrentProfile('staging');
|
|
77
|
+
const resolved = resolveProfileConfigPath({});
|
|
78
|
+
const expected = path.join(workspace, '.home', '.struggler-cli', 'profiles', 'staging.json');
|
|
79
|
+
assert.equal(samePath(resolved, expected), true);
|
|
80
|
+
});
|
|
81
|
+
} finally {
|
|
82
|
+
process.chdir(prev);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('-c prod resolves profile name; -c with path resolves file', () => {
|
|
87
|
+
const workspace = makeWorkspace();
|
|
88
|
+
const prev = process.cwd();
|
|
89
|
+
process.chdir(workspace);
|
|
90
|
+
try {
|
|
91
|
+
withTempProfileHome(workspace, () => {
|
|
92
|
+
importProfile('prod', path.resolve(__dirname, '../def/qiniu.json'));
|
|
93
|
+
const byName = resolveProfileConfigPath({ config: 'prod' });
|
|
94
|
+
assert.equal(
|
|
95
|
+
samePath(byName, path.join(workspace, '.home', '.struggler-cli', 'profiles', 'prod.json')),
|
|
96
|
+
true
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const legacy = path.join(workspace, 'command', 'qiniu.json');
|
|
100
|
+
fs.mkdirSync(path.dirname(legacy), { recursive: true });
|
|
101
|
+
fs.copyFileSync(path.resolve(__dirname, '../def/qiniu.json'), legacy);
|
|
102
|
+
assert.equal(isFileConfigPath('./command/qiniu.json'), true);
|
|
103
|
+
assert.equal(
|
|
104
|
+
samePath(resolveProfileConfigPath({ config: './command/qiniu.json' }), legacy),
|
|
105
|
+
true
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
process.chdir(prev);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('getQiniuConfig falls back to command/qiniu.json without profile', () => {
|
|
114
|
+
const workspace = makeWorkspace();
|
|
115
|
+
const prev = process.cwd();
|
|
116
|
+
process.chdir(workspace);
|
|
117
|
+
try {
|
|
118
|
+
withTempProfileHome(workspace, () => {
|
|
119
|
+
assert.equal(listProfiles().length, 0);
|
|
120
|
+
assert.equal(readCurrentProfile(), null);
|
|
121
|
+
assert.equal(
|
|
122
|
+
samePath(getQiniuConfig({}), path.join(workspace, 'command', 'qiniu.json')),
|
|
123
|
+
true
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
process.chdir(prev);
|
|
128
|
+
}
|
|
129
|
+
});
|