@ttmg/cli 0.4.6-vibe-beta.14 → 0.4.6-vibe-beta.15

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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.14",
3
+ "version": "0.4.6-vibe-beta.15",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
49
49
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
50
50
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
51
- "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
51
+ "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/run-ts-test.js src/commands/native/wasm/collect.test.ts && node scripts/acceptance-fixture.test.js",
52
52
  "test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
53
53
  "test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
54
54
  "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
@@ -404,6 +404,8 @@ assert.match(uploadHelp.stdout, /--vibe --web --dir/);
404
404
  assert.match(uploadHelp.stdout, /authorizationOpenCommand/);
405
405
  assert.match(uploadHelp.stdout, /simulation=false/);
406
406
  assert.match(uploadHelp.stdout, /--category <category>/);
407
+ assert.match(uploadHelp.stdout, /--description <description>/);
408
+ assert.match(uploadHelp.stdout, /Discover the fun of TikTok Mini Games\./);
407
409
  assert.match(uploadHelp.stdout, /--subcategory <subcategory>/);
408
410
  assert.match(uploadHelp.stdout, /--preview\b/);
409
411
  assert.match(uploadHelp.stdout, /--serve-preview-page\b/);
@@ -451,6 +453,17 @@ const wasmSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-
451
453
  });
452
454
  assertExit(wasmSmoke, 0, 'Wasm built-artifact process smoke');
453
455
  console.log(wasmSmoke.stdout.trim());
456
+ for (const operation of ['start', 'export']) {
457
+ assertExit(runWithoutWasmBinding(['wasm', 'collect', operation, '--help']), 0, `collect ${operation} help without binding`);
458
+ const invalid = runCli(['wasm', 'collect', operation, '--format', 'json']);
459
+ assertExit(invalid, 1, `collect ${operation} missing input`);
460
+ assert.equal(parseSingleJson(invalid, `collect ${operation}`).errorCode, 'WASM_COLLECT_INVALID_OPTIONS');
461
+ }
462
+ const collectSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-collect-smoke.js')], {
463
+ cwd: cliRoot, encoding: 'utf8', timeout: 60000,
464
+ });
465
+ assertExit(collectSmoke, 0, 'Wasm collection built-artifact process smoke');
466
+ console.log(collectSmoke.stdout.trim());
454
467
  const captureHelp = runCli(['dev', 'capture', '--help']);
455
468
  assertExit(captureHelp, 0, 'capture help');
456
469
  assert.match(captureHelp.stdout, /--timeout/);
@@ -31,6 +31,7 @@ let webMode = false;
31
31
  let webPolls = 0;
32
32
  let webFailureEndpoint;
33
33
  let expectedWebCategory = { category: 'Other', subcategory: 'Other' };
34
+ let expectedWebDescription = 'Discover the fun of TikTok Mini Games.';
34
35
  const env = {
35
36
  ...process.env,
36
37
  HOME: temp,
@@ -194,7 +195,7 @@ async function main() {
194
195
  assert.equal(info.app_info.icon_uri, 'web-icon-receipt');
195
196
  assert.equal(info.app_info.app_name, input[input.indexOf('--title') + 1]);
196
197
  assert.deepEqual(info.app_info, {
197
- description: 'DevTool PPE integration test fixture. Not a production game.',
198
+ description: expectedWebDescription,
198
199
  ...expectedWebCategory,
199
200
  terms_of_service: 'https://www.tiktok.com',
200
201
  privacy_policy: 'https://www.tiktok.com',
@@ -265,6 +266,19 @@ async function main() {
265
266
  assert.equal(events(completeDry)[0].classification.category, 'Casual');
266
267
  assert.equal(events(completeDry)[0].classification.subcategory, 'Puzzle');
267
268
  assert.equal(events(completeDry)[0].classification.source, 'app-info');
269
+ assert.equal(events(completeDry)[0].description, 'Process contract fixture');
270
+ assert.equal(events(completeDry)[0].descriptionSource, 'app-info');
271
+ const describedDry = await run([...input, '--description', ' Stack cats 🐈 ', '--dry-run', '--format', 'json']);
272
+ assert.equal(describedDry.code, 0, describedDry.stderr);
273
+ assert.equal(events(describedDry)[0].description, 'Stack cats 🐈');
274
+ assert.equal(events(describedDry)[0].descriptionSource, 'arguments');
275
+ assert.equal(events(describedDry)[0].classification.source, 'defaults');
276
+ assert.equal(events(describedDry)[0].platformWrite, false);
277
+ const descriptionOverrideDry = await run([...input, '--app-info', appInfoPath, '--description', 'New description', '--dry-run', '--format', 'json']);
278
+ assert.equal(descriptionOverrideDry.code, 0, descriptionOverrideDry.stderr);
279
+ assert.equal(events(descriptionOverrideDry)[0].description, 'New description');
280
+ assert.equal(events(descriptionOverrideDry)[0].descriptionSource, 'arguments');
281
+ assert.equal(events(descriptionOverrideDry)[0].classification.source, 'app-info');
268
282
  const overrideDry = await run([...input, '--app-info', appInfoPath, '--category', 'Puzzle', '--subcategory', 'Physics', '--dry-run', '--format', 'json']);
269
283
  assert.equal(overrideDry.code, 0, overrideDry.stderr);
270
284
  assert.equal(events(overrideDry)[0].classification.category, 'Puzzle');
@@ -380,6 +394,8 @@ async function main() {
380
394
  assert.equal(events(legacyWeb)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
381
395
  assert.equal(requests.length, beforeWeb);
382
396
  for (const [args, code] of [
397
+ [['--description', ''], 'VIBE_INVALID_DESCRIPTION'],
398
+ [['--description', ' \n '], 'VIBE_INVALID_DESCRIPTION'],
383
399
  [['--category', 'Puzzle'], 'VIBE_CATEGORY_PAIR_REQUIRED'],
384
400
  [['--subcategory', 'Physics'], 'VIBE_CATEGORY_PAIR_REQUIRED'],
385
401
  [['--category', 'Other', '--subcategory', 'Physics'], 'VIBE_INVALID_CATEGORY'],
@@ -394,14 +410,17 @@ async function main() {
394
410
  assert.equal(result.taskId, undefined);
395
411
  assert.equal(result.pageUrl, undefined);
396
412
  }
397
- assert.equal(requests.length, beforeWeb, 'Invalid classification must not create tasks or upload');
413
+ assert.equal(requests.length, beforeWeb, 'Invalid metadata must not create tasks or upload');
398
414
  webMode = true;
399
415
  for (const format of ['json', 'ndjson']) {
416
+ expectedWebDescription = format === 'json' ? 'Discover the fun of TikTok Mini Games.' : 'Stack cats on a rooftop 🐈';
417
+ const descriptionArgs = format === 'json' ? [] : ['--description', expectedWebDescription];
400
418
  expectedWebCategory = format === 'json' ? {category: 'Other', subcategory: 'Other'} : {category: 'Puzzle', subcategory: 'Physics'};
401
419
  const categoryArgs = format === 'json' ? [] : ['--category', 'Puzzle', '--subcategory', 'Physics'];
402
420
  const receipt = path.join(temp, `upload-web-${format}.ndjson`);
403
421
  const requestStart = requests.length;
404
- const web = await run([...input.filter(v => v !== '--no-open'), '--web', ...categoryArgs, '--format', format, '--poll-interval', '0.2'], async event => {
422
+ const web = await run([...input.filter(v => v !== '--no-open'), '--web', ...categoryArgs, ...descriptionArgs, '--format', format, '--poll-interval', '0.2'], async event => {
423
+ if (event.status === 'prepared') assert.equal(event.description, expectedWebDescription);
405
424
  if (event.status === 'prepared') assert.deepEqual(
406
425
  {category: event.classification.category, subcategory: event.classification.subcategory},
407
426
  expectedWebCategory);
@@ -437,6 +456,8 @@ async function main() {
437
456
  { category: output.at(-1).classification.category, subcategory: output.at(-1).classification.subcategory },
438
457
  expectedWebCategory);
439
458
  assert.equal(output.at(-1).classification.source, format === 'json' ? 'defaults' : 'arguments');
459
+ assert.equal(output.at(-1).description, expectedWebDescription);
460
+ assert.equal(output.at(-1).descriptionSource, format === 'json' ? 'defaults' : 'arguments');
440
461
  assert.equal(output.at(-1).taskStatusRaw, 3);
441
462
  assert.equal(output.filter(e => e.terminal).length, 1);
442
463
  assert.equal(webPolls, 5, 'Only true backend readiness unlocks authorization');
@@ -451,6 +472,7 @@ async function main() {
451
472
  assert.doesNotMatch(JSON.stringify(opens) + web.stdout + web.stderr, /process-test-secret|import_token|web-icon-receipt|web-package-receipt|aweme:\/\//);
452
473
  }
453
474
  expectedWebCategory = {category: 'Other', subcategory: 'Other'};
475
+ expectedWebDescription = 'Discover the fun of TikTok Mini Games.';
454
476
  webFailureEndpoint = '/bff/app_info/minis_import/submit_minis_import_info_for_agent/';
455
477
  const failedStart = requests.length;
456
478
  const webUploadFailure = await run([...input.filter(v => v !== '--no-open'), '--web', '--format', 'json'], undefined, {
@@ -588,6 +610,9 @@ async function main() {
588
610
  assert.equal(badMode.code, 1);
589
611
  assert.equal(events(badMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
590
612
  const badCategoryMode = await run(['upload', '--category', 'Puzzle', '--subcategory', 'Physics', '--format', 'json']);
613
+ const badDescriptionMode = await run(['upload', '--description', 'Game description', '--format', 'json']);
614
+ assert.equal(badDescriptionMode.code, 1);
615
+ assert.equal(events(badDescriptionMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
591
616
  assert.equal(events(badCategoryMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
592
617
  const beforeCatalog = requests.length;
593
618
  const catalog = await run(['game', 'categories', '--format', 'json']);
@@ -0,0 +1,78 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const http = require('node:http');
7
+ const { spawn } = require('node:child_process');
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-collect-process-'));
9
+ const entry = path.resolve(__dirname, '../dist/index.js');
10
+ const preload = path.resolve(__dirname, 'wasm-collect-test-preload.cjs');
11
+ const input = path.join(root, 'original.wasm');
12
+ const wasm = Buffer.from('0061736d010000000105016000017f0304030000000404017000030707010372756e00000909010041000b030001020a1003040010010b0400412a0b040041070b', 'hex');
13
+ fs.writeFileSync(input, wasm);
14
+ const md5 = crypto.createHash('md5').update(wasm).digest('hex');
15
+ const rc = path.join(root, 'rc.json');
16
+ const secret = 'sessionid=TEST_ONLY_COLLECT_SECRET';
17
+ fs.writeFileSync(rc, JSON.stringify({ cookie: secret }));
18
+ let mode = 'success'; const calls = [];
19
+ const server = http.createServer(async (req, res) => {
20
+ let body = ''; for await (const b of req) body += b;
21
+ calls.push({ url: req.url, method: req.method, body: body ? JSON.parse(body) : null, cookie: req.headers.cookie });
22
+ res.setHeader('content-type', 'application/json');
23
+ const data = mode === 'auth' ? { code: -401, message: secret }
24
+ : req.url.includes('/start') ? { code: 0, collect_state: 'open', cookie: secret }
25
+ : { code: 0, func_ids: [1, 0, 1], boot_func_ids: [0], cookie: secret };
26
+ res.end(JSON.stringify(data));
27
+ });
28
+ async function run(args) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(process.execPath, ['-r', preload, entry, ...args], { cwd: root,
31
+ env: { ...process.env, TTMGRC_PATH: rc, TTMG_COLLECT_TEST_PORT: String(server.address().port),
32
+ HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '',
33
+ NO_COLOR: '1', FORCE_COLOR: '0' } });
34
+ let stdout = '', stderr = '';
35
+ child.stdout.on('data', b => stdout += b); child.stderr.on('data', b => stderr += b);
36
+ const timer = setTimeout(() => { child.kill('SIGKILL'); reject(Error('CLI timeout')); }, 15000);
37
+ child.on('error', reject); child.on('close', code => { clearTimeout(timer); resolve({ code, stdout, stderr }); });
38
+ });
39
+ }
40
+ function result(r, code = 0) {
41
+ assert.equal(r.code, code, r.stderr || r.stdout);
42
+ assert.equal(r.stdout.trim().split('\n').length, 1, r.stdout);
43
+ assert.equal((r.stdout+r.stderr).includes(secret), false);
44
+ return JSON.parse(r.stdout);
45
+ }
46
+ (async () => {
47
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
48
+ const args = ['--client-key','fixture','--wasm-md5',md5,'--format','json'];
49
+ const a = result(await run(['wasm','collect','start', ...args, '--verbose']));
50
+ assert.equal(a.collectState,'open'); assert.equal(calls[0].cookie,secret);
51
+ assert.equal(calls[0].body.reset,false);
52
+ result(await run(['wasm','collect','start',...args,'--reset']));
53
+ assert.equal(calls[1].body.reset,true);
54
+ const beforeDefault = calls.length;
55
+ const defaultJson = result(await run(['--verbose', 'wasm','collect','start','--client-key','fixture','--wasm-md5',md5]));
56
+ assert.equal(defaultJson.collectState,'open');assert.equal(calls.length,beforeDefault+1);
57
+ const output=path.join(root,'collection.json');
58
+ const exported=result(await run(['wasm','collect','export',...args,'--input',input,'--output',output,'--verbose']));
59
+ assert.equal(exported.collection.path,output);assert.equal(calls[3].method,'GET');
60
+ assert.equal(calls[3].cookie,secret);assert.match(calls[3].url,/strategy=union/);
61
+ const collection=JSON.parse(fs.readFileSync(output,'utf8'));
62
+ assert.deepEqual(collection.funcIds,[0,1]);assert.deepEqual(collection.bootFuncIds,[0]);
63
+ assert.equal(fs.readFileSync(output,'utf8').includes(secret),false);
64
+ const split=result(await run(['wasm','split','--input',input,'--collection',output,
65
+ '--output',path.join(root,'split'),'--dry-run','--format','json']));
66
+ assert.equal(split.status,'ready');
67
+ mode='auth';
68
+ assert.equal(result(await run(['wasm','collect','start',...args,'--verbose']),1).errorCode,'WASM_AUTH_REQUIRED');
69
+ const count=calls.length;fs.writeFileSync(rc,'{}');
70
+ assert.equal(result(await run(['wasm','collect','start',...args]),1).errorCode,'WASM_AUTH_REQUIRED');
71
+ assert.equal(calls.length,count);
72
+ const unknown=await run(['wasm','collect','export',...args,'--reset']);assert.notEqual(unknown.code,0);
73
+ assert.equal(calls.length,count);
74
+ assert.deepEqual(fs.readFileSync(input),wasm);
75
+ console.log('Wasm collect process contract passed: local login transport, reset, export-to-split, clean verbose output; fixture only');
76
+ })().catch(e => { console.error(e); process.exitCode=1; }).finally(() => {
77
+ server.closeAllConnections();server.close();fs.rmSync(root,{recursive:true,force:true});
78
+ });
@@ -0,0 +1,13 @@
1
+ // Test-only transport: credentials and platform responses are synthetic.
2
+ const assert = require('node:assert/strict');
3
+ const https = require('node:https');
4
+ const http = require('node:http');
5
+ const port = Number(process.env.TTMG_COLLECT_TEST_PORT);
6
+ assert.ok(Number.isInteger(port) && port > 0 && port < 65536);
7
+ https.request = (options, callback) => {
8
+ assert.equal(options.hostname, 'developers.tiktok.com');
9
+ assert.match(options.path, /^\/api\/wasm-collect\/v1\/(start|export)(\?|$)/);
10
+ return http.request({ ...options, protocol: 'http:', hostname: '127.0.0.1', host: undefined,
11
+ port, agent: undefined, servername: undefined }, callback);
12
+ };
13
+ https.get = (options, callback) => { const req = https.request(options, callback); req.end(); return req; };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.14",
3
+ "version": "0.4.6-vibe-beta.15",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
49
49
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
50
50
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
51
- "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
51
+ "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/run-ts-test.js src/commands/native/wasm/collect.test.ts && node scripts/acceptance-fixture.test.js",
52
52
  "test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
53
53
  "test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
54
54
  "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
package/vibe-upload.md CHANGED
@@ -2,10 +2,17 @@
2
2
 
3
3
  ## 中文
4
4
 
5
- 默认资料修复已在 `0.4.6-vibe-beta.11` 发布:CLI 自动补齐此前跑通的 PPE 试用资料,用户只提供标题和头像。beta.10 仅修改了输入提示,没有自动补齐字段,真实基础信息提交会被拒绝。
5
+ 适用版本:`0.4.6-vibe-beta.15`。用户只提供标题和头像;Skill 可选填描述与分类,其他应用资料由 CLI 自动补齐。
6
6
 
7
7
  本次范围:本地准备 → 授权绑定 → 上传头像 → 提交应用信息 → 上传代码包 → 上传完成。不包含预览、审核或正式发布。`--web` 使用真实 PPE 授权和上传;`--mock` 的授权与素材上传仍为模拟。网页授权后自动上传的修复已在 `0.4.6-vibe-beta.9` 发布;用户输入要求已在 `0.4.6-vibe-beta.10` 修正。
8
8
 
9
+ ### Quick Start
10
+
11
+ 1. 安装并检查 `ttmg --version`、`ttmg capabilities --format json`。
12
+ 2. Skill 根据实际玩法,用 `ttmg game categories --format json` 查询并选择分类;可生成描述并传 `--description`。未传时默认描述为 `Discover the fun of TikTok Mini Games.`,默认分类为 `Other/Other`。
13
+ 3. 执行下方 Web 上传命令并保持原进程运行,在自动打开的浏览器完成授权。需要核对描述和分类时,先用 `--dry-run --format json` 本地检查。
14
+ 4. 等待 `terminal=true`、`status=uploaded`、`uploaded=true`、`simulation=false` 和退出码 0,再报告上传完成。失败读取 `errorCode` 与 `nextAction`,不自动重传。
15
+
9
16
  ### Web 上传入口
10
17
 
11
18
  真实授权和上传使用 `--web`。安装 `vibe-beta` 标签对应的版本,再用 `ttmg --version` 核对实际执行版本。`beta` 是另一个发布标签,不能用它判断 Web 链路是否包含最新修复。无需设置 PPE 环境变量或预先执行 `ttmg login`;用户在本次浏览器授权页完成授权。
@@ -15,7 +22,7 @@ Skill 只引导开发者准备游戏名称、本地头像和工程,然后调
15
22
  Skill 固定调用:
16
23
 
17
24
  ```sh
18
- npm install -g @ttmg/cli@vibe-beta
25
+ npm install -g @ttmg/cli@0.4.6-vibe-beta.15
19
26
  ttmg --version
20
27
  ttmg capabilities --format json
21
28
  ttmg upload --vibe --web --dir ./game --title "星环守卫" --icon ./avatar.png --format ndjson
@@ -39,6 +46,19 @@ ttmg upload --vibe --web --dir ./game --title "星环守卫" --icon ./avatar.png
39
46
 
40
47
  CLI 在 `app_info` 请求中自动带上 `description`、`category`、`subcategory`、`terms_of_service`、`privacy_policy`、`copyright_confirmation`、`contact_email`,再填入用户标题和真实上传头像 URI。默认资料标识为 `ppe-trial`;命令中没有这些参数,不代表请求里没传。能力查询的 `appInfoFields.defaultedByCli` 列出默认字段。
41
48
 
49
+ ### 可选游戏描述
50
+
51
+ 默认描述为 `Discover the fun of TikTok Mini Games.`,不包含游戏名称。Skill 可根据实际玩法生成描述并传入 `--description`,无需用户额外填写:
52
+
53
+ ```sh
54
+ ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --description "Stack cats on a rooftop and keep the tower balanced." --format ndjson
55
+ ```
56
+
57
+ - 优先级:`--description` > 旧 `--app-info` 文件中的描述 > 默认描述;只覆盖描述,其他字段保持原有规则。
58
+ - 参数首尾空白会被去除;空白文本或无效 Unicode 返回 `VIBE_INVALID_DESCRIPTION`,不会创建任务或上传。
59
+ - `prepared` 和后续终态返回 `description` 与 `descriptionSource`(`arguments / app-info / defaults`);可用 `--dry-run` 检查。
60
+ - 描述是可选 Skill 输入,省略仍可上传,不新增用户必填项。
61
+
42
62
  ### 查询并推荐游戏分类
43
63
 
44
64
  以下命令和参数从 `0.4.6-vibe-beta.14` 开始提供。查询离线执行,不登录、不创建任务、不写平台:
@@ -199,6 +219,13 @@ TTMG_VIBE_BACKEND=local-http TTMG_VIBE_API=http://127.0.0.1:3100 ttmg upload --v
199
219
 
200
220
  The default-metadata fix shipped in `0.4.6-vibe-beta.11`. The CLI fills the existing PPE trial metadata automatically; users provide only title and icon. Beta.10 changed input guidance without filling the fields, and its real app-info submission was rejected.
201
221
 
222
+ ### Quick Start
223
+
224
+ 1. Install, then check `ttmg --version` and `ttmg capabilities --format json`.
225
+ 2. The Skill queries `ttmg game categories --format json` and selects a pair from the actual gameplay. It may generate a description and pass `--description`. Omitting these options keeps `Discover the fun of TikTok Mini Games.` and `Other/Other`.
226
+ 3. Run the Web upload command below, keep the same process alive and authorize in the opened browser. To inspect description and classification first, use `--dry-run --format json` for local validation.
227
+ 4. Confirm `terminal=true`, `status=uploaded`, `uploaded=true`, `simulation=false` and exit code 0 before reporting upload completion. On failure, read `errorCode` and `nextAction`; do not automatically reupload.
228
+
202
229
  ### Web upload entry
203
230
 
204
231
  Scope: local preparation → authorization binding → avatar upload → app-info submission → package upload → **upload complete**. No preview, review or production release. `--web` uses real PPE authorization and uploads; `--mock` still simulates authorization and material uploads.
@@ -208,7 +235,7 @@ Automatic upload was fixed in `0.4.6-vibe-beta.9`; the user-input clarification
208
235
  Use `--web` for real authorization and upload. Install `vibe-beta` and check the executable with `ttmg --version`. The separate `beta` tag does not identify the latest Web fixes. No PPE environment variables or prior `ttmg login` are required; the user authorizes in the browser for this task.
209
236
 
210
237
  ```sh
211
- npm install -g @ttmg/cli@vibe-beta
238
+ npm install -g @ttmg/cli@0.4.6-vibe-beta.15
212
239
  ttmg --version
213
240
  ttmg capabilities --format json
214
241
  ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
@@ -230,6 +257,19 @@ The CLI submits `description`, `category`, `subcategory`, `terms_of_service`, `p
230
257
 
231
258
  Source checked on 2026-09-14: BFF enforces PNG/JPEG, 1024×1024, 5 MiB icons and a 100 MiB Native ZIP/uncompressed-size limit. Full local ZIP-content/uncompressed-size validation is not implemented. On 2026-09-15, a corrected build completed real PPE web authorization and icon, default app-info and package uploads with owned test fixtures. This does not cover every game, phone authorization or production.
232
259
 
260
+ ### Optional game description
261
+
262
+ The default is `Discover the fun of TikTok Mini Games.`, without the game title. A Skill may generate a description from the actual gameplay and pass `--description` without asking the user for another field:
263
+
264
+ ```sh
265
+ ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --description "Stack cats on a rooftop and keep the tower balanced." --format ndjson
266
+ ```
267
+
268
+ - Precedence: `--description` > legacy `--app-info` description > default. Only the description is overridden; other metadata follows existing rules.
269
+ - Leading and trailing whitespace is trimmed. Blank text or invalid Unicode returns `VIBE_INVALID_DESCRIPTION` before task creation or upload.
270
+ - `prepared` and subsequent terminal events expose `description` and `descriptionSource` (`arguments / app-info / defaults`). Inspect them with `--dry-run`.
271
+ - Description is an optional Skill input. Omitting it still permits upload and adds no required user input.
272
+
233
273
  ### Query and recommend game categories
234
274
 
235
275
  These commands and options are available from `0.4.6-vibe-beta.14`. Queries run offline without login, task creation or platform writes: