@ultikits/cli 1.2.0 → 2.0.1

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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.1]
4
+
5
+ ### Changed
6
+
7
+ - Release workflow now uses npm staged publishing via trusted publishing; no functional change. A tagged release
8
+ is staged on npm and becomes installable once a maintainer approves it on npmjs.com with 2FA.
9
+
10
+ ## [2.0.0]
11
+
12
+ ### Breaking
13
+
14
+ - **Requires the new UltiCloud backend.** This version only talks to the UltiCloud machine API (`/v1/...`). It is
15
+ published on UltiCloud cutover day, after the production smoke passes. Versions 1.x stop working against
16
+ `https://api.ultikits.com` once the cutover has happened, so upgrade with `npm install -g @ultikits/cli@latest`
17
+ and run `ultikits login` again. Do not upgrade before the cutover: 2.0.0 does not work against the old backend.
18
+
19
+ ### Changed
20
+
21
+ - **UltiCloud developer-client contract 1.1.0.** 对接 UltiCloud 机器客户端契约 1.1.0;识别已撤回版本的新状态,并在上传非 ZIP/JAR 文件时提示检查构建产物路径。
22
+ - **UltiCloud API.** Every command now uses the UltiCloud machine API (`/v1/...`). Log in again: 1.x credentials and
23
+ developer access tokens do not work with it.
24
+ - **Login** uses device authorization in the browser (`ultikits login`) or a `ucd_` developer key
25
+ (`ultikits login --token`, `ULTIKITS_TOKEN`). `ULTIKITS_API_URL` / `--api-url` select the API (https only).
26
+ - **`modules list` / `info`** read your modules through the developer API, all pages.
27
+ - **`versions list`** reads the public catalogue without sending a credential and marks withdrawn versions.
28
+ - **`versions delete`** is now an alias of `versions withdraw` and says that a withdrawal cannot be undone.
29
+
30
+ ### Added
31
+
32
+ - `ultikits modules create`, `modules update`, `modules restore` and `versions withdraw` (need a manage credential).
33
+ - `ultikits modules delete` is a recoverable delete: the CLI prints the deadline (30 days) and the
34
+ `ultikits modules restore <identifier>` command. The web panel has no restore.
35
+ - Destructive commands ask for the identifier (or `<identifier>@<version>`) to be typed again; without a terminal or
36
+ with `-y` they require `--confirm <same string>`, and exit with code 2 before sending anything when it does not match.
37
+ - `ultikits whoami`, `ultikits logout` (revokes a CLI credential on the server).
38
+ - `publish` creates a missing module and resumes a version still waiting for its upload.
39
+
40
+ ### Removed
41
+
42
+ - The pre-UltiCloud API client and its endpoints.
43
+
3
44
  ## 1.0.0 (2026-02-14)
4
45
 
5
46
  Initial release.
package/README.md CHANGED
@@ -10,6 +10,9 @@ 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
@@ -25,25 +28,41 @@ ultikits publish
25
28
 
26
29
  ## Authentication
27
30
 
28
- ### 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)
29
43
 
30
44
  ```bash
31
45
  ultikits login
32
- # Choose "Browser login (magic link)"
33
- # A browser window opens — approve the request
46
+ # Prints a code and opens the panel; approve the request there.
34
47
  ```
35
48
 
36
- ### 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
37
53
 
38
54
  ```bash
39
- ultikits login
40
- # Choose "Access token (from developer portal)"
41
- # 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
42
57
  ```
43
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
+
44
62
  ### CI/CD (environment variable)
45
63
 
46
- 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.
47
66
 
48
67
  ```yaml
49
68
  # GitHub Actions example
@@ -53,10 +72,33 @@ Set the `ULTIKITS_TOKEN` environment variable with your developer access token.
53
72
  run: ultikits publish --yes
54
73
  ```
55
74
 
56
- 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.
57
82
 
58
83
  ## Commands
59
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
+
60
102
  ### `ultikits create`
61
103
 
62
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:
@@ -81,15 +123,15 @@ The generated project compiles immediately with `mvn compile`.
81
123
 
82
124
  ### `ultikits login`
83
125
 
84
- 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).
85
127
 
86
128
  ### `ultikits logout`
87
129
 
88
- Remove stored credentials.
130
+ Delete the stored credential; a CLI credential is also revoked on the server.
89
131
 
90
132
  ### `ultikits whoami`
91
133
 
92
- Check current authentication status.
134
+ Show the account, the credential in use (masked), its kind and scope, and the API.
93
135
 
94
136
  ### `ultikits init`
95
137
 
@@ -106,55 +148,104 @@ Create an `ultikits.json` project config in the current directory. Interactive p
106
148
 
107
149
  ### `ultikits publish [jar-path]`
108
150
 
109
- 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.
110
152
 
111
- If no JAR path is given, automatically finds the built JAR in `./target/` (skips `-sources.jar` and `-javadoc.jar`).
112
-
113
- **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.
114
156
 
115
157
  | Flag | Description |
116
158
  |------|-------------|
117
- | `--id <string>` | Module identify string |
118
- | `--name <string>` | Module display name |
119
- | `--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`) |
120
162
  | `--changelog <text>` | Version changelog |
121
- | `--short-description <text>` | Short description |
122
- | `-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 |
123
166
 
124
167
  **Metadata resolution priority:** CLI flags > `ultikits.json` > `plugin.yml` from JAR.
125
168
 
126
- **Example:**
127
-
128
169
  ```bash
129
- # Auto-detect JAR and metadata from plugin.yml + ultikits.json
130
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.
179
+
180
+ ### `ultikits modules create`
131
181
 
132
- # Explicit JAR path with overrides
133
- ultikits publish target/MyPlugin-1.0.0.jar --version 1.0.1 --changelog "Bug fix"
182
+ ```bash
183
+ ultikits modules create --id my.plugin --name "My Plugin" [--short-description "..."] [--category-id 3]
184
+ ```
185
+
186
+ `--id`, `--name`, `--short-description` and `--category-id` default to `ultikits.json`. Needs manage.
187
+
188
+ ### `ultikits modules update <identifier>`
189
+
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.
196
+
197
+ ### `ultikits modules delete <identifier>`
198
+
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:
134
201
 
135
- # CI/CD mode (no prompts)
136
- ultikits publish --yes
137
202
  ```
203
+ 已删除 my.plugin。2026-12-01T12:00:00.000Z 前可用 ultikits modules restore my.plugin 恢复(网页面板不提供恢复)。
204
+ ```
205
+
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).
138
208
 
139
- ### `ultikits modules list`
209
+ ### `ultikits modules restore <identifier>`
140
210
 
141
- List all your published modules.
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.
142
214
 
143
- ### `ultikits modules info <identifyString>`
215
+ ### `ultikits versions list <identifier>`
144
216
 
145
- Show details of a specific module.
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.
146
219
 
147
- ### `ultikits modules delete <identifyString>`
220
+ ### `ultikits versions withdraw <identifier> <version>`
148
221
 
149
- Delete a module (with confirmation prompt).
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.
150
226
 
151
- ### `ultikits versions list <identifyString>`
227
+ ### Confirming `modules delete` and `versions withdraw`
152
228
 
153
- List all versions of a module.
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.
154
232
 
155
- ### `ultikits versions delete <identifyString> <version>`
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
+ ```
156
238
 
157
- Delete a specific version (with confirmation prompt).
239
+ ## Upgrading from 1.x
240
+
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.
158
249
 
159
250
  ## Project Config (`ultikits.json`)
160
251
 
@@ -190,6 +281,56 @@ npm run build
190
281
  npm test
191
282
  ```
192
283
 
284
+ ## Releasing
285
+
286
+ Releases are staged on npm by `.github/workflows/release.yml` through
287
+ [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) and
288
+ [staged publishing](https://docs.npmjs.com/cli/v11/commands/npm-stage): the workflow's
289
+ GitHub Actions OIDC token is exchanged for a short-lived credential, so no npm token is
290
+ stored in the repository or its secrets. That credential can only stage a version; a
291
+ maintainer makes it public by approving it with 2FA.
292
+
293
+ 1. Open a PR that bumps `version` in `package.json` (and `package-lock.json`, e.g. with
294
+ `npm version X.Y.Z --no-git-tag-version`) and adds the `CHANGELOG.md` entry.
295
+ 2. Merge it into `master` once CI is green.
296
+ 3. Tag the merge commit and push the tag:
297
+
298
+ ```bash
299
+ git checkout master && git pull
300
+ git tag -a vX.Y.Z -m "vX.Y.Z"
301
+ git push origin vX.Y.Z
302
+ ```
303
+
304
+ 4. The **Release** workflow builds, tests, refuses to continue unless the tag minus `v`
305
+ equals `package.json`'s version and that version is not yet on npm, upgrades npm to
306
+ at least 11.15.0, then runs `npm stage publish --access public`. The run's summary
307
+ shows the stage id. It can also be started by hand (Actions → Release → Run workflow)
308
+ with a `version` input that must equal `package.json`'s version.
309
+ 5. Approve the staged version: on npmjs.com open the `@ultikits/cli` package, find the
310
+ staged X.Y.Z, and approve it with your 2FA code (or run `npm stage approve <stage-id>`
311
+ while logged in). Until then the version is not installable and `latest` does not move.
312
+ A wrong build can be discarded with `npm stage reject <stage-id>`.
313
+
314
+ ### One-time npmjs.com setup
315
+
316
+ On npmjs.com, open the `@ultikits/cli` package → **Settings** → **Trusted Publisher**,
317
+ choose **GitHub Actions**, and enter:
318
+
319
+ | Field | Value |
320
+ |-------|-------|
321
+ | Organization or user | `UltiKits` |
322
+ | Repository | `ultikits-cli` |
323
+ | Workflow filename | `release.yml` |
324
+ | Environment name | *(leave empty)* |
325
+ | Allowed command | **stage publish only** (`npm stage publish`; `npm publish` is refused) |
326
+
327
+ After the first approved staged release, npm recommends setting **Settings →
328
+ Publishing access** to "Require two-factor authentication and disallow tokens".
329
+
330
+ Because this repository is private, npm does not attach a provenance attestation to
331
+ releases ("Provenance generation is not supported for private repositories, even when
332
+ publishing public packages"); trusted publishing itself still applies.
333
+
193
334
  ## License
194
335
 
195
336
  [MIT](LICENSE)
@@ -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.2.0');
15
+ .version('2.0.1');
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>;