@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.
@@ -1,2 +1,37 @@
1
1
  import { Command } from 'commander';
2
+ import type { CommandDeps } from './publish.js';
3
+ /** Exit code for a usage error or a refused confirmation; nothing was sent. */
4
+ export declare const EXIT_USAGE = 2;
5
+ export interface ManageDeps extends CommandDeps {
6
+ /** Overrides terminal detection (tests). */
7
+ interactive?: boolean;
8
+ /** Asks one question on the terminal (tests pass a stub). */
9
+ ask?: (question: string) => Promise<string>;
10
+ }
11
+ interface ApiUrlOption {
12
+ apiUrl?: string;
13
+ }
14
+ export interface CreateOptions extends ApiUrlOption {
15
+ id?: string;
16
+ name?: string;
17
+ shortDescription?: string;
18
+ categoryId?: string;
19
+ }
20
+ export interface UpdateOptions extends ApiUrlOption {
21
+ name?: string;
22
+ shortDescription?: string;
23
+ descriptionFile?: string;
24
+ categoryId?: string;
25
+ }
26
+ export interface DestructiveOptions extends ApiUrlOption {
27
+ confirm?: string;
28
+ yes?: boolean;
29
+ }
2
30
  export declare function registerModulesCommand(program: Command): void;
31
+ export declare function runModulesList(options: ApiUrlOption, deps?: ManageDeps): Promise<number>;
32
+ export declare function runModulesInfo(identifier: string, options: ApiUrlOption, deps?: ManageDeps): Promise<number>;
33
+ export declare function runModulesCreate(options: CreateOptions, deps?: ManageDeps): Promise<number>;
34
+ export declare function runModulesUpdate(identifier: string, options: UpdateOptions, deps?: ManageDeps): Promise<number>;
35
+ export declare function runModulesDelete(identifier: string, options: DestructiveOptions, deps?: ManageDeps): Promise<number>;
36
+ export declare function runModulesRestore(identifier: string, options: ApiUrlOption, deps?: ManageDeps): Promise<number>;
37
+ export {};
@@ -1,92 +1,254 @@
1
- import inquirer from 'inquirer';
2
- import { readCredentials } from '../lib/config.js';
1
+ import fs from 'node:fs';
2
+ import { resolveAuth } from '../lib/config.js';
3
3
  import { ApiClient } from '../lib/api-client.js';
4
- import * as log from '../lib/logger.js';
4
+ import { readProjectConfig } from '../lib/project-config.js';
5
+ import { redact } from '../lib/mask.js';
6
+ import { consoleIo } from '../lib/logger.js';
7
+ import { fetchAllPages, MAX_PAGE_SIZE } from '../lib/paging.js';
8
+ import { isInteractiveTerminal, requireConfirmation } from '../lib/confirm.js';
9
+ import { describeCommandError } from '../lib/command-errors.js';
10
+ /** Exit code for a usage error or a refused confirmation; nothing was sent. */
11
+ export const EXIT_USAGE = 2;
12
+ const API_URL_HELP = 'UltiCloud API URL (default: ULTIKITS_API_URL, stored login, or https://api.ultikits.com)';
13
+ const MANAGE_NOTE = '(needs a manage credential: `ultikits login`, or a developer key rotated to the manage scope)';
5
14
  export function registerModulesCommand(program) {
6
15
  const modules = program
7
16
  .command('modules')
8
- .description('Manage your marketplace modules');
17
+ .description('Manage your UltiCloud modules')
18
+ .addHelpText('after', '\nDeleted modules can be restored for 30 days with `ultikits modules restore <identifier>`.\n' +
19
+ 'The web panel has no restore: the CLI (or the API) is the only way back.');
9
20
  modules
10
21
  .command('list')
11
22
  .description('List your modules')
12
- .action(async () => {
13
- const client = getClient();
14
- const spin = log.spinner('Loading modules...');
15
- spin.start();
16
- const res = await client.get('/developer/modules');
17
- if (res.code !== '200') {
18
- spin.fail('Failed to list modules: ' + res.msg);
19
- process.exit(1);
20
- }
21
- spin.stop();
22
- const modulesList = res.data;
23
- if (modulesList.length === 0) {
24
- log.info('No modules found. Create one with `ultikits publish`.');
25
- return;
26
- }
27
- console.log();
28
- for (const mod of modulesList) {
29
- const version = mod.latestVersion?.version ?? 'no versions';
30
- console.log(` ${mod.identifyString} — ${mod.name} (${version})`);
31
- }
32
- console.log();
33
- log.info(`${modulesList.length} module(s)`);
23
+ .option('--api-url <url>', API_URL_HELP)
24
+ .action(async (options) => {
25
+ process.exitCode = await runModulesList(options);
34
26
  });
35
27
  modules
36
- .command('info <identifyString>')
37
- .description('Show module details')
38
- .action(async (identifyString) => {
39
- const client = getClient();
40
- const res = await client.get(`/plugin/get?identifyString=${identifyString}`);
41
- if (res.code !== '200' || !res.data) {
42
- log.error('Module not found: ' + identifyString);
43
- process.exit(1);
44
- }
45
- const mod = res.data;
46
- console.log();
47
- console.log(` Name: ${mod.name}`);
48
- console.log(` ID: ${mod.identifyString}`);
49
- console.log(` Description: ${mod.shortDescription || 'N/A'}`);
50
- console.log(` Downloads: ${mod.downloads ?? 0}`);
51
- console.log(` Version: ${mod.latestVersion?.version ?? 'N/A'}`);
52
- console.log();
28
+ .command('info <identifier>')
29
+ .description('Show one of your modules')
30
+ .option('--api-url <url>', API_URL_HELP)
31
+ .action(async (identifier, options) => {
32
+ process.exitCode = await runModulesInfo(identifier, options);
33
+ });
34
+ modules
35
+ .command('create')
36
+ .description(`Create a module ${MANAGE_NOTE}`)
37
+ .option('--id <identifier>', 'Module identifier (default: identifyString in ultikits.json)')
38
+ .option('--name <name>', 'Display name (default: name in ultikits.json)')
39
+ .option('--short-description <text>', 'Short description, up to 200 characters')
40
+ .option('--category-id <id>', 'Category id')
41
+ .option('--api-url <url>', API_URL_HELP)
42
+ .action(async (options) => {
43
+ process.exitCode = await runModulesCreate(options);
53
44
  });
54
45
  modules
55
- .command('delete <identifyString>')
56
- .description('Delete a module')
57
- .action(async (identifyString) => {
58
- const client = getClient();
59
- // Get module ID first
60
- const infoRes = await client.get(`/plugin/get?identifyString=${identifyString}`);
61
- if (infoRes.code !== '200' || !infoRes.data) {
62
- log.error('Module not found: ' + identifyString);
63
- process.exit(1);
46
+ .command('update <identifier>')
47
+ .description(`Change a module's profile; only the options given are sent ${MANAGE_NOTE}`)
48
+ .option('--name <name>', 'New display name')
49
+ .option('--short-description <text>', 'New short description')
50
+ .option('--description-file <path>', 'File whose content becomes the long description')
51
+ .option('--category-id <id>', 'New category id')
52
+ .option('--api-url <url>', API_URL_HELP)
53
+ .action(async (identifier, options) => {
54
+ process.exitCode = await runModulesUpdate(identifier, options);
55
+ });
56
+ modules
57
+ .command('delete <identifier>')
58
+ .description(`Delete a module; restorable for 30 days with \`ultikits modules restore\` ${MANAGE_NOTE}`)
59
+ .option('--confirm <identifier>', 'Required with -y or without a terminal: the same identifier again')
60
+ .option('-y, --yes', 'Do not prompt (still requires --confirm)')
61
+ .option('--api-url <url>', API_URL_HELP)
62
+ .action(async (identifier, options) => {
63
+ process.exitCode = await runModulesDelete(identifier, options);
64
+ });
65
+ modules
66
+ .command('restore <identifier>')
67
+ .description(`Restore a module deleted in the last 30 days ${MANAGE_NOTE}`)
68
+ .option('--api-url <url>', API_URL_HELP)
69
+ .action(async (identifier, options) => {
70
+ process.exitCode = await runModulesRestore(identifier, options);
71
+ });
72
+ }
73
+ function authenticate(options, deps) {
74
+ const io = deps.io ?? consoleIo;
75
+ const auth = resolveAuth({ apiUrl: options.apiUrl, env: deps.env ?? process.env, configDir: deps.configDir });
76
+ if (!auth.ok) {
77
+ io.err(auth.message);
78
+ return null;
79
+ }
80
+ return { auth, client: new ApiClient({ apiUrl: auth.apiUrl, credential: auth.credential }), io };
81
+ }
82
+ function fail(a, err, ctx) {
83
+ a.io.err(redact(describeCommandError(err, { ...ctx, apiUrl: a.auth.apiUrl }), a.auth.credential));
84
+ return 1;
85
+ }
86
+ function listAll(client) {
87
+ return fetchAllPages((page) => client.listModules({ page, pageSize: MAX_PAGE_SIZE }));
88
+ }
89
+ /** Parses a positive integer option; returns undefined when absent, null when invalid. */
90
+ function parseCategoryId(value) {
91
+ if (value === undefined)
92
+ return undefined;
93
+ return /^[1-9][0-9]{0,15}$/.test(value) ? Number(value) : null;
94
+ }
95
+ export async function runModulesList(options, deps = {}) {
96
+ const a = authenticate(options, deps);
97
+ if (!a)
98
+ return 1;
99
+ let items;
100
+ try {
101
+ items = await listAll(a.client);
102
+ }
103
+ catch (err) {
104
+ return fail(a, err, { op: 'list' });
105
+ }
106
+ if (items.length === 0) {
107
+ a.io.out('还没有模块:用 ultikits publish 发布(模块不存在时会自动创建),或 ultikits modules create。');
108
+ return 0;
109
+ }
110
+ for (const mod of items) {
111
+ a.io.out(` ${mod.identifier} — ${mod.name} (${mod.latestVersion ?? '无已发布版本 / no versions'})`);
112
+ }
113
+ a.io.out(`共 ${items.length} 个模块 / ${items.length} module(s)`);
114
+ return 0;
115
+ }
116
+ export async function runModulesInfo(identifier, options, deps = {}) {
117
+ const a = authenticate(options, deps);
118
+ if (!a)
119
+ return 1;
120
+ let items;
121
+ try {
122
+ items = await listAll(a.client);
123
+ }
124
+ catch (err) {
125
+ return fail(a, err, { op: 'list', identifier });
126
+ }
127
+ const mod = items.find((m) => m.identifier === identifier);
128
+ if (!mod) {
129
+ a.io.err(`你的模块里没有 ${identifier}(用 ultikits modules list 查看;删除后 30 天内可 ultikits modules restore ${identifier})。`);
130
+ return 1;
131
+ }
132
+ a.io.out(` 名称 / Name: ${mod.name}`);
133
+ a.io.out(` 标识符 / Identifier: ${mod.identifier}`);
134
+ a.io.out(` 简介 / Summary: ${mod.shortDescription || '-'}`);
135
+ a.io.out(` 分类 / Category: ${mod.category ? `${mod.category.name} (#${mod.category.id})` : '-'}`);
136
+ a.io.out(` 最新版本 / Latest: ${mod.latestVersion ?? '无已发布版本 / none'}`);
137
+ a.io.out(` 下载 / Downloads: ${mod.downloads}`);
138
+ a.io.out(` 更新 / Updated: ${mod.updatedAt}`);
139
+ return 0;
140
+ }
141
+ export async function runModulesCreate(options, deps = {}) {
142
+ const io = deps.io ?? consoleIo;
143
+ const config = readProjectConfig(deps.cwd ?? process.cwd());
144
+ const identifier = options.id ?? config?.identifyString;
145
+ const name = options.name ?? config?.name;
146
+ if (!identifier || !name) {
147
+ io.err('需要模块标识符与名称:用 --id 与 --name 指定,或写进 ultikits.json(identifyString、name)。未发送任何请求。');
148
+ return EXIT_USAGE;
149
+ }
150
+ const flagCategory = parseCategoryId(options.categoryId);
151
+ if (flagCategory === null) {
152
+ io.err(`--category-id 必须是正整数:${options.categoryId}。未发送任何请求。`);
153
+ return EXIT_USAGE;
154
+ }
155
+ const a = authenticate(options, deps);
156
+ if (!a)
157
+ return 1;
158
+ try {
159
+ const created = await a.client.createModule({
160
+ name,
161
+ identifier,
162
+ shortDescription: options.shortDescription ?? config?.shortDescription ?? '',
163
+ description: null,
164
+ categoryId: flagCategory ?? config?.categoryId ?? null,
165
+ compatibleApiVersions: null,
166
+ tags: [],
167
+ });
168
+ a.io.out(`已创建模块 ${created.identifier}(${created.name})。用 ultikits publish 发布第一个版本。`);
169
+ return 0;
170
+ }
171
+ catch (err) {
172
+ return fail(a, err, { op: 'create', identifier });
173
+ }
174
+ }
175
+ export async function runModulesUpdate(identifier, options, deps = {}) {
176
+ const io = deps.io ?? consoleIo;
177
+ const body = {};
178
+ if (options.name !== undefined)
179
+ body.name = options.name;
180
+ if (options.shortDescription !== undefined)
181
+ body.shortDescription = options.shortDescription;
182
+ if (options.descriptionFile !== undefined) {
183
+ try {
184
+ body.description = fs.readFileSync(options.descriptionFile, 'utf8');
64
185
  }
65
- const { confirm } = await inquirer.prompt([
66
- {
67
- type: 'confirm',
68
- name: 'confirm',
69
- message: `Delete module "${identifyString}"? This cannot be undone.`,
70
- default: false,
71
- },
72
- ]);
73
- if (!confirm)
74
- return;
75
- const res = await client.delete(`/developer/modules/${infoRes.data.id}`);
76
- if (res.code !== '200') {
77
- log.error('Failed to delete: ' + res.msg);
78
- process.exit(1);
186
+ catch {
187
+ io.err(`读不到描述文件:${options.descriptionFile}。未发送任何请求。`);
188
+ return EXIT_USAGE;
79
189
  }
80
- log.success(`Deleted module: ${identifyString}`);
190
+ }
191
+ const category = parseCategoryId(options.categoryId);
192
+ if (category === null) {
193
+ io.err(`--category-id 必须是正整数:${options.categoryId}。未发送任何请求。`);
194
+ return EXIT_USAGE;
195
+ }
196
+ if (category !== undefined)
197
+ body.categoryId = category;
198
+ if (Object.keys(body).length === 0) {
199
+ io.err('没有要改的内容:至少给出 --name、--short-description、--description-file 或 --category-id 之一。未发送任何请求。');
200
+ return EXIT_USAGE;
201
+ }
202
+ const a = authenticate(options, deps);
203
+ if (!a)
204
+ return 1;
205
+ try {
206
+ await a.client.updateModule(identifier, body);
207
+ a.io.out(`已更新模块 ${identifier}:${Object.keys(body).join(', ')}。`);
208
+ return 0;
209
+ }
210
+ catch (err) {
211
+ return fail(a, err, { op: 'update', identifier });
212
+ }
213
+ }
214
+ export async function runModulesDelete(identifier, options, deps = {}) {
215
+ const io = deps.io ?? consoleIo;
216
+ const confirmed = await requireConfirmation({
217
+ expected: identifier,
218
+ flagValue: options.confirm,
219
+ yes: options.yes,
220
+ interactive: deps.interactive ?? isInteractiveTerminal(),
221
+ warning: `将删除模块 ${identifier}:它会立刻从目录、下载与你的模块列表中消失。` +
222
+ `30 天内可用 ultikits modules restore ${identifier} 恢复(网页面板不提供恢复),之后永久清除。`,
223
+ ask: deps.ask,
81
224
  });
225
+ if (!confirmed.ok) {
226
+ io.err(confirmed.reason);
227
+ return EXIT_USAGE;
228
+ }
229
+ const a = authenticate(options, deps);
230
+ if (!a)
231
+ return 1;
232
+ try {
233
+ const result = await a.client.deleteModule(identifier);
234
+ a.io.out(`已删除 ${identifier}。${result.restorableUntil} 前可用 ultikits modules restore ${identifier} 恢复(网页面板不提供恢复)。`);
235
+ return 0;
236
+ }
237
+ catch (err) {
238
+ return fail(a, err, { op: 'delete', identifier });
239
+ }
82
240
  }
83
- function getClient() {
84
- const credentials = readCredentials();
85
- const client = ApiClient.fromEnvOrCredentials(credentials);
86
- if (!client) {
87
- log.error('Not logged in. Run `ultikits login` or set ULTIKITS_TOKEN.');
88
- process.exit(1);
89
- }
90
- return client;
241
+ export async function runModulesRestore(identifier, options, deps = {}) {
242
+ const a = authenticate(options, deps);
243
+ if (!a)
244
+ return 1;
245
+ try {
246
+ const mod = await a.client.restoreModule(identifier);
247
+ a.io.out(`已恢复 ${mod.identifier}(${mod.name})。`);
248
+ return 0;
249
+ }
250
+ catch (err) {
251
+ return fail(a, err, { op: 'restore', identifier });
252
+ }
91
253
  }
92
254
  //# sourceMappingURL=modules.js.map
@@ -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 {};