@ttmg/cli 0.4.6-vibe-beta.1 → 0.4.6-vibe-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.
Files changed (70) hide show
  1. package/README.md +173 -0
  2. package/dist/index.js +2241 -218
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +18 -3
  5. package/dist/public/assets/{Card-BAsimNoh.js → Card-CN2lxY0Y.js} +1 -1
  6. package/dist/public/assets/{Detail-rVxbEDgh.js → Detail-CELZZ-4h.js} +1 -1
  7. package/dist/public/assets/Detail-CELZZ-4h.js.br +0 -0
  8. package/dist/public/assets/{MonetizationMode-B20TAE9o.js → MonetizationMode-C7DNqium.js} +1 -1
  9. package/dist/public/assets/{MonetizationModeSummary-CnHqXyco.js → MonetizationModeSummary-DYE7XUq1.js} +1 -1
  10. package/dist/public/assets/{SectionHeader-Cn3FnOqh.js → SectionHeader-BKc3BWXP.js} +1 -1
  11. package/dist/public/assets/{baseForm-kBTxn8h8.js → baseForm-I9suIiQn.js} +1 -1
  12. package/dist/public/assets/baseForm-I9suIiQn.js.br +0 -0
  13. package/dist/public/assets/{col-VN3Ivd-x.js → col-w9Q-2mgM.js} +1 -1
  14. package/dist/public/assets/{compass-BabZVjAl.js → compass-D6waJ52O.js} +1 -1
  15. package/dist/public/assets/{index-DHdJsD10.js → index-8sDAOAcS.js} +1 -1
  16. package/dist/public/assets/index-8sDAOAcS.js.br +0 -0
  17. package/dist/public/assets/{index-DK4IvdRl.js → index-B6hY5GdS.js} +1 -1
  18. package/dist/public/assets/{index-Cl0_vMWE.js → index-BIrFkB6r.js} +1 -1
  19. package/dist/public/assets/index-BIrFkB6r.js.br +0 -0
  20. package/dist/public/assets/{index-BjQlJrX7.js → index-BLWgpW64.js} +1 -1
  21. package/dist/public/assets/{index-odEI7YQF.js → index-BY3eQmfP.js} +1 -1
  22. package/dist/public/assets/index-BY3eQmfP.js.br +0 -0
  23. package/dist/public/assets/{index-Bjn0ctEd.js → index-BegU9LBT.js} +2 -2
  24. package/dist/public/assets/index-BegU9LBT.js.br +0 -0
  25. package/dist/public/assets/{index-1WjGwqIV.js → index-BqwflQml.js} +1 -1
  26. package/dist/public/assets/{index-DXDZCuG4.js → index-BrOFdVbv.js} +1 -1
  27. package/dist/public/assets/{index-BxsB5E3u.js → index-BvPolBSn.js} +1 -1
  28. package/dist/public/assets/{index-BnOeYQAd.js → index-BwrZjkCc.js} +1 -1
  29. package/dist/public/assets/{index-CUuX90R9.js → index-CLKKui8G.js} +1 -1
  30. package/dist/public/assets/{index-Dohibgdp.js → index-Cb4Ja0tZ.js} +1 -1
  31. package/dist/public/assets/{index-C83QHLmD.js → index-CcUgN29Y.js} +1 -1
  32. package/dist/public/assets/{index-aSl6Wdrp.js → index-CgXOkYrX.js} +1 -1
  33. package/dist/public/assets/index-CgXOkYrX.js.br +0 -0
  34. package/dist/public/assets/{index-DulANwbq.js → index-CsiIrLKY.js} +1 -1
  35. package/dist/public/assets/{index-Ba-JWe4X.js → index-Csmg5oyu.js} +1 -1
  36. package/dist/public/assets/{index-DnnfZ7H_.js → index-DDecPuod.js} +1 -1
  37. package/dist/public/assets/{index-Clq0hTDI.js → index-DDy4PkWT.js} +1 -1
  38. package/dist/public/assets/{index-DIcz5ciL.js → index-DEQ2hVPA.js} +1 -1
  39. package/dist/public/assets/index-DEQ2hVPA.js.br +0 -0
  40. package/dist/public/assets/{index-CpM8t743.js → index-DGkgqtgJ.js} +1 -1
  41. package/dist/public/assets/{index-QNLvejKK.js → index-FVHabbDB.js} +1 -1
  42. package/dist/public/assets/{index-CzXMNZL3.js → index-M8jf-YAZ.js} +1 -1
  43. package/dist/public/assets/{index-BxokHznc.js → index-Q0GC8Irj.js} +1 -1
  44. package/dist/public/assets/index-Q0GC8Irj.js.br +0 -0
  45. package/dist/public/assets/{index--6iHQzho.js → index-YVB9ZW00.js} +1 -1
  46. package/dist/public/assets/{index-O_L1tU2b.js → index-mjEeEQcC.js} +1 -1
  47. package/dist/public/assets/index-mjEeEQcC.js.br +0 -0
  48. package/dist/public/assets/{index-BFFsJ47B.js → index-rLkONFpx.js} +1 -1
  49. package/dist/public/assets/{layers-B4zhx14P.js → layers-Dbwq9AyL.js} +1 -1
  50. package/dist/public/assets/{list-checks-Br6imjXC.js → list-checks-og1usOZf.js} +1 -1
  51. package/dist/public/assets/{sparkles-BjIkcbAi.js → sparkles-p-dZbjGb.js} +1 -1
  52. package/dist/public/assets/{triangle-alert-CgKAc3w5.js → triangle-alert-BZVAEUiL.js} +1 -1
  53. package/dist/public/assets/{zap-DFHw5liD.js → zap-BAhZUPPa.js} +1 -1
  54. package/dist/public/index.html +1 -1
  55. package/dist/scripts/dev-default-smoke.js +67 -0
  56. package/dist/scripts/dev-game-ready-smoke.js +155 -0
  57. package/dist/scripts/verify-agent-contract.js +178 -0
  58. package/dist/scripts/worker.js +28 -22
  59. package/package.json +18 -3
  60. package/CHANGELOG.md +0 -285
  61. package/dist/public/assets/Detail-rVxbEDgh.js.br +0 -0
  62. package/dist/public/assets/baseForm-kBTxn8h8.js.br +0 -0
  63. package/dist/public/assets/index-Bjn0ctEd.js.br +0 -0
  64. package/dist/public/assets/index-BxokHznc.js.br +0 -0
  65. package/dist/public/assets/index-Cl0_vMWE.js.br +0 -0
  66. package/dist/public/assets/index-DHdJsD10.js.br +0 -0
  67. package/dist/public/assets/index-DIcz5ciL.js.br +0 -0
  68. package/dist/public/assets/index-O_L1tU2b.js.br +0 -0
  69. package/dist/public/assets/index-aSl6Wdrp.js.br +0 -0
  70. package/dist/public/assets/index-odEI7YQF.js.br +0 -0
package/dist/index.js CHANGED
@@ -6,18 +6,13 @@ var axios = require('axios');
6
6
  var inquirer = require('inquirer');
7
7
  var chalk = require('chalk');
8
8
  var qs = require('qs');
9
+ var crypto = require('crypto');
9
10
  var path = require('path');
10
11
  var os = require('os');
11
12
  var fs = require('fs');
12
13
  var handlebars = require('handlebars');
13
14
  var esbuild = require('esbuild');
14
15
  var worker_threads = require('worker_threads');
15
- var crypto = require('crypto');
16
- var require$$0 = require('util');
17
- var require$$1$2 = require('stream');
18
- var zlib = require('zlib');
19
- var require$$3 = require('assert');
20
- var require$$0$2 = require('buffer');
21
16
  var jsdom = require('jsdom');
22
17
  var prettier = require('prettier');
23
18
  var express = require('express');
@@ -28,8 +23,12 @@ var WebSocket = require('ws');
28
23
  var net = require('net');
29
24
  var require$$1$1 = require('tls');
30
25
  var require$$2 = require('url');
26
+ var require$$3 = require('assert');
31
27
  var require$$1 = require('tty');
28
+ var require$$0 = require('util');
32
29
  var require$$0$1 = require('events');
30
+ var require$$0$2 = require('buffer');
31
+ var require$$1$2 = require('stream');
33
32
  var http = require('http');
34
33
  var https = require('https');
35
34
  var dns = require('dns');
@@ -45,7 +44,9 @@ var ttmgWasmtool = require('@anrans001/ttmg-wasmtool');
45
44
  var crypto$1 = require('node:crypto');
46
45
  var fs$1 = require('node:fs');
47
46
  var path$1 = require('node:path');
47
+ var zlib = require('zlib');
48
48
  var promises = require('fs/promises');
49
+ var child_process = require('child_process');
49
50
 
50
51
  function _interopNamespaceDefault(e) {
51
52
  var n = Object.create(null);
@@ -7199,14 +7200,51 @@ const messages = {
7199
7200
  'cli.option.dev.client': 'Debug TikTok Mini Games for Client',
7200
7201
  'cli.option.dev.h5': 'Debug TikTok Mini Games for Web',
7201
7202
  'cli.command.login.desc': 'Login with developer account',
7203
+ 'cli.command.login.implicit': 'Login non-interactively and return one JSON result',
7204
+ 'cli.command.login.email': 'Developer account email (implicit mode)',
7205
+ 'cli.command.login.password': 'Developer account password (implicit mode)',
7202
7206
  'cli.command.logout.desc': 'Logout and clear local user data',
7207
+ 'cli.command.auth.desc': 'Inspect local authentication state',
7208
+ 'cli.command.auth.status.desc': 'Return whether the local developer session is reusable',
7209
+ 'cli.command.auth.status.format': 'Output format: text | json',
7210
+ 'cli.command.auth.status.requireAuth': 'Exit non-zero when no reusable local session exists',
7211
+ 'cli.command.capabilities.desc': 'Describe the CLI features available to developers and Agents',
7212
+ 'cli.command.capabilities.format': 'Output format: text | json',
7203
7213
  'cli.command.setup.desc': 'Initialize ttmg environment',
7204
7214
  'cli.command.setup.lang': 'Language (only supports): en-US | zh-CN',
7205
7215
  'cli.command.reset.desc': 'Reset local CLI state',
7206
7216
  'cli.command.init.desc': 'Initialize project',
7207
7217
  'cli.command.dev.desc': 'Open browser dev environment',
7208
7218
  'cli.command.dev.clientKey': 'Client Key of the Mini Game',
7219
+ 'cli.command.dev.format': 'Session output format: text | json',
7220
+ 'cli.command.dev.events': 'Lifecycle event stream: none | ndjson',
7221
+ 'cli.command.dev.noOpen': 'Do not open the DevTool page automatically',
7222
+ 'cli.command.dev.evidenceDir': 'Session evidence output directory',
7223
+ 'cli.command.dev.timeout': 'Stop a structured session after this many seconds',
7224
+ 'cli.command.dev.outputConflict': 'Dev output modes cannot be combined. Use either --format json for one manifest or --events ndjson for a lifecycle event stream.',
7225
+ 'cli.command.dev.session.desc': 'Inspect and stop local DevTool sessions',
7226
+ 'cli.command.dev.session.inspect': 'Inspect one local DevTool session',
7227
+ 'cli.command.dev.session.list': 'List local DevTool sessions',
7228
+ 'cli.command.dev.session.events': 'Read new events from a DevTool session',
7229
+ 'cli.command.dev.session.stop': 'Safely stop one local DevTool session',
7230
+ 'cli.command.dev.session.dir': 'Session root directory (default: .ttmg/sessions)',
7231
+ 'cli.command.dev.session.format': 'Output format: text | json',
7232
+ 'cli.command.dev.session.after': 'Return events with seq greater than this',
7233
+ 'cli.command.dev.session.limit': 'Maximum events to return (1-1000)',
7234
+ 'cli.command.check.desc': 'Check the Mini Game project and return actionable diagnostics',
7235
+ 'cli.command.check.dir': 'Mini Game project directory (default: current directory)',
7236
+ 'cli.command.check.format': 'Output format: text | json',
7237
+ 'cli.command.check.includePassed': 'Include passed checks in structured diagnostics',
7238
+ 'cli.command.check.failOn': 'Non-zero exit threshold: error | warning',
7239
+ 'cli.command.check.ai': 'Add static Agent-debug readiness checks',
7240
+ 'cli.command.doctor.desc': 'Diagnose local CLI, project, auth, and network readiness',
7241
+ 'cli.command.doctor.dir': 'Mini Game project directory (default: current directory)',
7242
+ 'cli.command.doctor.format': 'Output format: text | json',
7243
+ 'cli.command.doctor.failOn': 'Non-zero exit threshold: error | warning',
7209
7244
  'cli.command.build.desc': 'Bundle project',
7245
+ 'cli.command.build.dir': 'Mini Game project directory (structured build only)',
7246
+ 'cli.command.build.format': 'Output format: text | json',
7247
+ 'cli.command.build.includePassed': 'Include passed checks in the structured build result',
7210
7248
  'cli.command.config.desc': 'Manage configuration',
7211
7249
  'cli.command.config.set.desc': 'Set a configuration value',
7212
7250
  'cli.command.config.get.desc': 'Get a configuration value',
@@ -7216,7 +7254,7 @@ const messages = {
7216
7254
  'cli.command.upload.note': 'Note for the upload',
7217
7255
  'cli.command.upload.dir': 'Game project directory (default: current directory)',
7218
7256
  'cli.command.upload.preview': 'Wait for platform processing and output the preview payload',
7219
- 'cli.command.upload.format': 'Output format: text | ndjson',
7257
+ 'cli.command.upload.format': 'Output format: text | json | ndjson',
7220
7258
  'cli.command.testAccount.desc': 'Manage Mini Game test-account access',
7221
7259
  'cli.command.testAccount.add.desc': 'Generate the authorization entry for adding a TikTok test account',
7222
7260
  'cli.command.testAccount.clientKey': 'Client Key of the Mini Game',
@@ -7263,8 +7301,13 @@ const messages = {
7263
7301
  'login.error.withCode': 'Login failed: {message}, error_code: {code}',
7264
7302
  'login.error.withMessage': 'Login failed: {message}',
7265
7303
  'login.error.noUserId': 'Login failed. No user_id in response.',
7304
+ 'login.error.noSessionCookie': 'Login failed. No reusable session cookie was returned.',
7305
+ 'login.error.implicitCredentialsRequired': 'Implicit login requires --email and --password, or TTMG_LOGIN_EMAIL and TTMG_LOGIN_PASSWORD.',
7266
7306
  'login.warning.proxyIssue': 'Login failed. Please refer to the proxy setup doc:\nhttps://bytedance.larkoffice.com/wiki/PeIawT4M4ia9R8kYyLMcjwHknOg',
7267
7307
  'login.error.connectService': 'Failed to connect to login service',
7308
+ 'auth.status.authenticated': 'Authenticated as {account}.',
7309
+ 'auth.status.notAuthenticated': 'No reusable local session. Run `ttmg login` first.',
7310
+ 'auth.error.invalidFormat': 'Unsupported auth status output format: {format}. Use text or json.',
7268
7311
  'login.error.networkBlocked': 'Network connection failed. Please refer to the proxy setup doc:\nhttps://bytedance.larkoffice.com/wiki/PeIawT4M4ia9R8kYyLMcjwHknOg',
7269
7312
  'logout.spinner.loggingOut': 'Logging out...',
7270
7313
  'logout.success': 'Logged out successfully!',
@@ -7292,6 +7335,21 @@ const messages = {
7292
7335
  'native.server.failedAfterRetries': 'Failed to start server after trying {retries} ports.',
7293
7336
  'native.check.notWorkspace': 'Current directory is not a Mini Game project entry directory, please enter the game project root directory for debugging',
7294
7337
  'native.check.subpackagesCaseError': "Error: 'subPackages' (camelCase) is found in {file}. Please use 'subpackages' (all lowercase) instead.",
7338
+ 'check.summary': 'Check {status}: {passed} passed, {warnings} warnings, {errors} errors',
7339
+ 'check.error.invalidFormat': 'Unsupported check output format: {format}. Use text or json.',
7340
+ 'check.error.invalidFailOn': 'Unsupported check failure threshold: {failOn}. Use error or warning.',
7341
+ 'check.error.dirNotExist': 'The specified project directory does not exist: {dir}',
7342
+ 'check.ai.gameReady.found': 'Detected reportScene({ sceneId: 9999 }) as a static GAME_READY checkpoint.',
7343
+ 'check.ai.gameReady.missing': 'No statically detectable reportScene({ sceneId: 9999 }) GAME_READY checkpoint was found.',
7344
+ 'check.ai.gameReady.help': 'Call reportScene({ sceneId: 9999 }) when the game becomes interactive, then verify it on a real device. Static detection does not prove runtime reachability.',
7345
+ 'build.error.invalidFormat': 'Unsupported build output format: {format}. Use text or json.',
7346
+ 'build.error.dirNotExist': 'The specified build directory does not exist: {dir}',
7347
+ 'build.error.failed': 'Mini Game build failed.',
7348
+ 'dev.session.error.invalidId': 'Invalid DevTool session ID: {sessionId}',
7349
+ 'dev.session.error.notFound': 'DevTool session not found: {sessionId}',
7350
+ 'dev.session.error.manifestMismatch': 'The session manifest ID does not match the requested session.',
7351
+ 'dev.session.error.unverifiedProcess': 'Refusing to stop an unverified process. The recorded PID is not a ttmg dev process.',
7352
+ 'dev.session.error.invalidCursor': 'Invalid event cursor. after must be >= 0 and limit must be between 1 and 1000.',
7295
7353
  'native.compile.watching': '🔔 Watching game assets for local debugging...',
7296
7354
  'native.compile.compiling': '🚀 Compiling game assets for local debugging...',
7297
7355
  'native.compile.success': '✔ Game resources compiled successfully!',
@@ -7342,6 +7400,7 @@ const messages = {
7342
7400
  'upload.error.assetFailed': 'The uploaded asset failed platform processing (status: {status}).',
7343
7401
  'upload.error.assetTimeout': 'Timed out waiting for platform asset processing.',
7344
7402
  'upload.error.previewTimeout': 'Timed out waiting for the preview URL.',
7403
+ 'upload.error.previewQueryFailed': 'Failed to query the preview URL from the platform.',
7345
7404
  'testAccount.success': 'Test-account authorization entry generated.',
7346
7405
  'testAccount.project': 'Project: {name} ({clientKey})',
7347
7406
  'testAccount.authorizationUrl': 'Authorization URL: {url}',
@@ -7353,6 +7412,7 @@ const messages = {
7353
7412
  'testAccount.error.notLoggedIn': 'Not logged in. Please run `ttmg login` first.',
7354
7413
  'testAccount.error.gameInfoFailed': 'Failed to fetch Mini Game project information.',
7355
7414
  'testAccount.error.noAppId': 'The platform response did not include a Mini Game ID.',
7415
+ 'testAccount.error.qrWriteFailed': 'Failed to write the QR code file.',
7356
7416
  },
7357
7417
  'zh-CN': {
7358
7418
  'cli.description': 'TikTok 小游戏命令行工具',
@@ -7361,14 +7421,51 @@ const messages = {
7361
7421
  'cli.option.dev.client': '客户端调试 TikTok 小游戏',
7362
7422
  'cli.option.dev.h5': 'Web 调试 TikTok 小游戏',
7363
7423
  'cli.command.login.desc': '使用开发者账号登录',
7424
+ 'cli.command.login.implicit': '非交互登录并返回一个 JSON 结果',
7425
+ 'cli.command.login.email': '开发者账号邮箱(implicit 模式)',
7426
+ 'cli.command.login.password': '开发者账号密码(implicit 模式)',
7364
7427
  'cli.command.logout.desc': '退出登录并清除本地用户信息',
7428
+ 'cli.command.auth.desc': '查看本地登录状态',
7429
+ 'cli.command.auth.status.desc': '返回本地开发者会话是否可复用',
7430
+ 'cli.command.auth.status.format': '输出格式:text | json',
7431
+ 'cli.command.auth.status.requireAuth': '没有可复用本地会话时以非零状态退出',
7432
+ 'cli.command.capabilities.desc': '返回开发者和 Agent 可使用的 CLI 能力',
7433
+ 'cli.command.capabilities.format': '输出格式:text | json',
7365
7434
  'cli.command.setup.desc': '初始化 ttmg 环境',
7366
7435
  'cli.command.setup.lang': '语言(仅支持):en-US | zh-CN',
7367
7436
  'cli.command.reset.desc': '重置本地 CLI 状态',
7368
7437
  'cli.command.init.desc': '初始化项目',
7369
7438
  'cli.command.dev.desc': '打开浏览器调试环境',
7370
7439
  'cli.command.dev.clientKey': '小游戏的 Client Key',
7440
+ 'cli.command.dev.format': '会话输出格式:text | json',
7441
+ 'cli.command.dev.events': '生命周期事件流:none | ndjson',
7442
+ 'cli.command.dev.noOpen': '不自动打开 DevTool 页面',
7443
+ 'cli.command.dev.evidenceDir': '会话证据输出目录',
7444
+ 'cli.command.dev.timeout': '在指定秒数后停止结构化调试会话',
7445
+ 'cli.command.dev.outputConflict': 'Dev 输出模式不能同时使用:请用 --format json 输出一份 manifest,或用 --events ndjson 输出生命周期事件流。',
7446
+ 'cli.command.dev.session.desc': '查询和停止本地 DevTool 会话',
7447
+ 'cli.command.dev.session.inspect': '查询一个本地 DevTool 会话',
7448
+ 'cli.command.dev.session.list': '列出本地 DevTool 会话',
7449
+ 'cli.command.dev.session.events': '增量读取 DevTool 会话事件',
7450
+ 'cli.command.dev.session.stop': '安全停止一个本地 DevTool 会话',
7451
+ 'cli.command.dev.session.dir': '会话根目录(默认:.ttmg/sessions)',
7452
+ 'cli.command.dev.session.format': '输出格式:text | json',
7453
+ 'cli.command.dev.session.after': '只返回 seq 大于该值的事件',
7454
+ 'cli.command.dev.session.limit': '最多返回的事件数(1-1000)',
7455
+ 'cli.command.check.desc': '检查小游戏项目并返回可执行的诊断结果',
7456
+ 'cli.command.check.dir': '小游戏项目目录(默认为当前目录)',
7457
+ 'cli.command.check.format': '输出格式:text | json',
7458
+ 'cli.command.check.includePassed': '在结构化诊断中包含已通过的检查项',
7459
+ 'cli.command.check.failOn': '非零退出阈值:error | warning',
7460
+ 'cli.command.check.ai': '增加 Agent 调试准备度静态检查',
7461
+ 'cli.command.doctor.desc': '诊断本地 CLI、工程、登录和网络准备状态',
7462
+ 'cli.command.doctor.dir': '小游戏项目目录(默认为当前目录)',
7463
+ 'cli.command.doctor.format': '输出格式:text | json',
7464
+ 'cli.command.doctor.failOn': '非零退出阈值:error | warning',
7371
7465
  'cli.command.build.desc': '打包项目',
7466
+ 'cli.command.build.dir': '小游戏项目目录(仅结构化构建使用)',
7467
+ 'cli.command.build.format': '输出格式:text | json',
7468
+ 'cli.command.build.includePassed': '在结构化构建结果中包含已通过的检查项',
7372
7469
  'cli.command.config.desc': '管理配置',
7373
7470
  'cli.command.config.set.desc': '设置配置项',
7374
7471
  'cli.command.config.get.desc': '获取配置项',
@@ -7378,7 +7475,7 @@ const messages = {
7378
7475
  'cli.command.upload.note': '上传备注',
7379
7476
  'cli.command.upload.dir': '小游戏项目根目录(默认为当前目录)',
7380
7477
  'cli.command.upload.preview': '等待平台处理并输出预览二维码原始内容',
7381
- 'cli.command.upload.format': '输出格式:text | ndjson',
7478
+ 'cli.command.upload.format': '输出格式:text | json | ndjson',
7382
7479
  'cli.command.testAccount.desc': '管理小游戏测试账号访问',
7383
7480
  'cli.command.testAccount.add.desc': '生成添加 TikTok 测试账号的授权入口',
7384
7481
  'cli.command.testAccount.clientKey': '小游戏的 Client Key',
@@ -7425,8 +7522,13 @@ const messages = {
7425
7522
  'login.error.withCode': '登录失败:{message},错误码:{code}',
7426
7523
  'login.error.withMessage': '登录失败:{message}',
7427
7524
  'login.error.noUserId': '登录失败,响应中未返回 user_id。',
7525
+ 'login.error.noSessionCookie': '登录失败,响应中没有可复用的会话 Cookie。',
7526
+ 'login.error.implicitCredentialsRequired': '隐式登录需要提供 --email 和 --password,或设置 TTMG_LOGIN_EMAIL 和 TTMG_LOGIN_PASSWORD。',
7428
7527
  'login.warning.proxyIssue': '登录失败,请参考代理设置文档:\nhttps://bytedance.larkoffice.com/wiki/PeIawT4M4ia9R8kYyLMcjwHknOg',
7429
7528
  'login.error.connectService': '连接登录服务失败',
7529
+ 'auth.status.authenticated': '当前登录账号:{account}。',
7530
+ 'auth.status.notAuthenticated': '没有可复用本地会话,请先执行 `ttmg login`。',
7531
+ 'auth.error.invalidFormat': '不支持的登录状态输出格式:{format},请使用 text 或 json。',
7430
7532
  'login.error.networkBlocked': '网络连接失败,请参考代理设置文档:\nhttps://bytedance.larkoffice.com/wiki/PeIawT4M4ia9R8kYyLMcjwHknOg',
7431
7533
  'logout.spinner.loggingOut': '正在退出登录...',
7432
7534
  'logout.success': '退出登录成功!',
@@ -7454,6 +7556,21 @@ const messages = {
7454
7556
  'native.server.failedAfterRetries': '连续尝试 {retries} 个端口后,服务启动失败。',
7455
7557
  'native.check.notWorkspace': '当前目录不是小游戏工程入口目录,请进入游戏项目根目录后再进行调试',
7456
7558
  'native.check.subpackagesCaseError': "错误:在 {file} 中发现 'subPackages'(驼峰),请改为全小写 'subpackages'。",
7559
+ 'check.summary': '检查状态 {status}:{passed} 项通过,{warnings} 项警告,{errors} 项错误',
7560
+ 'check.error.invalidFormat': '不支持的检查输出格式:{format},请使用 text 或 json。',
7561
+ 'check.error.invalidFailOn': '不支持的失败阈值:{failOn},请使用 error 或 warning。',
7562
+ 'check.error.dirNotExist': '指定的项目目录不存在:{dir}',
7563
+ 'check.ai.gameReady.found': '已静态检出 reportScene({ sceneId: 9999 }) GAME_READY 检查点。',
7564
+ 'check.ai.gameReady.missing': '未静态检出 reportScene({ sceneId: 9999 }) GAME_READY 检查点。',
7565
+ 'check.ai.gameReady.help': '请在游戏真正可交互时调用 reportScene({ sceneId: 9999 }),并继续完成真机验证;静态检出不代表运行时一定能够到达。',
7566
+ 'build.error.invalidFormat': '不支持的构建输出格式:{format},请使用 text 或 json。',
7567
+ 'build.error.dirNotExist': '指定的构建目录不存在:{dir}',
7568
+ 'build.error.failed': '小游戏构建失败。',
7569
+ 'dev.session.error.invalidId': '无效的 DevTool 会话 ID:{sessionId}',
7570
+ 'dev.session.error.notFound': '未找到 DevTool 会话:{sessionId}',
7571
+ 'dev.session.error.manifestMismatch': '会话 manifest ID 与请求的会话不一致。',
7572
+ 'dev.session.error.unverifiedProcess': '拒绝停止未验证进程:记录的 PID 不是 ttmg dev 进程。',
7573
+ 'dev.session.error.invalidCursor': '无效的事件游标:after 必须大于等于 0,limit 必须在 1 到 1000 之间。',
7457
7574
  'native.compile.watching': '🔔 正在监听游戏资源变更(本地调试)...',
7458
7575
  'native.compile.compiling': '🚀 正在编译游戏资源(本地调试)...',
7459
7576
  'native.compile.success': '✔ 游戏资源编译成功!',
@@ -7504,6 +7621,7 @@ const messages = {
7504
7621
  'upload.error.assetFailed': '上传版本处理失败(状态:{status})。',
7505
7622
  'upload.error.assetTimeout': '等待平台处理上传版本超时。',
7506
7623
  'upload.error.previewTimeout': '等待平台生成预览地址超时。',
7624
+ 'upload.error.previewQueryFailed': '查询平台预览地址失败。',
7507
7625
  'testAccount.success': '测试账号授权入口已生成。',
7508
7626
  'testAccount.project': '项目:{name}({clientKey})',
7509
7627
  'testAccount.authorizationUrl': '授权地址:{url}',
@@ -7515,6 +7633,7 @@ const messages = {
7515
7633
  'testAccount.error.notLoggedIn': '未登录,请先执行 `ttmg login`。',
7516
7634
  'testAccount.error.gameInfoFailed': '读取小游戏项目信息失败。',
7517
7635
  'testAccount.error.noAppId': '平台响应中没有小游戏 ID。',
7636
+ 'testAccount.error.qrWriteFailed': '写入二维码文件失败。',
7518
7637
  },
7519
7638
  };
7520
7639
 
@@ -7659,155 +7778,226 @@ function ensureDirSync(dirPath) {
7659
7778
  }
7660
7779
  }
7661
7780
 
7662
- function printMessage(type, message) {
7663
- const prefix = type === 'Error'
7664
- ? chalk.red(t('login.error.prefix'))
7665
- : chalk.yellow(t('login.warning.prefix'));
7666
- const log = type === 'Error' ? console.error : console.warn;
7667
- log(prefix + message);
7668
- }
7669
- let spinner$1;
7781
+ const defaultDependencies$7 = {
7782
+ post: axios.post,
7783
+ getProxyConfig: getAxiosProxyConfig,
7784
+ setTTMGRC,
7785
+ now: () => Date.now(),
7786
+ operationId: () => `login_${crypto.randomUUID()}`,
7787
+ };
7670
7788
  const LOGIN_TT4D = 'https://developers.tiktok.com/passport/web/email/login';
7671
7789
  const params = {
7672
7790
  aid: '2471',
7673
7791
  account_sdk_source: 'web',
7674
7792
  sdk_version: '2.1.6-tiktok',
7675
7793
  };
7676
- const prompt = inquirer.createPromptModule();
7677
- async function login(options) {
7678
- const verbose = options?.verbose === true;
7679
- const log = (msg, data) => {
7680
- if (!verbose)
7681
- return;
7682
- console.log(chalk.gray('[ttmg login]'), msg, data !== undefined ? data : '');
7794
+ class LoginCommandError extends Error {
7795
+ constructor(code, message) {
7796
+ super(message);
7797
+ this.code = code;
7798
+ this.name = 'LoginCommandError';
7799
+ }
7800
+ }
7801
+ function maskEmail(email) {
7802
+ return email.replace(/(.{2}).*(@.*)/, '$1***$2');
7803
+ }
7804
+ function createFailedResult({ operationId, email, errorCode, error, now, }) {
7805
+ return {
7806
+ schemaVersion: '1',
7807
+ operationId,
7808
+ ts: now(),
7809
+ operation: 'login',
7810
+ status: 'failed',
7811
+ authMethod: 'email_password',
7812
+ email,
7813
+ userId: '',
7814
+ credentialStore: 'ttmgrc',
7815
+ cookieStored: false,
7816
+ errorCode,
7817
+ error,
7683
7818
  };
7684
- if (verbose)
7685
- log(t('login.verbose.enabled'));
7686
- console.log(chalk.yellowBright(t('login.note')));
7687
- const { email, password } = await prompt([
7688
- {
7689
- type: 'input',
7690
- name: 'email',
7691
- message: t('login.email.prompt'),
7692
- validate: input => {
7693
- if (!input) {
7694
- return t('login.email.required');
7695
- }
7696
- else {
7697
- if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/.test(input)) {
7698
- return t('login.email.invalid');
7699
- }
7700
- }
7701
- return true;
7702
- },
7703
- },
7704
- {
7705
- type: 'password',
7706
- name: 'password',
7707
- message: t('login.password.prompt'),
7708
- mask: '*',
7709
- validate: input => {
7710
- if (!input) {
7711
- return t('login.password.required');
7712
- }
7713
- return true;
7714
- },
7715
- },
7716
- ]);
7819
+ }
7820
+ async function authenticateWithPassword(credentials, dependencies = {}) {
7821
+ const deps = { ...defaultDependencies$7, ...dependencies };
7822
+ const operationId = deps.operationId();
7717
7823
  const url = LOGIN_TT4D + '?' + new URLSearchParams(params);
7718
- log('Request URL', url);
7719
- log('Request params', { ...params, email: email.replace(/(.{2}).*(@.*)/, '$1***$2') });
7720
7824
  const headers = {
7721
7825
  'Content-Type': 'application/x-www-form-urlencoded',
7722
7826
  Accept: '*/*',
7723
7827
  'Accept-Encoding': 'gzip, deflate, br',
7724
7828
  Origin: 'https://developers.tiktok.com',
7725
- Referer: 'https://developers.tiktok.com/passport/web/email/login',
7829
+ Referer: LOGIN_TT4D,
7726
7830
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36',
7727
7831
  };
7728
- const ora = await import('ora');
7729
- spinner$1 = ora.default({
7730
- text: chalk.bold.cyan(t('login.spinner.loggingIn')),
7731
- spinner: 'dots',
7732
- });
7733
- spinner$1.start();
7734
- const data = qs.stringify({
7735
- email,
7736
- password,
7737
- mix_mode: '1',
7738
- fixed_mix_mode: '1',
7739
- });
7832
+ let response;
7740
7833
  try {
7741
- log('Sending POST request...');
7742
- const response = await axios.post(url, data, {
7834
+ response = await deps.post(url, qs.stringify({
7835
+ email: credentials.email,
7836
+ password: credentials.password,
7837
+ mix_mode: '1',
7838
+ fixed_mix_mode: '1',
7839
+ }), {
7743
7840
  headers,
7744
7841
  maxRedirects: 20,
7745
7842
  timeout: 30000,
7746
- ...getAxiosProxyConfig(),
7843
+ ...deps.getProxyConfig(),
7747
7844
  });
7748
- log('Response status', response.status);
7749
- log('Response data', response?.data);
7750
- if (!response?.data?.data?.user_id) {
7751
- const errCode = response.data?.data?.error_code;
7752
- const errMsg = response.data?.data?.description;
7753
- const statusText = response?.statusText ?? '';
7754
- spinner$1.fail(chalk.red(t('login.failed')));
7755
- if (verbose) {
7756
- console.log(chalk.gray('Response status:'), response?.status);
7757
- console.log(chalk.gray('Response statusText:'), statusText || '(empty)');
7758
- console.log(chalk.gray('Response data:'), response?.data ?? '(empty)');
7759
- console.log('');
7760
- }
7761
- if (errCode || errMsg) {
7762
- log('Login failed (api)', { errCode, errMsg, fullData: response?.data });
7763
- printMessage('Error', errCode
7764
- ? t('login.error.withCode', {
7765
- message: String(errMsg ?? ''),
7766
- code: String(errCode),
7767
- })
7768
- : errMsg
7769
- ? t('login.error.withMessage', { message: String(errMsg) })
7770
- : t('login.error.noUserId'));
7771
- }
7772
- else {
7773
- log('Login failed (no user_id)', { responseBody: response?.data });
7774
- if (verbose)
7775
- log('Full response (for debugging)', response);
7776
- const looksLikeProxyResponse = statusText === 'Connection established' ||
7777
- (response?.status === 200 &&
7778
- (response?.data == null ||
7779
- typeof response.data !== 'object' ||
7780
- !('data' in response.data)));
7781
- if (looksLikeProxyResponse) {
7782
- printMessage('Warning', t('login.warning.proxyIssue'));
7783
- }
7784
- else {
7785
- printMessage('Error', t('login.error.noUserId'));
7786
- }
7845
+ }
7846
+ catch {
7847
+ throw new LoginCommandError('NETWORK_ERROR', t('login.error.networkBlocked'));
7848
+ }
7849
+ const userId = response?.data?.data?.user_id;
7850
+ if (!userId) {
7851
+ const errCode = response?.data?.data?.error_code;
7852
+ const errMsg = response?.data?.data?.description;
7853
+ const statusText = response?.statusText ?? '';
7854
+ const looksLikeProxyResponse = statusText === 'Connection established' ||
7855
+ (response?.status === 200 &&
7856
+ (response?.data == null ||
7857
+ typeof response.data !== 'object' ||
7858
+ !('data' in response.data)));
7859
+ if (looksLikeProxyResponse) {
7860
+ throw new LoginCommandError('PROXY_RESPONSE', t('login.warning.proxyIssue'));
7861
+ }
7862
+ if (errCode) {
7863
+ throw new LoginCommandError(`PLATFORM_${String(errCode)}`, t('login.error.withCode', {
7864
+ message: String(errMsg ?? ''),
7865
+ code: String(errCode),
7866
+ }));
7867
+ }
7868
+ if (errMsg) {
7869
+ throw new LoginCommandError('PLATFORM_REJECTED', t('login.error.withMessage', { message: String(errMsg) }));
7870
+ }
7871
+ throw new LoginCommandError('INVALID_RESPONSE', t('login.error.noUserId'));
7872
+ }
7873
+ const setCookies = response?.headers['set-cookie'];
7874
+ const cookie = buildCookieHeaderFromSetCookies(setCookies);
7875
+ if (!cookie) {
7876
+ throw new LoginCommandError('COOKIE_MISSING', t('login.error.noSessionCookie'));
7877
+ }
7878
+ deps.setTTMGRC({
7879
+ email: credentials.email,
7880
+ user_id: String(userId),
7881
+ cookie,
7882
+ });
7883
+ return {
7884
+ schemaVersion: '1',
7885
+ operationId,
7886
+ ts: deps.now(),
7887
+ operation: 'login',
7888
+ status: 'authenticated',
7889
+ authMethod: 'email_password',
7890
+ email: credentials.email,
7891
+ userId: String(userId),
7892
+ credentialStore: 'ttmgrc',
7893
+ cookieStored: true,
7894
+ errorCode: null,
7895
+ error: null,
7896
+ };
7897
+ }
7898
+ async function login(options = {}, dependencies = {}) {
7899
+ const deps = { ...defaultDependencies$7, ...dependencies };
7900
+ const format = options.implicit ? 'json' : 'text';
7901
+ let email = '';
7902
+ const operationId = deps.operationId();
7903
+ const verbose = options.verbose === true;
7904
+ const debug = (message, data) => {
7905
+ if (!verbose)
7906
+ return;
7907
+ const args = [chalk.gray('[ttmg login]'), message];
7908
+ if (data !== undefined)
7909
+ args.push(data);
7910
+ if (format === 'json')
7911
+ console.error(...args);
7912
+ else
7913
+ console.log(...args);
7914
+ };
7915
+ try {
7916
+ if (verbose)
7917
+ debug(t('login.verbose.enabled'));
7918
+ if (format === 'text')
7919
+ console.log(chalk.yellowBright(t('login.note')));
7920
+ let password = '';
7921
+ if (options.implicit) {
7922
+ email = (options.email || process.env.TTMG_LOGIN_EMAIL || '').trim();
7923
+ password = options.password || process.env.TTMG_LOGIN_PASSWORD || '';
7924
+ if (!email || !password) {
7925
+ throw new LoginCommandError('CREDENTIALS_REQUIRED', t('login.error.implicitCredentialsRequired'));
7787
7926
  }
7788
- spinner$1.stop();
7789
- process.exit(1);
7790
7927
  }
7791
7928
  else {
7792
- const setCookies = response?.headers['set-cookie'];
7793
- const cookie = buildCookieHeaderFromSetCookies(setCookies);
7794
- log('Parsed cookies', { rawCount: setCookies?.length ?? 0, kept: cookie ? cookie.split('; ').length : 0 });
7795
- const loginData = {
7796
- email,
7797
- user_id: response?.data?.data?.user_id,
7798
- cookie,
7799
- };
7800
- log('Login success', { user_id: loginData.user_id, email: loginData.email });
7801
- setTTMGRC(loginData);
7802
- spinner$1.succeed(chalk.bold.green(t('login.success')));
7803
- process.exit(0);
7929
+ const prompt = inquirer.createPromptModule();
7930
+ const answers = await prompt([
7931
+ {
7932
+ type: 'input',
7933
+ name: 'email',
7934
+ message: t('login.email.prompt'),
7935
+ validate: input => {
7936
+ if (!input)
7937
+ return t('login.email.required');
7938
+ if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,}$/.test(input)) {
7939
+ return t('login.email.invalid');
7940
+ }
7941
+ return true;
7942
+ },
7943
+ },
7944
+ {
7945
+ type: 'password',
7946
+ name: 'password',
7947
+ message: t('login.password.prompt'),
7948
+ mask: '*',
7949
+ validate: input => (input ? true : t('login.password.required')),
7950
+ },
7951
+ ]);
7952
+ email = answers.email;
7953
+ password = answers.password;
7954
+ }
7955
+ debug('Request', { email: maskEmail(email) });
7956
+ const ora = await import('ora');
7957
+ const spinner = format === 'text'
7958
+ ? ora
7959
+ .default({
7960
+ text: chalk.bold.cyan(t('login.spinner.loggingIn')),
7961
+ spinner: 'dots',
7962
+ })
7963
+ .start()
7964
+ : null;
7965
+ try {
7966
+ const result = await authenticateWithPassword({ email, password }, { ...dependencies, operationId: () => operationId });
7967
+ if (format === 'json')
7968
+ console.log(JSON.stringify(result));
7969
+ else
7970
+ spinner?.succeed(chalk.bold.green(t('login.success')));
7971
+ return result;
7972
+ }
7973
+ catch (error) {
7974
+ spinner?.fail(chalk.red(t('login.failed')));
7975
+ throw error;
7804
7976
  }
7805
7977
  }
7806
7978
  catch (error) {
7807
- log('Request error', error instanceof Error ? { message: error.message, name: error.name, stack: error.stack } : error);
7808
- spinner$1.fail(chalk.red.bold(t('login.error.connectService')));
7809
- printMessage('Error', t('login.error.networkBlocked'));
7810
- process.exit(1);
7979
+ const commandError = error instanceof LoginCommandError
7980
+ ? error
7981
+ : new LoginCommandError('UNKNOWN_ERROR', error?.message || String(error));
7982
+ const failedResult = createFailedResult({
7983
+ operationId,
7984
+ email,
7985
+ errorCode: commandError.code,
7986
+ error: commandError.message,
7987
+ now: deps.now,
7988
+ });
7989
+ if (format === 'json') {
7990
+ console.log(JSON.stringify(failedResult));
7991
+ }
7992
+ else {
7993
+ const print = commandError.code === 'PROXY_RESPONSE' ? console.warn : console.error;
7994
+ const prefix = commandError.code === 'PROXY_RESPONSE'
7995
+ ? chalk.yellow(t('login.warning.prefix'))
7996
+ : chalk.red(t('login.error.prefix'));
7997
+ print(prefix + commandError.message);
7998
+ }
7999
+ process.exitCode = 1;
8000
+ return failedResult;
7811
8001
  }
7812
8002
  }
7813
8003
 
@@ -8038,6 +8228,9 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
8038
8228
  const defaultIgnores = [
8039
8229
  'node_modules/**',
8040
8230
  '.git/**',
8231
+ '.agent/**',
8232
+ '.claude/**',
8233
+ '.trae/**',
8041
8234
  '.DS_Store',
8042
8235
  ttmgPack.TTMG_TEMP_DIR,
8043
8236
  `${ttmgPack.TTMG_TEMP_DIR}/**`,
@@ -8059,7 +8252,7 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
8059
8252
 
8060
8253
  const DEFAULT_POLL_INTERVAL_MS = 3000;
8061
8254
  const DEFAULT_POLL_TIMEOUT_MS = 10 * 60 * 1000;
8062
- const defaultDependencies$1 = {
8255
+ const defaultDependencies$6 = {
8063
8256
  getCurrentUser,
8064
8257
  fetchGameInfo,
8065
8258
  zipDirectory: targetDir => zipCwdToBuffer([], targetDir),
@@ -8080,26 +8273,55 @@ function normalizeOutputFormat(format) {
8080
8273
  }
8081
8274
  return normalized;
8082
8275
  }
8083
- function getAssetStatusValue$1(asset) {
8084
- if (!asset) {
8085
- return '';
8276
+ function getAssetStatusRaw(asset) {
8277
+ return asset?.cdn_deploy_status ?? asset?.status ?? null;
8278
+ }
8279
+ function normalizeUploadPlatformStatus(rawStatus) {
8280
+ if (rawStatus === null || rawStatus === undefined || rawStatus === '') {
8281
+ return 'waiting_for_asset';
8282
+ }
8283
+ const numericStatus = Number(rawStatus);
8284
+ if (Number.isInteger(numericStatus)) {
8285
+ if (numericStatus === 0)
8286
+ return 'unknown';
8287
+ if (numericStatus === 1)
8288
+ return 'pending';
8289
+ if (numericStatus === 2)
8290
+ return 'success';
8291
+ if (numericStatus === 3)
8292
+ return 'failed';
8293
+ if (numericStatus === 4)
8294
+ return 'processing';
8295
+ return 'unknown';
8086
8296
  }
8087
- return String(asset.status ?? asset.cdn_deploy_status ?? asset.live_status ?? '').toLowerCase();
8088
- }
8089
- function getAssetState(asset) {
8090
- if (!asset) {
8091
- return 'pending';
8297
+ const normalized = String(rawStatus).trim().toLowerCase();
8298
+ const statusName = normalized.replace(/^deploy_status_/, '');
8299
+ if (['ready', 'success', 'succeeded', 'available'].includes(statusName)) {
8300
+ return 'success';
8092
8301
  }
8093
- if (Number(asset.cdn_deploy_status) === 2) {
8094
- return 'ready';
8302
+ if (statusName.includes('fail') ||
8303
+ statusName.includes('error') ||
8304
+ statusName.includes('reject')) {
8305
+ return 'failed';
8306
+ }
8307
+ if (statusName.includes('process') ||
8308
+ statusName.includes('upload') ||
8309
+ statusName.includes('deploy')) {
8310
+ return 'processing';
8311
+ }
8312
+ if (statusName.includes('pending') ||
8313
+ statusName.includes('queue') ||
8314
+ statusName.includes('wait')) {
8315
+ return 'pending';
8095
8316
  }
8096
- const rawStatus = getAssetStatusValue$1(asset);
8097
- if (['ready', 'success', 'succeeded', 'available'].includes(rawStatus)) {
8317
+ return 'unknown';
8318
+ }
8319
+ function getAssetState(asset) {
8320
+ const platformStatus = normalizeUploadPlatformStatus(getAssetStatusRaw(asset));
8321
+ if (platformStatus === 'success') {
8098
8322
  return 'ready';
8099
8323
  }
8100
- if (rawStatus.includes('fail') ||
8101
- rawStatus.includes('error') ||
8102
- rawStatus.includes('reject')) {
8324
+ if (platformStatus === 'failed') {
8103
8325
  return 'failed';
8104
8326
  }
8105
8327
  return 'pending';
@@ -8131,11 +8353,14 @@ function createEvent(base, status, progress, overrides = {}) {
8131
8353
  qrPayload: '',
8132
8354
  platformUrl: '',
8133
8355
  error: null,
8356
+ errorCode: null,
8357
+ platformWrite: false,
8134
8358
  ...overrides,
8135
8359
  };
8136
8360
  }
8137
8361
  async function waitForAssetReady({ assetId, miniGameId, deadline, eventBase, onEvent, deps, }) {
8138
- let lastPlatformStatus = '';
8362
+ let lastPlatformStatusRaw = null;
8363
+ let hasEmittedPlatformStatus = false;
8139
8364
  while (deps.now() < deadline) {
8140
8365
  const response = await deps.fetchGameAssets({ miniGameId });
8141
8366
  if (response.error) {
@@ -8144,12 +8369,17 @@ async function waitForAssetReady({ assetId, miniGameId, deadline, eventBase, onE
8144
8369
  t('upload.error.assetQueryFailed'));
8145
8370
  }
8146
8371
  const asset = extractAssets(response.data).find(item => String(item.asset_id || '') === assetId) || null;
8147
- const platformStatus = getAssetStatusValue$1(asset) || 'pending';
8148
- if (platformStatus !== lastPlatformStatus) {
8149
- lastPlatformStatus = platformStatus;
8372
+ const platformStatusRaw = getAssetStatusRaw(asset);
8373
+ const platformStatus = normalizeUploadPlatformStatus(platformStatusRaw);
8374
+ if (!hasEmittedPlatformStatus ||
8375
+ platformStatusRaw !== lastPlatformStatusRaw) {
8376
+ hasEmittedPlatformStatus = true;
8377
+ lastPlatformStatusRaw = platformStatusRaw;
8150
8378
  onEvent(createEvent(eventBase, 'processing', 0.7, {
8151
8379
  versionId: assetId,
8152
8380
  platformStatus,
8381
+ ...(platformStatusRaw === null ? {} : { platformStatusRaw }),
8382
+ platformWrite: true,
8153
8383
  }));
8154
8384
  }
8155
8385
  const assetState = getAssetState(asset);
@@ -8166,6 +8396,11 @@ async function waitForAssetReady({ assetId, miniGameId, deadline, eventBase, onE
8166
8396
  async function waitForPreviewUrl({ assetId, deadline, deps, }) {
8167
8397
  while (deps.now() < deadline) {
8168
8398
  const response = await deps.fetchGameAssetPreviewUrl({ assetId });
8399
+ if (response.error) {
8400
+ throw new Error(response.error?.StatusMessage ||
8401
+ response.error?.message ||
8402
+ t('upload.error.previewQueryFailed'));
8403
+ }
8169
8404
  const previewUrl = extractPreviewUrl(response.data);
8170
8405
  if (previewUrl) {
8171
8406
  return previewUrl;
@@ -8175,7 +8410,7 @@ async function waitForPreviewUrl({ assetId, deadline, deps, }) {
8175
8410
  throw new Error(t('upload.error.previewTimeout'));
8176
8411
  }
8177
8412
  async function executeUpload(options, dependencies = {}) {
8178
- const deps = { ...defaultDependencies$1, ...dependencies };
8413
+ const deps = { ...defaultDependencies$6, ...dependencies };
8179
8414
  const clientKey = options.clientKey?.trim();
8180
8415
  const note = options.note || '--';
8181
8416
  const eventBase = { operationId: options.operationId, clientKey };
@@ -8233,6 +8468,7 @@ async function executeUpload(options, dependencies = {}) {
8233
8468
  packageSizeBytes,
8234
8469
  versionId: assetId,
8235
8470
  platformUrl,
8471
+ platformWrite: true,
8236
8472
  });
8237
8473
  emit(event);
8238
8474
  return event;
@@ -8253,6 +8489,7 @@ async function executeUpload(options, dependencies = {}) {
8253
8489
  versionId: assetId,
8254
8490
  platformStatus: 'resolving_preview',
8255
8491
  platformUrl,
8492
+ platformWrite: true,
8256
8493
  }));
8257
8494
  const previewUrl = await waitForPreviewUrl({ assetId, deadline, deps });
8258
8495
  const event = createEvent(eventBase, 'ready', 1, {
@@ -8264,6 +8501,7 @@ async function executeUpload(options, dependencies = {}) {
8264
8501
  qrPayload: previewUrl,
8265
8502
  platformUrl,
8266
8503
  platformStatus: 'ready',
8504
+ platformWrite: true,
8267
8505
  });
8268
8506
  emit(event);
8269
8507
  return event;
@@ -8326,8 +8564,15 @@ async function upload(options) {
8326
8564
  catch (error) {
8327
8565
  const message = error?.message || String(error);
8328
8566
  const failedEvent = createEvent({ operationId, clientKey: options.clientKey || '' }, 'failed', lastEvent?.progress || 0, {
8567
+ appId: lastEvent?.appId,
8568
+ projectName: lastEvent?.projectName,
8569
+ packageSizeBytes: lastEvent?.packageSizeBytes,
8329
8570
  versionId: lastEvent?.versionId || '',
8330
8571
  platformUrl: lastEvent?.platformUrl || '',
8572
+ platformStatus: lastEvent?.platformStatus,
8573
+ platformStatusRaw: lastEvent?.platformStatusRaw,
8574
+ platformWrite: lastEvent?.platformWrite || false,
8575
+ errorCode: 'UPLOAD_FAILED',
8331
8576
  error: message,
8332
8577
  });
8333
8578
  if (format === 'json' || format === 'ndjson') {
@@ -14649,7 +14894,7 @@ function requireLib () {
14649
14894
  var libExports = requireLib();
14650
14895
  var QRCode = /*@__PURE__*/getDefaultExportFromCjs(libExports);
14651
14896
 
14652
- const defaultDependencies = {
14897
+ const defaultDependencies$5 = {
14653
14898
  getCurrentUser,
14654
14899
  fetchGameInfo,
14655
14900
  writeQrFile: async (outputPath, payload) => {
@@ -14660,11 +14905,20 @@ const defaultDependencies = {
14660
14905
  type: 'png',
14661
14906
  });
14662
14907
  },
14908
+ now: () => Date.now(),
14909
+ operationId: () => `test_account_${crypto.randomUUID()}`,
14663
14910
  };
14664
- function normalizeFormat(format) {
14911
+ class TestAccountCommandError extends Error {
14912
+ constructor(code, message) {
14913
+ super(message);
14914
+ this.code = code;
14915
+ this.name = 'TestAccountCommandError';
14916
+ }
14917
+ }
14918
+ function normalizeFormat$3(format) {
14665
14919
  const normalized = (format || 'text').toLowerCase();
14666
14920
  if (normalized !== 'text' && normalized !== 'json') {
14667
- throw new Error(t('testAccount.error.invalidFormat', { format: normalized }));
14921
+ throw new TestAccountCommandError('INVALID_FORMAT', t('testAccount.error.invalidFormat', { format: normalized }));
14668
14922
  }
14669
14923
  return normalized;
14670
14924
  }
@@ -14672,31 +14926,39 @@ function buildTestAccountAuthorizationUrl(clientKey) {
14672
14926
  return `https://www.tiktok.com/ttmg/dev/${encodeURIComponent(clientKey)}`;
14673
14927
  }
14674
14928
  async function generateTestAccountAuthorization(options, dependencies = {}) {
14675
- const deps = { ...defaultDependencies, ...dependencies };
14929
+ const deps = { ...defaultDependencies$5, ...dependencies };
14676
14930
  const clientKey = options.clientKey?.trim();
14931
+ const operationId = deps.operationId();
14677
14932
  if (!clientKey) {
14678
- throw new Error(t('testAccount.error.noClientKey'));
14933
+ throw new TestAccountCommandError('INVALID_ARGUMENT', t('testAccount.error.noClientKey'));
14679
14934
  }
14680
14935
  const user = deps.getCurrentUser();
14681
14936
  if (!user || !user.cookie) {
14682
- throw new Error(t('testAccount.error.notLoggedIn'));
14937
+ throw new TestAccountCommandError('NOT_AUTHENTICATED', t('testAccount.error.notLoggedIn'));
14683
14938
  }
14684
14939
  const infoResponse = await deps.fetchGameInfo(clientKey);
14685
14940
  if (infoResponse.error) {
14686
- throw new Error(infoResponse.error.errorMsg || t('testAccount.error.gameInfoFailed'));
14941
+ throw new TestAccountCommandError('PROJECT_LOOKUP_FAILED', infoResponse.error.errorMsg || t('testAccount.error.gameInfoFailed'));
14687
14942
  }
14688
14943
  const appId = infoResponse.data?.app_id || '';
14689
14944
  if (!appId) {
14690
- throw new Error(t('testAccount.error.noAppId'));
14945
+ throw new TestAccountCommandError('PROJECT_RESPONSE_INVALID', t('testAccount.error.noAppId'));
14691
14946
  }
14692
14947
  const projectName = infoResponse.data?.name || 'game';
14693
14948
  const authorizationUrl = buildTestAccountAuthorizationUrl(clientKey);
14694
14949
  const safeClientKey = clientKey.replace(/[^a-zA-Z0-9_-]/g, '_');
14695
14950
  const qrFile = path.resolve(process.cwd(), options.qrOutput || path.join('.ttmg', `test-account-${safeClientKey}.png`));
14696
- fs.mkdirSync(path.dirname(qrFile), { recursive: true });
14697
- await deps.writeQrFile(qrFile, authorizationUrl);
14951
+ try {
14952
+ fs.mkdirSync(path.dirname(qrFile), { recursive: true });
14953
+ await deps.writeQrFile(qrFile, authorizationUrl);
14954
+ }
14955
+ catch (error) {
14956
+ throw new TestAccountCommandError('QR_WRITE_FAILED', error?.message || t('testAccount.error.qrWriteFailed'));
14957
+ }
14698
14958
  return {
14699
14959
  schemaVersion: '1',
14960
+ operationId,
14961
+ ts: deps.now(),
14700
14962
  operation: 'test-account.add',
14701
14963
  status: 'authorization_required',
14702
14964
  clientKey,
@@ -14708,14 +14970,20 @@ async function generateTestAccountAuthorization(options, dependencies = {}) {
14708
14970
  portalUrl: `https://developers.tiktok.com/portal/game/${appId}`,
14709
14971
  platformWrite: false,
14710
14972
  verification: 'developer_portal',
14973
+ errorCode: null,
14711
14974
  error: null,
14712
14975
  };
14713
14976
  }
14714
- async function testAccountAdd(options) {
14977
+ async function testAccountAdd(options, dependencies = {}) {
14978
+ const deps = { ...defaultDependencies$5, ...dependencies };
14715
14979
  let format = 'text';
14980
+ const operationId = deps.operationId();
14716
14981
  try {
14717
- format = normalizeFormat(options.format);
14718
- const result = await generateTestAccountAuthorization(options);
14982
+ format = normalizeFormat$3(options.format);
14983
+ const result = await generateTestAccountAuthorization(options, {
14984
+ ...dependencies,
14985
+ operationId: () => operationId,
14986
+ });
14719
14987
  if (format === 'json') {
14720
14988
  console.log(JSON.stringify(result));
14721
14989
  }
@@ -14733,8 +15001,11 @@ async function testAccountAdd(options) {
14733
15001
  }
14734
15002
  catch (error) {
14735
15003
  const message = error?.message || String(error);
15004
+ const errorCode = error instanceof TestAccountCommandError ? error.code : 'UNKNOWN_ERROR';
14736
15005
  const failedResult = {
14737
15006
  schemaVersion: '1',
15007
+ operationId,
15008
+ ts: deps.now(),
14738
15009
  operation: 'test-account.add',
14739
15010
  status: 'failed',
14740
15011
  clientKey: options.clientKey || '',
@@ -14746,6 +15017,7 @@ async function testAccountAdd(options) {
14746
15017
  portalUrl: '',
14747
15018
  platformWrite: false,
14748
15019
  verification: 'developer_portal',
15020
+ errorCode,
14749
15021
  error: message,
14750
15022
  };
14751
15023
  if (format === 'json') {
@@ -14759,6 +15031,196 @@ async function testAccountAdd(options) {
14759
15031
  }
14760
15032
  }
14761
15033
 
15034
+ const defaultDependencies$4 = {
15035
+ getCurrentUser,
15036
+ operationId: () => `auth_${crypto.randomUUID()}`,
15037
+ };
15038
+ function maskAccountValue(value) {
15039
+ if (!value)
15040
+ return '';
15041
+ const emailMatch = value.match(/^(.{1,2})(.*)(@.+)$/);
15042
+ if (emailMatch) {
15043
+ return `${emailMatch[1]}***${emailMatch[3]}`;
15044
+ }
15045
+ if (value.length <= 4)
15046
+ return `${value[0]}***`;
15047
+ return `${value.slice(0, 2)}***${value.slice(-2)}`;
15048
+ }
15049
+ function normalizeFormat$2(format) {
15050
+ const value = (format || 'text').toLowerCase();
15051
+ if (value !== 'text' && value !== 'json') {
15052
+ throw new Error(t('auth.error.invalidFormat', { format: value }));
15053
+ }
15054
+ return value;
15055
+ }
15056
+ function getAuthStatus(dependencies = {}) {
15057
+ const deps = { ...defaultDependencies$4, ...dependencies };
15058
+ const user = deps.getCurrentUser();
15059
+ const authenticated = Boolean(user?.cookie);
15060
+ return {
15061
+ schemaVersion: '1',
15062
+ operationId: deps.operationId(),
15063
+ command: 'auth.status',
15064
+ status: authenticated ? 'authenticated' : 'not_authenticated',
15065
+ authenticated,
15066
+ account: {
15067
+ email: maskAccountValue(user?.email || ''),
15068
+ userId: maskAccountValue(user?.user_id || ''),
15069
+ },
15070
+ session: {
15071
+ source: 'local_config',
15072
+ reusable: authenticated,
15073
+ remotelyVerified: false,
15074
+ },
15075
+ errorCode: null,
15076
+ error: null,
15077
+ };
15078
+ }
15079
+ async function authStatus(options = {}) {
15080
+ let format = 'text';
15081
+ let result;
15082
+ try {
15083
+ format = normalizeFormat$2(options.format);
15084
+ result = getAuthStatus();
15085
+ }
15086
+ catch (error) {
15087
+ const message = error instanceof Error ? error.message : String(error);
15088
+ result = {
15089
+ schemaVersion: '1',
15090
+ operationId: `auth_${crypto.randomUUID()}`,
15091
+ command: 'auth.status',
15092
+ status: 'failed',
15093
+ authenticated: false,
15094
+ account: { email: '', userId: '' },
15095
+ session: {
15096
+ source: 'local_config',
15097
+ reusable: false,
15098
+ remotelyVerified: false,
15099
+ },
15100
+ errorCode: 'AUTH_STATUS_FAILED',
15101
+ error: message,
15102
+ };
15103
+ }
15104
+ if (format === 'json') {
15105
+ console.log(JSON.stringify(result));
15106
+ }
15107
+ else if (result.status === 'authenticated') {
15108
+ console.log(chalk.green(t('auth.status.authenticated', {
15109
+ account: result.account.email || result.account.userId,
15110
+ })));
15111
+ }
15112
+ else if (result.status === 'not_authenticated') {
15113
+ console.log(chalk.yellow(t('auth.status.notAuthenticated')));
15114
+ }
15115
+ else {
15116
+ console.error(chalk.red(result.error || t('common.unknownError')));
15117
+ }
15118
+ if (result.status === 'failed' ||
15119
+ (options.requireAuth && !result.authenticated)) {
15120
+ process.exitCode = 1;
15121
+ }
15122
+ return result;
15123
+ }
15124
+
15125
+ var version = "0.4.6-vibe-beta.3";
15126
+ var engines = {
15127
+ node: ">=20.19.1 <21 || >=22"
15128
+ };
15129
+ var pkg = {
15130
+ version: version,
15131
+ engines: engines
15132
+ };
15133
+
15134
+ function getCapabilities() {
15135
+ return {
15136
+ schemaVersion: '1',
15137
+ command: 'capabilities',
15138
+ cliVersion: pkg.version,
15139
+ structuredOutput: {
15140
+ json: true,
15141
+ ndjson: true,
15142
+ stdoutProtocolOnly: true,
15143
+ },
15144
+ commands: {
15145
+ authStatus: {
15146
+ command: 'ttmg auth status --format json',
15147
+ available: true,
15148
+ platformWrite: false,
15149
+ },
15150
+ loginImplicit: {
15151
+ command: 'ttmg login --implicit',
15152
+ available: true,
15153
+ interactive: false,
15154
+ persistsLocalSession: true,
15155
+ platformWrite: false,
15156
+ },
15157
+ testAccountAdd: {
15158
+ command: 'ttmg test-account add --format json',
15159
+ available: true,
15160
+ platformWrite: false,
15161
+ humanAuthorizationRequired: true,
15162
+ },
15163
+ check: {
15164
+ command: 'ttmg check --format json',
15165
+ available: true,
15166
+ platformWrite: false,
15167
+ ai: {
15168
+ option: '--ai',
15169
+ command: 'ttmg check --ai --format json',
15170
+ available: true,
15171
+ platformWrite: false,
15172
+ confidence: 'static',
15173
+ },
15174
+ },
15175
+ build: {
15176
+ command: 'ttmg build --format json',
15177
+ available: true,
15178
+ platformWrite: false,
15179
+ },
15180
+ dev: {
15181
+ command: 'ttmg dev --format json --no-open',
15182
+ eventsCommand: 'ttmg dev --events ndjson --no-open',
15183
+ outputModesMutuallyExclusive: true,
15184
+ available: true,
15185
+ platformWrite: false,
15186
+ timeout: true,
15187
+ sessionList: true,
15188
+ sessionInspect: true,
15189
+ sessionStop: true,
15190
+ },
15191
+ uploadPreview: {
15192
+ command: 'ttmg upload --preview --format ndjson',
15193
+ available: true,
15194
+ platformWrite: true,
15195
+ },
15196
+ },
15197
+ dev: {
15198
+ sessionManifest: true,
15199
+ lifecycleEvents: true,
15200
+ compileDiagnostics: true,
15201
+ rawLaunchUrl: true,
15202
+ qrCode: true,
15203
+ manualScan: true,
15204
+ gameReadyProtocol: true,
15205
+ gameReady: false,
15206
+ runtimeLogs: false,
15207
+ deviceLaunch: false,
15208
+ screenshot: false,
15209
+ actions: false,
15210
+ },
15211
+ };
15212
+ }
15213
+ async function capabilities(options = {}) {
15214
+ const result = getCapabilities();
15215
+ if ((options.format || 'text').toLowerCase() === 'json') {
15216
+ console.log(JSON.stringify(result));
15217
+ }
15218
+ else {
15219
+ console.log(JSON.stringify(result, null, 2));
15220
+ }
15221
+ return result;
15222
+ }
15223
+
14762
15224
  const CONFIG_FILE_NAME = 'minigame.config.json';
14763
15225
  const SDK_URL = 'https://connect.tiktok-minis.com/game/sdk.js';
14764
15226
  const VCONSOLE_URL = 'https://connect.tiktok-minis.com/libs/vConsole.js';
@@ -15153,15 +15615,18 @@ function getDesktopPath() {
15153
15615
  return defaultPath;
15154
15616
  }
15155
15617
 
15156
- var version = "0.4.6-vibe-beta.1";
15157
- var pkg = {
15158
- version: version};
15159
-
15160
15618
  const program = new commander.Command();
15161
15619
  const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
15162
15620
  const structuredFormatIndex = process.argv.indexOf('--format');
15163
- const structuredFormat = structuredFormatIndex >= 0 ? process.argv[structuredFormatIndex + 1] : '';
15164
- if (structuredFormat !== 'json' && structuredFormat !== 'ndjson') {
15621
+ const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
15622
+ const structuredFormat = inlineStructuredFormat
15623
+ ? inlineStructuredFormat.slice('--format='.length)
15624
+ : structuredFormatIndex >= 0
15625
+ ? process.argv[structuredFormatIndex + 1]
15626
+ : '';
15627
+ if (structuredFormat !== 'json' &&
15628
+ structuredFormat !== 'ndjson' &&
15629
+ !process.argv.includes('--implicit')) {
15165
15630
  maybeShowPostInstallNotice(pkg.version);
15166
15631
  }
15167
15632
  withVerboseOption(program)
@@ -15170,12 +15635,39 @@ withVerboseOption(program)
15170
15635
  .version(pkg.version, '-v, --version', t('cli.version.desc'))
15171
15636
  .option('dev', t('cli.option.dev.client'))
15172
15637
  .option('dev --h5', t('cli.option.dev.h5'));
15173
- withVerboseOption(program.command('login').description(t('cli.command.login.desc'))).action(async (cmd) => {
15174
- await login({ verbose: cmd.verbose });
15638
+ withVerboseOption(program
15639
+ .command('login')
15640
+ .description(t('cli.command.login.desc'))
15641
+ .option('--implicit', t('cli.command.login.implicit'))
15642
+ .option('--email <email>', t('cli.command.login.email'))
15643
+ .option('--password <password>', t('cli.command.login.password'))).action(async (cmd) => {
15644
+ await login({
15645
+ verbose: cmd.verbose,
15646
+ implicit: cmd.implicit,
15647
+ email: cmd.email,
15648
+ password: cmd.password,
15649
+ });
15175
15650
  });
15176
15651
  withVerboseOption(program.command('logout').description(t('cli.command.logout.desc'))).action(async () => {
15177
15652
  await logout();
15178
15653
  });
15654
+ const authCmd = program.command('auth').description(t('cli.command.auth.desc'));
15655
+ withVerboseOption(authCmd
15656
+ .command('status')
15657
+ .description(t('cli.command.auth.status.desc'))
15658
+ .option('--format <format>', t('cli.command.auth.status.format'), 'text')
15659
+ .option('--require-auth', t('cli.command.auth.status.requireAuth'))).action(async (cmd) => {
15660
+ await authStatus({
15661
+ format: cmd.format,
15662
+ requireAuth: cmd.requireAuth,
15663
+ });
15664
+ });
15665
+ withVerboseOption(program
15666
+ .command('capabilities')
15667
+ .description(t('cli.command.capabilities.desc'))
15668
+ .option('--format <format>', t('cli.command.capabilities.format'), 'text')).action(async (cmd) => {
15669
+ await capabilities({ format: cmd.format });
15670
+ });
15179
15671
  withVerboseOption(program
15180
15672
  .command('upload')
15181
15673
  .description(t('cli.command.upload.desc'))
@@ -15247,20 +15739,132 @@ withVerboseOption(program
15247
15739
  /**
15248
15740
  * ttmg dev 命令
15249
15741
  */
15250
- withVerboseOption(program
15742
+ const devCmd = program
15251
15743
  .option('--h5', t('cli.option.h5'))
15252
15744
  .command('dev')
15253
15745
  .description(t('cli.command.dev.desc'))
15254
- .option('-ck, --client-key <clientKey>', t('cli.command.dev.clientKey'))).action(async (cmd) => {
15746
+ .option('-ck, --client-key <clientKey>', t('cli.command.dev.clientKey'))
15747
+ .option('--format <format>', t('cli.command.dev.format'), 'text')
15748
+ .option('--events <events>', t('cli.command.dev.events'), 'none')
15749
+ .option('--no-open', t('cli.command.dev.noOpen'))
15750
+ .option('--evidence-dir <path>', t('cli.command.dev.evidenceDir'))
15751
+ .option('--timeout <seconds>', t('cli.command.dev.timeout'));
15752
+ withVerboseOption(devCmd).action(async (cmd) => {
15255
15753
  const options = program.opts();
15754
+ const format = String(cmd.format || 'text').toLowerCase();
15755
+ const events = String(cmd.events || 'none').toLowerCase();
15756
+ if (format === 'json' && events === 'ndjson') {
15757
+ console.error(t('cli.command.dev.outputConflict'));
15758
+ process.exitCode = 1;
15759
+ return;
15760
+ }
15256
15761
  if (options.h5) {
15257
15762
  await dev$1();
15258
15763
  }
15259
15764
  else {
15260
15765
  const Native = await Promise.resolve().then(function () { return index; });
15261
- await Native.dev({ clientKey: cmd.clientKey });
15766
+ await Native.dev({
15767
+ clientKey: cmd.clientKey,
15768
+ format: cmd.format,
15769
+ events: cmd.events,
15770
+ noOpen: cmd.noOpen,
15771
+ evidenceDir: cmd.evidenceDir,
15772
+ timeout: cmd.timeout,
15773
+ });
15774
+ }
15775
+ if (cmd.format === 'text' && cmd.events === 'none')
15776
+ checkUpdate();
15777
+ });
15778
+ const devSessionCmd = devCmd
15779
+ .command('session')
15780
+ .description(t('cli.command.dev.session.desc'));
15781
+ devSessionCmd
15782
+ .command('inspect <sessionId>')
15783
+ .description(t('cli.command.dev.session.inspect'))
15784
+ .option('--sessions-dir <path>', t('cli.command.dev.session.dir'))
15785
+ .option('--format <format>', t('cli.command.dev.session.format'))
15786
+ .action(async (sessionId, sessionOptions) => {
15787
+ const Native = await Promise.resolve().then(function () { return index; });
15788
+ await Native.devSessionInspectCommand(sessionId, {
15789
+ sessionsDir: sessionOptions.sessionsDir,
15790
+ format: sessionOptions.format || devCmd.opts().format,
15791
+ });
15792
+ });
15793
+ devSessionCmd
15794
+ .command('list')
15795
+ .description(t('cli.command.dev.session.list'))
15796
+ .option('--sessions-dir <path>', t('cli.command.dev.session.dir'))
15797
+ .option('--format <format>', t('cli.command.dev.session.format'))
15798
+ .action(async (sessionOptions) => {
15799
+ const Native = await Promise.resolve().then(function () { return index; });
15800
+ await Native.devSessionListCommand({
15801
+ sessionsDir: sessionOptions.sessionsDir,
15802
+ format: sessionOptions.format || devCmd.opts().format,
15803
+ });
15804
+ });
15805
+ devSessionCmd
15806
+ .command('events <sessionId>')
15807
+ .description(t('cli.command.dev.session.events'))
15808
+ .option('--sessions-dir <path>', t('cli.command.dev.session.dir'))
15809
+ .option('--after <seq>', t('cli.command.dev.session.after'), '0')
15810
+ .option('--limit <count>', t('cli.command.dev.session.limit'), '100')
15811
+ .option('--format <format>', t('cli.command.dev.session.format'))
15812
+ .action(async (sessionId, sessionOptions) => {
15813
+ const Native = await Promise.resolve().then(function () { return index; });
15814
+ await Native.devSessionEventsCommand(sessionId, {
15815
+ sessionsDir: sessionOptions.sessionsDir,
15816
+ after: sessionOptions.after,
15817
+ limit: sessionOptions.limit,
15818
+ format: sessionOptions.format || devCmd.opts().format,
15819
+ });
15820
+ });
15821
+ devSessionCmd
15822
+ .command('stop <sessionId>')
15823
+ .description(t('cli.command.dev.session.stop'))
15824
+ .option('--sessions-dir <path>', t('cli.command.dev.session.dir'))
15825
+ .option('--format <format>', t('cli.command.dev.session.format'))
15826
+ .action(async (sessionId, sessionOptions) => {
15827
+ const Native = await Promise.resolve().then(function () { return index; });
15828
+ await Native.devSessionStopCommand(sessionId, {
15829
+ sessionsDir: sessionOptions.sessionsDir,
15830
+ format: sessionOptions.format || devCmd.opts().format,
15831
+ });
15832
+ });
15833
+ const checkCmd = program
15834
+ .command('check')
15835
+ .description(t('cli.command.check.desc'))
15836
+ .allowExcessArguments(false)
15837
+ .option('-d, --dir <dir>', t('cli.command.check.dir'))
15838
+ .option('--format <format>', t('cli.command.check.format'), 'text')
15839
+ .option('--include-passed', t('cli.command.check.includePassed'))
15840
+ .option('--fail-on <level>', t('cli.command.check.failOn'), 'error')
15841
+ .option('--ai', t('cli.command.check.ai'));
15842
+ withVerboseOption(checkCmd).action(async (cmd) => {
15843
+ const Native = await Promise.resolve().then(function () { return index; });
15844
+ const options = {
15845
+ dir: cmd.dir,
15846
+ format: cmd.format,
15847
+ includePassed: cmd.includePassed,
15848
+ failOn: cmd.failOn,
15849
+ };
15850
+ if (cmd.ai) {
15851
+ await Native.checkAi(options);
15852
+ return;
15262
15853
  }
15263
- checkUpdate();
15854
+ await Native.check(options);
15855
+ });
15856
+ withVerboseOption(program
15857
+ .command('doctor')
15858
+ .description(t('cli.command.doctor.desc'))
15859
+ .option('-d, --dir <dir>', t('cli.command.doctor.dir'))
15860
+ .option('--format <format>', t('cli.command.doctor.format'), 'text')
15861
+ .option('--fail-on <level>', t('cli.command.doctor.failOn'), 'error')).action(async (cmd) => {
15862
+ const Native = await Promise.resolve().then(function () { return index; });
15863
+ await Native.doctor({
15864
+ dir: cmd.dir,
15865
+ format: cmd.format,
15866
+ failOn: cmd.failOn,
15867
+ });
15264
15868
  });
15265
15869
  /**
15266
15870
  * ttmg build 命令
@@ -15268,11 +15872,22 @@ withVerboseOption(program
15268
15872
  withVerboseOption(program
15269
15873
  .option('--h5', t('cli.option.h5'))
15270
15874
  .command('build')
15271
- .description(t('cli.command.build.desc'))).action(() => {
15875
+ .description(t('cli.command.build.desc'))
15876
+ .option('-d, --dir <dir>', t('cli.command.build.dir'))
15877
+ .option('--format <format>', t('cli.command.build.format'), 'text')
15878
+ .option('--include-passed', t('cli.command.build.includePassed'))).action(async (cmd) => {
15272
15879
  const options = program.opts(); // 获取 options
15273
15880
  if (options.h5) {
15274
15881
  build();
15275
15882
  }
15883
+ else if (cmd.format !== 'text') {
15884
+ const Native = await Promise.resolve().then(function () { return index; });
15885
+ await Native.build({
15886
+ dir: cmd.dir,
15887
+ format: cmd.format,
15888
+ includePassed: cmd.includePassed,
15889
+ });
15890
+ }
15276
15891
  else {
15277
15892
  console.log(t('cli.native.build.placeholder'));
15278
15893
  }
@@ -15430,20 +16045,51 @@ const store = Store.getInstance({
15430
16045
  checkResults: [],
15431
16046
  });
15432
16047
 
16048
+ function getGameReadyPayload(message) {
16049
+ if (!message || typeof message !== 'object')
16050
+ return null;
16051
+ const runtimeEvent = message;
16052
+ if (runtimeEvent.method !== 'reportScene' ||
16053
+ runtimeEvent.payload?.sceneId !== 9999) {
16054
+ return null;
16055
+ }
16056
+ return {
16057
+ sceneId: 9999,
16058
+ signal: 'reportScene',
16059
+ };
16060
+ }
16061
+ function emitRuntimeSessionEvent(message) {
16062
+ const gameReady = getGameReadyPayload(message);
16063
+ if (!gameReady)
16064
+ return false;
16065
+ eventEmitter.emit('devSession', {
16066
+ type: 'game_ready',
16067
+ payload: gameReady,
16068
+ });
16069
+ return true;
16070
+ }
15433
16071
  class WsServer {
15434
16072
  constructor() {
15435
16073
  this.started = false;
16074
+ this.quiet = false;
15436
16075
  }
15437
- start() {
16076
+ start(options = {}) {
15438
16077
  if (this.started) {
15439
16078
  return;
15440
16079
  }
16080
+ this.quiet = options.quiet === true;
15441
16081
  this.started = true;
15442
16082
  this.startWsServer(store.getState().nodeWsPort);
15443
16083
  }
15444
16084
  startWsServer(port) {
15445
16085
  this.ws = new WebSocket.Server({ port });
15446
16086
  this.ws.on('connection', ws => {
16087
+ ws.on('close', () => {
16088
+ eventEmitter.emit('devSession', {
16089
+ type: 'client_disconnected',
16090
+ payload: {},
16091
+ });
16092
+ });
15447
16093
  const { clientHttpPort, clientHost, clientWsPort } = store.getState();
15448
16094
  if (clientHost) {
15449
16095
  this.send({
@@ -15476,8 +16122,13 @@ class WsServer {
15476
16122
  case 'startUpload':
15477
16123
  eventEmitter.emit('startUpload', {});
15478
16124
  break;
16125
+ case 'runtimeEvent': {
16126
+ emitRuntimeSessionEvent(clientMessage.payload);
16127
+ break;
16128
+ }
15479
16129
  case 'closeLocalDebug':
15480
- console.log('closeLocalDebug');
16130
+ if (!this.quiet)
16131
+ console.log('closeLocalDebug');
15481
16132
  /**
15482
16133
  * 关闭调试服务
15483
16134
  */
@@ -15491,7 +16142,8 @@ class WsServer {
15491
16142
  case 'scanQRCodeResult': {
15492
16143
  const payload = clientMessage.payload || {};
15493
16144
  const { host, port, wsPort, errMsg, isSuccess } = payload;
15494
- console.log('scanQRCodeResult', payload);
16145
+ if (!this.quiet)
16146
+ console.log('scanQRCodeResult', payload);
15495
16147
  if (isSuccess) {
15496
16148
  store.setState({
15497
16149
  clientHttpPort: port,
@@ -15506,9 +16158,18 @@ class WsServer {
15506
16158
  clientWsPort: wsPort,
15507
16159
  },
15508
16160
  });
15509
- console.log('scanQRcodeSuccess');
16161
+ eventEmitter.emit('devSession', {
16162
+ type: 'client_connected',
16163
+ payload: { host, port, wsPort },
16164
+ });
16165
+ if (!this.quiet)
16166
+ console.log('scanQRcodeSuccess');
15510
16167
  }
15511
16168
  else {
16169
+ eventEmitter.emit('devSession', {
16170
+ type: 'client_connection_failed',
16171
+ payload: { error: errMsg || '' },
16172
+ });
15512
16173
  this.send({
15513
16174
  method: 'scanQRCodeFailed',
15514
16175
  payload: {
@@ -15550,7 +16211,8 @@ class WsServer {
15550
16211
  this.startWsServer(store.getState().nodeWsPort);
15551
16212
  }
15552
16213
  else {
15553
- console.log(chalk.red.bold(err.message));
16214
+ if (!this.quiet)
16215
+ console.log(chalk.red.bold(err.message));
15554
16216
  process.exit(1);
15555
16217
  }
15556
16218
  });
@@ -15621,21 +16283,35 @@ async function compile(context) {
15621
16283
  const { clientKey, msg } = getClientKey();
15622
16284
  if (!clientKey) {
15623
16285
  if (context?.mode !== 'watch') {
15624
- console.log(chalk.red.bold(msg));
16286
+ if (!context?.quiet)
16287
+ console.log(chalk.red.bold(msg));
16288
+ if (context?.throwOnError) {
16289
+ throw new Error(msg);
16290
+ }
15625
16291
  process.exit(1);
15626
16292
  }
15627
- console.log(chalk.red.bold(msg));
16293
+ if (!context?.quiet)
16294
+ console.log(chalk.red.bold(msg));
15628
16295
  }
15629
16296
  const startTip = context?.mode === 'watch'
15630
16297
  ? t('native.compile.watching')
15631
16298
  : t('native.compile.compiling');
15632
- console.log(chalk.bold.cyan(startTip));
16299
+ if (!context?.quiet)
16300
+ console.log(chalk.bold.cyan(startTip));
16301
+ context?.onEvent?.('started');
15633
16302
  wsServer.sendCompilationStatus('start');
15634
16303
  store.setState({
15635
16304
  isUnderCompiling: true,
15636
16305
  });
15637
16306
  return new Promise((resolve, reject) => {
15638
- const worker = new worker_threads.Worker(path.resolve(__dirname, './scripts/worker.js'));
16307
+ const worker = new worker_threads.Worker(path.resolve(__dirname, './scripts/worker.js'), {
16308
+ stdout: context?.quiet,
16309
+ stderr: context?.quiet,
16310
+ });
16311
+ if (context?.quiet) {
16312
+ worker.stdout?.on('data', chunk => process.stderr.write(chunk));
16313
+ worker.stderr?.on('data', chunk => process.stderr.write(chunk));
16314
+ }
15639
16315
  worker.on('message', msg => {
15640
16316
  if (msg.type === 'status' && msg.status === 'end') {
15641
16317
  const { isSuccess, errorMsg, packages } = msg;
@@ -15647,10 +16323,20 @@ async function compile(context) {
15647
16323
  errorMsg,
15648
16324
  isSuccess: false,
15649
16325
  });
15650
- console.log(chalk.red.bold(errorMsg));
16326
+ if (!context?.quiet)
16327
+ console.log(chalk.red.bold(errorMsg));
16328
+ context?.onEvent?.('failed', errorMsg, msg.checkResults || []);
15651
16329
  // 非 watch 模式下,校验失败直接退出进程
15652
16330
  if (context?.mode !== 'watch') {
15653
- process.exit(1);
16331
+ if (context?.throwOnError) {
16332
+ reject(new Error(errorMsg));
16333
+ }
16334
+ else {
16335
+ process.exit(1);
16336
+ }
16337
+ }
16338
+ else {
16339
+ resolve(msg);
15654
16340
  }
15655
16341
  }
15656
16342
  else {
@@ -15665,7 +16351,10 @@ async function compile(context) {
15665
16351
  wsServer.sendCompilationStatus('end', {
15666
16352
  isSuccess: true,
15667
16353
  });
15668
- console.log(chalk.green.bold(t('native.compile.success')));
16354
+ context?.onEvent?.('succeeded', undefined, msg.checkResults || []);
16355
+ if (!context?.quiet) {
16356
+ console.log(chalk.green.bold(t('native.compile.success')));
16357
+ }
15669
16358
  resolve(msg); // 编译结束,返回结果
15670
16359
  }
15671
16360
  worker.terminate();
@@ -15676,7 +16365,9 @@ async function compile(context) {
15676
16365
  }
15677
16366
  });
15678
16367
  worker.on('error', err => {
15679
- console.log(chalk.red.bold(err.message));
16368
+ if (!context?.quiet)
16369
+ console.log(chalk.red.bold(err.message));
16370
+ context?.onEvent?.('failed', err.message);
15680
16371
  reject(err);
15681
16372
  worker.terminate();
15682
16373
  });
@@ -15755,10 +16446,14 @@ async function withWatchSuspended(fn, options = {}) {
15755
16446
  resumeWatch({ emit: options.emitOnFinish ?? true });
15756
16447
  }
15757
16448
  }
15758
- async function watch() {
16449
+ async function watch(options = {}) {
15759
16450
  let debounceTimer = null;
15760
16451
  const runUpdate = async () => {
15761
- await compile({ mode: 'watch' });
16452
+ await compile({
16453
+ mode: 'watch',
16454
+ quiet: options.quiet,
16455
+ onEvent: options.onCompileEvent,
16456
+ });
15762
16457
  wsServer.sendResourceChange();
15763
16458
  };
15764
16459
  triggerUpdate = () => {
@@ -15801,6 +16496,7 @@ async function watch() {
15801
16496
  watcher.on('error', error => {
15802
16497
  // console.error(chalk.red('[watch] 监听发生错误:'), error);
15803
16498
  });
16499
+ return watcher;
15804
16500
  }
15805
16501
 
15806
16502
  let spinner;
@@ -16231,7 +16927,7 @@ function isWorkspace() {
16231
16927
  }
16232
16928
  }
16233
16929
 
16234
- async function check() {
16930
+ async function check(options = {}) {
16235
16931
  const boxenFn = boxen.default ??
16236
16932
  boxen;
16237
16933
  const printBox = (type, message) => {
@@ -16251,9 +16947,14 @@ async function check() {
16251
16947
  // `You are not logged in, please login first, run ttmg login to login with your TikTok Dev Portal account, then try again`,
16252
16948
  // );
16253
16949
  // process.exit(1);
16254
- // }
16950
+ // }
16255
16951
  if (!isWorkspace()) {
16256
- printBox('Error', t('native.check.notWorkspace'));
16952
+ if (options.throwOnError) {
16953
+ throw new Error(t('native.check.notWorkspace'));
16954
+ }
16955
+ if (!options.quiet) {
16956
+ printBox('Error', t('native.check.notWorkspace'));
16957
+ }
16257
16958
  process.exit(1);
16258
16959
  }
16259
16960
  // const checkResult = await checkPkgs({
@@ -16309,19 +17010,26 @@ async function listen(app, options) {
16309
17010
  const version = devToolVersion$1;
16310
17011
  const server = http.createServer(app);
16311
17012
  function tryListen(port) {
16312
- return new Promise(resolve => {
17013
+ return new Promise((resolve, reject) => {
16313
17014
  const onError = err => {
16314
17015
  server.removeListener('listening', onListening);
16315
17016
  if (err.code === 'EADDRINUSE') {
16316
- console.log(chalk(t('native.server.portInUse', {
16317
- port,
16318
- next: port + 1,
16319
- })));
17017
+ if (!options?.quiet) {
17018
+ console.log(chalk(t('native.server.portInUse', {
17019
+ port,
17020
+ next: port + 1,
17021
+ })));
17022
+ }
16320
17023
  resolve(false);
16321
17024
  }
16322
17025
  else {
16323
- console.log(chalk.red.bold(err.message));
16324
- process.exit(1);
17026
+ if (options?.quiet) {
17027
+ reject(err);
17028
+ }
17029
+ else {
17030
+ console.log(chalk.red.bold(err.message));
17031
+ process.exit(1);
17032
+ }
16325
17033
  }
16326
17034
  };
16327
17035
  const onListening = () => {
@@ -16343,6 +17051,9 @@ async function listen(app, options) {
16343
17051
  store.setState({ nodeServerPort: currentPort + 1 });
16344
17052
  }
16345
17053
  if (!isListening) {
17054
+ if (options?.quiet) {
17055
+ throw new Error(t('native.server.failedAfterRetries', { retries: maxRetries }));
17056
+ }
16346
17057
  console.log(chalk.red.bold(t('native.server.failedAfterRetries', { retries: maxRetries })));
16347
17058
  process.exit(1);
16348
17059
  }
@@ -20790,37 +21501,1349 @@ function registerRoutes(app, options) {
20790
21501
 
20791
21502
  const outputDir = getOutputDir();
20792
21503
  const publicPath = path.join(__dirname, 'public');
20793
- async function start() {
21504
+ async function start(options = {}) {
20794
21505
  const startTime = Date.now();
20795
21506
  const app = express();
20796
21507
  setupMiddlewares(app, { publicPath, outputDir });
20797
21508
  registerRoutes(app, { publicPath });
20798
- const { url, version } = await listen(app, { maxRetries: 20 });
20799
- console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(t('native.server.readyIn')), chalk.bold(`${Date.now() - startTime}ms`));
20800
- showTips({ server: url });
21509
+ const result = await listen(app, {
21510
+ maxRetries: 20,
21511
+ quiet: options.quiet,
21512
+ });
21513
+ const { url, version } = result;
21514
+ if (!options.quiet) {
21515
+ console.log(chalk.green.bold(`TTMG`), chalk.green(`v${version}`), chalk.gray(t('native.server.readyIn')), chalk.bold(`${Date.now() - startTime}ms`));
21516
+ showTips({ server: url });
21517
+ }
20801
21518
  // 联调场景下(root `dev:debug` 脚本)我们用 vite dev server 提供 IDE,
20802
21519
  // 不需要 CLI 自己再开浏览器到 dist/public 里那份打包后的旧 IDE。设置
20803
21520
  // `TTMG_DEV_NO_OPEN=1` 抑制 openUrl,由调度脚本统一负责开 5173。
20804
21521
  // 普通用户场景(npm 安装 ttmg)不会设置这个 env var,行为不变。
20805
- if (process.env.TTMG_DEV_NO_OPEN === '1') {
20806
- console.log(chalk.gray(`[dev-debug] TTMG_DEV_NO_OPEN=1 detected, skipping browser auto-open. Use vite dev server (likely http://localhost:5173) instead.`));
20807
- return;
21522
+ if (options.openBrowser === false || process.env.TTMG_DEV_NO_OPEN === '1') {
21523
+ if (!options.quiet && process.env.TTMG_DEV_NO_OPEN === '1') {
21524
+ console.log(chalk.gray(`[dev-debug] TTMG_DEV_NO_OPEN=1 detected, skipping browser auto-open. Use vite dev server (likely http://localhost:5173) instead.`));
21525
+ }
21526
+ return result;
20808
21527
  }
20809
21528
  openUrl(url);
21529
+ return result;
20810
21530
  }
20811
21531
 
21532
+ function buildDevLaunchUrl({ clientKey, host, hosts, wsPort, }) {
21533
+ return `https://www.tiktok.com/ttmg/dev/${encodeURIComponent(clientKey)}?host=${encodeURIComponent(host)}&port=${wsPort}&host_list=${encodeURIComponent(JSON.stringify(hosts))}`;
21534
+ }
21535
+ class DevSessionReporter {
21536
+ constructor(options) {
21537
+ this.options = options;
21538
+ this.seq = 0;
21539
+ this.sessionId = (options.id || (() => `dev_${crypto.randomUUID()}`))();
21540
+ this.startedAt = (this.options.now || Date.now)();
21541
+ this.directory = path.resolve(options.rootDir || path.join('.ttmg', 'sessions', this.sessionId));
21542
+ this.manifestFile = path.join(this.directory, 'manifest.json');
21543
+ this.eventsFile = path.join(this.directory, 'events.ndjson');
21544
+ this.actionsFile = path.join(this.directory, 'actions.ndjson');
21545
+ this.screenshotsDirectory = path.join(this.directory, 'screenshots');
21546
+ this.summaryFile = path.join(this.directory, 'summary.json');
21547
+ this.qrFile = path.join(this.directory, 'launch.png');
21548
+ fs.mkdirSync(this.directory, { recursive: true });
21549
+ fs.mkdirSync(this.screenshotsDirectory, { recursive: true });
21550
+ fs.writeFileSync(this.actionsFile, '', { flag: 'a' });
21551
+ }
21552
+ emit(type, payload, source = 'cli') {
21553
+ const event = {
21554
+ schemaVersion: '1',
21555
+ sessionId: this.sessionId,
21556
+ seq: ++this.seq,
21557
+ ts: (this.options.now || Date.now)(),
21558
+ source,
21559
+ type,
21560
+ payload,
21561
+ };
21562
+ const line = JSON.stringify(event);
21563
+ fs.appendFileSync(this.eventsFile, `${line}\n`, 'utf8');
21564
+ if (this.options.events === 'ndjson') {
21565
+ (this.options.stdout || console.log)(line);
21566
+ }
21567
+ return event;
21568
+ }
21569
+ async writeReadyManifest({ pageUrl, version, hosts, httpPort, wsPort, }) {
21570
+ const advertisedHost = hosts[0] || '127.0.0.1';
21571
+ const rawUrl = buildDevLaunchUrl({
21572
+ clientKey: this.options.clientKey,
21573
+ host: advertisedHost,
21574
+ hosts,
21575
+ wsPort,
21576
+ });
21577
+ await QRCode.toFile(this.qrFile, rawUrl, { width: 320, margin: 2 });
21578
+ const manifest = {
21579
+ schemaVersion: '1',
21580
+ cliVersion: version,
21581
+ sessionId: this.sessionId,
21582
+ status: 'ready',
21583
+ project: {
21584
+ clientKey: this.options.clientKey,
21585
+ directory: process.cwd(),
21586
+ },
21587
+ process: {
21588
+ pid: process.pid,
21589
+ startedAt: this.startedAt,
21590
+ command: 'ttmg dev',
21591
+ },
21592
+ endpoints: {
21593
+ page: pageUrl,
21594
+ runtimeWs: `ws://${advertisedHost}:${wsPort}`,
21595
+ },
21596
+ launch: {
21597
+ rawUrl,
21598
+ qrPayload: rawUrl,
21599
+ qrPngPath: this.qrFile,
21600
+ },
21601
+ network: {
21602
+ mode: hosts.length > 0 ? 'lan' : 'loopback',
21603
+ advertisedHost,
21604
+ hosts,
21605
+ httpPort,
21606
+ wsPort,
21607
+ },
21608
+ evidence: {
21609
+ directory: this.directory,
21610
+ manifest: this.manifestFile,
21611
+ events: this.eventsFile,
21612
+ actions: this.actionsFile,
21613
+ screenshots: this.screenshotsDirectory,
21614
+ summary: this.summaryFile,
21615
+ },
21616
+ capabilities: {
21617
+ structuredEvents: true,
21618
+ manualScan: true,
21619
+ gameReadyProtocol: true,
21620
+ gameReady: false,
21621
+ deviceLaunch: false,
21622
+ runtimeLogs: false,
21623
+ screenshot: false,
21624
+ actions: false,
21625
+ },
21626
+ errorCode: null,
21627
+ error: null,
21628
+ };
21629
+ fs.writeFileSync(this.manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
21630
+ return manifest;
21631
+ }
21632
+ writeFailedManifest(errorCode, error) {
21633
+ const result = {
21634
+ schemaVersion: '1',
21635
+ sessionId: this.sessionId,
21636
+ status: 'failed',
21637
+ project: {
21638
+ clientKey: this.options.clientKey,
21639
+ directory: process.cwd(),
21640
+ },
21641
+ evidence: {
21642
+ directory: this.directory,
21643
+ manifest: this.manifestFile,
21644
+ events: this.eventsFile,
21645
+ actions: this.actionsFile,
21646
+ screenshots: this.screenshotsDirectory,
21647
+ summary: this.summaryFile,
21648
+ },
21649
+ errorCode,
21650
+ error,
21651
+ };
21652
+ fs.writeFileSync(this.manifestFile, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
21653
+ return result;
21654
+ }
21655
+ writeSummary(status, reason, errorCode = null) {
21656
+ const summary = {
21657
+ schemaVersion: '1',
21658
+ sessionId: this.sessionId,
21659
+ status,
21660
+ reason,
21661
+ errorCode,
21662
+ completedAt: (this.options.now || Date.now)(),
21663
+ };
21664
+ fs.writeFileSync(this.summaryFile, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
21665
+ return summary;
21666
+ }
21667
+ }
21668
+ function resolveDevSessionCompletion({ signal, timeoutSeconds, clientConnectedObserved, gameReadyObserved, }) {
21669
+ if (gameReadyObserved) {
21670
+ return {
21671
+ status: 'passed',
21672
+ reason: 'GAME_READY was observed from reportScene({ sceneId: 9999 }).',
21673
+ errorCode: null,
21674
+ exitCode: 0,
21675
+ };
21676
+ }
21677
+ if (signal === 'TIMEOUT') {
21678
+ const errorCode = clientConnectedObserved
21679
+ ? 'GAME_READY_TIMEOUT'
21680
+ : 'CLIENT_CONNECT_TIMEOUT';
21681
+ return {
21682
+ status: 'blocked',
21683
+ reason: clientConnectedObserved
21684
+ ? `The local DevTool session timed out after ${timeoutSeconds} seconds before GAME_READY.`
21685
+ : `The local DevTool session timed out after ${timeoutSeconds} seconds before a client connected.`,
21686
+ errorCode,
21687
+ exitCode: 2,
21688
+ };
21689
+ }
21690
+ return {
21691
+ status: 'blocked',
21692
+ reason: 'The local DevTool session stopped without GAME_READY evidence; device launch and runtime log capture are not enabled in this beta.',
21693
+ errorCode: null,
21694
+ exitCode: 0,
21695
+ };
21696
+ }
21697
+
21698
+ const defaultDependencies$3 = {
21699
+ checkPackages: ttmgPack.checkPkgs,
21700
+ operationId: () => `check_${crypto.randomUUID()}`,
21701
+ };
21702
+ function normalizeFormat$1(format) {
21703
+ const value = (format || 'text').toLowerCase();
21704
+ if (value !== 'text' && value !== 'json') {
21705
+ throw new Error(t('check.error.invalidFormat', { format: value }));
21706
+ }
21707
+ return value;
21708
+ }
21709
+ function normalizeFailOn(failOn) {
21710
+ const value = (failOn || 'error').toLowerCase();
21711
+ if (value !== 'error' && value !== 'warning') {
21712
+ throw new Error(t('check.error.invalidFailOn', { failOn: value }));
21713
+ }
21714
+ return value;
21715
+ }
21716
+ function codePart(value) {
21717
+ return value
21718
+ .replace(/[^a-zA-Z0-9]+/g, '_')
21719
+ .replace(/^_+|_+$/g, '')
21720
+ .toUpperCase();
21721
+ }
21722
+ function toCheckDiagnostic(result) {
21723
+ const type = result.type || 'unknown';
21724
+ const name = result.name || result.dimension || 'project';
21725
+ return {
21726
+ code: `TTMG_CHECK_${codePart(type)}_${codePart(name)}`,
21727
+ severity: result.level || (result.passed ? 'info' : 'error'),
21728
+ passed: result.passed,
21729
+ type,
21730
+ dimension: result.dimension || 'project',
21731
+ name,
21732
+ required: result.required === true,
21733
+ message: result.msg,
21734
+ location: {
21735
+ file: result.file || null,
21736
+ files: Array.isArray(result.files) ? result.files : [],
21737
+ },
21738
+ metrics: {
21739
+ sizeBytes: typeof result.size === 'number' ? result.size : null,
21740
+ },
21741
+ help: {
21742
+ url: result.detail?.link || null,
21743
+ text: result.detail?.text || null,
21744
+ },
21745
+ };
21746
+ }
21747
+ function createFailedReport({ operationId, directory, code, message, }) {
21748
+ const diagnostic = {
21749
+ code,
21750
+ severity: 'error',
21751
+ passed: false,
21752
+ type: 'project',
21753
+ dimension: 'project',
21754
+ name: 'project',
21755
+ required: true,
21756
+ message,
21757
+ location: { file: null, files: [] },
21758
+ metrics: { sizeBytes: null },
21759
+ help: { url: null, text: null },
21760
+ };
21761
+ return {
21762
+ schemaVersion: '1',
21763
+ operationId,
21764
+ command: 'check',
21765
+ status: 'failed',
21766
+ project: { directory },
21767
+ summary: { total: 1, passed: 0, warnings: 0, errors: 1 },
21768
+ diagnostics: [diagnostic],
21769
+ errorCode: code,
21770
+ error: message,
21771
+ };
21772
+ }
21773
+ async function executeCheck(options, dependencies = {}) {
21774
+ const deps = { ...defaultDependencies$3, ...dependencies };
21775
+ const operationId = deps.operationId();
21776
+ const directory = path.resolve(process.cwd(), options.dir || '.');
21777
+ if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
21778
+ return createFailedReport({
21779
+ operationId,
21780
+ directory,
21781
+ code: 'CHECK_DIRECTORY_NOT_FOUND',
21782
+ message: t('check.error.dirNotExist', { dir: directory }),
21783
+ });
21784
+ }
21785
+ const hasEntry = NATIVE_GAME_ENTRY_FILES.some(file => fs.existsSync(path.join(directory, file)));
21786
+ if (!hasEntry) {
21787
+ return createFailedReport({
21788
+ operationId,
21789
+ directory,
21790
+ code: 'CHECK_INVALID_PROJECT',
21791
+ message: t('native.check.notWorkspace'),
21792
+ });
21793
+ }
21794
+ try {
21795
+ const results = await deps.checkPackages({
21796
+ entry: directory,
21797
+ output: directory,
21798
+ dev: {
21799
+ enable: true,
21800
+ enableSourcemap: false,
21801
+ enableLog: false,
21802
+ },
21803
+ build: {
21804
+ enableOdr: false,
21805
+ enableAPICheck: true,
21806
+ ...PKG_SIZE_LIMIT,
21807
+ },
21808
+ }, { lang: getCurrentLanguage() });
21809
+ const allDiagnostics = results.map(toCheckDiagnostic);
21810
+ const errors = allDiagnostics.filter(item => !item.passed && item.severity === 'error').length;
21811
+ const warnings = allDiagnostics.filter(item => !item.passed && item.severity === 'warning').length;
21812
+ const passed = allDiagnostics.filter(item => item.passed).length;
21813
+ return {
21814
+ schemaVersion: '1',
21815
+ operationId,
21816
+ command: 'check',
21817
+ status: errors > 0 ? 'failed' : warnings > 0 ? 'warning' : 'passed',
21818
+ project: { directory },
21819
+ summary: {
21820
+ total: allDiagnostics.length,
21821
+ passed,
21822
+ warnings,
21823
+ errors,
21824
+ },
21825
+ diagnostics: options.includePassed
21826
+ ? allDiagnostics
21827
+ : allDiagnostics.filter(item => !item.passed),
21828
+ errorCode: errors > 0 ? 'CHECK_FAILED' : null,
21829
+ error: null,
21830
+ };
21831
+ }
21832
+ catch (error) {
21833
+ const message = error instanceof Error ? error.message : String(error);
21834
+ return createFailedReport({
21835
+ operationId,
21836
+ directory,
21837
+ code: 'CHECK_EXECUTION_FAILED',
21838
+ message,
21839
+ });
21840
+ }
21841
+ }
21842
+ function printText(report) {
21843
+ const color = report.status === 'failed'
21844
+ ? chalk.red
21845
+ : report.status === 'warning'
21846
+ ? chalk.yellow
21847
+ : chalk.green;
21848
+ console.log(color.bold(t('check.summary', {
21849
+ status: report.status,
21850
+ passed: report.summary.passed,
21851
+ warnings: report.summary.warnings,
21852
+ errors: report.summary.errors,
21853
+ })));
21854
+ for (const diagnostic of report.diagnostics) {
21855
+ const location = diagnostic.location.file
21856
+ ? ` (${diagnostic.location.file})`
21857
+ : '';
21858
+ console.log(`${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}${location}`);
21859
+ if (diagnostic.help.url)
21860
+ console.log(` ${diagnostic.help.url}`);
21861
+ }
21862
+ }
21863
+ async function checkCommand(options = {}) {
21864
+ let format = 'text';
21865
+ let failOn = 'error';
21866
+ try {
21867
+ format = normalizeFormat$1(options.format);
21868
+ failOn = normalizeFailOn(options.failOn);
21869
+ }
21870
+ catch (error) {
21871
+ const report = createFailedReport({
21872
+ operationId: `check_${crypto.randomUUID()}`,
21873
+ directory: path.resolve(process.cwd(), options.dir || '.'),
21874
+ code: 'CHECK_INVALID_ARGUMENT',
21875
+ message: error instanceof Error ? error.message : String(error),
21876
+ });
21877
+ if (options.format === 'json')
21878
+ console.log(JSON.stringify(report));
21879
+ else
21880
+ printText(report);
21881
+ process.exitCode = 1;
21882
+ return report;
21883
+ }
21884
+ const report = await executeCheck(options);
21885
+ if (format === 'json')
21886
+ console.log(JSON.stringify(report));
21887
+ else
21888
+ printText(report);
21889
+ if (report.status === 'failed' ||
21890
+ (failOn === 'warning' && report.status === 'warning')) {
21891
+ process.exitCode = 1;
21892
+ }
21893
+ return report;
21894
+ }
21895
+
21896
+ function normalizeFormat(format) {
21897
+ const value = (format || 'text').toLowerCase();
21898
+ if (value !== 'text' && value !== 'json') {
21899
+ throw new Error(`Unsupported dev output format: ${value}. Use text or json.`);
21900
+ }
21901
+ return value;
21902
+ }
21903
+ function normalizeEvents(events) {
21904
+ const value = (events || 'none').toLowerCase();
21905
+ if (value !== 'none' && value !== 'ndjson') {
21906
+ throw new Error(`Unsupported dev events format: ${value}. Use none or ndjson.`);
21907
+ }
21908
+ return value;
21909
+ }
21910
+ function normalizeTimeout(timeout) {
21911
+ if (timeout === undefined || timeout === '')
21912
+ return 0;
21913
+ const seconds = Number(timeout);
21914
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
21915
+ throw new Error('Unsupported dev timeout. Use a number of seconds between 1 and 86400.');
21916
+ }
21917
+ return Math.round(seconds * 1000);
21918
+ }
20812
21919
  async function dev(options = {}) {
20813
- await check();
20814
- await init({ clientKey: options.clientKey });
20815
- wsServer.start();
20816
- await start();
20817
- await compile();
20818
- listen$1();
20819
- watch();
21920
+ const format = normalizeFormat(options.format);
21921
+ const events = normalizeEvents(options.events);
21922
+ if (format === 'json' && events === 'ndjson') {
21923
+ throw new Error('Dev output modes cannot be combined. Use either --format json or --events ndjson.');
21924
+ }
21925
+ const structured = format === 'json' || events === 'ndjson';
21926
+ const timeoutMs = normalizeTimeout(options.timeout);
21927
+ if (timeoutMs > 0 && !structured) {
21928
+ throw new Error('Dev timeout requires --format json or --events ndjson.');
21929
+ }
21930
+ const clientKey = options.clientKey?.trim() || '';
21931
+ const reporter = structured
21932
+ ? new DevSessionReporter({
21933
+ clientKey,
21934
+ rootDir: options.evidenceDir,
21935
+ events,
21936
+ })
21937
+ : null;
21938
+ let gameReadyObserved = false;
21939
+ let clientConnectedObserved = false;
21940
+ const unsubscribeClientEvents = reporter
21941
+ ? eventEmitter.on('devSession', event => {
21942
+ if (event.type === 'game_ready') {
21943
+ gameReadyObserved = true;
21944
+ reporter.emit('lifecycle', {
21945
+ state: 'GAME_READY',
21946
+ ...event.payload,
21947
+ }, 'runtime');
21948
+ return;
21949
+ }
21950
+ if (event.type === 'client_connected') {
21951
+ clientConnectedObserved = true;
21952
+ }
21953
+ reporter.emit('client', {
21954
+ state: event.type,
21955
+ ...event.payload,
21956
+ }, 'node');
21957
+ })
21958
+ : null;
21959
+ try {
21960
+ reporter?.emit('lifecycle', { state: 'CREATED' });
21961
+ if (structured && !clientKey) {
21962
+ throw new Error('Agent-friendly dev requires --client-key to avoid an interactive prompt.');
21963
+ }
21964
+ reporter?.emit('lifecycle', { state: 'CHECKING' });
21965
+ await check({ throwOnError: structured, quiet: structured });
21966
+ await init({ clientKey: options.clientKey });
21967
+ wsServer.start({ quiet: structured });
21968
+ const serverResult = await start({
21969
+ openBrowser: options.noOpen !== true,
21970
+ quiet: structured,
21971
+ });
21972
+ reporter?.emit('lifecycle', {
21973
+ state: 'SERVER_READY',
21974
+ page: serverResult.url,
21975
+ httpPort: serverResult.port,
21976
+ wsPort: store.getState().nodeWsPort,
21977
+ }, 'node');
21978
+ await compile({
21979
+ mode: 'start',
21980
+ quiet: structured,
21981
+ throwOnError: structured,
21982
+ onEvent: (state, error, checkResults) => reporter?.emit('compile', {
21983
+ state,
21984
+ error: error || null,
21985
+ diagnostics: (checkResults || []).map(result => toCheckDiagnostic(result)),
21986
+ }),
21987
+ });
21988
+ listen$1();
21989
+ const watcher = await watch({
21990
+ quiet: structured,
21991
+ onCompileEvent: (state, error, checkResults) => reporter?.emit('compile', {
21992
+ state,
21993
+ error: error || null,
21994
+ diagnostics: (checkResults || []).map(result => toCheckDiagnostic(result)),
21995
+ }),
21996
+ });
21997
+ if (reporter) {
21998
+ const manifest = await reporter.writeReadyManifest({
21999
+ pageUrl: serverResult.url,
22000
+ version: serverResult.version,
22001
+ hosts: getLocalIPs(),
22002
+ httpPort: serverResult.port,
22003
+ wsPort: store.getState().nodeWsPort,
22004
+ });
22005
+ reporter.emit('lifecycle', {
22006
+ state: 'READY',
22007
+ manifest: reporter.manifestFile,
22008
+ });
22009
+ if (format === 'json')
22010
+ console.log(JSON.stringify(manifest));
22011
+ let stopping = false;
22012
+ let timeoutTimer = null;
22013
+ const stop = async (signal) => {
22014
+ if (stopping)
22015
+ return;
22016
+ stopping = true;
22017
+ reporter.emit('lifecycle', { state: 'STOPPING', signal });
22018
+ if (timeoutTimer)
22019
+ clearTimeout(timeoutTimer);
22020
+ await watcher.close();
22021
+ unsubscribeClientEvents?.();
22022
+ wsServer.close();
22023
+ await new Promise(resolve => serverResult.server.close(() => resolve()));
22024
+ const completion = resolveDevSessionCompletion({
22025
+ signal,
22026
+ timeoutSeconds: timeoutMs / 1000,
22027
+ clientConnectedObserved,
22028
+ gameReadyObserved,
22029
+ });
22030
+ reporter.emit('lifecycle', {
22031
+ state: 'COMPLETED',
22032
+ signal,
22033
+ gameReadyObserved,
22034
+ errorCode: completion.errorCode,
22035
+ });
22036
+ reporter.writeSummary(completion.status, completion.reason, completion.errorCode);
22037
+ process.exit(completion.exitCode);
22038
+ };
22039
+ if (timeoutMs > 0) {
22040
+ timeoutTimer = setTimeout(() => void stop('TIMEOUT'), timeoutMs);
22041
+ }
22042
+ process.once('SIGINT', () => void stop('SIGINT'));
22043
+ process.once('SIGTERM', () => void stop('SIGTERM'));
22044
+ }
22045
+ }
22046
+ catch (error) {
22047
+ if (!reporter)
22048
+ throw error;
22049
+ const message = error instanceof Error ? error.message : String(error);
22050
+ unsubscribeClientEvents?.();
22051
+ wsServer.close();
22052
+ const errorCode = !clientKey
22053
+ ? 'CLIENT_KEY_REQUIRED'
22054
+ : message.includes('project entry') || message.includes('工程入口')
22055
+ ? 'INVALID_PROJECT'
22056
+ : 'DEV_START_FAILED';
22057
+ reporter.emit('lifecycle', {
22058
+ state: 'FAILED',
22059
+ errorCode,
22060
+ error: message,
22061
+ });
22062
+ const failed = reporter.writeFailedManifest(errorCode, message);
22063
+ reporter.writeSummary('failed', message, errorCode);
22064
+ if (format === 'json')
22065
+ console.log(JSON.stringify(failed));
22066
+ process.exitCode = 1;
22067
+ return failed;
22068
+ }
22069
+ }
22070
+
22071
+ const sourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
22072
+ const ignoredDirectories = new Set([
22073
+ '.git',
22074
+ '.ttmg',
22075
+ 'node_modules',
22076
+ '__TTMG__',
22077
+ ]);
22078
+ const maxFileBytes = 5 * 1024 * 1024;
22079
+ const maxScannedBytes = 100 * 1024 * 1024;
22080
+ const maxScannedFiles = 5000;
22081
+ const gameReadyPattern = /\breportScene\s*\(\s*\{[\s\S]{0,300}?(?:\bsceneId\b|['"]sceneId['"])\s*:\s*9999\b/;
22082
+ function scanGameReadySignal(directory) {
22083
+ const pending = [directory];
22084
+ let scannedFiles = 0;
22085
+ let skippedFiles = 0;
22086
+ let scannedBytes = 0;
22087
+ let matchedFile = null;
22088
+ let truncated = false;
22089
+ while (pending.length > 0 && !matchedFile) {
22090
+ const current = pending.pop();
22091
+ let entries;
22092
+ try {
22093
+ entries = fs.readdirSync(current, { withFileTypes: true });
22094
+ }
22095
+ catch {
22096
+ skippedFiles += 1;
22097
+ continue;
22098
+ }
22099
+ for (const entry of entries) {
22100
+ if (matchedFile)
22101
+ break;
22102
+ const absolute = path.join(current, entry.name);
22103
+ if (entry.isSymbolicLink()) {
22104
+ skippedFiles += 1;
22105
+ continue;
22106
+ }
22107
+ if (entry.isDirectory()) {
22108
+ if (!ignoredDirectories.has(entry.name))
22109
+ pending.push(absolute);
22110
+ continue;
22111
+ }
22112
+ if (!entry.isFile() || !sourceExtensions.has(path.extname(entry.name))) {
22113
+ continue;
22114
+ }
22115
+ if (scannedFiles >= maxScannedFiles ||
22116
+ scannedBytes >= maxScannedBytes) {
22117
+ truncated = true;
22118
+ break;
22119
+ }
22120
+ let stat;
22121
+ try {
22122
+ stat = fs.statSync(absolute);
22123
+ }
22124
+ catch {
22125
+ skippedFiles += 1;
22126
+ continue;
22127
+ }
22128
+ if (stat.size > maxFileBytes ||
22129
+ scannedBytes + stat.size > maxScannedBytes) {
22130
+ skippedFiles += 1;
22131
+ continue;
22132
+ }
22133
+ try {
22134
+ const source = fs.readFileSync(absolute, 'utf8');
22135
+ scannedFiles += 1;
22136
+ scannedBytes += stat.size;
22137
+ if (gameReadyPattern.test(source)) {
22138
+ matchedFile = path.relative(directory, absolute) || entry.name;
22139
+ }
22140
+ }
22141
+ catch {
22142
+ skippedFiles += 1;
22143
+ }
22144
+ }
22145
+ }
22146
+ return {
22147
+ confidence: 'static',
22148
+ gameReadySignal: {
22149
+ detected: matchedFile !== null,
22150
+ file: matchedFile,
22151
+ signal: 'reportScene({ sceneId: 9999 })',
22152
+ },
22153
+ scan: { scannedFiles, skippedFiles, scannedBytes, truncated },
22154
+ };
22155
+ }
22156
+ function readinessDiagnostic(evidence) {
22157
+ const detected = evidence.gameReadySignal.detected;
22158
+ return {
22159
+ code: 'TTMG_CHECK_AI_GAME_READY_SIGNAL',
22160
+ severity: detected ? 'info' : 'warning',
22161
+ passed: detected,
22162
+ type: 'ai',
22163
+ dimension: 'agent_debug',
22164
+ name: 'game_ready_signal',
22165
+ required: true,
22166
+ message: detected
22167
+ ? t('check.ai.gameReady.found')
22168
+ : t('check.ai.gameReady.missing'),
22169
+ location: {
22170
+ file: evidence.gameReadySignal.file,
22171
+ files: evidence.gameReadySignal.file
22172
+ ? [evidence.gameReadySignal.file]
22173
+ : [],
22174
+ },
22175
+ metrics: { sizeBytes: null },
22176
+ help: { url: null, text: t('check.ai.gameReady.help') },
22177
+ };
22178
+ }
22179
+ function emptyEvidence() {
22180
+ return {
22181
+ confidence: 'static',
22182
+ gameReadySignal: {
22183
+ detected: false,
22184
+ file: null,
22185
+ signal: 'reportScene({ sceneId: 9999 })',
22186
+ },
22187
+ scan: {
22188
+ scannedFiles: 0,
22189
+ skippedFiles: 0,
22190
+ scannedBytes: 0,
22191
+ truncated: false,
22192
+ },
22193
+ };
22194
+ }
22195
+ async function executeAiCheck(options, dependencies = {}) {
22196
+ const directory = path.resolve(process.cwd(), options.dir || '.');
22197
+ const base = await executeCheck({ ...options, includePassed: true }, {
22198
+ ...dependencies,
22199
+ operationId: dependencies.operationId || (() => `check_ai_${crypto.randomUUID()}`),
22200
+ });
22201
+ const canScan = base.errorCode !== 'CHECK_DIRECTORY_NOT_FOUND' &&
22202
+ base.errorCode !== 'CHECK_INVALID_PROJECT';
22203
+ const aiReadiness = canScan ? scanGameReadySignal(directory) : emptyEvidence();
22204
+ const allDiagnostics = canScan
22205
+ ? [...base.diagnostics, readinessDiagnostic(aiReadiness)]
22206
+ : base.diagnostics;
22207
+ const errors = allDiagnostics.filter(item => !item.passed && item.severity === 'error').length;
22208
+ const warnings = allDiagnostics.filter(item => !item.passed && item.severity === 'warning').length;
22209
+ const passed = allDiagnostics.filter(item => item.passed).length;
22210
+ return {
22211
+ schemaVersion: '1',
22212
+ operationId: base.operationId,
22213
+ command: 'check',
22214
+ mode: 'ai',
22215
+ status: errors > 0 ? 'failed' : warnings > 0 ? 'warning' : 'passed',
22216
+ project: { directory },
22217
+ summary: {
22218
+ total: allDiagnostics.length,
22219
+ passed,
22220
+ warnings,
22221
+ errors,
22222
+ },
22223
+ diagnostics: options.includePassed
22224
+ ? allDiagnostics
22225
+ : allDiagnostics.filter(item => !item.passed),
22226
+ aiReadiness,
22227
+ platformWrite: false,
22228
+ errorCode: errors > 0 ? base.errorCode || 'CHECK_AI_FAILED' : null,
22229
+ error: base.error,
22230
+ };
22231
+ }
22232
+ function printAiText(report) {
22233
+ const color = report.status === 'failed'
22234
+ ? chalk.red
22235
+ : report.status === 'warning'
22236
+ ? chalk.yellow
22237
+ : chalk.green;
22238
+ console.log(color.bold(t('check.summary', {
22239
+ status: report.status,
22240
+ passed: report.summary.passed,
22241
+ warnings: report.summary.warnings,
22242
+ errors: report.summary.errors,
22243
+ })));
22244
+ for (const diagnostic of report.diagnostics) {
22245
+ const location = diagnostic.location.file
22246
+ ? ` (${diagnostic.location.file})`
22247
+ : '';
22248
+ console.log(`${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}${location}`);
22249
+ if (diagnostic.help.text)
22250
+ console.log(` ${diagnostic.help.text}`);
22251
+ }
22252
+ }
22253
+ async function aiCheckCommand(options = {}) {
22254
+ let format = 'text';
22255
+ let failOn = 'error';
22256
+ try {
22257
+ format = normalizeFormat$1(options.format);
22258
+ failOn = normalizeFailOn(options.failOn);
22259
+ }
22260
+ catch (error) {
22261
+ const report = {
22262
+ schemaVersion: '1',
22263
+ operationId: `check_ai_${crypto.randomUUID()}`,
22264
+ command: 'check',
22265
+ mode: 'ai',
22266
+ status: 'failed',
22267
+ project: { directory: path.resolve(process.cwd(), options.dir || '.') },
22268
+ summary: { total: 1, passed: 0, warnings: 0, errors: 1 },
22269
+ diagnostics: [],
22270
+ aiReadiness: emptyEvidence(),
22271
+ platformWrite: false,
22272
+ errorCode: 'CHECK_INVALID_ARGUMENT',
22273
+ error: error instanceof Error ? error.message : String(error),
22274
+ };
22275
+ if (options.format === 'json')
22276
+ console.log(JSON.stringify(report));
22277
+ else
22278
+ printAiText(report);
22279
+ process.exitCode = 1;
22280
+ return report;
22281
+ }
22282
+ const report = await executeAiCheck(options);
22283
+ if (format === 'json')
22284
+ console.log(JSON.stringify(report));
22285
+ else
22286
+ printAiText(report);
22287
+ if (report.status === 'failed' ||
22288
+ (failOn === 'warning' && report.status === 'warning')) {
22289
+ process.exitCode = 1;
22290
+ }
22291
+ return report;
22292
+ }
22293
+
22294
+ const defaultDependencies$2 = {
22295
+ buildPackages: ttmgPack.debugPkgs,
22296
+ getPackages: ttmgPack.getPkgs,
22297
+ now: () => Date.now(),
22298
+ operationId: () => `build_${crypto.randomUUID()}`,
22299
+ };
22300
+ function failedReport({ operationId, directory, outputDirectory, durationMs, errorCode, error, diagnostics = [], }) {
22301
+ return {
22302
+ schemaVersion: '1',
22303
+ operationId,
22304
+ command: 'build',
22305
+ status: 'failed',
22306
+ project: { directory },
22307
+ artifact: {
22308
+ outputDirectory,
22309
+ engine: 'unknown',
22310
+ sourceSizeBytes: 0,
22311
+ packageSummaries: [],
22312
+ packages: {},
22313
+ packageConfig: null,
22314
+ },
22315
+ summary: {
22316
+ durationMs,
22317
+ checks: diagnostics.length,
22318
+ warnings: diagnostics.filter(item => !item.passed && item.severity === 'warning').length,
22319
+ errors: Math.max(1, diagnostics.filter(item => !item.passed && item.severity === 'error')
22320
+ .length),
22321
+ },
22322
+ diagnostics,
22323
+ platformWrite: false,
22324
+ errorCode,
22325
+ error,
22326
+ };
22327
+ }
22328
+ async function executeStructuredBuild(options, dependencies = {}) {
22329
+ const deps = { ...defaultDependencies$2, ...dependencies };
22330
+ const operationId = deps.operationId();
22331
+ const startedAt = deps.now();
22332
+ const directory = path.resolve(process.cwd(), options.dir || '.');
22333
+ const outputDirectory = path.join(os.homedir(), '__TTMG__', 'build', operationId);
22334
+ if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) {
22335
+ return failedReport({
22336
+ operationId,
22337
+ directory,
22338
+ outputDirectory,
22339
+ durationMs: deps.now() - startedAt,
22340
+ errorCode: 'BUILD_DIRECTORY_NOT_FOUND',
22341
+ error: t('build.error.dirNotExist', { dir: directory }),
22342
+ });
22343
+ }
22344
+ if (!NATIVE_GAME_ENTRY_FILES.some(file => fs.existsSync(path.join(directory, file)))) {
22345
+ return failedReport({
22346
+ operationId,
22347
+ directory,
22348
+ outputDirectory,
22349
+ durationMs: deps.now() - startedAt,
22350
+ errorCode: 'BUILD_INVALID_PROJECT',
22351
+ error: t('native.check.notWorkspace'),
22352
+ });
22353
+ }
22354
+ fs.mkdirSync(outputDirectory, { recursive: true });
22355
+ try {
22356
+ const result = await deps.buildPackages({
22357
+ entry: directory,
22358
+ output: outputDirectory,
22359
+ dev: {
22360
+ enable: true,
22361
+ port: 0,
22362
+ host: 'localhost',
22363
+ enableSourcemap: false,
22364
+ enableLog: false,
22365
+ },
22366
+ build: {
22367
+ enableAPICheck: true,
22368
+ enableOdr: false,
22369
+ ...PKG_SIZE_LIMIT,
22370
+ },
22371
+ }, { lang: getCurrentLanguage() });
22372
+ const allDiagnostics = (result.checkResults || []).map(toCheckDiagnostic);
22373
+ const diagnostics = options.includePassed
22374
+ ? allDiagnostics
22375
+ : allDiagnostics.filter(item => !item.passed);
22376
+ const errors = allDiagnostics.filter(item => !item.passed && item.severity === 'error').length;
22377
+ const warnings = allDiagnostics.filter(item => !item.passed && item.severity === 'warning').length;
22378
+ if (!result.isSuccess) {
22379
+ return failedReport({
22380
+ operationId,
22381
+ directory,
22382
+ outputDirectory,
22383
+ durationMs: deps.now() - startedAt,
22384
+ errorCode: 'BUILD_FAILED',
22385
+ error: result.errorMsg || t('build.error.failed'),
22386
+ diagnostics,
22387
+ });
22388
+ }
22389
+ const packageSummary = await deps.getPackages({ entryDir: directory });
22390
+ return {
22391
+ schemaVersion: '1',
22392
+ operationId,
22393
+ command: 'build',
22394
+ status: 'succeeded',
22395
+ project: { directory },
22396
+ artifact: {
22397
+ outputDirectory,
22398
+ engine: packageSummary.engine || 'unknown',
22399
+ sourceSizeBytes: packageSummary.size || 0,
22400
+ packageSummaries: packageSummary.packages || [],
22401
+ packages: result.packages || {},
22402
+ packageConfig: result.packageConfig || null,
22403
+ },
22404
+ summary: {
22405
+ durationMs: deps.now() - startedAt,
22406
+ checks: allDiagnostics.length,
22407
+ warnings,
22408
+ errors,
22409
+ },
22410
+ diagnostics,
22411
+ platformWrite: false,
22412
+ errorCode: null,
22413
+ error: null,
22414
+ };
22415
+ }
22416
+ catch (error) {
22417
+ const message = error instanceof Error ? error.message : String(error);
22418
+ return failedReport({
22419
+ operationId,
22420
+ directory,
22421
+ outputDirectory,
22422
+ durationMs: deps.now() - startedAt,
22423
+ errorCode: 'BUILD_EXECUTION_FAILED',
22424
+ error: message,
22425
+ });
22426
+ }
22427
+ }
22428
+ async function structuredBuildCommand(options = {}) {
22429
+ if ((options.format || '').toLowerCase() !== 'json') {
22430
+ console.error(t('build.error.invalidFormat', { format: options.format || '' }));
22431
+ process.exitCode = 1;
22432
+ return null;
22433
+ }
22434
+ const report = await executeStructuredBuild(options);
22435
+ console.log(JSON.stringify(report));
22436
+ if (report.status === 'failed')
22437
+ process.exitCode = 1;
22438
+ return report;
22439
+ }
22440
+
22441
+ const defaultDependencies$1 = {
22442
+ isProcessRunning: pid => {
22443
+ try {
22444
+ process.kill(pid, 0);
22445
+ return true;
22446
+ }
22447
+ catch (error) {
22448
+ return error?.code === 'EPERM';
22449
+ }
22450
+ },
22451
+ getProcessCommand: pid => {
22452
+ try {
22453
+ return child_process.execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
22454
+ encoding: 'utf8',
22455
+ stdio: ['ignore', 'pipe', 'ignore'],
22456
+ }).trim();
22457
+ }
22458
+ catch {
22459
+ return '';
22460
+ }
22461
+ },
22462
+ killProcess: pid => process.kill(pid, 'SIGTERM'),
22463
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
22464
+ };
22465
+ function parseJsonFile(file) {
22466
+ if (!fs.existsSync(file))
22467
+ return null;
22468
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
22469
+ }
22470
+ function resolveSessionDirectory(sessionId, sessionsDir) {
22471
+ if (!/^[a-zA-Z0-9._-]+$/.test(sessionId)) {
22472
+ throw new Error(t('dev.session.error.invalidId', { sessionId }));
22473
+ }
22474
+ return path.resolve(sessionsDir || '.ttmg/sessions', sessionId);
22475
+ }
22476
+ function inspectDevSession(sessionId, options = {}, dependencies = {}) {
22477
+ const deps = { ...defaultDependencies$1, ...dependencies };
22478
+ let directory = '';
22479
+ try {
22480
+ directory = resolveSessionDirectory(sessionId, options.sessionsDir);
22481
+ const manifest = parseJsonFile(path.join(directory, 'manifest.json'));
22482
+ if (!manifest) {
22483
+ return {
22484
+ schemaVersion: '1',
22485
+ command: 'dev.session.inspect',
22486
+ status: 'not_found',
22487
+ sessionId,
22488
+ directory,
22489
+ manifest: null,
22490
+ summary: null,
22491
+ eventCount: 0,
22492
+ lastEvent: null,
22493
+ process: { pid: null, running: false },
22494
+ platformWrite: false,
22495
+ errorCode: 'DEV_SESSION_NOT_FOUND',
22496
+ error: t('dev.session.error.notFound', { sessionId }),
22497
+ };
22498
+ }
22499
+ if (manifest.sessionId !== sessionId) {
22500
+ throw new Error(t('dev.session.error.manifestMismatch'));
22501
+ }
22502
+ const summary = parseJsonFile(path.join(directory, 'summary.json'));
22503
+ const eventsFile = path.join(directory, 'events.ndjson');
22504
+ const eventLines = fs.existsSync(eventsFile)
22505
+ ? fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean)
22506
+ : [];
22507
+ const lastEvent = eventLines.length
22508
+ ? JSON.parse(eventLines[eventLines.length - 1])
22509
+ : null;
22510
+ const pid = Number(manifest.process?.pid) || null;
22511
+ return {
22512
+ schemaVersion: '1',
22513
+ command: 'dev.session.inspect',
22514
+ status: 'found',
22515
+ sessionId,
22516
+ directory,
22517
+ manifest,
22518
+ summary,
22519
+ eventCount: eventLines.length,
22520
+ lastEvent,
22521
+ process: {
22522
+ pid,
22523
+ running: pid ? deps.isProcessRunning(pid) : false,
22524
+ },
22525
+ platformWrite: false,
22526
+ errorCode: null,
22527
+ error: null,
22528
+ };
22529
+ }
22530
+ catch (error) {
22531
+ return {
22532
+ schemaVersion: '1',
22533
+ command: 'dev.session.inspect',
22534
+ status: 'failed',
22535
+ sessionId,
22536
+ directory,
22537
+ manifest: null,
22538
+ summary: null,
22539
+ eventCount: 0,
22540
+ lastEvent: null,
22541
+ process: { pid: null, running: false },
22542
+ platformWrite: false,
22543
+ errorCode: 'DEV_SESSION_READ_FAILED',
22544
+ error: error instanceof Error ? error.message : String(error),
22545
+ };
22546
+ }
22547
+ }
22548
+ function listDevSessions(options = {}, dependencies = {}) {
22549
+ const sessionsDirectory = path.resolve(options.sessionsDir || '.ttmg/sessions');
22550
+ const sessionIds = fs.existsSync(sessionsDirectory)
22551
+ ? fs
22552
+ .readdirSync(sessionsDirectory, { withFileTypes: true })
22553
+ .filter(entry => entry.isDirectory())
22554
+ .map(entry => entry.name)
22555
+ .sort()
22556
+ : [];
22557
+ const sessions = sessionIds
22558
+ .map(sessionId => inspectDevSession(sessionId, options, dependencies))
22559
+ .filter(result => result.status === 'found')
22560
+ .map(result => ({
22561
+ sessionId: result.sessionId,
22562
+ directory: result.directory,
22563
+ status: result.summary?.status || result.manifest?.status || 'unknown',
22564
+ clientKey: result.manifest?.project?.clientKey || '',
22565
+ running: result.process.running,
22566
+ eventCount: result.eventCount,
22567
+ }));
22568
+ return {
22569
+ schemaVersion: '1',
22570
+ command: 'dev.session.list',
22571
+ status: 'succeeded',
22572
+ sessionsDirectory,
22573
+ count: sessions.length,
22574
+ sessions,
22575
+ platformWrite: false,
22576
+ errorCode: null,
22577
+ error: null,
22578
+ };
22579
+ }
22580
+ function readDevSessionEvents(sessionId, options = {}) {
22581
+ const inspected = inspectDevSession(sessionId, options);
22582
+ if (inspected.status !== 'found') {
22583
+ return { ...inspected, command: 'dev.session.events' };
22584
+ }
22585
+ const after = Number(options.after || 0);
22586
+ const limit = Number(options.limit || 100);
22587
+ if (!Number.isInteger(after) ||
22588
+ after < 0 ||
22589
+ !Number.isInteger(limit) ||
22590
+ limit < 1 ||
22591
+ limit > 1000) {
22592
+ return {
22593
+ schemaVersion: '1',
22594
+ command: 'dev.session.events',
22595
+ status: 'failed',
22596
+ sessionId,
22597
+ events: [],
22598
+ cursor: { after, next: after, hasMore: false },
22599
+ platformWrite: false,
22600
+ errorCode: 'DEV_SESSION_INVALID_CURSOR',
22601
+ error: t('dev.session.error.invalidCursor'),
22602
+ };
22603
+ }
22604
+ try {
22605
+ const eventsFile = path.join(inspected.directory, 'events.ndjson');
22606
+ const allEvents = fs.existsSync(eventsFile)
22607
+ ? fs
22608
+ .readFileSync(eventsFile, 'utf8')
22609
+ .split('\n')
22610
+ .filter(Boolean)
22611
+ .map(line => JSON.parse(line))
22612
+ .filter(event => Number(event.seq) > after)
22613
+ : [];
22614
+ const events = allEvents.slice(0, limit);
22615
+ return {
22616
+ schemaVersion: '1',
22617
+ command: 'dev.session.events',
22618
+ status: 'succeeded',
22619
+ sessionId,
22620
+ events,
22621
+ cursor: {
22622
+ after,
22623
+ next: events.length ? Number(events[events.length - 1].seq) : after,
22624
+ hasMore: allEvents.length > events.length,
22625
+ },
22626
+ platformWrite: false,
22627
+ errorCode: null,
22628
+ error: null,
22629
+ };
22630
+ }
22631
+ catch (error) {
22632
+ return {
22633
+ schemaVersion: '1',
22634
+ command: 'dev.session.events',
22635
+ status: 'failed',
22636
+ sessionId,
22637
+ events: [],
22638
+ cursor: { after, next: after, hasMore: false },
22639
+ platformWrite: false,
22640
+ errorCode: 'DEV_SESSION_EVENTS_READ_FAILED',
22641
+ error: error instanceof Error ? error.message : String(error),
22642
+ };
22643
+ }
22644
+ }
22645
+ async function stopDevSession(sessionId, options = {}, dependencies = {}) {
22646
+ const deps = { ...defaultDependencies$1, ...dependencies };
22647
+ const inspected = inspectDevSession(sessionId, options, deps);
22648
+ if (inspected.status !== 'found') {
22649
+ return { ...inspected, command: 'dev.session.stop' };
22650
+ }
22651
+ const pid = inspected.process.pid;
22652
+ if (!pid || !inspected.process.running) {
22653
+ return {
22654
+ schemaVersion: '1',
22655
+ command: 'dev.session.stop',
22656
+ status: 'not_running',
22657
+ sessionId,
22658
+ pid,
22659
+ platformWrite: false,
22660
+ errorCode: null,
22661
+ error: null,
22662
+ };
22663
+ }
22664
+ const processCommand = deps.getProcessCommand(pid);
22665
+ if (!/(^|\s|\/)ttmg(\s|$).*\bdev\b|dist\/index\.js\s+dev\b/.test(processCommand)) {
22666
+ return {
22667
+ schemaVersion: '1',
22668
+ command: 'dev.session.stop',
22669
+ status: 'failed',
22670
+ sessionId,
22671
+ pid,
22672
+ platformWrite: false,
22673
+ errorCode: 'DEV_SESSION_PROCESS_UNVERIFIED',
22674
+ error: t('dev.session.error.unverifiedProcess'),
22675
+ };
22676
+ }
22677
+ try {
22678
+ deps.killProcess(pid);
22679
+ for (let attempt = 0; attempt < 25; attempt += 1) {
22680
+ if (!deps.isProcessRunning(pid)) {
22681
+ return {
22682
+ schemaVersion: '1',
22683
+ command: 'dev.session.stop',
22684
+ status: 'stopped',
22685
+ sessionId,
22686
+ pid,
22687
+ platformWrite: false,
22688
+ errorCode: null,
22689
+ error: null,
22690
+ };
22691
+ }
22692
+ await deps.sleep(100);
22693
+ }
22694
+ return {
22695
+ schemaVersion: '1',
22696
+ command: 'dev.session.stop',
22697
+ status: 'stopping',
22698
+ sessionId,
22699
+ pid,
22700
+ platformWrite: false,
22701
+ errorCode: null,
22702
+ error: null,
22703
+ };
22704
+ }
22705
+ catch (error) {
22706
+ return {
22707
+ schemaVersion: '1',
22708
+ command: 'dev.session.stop',
22709
+ status: 'failed',
22710
+ sessionId,
22711
+ pid,
22712
+ platformWrite: false,
22713
+ errorCode: 'DEV_SESSION_STOP_FAILED',
22714
+ error: error instanceof Error ? error.message : String(error),
22715
+ };
22716
+ }
22717
+ }
22718
+ function printResult(result, format) {
22719
+ if ((format || 'text').toLowerCase() === 'json') {
22720
+ console.log(JSON.stringify(result));
22721
+ }
22722
+ else {
22723
+ console.log(JSON.stringify(result, null, 2));
22724
+ }
22725
+ if (result.status === 'failed' || result.status === 'not_found') {
22726
+ process.exitCode = 1;
22727
+ }
22728
+ return result;
22729
+ }
22730
+ async function devSessionInspectCommand(sessionId, options = {}) {
22731
+ return printResult(inspectDevSession(sessionId, options), options.format);
22732
+ }
22733
+ async function devSessionListCommand(options = {}) {
22734
+ return printResult(listDevSessions(options), options.format);
22735
+ }
22736
+ async function devSessionEventsCommand(sessionId, options = {}) {
22737
+ return printResult(readDevSessionEvents(sessionId, options), options.format);
22738
+ }
22739
+ async function devSessionStopCommand(sessionId, options = {}) {
22740
+ return printResult(await stopDevSession(sessionId, options), options.format);
22741
+ }
22742
+
22743
+ const defaultDependencies = {
22744
+ nodeVersion: process.version,
22745
+ getLocalIPs,
22746
+ authenticated: () => Boolean(getCurrentUser()?.cookie),
22747
+ proxyEnvironment: () => ({
22748
+ http: Boolean(process.env.HTTP_PROXY || process.env.http_proxy),
22749
+ https: Boolean(process.env.HTTPS_PROXY || process.env.https_proxy),
22750
+ noProxy: Boolean(process.env.NO_PROXY || process.env.no_proxy),
22751
+ }),
22752
+ };
22753
+ function getDoctorReport(options = {}, dependencies = {}) {
22754
+ const deps = { ...defaultDependencies, ...dependencies };
22755
+ const directory = path.resolve(process.cwd(), options.dir || '.');
22756
+ const directoryExists = fs.existsSync(directory) && fs.statSync(directory).isDirectory();
22757
+ const projectDetected = directoryExists &&
22758
+ NATIVE_GAME_ENTRY_FILES.some(file => fs.existsSync(path.join(directory, file)));
22759
+ const hosts = deps.getLocalIPs();
22760
+ const nodeSupported = semver.satisfies(semver.coerce(deps.nodeVersion) || '0.0.0', pkg.engines.node);
22761
+ const authenticated = deps.authenticated();
22762
+ const diagnostics = [];
22763
+ if (!nodeSupported) {
22764
+ diagnostics.push({
22765
+ code: 'DOCTOR_NODE_VERSION_UNSUPPORTED',
22766
+ severity: 'error',
22767
+ message: `Node.js ${deps.nodeVersion} does not satisfy ${pkg.engines.node}.`,
22768
+ });
22769
+ }
22770
+ if (!projectDetected) {
22771
+ diagnostics.push({
22772
+ code: directoryExists
22773
+ ? 'DOCTOR_PROJECT_NOT_FOUND'
22774
+ : 'DOCTOR_DIRECTORY_NOT_FOUND',
22775
+ severity: 'warning',
22776
+ message: directoryExists
22777
+ ? 'No Mini Game project entry file was found in the target directory.'
22778
+ : 'The target directory does not exist.',
22779
+ });
22780
+ }
22781
+ if (hosts.length === 0) {
22782
+ diagnostics.push({
22783
+ code: 'DOCTOR_LAN_ADDRESS_NOT_FOUND',
22784
+ severity: 'warning',
22785
+ message: 'No physical LAN IPv4 address was found; device debugging will fall back to loopback and require a manual network solution.',
22786
+ });
22787
+ }
22788
+ if (!authenticated) {
22789
+ diagnostics.push({
22790
+ code: 'DOCTOR_AUTH_NOT_AVAILABLE',
22791
+ severity: 'warning',
22792
+ message: 'No reusable local developer session was found.',
22793
+ });
22794
+ }
22795
+ const errors = diagnostics.filter(item => item.severity === 'error').length;
22796
+ const warnings = diagnostics.filter(item => item.severity === 'warning').length;
22797
+ return {
22798
+ schemaVersion: '1',
22799
+ command: 'doctor',
22800
+ status: errors > 0 ? 'failed' : warnings > 0 ? 'warning' : 'passed',
22801
+ cliVersion: pkg.version,
22802
+ environment: {
22803
+ nodeVersion: deps.nodeVersion,
22804
+ requiredNodeRange: pkg.engines.node,
22805
+ nodeSupported,
22806
+ proxyConfigured: deps.proxyEnvironment(),
22807
+ },
22808
+ project: { directory, directoryExists, detected: projectDetected },
22809
+ auth: { authenticated, remotelyVerified: false },
22810
+ network: {
22811
+ mode: hosts.length > 0 ? 'lan' : 'loopback',
22812
+ selectedHost: hosts[0] || '127.0.0.1',
22813
+ hosts,
22814
+ },
22815
+ summary: { errors, warnings },
22816
+ diagnostics,
22817
+ platformWrite: false,
22818
+ errorCode: errors > 0 ? 'DOCTOR_FAILED' : null,
22819
+ error: null,
22820
+ };
22821
+ }
22822
+ async function doctor(options = {}) {
22823
+ const report = getDoctorReport(options);
22824
+ const format = (options.format || 'text').toLowerCase();
22825
+ if (format === 'json')
22826
+ console.log(JSON.stringify(report));
22827
+ else
22828
+ console.log(JSON.stringify(report, null, 2));
22829
+ const failOnWarning = (options.failOn || 'error').toLowerCase() === 'warning';
22830
+ if (report.status === 'failed' ||
22831
+ (failOnWarning && report.status === 'warning')) {
22832
+ process.exitCode = 1;
22833
+ }
22834
+ return report;
20820
22835
  }
20821
22836
 
20822
22837
  var index = /*#__PURE__*/Object.freeze({
20823
22838
  __proto__: null,
20824
- dev: dev
22839
+ build: structuredBuildCommand,
22840
+ check: checkCommand,
22841
+ checkAi: aiCheckCommand,
22842
+ dev: dev,
22843
+ devSessionEventsCommand: devSessionEventsCommand,
22844
+ devSessionInspectCommand: devSessionInspectCommand,
22845
+ devSessionListCommand: devSessionListCommand,
22846
+ devSessionStopCommand: devSessionStopCommand,
22847
+ doctor: doctor
20825
22848
  });
20826
22849
  //# sourceMappingURL=index.js.map