@ultikits/cli 1.1.0 → 2.0.0
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/CHANGELOG.md +34 -0
- package/README.md +156 -44
- package/dist/bin/ultikits.js +3 -1
- package/dist/src/commands/login.d.ts +13 -0
- package/dist/src/commands/login.js +118 -97
- package/dist/src/commands/logout.d.ts +2 -0
- package/dist/src/commands/logout.js +43 -6
- package/dist/src/commands/modules.d.ts +35 -0
- package/dist/src/commands/modules.js +237 -75
- package/dist/src/commands/publish.d.ts +28 -0
- package/dist/src/commands/publish.js +195 -131
- package/dist/src/commands/versions.d.ts +10 -0
- package/dist/src/commands/versions.js +101 -67
- package/dist/src/commands/whoami.d.ts +6 -0
- package/dist/src/commands/whoami.js +45 -26
- package/dist/src/lib/api-client.d.ts +56 -10
- package/dist/src/lib/api-client.js +147 -61
- package/dist/src/lib/command-errors.d.ts +8 -0
- package/dist/src/lib/command-errors.js +54 -0
- package/dist/src/lib/config.d.ts +41 -4
- package/dist/src/lib/config.js +93 -14
- package/dist/src/lib/confirm.d.ts +22 -0
- package/dist/src/lib/confirm.js +47 -0
- package/dist/src/lib/logger.d.ts +6 -0
- package/dist/src/lib/logger.js +4 -0
- package/dist/src/lib/mask.d.ts +7 -0
- package/dist/src/lib/mask.js +18 -0
- package/dist/src/lib/paging.d.ts +5 -0
- package/dist/src/lib/paging.js +16 -0
- package/dist/src/types.d.ts +143 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0]
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Requires the new UltiCloud backend.** This version only talks to the UltiCloud machine API (`/v1/...`). It is
|
|
8
|
+
published on UltiCloud cutover day, after the production smoke passes. Versions 1.x stop working against
|
|
9
|
+
`https://api.ultikits.com` once the cutover has happened, so upgrade with `npm install -g @ultikits/cli@latest`
|
|
10
|
+
and run `ultikits login` again. Do not upgrade before the cutover: 2.0.0 does not work against the old backend.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **UltiCloud developer-client contract 1.1.0.** 对接 UltiCloud 机器客户端契约 1.1.0;识别已撤回版本的新状态,并在上传非 ZIP/JAR 文件时提示检查构建产物路径。
|
|
15
|
+
- **UltiCloud API.** Every command now uses the UltiCloud machine API (`/v1/...`). Log in again: 1.x credentials and
|
|
16
|
+
developer access tokens do not work with it.
|
|
17
|
+
- **Login** uses device authorization in the browser (`ultikits login`) or a `ucd_` developer key
|
|
18
|
+
(`ultikits login --token`, `ULTIKITS_TOKEN`). `ULTIKITS_API_URL` / `--api-url` select the API (https only).
|
|
19
|
+
- **`modules list` / `info`** read your modules through the developer API, all pages.
|
|
20
|
+
- **`versions list`** reads the public catalogue without sending a credential and marks withdrawn versions.
|
|
21
|
+
- **`versions delete`** is now an alias of `versions withdraw` and says that a withdrawal cannot be undone.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `ultikits modules create`, `modules update`, `modules restore` and `versions withdraw` (need a manage credential).
|
|
26
|
+
- `ultikits modules delete` is a recoverable delete: the CLI prints the deadline (30 days) and the
|
|
27
|
+
`ultikits modules restore <identifier>` command. The web panel has no restore.
|
|
28
|
+
- Destructive commands ask for the identifier (or `<identifier>@<version>`) to be typed again; without a terminal or
|
|
29
|
+
with `-y` they require `--confirm <same string>`, and exit with code 2 before sending anything when it does not match.
|
|
30
|
+
- `ultikits whoami`, `ultikits logout` (revokes a CLI credential on the server).
|
|
31
|
+
- `publish` creates a missing module and resumes a version still waiting for its upload.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- The pre-UltiCloud API client and its endpoints.
|
|
36
|
+
|
|
3
37
|
## 1.0.0 (2026-02-14)
|
|
4
38
|
|
|
5
39
|
Initial release.
|
package/README.md
CHANGED
|
@@ -10,41 +10,59 @@ npm install -g @ultikits/cli
|
|
|
10
10
|
|
|
11
11
|
Requires Node.js 18 or later.
|
|
12
12
|
|
|
13
|
+
> **2.0.0 is a breaking release.** It works only with the new UltiCloud backend (`https://api.ultikits.com` after the
|
|
14
|
+
> cutover). Version 1.x stops working at the cutover; upgrade and run `ultikits login` again. See [CHANGELOG](CHANGELOG.md).
|
|
15
|
+
|
|
13
16
|
## Quick Start
|
|
14
17
|
|
|
15
18
|
```bash
|
|
16
|
-
# 1.
|
|
17
|
-
ultikits
|
|
18
|
-
|
|
19
|
-
# 2. Initialize project config (optional, recommended)
|
|
20
|
-
ultikits init
|
|
19
|
+
# 1. Scaffold a new UltiTools module project
|
|
20
|
+
ultikits create
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# 2. Build and publish to UltiCloud
|
|
23
|
+
cd MyModule
|
|
23
24
|
mvn clean package
|
|
25
|
+
ultikits login
|
|
24
26
|
ultikits publish
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Authentication
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
The CLI talks to the UltiCloud API (`https://api.ultikits.com`). It accepts two kinds of credential:
|
|
32
|
+
|
|
33
|
+
| Credential | Prefix | How you get it | Scope |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| CLI credential | `ucc_` | `ultikits login` (device authorization in the browser) | always **manage** |
|
|
36
|
+
| Developer key | `ucd_` | the developer centre on the panel; `ultikits login --token` or `ULTIKITS_TOKEN` | **upload** (default) or **manage**, chosen when the key is rotated |
|
|
37
|
+
|
|
38
|
+
An **upload** key can list your modules, publish versions and resume uploads. Creating, updating, deleting and
|
|
39
|
+
restoring a module and withdrawing a version need a **manage** credential; with an upload key those commands fail
|
|
40
|
+
with "此操作需要 manage 凭据" (`key_scope_insufficient`) and send nothing else.
|
|
41
|
+
|
|
42
|
+
### Interactive login (device authorization)
|
|
30
43
|
|
|
31
44
|
```bash
|
|
32
45
|
ultikits login
|
|
33
|
-
#
|
|
34
|
-
# A browser window opens — approve the request
|
|
46
|
+
# Prints a code and opens the panel; approve the request there.
|
|
35
47
|
```
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
The CLI credential lasts 90 days, is listed (and can be revoked) on your account's device list, and is revoked
|
|
50
|
+
when you change your password. `ultikits logout` revokes it on the server and deletes the local file.
|
|
51
|
+
|
|
52
|
+
### Developer key
|
|
38
53
|
|
|
39
54
|
```bash
|
|
40
|
-
ultikits login
|
|
41
|
-
|
|
42
|
-
# Paste your access token from panel.ultikits.com/developer
|
|
55
|
+
ultikits login --token # prompts for the key
|
|
56
|
+
ultikits login --token ucd_... # or pass it directly
|
|
43
57
|
```
|
|
44
58
|
|
|
59
|
+
`ultikits logout` only deletes the local file for a developer key; it does not revoke the key, because the same key
|
|
60
|
+
usually also lives in CI.
|
|
61
|
+
|
|
45
62
|
### CI/CD (environment variable)
|
|
46
63
|
|
|
47
|
-
Set
|
|
64
|
+
Set `ULTIKITS_TOKEN` to a developer key; no `login` step is needed. Use an **upload**-scoped key in CI: a leaked
|
|
65
|
+
upload key cannot delete or withdraw anything.
|
|
48
66
|
|
|
49
67
|
```yaml
|
|
50
68
|
# GitHub Actions example
|
|
@@ -54,21 +72,66 @@ Set the `ULTIKITS_TOKEN` environment variable with your developer access token.
|
|
|
54
72
|
run: ultikits publish --yes
|
|
55
73
|
```
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
### Choosing the API
|
|
76
|
+
|
|
77
|
+
`--api-url <url>` (every command) or `ULTIKITS_API_URL` selects the API, for example the staging API
|
|
78
|
+
`https://api-staging.ultikits.com`. Only `https://` URLs are accepted (plain `http://` only for `127.0.0.1` and
|
|
79
|
+
`localhost`). The stored login remembers the API it was made against.
|
|
80
|
+
|
|
81
|
+
Credentials are stored in `~/.ultikits/credentials.json` (readable only by you). `ULTIKITS_TOKEN` wins over the file.
|
|
58
82
|
|
|
59
83
|
## Commands
|
|
60
84
|
|
|
85
|
+
| Command | Credential | What it does |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `ultikits create [dir]` | none | Scaffold a module project |
|
|
88
|
+
| `ultikits login` / `logout` / `whoami` | — | See [Authentication](#authentication) |
|
|
89
|
+
| `ultikits init` | none | Write `ultikits.json` |
|
|
90
|
+
| `ultikits publish [jar]` | upload | Publish a version (creates the module first if it does not exist — that step needs manage) |
|
|
91
|
+
| `ultikits modules list` / `info <id>` | upload | Your modules |
|
|
92
|
+
| `ultikits modules create` | **manage** | Create a module |
|
|
93
|
+
| `ultikits modules update <id>` | **manage** | Change name, short description, description, category |
|
|
94
|
+
| `ultikits modules delete <id>` | **manage** | Delete; **restorable for 30 days** |
|
|
95
|
+
| `ultikits modules restore <id>` | **manage** | Undo a delete within 30 days |
|
|
96
|
+
| `ultikits versions list <id>` | none | Published and withdrawn versions (public catalogue) |
|
|
97
|
+
| `ultikits versions withdraw <id> <version>` | **manage** | Withdraw a version; **irreversible** |
|
|
98
|
+
|
|
99
|
+
Every command takes `--api-url <url>`. Exit codes: `0` success, `1` the request failed (the message says why and
|
|
100
|
+
carries the server's `requestId`), `2` a usage error or a refused confirmation — nothing was sent.
|
|
101
|
+
|
|
102
|
+
### `ultikits create`
|
|
103
|
+
|
|
104
|
+
Scaffold a new UltiTools module project. Interactive prompts collect module name, package name, description, author, and API version, then generate a complete project with:
|
|
105
|
+
|
|
106
|
+
- `pom.xml` — Maven build config with UltiTools-API dependency, test deps, JaCoCo
|
|
107
|
+
- `src/main/java/.../ModuleName.java` — Main class with `@UltiToolsModule` annotation
|
|
108
|
+
- `src/main/resources/plugin.yml` — Plugin descriptor
|
|
109
|
+
- `src/main/resources/lang/en.yml` and `zh.yml` — i18n language files
|
|
110
|
+
- `README.md` — Bilingual project readme
|
|
111
|
+
- `.gitignore` — Standard Java/Maven ignores
|
|
112
|
+
- `.git/` — Initialized git repository
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Create in current directory (prompts for module name)
|
|
116
|
+
ultikits create
|
|
117
|
+
|
|
118
|
+
# Create in a specific directory
|
|
119
|
+
ultikits create my-plugin-dir
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The generated project compiles immediately with `mvn compile`.
|
|
123
|
+
|
|
61
124
|
### `ultikits login`
|
|
62
125
|
|
|
63
|
-
|
|
126
|
+
Device authorization in the browser, or `--token [ucd_...]` to store a developer key. See [Authentication](#authentication).
|
|
64
127
|
|
|
65
128
|
### `ultikits logout`
|
|
66
129
|
|
|
67
|
-
|
|
130
|
+
Delete the stored credential; a CLI credential is also revoked on the server.
|
|
68
131
|
|
|
69
132
|
### `ultikits whoami`
|
|
70
133
|
|
|
71
|
-
|
|
134
|
+
Show the account, the credential in use (masked), its kind and scope, and the API.
|
|
72
135
|
|
|
73
136
|
### `ultikits init`
|
|
74
137
|
|
|
@@ -85,55 +148,104 @@ Create an `ultikits.json` project config in the current directory. Interactive p
|
|
|
85
148
|
|
|
86
149
|
### `ultikits publish [jar-path]`
|
|
87
150
|
|
|
88
|
-
Publish a plugin module JAR to UltiCloud.
|
|
151
|
+
Publish a plugin module JAR to UltiCloud. Uploading is publishing: there is no separate review step in the CLI.
|
|
89
152
|
|
|
90
|
-
If no JAR path is given,
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
If no JAR path is given, the built JAR in `./target/` is used (skips `-sources.jar` and `-javadoc.jar`). If the module
|
|
154
|
+
does not exist yet, `publish` creates it first (needs a manage credential and a name from `--name`, `ultikits.json`
|
|
155
|
+
or `plugin.yml`). If the version already exists and is still waiting for its upload, `publish` resumes the upload.
|
|
93
156
|
|
|
94
157
|
| Flag | Description |
|
|
95
158
|
|------|-------------|
|
|
96
|
-
| `--id <
|
|
97
|
-
| `--name <
|
|
98
|
-
| `--version <
|
|
159
|
+
| `--id <identifier>` | Module identifier |
|
|
160
|
+
| `--name <name>` | Display name (only used when the module has to be created) |
|
|
161
|
+
| `--module-version <version>` | Version (default: `plugin.yml`) |
|
|
99
162
|
| `--changelog <text>` | Version changelog |
|
|
100
|
-
| `--short-description <text>` | Short description |
|
|
101
|
-
|
|
|
163
|
+
| `--short-description <text>` | Short description (only used when the module has to be created) |
|
|
164
|
+
| `--api-url <url>` | API to publish to |
|
|
165
|
+
| `-y, --yes` | Skip prompts |
|
|
102
166
|
|
|
103
167
|
**Metadata resolution priority:** CLI flags > `ultikits.json` > `plugin.yml` from JAR.
|
|
104
168
|
|
|
105
|
-
**Example:**
|
|
106
|
-
|
|
107
169
|
```bash
|
|
108
|
-
# Auto-detect JAR and metadata from plugin.yml + ultikits.json
|
|
109
170
|
ultikits publish
|
|
171
|
+
ultikits publish target/MyPlugin-1.0.0.jar --module-version 1.0.1 --changelog "Bug fix"
|
|
172
|
+
ultikits publish --yes # CI
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `ultikits modules list` / `ultikits modules info <identifier>`
|
|
176
|
+
|
|
177
|
+
List your modules (all pages), or show one of them: name, identifier, short description, category, latest version,
|
|
178
|
+
downloads, last update.
|
|
110
179
|
|
|
111
|
-
|
|
112
|
-
ultikits publish target/MyPlugin-1.0.0.jar --version 1.0.1 --changelog "Bug fix"
|
|
180
|
+
### `ultikits modules create`
|
|
113
181
|
|
|
114
|
-
|
|
115
|
-
ultikits
|
|
182
|
+
```bash
|
|
183
|
+
ultikits modules create --id my.plugin --name "My Plugin" [--short-description "..."] [--category-id 3]
|
|
116
184
|
```
|
|
117
185
|
|
|
118
|
-
|
|
186
|
+
`--id`, `--name`, `--short-description` and `--category-id` default to `ultikits.json`. Needs manage.
|
|
187
|
+
|
|
188
|
+
### `ultikits modules update <identifier>`
|
|
119
189
|
|
|
120
|
-
|
|
190
|
+
```bash
|
|
191
|
+
ultikits modules update my.plugin --name "New Name" --description-file README.md
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Sends only the options given (`--name`, `--short-description`, `--description-file <path>`, `--category-id`); with none
|
|
195
|
+
of them it exits 2 without sending anything. Needs manage.
|
|
121
196
|
|
|
122
|
-
### `ultikits modules
|
|
197
|
+
### `ultikits modules delete <identifier>`
|
|
123
198
|
|
|
124
|
-
|
|
199
|
+
Deletes the module: it disappears from the catalogue, downloads and your module list at once, and is **kept for 30
|
|
200
|
+
days**. On success the CLI prints the deadline and the restore command:
|
|
125
201
|
|
|
126
|
-
|
|
202
|
+
```
|
|
203
|
+
已删除 my.plugin。2026-12-01T12:00:00.000Z 前可用 ultikits modules restore my.plugin 恢复(网页面板不提供恢复)。
|
|
204
|
+
```
|
|
127
205
|
|
|
128
|
-
|
|
206
|
+
The identifier and name stay taken during those 30 days; after that the module is purged for good. The owner's
|
|
207
|
+
e-mail address is notified. Deletions and withdrawals are rate-limited per account (10 per hour together).
|
|
129
208
|
|
|
130
|
-
### `ultikits
|
|
209
|
+
### `ultikits modules restore <identifier>`
|
|
131
210
|
|
|
132
|
-
|
|
211
|
+
Undoes a delete made in the last 30 days. **The web panel has no restore button — the CLI (or the API's
|
|
212
|
+
`POST /v1/developer/modules/<identifier>/restore`) is the only way back.** After 30 days it reports that there is
|
|
213
|
+
nothing to restore.
|
|
214
|
+
|
|
215
|
+
### `ultikits versions list <identifier>`
|
|
216
|
+
|
|
217
|
+
Reads the public catalogue (no credential is sent) and lists published and withdrawn versions; withdrawn rows are
|
|
218
|
+
marked `已撤回 / withdrawn`. Versions still waiting for their upload are not listed.
|
|
219
|
+
|
|
220
|
+
### `ultikits versions withdraw <identifier> <version>`
|
|
221
|
+
|
|
222
|
+
Withdraws a published version. **This cannot be undone**: the version stays as a read-only record, its storage is
|
|
223
|
+
returned to your quota, and that version number and every lower one can never be published again. Already
|
|
224
|
+
downloaded copies are not affected. `ultikits versions delete` is kept as an alias of this command and prints that it
|
|
225
|
+
is an irreversible withdrawal.
|
|
226
|
+
|
|
227
|
+
### Confirming `modules delete` and `versions withdraw`
|
|
228
|
+
|
|
229
|
+
On a terminal the CLI asks you to type the target again: the identifier for a delete, `<identifier>@<version>` for a
|
|
230
|
+
withdrawal. Anything else cancels. Without a terminal, or with `-y`, you must pass `--confirm` with exactly the same
|
|
231
|
+
string (case-sensitive) — `-y` alone is refused. A refused confirmation exits with code 2 before any request is sent.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# CI / scripts
|
|
235
|
+
ultikits modules delete my.plugin --confirm my.plugin
|
|
236
|
+
ultikits versions withdraw my.plugin 1.2.3 -y --confirm my.plugin@1.2.3
|
|
237
|
+
```
|
|
133
238
|
|
|
134
|
-
|
|
239
|
+
## Upgrading from 1.x
|
|
135
240
|
|
|
136
|
-
|
|
241
|
+
- **Log in again.** 1.x credentials (`type: oauth` / `type: access_token` in `credentials.json`) and the old developer
|
|
242
|
+
access tokens do not work with the UltiCloud API. Run `ultikits login`, or put a new `ucd_` developer key into
|
|
243
|
+
`ULTIKITS_TOKEN`.
|
|
244
|
+
- **`versions delete` is now a withdrawal.** It calls `versions withdraw`, which cannot be undone, and it needs
|
|
245
|
+
`--confirm <identifier>@<version>` when not answered on a terminal.
|
|
246
|
+
- **`modules delete` is restorable for 30 days** and needs the identifier typed again (or `--confirm`).
|
|
247
|
+
- **`publish --version` is `--module-version`.**
|
|
248
|
+
- **`versions list`** shows the public catalogue: published and withdrawn versions only.
|
|
137
249
|
|
|
138
250
|
## Project Config (`ultikits.json`)
|
|
139
251
|
|
package/dist/bin/ultikits.js
CHANGED
|
@@ -12,7 +12,7 @@ const program = new Command();
|
|
|
12
12
|
program
|
|
13
13
|
.name('ultikits')
|
|
14
14
|
.description('CLI for publishing UltiTools plugin modules to UltiCloud')
|
|
15
|
-
.version('
|
|
15
|
+
.version('2.0.0');
|
|
16
16
|
registerLoginCommand(program);
|
|
17
17
|
registerLogoutCommand(program);
|
|
18
18
|
registerWhoamiCommand(program);
|
|
@@ -21,5 +21,7 @@ registerPublishCommand(program);
|
|
|
21
21
|
registerModulesCommand(program);
|
|
22
22
|
registerVersionsCommand(program);
|
|
23
23
|
registerCreateCommand(program);
|
|
24
|
+
program.addHelpText('after', '\nDeleted a module by mistake? Run `ultikits modules restore <identifier>` within 30 days\n' +
|
|
25
|
+
'(the web panel has no restore). Withdrawing a version cannot be undone.');
|
|
24
26
|
program.parse();
|
|
25
27
|
//# sourceMappingURL=ultikits.js.map
|
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import type { CommandDeps } from './publish.js';
|
|
3
|
+
export interface LoginOptions {
|
|
4
|
+
apiUrl?: string;
|
|
5
|
+
/** --token <value>, or true for "--token" without a value (prompt). */
|
|
6
|
+
token?: string | boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface LoginDeps extends CommandDeps {
|
|
9
|
+
sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
now?: () => number;
|
|
11
|
+
openBrowser?: (url: string) => Promise<unknown>;
|
|
12
|
+
hostname?: () => string;
|
|
13
|
+
}
|
|
2
14
|
export declare function registerLoginCommand(program: Command): void;
|
|
15
|
+
export declare function runLogin(options: LoginOptions, deps?: LoginDeps): Promise<number>;
|
|
@@ -1,120 +1,141 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import open from 'open';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DEVELOPER_KEY_PATTERN, getConfigDir, resolveApiUrl, writeCredentials } from '../lib/config.js';
|
|
6
|
+
import { ApiClient, ApiError } from '../lib/api-client.js';
|
|
7
|
+
import { maskCredential, redact } from '../lib/mask.js';
|
|
8
|
+
import { consoleIo } from '../lib/logger.js';
|
|
9
|
+
import { describeAuthError } from './whoami.js';
|
|
10
|
+
/** RFC 8628: add 5 seconds to the polling interval after slow_down. */
|
|
11
|
+
const SLOW_DOWN_STEP_SECONDS = 5;
|
|
8
12
|
export function registerLoginCommand(program) {
|
|
9
13
|
program
|
|
10
14
|
.command('login')
|
|
11
|
-
.description('
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
.description('Log in with device authorization (browser), or store a developer key with --token')
|
|
16
|
+
.option('--token [value]', 'Store a developer key (ucd_…) instead; prompts when no value is given')
|
|
17
|
+
.option('--api-url <url>', 'UltiCloud API URL (default: ULTIKITS_API_URL or https://api.ultikits.com)')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
process.exitCode = await runLogin(options);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function runLogin(options, deps = {}) {
|
|
23
|
+
const io = deps.io ?? consoleIo;
|
|
24
|
+
const env = deps.env ?? process.env;
|
|
25
|
+
let apiUrl;
|
|
26
|
+
try {
|
|
27
|
+
apiUrl = resolveApiUrl(options.apiUrl, env);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
io.err(err.message);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
if (options.token !== undefined && options.token !== false) {
|
|
34
|
+
return loginWithDeveloperKey(apiUrl, options.token, deps);
|
|
35
|
+
}
|
|
36
|
+
return loginWithDeviceAuthorization(apiUrl, deps);
|
|
37
|
+
}
|
|
38
|
+
async function loginWithDeviceAuthorization(apiUrl, deps) {
|
|
39
|
+
const io = deps.io ?? consoleIo;
|
|
40
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
41
|
+
const now = deps.now ?? Date.now;
|
|
42
|
+
const openBrowser = deps.openBrowser ?? ((url) => open(url));
|
|
43
|
+
const hostname = (deps.hostname ?? os.hostname)();
|
|
44
|
+
const suggestedName = `ultikits-cli on ${hostname}`.slice(0, 64);
|
|
45
|
+
const client = new ApiClient({ apiUrl });
|
|
46
|
+
let authorization;
|
|
47
|
+
try {
|
|
48
|
+
authorization = await client.deviceAuthorize(suggestedName);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof ApiError && err.code === 'rate_limited') {
|
|
52
|
+
io.err(`请求过于频繁,请 ${err.retryAfterSeconds ?? '稍'} 秒后再运行 ultikits login。`);
|
|
26
53
|
}
|
|
27
54
|
else {
|
|
28
|
-
|
|
55
|
+
io.err(`无法开始登录:${err.message}`);
|
|
29
56
|
}
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
io.out('');
|
|
60
|
+
io.out(` 用户码 / User code: ${authorization.userCode}`);
|
|
61
|
+
io.out(` 在浏览器中确认 / Confirm in your browser: ${authorization.verificationUriComplete}`);
|
|
62
|
+
io.out('');
|
|
35
63
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
await openBrowser(authorization.verificationUriComplete);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// No browser available; the URL is printed above.
|
|
68
|
+
}
|
|
69
|
+
io.out('等待在浏览器中确认… / Waiting for confirmation...');
|
|
70
|
+
let intervalSeconds = authorization.interval;
|
|
71
|
+
const deadline = now() + authorization.expiresIn * 1000;
|
|
72
|
+
for (;;) {
|
|
73
|
+
if (now() + intervalSeconds * 1000 > deadline) {
|
|
74
|
+
io.err('登录请求已过期,请重新运行 ultikits login。');
|
|
75
|
+
return 1;
|
|
41
76
|
}
|
|
42
|
-
|
|
43
|
-
spin.stop();
|
|
44
|
-
console.log();
|
|
45
|
-
log.info(`Opening browser to authenticate...`);
|
|
46
|
-
log.info(`If it doesn't open, visit: ${url}`);
|
|
47
|
-
console.log();
|
|
77
|
+
await sleep(intervalSeconds * 1000);
|
|
48
78
|
try {
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
const result = await client.deviceToken(authorization.deviceCode);
|
|
80
|
+
writeCredentials({ version: 2, kind: 'cli', credential: result.credential, credentialId: result.credentialId, apiUrl }, deps.configDir);
|
|
81
|
+
io.out(`已登录,CLI 凭据 ${maskCredential(result.credential)} 已保存到 ${credentialsPath(deps)}`);
|
|
82
|
+
return 0;
|
|
53
83
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
while (Date.now() < deadline) {
|
|
59
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
60
|
-
const pollRes = await fetch(`${API_URL}/auth/cli/poll?requestId=${requestId}`);
|
|
61
|
-
const pollBody = await pollRes.json();
|
|
62
|
-
if (pollBody.data?.status === 'completed') {
|
|
63
|
-
writeCredentials({
|
|
64
|
-
type: 'oauth',
|
|
65
|
-
access_token: pollBody.data.access_token,
|
|
66
|
-
refresh_token: pollBody.data.refresh_token,
|
|
67
|
-
expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
|
|
68
|
-
api_url: API_URL,
|
|
69
|
-
});
|
|
70
|
-
spin.succeed('Logged in successfully');
|
|
71
|
-
log.info('Credentials saved to ~/.ultikits/credentials.json');
|
|
72
|
-
return;
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (!(err instanceof ApiError)) {
|
|
86
|
+
io.err(`登录失败:${err.message}`);
|
|
87
|
+
return 1;
|
|
73
88
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
switch (err.code) {
|
|
90
|
+
case 'authorization_pending':
|
|
91
|
+
continue;
|
|
92
|
+
case 'slow_down':
|
|
93
|
+
intervalSeconds += SLOW_DOWN_STEP_SECONDS;
|
|
94
|
+
continue;
|
|
95
|
+
case 'rate_limited':
|
|
96
|
+
intervalSeconds = Math.max(intervalSeconds, err.retryAfterSeconds ?? intervalSeconds);
|
|
97
|
+
continue;
|
|
98
|
+
case 'expired_token':
|
|
99
|
+
io.err('登录请求已过期,请重新运行 ultikits login。');
|
|
100
|
+
return 1;
|
|
101
|
+
case 'access_denied':
|
|
102
|
+
io.err('登录已在浏览器中被拒绝。');
|
|
103
|
+
return 1;
|
|
104
|
+
default:
|
|
105
|
+
io.err(`登录失败:${err.message} [${err.code}]`);
|
|
106
|
+
return 1;
|
|
77
107
|
}
|
|
78
108
|
}
|
|
79
|
-
spin.fail('Authentication timed out.');
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
spin.fail('Login failed: ' + err.message);
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
|
-
async function
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
mask: '●',
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
async function loginWithDeveloperKey(apiUrl, token, deps) {
|
|
112
|
+
const io = deps.io ?? consoleIo;
|
|
113
|
+
const prompt = deps.prompt ?? ((q) => inquirer.prompt(q));
|
|
114
|
+
let value = typeof token === 'string' ? token : '';
|
|
115
|
+
if (token === true) {
|
|
116
|
+
const answer = await prompt([
|
|
117
|
+
{ type: 'password', name: 'token', message: 'Paste your developer key (ucd_…):', mask: '●' },
|
|
118
|
+
]);
|
|
119
|
+
value = String(answer.token ?? '');
|
|
120
|
+
}
|
|
121
|
+
value = value.trim();
|
|
122
|
+
if (!DEVELOPER_KEY_PATTERN.test(value)) {
|
|
123
|
+
io.err('这不是开发者密钥(应为 ucd_ 开头、共 47 个字符)。请在开发者中心复制密钥,或直接运行 ultikits login。');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
const client = new ApiClient({ apiUrl, credential: value });
|
|
96
127
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
api_url: API_URL,
|
|
102
|
-
});
|
|
103
|
-
const res = await client.get('/developer/modules');
|
|
104
|
-
if (res.code !== '200') {
|
|
105
|
-
spin.fail('Invalid token. Get yours at panel.ultikits.com/developer');
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
writeCredentials({
|
|
109
|
-
type: 'access_token',
|
|
110
|
-
token,
|
|
111
|
-
api_url: API_URL,
|
|
112
|
-
});
|
|
113
|
-
spin.succeed('Token verified — logged in');
|
|
114
|
-
log.info('Credentials saved to ~/.ultikits/credentials.json');
|
|
128
|
+
const me = await client.whoami();
|
|
129
|
+
writeCredentials({ version: 2, kind: 'developer', credential: value, apiUrl }, deps.configDir);
|
|
130
|
+
io.out(`开发者密钥 ${maskCredential(value)} 已验证(${me.username} / ${me.developerName}),已保存到 ${credentialsPath(deps)}`);
|
|
131
|
+
return 0;
|
|
115
132
|
}
|
|
116
133
|
catch (err) {
|
|
117
|
-
|
|
134
|
+
io.err(redact(describeAuthError(err, apiUrl), value));
|
|
135
|
+
return 1;
|
|
118
136
|
}
|
|
119
137
|
}
|
|
138
|
+
function credentialsPath(deps) {
|
|
139
|
+
return path.join(deps.configDir ?? getConfigDir(), 'credentials.json');
|
|
140
|
+
}
|
|
120
141
|
//# sourceMappingURL=login.js.map
|
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import { deleteCredentials } from '../lib/config.js';
|
|
2
|
-
import
|
|
1
|
+
import { assertAllowedApiUrl, deleteCredentials, readCredentials } from '../lib/config.js';
|
|
2
|
+
import { ApiClient } from '../lib/api-client.js';
|
|
3
|
+
import { consoleIo } from '../lib/logger.js';
|
|
3
4
|
export function registerLogoutCommand(program) {
|
|
4
5
|
program
|
|
5
6
|
.command('logout')
|
|
6
|
-
.description('Remove stored
|
|
7
|
-
.action(() => {
|
|
8
|
-
|
|
9
|
-
log.success('Logged out. Credentials removed.');
|
|
7
|
+
.description('Remove the stored credential (a CLI credential is also revoked on the server)')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
process.exitCode = await runLogout({});
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
+
export async function runLogout(_options, deps = {}) {
|
|
13
|
+
const io = deps.io ?? consoleIo;
|
|
14
|
+
const env = deps.env ?? process.env;
|
|
15
|
+
const state = readCredentials(deps.configDir);
|
|
16
|
+
let code = 0;
|
|
17
|
+
if (state.status === 'none') {
|
|
18
|
+
io.out('未登录,没有要删除的凭据。');
|
|
19
|
+
}
|
|
20
|
+
else if (state.status === 'legacy') {
|
|
21
|
+
deleteCredentials(deps.configDir);
|
|
22
|
+
io.out('已删除旧版凭据文件。');
|
|
23
|
+
}
|
|
24
|
+
else if (state.credential.kind === 'cli') {
|
|
25
|
+
// Revoke at the API that issued the credential, never at an overridden URL.
|
|
26
|
+
try {
|
|
27
|
+
const client = new ApiClient({ apiUrl: assertAllowedApiUrl(state.credential.apiUrl), credential: state.credential.credential });
|
|
28
|
+
await client.revokeCurrentCredential();
|
|
29
|
+
io.out('已在服务器端撤销 CLI 凭据。');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
io.err('本地已删除,服务器端未撤销,可在账号页设备列表撤销。');
|
|
33
|
+
code = 1;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
deleteCredentials(deps.configDir);
|
|
37
|
+
}
|
|
38
|
+
if (code === 0)
|
|
39
|
+
io.out('已登出,本地凭据已删除。');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
deleteCredentials(deps.configDir);
|
|
43
|
+
io.out('已登出,本地凭据已删除。开发者密钥本身仍然有效;如需作废,请在开发者中心轮换密钥。');
|
|
44
|
+
}
|
|
45
|
+
if (env.ULTIKITS_TOKEN)
|
|
46
|
+
io.out('注意:环境变量 ULTIKITS_TOKEN 仍已设置,命令会继续使用它。');
|
|
47
|
+
return code;
|
|
48
|
+
}
|
|
12
49
|
//# sourceMappingURL=logout.js.map
|