@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 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. Log in to your UltiKits developer account
17
- ultikits login
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
- # 3. Build your plugin and publish
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
- ### Interactive login (browser)
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
- # Choose "Browser login (magic link)"
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
- ### Access token (developer portal)
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
- # Choose "Access token (from developer portal)"
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 the `ULTIKITS_TOKEN` environment variable with your developer access token. No `login` step is needed.
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
- Credentials are stored in `~/.ultikits/credentials.json`.
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
- Authenticate with UltiKits. Choose between browser-based magic link or pasting an access token.
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
- Remove stored credentials.
130
+ Delete the stored credential; a CLI credential is also revoked on the server.
68
131
 
69
132
  ### `ultikits whoami`
70
133
 
71
- Check current authentication status.
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, automatically finds the built JAR in `./target/` (skips `-sources.jar` and `-javadoc.jar`).
91
-
92
- **Options:**
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 <string>` | Module identify string |
97
- | `--name <string>` | Module display name |
98
- | `--version <string>` | Module 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
- | `-y, --yes` | Skip confirmation prompts |
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
- # Explicit JAR path with overrides
112
- ultikits publish target/MyPlugin-1.0.0.jar --version 1.0.1 --changelog "Bug fix"
180
+ ### `ultikits modules create`
113
181
 
114
- # CI/CD mode (no prompts)
115
- ultikits publish --yes
182
+ ```bash
183
+ ultikits modules create --id my.plugin --name "My Plugin" [--short-description "..."] [--category-id 3]
116
184
  ```
117
185
 
118
- ### `ultikits modules list`
186
+ `--id`, `--name`, `--short-description` and `--category-id` default to `ultikits.json`. Needs manage.
187
+
188
+ ### `ultikits modules update <identifier>`
119
189
 
120
- List all your published modules.
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 info <identifyString>`
197
+ ### `ultikits modules delete <identifier>`
123
198
 
124
- Show details of a specific module.
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
- ### `ultikits modules delete <identifyString>`
202
+ ```
203
+ 已删除 my.plugin。2026-12-01T12:00:00.000Z 前可用 ultikits modules restore my.plugin 恢复(网页面板不提供恢复)。
204
+ ```
127
205
 
128
- Delete a module (with confirmation prompt).
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 versions list <identifyString>`
209
+ ### `ultikits modules restore <identifier>`
131
210
 
132
- List all versions of a module.
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
- ### `ultikits versions delete <identifyString> <version>`
239
+ ## Upgrading from 1.x
135
240
 
136
- Delete a specific version (with confirmation prompt).
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
 
@@ -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('1.1.0');
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 { writeCredentials } from '../lib/config.js';
4
- import { ApiClient } from '../lib/api-client.js';
5
- import * as log from '../lib/logger.js';
6
- const POLL_INTERVAL_MS = 2000;
7
- const API_URL = 'https://api.ultikits.com';
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('Authenticate with UltiKits')
12
- .action(async () => {
13
- const { method } = await inquirer.prompt([
14
- {
15
- type: 'list',
16
- name: 'method',
17
- message: 'How would you like to authenticate?',
18
- choices: [
19
- { name: 'Browser login (magic link)', value: 'magic-link' },
20
- { name: 'Access token (from developer portal)', value: 'access-token' },
21
- ],
22
- },
23
- ]);
24
- if (method === 'magic-link') {
25
- await loginWithMagicLink();
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
- await loginWithAccessToken();
55
+ io.err(`无法开始登录:${err.message}`);
29
56
  }
30
- });
31
- }
32
- async function loginWithMagicLink() {
33
- const spin = log.spinner('Requesting authentication...');
34
- spin.start();
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
- const response = await fetch(`${API_URL}/auth/cli/request`, { method: 'POST' });
37
- const body = await response.json();
38
- if (body.code !== '200') {
39
- spin.fail('Failed to create auth request: ' + body.msg);
40
- return;
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
- const { requestId, code, url } = body.data;
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 open(url);
50
- }
51
- catch {
52
- // Browser didn't open — user has the URL
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
- spin.text = 'Waiting for authentication...';
55
- spin.start();
56
- // Poll for completion
57
- const deadline = Date.now() + 10 * 60 * 1000; // 10 minute timeout
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
- if (pollBody.data?.status === 'expired') {
75
- spin.fail('Authentication request expired. Run `ultikits login` again.');
76
- return;
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 loginWithAccessToken() {
86
- const { token } = await inquirer.prompt([
87
- {
88
- type: 'password',
89
- name: 'token',
90
- message: 'Paste your access token:',
91
- mask: '●',
92
- },
93
- ]);
94
- const spin = log.spinner('Validating token...');
95
- spin.start();
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
- // Validate by trying to list developer modules
98
- const client = new ApiClient({
99
- type: 'access_token',
100
- token,
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
- spin.fail('Token validation failed: ' + err.message);
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,2 +1,4 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandDeps } from './publish.js';
2
3
  export declare function registerLogoutCommand(program: Command): void;
4
+ export declare function runLogout(_options: Record<string, never>, deps?: CommandDeps): Promise<number>;
@@ -1,12 +1,49 @@
1
- import { deleteCredentials } from '../lib/config.js';
2
- import * as log from '../lib/logger.js';
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 credentials')
7
- .action(() => {
8
- deleteCredentials();
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