@ttmg/cli 0.4.6-vibe-beta.2 → 0.4.6-vibe-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ var fs = require('fs');
13
13
  var handlebars = require('handlebars');
14
14
  var esbuild = require('esbuild');
15
15
  var worker_threads = require('worker_threads');
16
+ var child_process = require('child_process');
17
+ var http = require('http');
16
18
  var jsdom = require('jsdom');
17
19
  var prettier = require('prettier');
18
20
  var express = require('express');
@@ -29,7 +31,6 @@ var require$$0 = require('util');
29
31
  var require$$0$1 = require('events');
30
32
  var require$$0$2 = require('buffer');
31
33
  var require$$1$2 = require('stream');
32
- var http = require('http');
33
34
  var https = require('https');
34
35
  var dns = require('dns');
35
36
  var boxen = require('boxen');
@@ -46,7 +47,6 @@ var fs$1 = require('node:fs');
46
47
  var path$1 = require('node:path');
47
48
  var zlib = require('zlib');
48
49
  var promises = require('fs/promises');
49
- var child_process = require('child_process');
50
50
 
51
51
  function _interopNamespaceDefault(e) {
52
52
  var n = Object.create(null);
@@ -68,9 +68,9 @@ function _interopNamespaceDefault(e) {
68
68
  var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
69
69
  var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
70
70
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
71
+ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
71
72
  var net__namespace = /*#__PURE__*/_interopNamespaceDefault(net);
72
73
  var require$$1__namespace = /*#__PURE__*/_interopNamespaceDefault(require$$1$1);
73
- var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
74
74
  var dns__namespace = /*#__PURE__*/_interopNamespaceDefault(dns);
75
75
  var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
76
76
  var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
@@ -7221,6 +7221,7 @@ const messages = {
7221
7221
  'cli.command.dev.noOpen': 'Do not open the DevTool page automatically',
7222
7222
  'cli.command.dev.evidenceDir': 'Session evidence output directory',
7223
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.',
7224
7225
  'cli.command.dev.session.desc': 'Inspect and stop local DevTool sessions',
7225
7226
  'cli.command.dev.session.inspect': 'Inspect one local DevTool session',
7226
7227
  'cli.command.dev.session.list': 'List local DevTool sessions',
@@ -7253,6 +7254,8 @@ const messages = {
7253
7254
  'cli.command.upload.note': 'Note for the upload',
7254
7255
  'cli.command.upload.dir': 'Game project directory (default: current directory)',
7255
7256
  'cli.command.upload.preview': 'Wait for platform processing and output the preview payload',
7257
+ 'cli.command.upload.servePreviewPage': 'Serve a temporary local preview page with test-account and preview QR codes',
7258
+ 'cli.command.upload.noOpen': 'Do not open the local preview page in the default browser',
7256
7259
  'cli.command.upload.format': 'Output format: text | json | ndjson',
7257
7260
  'cli.command.testAccount.desc': 'Manage Mini Game test-account access',
7258
7261
  'cli.command.testAccount.add.desc': 'Generate the authorization entry for adding a TikTok test account',
@@ -7386,6 +7389,17 @@ const messages = {
7386
7389
  'upload.spinner.processing': 'Waiting for platform processing (status: {status})...',
7387
7390
  'upload.success': 'Upload success! View version at: {url}',
7388
7391
  'upload.previewSuccess': 'Preview version is ready: {url}',
7392
+ 'upload.previewPageReady': 'Local preview page: {url}',
7393
+ 'upload.previewPageHeading': 'Preview page',
7394
+ 'upload.previewPageOpenInBrowser': 'Open in browser',
7395
+ 'upload.previewPageAccessScope': 'Access scope',
7396
+ 'upload.previewPageSameMachine': 'Same machine only',
7397
+ 'upload.previewPageTerminalMode': 'Terminal fallback',
7398
+ 'upload.previewPageClickableLink': 'Clickable link',
7399
+ 'upload.previewPageExpiresAt': 'Expires at',
7400
+ 'upload.previewPageOpening': 'Opening the preview page in your browser...',
7401
+ 'upload.previewPageOpenFailed': 'Could not open the browser automatically. Use the link above.',
7402
+ 'upload.previewPageFailed': 'The preview was uploaded, but the local preview page failed: {error}',
7389
7403
  'upload.fail': 'Upload failed: {error}',
7390
7404
  'upload.error.noClientKey': 'Client Key is required for upload. Use --client-key <key>',
7391
7405
  'upload.error.notLoggedIn': 'Not logged in. Please run `ttmg login` first.',
@@ -7400,6 +7414,7 @@ const messages = {
7400
7414
  'upload.error.assetTimeout': 'Timed out waiting for platform asset processing.',
7401
7415
  'upload.error.previewTimeout': 'Timed out waiting for the preview URL.',
7402
7416
  'upload.error.previewQueryFailed': 'Failed to query the preview URL from the platform.',
7417
+ 'upload.error.previewPageRequiresPreview': '--serve-preview-page requires --preview.',
7403
7418
  'testAccount.success': 'Test-account authorization entry generated.',
7404
7419
  'testAccount.project': 'Project: {name} ({clientKey})',
7405
7420
  'testAccount.authorizationUrl': 'Authorization URL: {url}',
@@ -7441,6 +7456,7 @@ const messages = {
7441
7456
  'cli.command.dev.noOpen': '不自动打开 DevTool 页面',
7442
7457
  'cli.command.dev.evidenceDir': '会话证据输出目录',
7443
7458
  'cli.command.dev.timeout': '在指定秒数后停止结构化调试会话',
7459
+ 'cli.command.dev.outputConflict': 'Dev 输出模式不能同时使用:请用 --format json 输出一份 manifest,或用 --events ndjson 输出生命周期事件流。',
7444
7460
  'cli.command.dev.session.desc': '查询和停止本地 DevTool 会话',
7445
7461
  'cli.command.dev.session.inspect': '查询一个本地 DevTool 会话',
7446
7462
  'cli.command.dev.session.list': '列出本地 DevTool 会话',
@@ -7473,6 +7489,8 @@ const messages = {
7473
7489
  'cli.command.upload.note': '上传备注',
7474
7490
  'cli.command.upload.dir': '小游戏项目根目录(默认为当前目录)',
7475
7491
  'cli.command.upload.preview': '等待平台处理并输出预览二维码原始内容',
7492
+ 'cli.command.upload.servePreviewPage': '启动包含测试账号与预览二维码的临时本地页面',
7493
+ 'cli.command.upload.noOpen': '不使用默认浏览器打开本地预览页面',
7476
7494
  'cli.command.upload.format': '输出格式:text | json | ndjson',
7477
7495
  'cli.command.testAccount.desc': '管理小游戏测试账号访问',
7478
7496
  'cli.command.testAccount.add.desc': '生成添加 TikTok 测试账号的授权入口',
@@ -7606,6 +7624,17 @@ const messages = {
7606
7624
  'upload.spinner.processing': '正在等待平台处理(状态:{status})...',
7607
7625
  'upload.success': '上传成功!可在以下地址查看版本:{url}',
7608
7626
  'upload.previewSuccess': '预览版本已就绪:{url}',
7627
+ 'upload.previewPageReady': '本地预览页面:{url}',
7628
+ 'upload.previewPageHeading': '预览页面',
7629
+ 'upload.previewPageOpenInBrowser': '浏览器打开',
7630
+ 'upload.previewPageAccessScope': '访问范围',
7631
+ 'upload.previewPageSameMachine': '仅当前设备',
7632
+ 'upload.previewPageTerminalMode': '终端降级',
7633
+ 'upload.previewPageClickableLink': '可点击链接',
7634
+ 'upload.previewPageExpiresAt': '过期时间',
7635
+ 'upload.previewPageOpening': '正在使用浏览器打开预览页面...',
7636
+ 'upload.previewPageOpenFailed': '无法自动打开浏览器,请使用上方链接。',
7637
+ 'upload.previewPageFailed': '预览版本已上传,但本地预览页面启动失败:{error}',
7609
7638
  'upload.fail': '上传失败:{error}',
7610
7639
  'upload.error.noClientKey': '上传必须提供 Client Key,请使用 --client-key <key> 参数',
7611
7640
  'upload.error.notLoggedIn': '未登录,请先执行 `ttmg login` 登录开发者平台账号。',
@@ -7620,6 +7649,7 @@ const messages = {
7620
7649
  'upload.error.assetTimeout': '等待平台处理上传版本超时。',
7621
7650
  'upload.error.previewTimeout': '等待平台生成预览地址超时。',
7622
7651
  'upload.error.previewQueryFailed': '查询平台预览地址失败。',
7652
+ 'upload.error.previewPageRequiresPreview': '--serve-preview-page 必须与 --preview 一起使用。',
7623
7653
  'testAccount.success': '测试账号授权入口已生成。',
7624
7654
  'testAccount.project': '项目:{name}({clientKey})',
7625
7655
  'testAccount.authorizationUrl': '授权地址:{url}',
@@ -8226,6 +8256,9 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
8226
8256
  const defaultIgnores = [
8227
8257
  'node_modules/**',
8228
8258
  '.git/**',
8259
+ '.agent/**',
8260
+ '.claude/**',
8261
+ '.trae/**',
8229
8262
  '.DS_Store',
8230
8263
  ttmgPack.TTMG_TEMP_DIR,
8231
8264
  `${ttmgPack.TTMG_TEMP_DIR}/**`,
@@ -8245,495 +8278,159 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
8245
8278
  });
8246
8279
  };
8247
8280
 
8248
- const DEFAULT_POLL_INTERVAL_MS = 3000;
8249
- const DEFAULT_POLL_TIMEOUT_MS = 10 * 60 * 1000;
8250
- const defaultDependencies$6 = {
8251
- getCurrentUser,
8252
- fetchGameInfo,
8253
- zipDirectory: targetDir => zipCwdToBuffer([], targetDir),
8254
- uploadGameToPlatform,
8255
- fetchGameAssets,
8256
- fetchGameAssetPreviewUrl,
8257
- sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8258
- now: () => Date.now(),
8259
- pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
8260
- pollTimeoutMs: DEFAULT_POLL_TIMEOUT_MS,
8261
- };
8262
- function normalizeOutputFormat(format) {
8263
- const normalized = (format || 'text').toLowerCase();
8264
- if (normalized !== 'text' &&
8265
- normalized !== 'json' &&
8266
- normalized !== 'ndjson') {
8267
- throw new Error(t('upload.error.invalidFormat', { format: normalized }));
8268
- }
8269
- return normalized;
8270
- }
8271
- function getAssetStatusRaw(asset) {
8272
- return asset?.cdn_deploy_status ?? asset?.status ?? null;
8273
- }
8274
- function normalizeUploadPlatformStatus(rawStatus) {
8275
- if (rawStatus === null || rawStatus === undefined || rawStatus === '') {
8276
- return 'waiting_for_asset';
8277
- }
8278
- const numericStatus = Number(rawStatus);
8279
- if (Number.isInteger(numericStatus)) {
8280
- if (numericStatus === 0)
8281
- return 'unknown';
8282
- if (numericStatus === 1)
8283
- return 'pending';
8284
- if (numericStatus === 2)
8285
- return 'success';
8286
- if (numericStatus === 3)
8287
- return 'failed';
8288
- if (numericStatus === 4)
8289
- return 'processing';
8290
- return 'unknown';
8291
- }
8292
- const normalized = String(rawStatus).trim().toLowerCase();
8293
- const statusName = normalized.replace(/^deploy_status_/, '');
8294
- if (['ready', 'success', 'succeeded', 'available'].includes(statusName)) {
8295
- return 'success';
8296
- }
8297
- if (statusName.includes('fail') ||
8298
- statusName.includes('error') ||
8299
- statusName.includes('reject')) {
8300
- return 'failed';
8301
- }
8302
- if (statusName.includes('process') ||
8303
- statusName.includes('upload') ||
8304
- statusName.includes('deploy')) {
8305
- return 'processing';
8306
- }
8307
- if (statusName.includes('pending') ||
8308
- statusName.includes('queue') ||
8309
- statusName.includes('wait')) {
8310
- return 'pending';
8311
- }
8312
- return 'unknown';
8313
- }
8314
- function getAssetState(asset) {
8315
- const platformStatus = normalizeUploadPlatformStatus(getAssetStatusRaw(asset));
8316
- if (platformStatus === 'success') {
8317
- return 'ready';
8318
- }
8319
- if (platformStatus === 'failed') {
8320
- return 'failed';
8321
- }
8322
- return 'pending';
8281
+ var server = {};
8282
+
8283
+ var canPromise;
8284
+ var hasRequiredCanPromise;
8285
+
8286
+ function requireCanPromise () {
8287
+ if (hasRequiredCanPromise) return canPromise;
8288
+ hasRequiredCanPromise = 1;
8289
+ // can-promise has a crash in some versions of react native that dont have
8290
+ // standard global objects
8291
+ // https://github.com/soldair/node-qrcode/issues/157
8292
+
8293
+ canPromise = function () {
8294
+ return typeof Promise === 'function' && Promise.prototype && Promise.prototype.then
8295
+ };
8296
+ return canPromise;
8323
8297
  }
8324
- function extractAssets(data) {
8325
- if (!data) {
8326
- return [];
8327
- }
8328
- return [
8329
- data.preview_assets,
8330
- data.in_review_assets,
8331
- data.live_assets,
8332
- data.assets,
8333
- ].flatMap(group => (Array.isArray(group) ? group : []));
8298
+
8299
+ var qrcode = {};
8300
+
8301
+ var utils$1 = {};
8302
+
8303
+ var hasRequiredUtils$1;
8304
+
8305
+ function requireUtils$1 () {
8306
+ if (hasRequiredUtils$1) return utils$1;
8307
+ hasRequiredUtils$1 = 1;
8308
+ let toSJISFunction;
8309
+ const CODEWORDS_COUNT = [
8310
+ 0, // Not used
8311
+ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346,
8312
+ 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085,
8313
+ 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185,
8314
+ 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706
8315
+ ];
8316
+
8317
+ /**
8318
+ * Returns the QR Code size for the specified version
8319
+ *
8320
+ * @param {Number} version QR Code version
8321
+ * @return {Number} size of QR code
8322
+ */
8323
+ utils$1.getSymbolSize = function getSymbolSize (version) {
8324
+ if (!version) throw new Error('"version" cannot be null or undefined')
8325
+ if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40')
8326
+ return version * 4 + 17
8327
+ };
8328
+
8329
+ /**
8330
+ * Returns the total number of codewords used to store data and EC information.
8331
+ *
8332
+ * @param {Number} version QR Code version
8333
+ * @return {Number} Data length in bits
8334
+ */
8335
+ utils$1.getSymbolTotalCodewords = function getSymbolTotalCodewords (version) {
8336
+ return CODEWORDS_COUNT[version]
8337
+ };
8338
+
8339
+ /**
8340
+ * Encode data with Bose-Chaudhuri-Hocquenghem
8341
+ *
8342
+ * @param {Number} data Value to encode
8343
+ * @return {Number} Encoded value
8344
+ */
8345
+ utils$1.getBCHDigit = function (data) {
8346
+ let digit = 0;
8347
+
8348
+ while (data !== 0) {
8349
+ digit++;
8350
+ data >>>= 1;
8351
+ }
8352
+
8353
+ return digit
8354
+ };
8355
+
8356
+ utils$1.setToSJISFunction = function setToSJISFunction (f) {
8357
+ if (typeof f !== 'function') {
8358
+ throw new Error('"toSJISFunc" is not a valid function.')
8359
+ }
8360
+
8361
+ toSJISFunction = f;
8362
+ };
8363
+
8364
+ utils$1.isKanjiModeEnabled = function () {
8365
+ return typeof toSJISFunction !== 'undefined'
8366
+ };
8367
+
8368
+ utils$1.toSJIS = function toSJIS (kanji) {
8369
+ return toSJISFunction(kanji)
8370
+ };
8371
+ return utils$1;
8334
8372
  }
8335
- function extractPreviewUrl(data) {
8336
- return typeof data?.preview_url === 'string' ? data.preview_url : '';
8337
- }
8338
- function createEvent(base, status, progress, overrides = {}) {
8339
- return {
8340
- schemaVersion: '1',
8341
- operationId: base.operationId,
8342
- ts: Date.now(),
8343
- clientKey: base.clientKey,
8344
- versionId: '',
8345
- status,
8346
- progress,
8347
- previewUrl: '',
8348
- qrPayload: '',
8349
- platformUrl: '',
8350
- error: null,
8351
- errorCode: null,
8352
- platformWrite: false,
8353
- ...overrides,
8354
- };
8355
- }
8356
- async function waitForAssetReady({ assetId, miniGameId, deadline, eventBase, onEvent, deps, }) {
8357
- let lastPlatformStatusRaw = null;
8358
- let hasEmittedPlatformStatus = false;
8359
- while (deps.now() < deadline) {
8360
- const response = await deps.fetchGameAssets({ miniGameId });
8361
- if (response.error) {
8362
- throw new Error(response.error?.StatusMessage ||
8363
- response.error?.message ||
8364
- t('upload.error.assetQueryFailed'));
8365
- }
8366
- const asset = extractAssets(response.data).find(item => String(item.asset_id || '') === assetId) || null;
8367
- const platformStatusRaw = getAssetStatusRaw(asset);
8368
- const platformStatus = normalizeUploadPlatformStatus(platformStatusRaw);
8369
- if (!hasEmittedPlatformStatus ||
8370
- platformStatusRaw !== lastPlatformStatusRaw) {
8371
- hasEmittedPlatformStatus = true;
8372
- lastPlatformStatusRaw = platformStatusRaw;
8373
- onEvent(createEvent(eventBase, 'processing', 0.7, {
8374
- versionId: assetId,
8375
- platformStatus,
8376
- ...(platformStatusRaw === null ? {} : { platformStatusRaw }),
8377
- platformWrite: true,
8378
- }));
8379
- }
8380
- const assetState = getAssetState(asset);
8381
- if (assetState === 'ready') {
8382
- return asset;
8383
- }
8384
- if (assetState === 'failed') {
8385
- throw new Error(t('upload.error.assetFailed', { status: platformStatus }));
8386
- }
8387
- await deps.sleep(deps.pollIntervalMs);
8388
- }
8389
- throw new Error(t('upload.error.assetTimeout'));
8390
- }
8391
- async function waitForPreviewUrl({ assetId, deadline, deps, }) {
8392
- while (deps.now() < deadline) {
8393
- const response = await deps.fetchGameAssetPreviewUrl({ assetId });
8394
- if (response.error) {
8395
- throw new Error(response.error?.StatusMessage ||
8396
- response.error?.message ||
8397
- t('upload.error.previewQueryFailed'));
8398
- }
8399
- const previewUrl = extractPreviewUrl(response.data);
8400
- if (previewUrl) {
8401
- return previewUrl;
8402
- }
8403
- await deps.sleep(deps.pollIntervalMs);
8404
- }
8405
- throw new Error(t('upload.error.previewTimeout'));
8406
- }
8407
- async function executeUpload(options, dependencies = {}) {
8408
- const deps = { ...defaultDependencies$6, ...dependencies };
8409
- const clientKey = options.clientKey?.trim();
8410
- const note = options.note || '--';
8411
- const eventBase = { operationId: options.operationId, clientKey };
8412
- const emit = options.onEvent;
8413
- emit(createEvent(eventBase, 'precheck', 0.05));
8414
- const user = deps.getCurrentUser();
8415
- if (!user || !user.cookie) {
8416
- throw new Error(t('upload.error.notLoggedIn'));
8417
- }
8418
- if (!clientKey) {
8419
- throw new Error(t('upload.error.noClientKey'));
8420
- }
8421
- const infoResponse = await deps.fetchGameInfo(clientKey);
8422
- if (infoResponse.error) {
8423
- throw new Error(infoResponse.error.errorMsg || t('upload.error.gameInfoFailed'));
8424
- }
8425
- const appId = infoResponse.data?.app_id || '';
8426
- const projectName = infoResponse.data?.name || 'game';
8427
- if (!appId) {
8428
- throw new Error(t('upload.error.noAppId'));
8429
- }
8430
- const targetDir = options.dir
8431
- ? path.resolve(process.cwd(), options.dir)
8432
- : process.cwd();
8433
- if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
8434
- throw new Error(t('upload.error.dirNotExist', { dir: targetDir }));
8435
- }
8436
- emit(createEvent(eventBase, 'packaging', 0.2, { appId, projectName }));
8437
- const zipBuffer = await deps.zipDirectory(targetDir);
8438
- const packageSizeBytes = zipBuffer.length;
8439
- emit(createEvent(eventBase, 'uploading', 0.4, {
8440
- appId,
8441
- projectName,
8442
- packageSizeBytes,
8443
- }));
8444
- const uploadResponse = await deps.uploadGameToPlatform({
8445
- data: zipBuffer.buffer.slice(zipBuffer.byteOffset, zipBuffer.byteOffset + zipBuffer.byteLength),
8446
- name: projectName,
8447
- clientKey,
8448
- note,
8449
- appId,
8450
- });
8451
- if (uploadResponse.error) {
8452
- throw new Error(uploadResponse.error.message || t('upload.error.platformUploadFailed'));
8453
- }
8454
- const assetId = String(uploadResponse.data?.asset_id || '');
8455
- const platformUrl = uploadResponse.data?.detail || '';
8456
- if (!assetId) {
8457
- throw new Error(t('upload.error.missingAssetId'));
8458
- }
8459
- if (!options.preview) {
8460
- const event = createEvent(eventBase, 'ready', 1, {
8461
- appId,
8462
- projectName,
8463
- packageSizeBytes,
8464
- versionId: assetId,
8465
- platformUrl,
8466
- platformWrite: true,
8467
- });
8468
- emit(event);
8469
- return event;
8470
- }
8471
- const deadline = deps.now() + deps.pollTimeoutMs;
8472
- await waitForAssetReady({
8473
- assetId,
8474
- miniGameId: appId,
8475
- deadline,
8476
- eventBase,
8477
- onEvent: emit,
8478
- deps,
8479
- });
8480
- emit(createEvent(eventBase, 'processing', 0.85, {
8481
- appId,
8482
- projectName,
8483
- packageSizeBytes,
8484
- versionId: assetId,
8485
- platformStatus: 'resolving_preview',
8486
- platformUrl,
8487
- platformWrite: true,
8488
- }));
8489
- const previewUrl = await waitForPreviewUrl({ assetId, deadline, deps });
8490
- const event = createEvent(eventBase, 'ready', 1, {
8491
- appId,
8492
- projectName,
8493
- packageSizeBytes,
8494
- versionId: assetId,
8495
- previewUrl,
8496
- qrPayload: previewUrl,
8497
- platformUrl,
8498
- platformStatus: 'ready',
8499
- platformWrite: true,
8500
- });
8501
- emit(event);
8502
- return event;
8503
- }
8504
- function getHumanEventText(event) {
8505
- if (event.status === 'precheck') {
8506
- return t('upload.spinner.fetchInfo', { clientKey: event.clientKey });
8507
- }
8508
- if (event.status === 'packaging') {
8509
- return t('upload.spinner.zipping', { dir: '' });
8510
- }
8511
- if (event.status === 'uploading') {
8512
- return t('upload.spinner.uploading', {
8513
- size: ((event.packageSizeBytes || 0) / 1024 / 1024).toFixed(2),
8514
- });
8515
- }
8516
- if (event.status === 'processing') {
8517
- return t('upload.spinner.processing', {
8518
- status: event.platformStatus || 'pending',
8519
- });
8520
- }
8521
- return '';
8522
- }
8523
- async function upload(options) {
8524
- const format = normalizeOutputFormat(options.format);
8525
- const operationId = `upload_${crypto.randomUUID()}`;
8526
- const ora = await import('ora');
8527
- const spinner = format === 'text'
8528
- ? ora
8529
- .default({ spinner: 'dots', text: t('upload.spinner.start') })
8530
- .start()
8531
- : null;
8532
- let lastEvent = null;
8533
- const onEvent = (event) => {
8534
- lastEvent = event;
8535
- if (format === 'ndjson') {
8536
- console.log(JSON.stringify(event));
8537
- return;
8538
- }
8539
- const nextText = getHumanEventText(event);
8540
- if (nextText && spinner) {
8541
- spinner.text = chalk.cyan.bold(nextText);
8542
- }
8543
- };
8544
- try {
8545
- const result = await executeUpload({ ...options, operationId, onEvent });
8546
- if (format === 'json') {
8547
- console.log(JSON.stringify(result));
8548
- }
8549
- else if (format === 'text' && spinner) {
8550
- if (result.previewUrl) {
8551
- spinner.succeed(chalk.green.bold(t('upload.previewSuccess', { url: result.previewUrl })));
8552
- }
8553
- else {
8554
- spinner.succeed(chalk.green.bold(t('upload.success', { url: result.platformUrl })));
8555
- }
8556
- }
8557
- return result;
8558
- }
8559
- catch (error) {
8560
- const message = error?.message || String(error);
8561
- const failedEvent = createEvent({ operationId, clientKey: options.clientKey || '' }, 'failed', lastEvent?.progress || 0, {
8562
- appId: lastEvent?.appId,
8563
- projectName: lastEvent?.projectName,
8564
- packageSizeBytes: lastEvent?.packageSizeBytes,
8565
- versionId: lastEvent?.versionId || '',
8566
- platformUrl: lastEvent?.platformUrl || '',
8567
- platformStatus: lastEvent?.platformStatus,
8568
- platformStatusRaw: lastEvent?.platformStatusRaw,
8569
- platformWrite: lastEvent?.platformWrite || false,
8570
- errorCode: 'UPLOAD_FAILED',
8571
- error: message,
8572
- });
8573
- if (format === 'json' || format === 'ndjson') {
8574
- console.log(JSON.stringify(failedEvent));
8575
- }
8576
- else {
8577
- spinner?.fail(chalk.red.bold(t('upload.fail', { error: message })));
8578
- }
8579
- process.exitCode = 1;
8580
- return failedEvent;
8581
- }
8582
- }
8583
-
8584
- var server = {};
8585
-
8586
- var canPromise;
8587
- var hasRequiredCanPromise;
8588
-
8589
- function requireCanPromise () {
8590
- if (hasRequiredCanPromise) return canPromise;
8591
- hasRequiredCanPromise = 1;
8592
- // can-promise has a crash in some versions of react native that dont have
8593
- // standard global objects
8594
- // https://github.com/soldair/node-qrcode/issues/157
8595
-
8596
- canPromise = function () {
8597
- return typeof Promise === 'function' && Promise.prototype && Promise.prototype.then
8598
- };
8599
- return canPromise;
8600
- }
8601
-
8602
- var qrcode = {};
8603
-
8604
- var utils$1 = {};
8605
-
8606
- var hasRequiredUtils$1;
8607
-
8608
- function requireUtils$1 () {
8609
- if (hasRequiredUtils$1) return utils$1;
8610
- hasRequiredUtils$1 = 1;
8611
- let toSJISFunction;
8612
- const CODEWORDS_COUNT = [
8613
- 0, // Not used
8614
- 26, 44, 70, 100, 134, 172, 196, 242, 292, 346,
8615
- 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085,
8616
- 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185,
8617
- 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706
8618
- ];
8619
-
8620
- /**
8621
- * Returns the QR Code size for the specified version
8622
- *
8623
- * @param {Number} version QR Code version
8624
- * @return {Number} size of QR code
8625
- */
8626
- utils$1.getSymbolSize = function getSymbolSize (version) {
8627
- if (!version) throw new Error('"version" cannot be null or undefined')
8628
- if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40')
8629
- return version * 4 + 17
8630
- };
8631
-
8632
- /**
8633
- * Returns the total number of codewords used to store data and EC information.
8634
- *
8635
- * @param {Number} version QR Code version
8636
- * @return {Number} Data length in bits
8637
- */
8638
- utils$1.getSymbolTotalCodewords = function getSymbolTotalCodewords (version) {
8639
- return CODEWORDS_COUNT[version]
8640
- };
8641
-
8642
- /**
8643
- * Encode data with Bose-Chaudhuri-Hocquenghem
8644
- *
8645
- * @param {Number} data Value to encode
8646
- * @return {Number} Encoded value
8647
- */
8648
- utils$1.getBCHDigit = function (data) {
8649
- let digit = 0;
8650
-
8651
- while (data !== 0) {
8652
- digit++;
8653
- data >>>= 1;
8654
- }
8655
-
8656
- return digit
8657
- };
8658
-
8659
- utils$1.setToSJISFunction = function setToSJISFunction (f) {
8660
- if (typeof f !== 'function') {
8661
- throw new Error('"toSJISFunc" is not a valid function.')
8662
- }
8663
-
8664
- toSJISFunction = f;
8665
- };
8666
-
8667
- utils$1.isKanjiModeEnabled = function () {
8668
- return typeof toSJISFunction !== 'undefined'
8669
- };
8670
-
8671
- utils$1.toSJIS = function toSJIS (kanji) {
8672
- return toSJISFunction(kanji)
8673
- };
8674
- return utils$1;
8675
- }
8676
-
8677
- var errorCorrectionLevel = {};
8678
-
8679
- var hasRequiredErrorCorrectionLevel;
8680
-
8681
- function requireErrorCorrectionLevel () {
8682
- if (hasRequiredErrorCorrectionLevel) return errorCorrectionLevel;
8683
- hasRequiredErrorCorrectionLevel = 1;
8684
- (function (exports$1) {
8685
- exports$1.L = { bit: 1 };
8686
- exports$1.M = { bit: 0 };
8687
- exports$1.Q = { bit: 3 };
8688
- exports$1.H = { bit: 2 };
8689
-
8690
- function fromString (string) {
8691
- if (typeof string !== 'string') {
8692
- throw new Error('Param is not a string')
8693
- }
8694
-
8695
- const lcStr = string.toLowerCase();
8696
-
8697
- switch (lcStr) {
8698
- case 'l':
8699
- case 'low':
8700
- return exports$1.L
8701
-
8702
- case 'm':
8703
- case 'medium':
8704
- return exports$1.M
8705
-
8706
- case 'q':
8707
- case 'quartile':
8708
- return exports$1.Q
8709
-
8710
- case 'h':
8711
- case 'high':
8712
- return exports$1.H
8713
-
8714
- default:
8715
- throw new Error('Unknown EC Level: ' + string)
8716
- }
8717
- }
8718
-
8719
- exports$1.isValid = function isValid (level) {
8720
- return level && typeof level.bit !== 'undefined' &&
8721
- level.bit >= 0 && level.bit < 4
8722
- };
8723
-
8724
- exports$1.from = function from (value, defaultValue) {
8725
- if (exports$1.isValid(value)) {
8726
- return value
8727
- }
8728
-
8729
- try {
8730
- return fromString(value)
8731
- } catch (e) {
8732
- return defaultValue
8733
- }
8734
- };
8735
- } (errorCorrectionLevel));
8736
- return errorCorrectionLevel;
8373
+
8374
+ var errorCorrectionLevel = {};
8375
+
8376
+ var hasRequiredErrorCorrectionLevel;
8377
+
8378
+ function requireErrorCorrectionLevel () {
8379
+ if (hasRequiredErrorCorrectionLevel) return errorCorrectionLevel;
8380
+ hasRequiredErrorCorrectionLevel = 1;
8381
+ (function (exports$1) {
8382
+ exports$1.L = { bit: 1 };
8383
+ exports$1.M = { bit: 0 };
8384
+ exports$1.Q = { bit: 3 };
8385
+ exports$1.H = { bit: 2 };
8386
+
8387
+ function fromString (string) {
8388
+ if (typeof string !== 'string') {
8389
+ throw new Error('Param is not a string')
8390
+ }
8391
+
8392
+ const lcStr = string.toLowerCase();
8393
+
8394
+ switch (lcStr) {
8395
+ case 'l':
8396
+ case 'low':
8397
+ return exports$1.L
8398
+
8399
+ case 'm':
8400
+ case 'medium':
8401
+ return exports$1.M
8402
+
8403
+ case 'q':
8404
+ case 'quartile':
8405
+ return exports$1.Q
8406
+
8407
+ case 'h':
8408
+ case 'high':
8409
+ return exports$1.H
8410
+
8411
+ default:
8412
+ throw new Error('Unknown EC Level: ' + string)
8413
+ }
8414
+ }
8415
+
8416
+ exports$1.isValid = function isValid (level) {
8417
+ return level && typeof level.bit !== 'undefined' &&
8418
+ level.bit >= 0 && level.bit < 4
8419
+ };
8420
+
8421
+ exports$1.from = function from (value, defaultValue) {
8422
+ if (exports$1.isValid(value)) {
8423
+ return value
8424
+ }
8425
+
8426
+ try {
8427
+ return fromString(value)
8428
+ } catch (e) {
8429
+ return defaultValue
8430
+ }
8431
+ };
8432
+ } (errorCorrectionLevel));
8433
+ return errorCorrectionLevel;
8737
8434
  }
8738
8435
 
8739
8436
  var bitBuffer;
@@ -14889,7 +14586,732 @@ function requireLib () {
14889
14586
  var libExports = requireLib();
14890
14587
  var QRCode = /*@__PURE__*/getDefaultExportFromCjs(libExports);
14891
14588
 
14892
- const defaultDependencies$5 = {
14589
+ const PREVIEW_PAGE_SERVER_WORKER_ARG = '__preview-page-server';
14590
+ const PREVIEW_PAGE_CONTENT_TYPE = 'text/html; charset=utf-8';
14591
+ const DEFAULT_PREVIEW_PAGE_TTL_MS = 60 * 60 * 1000;
14592
+ const DEFAULT_PREVIEW_PAGE_EXPIRED_NOTICE_TTL_MS = 60 * 60 * 1000;
14593
+ const PREVIEW_PAGE_PURPOSE = 'authorize_test_account_and_open_preview';
14594
+ const PREVIEW_PAGE_PRESENTATION = {
14595
+ audience: 'user',
14596
+ preferredSurface: 'browser',
14597
+ preferredAction: 'open_url',
14598
+ terminalInline: false,
14599
+ fallback: 'show_clickable_link',
14600
+ sameHostRequired: true,
14601
+ };
14602
+ const PREVIEW_PAGE_START_TIMEOUT_MS = 5000;
14603
+ const PREVIEW_PAGE_CONTROL_PATH = '/__ttmg/preview-page/update';
14604
+ const PREVIEW_PAGE_STATE_DIR = 'ttmg-preview-pages';
14605
+ const PREVIEW_PAGE_LOCK_TIMEOUT_MS = 3000;
14606
+ const PREVIEW_PAGE_LOCK_STALE_MS = 10000;
14607
+ function createPreviewPageServerResult(url, expiresAt) {
14608
+ return {
14609
+ status: 'ready',
14610
+ url,
14611
+ contentType: PREVIEW_PAGE_CONTENT_TYPE,
14612
+ purpose: PREVIEW_PAGE_PURPOSE,
14613
+ presentation: PREVIEW_PAGE_PRESENTATION,
14614
+ expiresAt,
14615
+ localhostOnly: true,
14616
+ latestOnly: true,
14617
+ };
14618
+ }
14619
+ function escapeHtml(value) {
14620
+ return value
14621
+ .replace(/&/g, '&amp;')
14622
+ .replace(/</g, '&lt;')
14623
+ .replace(/>/g, '&gt;')
14624
+ .replace(/"/g, '&quot;')
14625
+ .replace(/'/g, '&#39;');
14626
+ }
14627
+ function isHttpUrl(value) {
14628
+ try {
14629
+ const url = new URL(value);
14630
+ return url.protocol === 'http:' || url.protocol === 'https:';
14631
+ }
14632
+ catch {
14633
+ return false;
14634
+ }
14635
+ }
14636
+ function isPreviewPageInput(value) {
14637
+ if (!value || typeof value !== 'object')
14638
+ return false;
14639
+ const input = value;
14640
+ return (typeof input.authorizationUrl === 'string' &&
14641
+ isHttpUrl(input.authorizationUrl) &&
14642
+ typeof input.previewUrl === 'string' &&
14643
+ isHttpUrl(input.previewUrl) &&
14644
+ typeof input.clientKey === 'string' &&
14645
+ input.clientKey.length > 0 &&
14646
+ typeof input.projectName === 'string' &&
14647
+ input.projectName.length > 0 &&
14648
+ typeof input.versionId === 'string' &&
14649
+ input.versionId.length > 0);
14650
+ }
14651
+ function isWorkerRequest(value) {
14652
+ if (!value || typeof value !== 'object')
14653
+ return false;
14654
+ const request = value;
14655
+ if (request.type !== 'start' || !isPreviewPageInput(request))
14656
+ return false;
14657
+ const workerRequest = value;
14658
+ return (typeof workerRequest.token === 'string' &&
14659
+ workerRequest.token.length >= 32 &&
14660
+ typeof workerRequest.ttlMs === 'number' &&
14661
+ workerRequest.ttlMs > 0 &&
14662
+ (workerRequest.expiredNoticeTtlMs === undefined ||
14663
+ (typeof workerRequest.expiredNoticeTtlMs === 'number' &&
14664
+ workerRequest.expiredNoticeTtlMs >= 0)) &&
14665
+ (workerRequest.controlToken === undefined ||
14666
+ (typeof workerRequest.controlToken === 'string' &&
14667
+ workerRequest.controlToken.length >= 32)) &&
14668
+ (workerRequest.statePath === undefined ||
14669
+ typeof workerRequest.statePath === 'string'));
14670
+ }
14671
+ function getClientKeyHash(clientKey) {
14672
+ return crypto.createHash('sha256').update(clientKey).digest('hex').slice(0, 24);
14673
+ }
14674
+ function getPreviewPageStatePath(clientKey) {
14675
+ return path.join(os.tmpdir(), PREVIEW_PAGE_STATE_DIR, `${getClientKeyHash(clientKey)}.json`);
14676
+ }
14677
+ function getPreviewPageLockPath(clientKey) {
14678
+ return `${getPreviewPageStatePath(clientKey)}.lock`;
14679
+ }
14680
+ function ensureStateDirectory(statePath) {
14681
+ fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
14682
+ }
14683
+ function readServerState(statePath) {
14684
+ try {
14685
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
14686
+ if (state.schemaVersion !== 1 ||
14687
+ !Number.isInteger(state.pid) ||
14688
+ !Number.isInteger(state.port) ||
14689
+ state.port <= 0 ||
14690
+ state.port > 65535 ||
14691
+ typeof state.controlToken !== 'string' ||
14692
+ state.controlToken.length < 32 ||
14693
+ typeof state.expiresAt !== 'string') {
14694
+ return null;
14695
+ }
14696
+ return state;
14697
+ }
14698
+ catch {
14699
+ return null;
14700
+ }
14701
+ }
14702
+ function writeServerState(statePath, state) {
14703
+ ensureStateDirectory(statePath);
14704
+ const tempPath = `${statePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
14705
+ fs.writeFileSync(tempPath, JSON.stringify(state), { mode: 0o600 });
14706
+ fs.rmSync(statePath, { force: true });
14707
+ fs.renameSync(tempPath, statePath);
14708
+ }
14709
+ function removeMatchingServerState(statePath, pid, controlToken) {
14710
+ if (!statePath)
14711
+ return;
14712
+ const state = readServerState(statePath);
14713
+ if (state?.pid === pid && state.controlToken === controlToken) {
14714
+ fs.rmSync(statePath, { force: true });
14715
+ }
14716
+ }
14717
+ function tokensMatch(actual, expected) {
14718
+ const actualBuffer = Buffer.from(actual);
14719
+ const expectedBuffer = Buffer.from(expected);
14720
+ return (actualBuffer.length === expectedBuffer.length &&
14721
+ crypto.timingSafeEqual(actualBuffer, expectedBuffer));
14722
+ }
14723
+ async function withClientKeyLock(clientKey, action) {
14724
+ const lockPath = getPreviewPageLockPath(clientKey);
14725
+ ensureStateDirectory(lockPath);
14726
+ const deadline = Date.now() + PREVIEW_PAGE_LOCK_TIMEOUT_MS;
14727
+ let descriptor;
14728
+ while (descriptor === undefined && Date.now() < deadline) {
14729
+ try {
14730
+ descriptor = fs.openSync(lockPath, 'wx', 0o600);
14731
+ }
14732
+ catch (error) {
14733
+ if (error?.code !== 'EEXIST')
14734
+ throw error;
14735
+ try {
14736
+ const ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
14737
+ if (ageMs > PREVIEW_PAGE_LOCK_STALE_MS) {
14738
+ fs.rmSync(lockPath, { force: true });
14739
+ continue;
14740
+ }
14741
+ }
14742
+ catch {
14743
+ continue;
14744
+ }
14745
+ await new Promise(resolve => setTimeout(resolve, 40));
14746
+ }
14747
+ }
14748
+ if (descriptor === undefined) {
14749
+ throw new Error('Timed out waiting for the preview page service lock.');
14750
+ }
14751
+ try {
14752
+ return await action();
14753
+ }
14754
+ finally {
14755
+ fs.closeSync(descriptor);
14756
+ fs.rmSync(lockPath, { force: true });
14757
+ }
14758
+ }
14759
+ async function renderPreviewPage(input, expiresAt) {
14760
+ const [authorizationQr, previewQr] = await Promise.all([
14761
+ QRCode.toDataURL(input.authorizationUrl, {
14762
+ errorCorrectionLevel: 'M',
14763
+ margin: 2,
14764
+ width: 360,
14765
+ type: 'image/png',
14766
+ }),
14767
+ QRCode.toDataURL(input.previewUrl, {
14768
+ errorCorrectionLevel: 'M',
14769
+ margin: 2,
14770
+ width: 360,
14771
+ type: 'image/png',
14772
+ }),
14773
+ ]);
14774
+ const projectName = escapeHtml(input.projectName);
14775
+ const clientKey = escapeHtml(input.clientKey);
14776
+ const versionId = escapeHtml(input.versionId);
14777
+ const authorizationUrl = escapeHtml(input.authorizationUrl);
14778
+ const previewUrl = escapeHtml(input.previewUrl);
14779
+ const rerunCommand = escapeHtml(`ttmg upload --preview --serve-preview-page --client-key ${input.clientKey}`);
14780
+ return `<!doctype html>
14781
+ <html lang="en">
14782
+ <head>
14783
+ <meta charset="utf-8" />
14784
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14785
+ <meta name="color-scheme" content="light" />
14786
+ <title>${projectName} · Preview build</title>
14787
+ <style>
14788
+ :root {
14789
+ --ink: #15171f;
14790
+ --muted: #626879;
14791
+ --line: rgba(21, 23, 31, 0.1);
14792
+ --paper: rgba(255, 255, 255, 0.92);
14793
+ --cyan: #00b8c8;
14794
+ --cyan-soft: #e9fbfc;
14795
+ --pink: #fe2c55;
14796
+ --shadow: 0 10px 30px rgba(28, 34, 52, 0.07);
14797
+ }
14798
+ * { box-sizing: border-box; }
14799
+ body {
14800
+ margin: 0;
14801
+ min-height: 100vh;
14802
+ overflow-x: hidden;
14803
+ color: var(--ink);
14804
+ background:
14805
+ linear-gradient(180deg, rgba(0, 184, 200, 0.035), transparent 18rem),
14806
+ #f6f7f9;
14807
+ font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
14808
+ }
14809
+ .shell { width: min(1040px, calc(100% - 32px)); margin: 0 auto; padding: 30px 0 40px; }
14810
+ .topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 36px; }
14811
+ .brand { display: flex; align-items: center; gap: 11px; font-size: 13px; font-weight: 800; letter-spacing: 0.15em; }
14812
+ .mark { width: 28px; height: 28px; border-radius: 9px; background: linear-gradient(135deg, var(--cyan) 0 48%, var(--pink) 52% 100%); box-shadow: 0 7px 20px rgba(0, 184, 200, 0.24); }
14813
+ .status { display: inline-flex; align-items: center; gap: 8px; padding: 8px 11px; border: 1px solid rgba(0, 184, 200, 0.24); border-radius: 999px; color: #007d89; background: var(--cyan-soft); font-size: 12px; font-weight: 750; }
14814
+ .status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 4px rgba(0, 184, 200, 0.12); }
14815
+ .guidance { margin: 0 0 20px; padding: 12px 15px; border: 1px solid var(--line); border-radius: 12px; color: var(--muted); background: rgba(255, 255, 255, 0.7); font-size: 13px; line-height: 1.5; }
14816
+ .cards { display: grid; min-width: 0; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
14817
+ .card { position: relative; min-width: 0; overflow: hidden; padding: 28px; border: 1px solid var(--line); border-radius: 20px; background: var(--paper); box-shadow: var(--shadow); }
14818
+ .card::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 4px; background: linear-gradient(90deg, var(--cyan), transparent 68%); }
14819
+ .card.preview::after { background: linear-gradient(90deg, var(--pink), transparent 68%); }
14820
+ .step { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 15px; color: #007d89; font-size: 11px; font-weight: 850; letter-spacing: 0.15em; text-transform: uppercase; }
14821
+ .preview .step { color: #c82143; }
14822
+ h2 { margin: 0; font-size: 22px; letter-spacing: -0.025em; }
14823
+ .description { min-height: 54px; margin: 10px 0 22px; color: var(--muted); font-size: 14px; line-height: 1.55; }
14824
+ .qr { display: grid; place-items: center; width: min(290px, 100%); aspect-ratio: 1; margin: 0 auto 22px; padding: 15px; border: 1px solid var(--line); border-radius: 22px; background: white; }
14825
+ .qr img { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
14826
+ .linkbox { min-width: 0; overflow: hidden; padding: 12px 13px; border: 1px solid var(--line); border-radius: 14px; background: #f8f9fb; }
14827
+ .linklabel { display: flex; align-items: center; justify-content: space-between; margin-bottom: 7px; color: #7b8190; font-size: 10px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
14828
+ .copy { appearance: none; border: 0; color: #007d89; background: transparent; cursor: pointer; font: inherit; font-size: 10px; font-weight: 850; letter-spacing: 0.08em; text-transform: uppercase; }
14829
+ .direct { display: block; overflow: hidden; color: #252936; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.45; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; }
14830
+ .meta { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 1px; overflow: hidden; margin-top: 22px; border: 1px solid var(--line); border-radius: 18px; background: var(--line); }
14831
+ .meta div { min-width: 0; padding: 15px 17px; background: rgba(255, 255, 255, 0.82); }
14832
+ .meta small { display: block; margin-bottom: 5px; color: #858b98; font-size: 9px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
14833
+ .meta strong { display: block; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
14834
+ .foot { margin: 17px 0 0; color: #7d8391; font-size: 11px; text-align: center; }
14835
+ [hidden] { display: none !important; }
14836
+ .expired-view { margin: 88px auto 0; max-width: 620px; padding: 42px; border: 1px solid var(--line); border-radius: 22px; background: var(--paper); box-shadow: var(--shadow); text-align: center; }
14837
+ .expired-view h1 { margin: 0; font-size: 28px; letter-spacing: -0.03em; }
14838
+ .expired-view p { margin: 13px auto 24px; max-width: 460px; color: var(--muted); font-size: 14px; line-height: 1.6; }
14839
+ .expired-command { display: block; padding: 14px 16px; border: 1px solid var(--line); border-radius: 14px; color: #252936; background: #f8f9fb; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; text-align: left; white-space: normal; }
14840
+ .status.expired { border-color: rgba(254, 44, 85, 0.2); color: #c82143; background: #fff0f3; }
14841
+ .status.expired::before { background: var(--pink); box-shadow: 0 0 0 4px rgba(254, 44, 85, 0.1); }
14842
+ @media (max-width: 760px) {
14843
+ .shell { width: 100%; max-width: 560px; padding: 22px 11px 36px; }
14844
+ .topbar { margin-bottom: 34px; }
14845
+ .status { display: none; }
14846
+ .cards { grid-template-columns: minmax(0, 1fr); }
14847
+ .card { width: 100%; padding: 23px; border-radius: 22px; }
14848
+ .description { min-height: 0; }
14849
+ .qr { width: 100%; max-width: 290px; }
14850
+ .meta { grid-template-columns: 1fr; }
14851
+ }
14852
+ </style>
14853
+ </head>
14854
+ <body data-expires-at="${escapeHtml(expiresAt)}">
14855
+ <main class="shell">
14856
+ <header class="topbar">
14857
+ <div class="brand"><span class="mark" aria-hidden="true"></span>TTMG PREVIEW</div>
14858
+ <div class="status">Preview ready</div>
14859
+ </header>
14860
+ <div id="active-view">
14861
+ <p class="guidance">New test account: start with Step 1. Already authorized: go directly to Step 2.</p>
14862
+ <section class="cards">
14863
+ <article class="card">
14864
+ <div class="step">Step 01 · Account access</div>
14865
+ <h2>Authorize your test account</h2>
14866
+ <p class="description">Scan once with the TikTok account you will use for testing. If it is already a test user for this Mini Game, skip this step.</p>
14867
+ <div class="qr"><img src="${authorizationQr}" alt="QR code for test-account authorization" /></div>
14868
+ <div class="linkbox">
14869
+ <div class="linklabel">Open directly <button class="copy" data-target="authorization-link">Copy</button></div>
14870
+ <a id="authorization-link" class="direct" href="${authorizationUrl}" target="_blank" rel="noreferrer">${authorizationUrl}</a>
14871
+ </div>
14872
+ </article>
14873
+ <article class="card preview">
14874
+ <div class="step">Step 02 · Preview build</div>
14875
+ <h2>Open the game preview</h2>
14876
+ <p class="description">Already added as a test user? Scan this code directly. Otherwise, complete Step 1 first with the same account.</p>
14877
+ <div class="qr"><img src="${previewQr}" alt="QR code for the game preview" /></div>
14878
+ <div class="linkbox">
14879
+ <div class="linklabel">Open directly <button class="copy" data-target="preview-link">Copy</button></div>
14880
+ <a id="preview-link" class="direct" href="${previewUrl}" target="_blank" rel="noreferrer">${previewUrl}</a>
14881
+ </div>
14882
+ </article>
14883
+ </section>
14884
+ <section class="meta" aria-label="Preview metadata">
14885
+ <div><small>Mini Game</small><strong>${projectName}</strong></div>
14886
+ <div><small>Client key</small><strong>${clientKey}</strong></div>
14887
+ <div><small>Version</small><strong>${versionId}</strong></div>
14888
+ </section>
14889
+ <p class="foot">Local-only preview page · expires in <span id="countdown">60:00</span></p>
14890
+ </div>
14891
+ <section id="expired-view" class="expired-view" hidden>
14892
+ <h1>This preview page has expired</h1>
14893
+ <p>Run the upload command again from your Mini Game project to generate a new preview page and preview build.</p>
14894
+ <code class="expired-command">${rerunCommand}</code>
14895
+ </section>
14896
+ </main>
14897
+ <script>
14898
+ document.querySelectorAll('[data-target]').forEach(function (button) {
14899
+ button.addEventListener('click', async function () {
14900
+ var target = document.getElementById(button.dataset.target);
14901
+ if (!target) return;
14902
+ await navigator.clipboard.writeText(target.textContent || '');
14903
+ var previous = button.textContent;
14904
+ button.textContent = 'Copied';
14905
+ setTimeout(function () { button.textContent = previous; }, 1200);
14906
+ });
14907
+ });
14908
+ var expiresAt = Date.parse(document.body.dataset.expiresAt || '');
14909
+ var countdown = document.getElementById('countdown');
14910
+ var activeView = document.getElementById('active-view');
14911
+ var expiredView = document.getElementById('expired-view');
14912
+ var status = document.querySelector('.status');
14913
+ var timer;
14914
+ function updateCountdown() {
14915
+ var seconds = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000));
14916
+ var minutes = Math.floor(seconds / 60);
14917
+ countdown.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds % 60).padStart(2, '0');
14918
+ if (seconds === 0) {
14919
+ activeView.hidden = true;
14920
+ activeView.replaceChildren();
14921
+ expiredView.hidden = false;
14922
+ status.textContent = 'Preview expired';
14923
+ status.classList.add('expired');
14924
+ clearInterval(timer);
14925
+ }
14926
+ }
14927
+ updateCountdown();
14928
+ timer = setInterval(updateCountdown, 1000);
14929
+ </script>
14930
+ </body>
14931
+ </html>`;
14932
+ }
14933
+ function renderExpiredPreviewPage(input) {
14934
+ const projectName = escapeHtml(input.projectName);
14935
+ const rerunCommand = escapeHtml(`ttmg upload --preview --serve-preview-page --client-key ${input.clientKey}`);
14936
+ return `<!doctype html>
14937
+ <html lang="en">
14938
+ <head>
14939
+ <meta charset="utf-8" />
14940
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14941
+ <meta name="color-scheme" content="light" />
14942
+ <title>${projectName} · Preview expired</title>
14943
+ <style>
14944
+ * { box-sizing: border-box; }
14945
+ body { display: grid; min-height: 100vh; margin: 0; place-items: center; padding: 24px; color: #15171f; background: #f6f7f9; font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
14946
+ main { width: min(620px, 100%); padding: 42px; border: 1px solid rgba(21, 23, 31, 0.1); border-radius: 22px; background: rgba(255, 255, 255, 0.94); box-shadow: 0 10px 30px rgba(28, 34, 52, 0.07); text-align: center; }
14947
+ .mark { width: 30px; height: 30px; margin: 0 auto 22px; border-radius: 9px; background: linear-gradient(135deg, #00b8c8 0 48%, #fe2c55 52% 100%); }
14948
+ h1 { margin: 0; font-size: 28px; letter-spacing: -0.03em; }
14949
+ p { margin: 13px auto 24px; max-width: 460px; color: #626879; font-size: 14px; line-height: 1.6; }
14950
+ code { display: block; padding: 14px 16px; border: 1px solid rgba(21, 23, 31, 0.1); border-radius: 14px; color: #252936; background: #f8f9fb; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; text-align: left; white-space: normal; }
14951
+ </style>
14952
+ </head>
14953
+ <body>
14954
+ <main>
14955
+ <div class="mark" aria-hidden="true"></div>
14956
+ <h1>This preview page has expired</h1>
14957
+ <p>Run the upload command again from your Mini Game project to generate a new preview page and preview build.</p>
14958
+ <code>${rerunCommand}</code>
14959
+ </main>
14960
+ </body>
14961
+ </html>`;
14962
+ }
14963
+ async function createPreviewPageHttpServer({ input, token, ttlMs = DEFAULT_PREVIEW_PAGE_TTL_MS, expiredNoticeTtlMs = DEFAULT_PREVIEW_PAGE_EXPIRED_NOTICE_TTL_MS, controlToken = crypto.randomBytes(24).toString('hex'), onClose, }) {
14964
+ if (!isPreviewPageInput(input)) {
14965
+ throw new Error('Preview page links must use HTTP or HTTPS.');
14966
+ }
14967
+ let port = 0;
14968
+ let closed = false;
14969
+ let expiryTimer;
14970
+ let shutdownTimer;
14971
+ let currentPage = {
14972
+ pathname: `/preview/${token}`,
14973
+ expiresAt: new Date(Date.now() + ttlMs).toISOString(),
14974
+ html: '',
14975
+ expiredHtml: renderExpiredPreviewPage(input),
14976
+ };
14977
+ currentPage.html = await renderPreviewPage(input, currentPage.expiresAt);
14978
+ const getResult = () => createPreviewPageServerResult(`http://127.0.0.1:${port}${currentPage.pathname}`, currentPage.expiresAt);
14979
+ let close;
14980
+ const scheduleLifecycle = () => {
14981
+ clearTimeout(expiryTimer);
14982
+ clearTimeout(shutdownTimer);
14983
+ expiryTimer = setTimeout(() => {
14984
+ currentPage.html = '';
14985
+ }, ttlMs);
14986
+ shutdownTimer = setTimeout(() => {
14987
+ void close();
14988
+ }, ttlMs + expiredNoticeTtlMs);
14989
+ };
14990
+ const update = async (nextInput) => {
14991
+ if (!isPreviewPageInput(nextInput)) {
14992
+ throw new Error('Preview page links must use HTTP or HTTPS.');
14993
+ }
14994
+ const nextToken = crypto.randomBytes(24).toString('hex');
14995
+ const nextExpiresAt = new Date(Date.now() + ttlMs).toISOString();
14996
+ const nextHtml = await renderPreviewPage(nextInput, nextExpiresAt);
14997
+ const nextExpiredHtml = renderExpiredPreviewPage(nextInput);
14998
+ currentPage = {
14999
+ pathname: `/preview/${nextToken}`,
15000
+ expiresAt: nextExpiresAt,
15001
+ html: nextHtml,
15002
+ expiredHtml: nextExpiredHtml,
15003
+ };
15004
+ scheduleLifecycle();
15005
+ return getResult();
15006
+ };
15007
+ const sendJson = (response, statusCode, value) => {
15008
+ const body = JSON.stringify(value);
15009
+ response.writeHead(statusCode, {
15010
+ 'Cache-Control': 'no-store',
15011
+ 'Content-Length': String(Buffer.byteLength(body)),
15012
+ 'Content-Type': 'application/json; charset=utf-8',
15013
+ 'X-Content-Type-Options': 'nosniff',
15014
+ });
15015
+ response.end(body);
15016
+ };
15017
+ const server = http.createServer(async (request, response) => {
15018
+ const requestUrl = new URL(request.url || '/', 'http://127.0.0.1');
15019
+ if (requestUrl.pathname === PREVIEW_PAGE_CONTROL_PATH) {
15020
+ const authorization = Array.isArray(request.headers.authorization)
15021
+ ? request.headers.authorization[0] || ''
15022
+ : request.headers.authorization || '';
15023
+ const bearerToken = authorization.startsWith('Bearer ')
15024
+ ? authorization.slice('Bearer '.length)
15025
+ : '';
15026
+ if (request.method !== 'POST' ||
15027
+ !tokensMatch(bearerToken, controlToken)) {
15028
+ sendJson(response, 404, { error: 'Not Found' });
15029
+ return;
15030
+ }
15031
+ try {
15032
+ const chunks = [];
15033
+ let totalBytes = 0;
15034
+ for await (const chunk of request) {
15035
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15036
+ totalBytes += buffer.length;
15037
+ if (totalBytes > 64 * 1024) {
15038
+ throw new Error('Preview page update is too large.');
15039
+ }
15040
+ chunks.push(buffer);
15041
+ }
15042
+ const nextInput = JSON.parse(Buffer.concat(chunks).toString('utf8'));
15043
+ const result = await update(nextInput);
15044
+ sendJson(response, 200, result);
15045
+ }
15046
+ catch (error) {
15047
+ sendJson(response, 400, {
15048
+ error: error?.message || String(error),
15049
+ });
15050
+ }
15051
+ return;
15052
+ }
15053
+ const matchesPath = requestUrl.pathname === currentPage.pathname ||
15054
+ requestUrl.pathname === `${currentPage.pathname}/`;
15055
+ if (!matchesPath ||
15056
+ (request.method !== 'GET' && request.method !== 'HEAD')) {
15057
+ response.writeHead(404, {
15058
+ 'Cache-Control': 'no-store',
15059
+ 'Content-Type': 'text/plain; charset=utf-8',
15060
+ 'X-Content-Type-Options': 'nosniff',
15061
+ });
15062
+ response.end('Not Found');
15063
+ return;
15064
+ }
15065
+ const isExpired = Date.now() >= Date.parse(currentPage.expiresAt);
15066
+ if (isExpired)
15067
+ currentPage.html = '';
15068
+ const responseHtml = isExpired ? currentPage.expiredHtml : currentPage.html;
15069
+ response.writeHead(200, {
15070
+ 'Cache-Control': 'no-store',
15071
+ 'Content-Length': String(Buffer.byteLength(responseHtml)),
15072
+ 'Content-Security-Policy': "default-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
15073
+ 'Content-Type': PREVIEW_PAGE_CONTENT_TYPE,
15074
+ 'Referrer-Policy': 'no-referrer',
15075
+ 'X-Content-Type-Options': 'nosniff',
15076
+ });
15077
+ response.end(request.method === 'HEAD' ? undefined : responseHtml);
15078
+ });
15079
+ await new Promise((resolve, reject) => {
15080
+ const onError = (error) => {
15081
+ server.off('listening', onListening);
15082
+ reject(error);
15083
+ };
15084
+ const onListening = () => {
15085
+ server.off('error', onError);
15086
+ resolve();
15087
+ };
15088
+ server.once('error', onError);
15089
+ server.once('listening', onListening);
15090
+ server.listen(0, '127.0.0.1');
15091
+ });
15092
+ const address = server.address();
15093
+ if (!address || typeof address === 'string') {
15094
+ server.close();
15095
+ throw new Error('Unable to resolve the preview page server address.');
15096
+ }
15097
+ port = address.port;
15098
+ close = () => new Promise(resolve => {
15099
+ if (closed) {
15100
+ resolve();
15101
+ return;
15102
+ }
15103
+ closed = true;
15104
+ clearTimeout(expiryTimer);
15105
+ clearTimeout(shutdownTimer);
15106
+ server.close(() => {
15107
+ currentPage.html = '';
15108
+ currentPage.expiredHtml = '';
15109
+ onClose?.();
15110
+ resolve();
15111
+ });
15112
+ });
15113
+ scheduleLifecycle();
15114
+ return {
15115
+ result: getResult(),
15116
+ update,
15117
+ close,
15118
+ };
15119
+ }
15120
+ function runPreviewPageServerWorker() {
15121
+ if (!process.send) {
15122
+ process.exitCode = 1;
15123
+ return;
15124
+ }
15125
+ process.once('message', async (message) => {
15126
+ if (!isWorkerRequest(message)) {
15127
+ process.send?.({
15128
+ type: 'error',
15129
+ error: 'Invalid preview page server request.',
15130
+ });
15131
+ process.disconnect?.();
15132
+ process.exitCode = 1;
15133
+ return;
15134
+ }
15135
+ try {
15136
+ const controlToken = message.controlToken || crypto.randomBytes(24).toString('hex');
15137
+ const pageServer = await createPreviewPageHttpServer({
15138
+ input: message,
15139
+ token: message.token,
15140
+ ttlMs: message.ttlMs,
15141
+ expiredNoticeTtlMs: message.expiredNoticeTtlMs,
15142
+ controlToken,
15143
+ onClose: () => removeMatchingServerState(message.statePath, process.pid, controlToken),
15144
+ });
15145
+ process.send?.({
15146
+ type: 'ready',
15147
+ url: pageServer.result.url,
15148
+ contentType: pageServer.result.contentType,
15149
+ expiresAt: pageServer.result.expiresAt,
15150
+ });
15151
+ process.disconnect?.();
15152
+ const shutdown = () => {
15153
+ void pageServer.close().finally(() => process.exit(0));
15154
+ };
15155
+ process.once('SIGINT', shutdown);
15156
+ process.once('SIGTERM', shutdown);
15157
+ }
15158
+ catch (error) {
15159
+ process.send?.({
15160
+ type: 'error',
15161
+ error: error?.message || String(error),
15162
+ });
15163
+ process.disconnect?.();
15164
+ process.exitCode = 1;
15165
+ }
15166
+ });
15167
+ }
15168
+ async function updateExistingPreviewPageServer(state, input) {
15169
+ const body = JSON.stringify(input);
15170
+ return new Promise((resolve, reject) => {
15171
+ const request = http.request({
15172
+ hostname: '127.0.0.1',
15173
+ port: state.port,
15174
+ path: PREVIEW_PAGE_CONTROL_PATH,
15175
+ method: 'POST',
15176
+ headers: {
15177
+ Authorization: `Bearer ${state.controlToken}`,
15178
+ 'Content-Length': String(Buffer.byteLength(body)),
15179
+ 'Content-Type': 'application/json; charset=utf-8',
15180
+ },
15181
+ }, response => {
15182
+ const chunks = [];
15183
+ let totalBytes = 0;
15184
+ response.on('data', chunk => {
15185
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15186
+ totalBytes += buffer.length;
15187
+ if (totalBytes <= 64 * 1024)
15188
+ chunks.push(buffer);
15189
+ });
15190
+ response.on('end', () => {
15191
+ if (totalBytes > 64 * 1024) {
15192
+ reject(new Error('Preview page service response is too large.'));
15193
+ return;
15194
+ }
15195
+ try {
15196
+ const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
15197
+ if (response.statusCode !== 200 || payload?.status !== 'ready') {
15198
+ throw new Error(payload?.error || 'Preview page service update failed.');
15199
+ }
15200
+ resolve(payload);
15201
+ }
15202
+ catch (error) {
15203
+ reject(error);
15204
+ }
15205
+ });
15206
+ });
15207
+ request.setTimeout(1500, () => {
15208
+ request.destroy(new Error('Preview page service update timed out.'));
15209
+ });
15210
+ request.once('error', reject);
15211
+ request.end(body);
15212
+ });
15213
+ }
15214
+ async function spawnDetachedPreviewPageServer(input, statePath, options) {
15215
+ const ttlMs = options.ttlMs || DEFAULT_PREVIEW_PAGE_TTL_MS;
15216
+ const expiredNoticeTtlMs = options.expiredNoticeTtlMs ?? DEFAULT_PREVIEW_PAGE_EXPIRED_NOTICE_TTL_MS;
15217
+ const entryPath = options.entryPath || process.argv[1];
15218
+ const token = crypto.randomBytes(24).toString('hex');
15219
+ const controlToken = crypto.randomBytes(24).toString('hex');
15220
+ const child = child_process.spawn(process.execPath, [entryPath, PREVIEW_PAGE_SERVER_WORKER_ARG], {
15221
+ detached: true,
15222
+ windowsHide: true,
15223
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
15224
+ });
15225
+ return new Promise((resolve, reject) => {
15226
+ let settled = false;
15227
+ const finish = (error, result) => {
15228
+ if (settled)
15229
+ return;
15230
+ settled = true;
15231
+ clearTimeout(timer);
15232
+ child.removeAllListeners('message');
15233
+ child.removeAllListeners('error');
15234
+ child.removeAllListeners('exit');
15235
+ if (error) {
15236
+ child.kill();
15237
+ reject(error);
15238
+ return;
15239
+ }
15240
+ if (child.connected)
15241
+ child.disconnect();
15242
+ child.unref();
15243
+ resolve(result);
15244
+ };
15245
+ const timer = setTimeout(() => {
15246
+ finish(new Error('Timed out starting the preview page server.'));
15247
+ }, options.startTimeoutMs || PREVIEW_PAGE_START_TIMEOUT_MS);
15248
+ child.once('error', error => finish(error));
15249
+ child.once('exit', code => {
15250
+ finish(new Error(`Preview page server exited before it was ready (code ${code ?? 'unknown'}).`));
15251
+ });
15252
+ child.on('message', message => {
15253
+ const response = message;
15254
+ if (response?.type === 'error') {
15255
+ finish(new Error(response.error));
15256
+ return;
15257
+ }
15258
+ if (response?.type === 'ready') {
15259
+ const result = createPreviewPageServerResult(response.url, response.expiresAt);
15260
+ try {
15261
+ const port = Number(new URL(result.url).port);
15262
+ writeServerState(statePath, {
15263
+ schemaVersion: 1,
15264
+ pid: child.pid,
15265
+ port,
15266
+ controlToken,
15267
+ expiresAt: result.expiresAt,
15268
+ });
15269
+ finish(undefined, result);
15270
+ }
15271
+ catch (error) {
15272
+ finish(error);
15273
+ }
15274
+ }
15275
+ });
15276
+ child.send({
15277
+ type: 'start',
15278
+ ...input,
15279
+ token,
15280
+ ttlMs,
15281
+ expiredNoticeTtlMs,
15282
+ controlToken,
15283
+ statePath,
15284
+ });
15285
+ });
15286
+ }
15287
+ async function startDetachedPreviewPageServer(input, options = {}) {
15288
+ if (!isPreviewPageInput(input)) {
15289
+ throw new Error('Preview page links must use HTTP or HTTPS.');
15290
+ }
15291
+ return withClientKeyLock(input.clientKey, async () => {
15292
+ const statePath = getPreviewPageStatePath(input.clientKey);
15293
+ const existingState = readServerState(statePath);
15294
+ if (existingState) {
15295
+ try {
15296
+ const result = await updateExistingPreviewPageServer(existingState, input);
15297
+ writeServerState(statePath, {
15298
+ ...existingState,
15299
+ expiresAt: result.expiresAt,
15300
+ });
15301
+ return result;
15302
+ }
15303
+ catch {
15304
+ fs.rmSync(statePath, { force: true });
15305
+ }
15306
+ }
15307
+ else if (fs.existsSync(statePath)) {
15308
+ fs.rmSync(statePath, { force: true });
15309
+ }
15310
+ return spawnDetachedPreviewPageServer(input, statePath, options);
15311
+ });
15312
+ }
15313
+
15314
+ const defaultDependencies$6 = {
14893
15315
  getCurrentUser,
14894
15316
  fetchGameInfo,
14895
15317
  writeQrFile: async (outputPath, payload) => {
@@ -14917,112 +15339,509 @@ function normalizeFormat$3(format) {
14917
15339
  }
14918
15340
  return normalized;
14919
15341
  }
14920
- function buildTestAccountAuthorizationUrl(clientKey) {
14921
- return `https://www.tiktok.com/ttmg/dev/${encodeURIComponent(clientKey)}`;
15342
+ function buildTestAccountAuthorizationUrl(clientKey) {
15343
+ return `https://www.tiktok.com/ttmg/dev/${encodeURIComponent(clientKey)}`;
15344
+ }
15345
+ async function generateTestAccountAuthorization(options, dependencies = {}) {
15346
+ const deps = { ...defaultDependencies$6, ...dependencies };
15347
+ const clientKey = options.clientKey?.trim();
15348
+ const operationId = deps.operationId();
15349
+ if (!clientKey) {
15350
+ throw new TestAccountCommandError('INVALID_ARGUMENT', t('testAccount.error.noClientKey'));
15351
+ }
15352
+ const user = deps.getCurrentUser();
15353
+ if (!user || !user.cookie) {
15354
+ throw new TestAccountCommandError('NOT_AUTHENTICATED', t('testAccount.error.notLoggedIn'));
15355
+ }
15356
+ const infoResponse = await deps.fetchGameInfo(clientKey);
15357
+ if (infoResponse.error) {
15358
+ throw new TestAccountCommandError('PROJECT_LOOKUP_FAILED', infoResponse.error.errorMsg || t('testAccount.error.gameInfoFailed'));
15359
+ }
15360
+ const appId = infoResponse.data?.app_id || '';
15361
+ if (!appId) {
15362
+ throw new TestAccountCommandError('PROJECT_RESPONSE_INVALID', t('testAccount.error.noAppId'));
15363
+ }
15364
+ const projectName = infoResponse.data?.name || 'game';
15365
+ const authorizationUrl = buildTestAccountAuthorizationUrl(clientKey);
15366
+ const safeClientKey = clientKey.replace(/[^a-zA-Z0-9_-]/g, '_');
15367
+ const qrFile = path.resolve(process.cwd(), options.qrOutput || path.join('.ttmg', `test-account-${safeClientKey}.png`));
15368
+ try {
15369
+ fs.mkdirSync(path.dirname(qrFile), { recursive: true });
15370
+ await deps.writeQrFile(qrFile, authorizationUrl);
15371
+ }
15372
+ catch (error) {
15373
+ throw new TestAccountCommandError('QR_WRITE_FAILED', error?.message || t('testAccount.error.qrWriteFailed'));
15374
+ }
15375
+ return {
15376
+ schemaVersion: '1',
15377
+ operationId,
15378
+ ts: deps.now(),
15379
+ operation: 'test-account.add',
15380
+ status: 'authorization_required',
15381
+ clientKey,
15382
+ appId,
15383
+ projectName,
15384
+ authorizationUrl,
15385
+ qrPayload: authorizationUrl,
15386
+ qrFile,
15387
+ portalUrl: `https://developers.tiktok.com/portal/game/${appId}`,
15388
+ platformWrite: false,
15389
+ verification: 'developer_portal',
15390
+ errorCode: null,
15391
+ error: null,
15392
+ };
15393
+ }
15394
+ async function testAccountAdd(options, dependencies = {}) {
15395
+ const deps = { ...defaultDependencies$6, ...dependencies };
15396
+ let format = 'text';
15397
+ const operationId = deps.operationId();
15398
+ try {
15399
+ format = normalizeFormat$3(options.format);
15400
+ const result = await generateTestAccountAuthorization(options, {
15401
+ ...dependencies,
15402
+ operationId: () => operationId,
15403
+ });
15404
+ if (format === 'json') {
15405
+ console.log(JSON.stringify(result));
15406
+ }
15407
+ else {
15408
+ console.log(chalk.green.bold(t('testAccount.success')));
15409
+ console.log(t('testAccount.project', {
15410
+ name: result.projectName,
15411
+ clientKey: result.clientKey,
15412
+ }));
15413
+ console.log(t('testAccount.authorizationUrl', { url: result.authorizationUrl }));
15414
+ console.log(t('testAccount.qrFile', { file: result.qrFile }));
15415
+ console.log(chalk.yellow(t('testAccount.verifyInPortal')));
15416
+ }
15417
+ return result;
15418
+ }
15419
+ catch (error) {
15420
+ const message = error?.message || String(error);
15421
+ const errorCode = error instanceof TestAccountCommandError ? error.code : 'UNKNOWN_ERROR';
15422
+ const failedResult = {
15423
+ schemaVersion: '1',
15424
+ operationId,
15425
+ ts: deps.now(),
15426
+ operation: 'test-account.add',
15427
+ status: 'failed',
15428
+ clientKey: options.clientKey || '',
15429
+ appId: '',
15430
+ projectName: '',
15431
+ authorizationUrl: '',
15432
+ qrPayload: '',
15433
+ qrFile: '',
15434
+ portalUrl: '',
15435
+ platformWrite: false,
15436
+ verification: 'developer_portal',
15437
+ errorCode,
15438
+ error: message,
15439
+ };
15440
+ if (format === 'json') {
15441
+ console.log(JSON.stringify(failedResult));
15442
+ }
15443
+ else {
15444
+ console.error(chalk.red.bold(t('testAccount.fail', { error: message })));
15445
+ }
15446
+ process.exitCode = 1;
15447
+ return failedResult;
15448
+ }
15449
+ }
15450
+
15451
+ const DEFAULT_POLL_INTERVAL_MS = 3000;
15452
+ const DEFAULT_POLL_TIMEOUT_MS = 10 * 60 * 1000;
15453
+ const defaultDependencies$5 = {
15454
+ getCurrentUser,
15455
+ fetchGameInfo,
15456
+ zipDirectory: targetDir => zipCwdToBuffer([], targetDir),
15457
+ uploadGameToPlatform,
15458
+ fetchGameAssets,
15459
+ fetchGameAssetPreviewUrl,
15460
+ startPreviewPageServer: startDetachedPreviewPageServer,
15461
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
15462
+ now: () => Date.now(),
15463
+ pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
15464
+ pollTimeoutMs: DEFAULT_POLL_TIMEOUT_MS,
15465
+ };
15466
+ const defaultUploadRuntime = {
15467
+ openUrl: async (url) => {
15468
+ const openModule = await import('open');
15469
+ return openModule.default(url);
15470
+ },
15471
+ };
15472
+ function normalizeOutputFormat(format) {
15473
+ const normalized = (format || 'text').toLowerCase();
15474
+ if (normalized !== 'text' &&
15475
+ normalized !== 'json' &&
15476
+ normalized !== 'ndjson') {
15477
+ throw new Error(t('upload.error.invalidFormat', { format: normalized }));
15478
+ }
15479
+ return normalized;
15480
+ }
15481
+ function getAssetStatusRaw(asset) {
15482
+ return asset?.cdn_deploy_status ?? asset?.status ?? null;
15483
+ }
15484
+ function normalizeUploadPlatformStatus(rawStatus) {
15485
+ if (rawStatus === null || rawStatus === undefined || rawStatus === '') {
15486
+ return 'waiting_for_asset';
15487
+ }
15488
+ const numericStatus = Number(rawStatus);
15489
+ if (Number.isInteger(numericStatus)) {
15490
+ if (numericStatus === 0)
15491
+ return 'unknown';
15492
+ if (numericStatus === 1)
15493
+ return 'pending';
15494
+ if (numericStatus === 2)
15495
+ return 'success';
15496
+ if (numericStatus === 3)
15497
+ return 'failed';
15498
+ if (numericStatus === 4)
15499
+ return 'processing';
15500
+ return 'unknown';
15501
+ }
15502
+ const normalized = String(rawStatus).trim().toLowerCase();
15503
+ const statusName = normalized.replace(/^deploy_status_/, '');
15504
+ if (['ready', 'success', 'succeeded', 'available'].includes(statusName)) {
15505
+ return 'success';
15506
+ }
15507
+ if (statusName.includes('fail') ||
15508
+ statusName.includes('error') ||
15509
+ statusName.includes('reject')) {
15510
+ return 'failed';
15511
+ }
15512
+ if (statusName.includes('process') ||
15513
+ statusName.includes('upload') ||
15514
+ statusName.includes('deploy')) {
15515
+ return 'processing';
15516
+ }
15517
+ if (statusName.includes('pending') ||
15518
+ statusName.includes('queue') ||
15519
+ statusName.includes('wait')) {
15520
+ return 'pending';
15521
+ }
15522
+ return 'unknown';
15523
+ }
15524
+ function getAssetState(asset) {
15525
+ const platformStatus = normalizeUploadPlatformStatus(getAssetStatusRaw(asset));
15526
+ if (platformStatus === 'success') {
15527
+ return 'ready';
15528
+ }
15529
+ if (platformStatus === 'failed') {
15530
+ return 'failed';
15531
+ }
15532
+ return 'pending';
15533
+ }
15534
+ function extractAssets(data) {
15535
+ if (!data) {
15536
+ return [];
15537
+ }
15538
+ return [
15539
+ data.preview_assets,
15540
+ data.in_review_assets,
15541
+ data.live_assets,
15542
+ data.assets,
15543
+ ].flatMap(group => (Array.isArray(group) ? group : []));
15544
+ }
15545
+ function extractPreviewUrl(data) {
15546
+ return typeof data?.preview_url === 'string' ? data.preview_url : '';
15547
+ }
15548
+ function createEvent(base, status, progress, overrides = {}) {
15549
+ return {
15550
+ schemaVersion: '1',
15551
+ operationId: base.operationId,
15552
+ ts: Date.now(),
15553
+ clientKey: base.clientKey,
15554
+ versionId: '',
15555
+ status,
15556
+ progress,
15557
+ previewUrl: '',
15558
+ qrPayload: '',
15559
+ platformUrl: '',
15560
+ error: null,
15561
+ errorCode: null,
15562
+ platformWrite: false,
15563
+ ...overrides,
15564
+ };
15565
+ }
15566
+ async function waitForAssetReady({ assetId, miniGameId, deadline, eventBase, onEvent, deps, }) {
15567
+ let lastPlatformStatusRaw = null;
15568
+ let hasEmittedPlatformStatus = false;
15569
+ while (deps.now() < deadline) {
15570
+ const response = await deps.fetchGameAssets({ miniGameId });
15571
+ if (response.error) {
15572
+ throw new Error(response.error?.StatusMessage ||
15573
+ response.error?.message ||
15574
+ t('upload.error.assetQueryFailed'));
15575
+ }
15576
+ const asset = extractAssets(response.data).find(item => String(item.asset_id || '') === assetId) || null;
15577
+ const platformStatusRaw = getAssetStatusRaw(asset);
15578
+ const platformStatus = normalizeUploadPlatformStatus(platformStatusRaw);
15579
+ if (!hasEmittedPlatformStatus ||
15580
+ platformStatusRaw !== lastPlatformStatusRaw) {
15581
+ hasEmittedPlatformStatus = true;
15582
+ lastPlatformStatusRaw = platformStatusRaw;
15583
+ onEvent(createEvent(eventBase, 'processing', 0.7, {
15584
+ versionId: assetId,
15585
+ platformStatus,
15586
+ ...(platformStatusRaw === null ? {} : { platformStatusRaw }),
15587
+ platformWrite: true,
15588
+ }));
15589
+ }
15590
+ const assetState = getAssetState(asset);
15591
+ if (assetState === 'ready') {
15592
+ return asset;
15593
+ }
15594
+ if (assetState === 'failed') {
15595
+ throw new Error(t('upload.error.assetFailed', { status: platformStatus }));
15596
+ }
15597
+ await deps.sleep(deps.pollIntervalMs);
15598
+ }
15599
+ throw new Error(t('upload.error.assetTimeout'));
15600
+ }
15601
+ async function waitForPreviewUrl({ assetId, deadline, deps, }) {
15602
+ while (deps.now() < deadline) {
15603
+ const response = await deps.fetchGameAssetPreviewUrl({ assetId });
15604
+ if (response.error) {
15605
+ throw new Error(response.error?.StatusMessage ||
15606
+ response.error?.message ||
15607
+ t('upload.error.previewQueryFailed'));
15608
+ }
15609
+ const previewUrl = extractPreviewUrl(response.data);
15610
+ if (previewUrl) {
15611
+ return previewUrl;
15612
+ }
15613
+ await deps.sleep(deps.pollIntervalMs);
15614
+ }
15615
+ throw new Error(t('upload.error.previewTimeout'));
14922
15616
  }
14923
- async function generateTestAccountAuthorization(options, dependencies = {}) {
15617
+ async function executeUpload(options, dependencies = {}) {
14924
15618
  const deps = { ...defaultDependencies$5, ...dependencies };
14925
15619
  const clientKey = options.clientKey?.trim();
14926
- const operationId = deps.operationId();
14927
- if (!clientKey) {
14928
- throw new TestAccountCommandError('INVALID_ARGUMENT', t('testAccount.error.noClientKey'));
15620
+ const note = options.note || '--';
15621
+ const eventBase = { operationId: options.operationId, clientKey };
15622
+ const emit = options.onEvent;
15623
+ if (options.servePreviewPage && !options.preview) {
15624
+ throw new Error(t('upload.error.previewPageRequiresPreview'));
14929
15625
  }
15626
+ emit(createEvent(eventBase, 'precheck', 0.05));
14930
15627
  const user = deps.getCurrentUser();
14931
15628
  if (!user || !user.cookie) {
14932
- throw new TestAccountCommandError('NOT_AUTHENTICATED', t('testAccount.error.notLoggedIn'));
15629
+ throw new Error(t('upload.error.notLoggedIn'));
15630
+ }
15631
+ if (!clientKey) {
15632
+ throw new Error(t('upload.error.noClientKey'));
14933
15633
  }
14934
15634
  const infoResponse = await deps.fetchGameInfo(clientKey);
14935
15635
  if (infoResponse.error) {
14936
- throw new TestAccountCommandError('PROJECT_LOOKUP_FAILED', infoResponse.error.errorMsg || t('testAccount.error.gameInfoFailed'));
15636
+ throw new Error(infoResponse.error.errorMsg || t('upload.error.gameInfoFailed'));
14937
15637
  }
14938
15638
  const appId = infoResponse.data?.app_id || '';
14939
- if (!appId) {
14940
- throw new TestAccountCommandError('PROJECT_RESPONSE_INVALID', t('testAccount.error.noAppId'));
14941
- }
14942
15639
  const projectName = infoResponse.data?.name || 'game';
14943
- const authorizationUrl = buildTestAccountAuthorizationUrl(clientKey);
14944
- const safeClientKey = clientKey.replace(/[^a-zA-Z0-9_-]/g, '_');
14945
- const qrFile = path.resolve(process.cwd(), options.qrOutput || path.join('.ttmg', `test-account-${safeClientKey}.png`));
14946
- try {
14947
- fs.mkdirSync(path.dirname(qrFile), { recursive: true });
14948
- await deps.writeQrFile(qrFile, authorizationUrl);
15640
+ if (!appId) {
15641
+ throw new Error(t('upload.error.noAppId'));
14949
15642
  }
14950
- catch (error) {
14951
- throw new TestAccountCommandError('QR_WRITE_FAILED', error?.message || t('testAccount.error.qrWriteFailed'));
15643
+ const targetDir = options.dir
15644
+ ? path.resolve(process.cwd(), options.dir)
15645
+ : process.cwd();
15646
+ if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
15647
+ throw new Error(t('upload.error.dirNotExist', { dir: targetDir }));
14952
15648
  }
14953
- return {
14954
- schemaVersion: '1',
14955
- operationId,
14956
- ts: deps.now(),
14957
- operation: 'test-account.add',
14958
- status: 'authorization_required',
15649
+ emit(createEvent(eventBase, 'packaging', 0.2, { appId, projectName }));
15650
+ const zipBuffer = await deps.zipDirectory(targetDir);
15651
+ const packageSizeBytes = zipBuffer.length;
15652
+ emit(createEvent(eventBase, 'uploading', 0.4, {
15653
+ appId,
15654
+ projectName,
15655
+ packageSizeBytes,
15656
+ }));
15657
+ const uploadResponse = await deps.uploadGameToPlatform({
15658
+ data: zipBuffer.buffer.slice(zipBuffer.byteOffset, zipBuffer.byteOffset + zipBuffer.byteLength),
15659
+ name: projectName,
14959
15660
  clientKey,
15661
+ note,
15662
+ appId,
15663
+ });
15664
+ if (uploadResponse.error) {
15665
+ throw new Error(uploadResponse.error.message || t('upload.error.platformUploadFailed'));
15666
+ }
15667
+ const assetId = String(uploadResponse.data?.asset_id || '');
15668
+ const platformUrl = uploadResponse.data?.detail || '';
15669
+ if (!assetId) {
15670
+ throw new Error(t('upload.error.missingAssetId'));
15671
+ }
15672
+ if (!options.preview) {
15673
+ const event = createEvent(eventBase, 'ready', 1, {
15674
+ appId,
15675
+ projectName,
15676
+ packageSizeBytes,
15677
+ versionId: assetId,
15678
+ platformUrl,
15679
+ platformWrite: true,
15680
+ });
15681
+ emit(event);
15682
+ return event;
15683
+ }
15684
+ const deadline = deps.now() + deps.pollTimeoutMs;
15685
+ await waitForAssetReady({
15686
+ assetId,
15687
+ miniGameId: appId,
15688
+ deadline,
15689
+ eventBase,
15690
+ onEvent: emit,
15691
+ deps,
15692
+ });
15693
+ emit(createEvent(eventBase, 'processing', 0.85, {
14960
15694
  appId,
14961
15695
  projectName,
14962
- authorizationUrl,
14963
- qrPayload: authorizationUrl,
14964
- qrFile,
14965
- portalUrl: `https://developers.tiktok.com/portal/game/${appId}`,
14966
- platformWrite: false,
14967
- verification: 'developer_portal',
14968
- errorCode: null,
14969
- error: null,
14970
- };
15696
+ packageSizeBytes,
15697
+ versionId: assetId,
15698
+ platformStatus: 'resolving_preview',
15699
+ platformUrl,
15700
+ platformWrite: true,
15701
+ }));
15702
+ const previewUrl = await waitForPreviewUrl({ assetId, deadline, deps });
15703
+ let previewPage;
15704
+ if (options.servePreviewPage) {
15705
+ try {
15706
+ previewPage = await deps.startPreviewPageServer({
15707
+ authorizationUrl: buildTestAccountAuthorizationUrl(clientKey),
15708
+ previewUrl,
15709
+ clientKey,
15710
+ projectName,
15711
+ versionId: assetId,
15712
+ });
15713
+ }
15714
+ catch (error) {
15715
+ previewPage = {
15716
+ status: 'failed',
15717
+ contentType: 'text/html; charset=utf-8',
15718
+ errorCode: 'PREVIEW_PAGE_SERVER_FAILED',
15719
+ error: error?.message || String(error),
15720
+ };
15721
+ }
15722
+ }
15723
+ const event = createEvent(eventBase, 'ready', 1, {
15724
+ appId,
15725
+ projectName,
15726
+ packageSizeBytes,
15727
+ versionId: assetId,
15728
+ previewUrl,
15729
+ qrPayload: previewUrl,
15730
+ ...(previewPage ? { previewPage } : {}),
15731
+ platformUrl,
15732
+ platformStatus: 'ready',
15733
+ platformWrite: true,
15734
+ });
15735
+ emit(event);
15736
+ return event;
14971
15737
  }
14972
- async function testAccountAdd(options, dependencies = {}) {
14973
- const deps = { ...defaultDependencies$5, ...dependencies };
14974
- let format = 'text';
14975
- const operationId = deps.operationId();
14976
- try {
14977
- format = normalizeFormat$3(options.format);
14978
- const result = await generateTestAccountAuthorization(options, {
14979
- ...dependencies,
14980
- operationId: () => operationId,
15738
+ function getHumanEventText(event) {
15739
+ if (event.status === 'precheck') {
15740
+ return t('upload.spinner.fetchInfo', { clientKey: event.clientKey });
15741
+ }
15742
+ if (event.status === 'packaging') {
15743
+ return t('upload.spinner.zipping', { dir: '' });
15744
+ }
15745
+ if (event.status === 'uploading') {
15746
+ return t('upload.spinner.uploading', {
15747
+ size: ((event.packageSizeBytes || 0) / 1024 / 1024).toFixed(2),
15748
+ });
15749
+ }
15750
+ if (event.status === 'processing') {
15751
+ return t('upload.spinner.processing', {
15752
+ status: event.platformStatus || 'pending',
14981
15753
  });
15754
+ }
15755
+ return '';
15756
+ }
15757
+ function getPreviewPageConsoleText(page) {
15758
+ return [
15759
+ '',
15760
+ t('upload.previewPageHeading'),
15761
+ ` ${t('upload.previewPageOpenInBrowser')} ${page.url}`,
15762
+ ` ${t('upload.previewPageAccessScope')} ${t('upload.previewPageSameMachine')}`,
15763
+ ` ${t('upload.previewPageTerminalMode')} ${t('upload.previewPageClickableLink')}`,
15764
+ ` ${t('upload.previewPageExpiresAt')} ${page.expiresAt}`,
15765
+ ].join('\n');
15766
+ }
15767
+ function shouldOpenPreviewPage(format, options, page) {
15768
+ return (format === 'text' && options.open !== false && page?.status === 'ready');
15769
+ }
15770
+ async function upload(options, runtime = defaultUploadRuntime) {
15771
+ const format = normalizeOutputFormat(options.format);
15772
+ const operationId = `upload_${crypto.randomUUID()}`;
15773
+ const ora = await import('ora');
15774
+ const spinner = format === 'text'
15775
+ ? ora
15776
+ .default({ spinner: 'dots', text: t('upload.spinner.start') })
15777
+ .start()
15778
+ : null;
15779
+ let lastEvent = null;
15780
+ const onEvent = (event) => {
15781
+ lastEvent = event;
15782
+ if (format === 'ndjson') {
15783
+ console.log(JSON.stringify(event));
15784
+ return;
15785
+ }
15786
+ const nextText = getHumanEventText(event);
15787
+ if (nextText && spinner) {
15788
+ spinner.text = chalk.cyan.bold(nextText);
15789
+ }
15790
+ };
15791
+ try {
15792
+ const result = await executeUpload({ ...options, operationId, onEvent });
14982
15793
  if (format === 'json') {
14983
15794
  console.log(JSON.stringify(result));
14984
15795
  }
14985
- else {
14986
- console.log(chalk.green.bold(t('testAccount.success')));
14987
- console.log(t('testAccount.project', {
14988
- name: result.projectName,
14989
- clientKey: result.clientKey,
14990
- }));
14991
- console.log(t('testAccount.authorizationUrl', { url: result.authorizationUrl }));
14992
- console.log(t('testAccount.qrFile', { file: result.qrFile }));
14993
- console.log(chalk.yellow(t('testAccount.verifyInPortal')));
15796
+ else if (format === 'text' && spinner) {
15797
+ if (result.previewUrl) {
15798
+ spinner.succeed(chalk.green.bold(t('upload.previewSuccess', { url: result.previewUrl })));
15799
+ if (result.previewPage?.status === 'ready') {
15800
+ console.log(getPreviewPageConsoleText(result.previewPage));
15801
+ if (shouldOpenPreviewPage(format, options, result.previewPage)) {
15802
+ console.log(t('upload.previewPageOpening'));
15803
+ try {
15804
+ await runtime.openUrl(result.previewPage.url);
15805
+ }
15806
+ catch {
15807
+ console.error(chalk.yellow(t('upload.previewPageOpenFailed')));
15808
+ }
15809
+ }
15810
+ }
15811
+ else if (result.previewPage?.status === 'failed') {
15812
+ console.error(chalk.yellow(t('upload.previewPageFailed', {
15813
+ error: result.previewPage.error,
15814
+ })));
15815
+ }
15816
+ }
15817
+ else {
15818
+ spinner.succeed(chalk.green.bold(t('upload.success', { url: result.platformUrl })));
15819
+ }
14994
15820
  }
14995
15821
  return result;
14996
15822
  }
14997
15823
  catch (error) {
14998
15824
  const message = error?.message || String(error);
14999
- const errorCode = error instanceof TestAccountCommandError ? error.code : 'UNKNOWN_ERROR';
15000
- const failedResult = {
15001
- schemaVersion: '1',
15002
- operationId,
15003
- ts: deps.now(),
15004
- operation: 'test-account.add',
15005
- status: 'failed',
15006
- clientKey: options.clientKey || '',
15007
- appId: '',
15008
- projectName: '',
15009
- authorizationUrl: '',
15010
- qrPayload: '',
15011
- qrFile: '',
15012
- portalUrl: '',
15013
- platformWrite: false,
15014
- verification: 'developer_portal',
15015
- errorCode,
15825
+ const failedEvent = createEvent({ operationId, clientKey: options.clientKey || '' }, 'failed', lastEvent?.progress || 0, {
15826
+ appId: lastEvent?.appId,
15827
+ projectName: lastEvent?.projectName,
15828
+ packageSizeBytes: lastEvent?.packageSizeBytes,
15829
+ versionId: lastEvent?.versionId || '',
15830
+ platformUrl: lastEvent?.platformUrl || '',
15831
+ platformStatus: lastEvent?.platformStatus,
15832
+ platformStatusRaw: lastEvent?.platformStatusRaw,
15833
+ platformWrite: lastEvent?.platformWrite || false,
15834
+ errorCode: 'UPLOAD_FAILED',
15016
15835
  error: message,
15017
- };
15018
- if (format === 'json') {
15019
- console.log(JSON.stringify(failedResult));
15836
+ });
15837
+ if (format === 'json' || format === 'ndjson') {
15838
+ console.log(JSON.stringify(failedEvent));
15020
15839
  }
15021
15840
  else {
15022
- console.error(chalk.red.bold(t('testAccount.fail', { error: message })));
15841
+ spinner?.fail(chalk.red.bold(t('upload.fail', { error: message })));
15023
15842
  }
15024
15843
  process.exitCode = 1;
15025
- return failedResult;
15844
+ return failedEvent;
15026
15845
  }
15027
15846
  }
15028
15847
 
@@ -15117,7 +15936,7 @@ async function authStatus(options = {}) {
15117
15936
  return result;
15118
15937
  }
15119
15938
 
15120
- var version = "0.4.6-vibe-beta.2";
15939
+ var version = "0.4.6-vibe-beta.4";
15121
15940
  var engines = {
15122
15941
  node: ">=20.19.1 <21 || >=22"
15123
15942
  };
@@ -15173,7 +15992,9 @@ function getCapabilities() {
15173
15992
  platformWrite: false,
15174
15993
  },
15175
15994
  dev: {
15176
- command: 'ttmg dev --format json --events ndjson --no-open',
15995
+ command: 'ttmg dev --format json --no-open',
15996
+ eventsCommand: 'ttmg dev --events ndjson --no-open',
15997
+ outputModesMutuallyExclusive: true,
15177
15998
  available: true,
15178
15999
  platformWrite: false,
15179
16000
  timeout: true,
@@ -15185,6 +16006,31 @@ function getCapabilities() {
15185
16006
  command: 'ttmg upload --preview --format ndjson',
15186
16007
  available: true,
15187
16008
  platformWrite: true,
16009
+ previewPage: {
16010
+ option: '--serve-preview-page',
16011
+ command: 'ttmg upload --preview --serve-preview-page --format ndjson',
16012
+ available: true,
16013
+ localhostOnly: true,
16014
+ inMemory: true,
16015
+ reuseScope: 'clientKey',
16016
+ latestOnly: true,
16017
+ renewsTtlOnUpload: true,
16018
+ textModeAutoOpen: true,
16019
+ structuredModeAutoOpen: false,
16020
+ noOpenOption: '--no-open',
16021
+ presentation: {
16022
+ audience: 'user',
16023
+ preferredSurface: 'browser',
16024
+ preferredAction: 'open_url',
16025
+ terminalInline: false,
16026
+ fallback: 'show_clickable_link',
16027
+ sameHostRequired: true,
16028
+ },
16029
+ includesTestAccountAuthorization: true,
16030
+ includesPreviewQr: true,
16031
+ ttlSeconds: 3600,
16032
+ expiredNoticeTtlSeconds: 3600,
16033
+ },
15188
16034
  },
15189
16035
  },
15190
16036
  dev: {
@@ -15609,6 +16455,7 @@ function getDesktopPath() {
15609
16455
  }
15610
16456
 
15611
16457
  const program = new commander.Command();
16458
+ const isPreviewPageServerWorker = process.argv[2] === '__preview-page-server';
15612
16459
  const withVerboseOption = (command) => command.option('--verbose', t('cli.option.verbose'));
15613
16460
  const structuredFormatIndex = process.argv.indexOf('--format');
15614
16461
  const inlineStructuredFormat = process.argv.find(arg => arg.startsWith('--format='));
@@ -15617,7 +16464,8 @@ const structuredFormat = inlineStructuredFormat
15617
16464
  : structuredFormatIndex >= 0
15618
16465
  ? process.argv[structuredFormatIndex + 1]
15619
16466
  : '';
15620
- if (structuredFormat !== 'json' &&
16467
+ if (!isPreviewPageServerWorker &&
16468
+ structuredFormat !== 'json' &&
15621
16469
  structuredFormat !== 'ndjson' &&
15622
16470
  !process.argv.includes('--implicit')) {
15623
16471
  maybeShowPostInstallNotice(pkg.version);
@@ -15668,12 +16516,16 @@ withVerboseOption(program
15668
16516
  .option('-n, --note <note>', t('cli.command.upload.note'))
15669
16517
  .option('-d, --dir <dir>', t('cli.command.upload.dir'))
15670
16518
  .option('--preview', t('cli.command.upload.preview'))
16519
+ .option('--serve-preview-page', t('cli.command.upload.servePreviewPage'))
16520
+ .option('--no-open', t('cli.command.upload.noOpen'))
15671
16521
  .option('--format <format>', t('cli.command.upload.format'), 'text')).action(async (cmd) => {
15672
16522
  await upload({
15673
16523
  clientKey: cmd.clientKey,
15674
16524
  note: cmd.note,
15675
16525
  dir: cmd.dir,
15676
16526
  preview: cmd.preview,
16527
+ servePreviewPage: cmd.servePreviewPage,
16528
+ open: cmd.open,
15677
16529
  format: cmd.format,
15678
16530
  });
15679
16531
  });
@@ -15744,6 +16596,13 @@ const devCmd = program
15744
16596
  .option('--timeout <seconds>', t('cli.command.dev.timeout'));
15745
16597
  withVerboseOption(devCmd).action(async (cmd) => {
15746
16598
  const options = program.opts();
16599
+ const format = String(cmd.format || 'text').toLowerCase();
16600
+ const events = String(cmd.events || 'none').toLowerCase();
16601
+ if (format === 'json' && events === 'ndjson') {
16602
+ console.error(t('cli.command.dev.outputConflict'));
16603
+ process.exitCode = 1;
16604
+ return;
16605
+ }
15747
16606
  if (options.h5) {
15748
16607
  await dev$1();
15749
16608
  }
@@ -15878,7 +16737,12 @@ withVerboseOption(program
15878
16737
  console.log(t('cli.native.build.placeholder'));
15879
16738
  }
15880
16739
  });
15881
- program.parse(process.argv);
16740
+ if (isPreviewPageServerWorker) {
16741
+ runPreviewPageServerWorker();
16742
+ }
16743
+ else {
16744
+ program.parse(process.argv);
16745
+ }
15882
16746
 
15883
16747
  /**
15884
16748
  * 一个类型安全的、通用的事件发射器类。
@@ -21905,6 +22769,9 @@ function normalizeTimeout(timeout) {
21905
22769
  async function dev(options = {}) {
21906
22770
  const format = normalizeFormat(options.format);
21907
22771
  const events = normalizeEvents(options.events);
22772
+ if (format === 'json' && events === 'ndjson') {
22773
+ throw new Error('Dev output modes cannot be combined. Use either --format json or --events ndjson.');
22774
+ }
21908
22775
  const structured = format === 'json' || events === 'ndjson';
21909
22776
  const timeoutMs = normalizeTimeout(options.timeout);
21910
22777
  if (timeoutMs > 0 && !structured) {