@ultikits/cli 1.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 ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2026-02-14)
4
+
5
+ Initial release.
6
+
7
+ ### Features
8
+
9
+ - **Authentication**: Browser-based magic link login and developer access token support
10
+ - **CI/CD**: `ULTIKITS_TOKEN` environment variable for automated pipelines
11
+ - **Publishing**: `ultikits publish` with automatic JAR detection and `plugin.yml` metadata extraction
12
+ - **Project config**: `ultikits init` creates `ultikits.json` for persistent module metadata
13
+ - **Module management**: List, inspect, and delete modules via `ultikits modules`
14
+ - **Version management**: List and delete versions via `ultikits versions`
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 UltiKits
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # @ultikits/cli
2
+
3
+ Command-line tool for publishing [UltiTools](https://github.com/UltiKits/UltiTools-Reborn) plugin modules to [UltiCloud](https://panel.ultikits.com) marketplace.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @ultikits/cli
9
+ ```
10
+
11
+ Requires Node.js 18 or later.
12
+
13
+ ## Quick Start
14
+
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
21
+
22
+ # 3. Build your plugin and publish
23
+ mvn clean package
24
+ ultikits publish
25
+ ```
26
+
27
+ ## Authentication
28
+
29
+ ### Interactive login (browser)
30
+
31
+ ```bash
32
+ ultikits login
33
+ # Choose "Browser login (magic link)"
34
+ # A browser window opens — approve the request
35
+ ```
36
+
37
+ ### Access token (developer portal)
38
+
39
+ ```bash
40
+ ultikits login
41
+ # Choose "Access token (from developer portal)"
42
+ # Paste your access token from panel.ultikits.com/developer
43
+ ```
44
+
45
+ ### CI/CD (environment variable)
46
+
47
+ Set the `ULTIKITS_TOKEN` environment variable with your developer access token. No `login` step is needed.
48
+
49
+ ```yaml
50
+ # GitHub Actions example
51
+ - name: Publish to UltiCloud
52
+ env:
53
+ ULTIKITS_TOKEN: ${{ secrets.ULTIKITS_TOKEN }}
54
+ run: ultikits publish --yes
55
+ ```
56
+
57
+ Credentials are stored in `~/.ultikits/credentials.json`.
58
+
59
+ ## Commands
60
+
61
+ ### `ultikits login`
62
+
63
+ Authenticate with UltiKits. Choose between browser-based magic link or pasting an access token.
64
+
65
+ ### `ultikits logout`
66
+
67
+ Remove stored credentials.
68
+
69
+ ### `ultikits whoami`
70
+
71
+ Check current authentication status.
72
+
73
+ ### `ultikits init`
74
+
75
+ Create an `ultikits.json` project config in the current directory. Interactive prompts for module ID, name, description, and category.
76
+
77
+ ```json
78
+ {
79
+ "identifyString": "my.awesome.plugin",
80
+ "name": "My Awesome Plugin",
81
+ "shortDescription": "A great UltiTools module",
82
+ "categoryId": 1
83
+ }
84
+ ```
85
+
86
+ ### `ultikits publish [jar-path]`
87
+
88
+ Publish a plugin module JAR to UltiCloud.
89
+
90
+ If no JAR path is given, automatically finds the built JAR in `./target/` (skips `-sources.jar` and `-javadoc.jar`).
91
+
92
+ **Options:**
93
+
94
+ | Flag | Description |
95
+ |------|-------------|
96
+ | `--id <string>` | Module identify string |
97
+ | `--name <string>` | Module display name |
98
+ | `--version <string>` | Module version |
99
+ | `--changelog <text>` | Version changelog |
100
+ | `--short-description <text>` | Short description |
101
+ | `-y, --yes` | Skip confirmation prompts |
102
+
103
+ **Metadata resolution priority:** CLI flags > `ultikits.json` > `plugin.yml` from JAR.
104
+
105
+ **Example:**
106
+
107
+ ```bash
108
+ # Auto-detect JAR and metadata from plugin.yml + ultikits.json
109
+ ultikits publish
110
+
111
+ # Explicit JAR path with overrides
112
+ ultikits publish target/MyPlugin-1.0.0.jar --version 1.0.1 --changelog "Bug fix"
113
+
114
+ # CI/CD mode (no prompts)
115
+ ultikits publish --yes
116
+ ```
117
+
118
+ ### `ultikits modules list`
119
+
120
+ List all your published modules.
121
+
122
+ ### `ultikits modules info <identifyString>`
123
+
124
+ Show details of a specific module.
125
+
126
+ ### `ultikits modules delete <identifyString>`
127
+
128
+ Delete a module (with confirmation prompt).
129
+
130
+ ### `ultikits versions list <identifyString>`
131
+
132
+ List all versions of a module.
133
+
134
+ ### `ultikits versions delete <identifyString> <version>`
135
+
136
+ Delete a specific version (with confirmation prompt).
137
+
138
+ ## Project Config (`ultikits.json`)
139
+
140
+ Create with `ultikits init` or manually. Fields are used as fallback metadata during `publish`:
141
+
142
+ ```json
143
+ {
144
+ "identifyString": "com.example.myplugin",
145
+ "name": "My Plugin",
146
+ "shortDescription": "Does amazing things",
147
+ "categoryId": 1
148
+ }
149
+ ```
150
+
151
+ **Categories:**
152
+
153
+ | ID | Name |
154
+ |----|------|
155
+ | 1 | Utilities |
156
+ | 2 | Economy |
157
+ | 3 | World Management |
158
+ | 4 | Social & Chat |
159
+ | 5 | Permissions & Security |
160
+ | 6 | Gameplay |
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ git clone https://github.com/UltiKits/ultikits-cli.git
166
+ cd ultikits-cli
167
+ npm install
168
+ npm run build
169
+ npm test
170
+ ```
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerLoginCommand } from '../src/commands/login.js';
4
+ import { registerLogoutCommand } from '../src/commands/logout.js';
5
+ import { registerWhoamiCommand } from '../src/commands/whoami.js';
6
+ import { registerInitCommand } from '../src/commands/init.js';
7
+ import { registerPublishCommand } from '../src/commands/publish.js';
8
+ import { registerModulesCommand } from '../src/commands/modules.js';
9
+ import { registerVersionsCommand } from '../src/commands/versions.js';
10
+ const program = new Command();
11
+ program
12
+ .name('ultikits')
13
+ .description('CLI for publishing UltiTools plugin modules to UltiCloud')
14
+ .version('1.0.0');
15
+ registerLoginCommand(program);
16
+ registerLogoutCommand(program);
17
+ registerWhoamiCommand(program);
18
+ registerInitCommand(program);
19
+ registerPublishCommand(program);
20
+ registerModulesCommand(program);
21
+ registerVersionsCommand(program);
22
+ program.parse();
23
+ //# sourceMappingURL=ultikits.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,62 @@
1
+ import inquirer from 'inquirer';
2
+ import { readProjectConfig, writeProjectConfig } from '../lib/project-config.js';
3
+ import * as log from '../lib/logger.js';
4
+ const CATEGORIES = [
5
+ { name: 'Utilities', value: 1 },
6
+ { name: 'Economy', value: 2 },
7
+ { name: 'World Management', value: 3 },
8
+ { name: 'Social & Chat', value: 4 },
9
+ { name: 'Permissions & Security', value: 5 },
10
+ { name: 'Gameplay', value: 6 },
11
+ ];
12
+ export function registerInitCommand(program) {
13
+ program
14
+ .command('init')
15
+ .description('Create ultikits.json project config')
16
+ .action(async () => {
17
+ const existing = readProjectConfig();
18
+ if (existing) {
19
+ const { overwrite } = await inquirer.prompt([
20
+ {
21
+ type: 'confirm',
22
+ name: 'overwrite',
23
+ message: 'ultikits.json already exists. Overwrite?',
24
+ default: false,
25
+ },
26
+ ]);
27
+ if (!overwrite)
28
+ return;
29
+ }
30
+ const answers = await inquirer.prompt([
31
+ {
32
+ type: 'input',
33
+ name: 'identifyString',
34
+ message: 'Module identify string (e.g., my.awesome.plugin):',
35
+ validate: (v) => /^[a-z][a-z0-9_.]{2,63}$/.test(v)
36
+ ? true
37
+ : 'Must be 3-64 chars, lowercase, start with letter, only a-z 0-9 . _',
38
+ },
39
+ {
40
+ type: 'input',
41
+ name: 'name',
42
+ message: 'Display name:',
43
+ validate: (v) => (v.trim().length > 0 ? true : 'Required'),
44
+ },
45
+ {
46
+ type: 'input',
47
+ name: 'shortDescription',
48
+ message: 'Short description (max 200 chars):',
49
+ validate: (v) => v.length <= 200 ? true : 'Must be 200 characters or fewer',
50
+ },
51
+ {
52
+ type: 'list',
53
+ name: 'categoryId',
54
+ message: 'Category:',
55
+ choices: CATEGORIES,
56
+ },
57
+ ]);
58
+ writeProjectConfig(answers);
59
+ log.success('Created ultikits.json');
60
+ });
61
+ }
62
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLoginCommand(program: Command): void;
@@ -0,0 +1,120 @@
1
+ import inquirer from 'inquirer';
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';
8
+ export function registerLoginCommand(program) {
9
+ program
10
+ .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();
26
+ }
27
+ else {
28
+ await loginWithAccessToken();
29
+ }
30
+ });
31
+ }
32
+ async function loginWithMagicLink() {
33
+ const spin = log.spinner('Requesting authentication...');
34
+ spin.start();
35
+ 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;
41
+ }
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();
48
+ try {
49
+ await open(url);
50
+ }
51
+ catch {
52
+ // Browser didn't open — user has the URL
53
+ }
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;
73
+ }
74
+ if (pollBody.data?.status === 'expired') {
75
+ spin.fail('Authentication request expired. Run `ultikits login` again.');
76
+ return;
77
+ }
78
+ }
79
+ spin.fail('Authentication timed out.');
80
+ }
81
+ catch (err) {
82
+ spin.fail('Login failed: ' + err.message);
83
+ }
84
+ }
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();
96
+ 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');
115
+ }
116
+ catch (err) {
117
+ spin.fail('Token validation failed: ' + err.message);
118
+ }
119
+ }
120
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerLogoutCommand(program: Command): void;
@@ -0,0 +1,12 @@
1
+ import { deleteCredentials } from '../lib/config.js';
2
+ import * as log from '../lib/logger.js';
3
+ export function registerLogoutCommand(program) {
4
+ program
5
+ .command('logout')
6
+ .description('Remove stored credentials')
7
+ .action(() => {
8
+ deleteCredentials();
9
+ log.success('Logged out. Credentials removed.');
10
+ });
11
+ }
12
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerModulesCommand(program: Command): void;
@@ -0,0 +1,92 @@
1
+ import inquirer from 'inquirer';
2
+ import { readCredentials } from '../lib/config.js';
3
+ import { ApiClient } from '../lib/api-client.js';
4
+ import * as log from '../lib/logger.js';
5
+ export function registerModulesCommand(program) {
6
+ const modules = program
7
+ .command('modules')
8
+ .description('Manage your marketplace modules');
9
+ modules
10
+ .command('list')
11
+ .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)`);
34
+ });
35
+ 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();
53
+ });
54
+ 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);
64
+ }
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);
79
+ }
80
+ log.success(`Deleted module: ${identifyString}`);
81
+ });
82
+ }
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;
91
+ }
92
+ //# sourceMappingURL=modules.js.map
@@ -0,0 +1,12 @@
1
+ import { Command } from 'commander';
2
+ import type { ModuleMetadata, ProjectConfig, PluginYml } from '../types.js';
3
+ interface PublishFlags {
4
+ id?: string;
5
+ name?: string;
6
+ version?: string;
7
+ changelog?: string;
8
+ shortDescription?: string;
9
+ }
10
+ export declare function resolveMetadata(flags: PublishFlags, projectConfig: ProjectConfig | null, pluginYml: PluginYml | null): ModuleMetadata;
11
+ export declare function registerPublishCommand(program: Command): void;
12
+ export {};
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import inquirer from 'inquirer';
4
+ import { readCredentials } from '../lib/config.js';
5
+ import { readProjectConfig } from '../lib/project-config.js';
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';
9
+ export function resolveMetadata(flags, projectConfig, pluginYml) {
10
+ const identifyString = flags.id ?? projectConfig?.identifyString;
11
+ const name = flags.name ?? projectConfig?.name ?? pluginYml?.name;
12
+ const version = flags.version ?? pluginYml?.version;
13
+ const shortDescription = flags.shortDescription ?? projectConfig?.shortDescription ?? pluginYml?.description;
14
+ const changelog = flags.changelog;
15
+ const categoryId = projectConfig?.categoryId;
16
+ const missing = [];
17
+ if (!identifyString)
18
+ missing.push('identifyString (use --id or ultikits.json)');
19
+ if (!name)
20
+ missing.push('name (use --name, ultikits.json, or plugin.yml)');
21
+ if (!version)
22
+ missing.push('version (use --version or plugin.yml in JAR)');
23
+ if (missing.length > 0) {
24
+ throw new Error('Missing required fields:\n - ' + missing.join('\n - '));
25
+ }
26
+ return {
27
+ identifyString: identifyString,
28
+ name: name,
29
+ version: version,
30
+ shortDescription,
31
+ changelog,
32
+ categoryId,
33
+ };
34
+ }
35
+ export function registerPublishCommand(program) {
36
+ program
37
+ .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('--version <version>', 'Module version')
42
+ .option('--changelog <text>', 'Version changelog')
43
+ .option('--short-description <text>', 'Short description')
44
+ .option('-y, --yes', 'Skip confirmation prompts')
45
+ .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
+ ]);
100
+ if (changelog)
101
+ metadata.changelog = changelog;
102
+ }
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;
115
+ }
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);
133
+ }
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);
165
+ }
166
+ uploadSpin.succeed(`Published ${metadata.identifyString}@${metadata.version}`);
167
+ console.log();
168
+ log.info(`https://panel.ultikits.com/modules/${metadata.identifyString}`);
169
+ });
170
+ }
171
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerVersionsCommand(program: Command): void;
@@ -0,0 +1,76 @@
1
+ import inquirer from 'inquirer';
2
+ import { readCredentials } from '../lib/config.js';
3
+ import { ApiClient } from '../lib/api-client.js';
4
+ import * as log from '../lib/logger.js';
5
+ export function registerVersionsCommand(program) {
6
+ const versions = program
7
+ .command('versions')
8
+ .description('Manage module versions');
9
+ 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)`);
38
+ });
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}`);
65
+ });
66
+ }
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);
73
+ }
74
+ return client;
75
+ }
76
+ //# sourceMappingURL=versions.js.map
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerWhoamiCommand(program: Command): void;
@@ -0,0 +1,32 @@
1
+ import { readCredentials } from '../lib/config.js';
2
+ import { ApiClient } from '../lib/api-client.js';
3
+ import * as log from '../lib/logger.js';
4
+ export function registerWhoamiCommand(program) {
5
+ program
6
+ .command('whoami')
7
+ .description('Show current authenticated user')
8
+ .action(async () => {
9
+ const credentials = readCredentials();
10
+ const client = ApiClient.fromEnvOrCredentials(credentials);
11
+ if (!client) {
12
+ log.error('Not logged in. Run `ultikits login` or set ULTIKITS_TOKEN.');
13
+ process.exit(1);
14
+ }
15
+ const spin = log.spinner('Checking...');
16
+ spin.start();
17
+ try {
18
+ const res = await client.get('/developer/modules');
19
+ if (res.code === '200') {
20
+ spin.succeed('Authenticated as developer');
21
+ log.info(`Auth type: ${credentials?.type ?? 'env var'}`);
22
+ }
23
+ else {
24
+ spin.fail('Authentication failed: ' + res.msg);
25
+ }
26
+ }
27
+ catch (err) {
28
+ spin.fail('Failed to verify: ' + err.message);
29
+ }
30
+ });
31
+ }
32
+ //# sourceMappingURL=whoami.js.map
@@ -0,0 +1,12 @@
1
+ import type { Credentials, ApiResponse } from '../types.js';
2
+ export declare class ApiClient {
3
+ private baseUrl;
4
+ private credentials;
5
+ constructor(credentials: Credentials);
6
+ static fromEnvOrCredentials(credentials: Credentials | null): ApiClient | null;
7
+ private getHeaders;
8
+ get<T = any>(path: string): Promise<ApiResponse<T>>;
9
+ post<T = any>(path: string, body?: any): Promise<ApiResponse<T>>;
10
+ postMultipart<T = any>(path: string, formData: FormData, extraHeaders?: Record<string, string>): Promise<ApiResponse<T>>;
11
+ delete<T = any>(path: string): Promise<ApiResponse<T>>;
12
+ }
@@ -0,0 +1,71 @@
1
+ export class ApiClient {
2
+ baseUrl;
3
+ credentials;
4
+ constructor(credentials) {
5
+ this.credentials = credentials;
6
+ this.baseUrl = credentials.api_url;
7
+ }
8
+ static fromEnvOrCredentials(credentials) {
9
+ const envToken = process.env.ULTIKITS_TOKEN;
10
+ if (envToken) {
11
+ return new ApiClient({
12
+ type: 'access_token',
13
+ token: envToken,
14
+ api_url: 'https://api.ultikits.com',
15
+ });
16
+ }
17
+ if (credentials) {
18
+ return new ApiClient(credentials);
19
+ }
20
+ return null;
21
+ }
22
+ getHeaders() {
23
+ const headers = {};
24
+ if (this.credentials.type === 'oauth') {
25
+ headers['Authorization'] = `Bearer ${this.credentials.access_token}`;
26
+ }
27
+ else {
28
+ headers['X-Access-Token'] = this.credentials.token;
29
+ }
30
+ return headers;
31
+ }
32
+ async get(path) {
33
+ const response = await fetch(`${this.baseUrl}${path}`, {
34
+ method: 'GET',
35
+ headers: this.getHeaders(),
36
+ });
37
+ return response.json();
38
+ }
39
+ async post(path, body) {
40
+ const headers = {
41
+ ...this.getHeaders(),
42
+ 'Content-Type': 'application/json',
43
+ };
44
+ const response = await fetch(`${this.baseUrl}${path}`, {
45
+ method: 'POST',
46
+ headers,
47
+ body: body ? JSON.stringify(body) : undefined,
48
+ });
49
+ return response.json();
50
+ }
51
+ async postMultipart(path, formData, extraHeaders) {
52
+ const headers = {
53
+ ...this.getHeaders(),
54
+ ...(extraHeaders ?? {}),
55
+ };
56
+ const response = await fetch(`${this.baseUrl}${path}`, {
57
+ method: 'POST',
58
+ headers,
59
+ body: formData,
60
+ });
61
+ return response.json();
62
+ }
63
+ async delete(path) {
64
+ const response = await fetch(`${this.baseUrl}${path}`, {
65
+ method: 'DELETE',
66
+ headers: this.getHeaders(),
67
+ });
68
+ return response.json();
69
+ }
70
+ }
71
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1,6 @@
1
+ import type { Credentials } from '../types.js';
2
+ export declare function getConfigDir(): string;
3
+ export declare function readCredentials(configDir?: string): Credentials | null;
4
+ export declare function writeCredentials(credentials: Credentials, configDir?: string): void;
5
+ export declare function deleteCredentials(configDir?: string): void;
6
+ export declare function getApiUrl(credentials: Credentials | null): string;
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const CREDENTIALS_FILE = 'credentials.json';
5
+ const DEFAULT_API_URL = 'https://api.ultikits.com';
6
+ export function getConfigDir() {
7
+ return path.join(os.homedir(), '.ultikits');
8
+ }
9
+ export function readCredentials(configDir) {
10
+ const dir = configDir ?? getConfigDir();
11
+ const filePath = path.join(dir, CREDENTIALS_FILE);
12
+ try {
13
+ const content = fs.readFileSync(filePath, 'utf-8');
14
+ return JSON.parse(content);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function writeCredentials(credentials, configDir) {
21
+ const dir = configDir ?? getConfigDir();
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ const filePath = path.join(dir, CREDENTIALS_FILE);
24
+ fs.writeFileSync(filePath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
25
+ }
26
+ export function deleteCredentials(configDir) {
27
+ const dir = configDir ?? getConfigDir();
28
+ const filePath = path.join(dir, CREDENTIALS_FILE);
29
+ try {
30
+ fs.unlinkSync(filePath);
31
+ }
32
+ catch {
33
+ // Already gone
34
+ }
35
+ }
36
+ export function getApiUrl(credentials) {
37
+ return credentials?.api_url ?? DEFAULT_API_URL;
38
+ }
39
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ import type { PluginYml } from '../types.js';
2
+ export declare function readPluginYml(jarPath: string): Promise<PluginYml | null>;
3
+ export declare function findJarInTarget(dir?: string): string | null;
@@ -0,0 +1,42 @@
1
+ import JSZip from 'jszip';
2
+ import yaml from 'js-yaml';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ export async function readPluginYml(jarPath) {
6
+ try {
7
+ const data = fs.readFileSync(jarPath);
8
+ const zip = await JSZip.loadAsync(data);
9
+ const pluginYml = zip.file('plugin.yml');
10
+ if (!pluginYml)
11
+ return null;
12
+ const content = await pluginYml.async('text');
13
+ const parsed = yaml.load(content);
14
+ return {
15
+ name: parsed.name,
16
+ version: String(parsed.version),
17
+ description: parsed.description,
18
+ authors: parsed.authors,
19
+ };
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function findJarInTarget(dir) {
26
+ const targetDir = path.join(dir ?? process.cwd(), 'target');
27
+ try {
28
+ const files = fs.readdirSync(targetDir);
29
+ const jars = files.filter((f) => f.endsWith('.jar') && !f.endsWith('-sources.jar') && !f.endsWith('-javadoc.jar'));
30
+ if (jars.length === 0)
31
+ return null;
32
+ if (jars.length === 1)
33
+ return path.join(targetDir, jars[0]);
34
+ // Multiple JARs — prefer the shaded/fat JAR (without classifier)
35
+ const simple = jars.find((f) => !f.includes('-original'));
36
+ return path.join(targetDir, simple ?? jars[0]);
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ //# sourceMappingURL=jar-reader.js.map
@@ -0,0 +1,5 @@
1
+ export declare function success(msg: string): void;
2
+ export declare function error(msg: string): void;
3
+ export declare function info(msg: string): void;
4
+ export declare function warn(msg: string): void;
5
+ export declare function spinner(text: string): import("ora").Ora;
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ export function success(msg) {
4
+ console.log(chalk.green('✓') + ' ' + msg);
5
+ }
6
+ export function error(msg) {
7
+ console.error(chalk.red('✗') + ' ' + msg);
8
+ }
9
+ export function info(msg) {
10
+ console.log(chalk.blue('ℹ') + ' ' + msg);
11
+ }
12
+ export function warn(msg) {
13
+ console.log(chalk.yellow('⚠') + ' ' + msg);
14
+ }
15
+ export function spinner(text) {
16
+ return ora({ text, color: 'cyan' });
17
+ }
18
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../types.js';
2
+ export declare function readProjectConfig(dir?: string): ProjectConfig | null;
3
+ export declare function writeProjectConfig(config: ProjectConfig, dir?: string): void;
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const CONFIG_FILE = 'ultikits.json';
4
+ export function readProjectConfig(dir) {
5
+ const filePath = path.join(dir ?? process.cwd(), CONFIG_FILE);
6
+ try {
7
+ const content = fs.readFileSync(filePath, 'utf-8');
8
+ return JSON.parse(content);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export function writeProjectConfig(config, dir) {
15
+ const filePath = path.join(dir ?? process.cwd(), CONFIG_FILE);
16
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
17
+ }
18
+ //# sourceMappingURL=project-config.js.map
@@ -0,0 +1,38 @@
1
+ export interface OAuthCredentials {
2
+ type: 'oauth';
3
+ access_token: string;
4
+ refresh_token: string;
5
+ expires_at: string;
6
+ api_url: string;
7
+ }
8
+ export interface AccessTokenCredentials {
9
+ type: 'access_token';
10
+ token: string;
11
+ api_url: string;
12
+ }
13
+ export type Credentials = OAuthCredentials | AccessTokenCredentials;
14
+ export interface ProjectConfig {
15
+ identifyString: string;
16
+ name: string;
17
+ shortDescription?: string;
18
+ categoryId?: number;
19
+ }
20
+ export interface PluginYml {
21
+ name: string;
22
+ version: string;
23
+ description?: string;
24
+ authors?: string[];
25
+ }
26
+ export interface ModuleMetadata {
27
+ identifyString: string;
28
+ name: string;
29
+ version: string;
30
+ shortDescription?: string;
31
+ changelog?: string;
32
+ categoryId?: number;
33
+ }
34
+ export interface ApiResponse<T = any> {
35
+ code: string;
36
+ msg: string;
37
+ data?: T;
38
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@ultikits/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for publishing UltiTools plugin modules to UltiCloud marketplace",
5
+ "type": "module",
6
+ "bin": {
7
+ "ultikits": "dist/bin/ultikits.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "prepublishOnly": "npm run build && npm test"
15
+ },
16
+ "keywords": [
17
+ "ultikits",
18
+ "minecraft",
19
+ "spigot",
20
+ "paper",
21
+ "plugin",
22
+ "cli",
23
+ "marketplace",
24
+ "module",
25
+ "publish"
26
+ ],
27
+ "author": "UltiKits <dev@ultikits.com> (https://ultikits.com)",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/UltiKits/ultikits-cli.git"
32
+ },
33
+ "homepage": "https://github.com/UltiKits/ultikits-cli#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/UltiKits/ultikits-cli/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "files": [
41
+ "dist/**/*.js",
42
+ "dist/**/*.d.ts",
43
+ "LICENSE",
44
+ "README.md",
45
+ "CHANGELOG.md"
46
+ ],
47
+ "dependencies": {
48
+ "commander": "^12.1.0",
49
+ "inquirer": "^9.3.0",
50
+ "ora": "^8.1.0",
51
+ "chalk": "^5.3.0",
52
+ "jszip": "^3.10.1",
53
+ "js-yaml": "^4.1.0",
54
+ "open": "^10.1.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/inquirer": "^9.0.7",
58
+ "@types/js-yaml": "^4.0.9",
59
+ "@types/node": "^20.14.0",
60
+ "typescript": "^5.5.0",
61
+ "vitest": "^2.0.0"
62
+ }
63
+ }