@ultikits/cli 1.1.0 → 1.2.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/README.md CHANGED
@@ -13,14 +13,13 @@ Requires Node.js 18 or later.
13
13
  ## Quick Start
14
14
 
15
15
  ```bash
16
- # 1. Log in to your UltiKits developer account
17
- ultikits login
18
-
19
- # 2. Initialize project config (optional, recommended)
20
- ultikits init
16
+ # 1. Scaffold a new UltiTools module project
17
+ ultikits create
21
18
 
22
- # 3. Build your plugin and publish
19
+ # 2. Build and publish to UltiCloud
20
+ cd MyModule
23
21
  mvn clean package
22
+ ultikits login
24
23
  ultikits publish
25
24
  ```
26
25
 
@@ -58,6 +57,28 @@ Credentials are stored in `~/.ultikits/credentials.json`.
58
57
 
59
58
  ## Commands
60
59
 
60
+ ### `ultikits create`
61
+
62
+ Scaffold a new UltiTools module project. Interactive prompts collect module name, package name, description, author, and API version, then generate a complete project with:
63
+
64
+ - `pom.xml` — Maven build config with UltiTools-API dependency, test deps, JaCoCo
65
+ - `src/main/java/.../ModuleName.java` — Main class with `@UltiToolsModule` annotation
66
+ - `src/main/resources/plugin.yml` — Plugin descriptor
67
+ - `src/main/resources/lang/en.yml` and `zh.yml` — i18n language files
68
+ - `README.md` — Bilingual project readme
69
+ - `.gitignore` — Standard Java/Maven ignores
70
+ - `.git/` — Initialized git repository
71
+
72
+ ```bash
73
+ # Create in current directory (prompts for module name)
74
+ ultikits create
75
+
76
+ # Create in a specific directory
77
+ ultikits create my-plugin-dir
78
+ ```
79
+
80
+ The generated project compiles immediately with `mvn compile`.
81
+
61
82
  ### `ultikits login`
62
83
 
63
84
  Authenticate with UltiKits. Choose between browser-based magic link or pasting an access token.
@@ -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('1.2.0');
16
16
  registerLoginCommand(program);
17
17
  registerLogoutCommand(program);
18
18
  registerWhoamiCommand(program);
@@ -1,7 +1,16 @@
1
+ import fs from 'node:fs';
1
2
  import inquirer from 'inquirer';
2
3
  import { readCredentials } from '../lib/config.js';
3
4
  import { ApiClient } from '../lib/api-client.js';
4
5
  import * as log from '../lib/logger.js';
6
+ const CATEGORY_NAMES = {
7
+ 1: 'Utilities',
8
+ 2: 'Economy',
9
+ 3: 'World Management',
10
+ 4: 'Social & Chat',
11
+ 5: 'Permissions & Security',
12
+ 6: 'Gameplay',
13
+ };
5
14
  export function registerModulesCommand(program) {
6
15
  const modules = program
7
16
  .command('modules')
@@ -43,14 +52,98 @@ export function registerModulesCommand(program) {
43
52
  process.exit(1);
44
53
  }
45
54
  const mod = res.data;
55
+ const category = mod.categoryId ? (CATEGORY_NAMES[mod.categoryId] ?? `#${mod.categoryId}`) : 'N/A';
46
56
  console.log();
47
57
  console.log(` Name: ${mod.name}`);
48
58
  console.log(` ID: ${mod.identifyString}`);
49
- console.log(` Description: ${mod.shortDescription || 'N/A'}`);
59
+ console.log(` Summary: ${mod.shortDescription || 'N/A'}`);
60
+ console.log(` Category: ${category}`);
61
+ console.log(` Compatible: ${mod.compatibleVersions || 'N/A'}`);
62
+ console.log(` Tags: ${mod.tags?.length ? mod.tags.join(', ') : 'N/A'}`);
50
63
  console.log(` Downloads: ${mod.downloads ?? 0}`);
51
64
  console.log(` Version: ${mod.latestVersion?.version ?? 'N/A'}`);
65
+ if (mod.description) {
66
+ const preview = mod.description.length > 200
67
+ ? mod.description.slice(0, 200) + '...'
68
+ : mod.description;
69
+ console.log(` Description: ${preview.split('\n')[0]}`);
70
+ const lines = preview.split('\n').slice(1);
71
+ for (const line of lines) {
72
+ console.log(` ${line}`);
73
+ }
74
+ }
75
+ else {
76
+ console.log(` Description: N/A`);
77
+ }
52
78
  console.log();
53
79
  });
80
+ modules
81
+ .command('update <identifyString>')
82
+ .description('Update module metadata')
83
+ .option('--name <name>', 'Display name')
84
+ .option('--short-description <text>', 'Short description (one-liner)')
85
+ .option('--description <text>', 'Full description (Markdown)')
86
+ .option('--description-file <path>', 'Read full description from a file')
87
+ .option('--category <id>', 'Category ID (1-6)')
88
+ .option('--compatible-versions <range>', 'Compatible MC versions (e.g. "1.8-1.21+")')
89
+ .option('--tags <csv>', 'Comma-separated tags (max 5)')
90
+ .option('-y, --yes', 'Skip confirmation')
91
+ .action(async (identifyString, options) => {
92
+ const client = getClient();
93
+ const spin = log.spinner('Loading module...');
94
+ spin.start();
95
+ const infoRes = await client.get(`/plugin/get?identifyString=${identifyString}`);
96
+ if (infoRes.code !== '200' || !infoRes.data) {
97
+ spin.fail('Module not found: ' + identifyString);
98
+ process.exit(1);
99
+ }
100
+ spin.succeed(`Found module: ${infoRes.data.name}`);
101
+ const mod = infoRes.data;
102
+ const hasFlags = options.name || options.shortDescription || options.description
103
+ || options.descriptionFile || options.category || options.compatibleVersions || options.tags;
104
+ let updates;
105
+ if (hasFlags) {
106
+ updates = buildUpdatesFromFlags(options);
107
+ }
108
+ else {
109
+ updates = await promptForUpdates(mod);
110
+ }
111
+ if (Object.keys(updates).length === 0) {
112
+ log.info('No changes to apply.');
113
+ return;
114
+ }
115
+ // Show summary
116
+ console.log();
117
+ for (const [key, value] of Object.entries(updates)) {
118
+ const display = key === 'description'
119
+ ? value.split('\n')[0].slice(0, 80) + (value.length > 80 ? '...' : '')
120
+ : key === 'tags'
121
+ ? value.join(', ')
122
+ : String(value);
123
+ console.log(` ${key}: ${display}`);
124
+ }
125
+ console.log();
126
+ if (!options.yes) {
127
+ const { confirm } = await inquirer.prompt([
128
+ {
129
+ type: 'confirm',
130
+ name: 'confirm',
131
+ message: `Update module "${identifyString}"?`,
132
+ default: true,
133
+ },
134
+ ]);
135
+ if (!confirm)
136
+ return;
137
+ }
138
+ const updateSpin = log.spinner('Updating module...');
139
+ updateSpin.start();
140
+ const res = await client.put(`/developer/modules/${mod.id}`, updates);
141
+ if (res.code !== '200') {
142
+ updateSpin.fail('Failed to update: ' + res.msg);
143
+ process.exit(1);
144
+ }
145
+ updateSpin.succeed(`Updated module: ${identifyString}`);
146
+ });
54
147
  modules
55
148
  .command('delete <identifyString>')
56
149
  .description('Delete a module')
@@ -80,6 +173,132 @@ export function registerModulesCommand(program) {
80
173
  log.success(`Deleted module: ${identifyString}`);
81
174
  });
82
175
  }
176
+ function buildUpdatesFromFlags(options) {
177
+ const updates = {};
178
+ if (options.name)
179
+ updates.name = options.name;
180
+ if (options.shortDescription)
181
+ updates.shortDescription = options.shortDescription;
182
+ if (options.descriptionFile) {
183
+ const filePath = options.descriptionFile;
184
+ if (!fs.existsSync(filePath)) {
185
+ log.error(`File not found: ${filePath}`);
186
+ process.exit(1);
187
+ }
188
+ updates.description = fs.readFileSync(filePath, 'utf-8');
189
+ }
190
+ else if (options.description) {
191
+ updates.description = options.description;
192
+ }
193
+ if (options.category) {
194
+ const id = parseInt(options.category);
195
+ if (isNaN(id) || id < 1 || id > 6) {
196
+ log.error('Category must be 1-6');
197
+ process.exit(1);
198
+ }
199
+ updates.categoryId = id;
200
+ }
201
+ if (options.compatibleVersions)
202
+ updates.compatibleVersions = options.compatibleVersions;
203
+ if (options.tags) {
204
+ const tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
205
+ if (tags.length > 5) {
206
+ log.error('Maximum 5 tags allowed');
207
+ process.exit(1);
208
+ }
209
+ updates.tags = tags;
210
+ }
211
+ return updates;
212
+ }
213
+ async function promptForUpdates(mod) {
214
+ const updates = {};
215
+ const { name } = await inquirer.prompt([{
216
+ type: 'input',
217
+ name: 'name',
218
+ message: 'Display name:',
219
+ default: mod.name,
220
+ }]);
221
+ if (name !== mod.name)
222
+ updates.name = name;
223
+ const { shortDesc } = await inquirer.prompt([{
224
+ type: 'input',
225
+ name: 'shortDesc',
226
+ message: 'Short description:',
227
+ default: mod.shortDescription || '',
228
+ }]);
229
+ if (shortDesc !== (mod.shortDescription || ''))
230
+ updates.shortDescription = shortDesc;
231
+ const { descMode } = await inquirer.prompt([{
232
+ type: 'list',
233
+ name: 'descMode',
234
+ message: 'Full description:',
235
+ choices: [
236
+ { name: 'Keep current', value: 'keep' },
237
+ { name: 'Enter text', value: 'text' },
238
+ { name: 'Read from file', value: 'file' },
239
+ ],
240
+ }]);
241
+ if (descMode === 'text') {
242
+ const { desc } = await inquirer.prompt([{
243
+ type: 'editor',
244
+ name: 'desc',
245
+ message: 'Enter description (opens editor):',
246
+ default: mod.description || '',
247
+ }]);
248
+ if (desc.trim())
249
+ updates.description = desc.trim();
250
+ }
251
+ else if (descMode === 'file') {
252
+ const { filePath } = await inquirer.prompt([{
253
+ type: 'input',
254
+ name: 'filePath',
255
+ message: 'File path:',
256
+ }]);
257
+ if (filePath && fs.existsSync(filePath)) {
258
+ updates.description = fs.readFileSync(filePath, 'utf-8');
259
+ }
260
+ else if (filePath) {
261
+ log.warn('File not found, skipping description.');
262
+ }
263
+ }
264
+ const { category } = await inquirer.prompt([{
265
+ type: 'list',
266
+ name: 'category',
267
+ message: 'Category:',
268
+ choices: [
269
+ { name: `Keep current (${CATEGORY_NAMES[mod.categoryId] ?? 'N/A'})`, value: 0 },
270
+ ...Object.entries(CATEGORY_NAMES).map(([id, label]) => ({
271
+ name: `${id}. ${label}`,
272
+ value: parseInt(id),
273
+ })),
274
+ ],
275
+ }]);
276
+ if (category !== 0 && category !== mod.categoryId)
277
+ updates.categoryId = category;
278
+ const { compat } = await inquirer.prompt([{
279
+ type: 'input',
280
+ name: 'compat',
281
+ message: 'Compatible MC versions:',
282
+ default: mod.compatibleVersions || '',
283
+ }]);
284
+ if (compat !== (mod.compatibleVersions || ''))
285
+ updates.compatibleVersions = compat;
286
+ const currentTags = mod.tags?.join(', ') || '';
287
+ const { tags } = await inquirer.prompt([{
288
+ type: 'input',
289
+ name: 'tags',
290
+ message: 'Tags (comma-separated, max 5):',
291
+ default: currentTags,
292
+ }]);
293
+ const tagList = tags.split(',').map((t) => t.trim()).filter(Boolean);
294
+ if (tags !== currentTags && tagList.length <= 5) {
295
+ updates.tags = tagList;
296
+ }
297
+ else if (tagList.length > 5) {
298
+ log.warn('Too many tags (max 5), skipping.');
299
+ }
300
+ return updates;
301
+ }
83
302
  function getClient() {
84
303
  const credentials = readCredentials();
85
304
  const client = ApiClient.fromEnvOrCredentials(credentials);
@@ -8,5 +8,6 @@ export declare class ApiClient {
8
8
  get<T = any>(path: string): Promise<ApiResponse<T>>;
9
9
  post<T = any>(path: string, body?: any): Promise<ApiResponse<T>>;
10
10
  postMultipart<T = any>(path: string, formData: FormData, extraHeaders?: Record<string, string>): Promise<ApiResponse<T>>;
11
+ put<T = any>(path: string, body?: any): Promise<ApiResponse<T>>;
11
12
  delete<T = any>(path: string): Promise<ApiResponse<T>>;
12
13
  }
@@ -60,6 +60,18 @@ export class ApiClient {
60
60
  });
61
61
  return response.json();
62
62
  }
63
+ async put(path, body) {
64
+ const headers = {
65
+ ...this.getHeaders(),
66
+ 'Content-Type': 'application/json',
67
+ };
68
+ const response = await fetch(`${this.baseUrl}${path}`, {
69
+ method: 'PUT',
70
+ headers,
71
+ body: body ? JSON.stringify(body) : undefined,
72
+ });
73
+ return response.json();
74
+ }
63
75
  async delete(path) {
64
76
  const response = await fetch(`${this.baseUrl}${path}`, {
65
77
  method: 'DELETE',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultikits/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI tool for publishing UltiTools plugin modules to UltiCloud marketplace",
5
5
  "type": "module",
6
6
  "bin": {