@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/README.md CHANGED
@@ -9,6 +9,10 @@ Vibe real web authorization and upload: use `--web` with the title, local icon a
9
9
 
10
10
  #### Installation 安装
11
11
 
12
+ `0.4.6-vibe-beta.15`:Vibe 上传支持 Skill 传入 `--description "游戏描述"`。省略时默认使用 `Discover the fun of TikTok Mini Games.`;旧 `--app-info` 文件中的描述继续保留。详见 [Quick Start 与描述参数](vibe-upload.md)。
13
+
14
+ `0.4.6-vibe-beta.15`: Vibe upload accepts `--description "Game description"` from a Skill. When omitted, it uses `Discover the fun of TikTok Mini Games.` or preserves the description from a legacy `--app-info` file. See [Quick Start and description usage](vibe-upload.md).
15
+
12
16
  `0.4.6-vibe-beta.14` 支持分类查询与可选上传分类参数。Skill 可用 `ttmg game categories --format json` 查询,再向 Vibe 上传传入 `--category Puzzle --subcategory Physics`。两个参数都省略时保留 Other/Other;详见 [分类用法](vibe-upload.md)。
13
17
 
14
18
  `0.4.6-vibe-beta.14` supports category discovery and optional upload classification. Query `ttmg game categories --format json`, then pass `--category Puzzle --subcategory Physics` to Vibe upload. Omitting both preserves Other/Other. See [category usage](vibe-upload.md).
@@ -30,7 +34,7 @@ For Agent and Monetization workflows, the published beta is:
30
34
  Agent、商业化流程的已发布 beta 为:
31
35
 
32
36
  ```bash
33
- npm install -g @ttmg/cli@0.4.6-agent-beta.2
37
+ npm install -g @ttmg/cli@0.4.6-agent-beta.3
34
38
  ttmg --version
35
39
  ttmg capabilities --format json
36
40
  ```
@@ -237,9 +241,70 @@ ttmg dev session stop <SESSION_ID> --format json
237
241
 
238
242
  ### Wasm prepare and split / Wasm 准备与分包
239
243
 
244
+ #### Platform collection commands (agent-beta.3) / 平台采集命令
245
+
246
+ Install `0.4.6-agent-beta.3` and check `commands.wasmCollect` in `ttmg capabilities`.
247
+ The CLI uses its own `ttmg login` session; do not pass or read Cookie values in
248
+ Agent prompts, arguments or local HTTP requests. No running DevTool server is required.
249
+
250
+ ```bash
251
+ ttmg wasm prepare --input original.wasm.br --output .ttmg/wasm/prepared --format json
252
+ # Use prepare's input.wasmMd5 for <MD5> and configure the instrumented game
253
+ # to report that same original MD5 and Client Key.
254
+ ttmg wasm collect start --client-key <CLIENT_KEY> --wasm-md5 <MD5> --format json
255
+ # Integrate the prepared copy, scan, clear device storage and play the game.
256
+ ttmg wasm collect export --client-key <CLIENT_KEY> --wasm-md5 <MD5> \
257
+ --input original.wasm.br --output .ttmg/wasm/collection.json --format json
258
+ ttmg wasm split --input original.wasm.br --collection .ttmg/wasm/collection.json --output .ttmg/wasm/split --format json
259
+ ```
260
+
261
+ Run these examples at the game project root. Keep collection and temporary
262
+ artifacts outside the project or under its root `.ttmg`; an ordinary JSON file
263
+ in game assets can be included in future builds and uploads.
264
+ `start` opens or resumes a window and preserves existing IDs by default.
265
+ `--reset` clears platform collection for this game and MD5; it does not clear
266
+ phone storage. On an uncertain start or timeout, retry without `--reset`.
267
+ `export` reads a union snapshot without closing the window. It needs the original
268
+ Wasm, validates the collection MD5 against its file bytes or uncompressed bytes,
269
+ and writes schema 1 with the uncompressed `wasmSha256`, `funcIds` and `bootFuncIds`.
270
+ Read the file at the returned `collection.path`. Existing files are rejected;
271
+ use a new output name for each snapshot. Credentials and raw API responses are
272
+ omitted even with `--verbose`.
273
+
274
+ Only `start` writes platform state. `export` only reads collection and writes a
275
+ local file. Neither command changes Unity files or clears device storage.
276
+ Platform requests default to 30 seconds (`--timeout`, 1–300). Missing or expired
277
+ login returns `WASM_AUTH_REQUIRED`; empty collection returns `WASM_COLLECTION_EMPTY`;
278
+ an unmatched original returns `WASM_COLLECTION_MISMATCH`. No file is published on
279
+ these failures. A nonempty collection does not prove gameplay coverage.
280
+ The commands reuse the existing platform request environment; local fixture
281
+ tests do not establish real platform or device acceptance.
282
+
283
+ 安装 `0.4.6-agent-beta.3`,通过 `ttmg capabilities` 中的 `commands.wasmCollect` 确认支持。
284
+ CLI 内部复用 `ttmg login` 登录态,无需运行 DevTool 本地服务,Agent 不读取或传递 Cookie。
285
+ 按上方命令执行:prepare → 开采集窗口 → 接入插桩副本并扫码、清存储、玩游戏 → export → split。
286
+ `<MD5>` 使用 prepare 返回的 `input.wasmMd5`,插桩游戏须上报相同的原始 MD5 和 Client Key。
287
+
288
+ 示例在游戏工程根目录执行。采集文件和临时产物应放工程外或根目录 `.ttmg` 下;
289
+ 放在普通游戏资源目录的 JSON 会参与后续构建和上传。
290
+ `start` 默认保留已有函数 ID;只有 `--reset` 清空平台上此游戏、此 MD5 的历史采集,
291
+ 不会清理手机存储。超时或不确定是否成功时,不带 `--reset` 重试。
292
+ `export` 读取当前合集快照,不关闭采集窗口;必须提供原始 Wasm,命令校验文件字节或
293
+ 解压后字节的 MD5,再写入 schema 1、解压后原始 `wasmSha256`、`funcIds` 和 `bootFuncIds`。
294
+ 从返回的 `collection.path` 读取文件;已有文件不覆盖,每轮快照使用新文件名。
295
+ 即使开启 `--verbose`,也不输出登录凭证或原始接口响应。
296
+
297
+ 仅 `start` 写平台状态;`export` 只读平台并写本地文件。两者均不写回 Unity 工程或清理手机存储。
298
+ 请求默认超时 30 秒,`--timeout` 范围为 1–300 秒。未登录或登录过期返回 `WASM_AUTH_REQUIRED`,
299
+ 空采集返回 `WASM_COLLECTION_EMPTY`,原始文件不匹配返回 `WASM_COLLECTION_MISMATCH`;
300
+ 这些失败不会生成采集文件。非空结果不等于玩法覆盖完整。
301
+ 命令沿用现有平台请求环境;本地模拟测试不代表真实平台或真机验收。
302
+
303
+ #### Local artifacts / 本地产物
304
+
240
305
  ```bash
241
- ttmg wasm prepare --input original.wasm.br --output prepared --format json
242
- ttmg wasm split --input original.wasm.br --collection collection.json --output split --format json
306
+ ttmg wasm prepare --input original.wasm.br --output .ttmg/wasm/prepared --format json
307
+ ttmg wasm split --input original.wasm.br --collection .ttmg/wasm/collection.json --output .ttmg/wasm/split --format json
243
308
  ```
244
309
 
245
310
  These experimental commands create artifacts in a new directory; they do not
package/dist/index.js CHANGED
@@ -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)',
@@ -7239,12 +7249,14 @@ const messages = {
7239
7249
  'cli.command.game.categories.unknown': 'Unknown category. Run ttmg game categories --format json for valid values.',
7240
7250
  'cli.command.game.categories.invalidFormat': 'Use text, json, or ndjson output.',
7241
7251
  'cli.command.upload.vibeCategory': 'Optional game category; provide with --subcategory (--vibe). Query ttmg game categories --format json',
7252
+ 'cli.command.upload.vibeDescription': 'Optional game description (--vibe); default: Discover the fun of TikTok Mini Games.',
7242
7253
  'cli.command.upload.vibeSubcategory': 'Optional subcategory belonging to --category; omit both to keep Other/Other (--vibe)',
7243
7254
  'cli.command.upload.vibeWebGuide': `
7244
7255
  Vibe web upload (real PPE trial):
7245
7256
  ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
7246
7257
  Use --package ./game.zip instead of --dir for an existing ZIP.
7247
7258
  Collect only title and icon; the CLI fills other app_info fields with PPE trial defaults.
7259
+ Skill may pass --description "Game description"; otherwise use "Discover the fun of TikTok Mini Games." (legacy metadata-file descriptions are preserved).
7248
7260
  Skill may query ttmg game categories --format json and pass --category Puzzle --subcategory Physics together.
7249
7261
  Omit both to keep Other/Other (or the legacy metadata-file classification). Invalid pairs fail before task creation.
7250
7262
  prepared and terminal events expose the submitted classification and its source.
@@ -7645,6 +7657,16 @@ Vibe web upload (real PPE trial):
7645
7657
  'zh-CN': {
7646
7658
  'cli.command.wasm.desc': '准备或分包 Wasm,产物写入独立目录',
7647
7659
  'cli.command.wasm.prepare': '对原始 Wasm 插桩,用于函数采集',
7660
+ 'cli.command.wasm.collect': '复用 CLI 本机登录态管理平台函数采集',
7661
+ 'cli.command.wasm.collect.start': '开启或恢复采集窗口;未指定 --reset 时保留已有函数 ID',
7662
+ 'cli.command.wasm.collect.export': '导出函数 ID,生成与原始 Wasm 匹配的 collection.json',
7663
+ 'cli.command.wasm.collect.clientKey': '采集游戏的 Client Key',
7664
+ 'cli.command.wasm.collect.md5': '插桩游戏上报使用的原始 Wasm 采集 MD5',
7665
+ 'cli.command.wasm.collect.timeout': '平台请求超时:1–300 秒',
7666
+ 'cli.command.wasm.collect.reset': '清空此游戏、此 Wasm 的历史函数 ID,再开始采集',
7667
+ 'cli.command.wasm.collect.output': '新的采集 JSON 文件(不覆盖已有文件)',
7668
+ 'wasm.collect.succeeded': 'Wasm 采集命令已完成。',
7669
+ 'wasm.collect.failed': 'Wasm 采集命令失败。',
7648
7670
  'cli.command.wasm.split': '使用与原始 Wasm 匹配的采集数据分包',
7649
7671
  'cli.command.wasm.input': '原始 .wasm 或 Brotli .wasm.br 文件',
7650
7672
  'cli.command.wasm.output': '新的输出目录(不覆盖已有内容)',
@@ -7674,12 +7696,14 @@ Vibe web upload (real PPE trial):
7674
7696
  'cli.command.game.categories.unknown': '类型不存在。使用 ttmg game categories --format json 查询有效值。',
7675
7697
  'cli.command.game.categories.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
7676
7698
  'cli.command.upload.vibeCategory': '可选游戏类型,须与 --subcategory 同传(--vibe);用 ttmg game categories --format json 查询',
7699
+ 'cli.command.upload.vibeDescription': '可选游戏描述(--vibe);默认值:Discover the fun of TikTok Mini Games.',
7677
7700
  'cli.command.upload.vibeSubcategory': '所属类型的可选子类型;两者均不传保留 Other/Other(--vibe)',
7678
7701
  'cli.command.upload.vibeWebGuide': `
7679
7702
  Vibe 网页上传(真实 PPE 试用):
7680
7703
  ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
7681
7704
  已有 ZIP 时,用 --package ./game.zip 替换 --dir。
7682
7705
  只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
7706
+ Skill 可传 --description "游戏描述";省略时使用 Discover the fun of TikTok Mini Games.(旧资料文件中的描述保留)。
7683
7707
  Skill 可先查 ttmg game categories --format json,再成对传入 --category Puzzle --subcategory Physics。
7684
7708
  两者均不传保留 Other/Other(旧资料文件调用保留文件分类);无效组合在创建任务前报错。
7685
7709
  prepared 和终态事件返回最终 classification 及其来源。
@@ -16352,7 +16376,7 @@ async function authStatus(options = {}) {
16352
16376
  return result;
16353
16377
  }
16354
16378
 
16355
- var version = "0.4.6-vibe-beta.14";
16379
+ var version = "0.4.6-vibe-beta.15";
16356
16380
  var engines = {
16357
16381
  node: ">=20.19.1 <21 || >=22"
16358
16382
  };
@@ -16362,13 +16386,15 @@ var pkg = {
16362
16386
  };
16363
16387
 
16364
16388
  // Skill-facing v2 contract. Backend credentials never cross this boundary.
16389
+ const VIBE_DEFAULT_DESCRIPTION = 'Discover the fun of TikTok Mini Games.';
16365
16390
  const VIBE_INPUT_REQUIREMENTS = {
16366
16391
  requiredUserInputs: ['title', 'icon'],
16367
16392
  codeSource: ['dir', 'package'],
16368
16393
  additionalMetadataSource: 'cli-defaults',
16369
16394
  defaultMetadataProfile: 'ppe-trial',
16370
16395
  requestAdditionalMetadata: false,
16371
- optionalSkillInputs: ['category', 'subcategory'],
16396
+ optionalSkillInputs: ['category', 'subcategory', 'description'],
16397
+ defaultDescription: VIBE_DEFAULT_DESCRIPTION,
16372
16398
  categoryQuery: 'ttmg game categories --format json',
16373
16399
  categoryPairRequiredWhenProvided: true,
16374
16400
  defaultCategory: { category: 'Other', subcategory: 'Other' },
@@ -16963,9 +16989,10 @@ var categories = /*#__PURE__*/Object.freeze({
16963
16989
 
16964
16990
  // Current PPE trial defaults: the owner-approved metadata used by the successful
16965
16991
  // real upload fixture on 2026-09-14, with owner-specified legal URLs updated on
16966
- // 2026-09-15. This is not a production metadata profile.
16992
+ // 2026-09-15 and the owner-approved brand description on 2026-09-16.
16993
+ // This is not a production metadata profile.
16967
16994
  const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
16968
- description: 'DevTool PPE integration test fixture. Not a production game.',
16995
+ description: VIBE_DEFAULT_DESCRIPTION,
16969
16996
  category: 'Other',
16970
16997
  subcategory: 'Other',
16971
16998
  terms_of_service: 'https://www.tiktok.com',
@@ -16979,6 +17006,14 @@ const requiredStrings = [
16979
17006
  'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
16980
17007
  ];
16981
17008
  const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
17009
+ function applyVibeDescription(description, appInfo) {
17010
+ if (description === undefined)
17011
+ return appInfo;
17012
+ if (typeof description !== 'string' || !description.trim() ||
17013
+ [...description].some(char => /^[\uD800-\uDFFF]$/.test(char)))
17014
+ throw new VibeError('VIBE_INVALID_DESCRIPTION');
17015
+ return { ...(appInfo || VIBE_PPE_DEFAULT_APP_INFO), description: description.trim() };
17016
+ }
16982
17017
  function applyVibeCategory(options, appInfo) {
16983
17018
  if (options.category === undefined && options.subcategory === undefined)
16984
17019
  return appInfo;
@@ -17133,6 +17168,15 @@ function getCapabilities() {
17133
17168
  sourceProjectIdentity: '.ttmg/vibe-project.json',
17134
17169
  metadataInputs: ['title', 'icon', 'dir|package'],
17135
17170
  inputRequirements: VIBE_INPUT_REQUIREMENTS,
17171
+ description: {
17172
+ option: '--description',
17173
+ required: false,
17174
+ default: VIBE_PPE_DEFAULT_APP_INFO.description,
17175
+ precedence: ['argument', 'legacy-app-info', 'default'],
17176
+ invalidInput: 'reject-before-task-creation',
17177
+ resultField: 'description',
17178
+ sourceField: 'descriptionSource',
17179
+ },
17136
17180
  classification: {
17137
17181
  queryCommand: 'ttmg game categories --format json',
17138
17182
  options: ['--category', '--subcategory'],
@@ -17270,6 +17314,20 @@ function getCapabilities() {
17270
17314
  projectIntegration: false,
17271
17315
  platformWrite: false,
17272
17316
  },
17317
+ wasmCollect: {
17318
+ available: true,
17319
+ experimental: true,
17320
+ authentication: 'local_cli_login',
17321
+ credentialOutput: false,
17322
+ nativeBindingRequired: false,
17323
+ platformVerified: false,
17324
+ outputFormats: ['text', 'json'],
17325
+ start: { command: 'ttmg wasm collect start', platformWrite: true,
17326
+ resetDefault: false, resetOption: '--reset', automaticRetry: false },
17327
+ export: { command: 'ttmg wasm collect export', platformWrite: false,
17328
+ collectionSchemaVersion: '1', originalInputRequired: true,
17329
+ outputPathField: 'collection.path', overwrite: false, strategy: 'union' },
17330
+ },
17273
17331
  dev: {
17274
17332
  command: 'ttmg dev --format json --no-open',
17275
17333
  eventsCommand: 'ttmg dev --events ndjson --no-open',
@@ -17861,6 +17919,8 @@ function getDesktopPath() {
17861
17919
  const program = new commander.Command();
17862
17920
  const isPreviewPageServerWorker = process.argv[2] === '__preview-page-server';
17863
17921
  const isVibePageServerWorker = process.argv[2] === '__vibe-page-server';
17922
+ const wasmArgumentIndex = process.argv.indexOf('wasm', 2);
17923
+ const isWasmCollectCommand = wasmArgumentIndex >= 2 && process.argv[wasmArgumentIndex + 1] === 'collect';
17864
17924
  const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
17865
17925
  const structuredFormatIndex = process.argv.indexOf('--format');
17866
17926
  const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
@@ -17871,6 +17931,7 @@ const structuredFormat = inlineStructuredFormat
17871
17931
  : '';
17872
17932
  if (!isPreviewPageServerWorker &&
17873
17933
  !isVibePageServerWorker &&
17934
+ !(isWasmCollectCommand && !structuredFormat) &&
17874
17935
  structuredFormat !== 'json' &&
17875
17936
  structuredFormat !== 'ndjson' &&
17876
17937
  !process.argv.includes('--implicit')) {
@@ -17943,6 +18004,7 @@ withVerboseOption(program
17943
18004
  .option('--web', t('cli.command.upload.vibeWeb'))
17944
18005
  // Vibe v2 backend selection (including opt-in ppe-mock) stays outside the Skill workflow.
17945
18006
  .option('--title <title>', t('cli.command.upload.vibeTitle'))
18007
+ .option('--description <description>', t('cli.command.upload.vibeDescription'))
17946
18008
  .option('--icon <path>', t('cli.command.upload.vibeIcon'))
17947
18009
  .option('--category <category>', t('cli.command.upload.vibeCategory'))
17948
18010
  .option('--subcategory <subcategory>', t('cli.command.upload.vibeSubcategory'))
@@ -17964,6 +18026,7 @@ withVerboseOption(program
17964
18026
  }
17965
18027
  if ([
17966
18028
  cmd.title,
18029
+ cmd.description,
17967
18030
  cmd.icon,
17968
18031
  cmd.appInfo,
17969
18032
  cmd.category,
@@ -18310,6 +18373,25 @@ devSessionCmd
18310
18373
  });
18311
18374
  });
18312
18375
  const wasmCmd = program.command('wasm').description(t('cli.command.wasm.desc'));
18376
+ const collectCmd = wasmCmd.command('collect').description(t('cli.command.wasm.collect'));
18377
+ for (const operation of ['start', 'export']) {
18378
+ const command = collectCmd.command(operation)
18379
+ .description(t(operation === 'start' ? 'cli.command.wasm.collect.start' : 'cli.command.wasm.collect.export'))
18380
+ .allowExcessArguments(false)
18381
+ .option('--client-key <key>', t('cli.command.wasm.collect.clientKey'))
18382
+ .option('--wasm-md5 <md5>', t('cli.command.wasm.collect.md5'))
18383
+ .option('--timeout <seconds>', t('cli.command.wasm.collect.timeout'), '30')
18384
+ .option('--format <format>', t('cli.command.wasm.format'), 'json');
18385
+ if (operation === 'start')
18386
+ command.option('--reset', t('cli.command.wasm.collect.reset'));
18387
+ else
18388
+ command.option('--input <path>', t('cli.command.wasm.input'))
18389
+ .option('--output <path>', t('cli.command.wasm.collect.output'));
18390
+ command.action(async (options) => {
18391
+ const Native = await Promise.resolve().then(function () { return index; });
18392
+ await Native.wasmCollectCommand(operation, options);
18393
+ });
18394
+ }
18313
18395
  for (const operation of ['prepare', 'split']) {
18314
18396
  const command = wasmCmd.command(operation)
18315
18397
  .description(t(operation === 'prepare' ? 'cli.command.wasm.prepare' : 'cli.command.wasm.split'))
@@ -19126,7 +19208,7 @@ async function prepareVibeInput(options, onPackaging, signal, deps = { check: ex
19126
19208
  const title = options.title?.trim();
19127
19209
  if (!title)
19128
19210
  throw new VibeError('VIBE_TITLE_REQUIRED');
19129
- const appInfo = applyVibeCategory(options, readVibeAppInfo(options.appInfo));
19211
+ const appInfo = applyVibeCategory(options, applyVibeDescription(options.description, readVibeAppInfo(options.appInfo)));
19130
19212
  if (options.package && options.dir)
19131
19213
  throw new VibeError('VIBE_INPUT_CONFLICT');
19132
19214
  const image = readFile(options.icon, VIBE_ICON_LIMIT, 'VIBE_INVALID_ICON');
@@ -19681,10 +19763,12 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19681
19763
  terminal: !!options.dryRun,
19682
19764
  nextAction: options.dryRun ? 'none' : 'wait',
19683
19765
  title: input.title,
19766
+ description: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).description,
19767
+ descriptionSource: options.description !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
19684
19768
  classification: {
19685
19769
  category: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).category,
19686
19770
  subcategory: (input.appInfo || VIBE_PPE_DEFAULT_APP_INFO).subcategory,
19687
- source: options.category !== undefined ? 'arguments' : input.appInfo ? 'app-info' : 'defaults',
19771
+ source: options.category !== undefined ? 'arguments' : options.appInfo !== undefined ? 'app-info' : 'defaults',
19688
19772
  catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
19689
19773
  },
19690
19774
  iconSizeBytes: input.icon.bytes.length,
@@ -19880,6 +19964,7 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
19880
19964
  const messages = {
19881
19965
  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.'],
19882
19966
  VIBE_INVALID_CATEGORY: ['类型或子类型无效,或两者不匹配。使用 ttmg game categories --format json 查询有效组合。', 'Invalid category/subcategory pair. Query valid values with ttmg game categories --format json.'],
19967
+ VIBE_INVALID_DESCRIPTION: ['描述必须是非空的有效 Unicode 文本;省略 --description 可使用默认描述。', 'Description must be non-empty valid Unicode text; omit --description to use the default.'],
19883
19968
  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.'],
19884
19969
  VIBE_AUTH_DENIED: [
19885
19970
  '已取消授权,本次未上传。',
@@ -31195,12 +31280,17 @@ async function snapshotWasm(input, output) {
31195
31280
  callback(bytes > 512 * 1024 * 1024 ? fail('WASM_INPUT_INVALID', 'Expanded Wasm exceeds 512 MiB.') : null, chunk);
31196
31281
  } });
31197
31282
  const source = fs$1.createReadStream(input);
31283
+ const fileMd5 = crypto$1.createHash('md5');
31284
+ const identity = new node_stream.Transform({ transform(chunk, _encoding, callback) {
31285
+ fileMd5.update(chunk);
31286
+ callback(null, chunk);
31287
+ } });
31198
31288
  const target = fs$1.createWriteStream(output, { flags: 'wx', mode: 0o600 });
31199
31289
  try {
31200
31290
  if (input.endsWith('.br'))
31201
- await promises$2.pipeline(source, zlib$1.createBrotliDecompress(), bound, target);
31291
+ await promises$2.pipeline(source, identity, zlib$1.createBrotliDecompress(), bound, target);
31202
31292
  else
31203
- await promises$2.pipeline(source, bound, target);
31293
+ await promises$2.pipeline(source, identity, bound, target);
31204
31294
  }
31205
31295
  catch (error) {
31206
31296
  throw fail(error.code === 'WASM_INPUT_INVALID' ? error.code : 'WASM_INPUT_INVALID', 'Could not read the original Wasm input.');
@@ -31217,7 +31307,11 @@ async function snapshotWasm(input, output) {
31217
31307
  throw fail('WASM_INPUT_INVALID', 'Expected a WebAssembly version 1 binary.');
31218
31308
  if (!WebAssembly.validate(await fsp.readFile(output)))
31219
31309
  throw fail('WASM_INPUT_INVALID', 'Original Wasm is invalid or unsupported by this Node.js WebAssembly engine.');
31220
- return { bytes, wasmSha256: await digest(output) };
31310
+ const rawMd5 = crypto$1.createHash('md5');
31311
+ for await (const chunk of fs$1.createReadStream(output))
31312
+ rawMd5.update(chunk);
31313
+ return { bytes, wasmSha256: await digest(output), wasmMd5: fileMd5.digest('hex'),
31314
+ uncompressedWasmMd5: rawMd5.digest('hex') };
31221
31315
  }
31222
31316
  async function executeWasm(operation, options, dependencies = {}) {
31223
31317
  const base = { schemaVersion: '1', command: `wasm.${operation}`, operationId: `wasm_${crypto$1.randomUUID()}`,
@@ -31325,6 +31419,149 @@ async function wasmCommand(operation, options) {
31325
31419
  process.exitCode = result.status === 'failed' ? 1 : 0;
31326
31420
  }
31327
31421
 
31422
+ class CollectError extends Error {
31423
+ constructor(code, message) {
31424
+ super(message);
31425
+ this.code = code;
31426
+ }
31427
+ }
31428
+ const invalid = (code, message) => { throw new CollectError(code, message); };
31429
+ async function executeWasmCollect(operation, options, overrides = {}) {
31430
+ const deps = { authenticated: () => Boolean(getCurrentUser()?.cookie), request, ...overrides };
31431
+ const base = { schemaVersion: '1', command: `wasm.collect.${operation}`,
31432
+ operationId: `wasm_collect_${crypto$1.randomUUID()}`, platformWrite: operation === 'start',
31433
+ platformWriteAttempted: false, sourceModified: false };
31434
+ let temporary;
31435
+ let staging;
31436
+ let timer;
31437
+ try {
31438
+ const timeout = Number(options.timeout ?? 30);
31439
+ if (!['start', 'export'].includes(operation) || !['json', 'text'].includes(options.format || 'json') ||
31440
+ !/^[A-Za-z0-9_-]{1,128}$/.test(options.clientKey || '') || !/^[a-fA-F0-9]{32}$/.test(options.wasmMd5 || '') ||
31441
+ !Number.isInteger(timeout) || timeout < 1 || timeout > 300 ||
31442
+ (options.reset !== undefined && typeof options.reset !== 'boolean') ||
31443
+ (operation === 'export' && (!options.input || !options.output || options.reset !== undefined)) ||
31444
+ (operation === 'start' && (options.input || options.output))) {
31445
+ 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.');
31446
+ }
31447
+ const clientKey = options.clientKey;
31448
+ const wasmMd5 = options.wasmMd5.toLowerCase();
31449
+ const output = options.output ? path$1.resolve(options.output) : undefined;
31450
+ let source;
31451
+ if (operation === 'export') {
31452
+ // lstat detects dangling links as well as existing files. Never overwrite.
31453
+ if (await fsp.lstat(output).catch((e) => { if (e.code !== 'ENOENT')
31454
+ throw e; return null; }))
31455
+ invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
31456
+ temporary = await fsp.mkdtemp(path$1.join(os$1.tmpdir(), 'ttmg-collect-'));
31457
+ try {
31458
+ source = await snapshotWasm(path$1.resolve(options.input), path$1.join(temporary, 'original.wasm'));
31459
+ }
31460
+ catch {
31461
+ return invalid('WASM_INPUT_INVALID', 'Expected a valid original Wasm or Brotli Wasm file.');
31462
+ }
31463
+ if (wasmMd5 !== source.wasmMd5 && wasmMd5 !== source.uncompressedWasmMd5)
31464
+ invalid('WASM_COLLECTION_MISMATCH', 'Collection MD5 does not match this original Wasm (file bytes or uncompressed bytes).');
31465
+ }
31466
+ if (!deps.authenticated())
31467
+ invalid('WASM_AUTH_REQUIRED', 'Run ttmg login on this machine before collecting or exporting.');
31468
+ const controller = new AbortController();
31469
+ const deadline = new Promise((_, reject) => {
31470
+ timer = setTimeout(() => {
31471
+ controller.abort();
31472
+ reject(new CollectError('WASM_COLLECT_TIMEOUT', 'Platform request timed out. A start may have reached the server; retry without --reset to preserve collection.'));
31473
+ }, timeout * 1000);
31474
+ });
31475
+ base.platformWriteAttempted = operation === 'start';
31476
+ const response = await Promise.race([deps.request({
31477
+ url: `${WASM_COLLECT_BASE_URL}/${operation === 'start' ? 'start' : 'export'}`,
31478
+ method: operation === 'start' ? 'POST' : 'GET',
31479
+ ...(operation === 'start'
31480
+ ? { data: { app_id: clientKey, wasm_md5: wasmMd5, reset: options.reset ?? false } }
31481
+ : { params: { app_id: clientKey, wasm_md5: wasmMd5, strategy: 'union' } }),
31482
+ signal: controller.signal,
31483
+ logRequestBody: false, logApiCalls: false,
31484
+ }), deadline]);
31485
+ clearTimeout(timer);
31486
+ const code = response.error?.code ?? response.error?.StatusCode ?? response.data?.code;
31487
+ const httpStatus = response.ctx?.httpStatusCode;
31488
+ // Upstream bodies/errors are deliberately never included in output: they
31489
+ // can contain credentials or gateway HTML, even with verbose enabled.
31490
+ if ([401, -401].includes(Number(code)) || httpStatus === 401 ||
31491
+ (Number(code) === -1 && /login|log.?in|登录|未登|auth/i.test(String(response.error?.message ?? response.error?.StatusMessage ?? response.data?.message ?? ''))))
31492
+ invalid('WASM_AUTH_REQUIRED', 'Platform login expired. Run ttmg login and retry.');
31493
+ if ([403, -403].includes(Number(code)) || httpStatus === 403)
31494
+ invalid('WASM_COLLECT_FORBIDDEN', 'This account cannot access collection for this game.');
31495
+ if (response.error || response.data?.code !== 0 || (httpStatus && (httpStatus < 200 || httpStatus >= 300)))
31496
+ invalid('WASM_COLLECT_API_FAILED', 'Platform collection request failed; no collection file was written.');
31497
+ if (operation === 'start') {
31498
+ if (response.data.collect_state !== 'open')
31499
+ invalid('WASM_COLLECT_INVALID_RESPONSE', 'Platform did not confirm an open collection window.');
31500
+ return { ...base, status: 'succeeded', clientKey, wasmMd5, reset: options.reset ?? false,
31501
+ collectState: 'open', nextAction: 'run_instrumented_game_and_collect', errorCode: null, error: null };
31502
+ }
31503
+ if (Array.isArray(response.data.func_ids) && response.data.func_ids.length === 0)
31504
+ invalid('WASM_COLLECTION_EMPTY', 'No function IDs collected. Run the instrumented game and export again.');
31505
+ let ids;
31506
+ try {
31507
+ ids = parseCollection({ schemaVersion: '1', wasmSha256: source.wasmSha256,
31508
+ funcIds: response.data.func_ids, bootFuncIds: response.data.boot_func_ids }, source.wasmSha256);
31509
+ }
31510
+ catch {
31511
+ return invalid('WASM_COLLECTION_INVALID', 'Platform returned invalid function IDs; no collection file was written.');
31512
+ }
31513
+ const collection = { schemaVersion: '1', wasmSha256: source.wasmSha256, ...ids,
31514
+ clientKey, wasmMd5, strategy: 'union', exportedAt: new Date().toISOString() };
31515
+ const bytes = Buffer.from(JSON.stringify(collection, null, 2) + '\n');
31516
+ if (bytes.length > 32 * 1024 * 1024)
31517
+ invalid('WASM_COLLECTION_INVALID', 'Collection JSON exceeds 32 MiB.');
31518
+ await fsp.mkdir(path$1.dirname(output), { recursive: true });
31519
+ staging = path$1.join(path$1.dirname(output), `.ttmg-collection-${crypto$1.randomUUID()}.tmp`);
31520
+ await fsp.writeFile(staging, bytes, { flag: 'wx', mode: 0o600 });
31521
+ // Publish a complete file atomically, with exclusive no-clobber semantics.
31522
+ try {
31523
+ await fsp.link(staging, output);
31524
+ }
31525
+ catch (e) {
31526
+ if (e.code === 'EEXIST')
31527
+ invalid('WASM_OUTPUT_EXISTS', 'Collection output already exists; choose a new file.');
31528
+ throw e;
31529
+ }
31530
+ return { ...base, status: 'succeeded', clientKey, wasmMd5,
31531
+ collection: { path: output, schemaVersion: '1', wasmSha256: source.wasmSha256,
31532
+ funcCount: ids.funcIds.length, bootFuncCount: ids.bootFuncIds.length,
31533
+ bytes: bytes.length, sha256: crypto$1.createHash('sha256').update(bytes).digest('hex') },
31534
+ nextAction: 'split_with_collection', errorCode: null, error: null };
31535
+ }
31536
+ catch (error) {
31537
+ const known = error instanceof CollectError;
31538
+ return { ...base, status: 'failed', errorCode: known ? error.code : 'WASM_COLLECT_FAILED',
31539
+ error: known ? error.message : 'Collection operation failed; check the original Wasm, output directory and connection.',
31540
+ nextAction: known && error.code === 'WASM_AUTH_REQUIRED' ? 'login' : 'fix_and_retry' };
31541
+ }
31542
+ finally {
31543
+ if (timer)
31544
+ clearTimeout(timer);
31545
+ if (staging)
31546
+ await fsp.unlink(staging).catch(() => { });
31547
+ if (temporary)
31548
+ await fsp.rm(temporary, { recursive: true, force: true });
31549
+ }
31550
+ }
31551
+ async function wasmCollectCommand(operation, options) {
31552
+ const result = await executeWasmCollect(operation, options);
31553
+ if ((options.format || 'json') === 'json')
31554
+ console.log(JSON.stringify(result));
31555
+ else {
31556
+ console.log(t(result.status === 'failed' ? 'wasm.collect.failed' : 'wasm.collect.succeeded'));
31557
+ if (result.error)
31558
+ console.error(result.error);
31559
+ if ('collection' in result)
31560
+ console.log(result.collection.path);
31561
+ }
31562
+ process.exitCode = result.status === 'failed' ? 1 : 0;
31563
+ }
31564
+
31328
31565
  const problem = (code, message) => Object.assign(new Error(message), { code });
31329
31566
  const MAX_BYTES = 64 * 1024 * 1024;
31330
31567
  // Export one finite Runtime interval. Sequence numbers are global, so gaps in
@@ -31528,6 +31765,7 @@ var index = /*#__PURE__*/Object.freeze({
31528
31765
  devSessionStopCommand: devSessionStopCommand,
31529
31766
  devWorkflowCommand: devWorkflowCommand,
31530
31767
  doctor: doctor,
31768
+ wasmCollectCommand: wasmCollectCommand,
31531
31769
  wasmCommand: wasmCommand
31532
31770
  });
31533
31771
  //# sourceMappingURL=index.js.map