v8scli 0.2.1 → 0.3.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/bin/v8s.js +9 -1
- package/package.json +1 -1
- package/src/commands/mod.js +168 -0
- package/src/commands/upload.js +2 -2
- package/templates/package.json +1 -1
package/bin/v8s.js
CHANGED
|
@@ -9,14 +9,22 @@ import { deps } from '../src/commands/deps.js';
|
|
|
9
9
|
import { pull } from '../src/commands/pull.js';
|
|
10
10
|
import { search } from '../src/commands/search.js';
|
|
11
11
|
import { add, remove, update } from '../src/commands/add.js';
|
|
12
|
+
import { create, edit, icon, cover, submit, release, whoami } from '../src/commands/mod.js';
|
|
12
13
|
|
|
13
14
|
const [, , command, ...args] = process.argv;
|
|
14
15
|
|
|
15
|
-
const commands = { init, build, login, upload, status, deps, pull, search, add, remove, update
|
|
16
|
+
const commands = { init, build, login, upload, status, deps, pull, search, add, remove, update,
|
|
17
|
+
create, edit, icon, cover, submit, release, whoami };
|
|
16
18
|
|
|
17
19
|
const usage = `v8scli <command>
|
|
18
20
|
|
|
19
21
|
init <slug> [--local] scaffold a new mod in ./<slug> (--local: SDK/CLI from checkouts)
|
|
22
|
+
create [--library] register the mod from ./mod.json on the portal
|
|
23
|
+
edit <flags> change name/description/visibility (--name, --description, --full <file>, --visibility)
|
|
24
|
+
icon <file> upload the square icon cover <file> upload the 16:9 cover
|
|
25
|
+
submit send the latest uploaded version to dev review
|
|
26
|
+
release "notes" publish the latest version to prod (admin approves)
|
|
27
|
+
whoami show whose token is in use
|
|
20
28
|
build bundle the mod locally (self-check, dist/<slug>.js)
|
|
21
29
|
login save portal URL and API token (~/.config/v8s/config.json)
|
|
22
30
|
upload pack sources and upload a new version to the portal
|
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
3
|
+
import { api, readConfig, readManifest } from '../common.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Жизненный цикл мода из CLI: создать, описать, дать картинки, отправить.
|
|
7
|
+
*
|
|
8
|
+
* До этого создание и оформление жили только в веб-панели, и написать мод
|
|
9
|
+
* целиком из редактора было нельзя — приходилось прерываться на браузер.
|
|
10
|
+
* Все команды работают по API-токену.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** modId по slug из mod.json; вызывающему нужен и сам манифест */
|
|
14
|
+
async function resolveMod(config, manifest) {
|
|
15
|
+
const { mods } = await api(config, '/api/mods/my');
|
|
16
|
+
const mod = mods.find((m) => m.slug === manifest.slug);
|
|
17
|
+
if (!mod) throw new Error(`mod "${manifest.slug}" not found on the portal — run \`v8scli create\` first`);
|
|
18
|
+
return mod;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Последняя версия мода — с ней работают submit и release */
|
|
22
|
+
async function latestVersion(config, modId) {
|
|
23
|
+
const { versions } = await api(config, `/api/mods/${modId}`);
|
|
24
|
+
if (!versions?.length) throw new Error('no versions uploaded yet — run `v8scli upload` first');
|
|
25
|
+
return versions[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
29
|
+
|
|
30
|
+
async function uploadImage(config, modId, path, kind) {
|
|
31
|
+
const body = await readFile(path);
|
|
32
|
+
const type = MIME[extname(path).toLowerCase()];
|
|
33
|
+
if (!type) throw new Error(`unsupported image type: ${extname(path)} (png, jpg, webp)`);
|
|
34
|
+
|
|
35
|
+
const form = new FormData();
|
|
36
|
+
form.append('file', new Blob([body], { type }), basename(path));
|
|
37
|
+
|
|
38
|
+
const { mod } = await api(config, `/api/mods/${modId}/${kind}`, { method: 'POST', body: form });
|
|
39
|
+
console.log(`${kind}: ${kind === 'icon' ? mod.iconUrl : mod.coverUrl}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** v8scli create — заводит мод на портале по mod.json текущей папки */
|
|
43
|
+
export async function create(args) {
|
|
44
|
+
const config = await readConfig();
|
|
45
|
+
const manifest = await readManifest();
|
|
46
|
+
|
|
47
|
+
const isLibrary = args.includes('--library') || Boolean(manifest.library);
|
|
48
|
+
|
|
49
|
+
const { mod } = await api(config, '/api/mods/create', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
slug: manifest.slug,
|
|
54
|
+
name: typeof manifest.name === 'string' ? manifest.name : manifest.slug,
|
|
55
|
+
description: typeof manifest.description === 'string' ? manifest.description : '',
|
|
56
|
+
isLibrary,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log(`created: ${mod.slug} (#${mod.id})${isLibrary ? ' [library]' : ''}`);
|
|
61
|
+
console.log('next: v8scli upload');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** v8scli edit --name … --description … --full FILE --visibility … */
|
|
65
|
+
export async function edit(args) {
|
|
66
|
+
const config = await readConfig();
|
|
67
|
+
const manifest = await readManifest();
|
|
68
|
+
const mod = await resolveMod(config, manifest);
|
|
69
|
+
|
|
70
|
+
const value = (flag) => {
|
|
71
|
+
const at = args.indexOf(flag);
|
|
72
|
+
return at >= 0 ? args[at + 1] : undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const body = {};
|
|
76
|
+
if (value('--name')) body.name = value('--name');
|
|
77
|
+
if (value('--description')) body.description = value('--description');
|
|
78
|
+
if (value('--visibility')) body.visibility = value('--visibility');
|
|
79
|
+
if (value('--full')) body.descriptionFull = await readFile(value('--full'), 'utf-8');
|
|
80
|
+
|
|
81
|
+
if (!Object.keys(body).length)
|
|
82
|
+
throw new Error('nothing to change: --name, --description, --full <file>, --visibility <public|whitelist|private>');
|
|
83
|
+
|
|
84
|
+
const { mod: updated } = await api(config, `/api/mods/${mod.id}/update`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(`updated: ${updated.slug} — ${Object.keys(body).join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** v8scli icon <file> / v8scli cover <file> */
|
|
94
|
+
export async function icon(args) {
|
|
95
|
+
const config = await readConfig();
|
|
96
|
+
const mod = await resolveMod(config, await readManifest());
|
|
97
|
+
if (!args[0]) throw new Error('usage: v8scli icon <file.png>');
|
|
98
|
+
await uploadImage(config, mod.id, args[0], 'icon');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function cover(args) {
|
|
102
|
+
const config = await readConfig();
|
|
103
|
+
const mod = await resolveMod(config, await readManifest());
|
|
104
|
+
if (!args[0]) throw new Error('usage: v8scli cover <file.png>');
|
|
105
|
+
await uploadImage(config, mod.id, args[0], 'cover');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** v8scli submit — последнюю загруженную версию на дев-проверку */
|
|
109
|
+
export async function submit() {
|
|
110
|
+
const config = await readConfig();
|
|
111
|
+
const mod = await resolveMod(config, await readManifest());
|
|
112
|
+
const version = await latestVersion(config, mod.id);
|
|
113
|
+
|
|
114
|
+
// У разработчиков команды загрузка сразу даёт dev_ready — отправлять нечего,
|
|
115
|
+
// и это нормальный исход, а не ошибка: сценарий «залил и тестирую» не ломается
|
|
116
|
+
if (version.status === 'dev_ready') {
|
|
117
|
+
console.log(`${version.version}: already dev_ready — ready to test in your lobby`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (version.status !== 'draft')
|
|
122
|
+
throw new Error(`version ${version.version} is ${version.status}, nothing to submit`);
|
|
123
|
+
|
|
124
|
+
const { version: moved } = await api(config, `/api/mods/${mod.id}/versions/${version.id}/action`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ action: 'submit_dev' }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// У разработчиков команды дев-ревью пропускается, и версия сразу dev_ready
|
|
131
|
+
console.log(`${moved.version}: ${moved.status}`);
|
|
132
|
+
console.log(moved.status === 'dev_ready'
|
|
133
|
+
? 'ready to test in your lobby'
|
|
134
|
+
: 'sent to review');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** v8scli release "заметки к релизу" — публикация версии в прод */
|
|
138
|
+
export async function release(args) {
|
|
139
|
+
const config = await readConfig();
|
|
140
|
+
const mod = await resolveMod(config, await readManifest());
|
|
141
|
+
const version = await latestVersion(config, mod.id);
|
|
142
|
+
|
|
143
|
+
const notes = args.join(' ').trim();
|
|
144
|
+
if (notes.length < 10)
|
|
145
|
+
throw new Error('release notes are required (10 characters or more): v8scli release "what changed"');
|
|
146
|
+
|
|
147
|
+
if (version.status === 'draft')
|
|
148
|
+
throw new Error(`version ${version.version} is still a draft — run \`v8scli submit\` first`);
|
|
149
|
+
|
|
150
|
+
const { version: moved } = await api(config, `/api/mods/${mod.id}/versions/${version.id}/action`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({ action: 'submit_release', changelog: notes }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log(`${moved.version}: ${moved.status} — waiting for an admin to publish it`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** v8scli whoami — чей это токен и можно ли им работать */
|
|
160
|
+
export async function whoami() {
|
|
161
|
+
const config = await readConfig();
|
|
162
|
+
const { developer } = await api(config, '/api/developers/me');
|
|
163
|
+
|
|
164
|
+
if (!developer) throw new Error('token is valid but the account is not a developer yet');
|
|
165
|
+
|
|
166
|
+
console.log(`${developer.nickname} (${developer.accountId}) — ${developer.status}`);
|
|
167
|
+
console.log(`portal: ${config.portalUrl.replace(/\/\/[^@]*@/, '//')}`);
|
|
168
|
+
}
|
package/src/commands/upload.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function upload() {
|
|
|
45
45
|
const { mods } = await api(config, '/api/mods/my');
|
|
46
46
|
const mod = mods.find((m) => m.slug === manifest.slug);
|
|
47
47
|
if (!mod)
|
|
48
|
-
throw new Error(`mod "${manifest.slug}" not found on the portal —
|
|
48
|
+
throw new Error(`mod "${manifest.slug}" not found on the portal — run \`v8scli create\` first`);
|
|
49
49
|
|
|
50
50
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
51
51
|
const archive = await packSources(manifest);
|
|
@@ -59,7 +59,7 @@ export async function upload() {
|
|
|
59
59
|
body: form,
|
|
60
60
|
});
|
|
61
61
|
console.log(`uploaded: ${manifest.slug}@${version.version} status=${version.status}`);
|
|
62
|
-
console.log('next: submit
|
|
62
|
+
console.log('next: v8scli submit');
|
|
63
63
|
return;
|
|
64
64
|
} catch (error) {
|
|
65
65
|
const match = String(error?.message ?? '').match(/version_not_incremented: last is (\d+\.\d+\.\d+)/);
|