@ttmg/cli 0.4.6-agent-beta.2 → 0.4.6-agent-beta.3
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 +105 -13
- package/dist/index.js +1167 -168
- package/dist/index.js.map +1 -1
- package/dist/package.json +6 -4
- package/dist/scripts/verify-agent-contract.js +30 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +16 -2
- package/dist/scripts/vibe-upload-smoke.js +189 -7
- package/dist/scripts/wasm-collect-smoke.js +78 -0
- package/dist/scripts/wasm-collect-test-preload.cjs +13 -0
- package/package.json +6 -4
- package/vibe-upload.md +358 -0
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var express = require('express');
|
|
|
21
21
|
var cheerio = require('cheerio');
|
|
22
22
|
var archiver = require('archiver');
|
|
23
23
|
var FormData$1 = require('form-data');
|
|
24
|
+
var https = require('https');
|
|
24
25
|
var promises = require('timers/promises');
|
|
25
26
|
var chokidar = require('chokidar');
|
|
26
27
|
var WebSocket = require('ws');
|
|
@@ -34,7 +35,6 @@ var require$$0 = require('util');
|
|
|
34
35
|
var require$$0$1 = require('events');
|
|
35
36
|
var require$$0$2 = require('buffer');
|
|
36
37
|
var require$$1$2 = require('stream');
|
|
37
|
-
var https = require('https');
|
|
38
38
|
var dns = require('dns');
|
|
39
39
|
var boxen = require('boxen');
|
|
40
40
|
var semver = require('semver');
|
|
@@ -6780,11 +6780,11 @@ function printApiResponseLog(title, result) {
|
|
|
6780
6780
|
['Return Value', result.data],
|
|
6781
6781
|
]);
|
|
6782
6782
|
}
|
|
6783
|
-
async function request({ url, method, data, headers, params, logRequestBody = true, signal, }) {
|
|
6783
|
+
async function request({ url, method, data, headers, params, logRequestBody = true, logApiCalls = true, signal, }) {
|
|
6784
6784
|
const config = getTTMGRC();
|
|
6785
6785
|
const cookie = sanitizeCookieHeader(config?.cookie);
|
|
6786
6786
|
const proxyConfig = getAxiosProxyConfig();
|
|
6787
|
-
if (isVerboseEnabled()) {
|
|
6787
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6788
6788
|
printApiRequestLog({
|
|
6789
6789
|
url,
|
|
6790
6790
|
method,
|
|
@@ -6811,7 +6811,7 @@ async function request({ url, method, data, headers, params, logRequestBody = tr
|
|
|
6811
6811
|
},
|
|
6812
6812
|
...proxyConfig,
|
|
6813
6813
|
});
|
|
6814
|
-
if (isVerboseEnabled()) {
|
|
6814
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6815
6815
|
printApiResponseLog('API Response', {
|
|
6816
6816
|
status: res?.status,
|
|
6817
6817
|
logid: res?.headers['x-tt-logid'],
|
|
@@ -6839,7 +6839,7 @@ async function request({ url, method, data, headers, params, logRequestBody = tr
|
|
|
6839
6839
|
NetworkCode: axiosError.code,
|
|
6840
6840
|
NetworkMessage: axiosError.message,
|
|
6841
6841
|
};
|
|
6842
|
-
if (isVerboseEnabled()) {
|
|
6842
|
+
if (isVerboseEnabled() && logApiCalls) {
|
|
6843
6843
|
printApiResponseLog('API Error', {
|
|
6844
6844
|
status: axiosError.response?.status,
|
|
6845
6845
|
logid: axiosError.response?.headers['x-tt-logid'],
|
|
@@ -7210,6 +7210,16 @@ const messages = {
|
|
|
7210
7210
|
'en-US': {
|
|
7211
7211
|
'cli.command.wasm.desc': 'Prepare or split Wasm into a separate output directory',
|
|
7212
7212
|
'cli.command.wasm.prepare': 'Instrument original Wasm for function collection',
|
|
7213
|
+
'cli.command.wasm.collect': 'Manage platform function collection using the local CLI login',
|
|
7214
|
+
'cli.command.wasm.collect.start': 'Open or resume a collection window; preserve existing IDs unless --reset is set',
|
|
7215
|
+
'cli.command.wasm.collect.export': 'Export collected IDs to an identity-matched collection.json file',
|
|
7216
|
+
'cli.command.wasm.collect.clientKey': 'Client Key for the game being collected',
|
|
7217
|
+
'cli.command.wasm.collect.md5': 'Original Wasm collection MD5 used by the instrumented game',
|
|
7218
|
+
'cli.command.wasm.collect.timeout': 'Platform request deadline: 1–300 seconds',
|
|
7219
|
+
'cli.command.wasm.collect.reset': 'Clear historical function IDs for this game and Wasm before collecting',
|
|
7220
|
+
'cli.command.wasm.collect.output': 'New collection JSON file (never overwrite)',
|
|
7221
|
+
'wasm.collect.succeeded': 'Wasm collection command completed.',
|
|
7222
|
+
'wasm.collect.failed': 'Wasm collection command failed.',
|
|
7213
7223
|
'cli.command.wasm.split': 'Split original Wasm using an identity-matched collection',
|
|
7214
7224
|
'cli.command.wasm.input': 'Original .wasm or Brotli .wasm.br file',
|
|
7215
7225
|
'cli.command.wasm.output': 'New output directory (never overwrite)',
|
|
@@ -7232,6 +7242,29 @@ const messages = {
|
|
|
7232
7242
|
'cli.command.logout.desc': 'Logout and clear local user data',
|
|
7233
7243
|
'cli.command.auth.desc': 'Inspect local authentication state',
|
|
7234
7244
|
'cli.command.auth.open.desc': 'Open an existing Vibe task authorization page in the system browser',
|
|
7245
|
+
'cli.command.upload.vibeWeb': 'Authorize in the browser, then upload to PPE (--vibe; no Mock or --no-open)',
|
|
7246
|
+
'cli.command.game.desc': 'Inspect game metadata supported by the platform',
|
|
7247
|
+
'cli.command.game.categories.desc': 'List game categories and subcategories with gameplay descriptions (offline, read-only)',
|
|
7248
|
+
'cli.command.game.categories.category': 'Filter by an exact category value, e.g. Puzzle',
|
|
7249
|
+
'cli.command.game.categories.unknown': 'Unknown category. Run ttmg game categories --format json for valid values.',
|
|
7250
|
+
'cli.command.game.categories.invalidFormat': 'Use text, json, or ndjson output.',
|
|
7251
|
+
'cli.command.upload.vibeCategory': 'Optional game category; provide with --subcategory (--vibe). Query ttmg game categories --format json',
|
|
7252
|
+
'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
|
|
7253
|
+
'cli.command.upload.vibeWebGuide': `
|
|
7254
|
+
Vibe web upload (real PPE trial):
|
|
7255
|
+
ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
|
|
7256
|
+
Use --package ./game.zip instead of --dir for an existing ZIP.
|
|
7257
|
+
Collect only title and icon; the CLI fills other app_info fields with PPE trial defaults.
|
|
7258
|
+
Skill may query ttmg game categories --format json and pass --category Puzzle --subcategory Physics together.
|
|
7259
|
+
Omit both to keep Other/Other (or the legacy metadata-file classification). Invalid pairs fail before task creation.
|
|
7260
|
+
prepared and terminal events expose the submitted classification and its source.
|
|
7261
|
+
Keep this process running. At waiting_authorization, complete authorization in the system browser on the CLI computer.
|
|
7262
|
+
pageUrl is the local binding/progress page. To reopen authorization, execute the returned authorizationOpenCommand.
|
|
7263
|
+
The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
|
|
7264
|
+
Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
|
|
7265
|
+
--web opens the browser for every output format. --mock is a separate simulation mode.
|
|
7266
|
+
Upload completion is not review approval or production release. See bundled vibe-upload.md and ttmg capabilities --format json.
|
|
7267
|
+
`,
|
|
7235
7268
|
'cli.command.auth.open.pageUrl': 'Local pageUrl returned by the running Vibe upload command',
|
|
7236
7269
|
'cli.command.auth.open.requested': 'Browser open requested. Complete authorization there; the existing upload process continues polling.',
|
|
7237
7270
|
'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
|
|
@@ -7434,9 +7467,9 @@ const messages = {
|
|
|
7434
7467
|
'cli.command.upload.servePreviewPage': 'Serve a temporary local preview page with test-account and preview QR codes',
|
|
7435
7468
|
'cli.command.upload.noOpen': 'Do not open the local preview page in the default browser',
|
|
7436
7469
|
'cli.command.upload.format': 'Output format: text | json | ndjson',
|
|
7437
|
-
'cli.command.upload.vibe': '
|
|
7470
|
+
'cli.command.upload.vibe': 'Binding and upload: collect only title and icon; CLI fills other fields with PPE trial defaults (schema v2)',
|
|
7438
7471
|
'cli.command.upload.vibeTitle': 'Game title supplied by the Skill (--vibe)',
|
|
7439
|
-
'cli.command.upload.vibeAppInfo': '
|
|
7472
|
+
'cli.command.upload.vibeAppInfo': 'Legacy metadata file compatibility; not needed for current uploads and must not be requested from users',
|
|
7440
7473
|
'cli.command.upload.vibeIcon': 'Local PNG/JPEG/WebP selected by the user (--vibe)',
|
|
7441
7474
|
'cli.command.upload.vibePackage': 'Prepared ZIP instead of --dir (--vibe)',
|
|
7442
7475
|
'cli.command.upload.vibeDryRun': 'Validate and package locally without authorization or upload (--vibe)',
|
|
@@ -7622,6 +7655,16 @@ const messages = {
|
|
|
7622
7655
|
'zh-CN': {
|
|
7623
7656
|
'cli.command.wasm.desc': '准备或分包 Wasm,产物写入独立目录',
|
|
7624
7657
|
'cli.command.wasm.prepare': '对原始 Wasm 插桩,用于函数采集',
|
|
7658
|
+
'cli.command.wasm.collect': '复用 CLI 本机登录态管理平台函数采集',
|
|
7659
|
+
'cli.command.wasm.collect.start': '开启或恢复采集窗口;未指定 --reset 时保留已有函数 ID',
|
|
7660
|
+
'cli.command.wasm.collect.export': '导出函数 ID,生成与原始 Wasm 匹配的 collection.json',
|
|
7661
|
+
'cli.command.wasm.collect.clientKey': '采集游戏的 Client Key',
|
|
7662
|
+
'cli.command.wasm.collect.md5': '插桩游戏上报使用的原始 Wasm 采集 MD5',
|
|
7663
|
+
'cli.command.wasm.collect.timeout': '平台请求超时:1–300 秒',
|
|
7664
|
+
'cli.command.wasm.collect.reset': '清空此游戏、此 Wasm 的历史函数 ID,再开始采集',
|
|
7665
|
+
'cli.command.wasm.collect.output': '新的采集 JSON 文件(不覆盖已有文件)',
|
|
7666
|
+
'wasm.collect.succeeded': 'Wasm 采集命令已完成。',
|
|
7667
|
+
'wasm.collect.failed': 'Wasm 采集命令失败。',
|
|
7625
7668
|
'cli.command.wasm.split': '使用与原始 Wasm 匹配的采集数据分包',
|
|
7626
7669
|
'cli.command.wasm.input': '原始 .wasm 或 Brotli .wasm.br 文件',
|
|
7627
7670
|
'cli.command.wasm.output': '新的输出目录(不覆盖已有内容)',
|
|
@@ -7644,6 +7687,29 @@ const messages = {
|
|
|
7644
7687
|
'cli.command.logout.desc': '退出登录并清除本地用户信息',
|
|
7645
7688
|
'cli.command.auth.desc': '查看本地登录状态',
|
|
7646
7689
|
'cli.command.auth.open.desc': '在系统浏览器打开已有 Vibe 任务的授权页',
|
|
7690
|
+
'cli.command.upload.vibeWeb': '网页授权就绪后自动上传到 PPE(--vibe;不能与 Mock 或 --no-open 同用)',
|
|
7691
|
+
'cli.command.game.desc': '查询平台支持的游戏资料',
|
|
7692
|
+
'cli.command.game.categories.desc': '查询游戏类型、子类型及玩法说明(离线、只读)',
|
|
7693
|
+
'cli.command.game.categories.category': '按类型的精确值筛选,例如 Puzzle',
|
|
7694
|
+
'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
|
|
7695
|
+
'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
|
|
7696
|
+
'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
|
|
7697
|
+
'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
|
|
7698
|
+
'cli.command.upload.vibeWebGuide': `
|
|
7699
|
+
Vibe 网页上传(真实 PPE 试用):
|
|
7700
|
+
ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
|
|
7701
|
+
已有 ZIP 时,用 --package ./game.zip 替换 --dir。
|
|
7702
|
+
只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
|
|
7703
|
+
Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
|
|
7704
|
+
两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
|
|
7705
|
+
prepared 和终态事件返回最终 classification 及其来源。
|
|
7706
|
+
保持原进程运行。在 waiting_authorization 阶段,在 CLI 所在电脑的系统浏览器完成授权。
|
|
7707
|
+
pageUrl 是本机绑定/进度页;重新打开授权页时,执行返回的 authorizationOpenCommand。
|
|
7708
|
+
CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
|
|
7709
|
+
真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
|
|
7710
|
+
--web 在所有输出格式下都会打开浏览器;--mock 是独立的模拟模式。
|
|
7711
|
+
上传完成不代表审核通过或正式上线。详见包内 vibe-upload.md 和 ttmg capabilities --format json。
|
|
7712
|
+
`,
|
|
7647
7713
|
'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
|
|
7648
7714
|
'cli.command.auth.open.requested': '已请求浏览器打开授权页。请在页面完成授权,原上传进程继续轮询。',
|
|
7649
7715
|
'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
|
|
@@ -7846,8 +7912,8 @@ const messages = {
|
|
|
7846
7912
|
'cli.command.upload.servePreviewPage': '启动包含测试账号与预览二维码的临时本地页面',
|
|
7847
7913
|
'cli.command.upload.noOpen': '不使用默认浏览器打开本地预览页面',
|
|
7848
7914
|
'cli.command.upload.format': '输出格式:text | json | ndjson',
|
|
7849
|
-
'cli.command.upload.vibe': '
|
|
7850
|
-
'cli.command.upload.vibeAppInfo': '
|
|
7915
|
+
'cli.command.upload.vibe': '绑定上传:只收集标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值(schema v2)',
|
|
7916
|
+
'cli.command.upload.vibeAppInfo': '仅兼容旧资料文件;当前上传无需此参数,不应向用户索要',
|
|
7851
7917
|
'cli.command.upload.vibeTitle': 'Skill 提供的游戏标题(--vibe)',
|
|
7852
7918
|
'cli.command.upload.vibeIcon': '用户指定的本地 PNG/JPEG/WebP 图片(--vibe)',
|
|
7853
7919
|
'cli.command.upload.vibePackage': '已准备的 ZIP,与 --dir 二选一(--vibe)',
|
|
@@ -16306,7 +16372,7 @@ async function authStatus(options = {}) {
|
|
|
16306
16372
|
return result;
|
|
16307
16373
|
}
|
|
16308
16374
|
|
|
16309
|
-
var version = "0.4.6-agent-beta.
|
|
16375
|
+
var version = "0.4.6-agent-beta.3";
|
|
16310
16376
|
var engines = {
|
|
16311
16377
|
node: ">=20.19.1 <21 || >=22"
|
|
16312
16378
|
};
|
|
@@ -16315,6 +16381,722 @@ var pkg = {
|
|
|
16315
16381
|
engines: engines
|
|
16316
16382
|
};
|
|
16317
16383
|
|
|
16384
|
+
// Skill-facing v2 contract. Backend credentials never cross this boundary.
|
|
16385
|
+
const VIBE_INPUT_REQUIREMENTS = {
|
|
16386
|
+
requiredUserInputs: ['title', 'icon'],
|
|
16387
|
+
codeSource: ['dir', 'package'],
|
|
16388
|
+
additionalMetadataSource: 'cli-defaults',
|
|
16389
|
+
defaultMetadataProfile: 'ppe-trial',
|
|
16390
|
+
requestAdditionalMetadata: false,
|
|
16391
|
+
optionalSkillInputs: ['category', 'subcategory'],
|
|
16392
|
+
categoryQuery: 'ttmg game categories --format json',
|
|
16393
|
+
categoryPairRequiredWhenProvided: true,
|
|
16394
|
+
defaultCategory: { category: 'Other', subcategory: 'Other' },
|
|
16395
|
+
};
|
|
16396
|
+
class VibeError extends Error {
|
|
16397
|
+
constructor(code) {
|
|
16398
|
+
super(code);
|
|
16399
|
+
this.code = code;
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16402
|
+
|
|
16403
|
+
var catalogVersion = "sha256:f1d67b08cfbfb9396cba1ec246a84a4c1b6b2c9f077e236498c42e9c154b6900";
|
|
16404
|
+
var source = "bundled";
|
|
16405
|
+
var categories$1 = [
|
|
16406
|
+
{
|
|
16407
|
+
value: "Action",
|
|
16408
|
+
subcategories: [
|
|
16409
|
+
{
|
|
16410
|
+
value: "Artillery Shooter",
|
|
16411
|
+
description: "Turn based tactics game in which players aim and shoot other players"
|
|
16412
|
+
},
|
|
16413
|
+
{
|
|
16414
|
+
value: "Asymmetric Battle",
|
|
16415
|
+
description: "Game in which different players have different roles and capabilities as well as win condition (e.g. Identity 5)"
|
|
16416
|
+
},
|
|
16417
|
+
{
|
|
16418
|
+
value: "Brawl",
|
|
16419
|
+
description: "Multiplayer battle where player controls a character in team deathmatch battle without aiming a gun"
|
|
16420
|
+
},
|
|
16421
|
+
{
|
|
16422
|
+
value: "Fighting",
|
|
16423
|
+
description: "A game where the player controls a character to fight against other characters"
|
|
16424
|
+
},
|
|
16425
|
+
{
|
|
16426
|
+
value: "MOBA",
|
|
16427
|
+
description: "Multiplayer battle arena where the player controls a character on a team to destroy an enemy base"
|
|
16428
|
+
},
|
|
16429
|
+
{
|
|
16430
|
+
value: "Music & Rhythm",
|
|
16431
|
+
description: "Games that feature music as the primary gameplay driver"
|
|
16432
|
+
},
|
|
16433
|
+
{
|
|
16434
|
+
value: "Open World",
|
|
16435
|
+
description: "Action game in which players can explore the world and attempt objectives freely"
|
|
16436
|
+
},
|
|
16437
|
+
{
|
|
16438
|
+
value: "Platformer",
|
|
16439
|
+
description: "A game where players complete stages by avoiding obstacles and jumping between platforms"
|
|
16440
|
+
},
|
|
16441
|
+
{
|
|
16442
|
+
value: "Royale Battle",
|
|
16443
|
+
description: "Battle royale game (many players compete but with a single winner) that is action based and not a shooter"
|
|
16444
|
+
},
|
|
16445
|
+
{
|
|
16446
|
+
value: "Run & Gun Platformer",
|
|
16447
|
+
description: "Action games similar to side-scrolling platformers, but players are typically battling an onslaught of enemies appearing in front or behind them"
|
|
16448
|
+
},
|
|
16449
|
+
{
|
|
16450
|
+
value: "Runner",
|
|
16451
|
+
description: "A game where the player automatically progresses and must avoid obstacles to continue"
|
|
16452
|
+
},
|
|
16453
|
+
{
|
|
16454
|
+
value: "Shark",
|
|
16455
|
+
description: "Game in which the player eats other objects in the game to grow or stay alive"
|
|
16456
|
+
},
|
|
16457
|
+
{
|
|
16458
|
+
value: "Shoot'em Up",
|
|
16459
|
+
description: "A game where a lone unit (usually a spaceship) shoots projectiles against a large number of other enemies. Generally in a 2D movement space"
|
|
16460
|
+
},
|
|
16461
|
+
{
|
|
16462
|
+
value: "Slicing (Slasher)",
|
|
16463
|
+
description: "Game in which the main objective is to cut objects into pieces using a swiping motion"
|
|
16464
|
+
},
|
|
16465
|
+
{
|
|
16466
|
+
value: "Stealth",
|
|
16467
|
+
description: "Game where the player attempts to sneak around a game world and achieve objectives or escape"
|
|
16468
|
+
},
|
|
16469
|
+
{
|
|
16470
|
+
value: "Adventure",
|
|
16471
|
+
description: "Focuses on the actions themselves (shooting, twitch combat, etc.) or exploration of the world instead (finding non-gameplay collectables, solving puzzles, etc.) - Super Mario"
|
|
16472
|
+
},
|
|
16473
|
+
{
|
|
16474
|
+
value: "Beat 'em up",
|
|
16475
|
+
description: "Featuring hand-to-hand combat between the protagonist and an improbably large number of opponents."
|
|
16476
|
+
},
|
|
16477
|
+
{
|
|
16478
|
+
value: "Other Action",
|
|
16479
|
+
description: "Other general action gameplay or games that feature arcade-style gameplay"
|
|
16480
|
+
}
|
|
16481
|
+
]
|
|
16482
|
+
},
|
|
16483
|
+
{
|
|
16484
|
+
value: "Match",
|
|
16485
|
+
subcategories: [
|
|
16486
|
+
{
|
|
16487
|
+
value: "Blast-Meta",
|
|
16488
|
+
description: "Blast-style game with a heavy emphasis on an investable meta game system"
|
|
16489
|
+
},
|
|
16490
|
+
{
|
|
16491
|
+
value: "Blast-Saga",
|
|
16492
|
+
description: "Game where primary mechanic involves blasting/removing adjacent like items with saga based progression"
|
|
16493
|
+
},
|
|
16494
|
+
{
|
|
16495
|
+
value: "Bubble Shooter",
|
|
16496
|
+
description: "Game where bubbles are shot at different angles clearing groups of bubbles arranged on top of screen"
|
|
16497
|
+
},
|
|
16498
|
+
{
|
|
16499
|
+
value: "Connect Tiles",
|
|
16500
|
+
description: "Game in which two like tiles are linked via a pathway on a board and removed from the board"
|
|
16501
|
+
},
|
|
16502
|
+
{
|
|
16503
|
+
value: "Line-Saga",
|
|
16504
|
+
description: "Game where a line is created between like items to remove them from the board with saga based progression"
|
|
16505
|
+
},
|
|
16506
|
+
{
|
|
16507
|
+
value: "M3-Meta",
|
|
16508
|
+
description: "Match 3 swapping game with a focus on some form of building in its meta game"
|
|
16509
|
+
},
|
|
16510
|
+
{
|
|
16511
|
+
value: "M3-Saga",
|
|
16512
|
+
description: "Primary gameplay mechanism is matching 3 items on screen and swapping matched items"
|
|
16513
|
+
},
|
|
16514
|
+
{
|
|
16515
|
+
value: "Merge-Saga",
|
|
16516
|
+
description: "Game with focus on merging like items to create more powerful units or items and utilizing saga based progression"
|
|
16517
|
+
},
|
|
16518
|
+
{
|
|
16519
|
+
value: "Other-Meta",
|
|
16520
|
+
description: "Other match style game with a heavy emphasis on an investable meta game system"
|
|
16521
|
+
},
|
|
16522
|
+
{
|
|
16523
|
+
value: "Other Match",
|
|
16524
|
+
description: "Other kind of matching game"
|
|
16525
|
+
}
|
|
16526
|
+
]
|
|
16527
|
+
},
|
|
16528
|
+
{
|
|
16529
|
+
value: "Puzzle",
|
|
16530
|
+
subcategories: [
|
|
16531
|
+
{
|
|
16532
|
+
value: "Block Puzzle",
|
|
16533
|
+
description: "Game in which players need to fit different shaped block pieces to fit onto a game board efficiently"
|
|
16534
|
+
},
|
|
16535
|
+
{
|
|
16536
|
+
value: "Brain Puzzle",
|
|
16537
|
+
description: "Knowledge or intelligence-based puzzle games"
|
|
16538
|
+
},
|
|
16539
|
+
{
|
|
16540
|
+
value: "Brick-crusher",
|
|
16541
|
+
description: "A bubble shooter game featuring many balls shot, high action, and real time (not turn based) gameplay"
|
|
16542
|
+
},
|
|
16543
|
+
{
|
|
16544
|
+
value: "Detective",
|
|
16545
|
+
description: "Game in which players must solve puzzles based on various clues and involves solving a narrative arc"
|
|
16546
|
+
},
|
|
16547
|
+
{
|
|
16548
|
+
value: "Hidden Object",
|
|
16549
|
+
description: "Games where the primary objective is identifying visual elements"
|
|
16550
|
+
},
|
|
16551
|
+
{
|
|
16552
|
+
value: "Hybrid Puzzle",
|
|
16553
|
+
description: "Puzzle game involving multiple types of puzzles instead of focusing on just one"
|
|
16554
|
+
},
|
|
16555
|
+
{
|
|
16556
|
+
value: "Jigsaw",
|
|
16557
|
+
description: "Jigsaw puzzle game"
|
|
16558
|
+
},
|
|
16559
|
+
{
|
|
16560
|
+
value: "Klotski",
|
|
16561
|
+
description: "A sliding block puzzle game"
|
|
16562
|
+
},
|
|
16563
|
+
{
|
|
16564
|
+
value: "Maze",
|
|
16565
|
+
description: "Game in which players must determine paths out of a maze"
|
|
16566
|
+
},
|
|
16567
|
+
{
|
|
16568
|
+
value: "Minesweeper",
|
|
16569
|
+
description: "Games similar to the classic Minesweeper game"
|
|
16570
|
+
},
|
|
16571
|
+
{
|
|
16572
|
+
value: "Nonogram",
|
|
16573
|
+
description: "Game in which cells in a grid are colored or left blank according to numbers on the side of a grid to reveal a hidden picture"
|
|
16574
|
+
},
|
|
16575
|
+
{
|
|
16576
|
+
value: "Physics",
|
|
16577
|
+
description: "Games that utilize a physics engine-based primary gameplay mechanism"
|
|
16578
|
+
},
|
|
16579
|
+
{
|
|
16580
|
+
value: "Room Escape",
|
|
16581
|
+
description: "Puzzles focused on progressing a player to escape from a room or building"
|
|
16582
|
+
},
|
|
16583
|
+
{
|
|
16584
|
+
value: "Spot the Difference",
|
|
16585
|
+
description: "Matching images and determining the difference between the images"
|
|
16586
|
+
},
|
|
16587
|
+
{
|
|
16588
|
+
value: "Sudoku",
|
|
16589
|
+
description: "Sudoku based game"
|
|
16590
|
+
},
|
|
16591
|
+
{
|
|
16592
|
+
value: "Tile-Matching",
|
|
16593
|
+
description: "Game in which tiles are matched to remove them from the game board (like Tetris)"
|
|
16594
|
+
},
|
|
16595
|
+
{
|
|
16596
|
+
value: "Word",
|
|
16597
|
+
description: "Word Games"
|
|
16598
|
+
},
|
|
16599
|
+
{
|
|
16600
|
+
value: "Other Puzzle",
|
|
16601
|
+
description: "Puzzle-based games that do not fall into the above categories"
|
|
16602
|
+
}
|
|
16603
|
+
]
|
|
16604
|
+
},
|
|
16605
|
+
{
|
|
16606
|
+
value: "Racing",
|
|
16607
|
+
subcategories: [
|
|
16608
|
+
{
|
|
16609
|
+
value: "Arcade Racing",
|
|
16610
|
+
description: "Casual and fun racing arcade type of game"
|
|
16611
|
+
},
|
|
16612
|
+
{
|
|
16613
|
+
value: "Competitive Racing",
|
|
16614
|
+
description: "PVP Racing that can use a variety of control schemes"
|
|
16615
|
+
},
|
|
16616
|
+
{
|
|
16617
|
+
value: "Simulation Racing",
|
|
16618
|
+
description: "Simulated game of realistic racing"
|
|
16619
|
+
}
|
|
16620
|
+
]
|
|
16621
|
+
},
|
|
16622
|
+
{
|
|
16623
|
+
value: "RPG",
|
|
16624
|
+
subcategories: [
|
|
16625
|
+
{
|
|
16626
|
+
value: "Empire Simulation RPG",
|
|
16627
|
+
description: "RPG game in which the player rules an empire or kingdom of some kind"
|
|
16628
|
+
},
|
|
16629
|
+
{
|
|
16630
|
+
value: "Idle RPG",
|
|
16631
|
+
description: "Squad based RPG that includes idle progression mechanics"
|
|
16632
|
+
},
|
|
16633
|
+
{
|
|
16634
|
+
value: "Location RPG",
|
|
16635
|
+
description: "Game where location plays a critical component in the core gameplay"
|
|
16636
|
+
},
|
|
16637
|
+
{
|
|
16638
|
+
value: "MMORPG",
|
|
16639
|
+
description: "Massively multiplayer online role-playing game typically featuring worlds or servers with hundreds or thousands of players online simultaneously"
|
|
16640
|
+
},
|
|
16641
|
+
{
|
|
16642
|
+
value: "Open World RPG",
|
|
16643
|
+
description: "RPG game where players traverse an open world relatively freely"
|
|
16644
|
+
},
|
|
16645
|
+
{
|
|
16646
|
+
value: "Puzzle RPG",
|
|
16647
|
+
description: "RPG games that utilize a puzzle system as the primary combat mechanism"
|
|
16648
|
+
},
|
|
16649
|
+
{
|
|
16650
|
+
value: "Roguelike ARPG",
|
|
16651
|
+
description: "ARPG with roguelike (sub-genre of role-playing games characterized by the exploration of randomly-generated dungeons and permanent death) gameplay elements (e.g. Hades)"
|
|
16652
|
+
},
|
|
16653
|
+
{
|
|
16654
|
+
value: "Survival",
|
|
16655
|
+
description: "RPG based game with a strong focus on crafting mechanics"
|
|
16656
|
+
},
|
|
16657
|
+
{
|
|
16658
|
+
value: "Tactics",
|
|
16659
|
+
description: "RPG game involving turn based movement of different characters on a tile-based map"
|
|
16660
|
+
},
|
|
16661
|
+
{
|
|
16662
|
+
value: "Team Battle",
|
|
16663
|
+
description: "RPG where you control multiple characters to advance the gameplay and story"
|
|
16664
|
+
},
|
|
16665
|
+
{
|
|
16666
|
+
value: "Tower Defense RPG",
|
|
16667
|
+
description: "Tower defense game with a strong emphasis on character collection and stateful upgrading"
|
|
16668
|
+
},
|
|
16669
|
+
{
|
|
16670
|
+
value: "Other RPG",
|
|
16671
|
+
description: "Other RPG Game"
|
|
16672
|
+
}
|
|
16673
|
+
]
|
|
16674
|
+
},
|
|
16675
|
+
{
|
|
16676
|
+
value: "Shooting",
|
|
16677
|
+
subcategories: [
|
|
16678
|
+
{
|
|
16679
|
+
value: "Battle Royale",
|
|
16680
|
+
description: "Shooter game in which battle royale is the primary game mode"
|
|
16681
|
+
},
|
|
16682
|
+
{
|
|
16683
|
+
value: "Cover-fire",
|
|
16684
|
+
description: "Shooting game where the player takes cover and shoots out of cover"
|
|
16685
|
+
},
|
|
16686
|
+
{
|
|
16687
|
+
value: "PvE Shooting",
|
|
16688
|
+
description: "Shooter game in which players primarily battle against computer generated, player-vs-environment enemies"
|
|
16689
|
+
},
|
|
16690
|
+
{
|
|
16691
|
+
value: "Sniper",
|
|
16692
|
+
description: "Shooting game focused on small number of accurate shots through a scope"
|
|
16693
|
+
},
|
|
16694
|
+
{
|
|
16695
|
+
value: "Team Deathmatch",
|
|
16696
|
+
description: "Shooter game in which team deathmatch is the primary gameplay mode"
|
|
16697
|
+
}
|
|
16698
|
+
]
|
|
16699
|
+
},
|
|
16700
|
+
{
|
|
16701
|
+
value: "Simulation",
|
|
16702
|
+
subcategories: [
|
|
16703
|
+
{
|
|
16704
|
+
value: "Animal Avatar",
|
|
16705
|
+
description: "Game in which players take the form and play as an animal in a virtual world"
|
|
16706
|
+
},
|
|
16707
|
+
{
|
|
16708
|
+
value: "Avatar Life",
|
|
16709
|
+
description: "Simulated life based on avatars in an alternate reality world"
|
|
16710
|
+
},
|
|
16711
|
+
{
|
|
16712
|
+
value: "Build Breed",
|
|
16713
|
+
description: "Games in which the player grows a city used to breed creatures for combat"
|
|
16714
|
+
},
|
|
16715
|
+
{
|
|
16716
|
+
value: "Building",
|
|
16717
|
+
description: "Games focused on building up a city with limited/no focus on combat"
|
|
16718
|
+
},
|
|
16719
|
+
{
|
|
16720
|
+
value: "Creative Sandbox",
|
|
16721
|
+
description: "Players have tools to create user generated levels or games shared with the overall game community"
|
|
16722
|
+
},
|
|
16723
|
+
{
|
|
16724
|
+
value: "Dress Up & Lifestyle",
|
|
16725
|
+
description: "Games built around dressing up avatars and simulated life-like experiences with emphasis on lifestyle"
|
|
16726
|
+
},
|
|
16727
|
+
{
|
|
16728
|
+
value: "Driving",
|
|
16729
|
+
description: "Games where the primary gameplay loop is centered around simulating driving or parking (non-competitive)."
|
|
16730
|
+
},
|
|
16731
|
+
{
|
|
16732
|
+
value: "Farming",
|
|
16733
|
+
description: "Farming-based simulation titles"
|
|
16734
|
+
},
|
|
16735
|
+
{
|
|
16736
|
+
value: "Home Design",
|
|
16737
|
+
description: "Games involving decorating homes"
|
|
16738
|
+
},
|
|
16739
|
+
{
|
|
16740
|
+
value: "Idle",
|
|
16741
|
+
description: "Games where game progression continues even when a player leaves the game session"
|
|
16742
|
+
},
|
|
16743
|
+
{
|
|
16744
|
+
value: "Idol Training Sim",
|
|
16745
|
+
description: "Game in which players train and level up Japanese style idols against different types of activities or professions"
|
|
16746
|
+
},
|
|
16747
|
+
{
|
|
16748
|
+
value: "Life Simulator",
|
|
16749
|
+
description: "Simulation of a virtual character from birth to death in which the player makes different life choices"
|
|
16750
|
+
},
|
|
16751
|
+
{
|
|
16752
|
+
value: "Manager",
|
|
16753
|
+
description: "Games simulating the management of a team or business"
|
|
16754
|
+
},
|
|
16755
|
+
{
|
|
16756
|
+
value: "Pet",
|
|
16757
|
+
description: "Games where the primary gameplay loop is centered around raising a pet (non-combat)"
|
|
16758
|
+
},
|
|
16759
|
+
{
|
|
16760
|
+
value: "Time Management",
|
|
16761
|
+
description: "Simulation games where players are challenged to complete tasks or quests within a specific (usually short) time frame"
|
|
16762
|
+
},
|
|
16763
|
+
{
|
|
16764
|
+
value: "Weapon Simulator",
|
|
16765
|
+
description: "Video game that focuses on simulating the experience of using various weapons. These games aim to provide players with a realistic or immersive representation of different types of firearms, melee weapons, or other tools used in combat"
|
|
16766
|
+
},
|
|
16767
|
+
{
|
|
16768
|
+
value: "Other Sim",
|
|
16769
|
+
description: "Other Sim/Management games"
|
|
16770
|
+
}
|
|
16771
|
+
]
|
|
16772
|
+
},
|
|
16773
|
+
{
|
|
16774
|
+
value: "Sports",
|
|
16775
|
+
subcategories: [
|
|
16776
|
+
{
|
|
16777
|
+
value: "Arcade Sports",
|
|
16778
|
+
description: "Sports games where gameplay focuses on non-realistic simulation of gameplay"
|
|
16779
|
+
},
|
|
16780
|
+
{
|
|
16781
|
+
value: "Simulation Sports",
|
|
16782
|
+
description: "Sports games where the gameplay focuses on realistic simulation of gameplay"
|
|
16783
|
+
},
|
|
16784
|
+
{
|
|
16785
|
+
value: "Team Manager",
|
|
16786
|
+
description: "Sports games where the player focuses on manage a team"
|
|
16787
|
+
},
|
|
16788
|
+
{
|
|
16789
|
+
value: "Other Sports",
|
|
16790
|
+
description: "Other Sports Games"
|
|
16791
|
+
}
|
|
16792
|
+
]
|
|
16793
|
+
},
|
|
16794
|
+
{
|
|
16795
|
+
value: "Strategy",
|
|
16796
|
+
subcategories: [
|
|
16797
|
+
{
|
|
16798
|
+
value: "4X March-Battle",
|
|
16799
|
+
description: "Games where player grows a city, builds an army that attacks through timed marches on a map"
|
|
16800
|
+
},
|
|
16801
|
+
{
|
|
16802
|
+
value: "Auto Chess",
|
|
16803
|
+
description: "Tactical strategy game with drafting and in which play is conducted PVP in multiple rounds"
|
|
16804
|
+
},
|
|
16805
|
+
{
|
|
16806
|
+
value: "Build-Battle",
|
|
16807
|
+
description: "Games focused on building troops that attack enemies and can be lost. City building is usually used to upgrade troops."
|
|
16808
|
+
},
|
|
16809
|
+
{
|
|
16810
|
+
value: "CCG-Battle",
|
|
16811
|
+
description: "Strategy game based on card decks typically associated with mana costs to play"
|
|
16812
|
+
},
|
|
16813
|
+
{
|
|
16814
|
+
value: "Lane-Battle",
|
|
16815
|
+
description: "Strategy game involving multiple lanes where units are sent down a specific lane to battle"
|
|
16816
|
+
},
|
|
16817
|
+
{
|
|
16818
|
+
value: "Legion Battle",
|
|
16819
|
+
description: "Game with a focus on building different types of army units and battling other players' armies"
|
|
16820
|
+
},
|
|
16821
|
+
{
|
|
16822
|
+
value: "Summon-Battle",
|
|
16823
|
+
description: "Games where units are summoned via some means (e.g., cards or mana) and then auto-battle"
|
|
16824
|
+
},
|
|
16825
|
+
{
|
|
16826
|
+
value: "Tower Defense Strategy",
|
|
16827
|
+
description: "Games where players establish defenses against NPC enemies that attack the player at regular intervals"
|
|
16828
|
+
},
|
|
16829
|
+
{
|
|
16830
|
+
value: "RTS",
|
|
16831
|
+
description: "Real-time Strategy is a strategy game that allow all users to play simultaneously instead of processing incrementally in turns"
|
|
16832
|
+
},
|
|
16833
|
+
{
|
|
16834
|
+
value: "Other Strategy",
|
|
16835
|
+
description: "Other strategy games"
|
|
16836
|
+
}
|
|
16837
|
+
]
|
|
16838
|
+
},
|
|
16839
|
+
{
|
|
16840
|
+
value: "Party",
|
|
16841
|
+
subcategories: [
|
|
16842
|
+
{
|
|
16843
|
+
value: "Guess Something",
|
|
16844
|
+
description: "Social game in which players must guess what another player has in mind from different kinds of hints or drawings"
|
|
16845
|
+
},
|
|
16846
|
+
{
|
|
16847
|
+
value: "Karaoke",
|
|
16848
|
+
description: "A Karaoke game is a type of video or computer game that simulates the experience of karaoke singing"
|
|
16849
|
+
},
|
|
16850
|
+
{
|
|
16851
|
+
value: "Dancing",
|
|
16852
|
+
description: "Video game that involves gameplay elements centered around dance and rhythm. These games often require players to mimic dance moves or follow on-screen prompts in synchronization with music."
|
|
16853
|
+
},
|
|
16854
|
+
{
|
|
16855
|
+
value: "Mafia/Betrayal",
|
|
16856
|
+
description: "Mafia based game"
|
|
16857
|
+
},
|
|
16858
|
+
{
|
|
16859
|
+
value: "Mini Games Collection",
|
|
16860
|
+
description: "Multiplayer social game in which players can choose to play from a collection of multiple minigames"
|
|
16861
|
+
},
|
|
16862
|
+
{
|
|
16863
|
+
value: "Party Royale",
|
|
16864
|
+
description: "Party based battle royale game"
|
|
16865
|
+
},
|
|
16866
|
+
{
|
|
16867
|
+
value: "Quiz",
|
|
16868
|
+
description: "Game involving trivia or quizzes"
|
|
16869
|
+
},
|
|
16870
|
+
{
|
|
16871
|
+
value: "Other Party",
|
|
16872
|
+
description: "Other party game"
|
|
16873
|
+
}
|
|
16874
|
+
]
|
|
16875
|
+
},
|
|
16876
|
+
{
|
|
16877
|
+
value: "Tabletop",
|
|
16878
|
+
subcategories: [
|
|
16879
|
+
{
|
|
16880
|
+
value: "Board Game",
|
|
16881
|
+
description: "Board game is a game that involves the movement of counters or other objects round a board."
|
|
16882
|
+
},
|
|
16883
|
+
{
|
|
16884
|
+
value: "Card Games",
|
|
16885
|
+
description: "[Non gambling] A card game is any game using playing cards as the primary device with which the game is played, be they traditional or game-specific"
|
|
16886
|
+
},
|
|
16887
|
+
{
|
|
16888
|
+
value: "Chess",
|
|
16889
|
+
description: "Chess is a board game for two players, called White and Black. It is sometimes called international chess or Western chess"
|
|
16890
|
+
},
|
|
16891
|
+
{
|
|
16892
|
+
value: "Dice",
|
|
16893
|
+
description: "Dice games are games that use or incorporate one or more dice as their sole or central component, usually as a random device"
|
|
16894
|
+
},
|
|
16895
|
+
{
|
|
16896
|
+
value: "Ludo",
|
|
16897
|
+
description: "Ludo is a strategy board game for two to four players, in which the players race their four tokens from start to finish according to the rolls of a single die"
|
|
16898
|
+
},
|
|
16899
|
+
{
|
|
16900
|
+
value: "Othello",
|
|
16901
|
+
description: "Game based on the classic Othello game"
|
|
16902
|
+
},
|
|
16903
|
+
{
|
|
16904
|
+
value: "Solitaire",
|
|
16905
|
+
description: "Solitaire games are card based games that can be played by a single player."
|
|
16906
|
+
},
|
|
16907
|
+
{
|
|
16908
|
+
value: "Other Tabletop",
|
|
16909
|
+
description: "Other tabletop game"
|
|
16910
|
+
}
|
|
16911
|
+
]
|
|
16912
|
+
},
|
|
16913
|
+
{
|
|
16914
|
+
value: "Other",
|
|
16915
|
+
subcategories: [
|
|
16916
|
+
{
|
|
16917
|
+
value: "Other",
|
|
16918
|
+
description: "Game that doesn't fit in any of the other categories"
|
|
16919
|
+
}
|
|
16920
|
+
]
|
|
16921
|
+
}
|
|
16922
|
+
];
|
|
16923
|
+
var catalog = {
|
|
16924
|
+
catalogVersion: catalogVersion,
|
|
16925
|
+
source: source,
|
|
16926
|
+
categories: categories$1
|
|
16927
|
+
};
|
|
16928
|
+
|
|
16929
|
+
const GAME_CATEGORY_CATALOG_VERSION = catalog.catalogVersion;
|
|
16930
|
+
const GAME_CATEGORIES = catalog.categories;
|
|
16931
|
+
function isGameCategoryPair(category, subcategory) {
|
|
16932
|
+
return GAME_CATEGORIES.some(item => item.value === category &&
|
|
16933
|
+
item.subcategories.some(child => child.value === subcategory));
|
|
16934
|
+
}
|
|
16935
|
+
function getGameCategories(options = {}) {
|
|
16936
|
+
const errorCode = !['text', 'json', 'ndjson'].includes(options.format || 'text')
|
|
16937
|
+
? 'GAME_CATEGORIES_INVALID_FORMAT'
|
|
16938
|
+
: options.category !== undefined && !GAME_CATEGORIES.some(item => item.value === options.category)
|
|
16939
|
+
? 'GAME_CATEGORY_UNKNOWN' : null;
|
|
16940
|
+
const categories = errorCode ? [] : GAME_CATEGORIES.filter(item => options.category === undefined || item.value === options.category);
|
|
16941
|
+
return {
|
|
16942
|
+
schemaVersion: '1',
|
|
16943
|
+
command: 'game categories',
|
|
16944
|
+
operationId: `game_categories_${crypto.randomUUID()}`,
|
|
16945
|
+
status: errorCode ? 'failed' : 'ready',
|
|
16946
|
+
terminal: true,
|
|
16947
|
+
platformWrite: false,
|
|
16948
|
+
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
16949
|
+
source: catalog.source,
|
|
16950
|
+
descriptionLanguage: 'en',
|
|
16951
|
+
categoryCount: categories.length,
|
|
16952
|
+
subcategoryCount: categories.reduce((count, item) => count + item.subcategories.length, 0),
|
|
16953
|
+
categories,
|
|
16954
|
+
errorCode,
|
|
16955
|
+
error: errorCode ? t(errorCode === 'GAME_CATEGORY_UNKNOWN'
|
|
16956
|
+
? 'cli.command.game.categories.unknown' : 'cli.command.game.categories.invalidFormat') : null,
|
|
16957
|
+
};
|
|
16958
|
+
}
|
|
16959
|
+
function gameCategories(options) {
|
|
16960
|
+
const result = getGameCategories(options);
|
|
16961
|
+
if (options.format && options.format !== 'text')
|
|
16962
|
+
console.log(JSON.stringify(result));
|
|
16963
|
+
else if (result.error)
|
|
16964
|
+
console.error(result.error);
|
|
16965
|
+
else
|
|
16966
|
+
for (const category of result.categories) {
|
|
16967
|
+
console.log(category.value);
|
|
16968
|
+
for (const item of category.subcategories)
|
|
16969
|
+
console.log(` ${item.value}: ${item.description}`);
|
|
16970
|
+
}
|
|
16971
|
+
if (result.errorCode)
|
|
16972
|
+
process.exitCode = 1;
|
|
16973
|
+
}
|
|
16974
|
+
|
|
16975
|
+
var categories = /*#__PURE__*/Object.freeze({
|
|
16976
|
+
__proto__: null,
|
|
16977
|
+
GAME_CATEGORIES: GAME_CATEGORIES,
|
|
16978
|
+
GAME_CATEGORY_CATALOG_VERSION: GAME_CATEGORY_CATALOG_VERSION,
|
|
16979
|
+
gameCategories: gameCategories,
|
|
16980
|
+
getGameCategories: getGameCategories,
|
|
16981
|
+
isGameCategoryPair: isGameCategoryPair
|
|
16982
|
+
});
|
|
16983
|
+
|
|
16984
|
+
// Current PPE trial defaults: the owner-approved metadata used by the successful
|
|
16985
|
+
// real upload fixture on 2026-09-14, with owner-specified legal URLs updated on
|
|
16986
|
+
// 2026-09-15. This is not a production metadata profile.
|
|
16987
|
+
const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
|
|
16988
|
+
description: 'DevTool PPE integration test fixture. Not a production game.',
|
|
16989
|
+
category: 'Other',
|
|
16990
|
+
subcategory: 'Other',
|
|
16991
|
+
terms_of_service: 'https://www.tiktok.com',
|
|
16992
|
+
privacy_policy: 'https://www.tiktok.com',
|
|
16993
|
+
copyright_confirmation: true,
|
|
16994
|
+
contact_email: 'devtool-test@example.invalid',
|
|
16995
|
+
});
|
|
16996
|
+
// BFF IDL + Import Service ValidateAppInfo, feat-minis-import (2026-09-14).
|
|
16997
|
+
// Keep credentials, app_name and icon_uri out of this file: CLI owns those fields.
|
|
16998
|
+
const requiredStrings = [
|
|
16999
|
+
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
17000
|
+
];
|
|
17001
|
+
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
17002
|
+
function applyVibeCategory(options, appInfo) {
|
|
17003
|
+
if (options.category === undefined && options.subcategory === undefined)
|
|
17004
|
+
return appInfo;
|
|
17005
|
+
if (options.category === undefined || options.subcategory === undefined)
|
|
17006
|
+
throw new VibeError('VIBE_CATEGORY_PAIR_REQUIRED');
|
|
17007
|
+
if (!isGameCategoryPair(options.category, options.subcategory))
|
|
17008
|
+
throw new VibeError('VIBE_INVALID_CATEGORY');
|
|
17009
|
+
return { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), category: options.category, subcategory: options.subcategory };
|
|
17010
|
+
}
|
|
17011
|
+
function readVibeAppInfo(file) {
|
|
17012
|
+
if (file === undefined)
|
|
17013
|
+
return undefined;
|
|
17014
|
+
try {
|
|
17015
|
+
const stat = fs.statSync(file);
|
|
17016
|
+
if (!stat.isFile() || stat.size > 64 * 1024)
|
|
17017
|
+
throw new Error();
|
|
17018
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17019
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17020
|
+
throw new Error();
|
|
17021
|
+
const allowed = new Set([...requiredStrings, ...optionalStrings, 'copyright_confirmation']);
|
|
17022
|
+
if (Object.keys(value).some(key => !allowed.has(key)))
|
|
17023
|
+
throw new Error();
|
|
17024
|
+
// JSON can encode lone surrogates that Go rejects as invalid UTF-8 input.
|
|
17025
|
+
for (const item of Object.values(value)) {
|
|
17026
|
+
if (typeof item === 'string' && [...item].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
17027
|
+
throw new Error();
|
|
17028
|
+
}
|
|
17029
|
+
for (const key of requiredStrings) {
|
|
17030
|
+
if (typeof value[key] !== 'string' || !value[key].trim())
|
|
17031
|
+
throw new Error();
|
|
17032
|
+
}
|
|
17033
|
+
for (const key of optionalStrings) {
|
|
17034
|
+
if (value[key] !== undefined && (typeof value[key] !== 'string' || !value[key].trim()))
|
|
17035
|
+
throw new Error();
|
|
17036
|
+
}
|
|
17037
|
+
// An explicit declaration is required; never infer consent or supply a default.
|
|
17038
|
+
if (value.copyright_confirmation !== true)
|
|
17039
|
+
throw new Error();
|
|
17040
|
+
for (const key of ['terms_of_service', 'privacy_policy']) {
|
|
17041
|
+
if (/\s/.test(value[key]))
|
|
17042
|
+
throw new Error();
|
|
17043
|
+
const url = new URL(value[key]);
|
|
17044
|
+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password)
|
|
17045
|
+
throw new Error();
|
|
17046
|
+
}
|
|
17047
|
+
if (value.contact_email !== undefined && !/^[^\s<>@]+@[^\s<>@]+$/.test(value.contact_email))
|
|
17048
|
+
throw new Error();
|
|
17049
|
+
return value;
|
|
17050
|
+
}
|
|
17051
|
+
catch {
|
|
17052
|
+
// Do not expose paths, file contents or parser errors in structured output.
|
|
17053
|
+
throw new VibeError('VIBE_INVALID_APP_INFO');
|
|
17054
|
+
}
|
|
17055
|
+
}
|
|
17056
|
+
const VIBE_PLATFORM_ICON_LIMIT = 5 * 1024 * 1024;
|
|
17057
|
+
// Header preflight only. The BFF remains responsible for decoding the image.
|
|
17058
|
+
function validateVibeUploadInput(input) {
|
|
17059
|
+
if ([...input.title].length > 30)
|
|
17060
|
+
throw new VibeError('VIBE_TITLE_TOO_LONG');
|
|
17061
|
+
const image = input.icon.bytes;
|
|
17062
|
+
if (image.length > VIBE_PLATFORM_ICON_LIMIT)
|
|
17063
|
+
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
17064
|
+
let width = 0, height = 0;
|
|
17065
|
+
if (image.length >= 33 && image.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')) &&
|
|
17066
|
+
image.readUInt32BE(8) === 13 && image.toString('ascii', 12, 16) === 'IHDR') {
|
|
17067
|
+
width = image.readUInt32BE(16);
|
|
17068
|
+
height = image.readUInt32BE(20);
|
|
17069
|
+
}
|
|
17070
|
+
else if (image[0] === 0xff && image[1] === 0xd8) {
|
|
17071
|
+
let offset = 2;
|
|
17072
|
+
while (offset + 4 <= image.length) {
|
|
17073
|
+
if (image[offset++] !== 0xff)
|
|
17074
|
+
break;
|
|
17075
|
+
while (image[offset] === 0xff)
|
|
17076
|
+
offset++;
|
|
17077
|
+
const marker = image[offset++];
|
|
17078
|
+
if (marker === 0xda || marker === 0xd9)
|
|
17079
|
+
break;
|
|
17080
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
|
|
17081
|
+
continue;
|
|
17082
|
+
if (offset + 2 > image.length)
|
|
17083
|
+
break;
|
|
17084
|
+
const length = image.readUInt16BE(offset);
|
|
17085
|
+
if (length < 2 || offset + length > image.length)
|
|
17086
|
+
break;
|
|
17087
|
+
// Go image/jpeg accepts baseline, extended sequential and progressive SOF.
|
|
17088
|
+
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 8) {
|
|
17089
|
+
height = image.readUInt16BE(offset + 3);
|
|
17090
|
+
width = image.readUInt16BE(offset + 5);
|
|
17091
|
+
break;
|
|
17092
|
+
}
|
|
17093
|
+
offset += length;
|
|
17094
|
+
}
|
|
17095
|
+
}
|
|
17096
|
+
if (width !== 1024 || height !== 1024)
|
|
17097
|
+
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
17098
|
+
}
|
|
17099
|
+
|
|
16318
17100
|
function getCapabilities() {
|
|
16319
17101
|
return {
|
|
16320
17102
|
schemaVersion: '1',
|
|
@@ -16326,8 +17108,22 @@ function getCapabilities() {
|
|
|
16326
17108
|
stdoutProtocolOnly: true,
|
|
16327
17109
|
},
|
|
16328
17110
|
commands: {
|
|
17111
|
+
gameCategories: {
|
|
17112
|
+
command: 'ttmg game categories --format json',
|
|
17113
|
+
available: true,
|
|
17114
|
+
schemaVersion: '1',
|
|
17115
|
+
platformWrite: false,
|
|
17116
|
+
source: 'bundled',
|
|
17117
|
+
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
17118
|
+
filterOption: '--category',
|
|
17119
|
+
includesGameplayDescriptions: true,
|
|
17120
|
+
requiresLogin: false,
|
|
17121
|
+
networkRequired: false,
|
|
17122
|
+
},
|
|
16329
17123
|
vibeUpload: {
|
|
16330
|
-
command: 'ttmg upload --vibe --
|
|
17124
|
+
command: 'ttmg upload --vibe --web --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
17125
|
+
mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
17126
|
+
guide: 'vibe-upload.md',
|
|
16331
17127
|
option: '--vibe',
|
|
16332
17128
|
available: true,
|
|
16333
17129
|
platformWrite: true,
|
|
@@ -16349,19 +17145,55 @@ function getCapabilities() {
|
|
|
16349
17145
|
taskCreation: true,
|
|
16350
17146
|
statusPolling: true,
|
|
16351
17147
|
authorizationScheme: 'aweme://webview?url=<encoded landing_url>',
|
|
16352
|
-
uploadEnabled:
|
|
16353
|
-
uploadBlockedError: 'VIBE_REAL_UPLOAD_NOT_READY',
|
|
17148
|
+
uploadEnabled: true,
|
|
16354
17149
|
},
|
|
16355
17150
|
terminalSuccess: 'uploaded',
|
|
16356
17151
|
stages: ['precheck', 'packaging', 'prepared', 'creating_task', 'waiting_authorization', 'uploading', 'uploaded', 'failed', 'cancelled'],
|
|
16357
17152
|
uploadSteps: ['avatar', 'app_info', 'package'],
|
|
16358
17153
|
sourceProjectIdentity: '.ttmg/vibe-project.json',
|
|
16359
17154
|
metadataInputs: ['title', 'icon', 'dir|package'],
|
|
17155
|
+
inputRequirements: VIBE_INPUT_REQUIREMENTS,
|
|
17156
|
+
classification: {
|
|
17157
|
+
queryCommand: 'ttmg game categories --format json',
|
|
17158
|
+
options: ['--category', '--subcategory'],
|
|
17159
|
+
suppliedTogether: true,
|
|
17160
|
+
default: { category: 'Other', subcategory: 'Other' },
|
|
17161
|
+
invalidPair: 'reject-before-task-creation',
|
|
17162
|
+
legacyAppInfo: 'preserved-when-options-omitted; explicit-pair-overrides-category-fields',
|
|
17163
|
+
resultField: 'classification',
|
|
17164
|
+
},
|
|
17165
|
+
webAuthorization: { option: '--web', backend: 'ppe', defaultLane: 'ppe_op_vc', simulation: false, opensSystemBrowser: true, continuesPolling: true, formatIndependent: true, conflictsWith: ['--mock', '--mock-scenario', '--vibe-api', '--no-open'], mockBehaviorUnchanged: true, materialUploadEnabled: true },
|
|
17166
|
+
skillWorkflow: {
|
|
17167
|
+
preferredFormat: 'ndjson',
|
|
17168
|
+
packageAlternative: '--package <zipPath> replaces --dir <gameDir>',
|
|
17169
|
+
keepUploadProcessRunning: true,
|
|
17170
|
+
pollingOwner: 'cli',
|
|
17171
|
+
uploadOwner: 'cli',
|
|
17172
|
+
waitingAuthorization: {
|
|
17173
|
+
status: 'waiting_authorization',
|
|
17174
|
+
userAction: 'complete authorization in the system browser on the CLI computer',
|
|
17175
|
+
pageUrlField: 'pageUrl',
|
|
17176
|
+
pageUrlPurpose: 'local binding and upload progress page',
|
|
17177
|
+
reopenCommandField: 'authorizationOpenCommand',
|
|
17178
|
+
browserOpenRequestedProvesAuthorization: false,
|
|
17179
|
+
createAnotherUploadTask: false,
|
|
17180
|
+
},
|
|
17181
|
+
realUploadSuccess: { terminal: true, status: 'uploaded', uploaded: true, simulation: false },
|
|
17182
|
+
failure: 'Read errorCode and nextAction; do not automatically create another task or reupload',
|
|
17183
|
+
completionMeaning: 'materials uploaded to PPE; review and production release are separate',
|
|
17184
|
+
sameHostRequired: true,
|
|
17185
|
+
},
|
|
16360
17186
|
optionalMetadataFile: '--app-info',
|
|
16361
17187
|
appInfoFields: {
|
|
16362
|
-
required: [
|
|
16363
|
-
optional: [
|
|
17188
|
+
required: [],
|
|
17189
|
+
optional: [],
|
|
16364
17190
|
suppliedByCli: ['app_name', 'icon_uri'],
|
|
17191
|
+
defaultedByCli: Object.keys(VIBE_PPE_DEFAULT_APP_INFO),
|
|
17192
|
+
additionalMetadataSource: 'cli-defaults',
|
|
17193
|
+
defaultMetadataProfile: 'ppe-trial',
|
|
17194
|
+
requestFromUser: false,
|
|
17195
|
+
legacyFileOption: '--app-info',
|
|
17196
|
+
legacyFileOptionRecommended: false,
|
|
16365
17197
|
},
|
|
16366
17198
|
appInfoPreflight: {
|
|
16367
17199
|
titleMaxCharacters: 30,
|
|
@@ -16387,8 +17219,9 @@ function getCapabilities() {
|
|
|
16387
17219
|
httpContract: 'minis-import-task',
|
|
16388
17220
|
authorizationQr: true,
|
|
16389
17221
|
previewQr: false,
|
|
16390
|
-
realAuthorizationVerified:
|
|
16391
|
-
realUploadVerified:
|
|
17222
|
+
realAuthorizationVerified: true,
|
|
17223
|
+
realUploadVerified: true,
|
|
17224
|
+
verificationScope: { backend: 'ppe', assets: 'test-fixtures', date: '2026-09-15', production: false },
|
|
16392
17225
|
autoUploadAfterAuthorization: true,
|
|
16393
17226
|
boundedPolling: true,
|
|
16394
17227
|
cancellation: true,
|
|
@@ -16457,6 +17290,20 @@ function getCapabilities() {
|
|
|
16457
17290
|
projectIntegration: false,
|
|
16458
17291
|
platformWrite: false,
|
|
16459
17292
|
},
|
|
17293
|
+
wasmCollect: {
|
|
17294
|
+
available: true,
|
|
17295
|
+
experimental: true,
|
|
17296
|
+
authentication: 'local_cli_login',
|
|
17297
|
+
credentialOutput: false,
|
|
17298
|
+
nativeBindingRequired: false,
|
|
17299
|
+
platformVerified: false,
|
|
17300
|
+
outputFormats: ['text', 'json'],
|
|
17301
|
+
start: { command: 'ttmg wasm collect start', platformWrite: true,
|
|
17302
|
+
resetDefault: false, resetOption: '--reset', automaticRetry: false },
|
|
17303
|
+
export: { command: 'ttmg wasm collect export', platformWrite: false,
|
|
17304
|
+
collectionSchemaVersion: '1', originalInputRequired: true,
|
|
17305
|
+
outputPathField: 'collection.path', overwrite: false, strategy: 'union' },
|
|
17306
|
+
},
|
|
16460
17307
|
dev: {
|
|
16461
17308
|
command: 'ttmg dev --format json --no-open',
|
|
16462
17309
|
eventsCommand: 'ttmg dev --events ndjson --no-open',
|
|
@@ -17048,6 +17895,8 @@ function getDesktopPath() {
|
|
|
17048
17895
|
const program = new commander.Command();
|
|
17049
17896
|
const isPreviewPageServerWorker = process.argv[2] === '__preview-page-server';
|
|
17050
17897
|
const isVibePageServerWorker = process.argv[2] === '__vibe-page-server';
|
|
17898
|
+
const wasmArgumentIndex = process.argv.indexOf('wasm', 2);
|
|
17899
|
+
const isWasmCollectCommand = wasmArgumentIndex >= 2 && process.argv[wasmArgumentIndex + 1] === 'collect';
|
|
17051
17900
|
const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
|
|
17052
17901
|
const structuredFormatIndex = process.argv.indexOf('--format');
|
|
17053
17902
|
const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
|
|
@@ -17058,6 +17907,7 @@ const structuredFormat = inlineStructuredFormat
|
|
|
17058
17907
|
: '';
|
|
17059
17908
|
if (!isPreviewPageServerWorker &&
|
|
17060
17909
|
!isVibePageServerWorker &&
|
|
17910
|
+
!(isWasmCollectCommand && !structuredFormat) &&
|
|
17061
17911
|
structuredFormat !== 'json' &&
|
|
17062
17912
|
structuredFormat !== 'ndjson' &&
|
|
17063
17913
|
!process.argv.includes('--implicit')) {
|
|
@@ -17086,6 +17936,14 @@ withVerboseOption(program.command('logout').description(t('cli.command.logout.de
|
|
|
17086
17936
|
await logout();
|
|
17087
17937
|
});
|
|
17088
17938
|
const authCmd = program.command('auth').description(t('cli.command.auth.desc'));
|
|
17939
|
+
const gameCmd = program.command('game').description(t('cli.command.game.desc'));
|
|
17940
|
+
withVerboseOption(gameCmd.command('categories')
|
|
17941
|
+
.description(t('cli.command.game.categories.desc'))
|
|
17942
|
+
.option('--category <category>', t('cli.command.game.categories.category'))
|
|
17943
|
+
.option('--format <format>', t('cli.command.capabilities.format'), 'text')).action(async (cmd) => {
|
|
17944
|
+
const { gameCategories } = await Promise.resolve().then(function () { return categories; });
|
|
17945
|
+
gameCategories(cmd);
|
|
17946
|
+
});
|
|
17089
17947
|
withVerboseOption(authCmd.command('open')
|
|
17090
17948
|
.description(t('cli.command.auth.open.desc'))
|
|
17091
17949
|
.option('--page-url <url>', t('cli.command.auth.open.pageUrl'))
|
|
@@ -17119,10 +17977,13 @@ withVerboseOption(program
|
|
|
17119
17977
|
.option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
|
|
17120
17978
|
.option('--no-open', t('cli.command.upload.noOpen'))
|
|
17121
17979
|
.option('--vibe', t('cli.command.upload.vibe'))
|
|
17980
|
+
.option('--web', t('cli.command.upload.vibeWeb'))
|
|
17122
17981
|
// Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
|
|
17123
17982
|
.option('--title <title>', t('cli.command.upload.vibeTitle'))
|
|
17124
17983
|
.option('--icon <path>', t('cli.command.upload.vibeIcon'))
|
|
17125
|
-
.option('--
|
|
17984
|
+
.option('--category <category>', t('cli.command.upload.vibeCategory'))
|
|
17985
|
+
.option('--subcategory <subcategory>', t('cli.command.upload.vibeSubcategory'))
|
|
17986
|
+
.addOption(new commander.Option('--app-info <path>', t('cli.command.upload.vibeAppInfo')).hideHelp())
|
|
17126
17987
|
.option('--package <path>', t('cli.command.upload.vibePackage'))
|
|
17127
17988
|
.option('--dry-run', t('cli.command.upload.vibeDryRun'))
|
|
17128
17989
|
.option('--mock', t('cli.command.upload.vibeMock'))
|
|
@@ -17131,7 +17992,8 @@ withVerboseOption(program
|
|
|
17131
17992
|
.option('--vibe-authorize-url <url>', t('cli.command.upload.vibeAuthorizeUrl'))
|
|
17132
17993
|
.option('--poll-interval <seconds>', t('cli.command.upload.vibePollInterval'))
|
|
17133
17994
|
.option('--timeout <seconds>', t('cli.command.upload.vibeTimeout'))
|
|
17134
|
-
.option('--format <format>', t('cli.command.upload.format'), 'text')
|
|
17995
|
+
.option('--format <format>', t('cli.command.upload.format'), 'text')
|
|
17996
|
+
.addHelpText('after', t('cli.command.upload.vibeWebGuide'))).action(async (cmd) => {
|
|
17135
17997
|
if (cmd.vibe) {
|
|
17136
17998
|
const { vibeUpload } = await Promise.resolve().then(function () { return index$1; });
|
|
17137
17999
|
await vibeUpload(cmd);
|
|
@@ -17141,6 +18003,9 @@ withVerboseOption(program
|
|
|
17141
18003
|
cmd.title,
|
|
17142
18004
|
cmd.icon,
|
|
17143
18005
|
cmd.appInfo,
|
|
18006
|
+
cmd.category,
|
|
18007
|
+
cmd.subcategory,
|
|
18008
|
+
cmd.web,
|
|
17144
18009
|
cmd.package,
|
|
17145
18010
|
cmd.dryRun,
|
|
17146
18011
|
cmd.mock,
|
|
@@ -17482,6 +18347,25 @@ devSessionCmd
|
|
|
17482
18347
|
});
|
|
17483
18348
|
});
|
|
17484
18349
|
const wasmCmd = program.command('wasm').description(t('cli.command.wasm.desc'));
|
|
18350
|
+
const collectCmd = wasmCmd.command('collect').description(t('cli.command.wasm.collect'));
|
|
18351
|
+
for (const operation of ['start', 'export']) {
|
|
18352
|
+
const command = collectCmd.command(operation)
|
|
18353
|
+
.description(t(operation === 'start' ? 'cli.command.wasm.collect.start' : 'cli.command.wasm.collect.export'))
|
|
18354
|
+
.allowExcessArguments(false)
|
|
18355
|
+
.option('--client-key <key>', t('cli.command.wasm.collect.clientKey'))
|
|
18356
|
+
.option('--wasm-md5 <md5>', t('cli.command.wasm.collect.md5'))
|
|
18357
|
+
.option('--timeout <seconds>', t('cli.command.wasm.collect.timeout'), '30')
|
|
18358
|
+
.option('--format <format>', t('cli.command.wasm.format'), 'json');
|
|
18359
|
+
if (operation === 'start')
|
|
18360
|
+
command.option('--reset', t('cli.command.wasm.collect.reset'));
|
|
18361
|
+
else
|
|
18362
|
+
command.option('--input <path>', t('cli.command.wasm.input'))
|
|
18363
|
+
.option('--output <path>', t('cli.command.wasm.collect.output'));
|
|
18364
|
+
command.action(async (options) => {
|
|
18365
|
+
const Native = await Promise.resolve().then(function () { return index; });
|
|
18366
|
+
await Native.wasmCollectCommand(operation, options);
|
|
18367
|
+
});
|
|
18368
|
+
}
|
|
17485
18369
|
for (const operation of ['prepare', 'split']) {
|
|
17486
18370
|
const command = wasmCmd.command(operation)
|
|
17487
18371
|
.description(t(operation === 'prepare' ? 'cli.command.wasm.prepare' : 'cli.command.wasm.split'))
|
|
@@ -17571,13 +18455,6 @@ else {
|
|
|
17571
18455
|
program.parse(process.argv);
|
|
17572
18456
|
}
|
|
17573
18457
|
|
|
17574
|
-
class VibeError extends Error {
|
|
17575
|
-
constructor(code) {
|
|
17576
|
-
super(code);
|
|
17577
|
-
this.code = code;
|
|
17578
|
-
}
|
|
17579
|
-
}
|
|
17580
|
-
|
|
17581
18458
|
// Only the backend-owned authorization page can be a browser redirect target.
|
|
17582
18459
|
// Never return its import_token to Skill stdout or put it in a CLI argument.
|
|
17583
18460
|
function browserLandingUrl(value) {
|
|
@@ -17630,16 +18507,17 @@ function checkAuthorizationPage(url) {
|
|
|
17630
18507
|
req.end();
|
|
17631
18508
|
});
|
|
17632
18509
|
}
|
|
17633
|
-
async function
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
18510
|
+
async function openVibeBrowser(url) {
|
|
18511
|
+
const child = await (await import('open')).default(url);
|
|
18512
|
+
return new Promise((resolve, reject) => {
|
|
18513
|
+
child.once('error', reject);
|
|
18514
|
+
if (child.pid)
|
|
18515
|
+
resolve(child);
|
|
18516
|
+
else
|
|
18517
|
+
child.once('spawn', () => resolve(child));
|
|
18518
|
+
});
|
|
18519
|
+
}
|
|
18520
|
+
async function executeVibeAuthOpen(options, deps = { open: openVibeBrowser }) {
|
|
17643
18521
|
const result = {
|
|
17644
18522
|
schemaVersion: '1', command: 'auth open', operationId: `auth_open_${crypto.randomUUID()}`,
|
|
17645
18523
|
status: 'failed', platformWrite: false, browserOpenRequested: false,
|
|
@@ -17684,10 +18562,11 @@ var authOpen = /*#__PURE__*/Object.freeze({
|
|
|
17684
18562
|
authorizationOpenUrl: authorizationOpenUrl,
|
|
17685
18563
|
browserLandingUrl: browserLandingUrl,
|
|
17686
18564
|
executeVibeAuthOpen: executeVibeAuthOpen,
|
|
18565
|
+
openVibeBrowser: openVibeBrowser,
|
|
17687
18566
|
vibeAuthOpen: vibeAuthOpen
|
|
17688
18567
|
});
|
|
17689
18568
|
|
|
17690
|
-
// BFF IDL: feat-minis-import. PPE
|
|
18569
|
+
// BFF IDL: feat-minis-import. PPE import flow; no production adapter.
|
|
17691
18570
|
const prefix = '/bff/app_info/minis_import/';
|
|
17692
18571
|
const MINIS_IMPORT_PATHS = {
|
|
17693
18572
|
create: `${prefix}create_minis_import_task_for_agent/`,
|
|
@@ -17807,7 +18686,7 @@ function createBackend(mode, transport) {
|
|
|
17807
18686
|
path: MINIS_IMPORT_PATHS.info,
|
|
17808
18687
|
data: {
|
|
17809
18688
|
...credentials(task),
|
|
17810
|
-
app_info: { ...appInfo, app_name: title, icon_uri: iconUri },
|
|
18689
|
+
app_info: { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), app_name: title, icon_uri: iconUri },
|
|
17811
18690
|
},
|
|
17812
18691
|
signal,
|
|
17813
18692
|
});
|
|
@@ -17855,15 +18734,9 @@ function createPpeBackend(lane, adapter) {
|
|
|
17855
18734
|
'x-use-ppe': '1',
|
|
17856
18735
|
'x-tt-env': lane,
|
|
17857
18736
|
}, adapter);
|
|
17858
|
-
const uploadUnavailable = async () => {
|
|
17859
|
-
throw new VibeError('VIBE_REAL_UPLOAD_NOT_READY');
|
|
17860
|
-
};
|
|
17861
18737
|
return {
|
|
17862
18738
|
...backend,
|
|
17863
|
-
uploadAvailable:
|
|
17864
|
-
uploadIcon: uploadUnavailable,
|
|
17865
|
-
submitInfo: uploadUnavailable,
|
|
17866
|
-
uploadPackage: uploadUnavailable,
|
|
18739
|
+
uploadAvailable: true,
|
|
17867
18740
|
};
|
|
17868
18741
|
}
|
|
17869
18742
|
/** Opt-in integration demo: only creation and polling can reach PPE. */
|
|
@@ -17887,6 +18760,7 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
|
|
|
17887
18760
|
return {
|
|
17888
18761
|
mode: 'ppe-mock',
|
|
17889
18762
|
uploadAvailable: true,
|
|
18763
|
+
dispose: () => real.dispose?.(),
|
|
17890
18764
|
async createTask(input, signal) {
|
|
17891
18765
|
if (realTask)
|
|
17892
18766
|
throw new VibeError('VIBE_TASK_NOT_ACCEPTING_INPUT');
|
|
@@ -17918,14 +18792,20 @@ function createPpeMockBackend(lane, adapter, scenario = 'success') {
|
|
|
17918
18792
|
};
|
|
17919
18793
|
}
|
|
17920
18794
|
function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter) {
|
|
18795
|
+
// Own the agents so an early multipart response cannot leave upload sockets
|
|
18796
|
+
// keeping the CLI alive after its terminal result. Never destroy global agents.
|
|
18797
|
+
const httpAgent = new http.Agent();
|
|
18798
|
+
const httpsAgent = new https.Agent();
|
|
17921
18799
|
const client = axios.create({
|
|
17922
18800
|
baseURL,
|
|
17923
18801
|
timeout: 30000,
|
|
17924
18802
|
maxRedirects: 0,
|
|
17925
18803
|
proxy: false,
|
|
17926
18804
|
adapter,
|
|
18805
|
+
httpAgent,
|
|
18806
|
+
httpsAgent,
|
|
17927
18807
|
});
|
|
17928
|
-
|
|
18808
|
+
const backend = createBackend(mode, async (request) => {
|
|
17929
18809
|
try {
|
|
17930
18810
|
let body;
|
|
17931
18811
|
let headers = {};
|
|
@@ -17964,6 +18844,13 @@ function createHttpBackend(mode, baseURL, environmentHeaders = {}, adapter) {
|
|
|
17964
18844
|
: 'VIBE_BACKEND_REQUEST_FAILED');
|
|
17965
18845
|
}
|
|
17966
18846
|
});
|
|
18847
|
+
return {
|
|
18848
|
+
...backend,
|
|
18849
|
+
dispose() {
|
|
18850
|
+
httpAgent.destroy();
|
|
18851
|
+
httpsAgent.destroy();
|
|
18852
|
+
},
|
|
18853
|
+
};
|
|
17967
18854
|
}
|
|
17968
18855
|
const MOCK_SCENARIOS = [
|
|
17969
18856
|
'success',
|
|
@@ -18275,101 +19162,6 @@ async function checkCommand(options = {}) {
|
|
|
18275
19162
|
return report;
|
|
18276
19163
|
}
|
|
18277
19164
|
|
|
18278
|
-
// BFF IDL + Import Service ValidateAppInfo, feat-minis-import (2026-09-14).
|
|
18279
|
-
// Keep credentials, app_name and icon_uri out of this file: CLI owns those fields.
|
|
18280
|
-
const requiredStrings = [
|
|
18281
|
-
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
18282
|
-
];
|
|
18283
|
-
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
18284
|
-
function readVibeAppInfo(file) {
|
|
18285
|
-
if (file === undefined)
|
|
18286
|
-
return undefined;
|
|
18287
|
-
try {
|
|
18288
|
-
const stat = fs.statSync(file);
|
|
18289
|
-
if (!stat.isFile() || stat.size > 64 * 1024)
|
|
18290
|
-
throw new Error();
|
|
18291
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18292
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
18293
|
-
throw new Error();
|
|
18294
|
-
const allowed = new Set([...requiredStrings, ...optionalStrings, 'copyright_confirmation']);
|
|
18295
|
-
if (Object.keys(value).some(key => !allowed.has(key)))
|
|
18296
|
-
throw new Error();
|
|
18297
|
-
// JSON can encode lone surrogates that Go rejects as invalid UTF-8 input.
|
|
18298
|
-
for (const item of Object.values(value)) {
|
|
18299
|
-
if (typeof item === 'string' && [...item].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
18300
|
-
throw new Error();
|
|
18301
|
-
}
|
|
18302
|
-
for (const key of requiredStrings) {
|
|
18303
|
-
if (typeof value[key] !== 'string' || !value[key].trim())
|
|
18304
|
-
throw new Error();
|
|
18305
|
-
}
|
|
18306
|
-
for (const key of optionalStrings) {
|
|
18307
|
-
if (value[key] !== undefined && (typeof value[key] !== 'string' || !value[key].trim()))
|
|
18308
|
-
throw new Error();
|
|
18309
|
-
}
|
|
18310
|
-
// An explicit declaration is required; never infer consent or supply a default.
|
|
18311
|
-
if (value.copyright_confirmation !== true)
|
|
18312
|
-
throw new Error();
|
|
18313
|
-
for (const key of ['terms_of_service', 'privacy_policy']) {
|
|
18314
|
-
if (/\s/.test(value[key]))
|
|
18315
|
-
throw new Error();
|
|
18316
|
-
const url = new URL(value[key]);
|
|
18317
|
-
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password)
|
|
18318
|
-
throw new Error();
|
|
18319
|
-
}
|
|
18320
|
-
if (value.contact_email !== undefined && !/^[^\s<>@]+@[^\s<>@]+$/.test(value.contact_email))
|
|
18321
|
-
throw new Error();
|
|
18322
|
-
return value;
|
|
18323
|
-
}
|
|
18324
|
-
catch {
|
|
18325
|
-
// Do not expose paths, file contents or parser errors in structured output.
|
|
18326
|
-
throw new VibeError('VIBE_INVALID_APP_INFO');
|
|
18327
|
-
}
|
|
18328
|
-
}
|
|
18329
|
-
const VIBE_PLATFORM_ICON_LIMIT = 5 * 1024 * 1024;
|
|
18330
|
-
// Header preflight only. The BFF remains responsible for decoding the image.
|
|
18331
|
-
function validateVibeUploadInput(input) {
|
|
18332
|
-
if ([...input.title].length > 30)
|
|
18333
|
-
throw new VibeError('VIBE_TITLE_TOO_LONG');
|
|
18334
|
-
const image = input.icon.bytes;
|
|
18335
|
-
if (image.length > VIBE_PLATFORM_ICON_LIMIT)
|
|
18336
|
-
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
18337
|
-
let width = 0, height = 0;
|
|
18338
|
-
if (image.length >= 33 && image.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')) &&
|
|
18339
|
-
image.readUInt32BE(8) === 13 && image.toString('ascii', 12, 16) === 'IHDR') {
|
|
18340
|
-
width = image.readUInt32BE(16);
|
|
18341
|
-
height = image.readUInt32BE(20);
|
|
18342
|
-
}
|
|
18343
|
-
else if (image[0] === 0xff && image[1] === 0xd8) {
|
|
18344
|
-
let offset = 2;
|
|
18345
|
-
while (offset + 4 <= image.length) {
|
|
18346
|
-
if (image[offset++] !== 0xff)
|
|
18347
|
-
break;
|
|
18348
|
-
while (image[offset] === 0xff)
|
|
18349
|
-
offset++;
|
|
18350
|
-
const marker = image[offset++];
|
|
18351
|
-
if (marker === 0xda || marker === 0xd9)
|
|
18352
|
-
break;
|
|
18353
|
-
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
|
|
18354
|
-
continue;
|
|
18355
|
-
if (offset + 2 > image.length)
|
|
18356
|
-
break;
|
|
18357
|
-
const length = image.readUInt16BE(offset);
|
|
18358
|
-
if (length < 2 || offset + length > image.length)
|
|
18359
|
-
break;
|
|
18360
|
-
// Go image/jpeg accepts baseline, extended sequential and progressive SOF.
|
|
18361
|
-
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 8) {
|
|
18362
|
-
height = image.readUInt16BE(offset + 3);
|
|
18363
|
-
width = image.readUInt16BE(offset + 5);
|
|
18364
|
-
break;
|
|
18365
|
-
}
|
|
18366
|
-
offset += length;
|
|
18367
|
-
}
|
|
18368
|
-
}
|
|
18369
|
-
if (width !== 1024 || height !== 1024)
|
|
18370
|
-
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
18371
|
-
}
|
|
18372
|
-
|
|
18373
19165
|
// Prototype resource guards, not agreed platform validation rules.
|
|
18374
19166
|
const VIBE_ICON_LIMIT = 10 * 1024 * 1024;
|
|
18375
19167
|
const VIBE_ZIP_LIMIT = 100 * 1024 * 1024;
|
|
@@ -18390,7 +19182,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
|
|
|
18390
19182
|
const title = options.title?.trim();
|
|
18391
19183
|
if (!title)
|
|
18392
19184
|
throw new VibeError('VIBE_TITLE_REQUIRED');
|
|
18393
|
-
const appInfo = readVibeAppInfo(options.appInfo);
|
|
19185
|
+
const appInfo = applyVibeCategory(options, readVibeAppInfo(options.appInfo));
|
|
18394
19186
|
if (options.package && options.dir)
|
|
18395
19187
|
throw new VibeError('VIBE_INPUT_CONFLICT');
|
|
18396
19188
|
const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
|
|
@@ -18433,7 +19225,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
|
|
|
18433
19225
|
},
|
|
18434
19226
|
code: { bytes: code, name: 'game.zip', contentType: 'application/zip' },
|
|
18435
19227
|
};
|
|
18436
|
-
if (appInfo)
|
|
19228
|
+
if (options.appInfo !== undefined)
|
|
18437
19229
|
validateVibeUploadInput(input);
|
|
18438
19230
|
return input;
|
|
18439
19231
|
}
|
|
@@ -18517,6 +19309,14 @@ async function renderVibePage(input) {
|
|
|
18517
19309
|
mockNote: input.hybrid ? 'Real task creation / polling · Mock authorization / upload · Real task status unchanged' : 'Mock only · no scan needed, nothing is uploaded',
|
|
18518
19310
|
footer: 'Temporary page · closes after 15 minutes',
|
|
18519
19311
|
};
|
|
19312
|
+
if (input.web) {
|
|
19313
|
+
copy.steps[0] = zh ? '网页授权' : 'Web authorization';
|
|
19314
|
+
copy.bindingAction = zh ? '在浏览器完成授权绑定' : 'Authorize in your browser';
|
|
19315
|
+
copy.bindingSubtitle = input.simulation
|
|
19316
|
+
? (zh ? '本次为模拟运行,无需完成网页授权,稍后继续模拟上传。' : 'Mock run: browser approval is not required; simulated upload continues shortly.')
|
|
19317
|
+
: (zh ? '请在已打开的 TikTok 页面确认授权,当前命令会继续查询绑定状态。' : 'Confirm authorization on the opened TikTok page. The command continues polling.');
|
|
19318
|
+
copy.bindingStatus = input.simulation ? (zh ? '模拟等待授权' : 'Simulating authorization') : (zh ? '等待网页授权' : 'Waiting for browser authorization');
|
|
19319
|
+
}
|
|
18520
19320
|
const step = phase === 'binding' ? 0 : phase === 'uploading' ? 1 : 2;
|
|
18521
19321
|
// Only embed the chosen raster image; never fetch a URL or expose its local path.
|
|
18522
19322
|
const icon = input.iconDataUrl &&
|
|
@@ -18540,7 +19340,7 @@ async function renderVibePage(input) {
|
|
|
18540
19340
|
: phase === 'failed'
|
|
18541
19341
|
? escape(input.message)
|
|
18542
19342
|
: copy.completeSubtitle;
|
|
18543
|
-
const qr = phase === 'binding' && input.qrUrl
|
|
19343
|
+
const qr = phase === 'binding' && !input.web && input.qrUrl
|
|
18544
19344
|
? await QRCode.toDataURL(input.qrUrl, {
|
|
18545
19345
|
width: 384,
|
|
18546
19346
|
margin: 4,
|
|
@@ -18559,11 +19359,13 @@ async function renderVibePage(input) {
|
|
|
18559
19359
|
: phase === 'failed'
|
|
18560
19360
|
? copy.failedTitle
|
|
18561
19361
|
: copy.accepted;
|
|
18562
|
-
const content =
|
|
18563
|
-
?
|
|
18564
|
-
:
|
|
18565
|
-
? `<div class="
|
|
18566
|
-
:
|
|
19362
|
+
const content = phase === 'binding' && input.web
|
|
19363
|
+
? '<div class="upload-area"><span class="spinner" aria-hidden="true"></span></div>'
|
|
19364
|
+
: qr
|
|
19365
|
+
? `<div class="qr-area"><div class="qr-frame"><img width="192" height="192" alt="${purpose}" src="${qr}"></div></div>`
|
|
19366
|
+
: phase === 'uploading'
|
|
19367
|
+
? `<div class="upload-area"><div class="upload-symbol"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 16V4m-4 4 4-4 4 4M5 15v5h14v-5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>${progress !== undefined ? `<div class="percentage">${progress}<span>%</span></div><div class="progress-wrap"><progress aria-label="${copy.uploadLabel}" max="100" value="${progress}"></progress><p class="transfer-label">${copy.uploadLabel}</p></div>` : '<span class="spinner" aria-hidden="true"></span>'}</div>`
|
|
19368
|
+
: `<div class="upload-area"><div class="pending-symbol">${phase === 'failed' ? '!' : checkIcon}</div></div>`;
|
|
18567
19369
|
return `<!doctype html><html lang="${zh ? 'zh-CN' : 'en'}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="refresh" content="2"><title>${escape(title)} · ${escape(input.title)}</title><style>${vibePageStyles}</style></head>
|
|
18568
19370
|
<body><main class="shell" data-phase="${phase}"><header class="brand"><span class="brand-mark" aria-hidden="true"><svg width="16" height="16" viewBox="0 0 20 20" fill="none"><path d="m7 5-4 5 4 5m6-10 4 5-4 5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="brand-name">TikTok Minis</span><span class="brand-tool">DevTool</span>${input.simulation ? `<span class="mock-badge">${copy.mock}</span>` : ''}</header>
|
|
18569
19371
|
<section class="card" aria-labelledby="page-title">${phase === 'failed' ? '' : `<ol class="steps" aria-label="${zh ? '上传步骤' : 'Upload steps'}">${copy.steps.map((label, i) => `<li class="step ${i < step ? 'done' : i === step ? 'active' : ''}"${i === step ? ' aria-current="step"' : ''}><span class="step-number">${i < step ? checkIcon : i + 1}</span>${label}</li>`).join('')}</ol>`}
|
|
@@ -18808,7 +19610,7 @@ const defaults = {
|
|
|
18808
19610
|
startPage: startVibePage,
|
|
18809
19611
|
sleep: abortableDelay,
|
|
18810
19612
|
projectId: getVibeProjectId,
|
|
18811
|
-
open:
|
|
19613
|
+
open: openVibeBrowser,
|
|
18812
19614
|
};
|
|
18813
19615
|
async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}, externalSignal) {
|
|
18814
19616
|
const deps = { ...defaults, ...dependencies };
|
|
@@ -18820,6 +19622,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18820
19622
|
let timeout;
|
|
18821
19623
|
let timedOut = false;
|
|
18822
19624
|
let page;
|
|
19625
|
+
let backend = deps.backend;
|
|
18823
19626
|
let detached = false;
|
|
18824
19627
|
let progressUpdates = Promise.resolve();
|
|
18825
19628
|
let ppeLane;
|
|
@@ -18829,6 +19632,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18829
19632
|
schemaVersion: '2',
|
|
18830
19633
|
command: 'upload',
|
|
18831
19634
|
mode: 'vibe',
|
|
19635
|
+
inputRequirements: VIBE_INPUT_REQUIREMENTS,
|
|
18832
19636
|
operationId: `vibe_${crypto.randomUUID()}`,
|
|
18833
19637
|
ts: Date.now(),
|
|
18834
19638
|
status: 'precheck',
|
|
@@ -18862,7 +19666,17 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18862
19666
|
// --mock is the stable trial entry: working steps use the built-in PPE lane,
|
|
18863
19667
|
// blocked steps remain simulated. No environment setup is required.
|
|
18864
19668
|
// Explicit command-line selection wins; real failures never fall back to mock.
|
|
18865
|
-
if (
|
|
19669
|
+
if (options.web) {
|
|
19670
|
+
if (options.mock || options.mockScenario || options.vibeApi || options.open === false)
|
|
19671
|
+
throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
|
|
19672
|
+
ppeSelected = true;
|
|
19673
|
+
ppeLane = process.env.TTMG_VIBE_PPE || 'ppe_op_vc';
|
|
19674
|
+
if (!/^ppe_[a-zA-Z0-9_-]{1,64}$/.test(ppeLane))
|
|
19675
|
+
throw new VibeError('VIBE_INVALID_PPE_ENV');
|
|
19676
|
+
if (deps.backend && deps.backend.mode !== 'ppe')
|
|
19677
|
+
throw new VibeError('VIBE_WEB_OPTION_CONFLICT');
|
|
19678
|
+
}
|
|
19679
|
+
else if (!options.mock && !options.vibeApi && process.env.TTMG_VIBE_BACKEND) {
|
|
18866
19680
|
const configured = process.env.TTMG_VIBE_BACKEND;
|
|
18867
19681
|
if (configured === 'mock')
|
|
18868
19682
|
options = { ...options, mock: true };
|
|
@@ -18899,7 +19713,6 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18899
19713
|
timedOut = true;
|
|
18900
19714
|
controller.abort();
|
|
18901
19715
|
}, timeoutMs);
|
|
18902
|
-
let backend = deps.backend;
|
|
18903
19716
|
if (!options.dryRun && !backend) {
|
|
18904
19717
|
if (options.mock)
|
|
18905
19718
|
backend = deps.createTrialBackend(options.mockScenario);
|
|
@@ -18924,6 +19737,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18924
19737
|
terminal: !!options.dryRun,
|
|
18925
19738
|
nextAction: options.dryRun ? 'none' : 'wait',
|
|
18926
19739
|
title: input.title,
|
|
19740
|
+
classification: {
|
|
19741
|
+
category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
|
|
19742
|
+
subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
|
|
19743
|
+
source: options.category !== undefined ? 'arguments' : input.appInfo ? 'app-info' : 'defaults',
|
|
19744
|
+
catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
|
|
19745
|
+
},
|
|
18927
19746
|
iconSizeBytes: input.icon.bytes.length,
|
|
18928
19747
|
packageSizeBytes: input.code.bytes.length,
|
|
18929
19748
|
packageSha256: crypto.createHash('sha256')
|
|
@@ -18937,6 +19756,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18937
19756
|
iconDataUrl: `data:${input.icon.contentType};base64,${input.icon.bytes.toString('base64')}`,
|
|
18938
19757
|
uploadAvailable: backend.uploadAvailable !== false,
|
|
18939
19758
|
hybrid: backend.mode === 'ppe-mock',
|
|
19759
|
+
web: !!options.web,
|
|
18940
19760
|
};
|
|
18941
19761
|
const sourceProjectId = deps.projectId(options);
|
|
18942
19762
|
emit('creating_task', { platformWrite: backend.mode === 'ppe' || backend.mode === 'ppe-mock' });
|
|
@@ -18948,6 +19768,8 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18948
19768
|
}, controller.signal);
|
|
18949
19769
|
ensureActive();
|
|
18950
19770
|
const authorizationUrl = createAuthorizationScheme(task.landingUrl);
|
|
19771
|
+
if (options.web && !browserLandingUrl(task.landingUrl))
|
|
19772
|
+
throw new VibeError('VIBE_INVALID_RESPONSE');
|
|
18951
19773
|
const pollMs = seconds(options.pollInterval, task.pollIntervalSeconds);
|
|
18952
19774
|
result = {
|
|
18953
19775
|
...result,
|
|
@@ -18956,35 +19778,55 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18956
19778
|
};
|
|
18957
19779
|
page = await deps.startPage();
|
|
18958
19780
|
ensureActive();
|
|
18959
|
-
const bindingMessage =
|
|
19781
|
+
const bindingMessage = options.web
|
|
18960
19782
|
? (getCurrentLanguage() === 'zh-CN'
|
|
18961
|
-
? '
|
|
18962
|
-
: '
|
|
18963
|
-
: backend.
|
|
18964
|
-
? (getCurrentLanguage() === 'zh-CN'
|
|
18965
|
-
|
|
19783
|
+
? '请在浏览器完成真实授权,当前命令会继续查询绑定状态。'
|
|
19784
|
+
: 'Complete real authorization in the browser; this command keeps polling the binding status.')
|
|
19785
|
+
: backend.mode === 'ppe-mock'
|
|
19786
|
+
? (getCurrentLanguage() === 'zh-CN'
|
|
19787
|
+
? '真实 PPE 任务和授权二维码;查询三次后模拟授权、上传,无需完成手机授权。'
|
|
19788
|
+
: 'Real PPE task and authorization QR. After three polls, authorization and upload are simulated; phone authorization is not required.')
|
|
19789
|
+
: backend.uploadAvailable === false
|
|
19790
|
+
? (getCurrentLanguage() === 'zh-CN' ? '请扫码完成真实 PPE 授权;上传尚未开放。' : 'Scan to authorize in PPE. Upload is not enabled yet.')
|
|
19791
|
+
: text('waiting_authorization', result.simulation);
|
|
18966
19792
|
await page.update({
|
|
18967
19793
|
...pageGame,
|
|
18968
19794
|
language: getCurrentLanguage(),
|
|
18969
19795
|
phase: 'binding',
|
|
18970
19796
|
simulation: result.simulation,
|
|
18971
19797
|
message: bindingMessage,
|
|
18972
|
-
qrUrl: authorizationUrl,
|
|
19798
|
+
qrUrl: options.web ? undefined : authorizationUrl,
|
|
18973
19799
|
browserLandingUrl: task.landingUrl,
|
|
18974
19800
|
purpose: 'authorization_binding',
|
|
18975
19801
|
});
|
|
18976
19802
|
ensureActive();
|
|
19803
|
+
let browserOpenRequested = false;
|
|
19804
|
+
let browserOpenErrorCode;
|
|
19805
|
+
if (options.web) {
|
|
19806
|
+
try {
|
|
19807
|
+
await deps.open(authorizationOpenUrl(page.url));
|
|
19808
|
+
browserOpenRequested = true;
|
|
19809
|
+
}
|
|
19810
|
+
catch {
|
|
19811
|
+
// Preserve the task and polling; the returned helper retries this same page.
|
|
19812
|
+
browserOpenErrorCode = 'VIBE_BROWSER_OPEN_FAILED';
|
|
19813
|
+
}
|
|
19814
|
+
}
|
|
18977
19815
|
emit('waiting_authorization', {
|
|
18978
|
-
message:
|
|
19816
|
+
message: browserOpenErrorCode
|
|
19817
|
+
? (getCurrentLanguage() === 'zh-CN' ? '浏览器未能打开,请使用 authorizationOpenCommand 重试;当前任务仍在等待授权。' : 'Browser opening failed. Retry authorizationOpenCommand; this task is still waiting for authorization.')
|
|
19818
|
+
: bindingMessage,
|
|
18979
19819
|
pageUrl: page.url,
|
|
19820
|
+
authorizationMode: options.web ? 'web' : 'qr',
|
|
19821
|
+
...(options.web ? { browserOpenRequested, browserOpenErrorCode } : {}),
|
|
18980
19822
|
authorizationOpenCommand: browserLandingUrl(task.landingUrl)
|
|
18981
19823
|
? `ttmg auth open --page-url ${page.url} --format json` : undefined,
|
|
18982
|
-
qrPurpose: 'authorization_binding',
|
|
18983
|
-
nextAction: result.simulation ? 'wait' : 'scan_qr',
|
|
19824
|
+
qrPurpose: options.web ? undefined : 'authorization_binding',
|
|
19825
|
+
nextAction: result.simulation ? 'wait' : options.web ? 'open_browser' : 'scan_qr',
|
|
18984
19826
|
readyForInput: false,
|
|
18985
19827
|
});
|
|
18986
19828
|
// Opening a browser is presentation only and never gates token polling.
|
|
18987
|
-
if ((options.format || 'text') === 'text' && options.open !== false)
|
|
19829
|
+
if (!options.web && (options.format || 'text') === 'text' && options.open !== false)
|
|
18988
19830
|
void deps.open(page.url).catch(() => { });
|
|
18989
19831
|
while (true) {
|
|
18990
19832
|
ensureActive();
|
|
@@ -19092,6 +19934,9 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19092
19934
|
? 'cancelled'
|
|
19093
19935
|
: 'failed';
|
|
19094
19936
|
const messages = {
|
|
19937
|
+
VIBE_CATEGORY_PAIR_REQUIRED: ['--category 和 --subcategory 必须同时提供;均不传时保留默认分类。使用 ttmg game categories --format json 查询。', 'Provide --category and --subcategory together, or omit both to keep the default. Query ttmg game categories --format json.'],
|
|
19938
|
+
VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
|
|
19939
|
+
VIBE_WEB_OPTION_CONFLICT: ['--web 使用真实网页授权,不能与 --mock、--mock-scenario、--vibe-api 或 --no-open 同时使用。', '--web uses real browser authorization and cannot be combined with --mock, --mock-scenario, --vibe-api or --no-open.'],
|
|
19095
19940
|
VIBE_AUTH_DENIED: [
|
|
19096
19941
|
'已取消授权,本次未上传。',
|
|
19097
19942
|
'Authorization cancelled. No upload was performed.',
|
|
@@ -19108,8 +19953,8 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19108
19953
|
VIBE_TITLE_REQUIRED: ['请提供游戏名称。', 'Provide a game title.'],
|
|
19109
19954
|
VIBE_TITLE_TOO_LONG: ['游戏名称不能超过 30 个字符。', 'The game title must not exceed 30 characters.'],
|
|
19110
19955
|
VIBE_INVALID_APP_INFO: [
|
|
19111
|
-
'
|
|
19112
|
-
'
|
|
19956
|
+
'旧 --app-info 文件无效。当前上传只需标题和头像,CLI 自动补齐其他字段的 PPE 试用默认值;移除 --app-info 后重试,不要向用户索要额外资料。',
|
|
19957
|
+
'Invalid legacy --app-info file. Current uploads need only title and icon; CLI fills other fields with PPE trial defaults. Remove --app-info and retry without requesting extra fields from the user.',
|
|
19113
19958
|
],
|
|
19114
19959
|
VIBE_PLATFORM_ICON_INVALID: [
|
|
19115
19960
|
'平台头像要求 PNG/JPEG、1024×1024,且不超过 5 MiB,请重新选择本地图片。',
|
|
@@ -19214,6 +20059,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
19214
20059
|
finally {
|
|
19215
20060
|
clearTimeout(timeout);
|
|
19216
20061
|
externalSignal?.removeEventListener('abort', cancel);
|
|
20062
|
+
backend?.dispose?.();
|
|
19217
20063
|
if (page && !detached)
|
|
19218
20064
|
page.close();
|
|
19219
20065
|
}
|
|
@@ -30405,12 +31251,17 @@ async function snapshotWasm(input, output) {
|
|
|
30405
31251
|
callback(bytes > 512 * 1024 * 1024 ? fail('WASM_INPUT_INVALID', 'Expanded Wasm exceeds 512 MiB.') : null, chunk);
|
|
30406
31252
|
} });
|
|
30407
31253
|
const source = fs$1.createReadStream(input);
|
|
31254
|
+
const fileMd5 = crypto$1.createHash('md5');
|
|
31255
|
+
const identity = new node_stream.Transform({ transform(chunk, _encoding, callback) {
|
|
31256
|
+
fileMd5.update(chunk);
|
|
31257
|
+
callback(null, chunk);
|
|
31258
|
+
} });
|
|
30408
31259
|
const target = fs$1.createWriteStream(output, { flags: 'wx', mode: 0o600 });
|
|
30409
31260
|
try {
|
|
30410
31261
|
if (input.endsWith('.br'))
|
|
30411
|
-
await promises$2.pipeline(source, zlib$1.createBrotliDecompress(), bound, target);
|
|
31262
|
+
await promises$2.pipeline(source, identity, zlib$1.createBrotliDecompress(), bound, target);
|
|
30412
31263
|
else
|
|
30413
|
-
await promises$2.pipeline(source, bound, target);
|
|
31264
|
+
await promises$2.pipeline(source, identity, bound, target);
|
|
30414
31265
|
}
|
|
30415
31266
|
catch (error) {
|
|
30416
31267
|
throw fail(error.code === 'WASM_INPUT_INVALID' ? error.code : 'WASM_INPUT_INVALID', 'Could not read the original Wasm input.');
|
|
@@ -30427,7 +31278,11 @@ async function snapshotWasm(input, output) {
|
|
|
30427
31278
|
throw fail('WASM_INPUT_INVALID', 'Expected a WebAssembly version 1 binary.');
|
|
30428
31279
|
if (!WebAssembly.validate(await fsp.readFile(output)))
|
|
30429
31280
|
throw fail('WASM_INPUT_INVALID', 'Original Wasm is invalid or unsupported by this Node.js WebAssembly engine.');
|
|
30430
|
-
|
|
31281
|
+
const rawMd5 = crypto$1.createHash('md5');
|
|
31282
|
+
for await (const chunk of fs$1.createReadStream(output))
|
|
31283
|
+
rawMd5.update(chunk);
|
|
31284
|
+
return { bytes, wasmSha256: await digest(output), wasmMd5: fileMd5.digest('hex'),
|
|
31285
|
+
uncompressedWasmMd5: rawMd5.digest('hex') };
|
|
30431
31286
|
}
|
|
30432
31287
|
async function executeWasm(operation, options, dependencies = {}) {
|
|
30433
31288
|
const base = { schemaVersion: '1', command: `wasm.${operation}`, operationId: `wasm_${crypto$1.randomUUID()}`,
|
|
@@ -30535,6 +31390,149 @@ async function wasmCommand(operation, options) {
|
|
|
30535
31390
|
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
30536
31391
|
}
|
|
30537
31392
|
|
|
31393
|
+
class CollectError extends Error {
|
|
31394
|
+
constructor(code, message) {
|
|
31395
|
+
super(message);
|
|
31396
|
+
this.code = code;
|
|
31397
|
+
}
|
|
31398
|
+
}
|
|
31399
|
+
const invalid = (code, message) => { throw new CollectError(code, message); };
|
|
31400
|
+
async function executeWasmCollect(operation, options, overrides = {}) {
|
|
31401
|
+
const deps = { authenticated: () => Boolean(getCurrentUser()?.cookie), request, ...overrides };
|
|
31402
|
+
const base = { schemaVersion: '1', command: `wasm.collect.${operation}`,
|
|
31403
|
+
operationId: `wasm_collect_${crypto$1.randomUUID()}`, platformWrite: operation === 'start',
|
|
31404
|
+
platformWriteAttempted: false, sourceModified: false };
|
|
31405
|
+
let temporary;
|
|
31406
|
+
let staging;
|
|
31407
|
+
let timer;
|
|
31408
|
+
try {
|
|
31409
|
+
const timeout = Number(options.timeout ?? 30);
|
|
31410
|
+
if (!['start', 'export'].includes(operation) || !['json', 'text'].includes(options.format || 'json') ||
|
|
31411
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(options.clientKey || '') || !/^[a-fA-F0-9]{32}$/.test(options.wasmMd5 || '') ||
|
|
31412
|
+
!Number.isInteger(timeout) || timeout < 1 || timeout > 300 ||
|
|
31413
|
+
(options.reset !== undefined && typeof options.reset !== 'boolean') ||
|
|
31414
|
+
(operation === 'export' && (!options.input || !options.output || options.reset !== undefined)) ||
|
|
31415
|
+
(operation === 'start' && (options.input || options.output))) {
|
|
31416
|
+
invalid('WASM_COLLECT_INVALID_OPTIONS', 'Provide client-key and wasm-md5; export also requires original input and a new output file. Timeout: 1–300 seconds.');
|
|
31417
|
+
}
|
|
31418
|
+
const clientKey = options.clientKey;
|
|
31419
|
+
const wasmMd5 = options.wasmMd5.toLowerCase();
|
|
31420
|
+
const output = options.output ? path$1.resolve(options.output) : undefined;
|
|
31421
|
+
let source;
|
|
31422
|
+
if (operation === 'export') {
|
|
31423
|
+
// lstat detects dangling links as well as existing files. Never overwrite.
|
|
31424
|
+
if (await fsp.lstat(output).catch((e) => { if (e.code !== 'ENOENT')
|
|
31425
|
+
throw e; return null; }))
|
|
31426
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31427
|
+
temporary = await fsp.mkdtemp(path$1.join(os$1.tmpdir(), 'ttmg-collect-'));
|
|
31428
|
+
try {
|
|
31429
|
+
source = await snapshotWasm(path$1.resolve(options.input), path$1.join(temporary, 'original.wasm'));
|
|
31430
|
+
}
|
|
31431
|
+
catch {
|
|
31432
|
+
return invalid('WASM_INPUT_INVALID', 'Expected a valid original Wasm or Brotli Wasm file.');
|
|
31433
|
+
}
|
|
31434
|
+
if (wasmMd5 !== source.wasmMd5 && wasmMd5 !== source.uncompressedWasmMd5)
|
|
31435
|
+
invalid('WASM_COLLECTION_MISMATCH', 'Collection MD5 does not match this original Wasm (file bytes or uncompressed bytes).');
|
|
31436
|
+
}
|
|
31437
|
+
if (!deps.authenticated())
|
|
31438
|
+
invalid('WASM_AUTH_REQUIRED', 'Run ttmg login on this machine before collecting or exporting.');
|
|
31439
|
+
const controller = new AbortController();
|
|
31440
|
+
const deadline = new Promise((_, reject) => {
|
|
31441
|
+
timer = setTimeout(() => {
|
|
31442
|
+
controller.abort();
|
|
31443
|
+
reject(new CollectError('WASM_COLLECT_TIMEOUT', 'Platform request timed out. A start may have reached the server; retry without --reset to preserve collection.'));
|
|
31444
|
+
}, timeout * 1000);
|
|
31445
|
+
});
|
|
31446
|
+
base.platformWriteAttempted = operation === 'start';
|
|
31447
|
+
const response = await Promise.race([deps.request({
|
|
31448
|
+
url: `${WASM_COLLECT_BASE_URL}/${operation === 'start' ? 'start' : 'export'}`,
|
|
31449
|
+
method: operation === 'start' ? 'POST' : 'GET',
|
|
31450
|
+
...(operation === 'start'
|
|
31451
|
+
? { data: { app_id: clientKey, wasm_md5: wasmMd5, reset: options.reset ?? false } }
|
|
31452
|
+
: { params: { app_id: clientKey, wasm_md5: wasmMd5, strategy: 'union' } }),
|
|
31453
|
+
signal: controller.signal,
|
|
31454
|
+
logRequestBody: false, logApiCalls: false,
|
|
31455
|
+
}), deadline]);
|
|
31456
|
+
clearTimeout(timer);
|
|
31457
|
+
const code = response.error?.code ?? response.error?.StatusCode ?? response.data?.code;
|
|
31458
|
+
const httpStatus = response.ctx?.httpStatusCode;
|
|
31459
|
+
// Upstream bodies/errors are deliberately never included in output: they
|
|
31460
|
+
// can contain credentials or gateway HTML, even with verbose enabled.
|
|
31461
|
+
if ([401, -401].includes(Number(code)) || httpStatus === 401 ||
|
|
31462
|
+
(Number(code) === -1 && /login|log.?in|登录|未登|auth/i.test(String(response.error?.message ?? response.error?.StatusMessage ?? response.data?.message ?? ''))))
|
|
31463
|
+
invalid('WASM_AUTH_REQUIRED', 'Platform login expired. Run ttmg login and retry.');
|
|
31464
|
+
if ([403, -403].includes(Number(code)) || httpStatus === 403)
|
|
31465
|
+
invalid('WASM_COLLECT_FORBIDDEN', 'This account cannot access collection for this game.');
|
|
31466
|
+
if (response.error || response.data?.code !== 0 || (httpStatus && (httpStatus < 200 || httpStatus >= 300)))
|
|
31467
|
+
invalid('WASM_COLLECT_API_FAILED', 'Platform collection request failed; no collection file was written.');
|
|
31468
|
+
if (operation === 'start') {
|
|
31469
|
+
if (response.data.collect_state !== 'open')
|
|
31470
|
+
invalid('WASM_COLLECT_INVALID_RESPONSE', 'Platform did not confirm an open collection window.');
|
|
31471
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5, reset: options.reset ?? false,
|
|
31472
|
+
collectState: 'open', nextAction: 'run_instrumented_game_and_collect', errorCode: null, error: null };
|
|
31473
|
+
}
|
|
31474
|
+
if (Array.isArray(response.data.func_ids) && response.data.func_ids.length === 0)
|
|
31475
|
+
invalid('WASM_COLLECTION_EMPTY', 'No function IDs collected. Run the instrumented game and export again.');
|
|
31476
|
+
let ids;
|
|
31477
|
+
try {
|
|
31478
|
+
ids = parseCollection({ schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31479
|
+
funcIds: response.data.func_ids, bootFuncIds: response.data.boot_func_ids }, source.wasmSha256);
|
|
31480
|
+
}
|
|
31481
|
+
catch {
|
|
31482
|
+
return invalid('WASM_COLLECTION_INVALID', 'Platform returned invalid function IDs; no collection file was written.');
|
|
31483
|
+
}
|
|
31484
|
+
const collection = { schemaVersion: '1', wasmSha256: source.wasmSha256, ...ids,
|
|
31485
|
+
clientKey, wasmMd5, strategy: 'union', exportedAt: new Date().toISOString() };
|
|
31486
|
+
const bytes = Buffer.from(JSON.stringify(collection, null, 2) + '\n');
|
|
31487
|
+
if (bytes.length > 32 * 1024 * 1024)
|
|
31488
|
+
invalid('WASM_COLLECTION_INVALID', 'Collection JSON exceeds 32 MiB.');
|
|
31489
|
+
await fsp.mkdir(path$1.dirname(output), { recursive: true });
|
|
31490
|
+
staging = path$1.join(path$1.dirname(output), `.ttmg-collection-${crypto$1.randomUUID()}.tmp`);
|
|
31491
|
+
await fsp.writeFile(staging, bytes, { flag: 'wx', mode: 0o600 });
|
|
31492
|
+
// Publish a complete file atomically, with exclusive no-clobber semantics.
|
|
31493
|
+
try {
|
|
31494
|
+
await fsp.link(staging, output);
|
|
31495
|
+
}
|
|
31496
|
+
catch (e) {
|
|
31497
|
+
if (e.code === 'EEXIST')
|
|
31498
|
+
invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
|
|
31499
|
+
throw e;
|
|
31500
|
+
}
|
|
31501
|
+
return { ...base, status: 'succeeded', clientKey, wasmMd5,
|
|
31502
|
+
collection: { path: output, schemaVersion: '1', wasmSha256: source.wasmSha256,
|
|
31503
|
+
funcCount: ids.funcIds.length, bootFuncCount: ids.bootFuncIds.length,
|
|
31504
|
+
bytes: bytes.length, sha256: crypto$1.createHash('sha256').update(bytes).digest('hex') },
|
|
31505
|
+
nextAction: 'split_with_collection', errorCode: null, error: null };
|
|
31506
|
+
}
|
|
31507
|
+
catch (error) {
|
|
31508
|
+
const known = error instanceof CollectError;
|
|
31509
|
+
return { ...base, status: 'failed', errorCode: known ? error.code : 'WASM_COLLECT_FAILED',
|
|
31510
|
+
error: known ? error.message : 'Collection operation failed; check the original Wasm, output directory and connection.',
|
|
31511
|
+
nextAction: known && error.code === 'WASM_AUTH_REQUIRED' ? 'login' : 'fix_and_retry' };
|
|
31512
|
+
}
|
|
31513
|
+
finally {
|
|
31514
|
+
if (timer)
|
|
31515
|
+
clearTimeout(timer);
|
|
31516
|
+
if (staging)
|
|
31517
|
+
await fsp.unlink(staging).catch(() => { });
|
|
31518
|
+
if (temporary)
|
|
31519
|
+
await fsp.rm(temporary, { recursive: true, force: true });
|
|
31520
|
+
}
|
|
31521
|
+
}
|
|
31522
|
+
async function wasmCollectCommand(operation, options) {
|
|
31523
|
+
const result = await executeWasmCollect(operation, options);
|
|
31524
|
+
if ((options.format || 'json') === 'json')
|
|
31525
|
+
console.log(JSON.stringify(result));
|
|
31526
|
+
else {
|
|
31527
|
+
console.log(t(result.status === 'failed' ? 'wasm.collect.failed' : 'wasm.collect.succeeded'));
|
|
31528
|
+
if (result.error)
|
|
31529
|
+
console.error(result.error);
|
|
31530
|
+
if ('collection' in result)
|
|
31531
|
+
console.log(result.collection.path);
|
|
31532
|
+
}
|
|
31533
|
+
process.exitCode = result.status === 'failed' ? 1 : 0;
|
|
31534
|
+
}
|
|
31535
|
+
|
|
30538
31536
|
const problem = (code, message) => Object.assign(new Error(message), { code });
|
|
30539
31537
|
const MAX_BYTES = 64 * 1024 * 1024;
|
|
30540
31538
|
// Export one finite Runtime interval. Sequence numbers are global, so gaps in
|
|
@@ -30738,6 +31736,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
30738
31736
|
devSessionStopCommand: devSessionStopCommand,
|
|
30739
31737
|
devWorkflowCommand: devWorkflowCommand,
|
|
30740
31738
|
doctor: doctor,
|
|
31739
|
+
wasmCollectCommand: wasmCollectCommand,
|
|
30741
31740
|
wasmCommand: wasmCommand
|
|
30742
31741
|
});
|
|
30743
31742
|
//# sourceMappingURL=index.js.map
|