@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.
@@ -1,5 +1,9 @@
1
1
  import { Command } from 'commander';
2
+ import { ApiError } from '../lib/api-client.js';
3
+ import { type Io } from '../lib/logger.js';
2
4
  import type { ModuleMetadata, ProjectConfig, PluginYml } from '../types.js';
5
+ /** Upload limit from the contract (versionsCreate.sizeBytes maximum). */
6
+ export declare const MAX_JAR_BYTES = 52428800;
3
7
  interface PublishFlags {
4
8
  id?: string;
5
9
  name?: string;
@@ -7,6 +11,30 @@ interface PublishFlags {
7
11
  changelog?: string;
8
12
  shortDescription?: string;
9
13
  }
14
+ export interface PublishOptions extends PublishFlags {
15
+ yes?: boolean;
16
+ apiUrl?: string;
17
+ }
18
+ export interface CommandDeps {
19
+ env?: NodeJS.ProcessEnv;
20
+ configDir?: string;
21
+ cwd?: string;
22
+ io?: Io;
23
+ prompt?: (questions: any[]) => Promise<Record<string, any>>;
24
+ }
10
25
  export declare function resolveMetadata(flags: PublishFlags, projectConfig: ProjectConfig | null, pluginYml: PluginYml | null): ModuleMetadata;
11
26
  export declare function registerPublishCommand(program: Command): void;
27
+ export declare function runPublish(jarPath: string | undefined, options: PublishOptions, deps?: CommandDeps): Promise<number>;
28
+ export interface PublishErrorContext {
29
+ identifier: string;
30
+ version: string;
31
+ apiUrl: string;
32
+ phase?: 'publish' | 'createModule';
33
+ }
34
+ /**
35
+ * One message per error code of the developer-client contract (keys must be codes
36
+ * that appear in tests/contract/.../schemas; DEVELOPER-CLIENT.md "错误码与客户端动作").
37
+ */
38
+ export declare const PUBLISH_ERROR_MESSAGES: Record<string, (err: ApiError, ctx: PublishErrorContext) => string>;
39
+ export declare function publishErrorMessage(err: unknown, ctx: PublishErrorContext): string;
12
40
  export {};
@@ -1,11 +1,14 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
3
  import inquirer from 'inquirer';
4
- import { readCredentials } from '../lib/config.js';
4
+ import { resolveAuth, developerCentreUrl } from '../lib/config.js';
5
5
  import { readProjectConfig } from '../lib/project-config.js';
6
6
  import { readPluginYml, findJarInTarget } from '../lib/jar-reader.js';
7
- import { ApiClient } from '../lib/api-client.js';
8
- import * as log from '../lib/logger.js';
7
+ import { ApiClient, ApiError } from '../lib/api-client.js';
8
+ import { redact } from '../lib/mask.js';
9
+ import { consoleIo } from '../lib/logger.js';
10
+ /** Upload limit from the contract (versionsCreate.sizeBytes maximum). */
11
+ export const MAX_JAR_BYTES = 52428800;
9
12
  export function resolveMetadata(flags, projectConfig, pluginYml) {
10
13
  const identifyString = flags.id ?? projectConfig?.identifyString;
11
14
  const name = flags.name ?? projectConfig?.name ?? pluginYml?.name;
@@ -16,16 +19,14 @@ export function resolveMetadata(flags, projectConfig, pluginYml) {
16
19
  const missing = [];
17
20
  if (!identifyString)
18
21
  missing.push('identifyString (use --id or ultikits.json)');
19
- if (!name)
20
- missing.push('name (use --name, ultikits.json, or plugin.yml)');
21
22
  if (!version)
22
- missing.push('version (use --version or plugin.yml in JAR)');
23
+ missing.push('version (use --module-version or plugin.yml in JAR)');
23
24
  if (missing.length > 0) {
24
25
  throw new Error('Missing required fields:\n - ' + missing.join('\n - '));
25
26
  }
26
27
  return {
27
28
  identifyString: identifyString,
28
- name: name,
29
+ name: name || undefined,
29
30
  version: version,
30
31
  shortDescription,
31
32
  changelog,
@@ -35,137 +36,200 @@ export function resolveMetadata(flags, projectConfig, pluginYml) {
35
36
  export function registerPublishCommand(program) {
36
37
  program
37
38
  .command('publish [jar-path]')
38
- .description('Publish a plugin module to UltiCloud')
39
- .option('--id <identifyString>', 'Module identify string')
40
- .option('--name <name>', 'Module display name')
41
- .option('--module-version <version>', 'Module version')
39
+ .description('Publish a plugin module version to UltiCloud')
40
+ .option('--id <identifyString>', 'Module identifier')
41
+ .option('--name <name>', 'Module display name (used only when the module has to be created)')
42
+ .option('--module-version <version>', 'Module version (default: plugin.yml version)')
42
43
  .option('--changelog <text>', 'Version changelog')
43
- .option('--short-description <text>', 'Short description')
44
+ .option('--short-description <text>', 'Short description (used only when the module has to be created)')
45
+ .option('--api-url <url>', 'UltiCloud API URL (default: ULTIKITS_API_URL, stored login, or https://api.ultikits.com)')
44
46
  .option('-y, --yes', 'Skip confirmation prompts')
45
47
  .action(async (jarPath, options) => {
46
- // 1. Resolve auth
47
- const credentials = readCredentials();
48
- const client = ApiClient.fromEnvOrCredentials(credentials);
49
- if (!client) {
50
- log.error('Not logged in. Run `ultikits login` or set ULTIKITS_TOKEN.');
51
- process.exit(1);
52
- }
53
- // 2. Resolve JAR path
54
- const resolvedJarPath = jarPath ?? findJarInTarget();
55
- if (!resolvedJarPath) {
56
- log.error('No JAR file specified and none found in ./target/');
57
- log.info('Usage: ultikits publish <path-to-jar>');
58
- process.exit(1);
59
- }
60
- if (!fs.existsSync(resolvedJarPath)) {
61
- log.error(`File not found: ${resolvedJarPath}`);
62
- process.exit(1);
63
- }
64
- // 3. Read plugin.yml from JAR
65
- const pluginYml = await readPluginYml(resolvedJarPath);
66
- if (pluginYml) {
67
- log.success(`Read plugin.yml from JAR: name=${pluginYml.name}, version=${pluginYml.version}`);
68
- }
69
- // 4. Read project config
70
- const projectConfig = readProjectConfig();
71
- if (projectConfig) {
72
- log.success(`Using config from ultikits.json: id=${projectConfig.identifyString}`);
73
- }
74
- // 5. Resolve metadata
75
- let metadata;
76
- try {
77
- metadata = resolveMetadata(options, projectConfig, pluginYml);
78
- }
79
- catch (err) {
80
- log.error(err.message);
81
- process.exit(1);
82
- }
83
- // 6. Show summary
84
- const fileSize = fs.statSync(resolvedJarPath).size;
85
- console.log();
86
- console.log(` Module: ${metadata.name}`);
87
- console.log(` ID: ${metadata.identifyString}`);
88
- console.log(` Version: ${metadata.version}`);
89
- console.log(` Size: ${(fileSize / 1024).toFixed(0)} KB`);
90
- console.log();
91
- // 7. Ask for changelog if not provided
92
- if (!metadata.changelog && !options.yes) {
93
- const { changelog } = await inquirer.prompt([
94
- {
95
- type: 'input',
96
- name: 'changelog',
97
- message: 'Changelog (optional):',
98
- },
99
- ]);
48
+ process.exitCode = await runPublish(jarPath, options);
49
+ });
50
+ }
51
+ export async function runPublish(jarPath, options, deps = {}) {
52
+ const io = deps.io ?? consoleIo;
53
+ const env = deps.env ?? process.env;
54
+ const cwd = deps.cwd ?? process.cwd();
55
+ const prompt = deps.prompt ?? ((q) => inquirer.prompt(q));
56
+ // 1. Credential and API origin; nothing is sent before both are valid.
57
+ const auth = resolveAuth({ apiUrl: options.apiUrl, env, configDir: deps.configDir });
58
+ if (!auth.ok) {
59
+ io.err(auth.message);
60
+ return 1;
61
+ }
62
+ // 2. JAR: read once; the same bytes are hashed, measured and uploaded.
63
+ const resolvedJarPath = jarPath ?? findJarInTarget(cwd);
64
+ if (!resolvedJarPath) {
65
+ io.err('No JAR file specified and none found in ./target/ (usage: ultikits publish <path-to-jar>)');
66
+ return 1;
67
+ }
68
+ let bytes;
69
+ try {
70
+ bytes = fs.readFileSync(resolvedJarPath);
71
+ }
72
+ catch {
73
+ io.err(`File not found: ${resolvedJarPath}`);
74
+ return 1;
75
+ }
76
+ if (bytes.length > MAX_JAR_BYTES) {
77
+ io.err(`JAR 大小 ${bytes.length} 字节超过上限 ${MAX_JAR_BYTES} 字节(50 MiB),未发送任何请求。`);
78
+ return 1;
79
+ }
80
+ const pluginYml = await readPluginYml(resolvedJarPath);
81
+ const projectConfig = readProjectConfig(cwd);
82
+ let metadata;
83
+ try {
84
+ metadata = resolveMetadata(options, projectConfig, pluginYml);
85
+ }
86
+ catch (err) {
87
+ io.err(err.message);
88
+ return 1;
89
+ }
90
+ const sha256 = createHash('sha256').update(bytes).digest('hex');
91
+ io.out(` Module: ${metadata.identifyString}`);
92
+ io.out(` Version: ${metadata.version}`);
93
+ io.out(` Size: ${(bytes.length / 1024).toFixed(0)} KB`);
94
+ io.out(` API: ${auth.apiUrl}`);
95
+ if (!options.yes) {
96
+ if (metadata.changelog === undefined) {
97
+ const { changelog } = await prompt([{ type: 'input', name: 'changelog', message: 'Changelog (optional):' }]);
100
98
  if (changelog)
101
99
  metadata.changelog = changelog;
102
100
  }
103
- // 8. Confirm
104
- if (!options.yes) {
105
- const { confirm } = await inquirer.prompt([
106
- {
107
- type: 'confirm',
108
- name: 'confirm',
109
- message: `Publish ${metadata.identifyString}@${metadata.version}?`,
110
- default: true,
111
- },
112
- ]);
113
- if (!confirm)
114
- return;
101
+ const { confirm } = await prompt([
102
+ { type: 'confirm', name: 'confirm', message: `Publish ${metadata.identifyString}@${metadata.version}?`, default: true },
103
+ ]);
104
+ if (!confirm)
105
+ return 1;
106
+ }
107
+ const client = new ApiClient({ apiUrl: auth.apiUrl, credential: auth.credential });
108
+ const ref = metadata.identifyString;
109
+ const label = `${ref}@${metadata.version}`;
110
+ const ctx = { identifier: ref, version: metadata.version, apiUrl: auth.apiUrl };
111
+ const versionBody = {
112
+ version: metadata.version,
113
+ changelog: metadata.changelog ?? null,
114
+ sha256,
115
+ sizeBytes: bytes.length,
116
+ };
117
+ try {
118
+ let ticket;
119
+ try {
120
+ ticket = await declareVersion(client, ref, versionBody, io, label);
115
121
  }
116
- // 9. Check if module exists
117
- const spin = log.spinner('Checking module...');
118
- spin.start();
119
- const pluginRes = await client.get(`/plugin/get?identifyString=${metadata.identifyString}`);
120
- let moduleId;
121
- if (pluginRes.code !== '200' || !pluginRes.data) {
122
- // Module doesn't exist — create it
123
- spin.text = 'Creating module...';
124
- const createRes = await client.post('/developer/modules', {
125
- name: metadata.name,
126
- identifyString: metadata.identifyString,
127
- shortDescription: metadata.shortDescription ?? '',
128
- categoryId: metadata.categoryId,
129
- });
130
- if (createRes.code !== '200') {
131
- spin.fail('Failed to create module: ' + createRes.msg);
132
- process.exit(1);
122
+ catch (err) {
123
+ if (!(err instanceof ApiError && err.code === 'not_found'))
124
+ throw err;
125
+ // The module does not exist yet: create it (needs a manage credential), then retry once.
126
+ const name = metadata.name;
127
+ if (!name) {
128
+ io.err(`模块 ${ref} 不存在,且取不到模块名称:请用 --name 指定名称(或写进 ultikits.json)后重试。`);
129
+ return 1;
133
130
  }
134
- moduleId = createRes.data.id;
135
- spin.succeed('Module created');
136
- }
137
- else {
138
- moduleId = pluginRes.data.id;
139
- spin.succeed('Module found');
140
- }
141
- // 10. Create version
142
- const versionSpin = log.spinner(`Creating version ${metadata.version}...`);
143
- versionSpin.start();
144
- const versionRes = await client.post(`/developer/modules/${moduleId}/versions`, {
145
- version: metadata.version,
146
- changelog: metadata.changelog,
147
- });
148
- if (versionRes.code !== '200') {
149
- versionSpin.fail('Failed to create version: ' + versionRes.msg);
150
- process.exit(1);
151
- }
152
- const { uploadToken } = versionRes.data;
153
- versionSpin.succeed('Upload token acquired');
154
- // 11. Upload JAR
155
- const uploadSpin = log.spinner('Uploading JAR...');
156
- uploadSpin.start();
157
- const fileBuffer = fs.readFileSync(resolvedJarPath);
158
- const blob = new Blob([fileBuffer]);
159
- const formData = new FormData();
160
- formData.append('file', blob, path.basename(resolvedJarPath));
161
- const uploadRes = await client.postMultipart(`/developer/modules/${moduleId}/upload`, formData, { 'X-Upload-Token': uploadToken });
162
- if (uploadRes.code !== '200') {
163
- uploadSpin.fail('Upload failed: ' + uploadRes.msg);
164
- process.exit(1);
131
+ io.out(`模块 ${ref} 不存在,正在创建…`);
132
+ try {
133
+ await client.createModule({
134
+ identifier: ref,
135
+ name,
136
+ shortDescription: (metadata.shortDescription ?? '').slice(0, 200),
137
+ categoryId: metadata.categoryId ?? null,
138
+ description: null,
139
+ compatibleApiVersions: null,
140
+ tags: [],
141
+ });
142
+ }
143
+ catch (createErr) {
144
+ throw new PhaseError('createModule', createErr);
145
+ }
146
+ io.out(`模块 ${ref} 已创建。`);
147
+ ticket = await declareVersion(client, ref, versionBody, io, label);
165
148
  }
166
- uploadSpin.succeed(`Published ${metadata.identifyString}@${metadata.version}`);
167
- console.log();
168
- log.info(`https://panel.ultikits.com/modules/${metadata.identifyString}`);
169
- });
149
+ io.out(`正在上传 ${bytes.length} 字节…`);
150
+ await client.uploadJar(ticket, bytes);
151
+ }
152
+ catch (err) {
153
+ const phase = err instanceof PhaseError ? err.phase : 'publish';
154
+ const cause = err instanceof PhaseError ? err.cause : err;
155
+ io.err(redact(publishErrorMessage(cause, { ...ctx, phase }), auth.credential));
156
+ return 1;
157
+ }
158
+ io.out(`已发布 ${label}`);
159
+ const centre = developerCentreUrl(auth.apiUrl);
160
+ if (centre)
161
+ io.out(centre);
162
+ return 0;
163
+ }
164
+ /** Creates the version, or re-signs the ticket of a version still waiting for its upload. */
165
+ async function declareVersion(client, ref, body, io, label) {
166
+ try {
167
+ const created = await client.createVersion(ref, body);
168
+ io.out(`已创建版本 ${label}。`);
169
+ return created.upload;
170
+ }
171
+ catch (err) {
172
+ if (!(err instanceof ApiError && err.code === 'version_exists'))
173
+ throw err;
174
+ io.out(`版本 ${label} 已存在且等待上传,正在重签上传票据续传…`);
175
+ return client.requestUploadToken(ref, body.version);
176
+ }
177
+ }
178
+ class PhaseError extends Error {
179
+ phase;
180
+ cause;
181
+ constructor(phase, cause) {
182
+ super('phase error');
183
+ this.phase = phase;
184
+ this.cause = cause;
185
+ }
186
+ }
187
+ function centreSuffix(apiUrl) {
188
+ const centre = developerCentreUrl(apiUrl);
189
+ return centre ? `:${centre}` : '。';
190
+ }
191
+ function fieldList(err) {
192
+ const fields = err.details?.fields;
193
+ if (!fields || typeof fields !== 'object')
194
+ return '';
195
+ return Object.entries(fields)
196
+ .map(([field, reason]) => `${field}: ${String(reason)}`)
197
+ .join(';');
198
+ }
199
+ /**
200
+ * One message per error code of the developer-client contract (keys must be codes
201
+ * that appear in tests/contract/.../schemas; DEVELOPER-CLIENT.md "错误码与客户端动作").
202
+ */
203
+ export const PUBLISH_ERROR_MESSAGES = {
204
+ invalid_credential: () => '凭据无效或已过期,请重新 ultikits login(或更换 ULTIKITS_TOKEN)。',
205
+ role_required: (_e, c) => `该账号还不是开发者,请先去开发者中心申请${centreSuffix(c.apiUrl)}`,
206
+ not_found: (_e, c) => `模块 ${c.identifier} 或版本 ${c.version} 不存在,请检查标识符${centreSuffix(c.apiUrl)}`,
207
+ version_exists: (_e, c) => `${c.identifier}@${c.version} 已存在,无法续传。`,
208
+ version_not_greater: (_e, c) => `版本号 ${c.version} 不大于 ${c.identifier} 已有的最新版本,请提高版本号。`,
209
+ version_not_pending: (_e, c) => `${c.identifier}@${c.version} 已发布,请升版本号。`,
210
+ version_format_invalid: (_e, c) => `版本号 ${c.version} 格式不对,应为 X.Y.Z(可带预发布后缀)。`,
211
+ storage_quota_exceeded: () => '存储配额已用完:请撤回旧版本或联系支持。',
212
+ payload_too_large: () => '请求体过大,服务器拒绝。',
213
+ validation_failed: (e, c) => `${c.phase === 'createModule' ? '建模块的资料' : '请求参数'}不合格${fieldList(e) ? `:${fieldList(e)}` : '。'}`,
214
+ upload_token_expired: () => '上传票据已过期:请重新运行同一条 ultikits publish(版本仍在等待上传时会自动重签票据续传)。',
215
+ upload_too_large: () => '文件超过声明的大小或 50 MiB 上限:请检查构建产物。',
216
+ upload_invalid_file_type: () => '服务器只接受 application/java-archive(JAR)文件。',
217
+ upload_checksum_mismatch: () => '服务器收到的文件 SHA-256 与声明不一致(文件可能在上传过程中被改动):请确认构建产物后重新发布。',
218
+ upload_jar_invalid: () => '上传的文件不是有效的 ZIP/JAR:检查构建产物路径。',
219
+ upload_api_level_missing: () => 'JAR 里的 plugin.yml 需要整数 api-version:补上后重新构建再发布。',
220
+ rate_limited: (e) => `请求过于频繁,请 ${e.retryAfterSeconds ?? '稍'} 秒后再试(不会自动重试)。`,
221
+ key_scope_insufficient: (_e, c) => `模块 ${c.identifier} 不存在;建模块需要 manage 凭据(ultikits login,或把开发者密钥轮换为管理作用域),也可以先在开发者中心建${centreSuffix(c.apiUrl)}`,
222
+ module_identifier_taken: (_e, c) => `建模块失败:标识符 ${c.identifier} 已被占用。若这是你 30 天内删除的模块,可用 ultikits modules restore ${c.identifier} 恢复。`,
223
+ module_name_taken: (_e, c) => `建模块失败:模块名称已被占用。若这是你 30 天内删除的模块,可用 ultikits modules restore ${c.identifier} 恢复;否则用 --name 换一个名称。`,
224
+ };
225
+ export function publishErrorMessage(err, ctx) {
226
+ if (err instanceof ApiError) {
227
+ const describe = PUBLISH_ERROR_MESSAGES[err.code];
228
+ const id = err.requestId ? ` (requestId ${err.requestId})` : '';
229
+ if (describe)
230
+ return `${describe(err, ctx)} [${err.code}]${id}`;
231
+ return `发布 ${ctx.identifier}@${ctx.version} 失败:${err.message} [${err.code}]${id}`;
232
+ }
233
+ return `发布 ${ctx.identifier}@${ctx.version} 失败:${err?.message ?? String(err)}`;
170
234
  }
171
235
  //# sourceMappingURL=publish.js.map
@@ -1,2 +1,12 @@
1
1
  import { Command } from 'commander';
2
+ import { type DestructiveOptions, type ManageDeps } from './modules.js';
2
3
  export declare function registerVersionsCommand(program: Command): void;
4
+ /**
5
+ * The API that authenticated commands would use (same rule as resolveAuth): with
6
+ * ULTIKITS_TOKEN set, the stored login's API is ignored. Never reads the credential value.
7
+ */
8
+ export declare function publicApiUrl(flag: string | undefined, env: NodeJS.ProcessEnv, configDir?: string): string;
9
+ export declare function runVersionsList(identifier: string, options: {
10
+ apiUrl?: string;
11
+ }, deps?: ManageDeps): Promise<number>;
12
+ export declare function runVersionsWithdraw(identifier: string, version: string, options: DestructiveOptions, deps?: ManageDeps): Promise<number>;
@@ -1,76 +1,110 @@
1
- import inquirer from 'inquirer';
2
- import { readCredentials } from '../lib/config.js';
1
+ import { resolveApiUrl, readCredentials, resolveAuth } from '../lib/config.js';
3
2
  import { ApiClient } from '../lib/api-client.js';
4
- import * as log from '../lib/logger.js';
3
+ import { redact } from '../lib/mask.js';
4
+ import { consoleIo } from '../lib/logger.js';
5
+ import { fetchAllPages, MAX_PAGE_SIZE } from '../lib/paging.js';
6
+ import { isInteractiveTerminal, requireConfirmation } from '../lib/confirm.js';
7
+ import { describeCommandError } from '../lib/command-errors.js';
8
+ import { EXIT_USAGE } from './modules.js';
9
+ const API_URL_HELP = 'UltiCloud API URL (default: ULTIKITS_API_URL, stored login, or https://api.ultikits.com)';
5
10
  export function registerVersionsCommand(program) {
6
- const versions = program
7
- .command('versions')
8
- .description('Manage module versions');
11
+ const versions = program.command('versions').description('List and withdraw module versions');
9
12
  versions
10
- .command('list <identifyString>')
11
- .description('List versions of a module')
12
- .action(async (identifyString) => {
13
- const client = getClient();
14
- // Get module ID
15
- const infoRes = await client.get(`/plugin/get?identifyString=${identifyString}`);
16
- if (infoRes.code !== '200' || !infoRes.data) {
17
- log.error('Module not found: ' + identifyString);
18
- process.exit(1);
19
- }
20
- const res = await client.get(`/plugin/${infoRes.data.id}/versions`);
21
- if (res.code !== '200') {
22
- log.error('Failed to list versions: ' + res.msg);
23
- process.exit(1);
24
- }
25
- const versionsList = res.data;
26
- if (versionsList.length === 0) {
27
- log.info('No versions published yet.');
28
- return;
29
- }
30
- console.log();
31
- for (const v of versionsList) {
32
- const latest = v.is_latest ? ' (latest)' : '';
33
- const size = v.file_size ? ` ${(v.file_size / 1024).toFixed(0)} KB` : '';
34
- console.log(` ${v.version}${latest}${size} — ${v.created_at}`);
35
- }
36
- console.log();
37
- log.info(`${versionsList.length} version(s)`);
13
+ .command('list <identifier>')
14
+ .description('List the published and withdrawn versions of a module (public catalogue, no credential sent)')
15
+ .option('--api-url <url>', API_URL_HELP)
16
+ .action(async (identifier, options) => {
17
+ process.exitCode = await runVersionsList(identifier, options);
38
18
  });
39
- versions
40
- .command('delete <identifyString> <version>')
41
- .description('Delete a specific version')
42
- .action(async (identifyString, version) => {
43
- const client = getClient();
44
- const infoRes = await client.get(`/plugin/get?identifyString=${identifyString}`);
45
- if (infoRes.code !== '200' || !infoRes.data) {
46
- log.error('Module not found: ' + identifyString);
47
- process.exit(1);
48
- }
49
- const { confirm } = await inquirer.prompt([
50
- {
51
- type: 'confirm',
52
- name: 'confirm',
53
- message: `Delete version ${version} of "${identifyString}"?`,
54
- default: false,
55
- },
56
- ]);
57
- if (!confirm)
58
- return;
59
- const res = await client.delete(`/developer/modules/${infoRes.data.id}/versions/${version}`);
60
- if (res.code !== '200') {
61
- log.error('Failed to delete version: ' + res.msg);
62
- process.exit(1);
63
- }
64
- log.success(`Deleted version ${version}`);
19
+ const withdrawDescription = 'Withdraw a published version. Irreversible: this version and lower ones can never be published again ' +
20
+ '(needs a manage credential)';
21
+ const addWithdrawOptions = (cmd) => cmd
22
+ .option('--confirm <identifier@version>', 'Required with -y or without a terminal: <identifier>@<version>')
23
+ .option('-y, --yes', 'Do not prompt (still requires --confirm)')
24
+ .option('--api-url <url>', API_URL_HELP);
25
+ addWithdrawOptions(versions.command('withdraw <identifier> <version>').description(withdrawDescription)).action(async (identifier, version, options) => {
26
+ process.exitCode = await runVersionsWithdraw(identifier, version, options);
27
+ });
28
+ addWithdrawOptions(versions
29
+ .command('delete <identifier> <version>')
30
+ .description('Old name of `versions withdraw` (an irreversible withdrawal, not a delete)')).action(async (identifier, version, options) => {
31
+ consoleIo.err('注意:versions delete 现在就是 versions withdraw——撤回不可恢复,不是可恢复的删除。');
32
+ process.exitCode = await runVersionsWithdraw(identifier, version, options);
65
33
  });
66
34
  }
67
- function getClient() {
68
- const credentials = readCredentials();
69
- const client = ApiClient.fromEnvOrCredentials(credentials);
70
- if (!client) {
71
- log.error('Not logged in. Run `ultikits login` or set ULTIKITS_TOKEN.');
72
- process.exit(1);
35
+ /**
36
+ * The API that authenticated commands would use (same rule as resolveAuth): with
37
+ * ULTIKITS_TOKEN set, the stored login's API is ignored. Never reads the credential value.
38
+ */
39
+ export function publicApiUrl(flag, env, configDir) {
40
+ if (env.ULTIKITS_TOKEN?.trim())
41
+ return resolveApiUrl(flag, env);
42
+ const state = readCredentials(configDir);
43
+ return resolveApiUrl(flag, env, state.status === 'ok' ? state.credential.apiUrl : undefined);
44
+ }
45
+ export async function runVersionsList(identifier, options, deps = {}) {
46
+ const io = deps.io ?? consoleIo;
47
+ const env = deps.env ?? process.env;
48
+ let apiUrl;
49
+ try {
50
+ // Public route: only the API origin is needed, never the credential.
51
+ apiUrl = publicApiUrl(options.apiUrl, env, deps.configDir);
52
+ }
53
+ catch (err) {
54
+ io.err(err.message);
55
+ return 1;
56
+ }
57
+ const client = new ApiClient({ apiUrl });
58
+ let items;
59
+ try {
60
+ items = await fetchAllPages((page) => client.listPublicVersions(identifier, { page, pageSize: MAX_PAGE_SIZE, includeWithdrawn: true }));
61
+ }
62
+ catch (err) {
63
+ io.err(describeCommandError(err, { op: 'versions', identifier, apiUrl }));
64
+ return 1;
65
+ }
66
+ if (items.length === 0) {
67
+ io.out(`${identifier} 还没有已发布的版本(等待上传的版本不在公开目录里)。`);
68
+ return 0;
69
+ }
70
+ for (const v of items) {
71
+ const marks = [v.isLatest ? '最新 / latest' : '', v.download.kind === 'withdrawn' ? `已撤回 / withdrawn ${v.download.withdrawnAt}` : '']
72
+ .filter(Boolean)
73
+ .join(', ');
74
+ io.out(` ${v.version}${marks ? ` [${marks}]` : ''} — ${(v.sizeBytes / 1024).toFixed(0)} KB, ${v.publishedAt}`);
75
+ }
76
+ io.out(`共 ${items.length} 个版本 / ${items.length} version(s)`);
77
+ return 0;
78
+ }
79
+ export async function runVersionsWithdraw(identifier, version, options, deps = {}) {
80
+ const io = deps.io ?? consoleIo;
81
+ const target = `${identifier}@${version}`;
82
+ const confirmed = await requireConfirmation({
83
+ expected: target,
84
+ flagValue: options.confirm,
85
+ yes: options.yes,
86
+ interactive: deps.interactive ?? isInteractiveTerminal(),
87
+ warning: `将撤回 ${target}。撤回不可恢复:版本号 ${version} 及更低版本不能再发布;已下载的副本不受影响。`,
88
+ ask: deps.ask,
89
+ });
90
+ if (!confirmed.ok) {
91
+ io.err(confirmed.reason);
92
+ return EXIT_USAGE;
93
+ }
94
+ const auth = resolveAuth({ apiUrl: options.apiUrl, env: deps.env ?? process.env, configDir: deps.configDir });
95
+ if (!auth.ok) {
96
+ io.err(auth.message);
97
+ return 1;
98
+ }
99
+ const client = new ApiClient({ apiUrl: auth.apiUrl, credential: auth.credential });
100
+ try {
101
+ await client.withdrawVersion(identifier, version);
102
+ io.out(`已撤回 ${target}(不可恢复)。`);
103
+ return 0;
104
+ }
105
+ catch (err) {
106
+ io.err(redact(describeCommandError(err, { op: 'withdraw', identifier, version, apiUrl: auth.apiUrl }), auth.credential));
107
+ return 1;
73
108
  }
74
- return client;
75
109
  }
76
110
  //# sourceMappingURL=versions.js.map
@@ -1,2 +1,8 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandDeps } from './publish.js';
2
3
  export declare function registerWhoamiCommand(program: Command): void;
4
+ /** Message for the identity errors every authenticated command can hit. */
5
+ export declare function describeAuthError(err: unknown, apiUrl: string): string;
6
+ export declare function runWhoami(options: {
7
+ apiUrl?: string;
8
+ }, deps?: CommandDeps): Promise<number>;