@ttmg/cli 0.4.6-agent-beta.1 → 0.4.6-agent-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +218 -11
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -3
- package/dist/scripts/dev-default-smoke.js +22 -1
- package/dist/scripts/verify-agent-contract.js +40 -2
- package/dist/scripts/vibe-auth-open-test-preload.cjs +11 -0
- package/dist/scripts/vibe-upload-smoke.js +54 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -29,6 +29,16 @@ npm install @ttmg/cli --save-dev
|
|
|
29
29
|
|
|
30
30
|
#### Login 登录
|
|
31
31
|
|
|
32
|
+
To open an existing Vibe task in the system browser, use the `pageUrl` from its
|
|
33
|
+
`waiting_authorization` event: `ttmg auth open --page-url <pageUrl> --format json`.
|
|
34
|
+
Keep the upload process running; this command creates no task and reports only
|
|
35
|
+
`open_requested`, not authorization success. It runs on the CLI computer.
|
|
36
|
+
|
|
37
|
+
Skill 如需手动打开网页授权,读取上传进程 `waiting_authorization` 事件的 `pageUrl`,
|
|
38
|
+
执行 `ttmg auth open --page-url <pageUrl> --format json`。保持原上传进程运行;
|
|
39
|
+
新命令在 CLI 所在电脑打开系统浏览器,不新建任务,`open_requested` 不代表授权成功。
|
|
40
|
+
该功能需使用包含新命令的本地构建;已发布旧版不会因此自动增加命令。
|
|
41
|
+
|
|
32
42
|
The experimental `upload --vibe` local prototype does not require platform login.
|
|
33
43
|
Use `ttmg upload --vibe --mock` for the integration trial. No environment setup:
|
|
34
44
|
task creation/polling use the built-in PPE lane; authorization and uploads are
|
|
@@ -369,12 +379,17 @@ specified with `--evidence-dir`. Screenshot output prints the saved file path;
|
|
|
369
379
|
use `--output <path>` for a custom destination. Log follow ends after 30 seconds
|
|
370
380
|
by default without stopping preview; the owner terminal streams logs until you
|
|
371
381
|
stop preview with Ctrl+C. Both logs and screenshots require a compatible client.
|
|
382
|
+
Keep custom evidence outside the project or under its root `.ttmg` directory;
|
|
383
|
+
other in-project destinations fail before the session starts. CLI state is
|
|
384
|
+
excluded from game builds, package-size checks and directory uploads.
|
|
372
385
|
快捷命令自动找到当前工程(含子目录)唯一的运行中真机预览会话;多个会话时列出
|
|
373
386
|
候选,需用 `--session <session-id>` 明确选择,不会自动连接任意设备。
|
|
374
387
|
自定义证据目录使用 `--sessions-dir <path>`,可以指定会话集合目录或启动时的
|
|
375
388
|
`--evidence-dir`。截图完成后输出本地保存路径,支持 `--output <path>`。
|
|
376
389
|
`logs --follow` 默认读取 30 秒后退出,不停止预览;主终端持续输出日志,Ctrl+C
|
|
377
390
|
停止预览。日志和截图均需要客户端支持对应协议。
|
|
391
|
+
自定义证据目录须位于工程外或工程根目录的 `.ttmg` 下;其他工程内目录会在启动前
|
|
392
|
+
被拒绝。CLI 状态目录不进入游戏构建、包体积检查和目录上传。
|
|
378
393
|
|
|
379
394
|
For automation, use `dev logs --format json`, `dev logs --follow --format ndjson`,
|
|
380
395
|
or `dev screenshot --format json`. Successful results reuse the existing
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var https = require('https');
|
|
|
38
38
|
var dns = require('dns');
|
|
39
39
|
var boxen = require('boxen');
|
|
40
40
|
var semver = require('semver');
|
|
41
|
+
var path$1 = require('node:path');
|
|
41
42
|
var glob = require('glob');
|
|
42
43
|
var got = require('got');
|
|
43
44
|
var ttmgPack = require('ttmg-pack');
|
|
@@ -45,7 +46,6 @@ var expressStaticGzip = require('express-static-gzip');
|
|
|
45
46
|
var fileUpload = require('express-fileupload');
|
|
46
47
|
var crypto$1 = require('node:crypto');
|
|
47
48
|
var fs$1 = require('node:fs');
|
|
48
|
-
var path$1 = require('node:path');
|
|
49
49
|
var zlib = require('zlib');
|
|
50
50
|
var promises$1 = require('fs/promises');
|
|
51
51
|
var os$1 = require('node:os');
|
|
@@ -80,9 +80,9 @@ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
|
|
|
80
80
|
var net__namespace = /*#__PURE__*/_interopNamespaceDefault(net);
|
|
81
81
|
var require$$1__namespace = /*#__PURE__*/_interopNamespaceDefault(require$$1$1);
|
|
82
82
|
var dns__namespace = /*#__PURE__*/_interopNamespaceDefault(dns);
|
|
83
|
+
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
|
|
83
84
|
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
|
|
84
85
|
var fs__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(fs$1);
|
|
85
|
-
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);
|
|
86
86
|
|
|
87
87
|
async function openUrl(url) {
|
|
88
88
|
const { launch } = await import('chrome-launcher');
|
|
@@ -7231,6 +7231,13 @@ const messages = {
|
|
|
7231
7231
|
'cli.command.login.password': 'Developer account password (implicit mode)',
|
|
7232
7232
|
'cli.command.logout.desc': 'Logout and clear local user data',
|
|
7233
7233
|
'cli.command.auth.desc': 'Inspect local authentication state',
|
|
7234
|
+
'cli.command.auth.open.desc': 'Open an existing Vibe task authorization page in the system browser',
|
|
7235
|
+
'cli.command.auth.open.pageUrl': 'Local pageUrl returned by the running Vibe upload command',
|
|
7236
|
+
'cli.command.auth.open.requested': 'Browser open requested. Complete authorization there; the existing upload process continues polling.',
|
|
7237
|
+
'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
|
|
7238
|
+
'cli.command.auth.open.unavailable': 'Authorization page is unavailable, expired, or no longer waiting. Check the existing upload process; no new task was created.',
|
|
7239
|
+
'cli.command.auth.open.invalidFormat': 'Use text, json, or ndjson output.',
|
|
7240
|
+
'cli.command.auth.open.failed': 'Could not open the system browser. Retry this command on the CLI computer.',
|
|
7234
7241
|
'cli.command.auth.status.desc': 'Return whether the local developer session is reusable',
|
|
7235
7242
|
'cli.command.auth.status.format': 'Output format: text | json',
|
|
7236
7243
|
'cli.command.auth.status.requireAuth': 'Exit non-zero when no reusable local session exists',
|
|
@@ -7258,7 +7265,8 @@ const messages = {
|
|
|
7258
7265
|
'cli.command.dev.format': 'Session output format: text | json',
|
|
7259
7266
|
'cli.command.dev.events': 'Lifecycle event stream: none | ndjson',
|
|
7260
7267
|
'cli.command.dev.noOpen': 'Do not automatically open the IDE or scan page',
|
|
7261
|
-
'cli.command.dev.evidenceDir': 'Session evidence output directory',
|
|
7268
|
+
'cli.command.dev.evidenceDir': 'Session evidence output directory (outside the project or under its .ttmg directory)',
|
|
7269
|
+
'dev.evidence.unsafeDirectory': 'Keep session evidence outside the game project or under its root .ttmg directory so it cannot enter game packages.',
|
|
7262
7270
|
'dev.logs.recordModeRequired': '--record-logs requires --mode client-preview.',
|
|
7263
7271
|
'dev.logs.archiveFailed': 'Local log recording stopped [DEV_LOG_ARCHIVE_WRITE_FAILED]. Live debugging is still available.',
|
|
7264
7272
|
'cli.command.dev.recordLogs': 'Save redacted Runtime logs locally (client-preview only; bounded retention)',
|
|
@@ -7635,6 +7643,13 @@ const messages = {
|
|
|
7635
7643
|
'cli.command.login.password': '开发者账号密码(implicit 模式)',
|
|
7636
7644
|
'cli.command.logout.desc': '退出登录并清除本地用户信息',
|
|
7637
7645
|
'cli.command.auth.desc': '查看本地登录状态',
|
|
7646
|
+
'cli.command.auth.open.desc': '在系统浏览器打开已有 Vibe 任务的授权页',
|
|
7647
|
+
'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
|
|
7648
|
+
'cli.command.auth.open.requested': '已请求浏览器打开授权页。请在页面完成授权,原上传进程继续轮询。',
|
|
7649
|
+
'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
|
|
7650
|
+
'cli.command.auth.open.unavailable': '授权页不可用、已过期或已不在等待授权。请查看原上传进程状态;未新建任务。',
|
|
7651
|
+
'cli.command.auth.open.invalidFormat': '输出格式请使用 text、json 或 ndjson。',
|
|
7652
|
+
'cli.command.auth.open.failed': '无法打开系统浏览器,请在 CLI 所在电脑重试此命令。',
|
|
7638
7653
|
'cli.command.auth.status.desc': '返回本地开发者会话是否可复用',
|
|
7639
7654
|
'cli.command.auth.status.format': '输出格式:text | json',
|
|
7640
7655
|
'cli.command.auth.status.requireAuth': '没有可复用本地会话时以非零状态退出',
|
|
@@ -7662,7 +7677,8 @@ const messages = {
|
|
|
7662
7677
|
'cli.command.dev.format': '会话输出格式:text | json',
|
|
7663
7678
|
'cli.command.dev.events': '生命周期事件流:none | ndjson',
|
|
7664
7679
|
'cli.command.dev.noOpen': '不自动打开 IDE 或扫码网页',
|
|
7665
|
-
'cli.command.dev.evidenceDir': '
|
|
7680
|
+
'cli.command.dev.evidenceDir': '会话证据输出目录(工程外或工程根目录的 .ttmg 下)',
|
|
7681
|
+
'dev.evidence.unsafeDirectory': '请将会话证据放在游戏工程外,或工程根目录的 .ttmg 下,避免日志、截图和构建输出进入游戏包。',
|
|
7666
7682
|
'dev.logs.recordModeRequired': '--record-logs 仅支持 --mode client-preview。',
|
|
7667
7683
|
'dev.logs.archiveFailed': '本地日志留存失败 [DEV_LOG_ARCHIVE_WRITE_FAILED],已停止留存;实时调试仍可继续。',
|
|
7668
7684
|
'cli.command.dev.recordLogs': '将脱敏后的 Runtime 日志保存在本地(仅 client-preview,有容量上限)',
|
|
@@ -8606,6 +8622,8 @@ const zipCwdToBuffer = (customIgnores = [], targetDir = process.cwd()) => {
|
|
|
8606
8622
|
// 推到设备,体积虚胖外,host 在 fallback 路径下还可能错误命中未插桩 wasm。
|
|
8607
8623
|
// 同时保留裸名 `TTMG_TEMP_DIR` 以兜底空目录场景。
|
|
8608
8624
|
const defaultIgnores = [
|
|
8625
|
+
'.ttmg',
|
|
8626
|
+
'.ttmg/**',
|
|
8609
8627
|
'node_modules/**',
|
|
8610
8628
|
'.git/**',
|
|
8611
8629
|
'.agents/**',
|
|
@@ -16288,7 +16306,7 @@ async function authStatus(options = {}) {
|
|
|
16288
16306
|
return result;
|
|
16289
16307
|
}
|
|
16290
16308
|
|
|
16291
|
-
var version = "0.4.6-agent-beta.
|
|
16309
|
+
var version = "0.4.6-agent-beta.2";
|
|
16292
16310
|
var engines = {
|
|
16293
16311
|
node: ">=20.19.1 <21 || >=22"
|
|
16294
16312
|
};
|
|
@@ -16380,6 +16398,18 @@ function getCapabilities() {
|
|
|
16380
16398
|
available: true,
|
|
16381
16399
|
platformWrite: false,
|
|
16382
16400
|
},
|
|
16401
|
+
vibeAuthOpen: {
|
|
16402
|
+
command: 'ttmg auth open --page-url <pageUrl> --format json',
|
|
16403
|
+
available: true,
|
|
16404
|
+
schemaVersion: '1',
|
|
16405
|
+
platformWrite: false,
|
|
16406
|
+
requiresRunningUpload: true,
|
|
16407
|
+
createsTask: false,
|
|
16408
|
+
outputContainsLandingCredentials: false,
|
|
16409
|
+
target: 'system-browser',
|
|
16410
|
+
successStatus: 'open_requested',
|
|
16411
|
+
provesAuthorization: false,
|
|
16412
|
+
},
|
|
16383
16413
|
loginImplicit: {
|
|
16384
16414
|
command: 'ttmg login --implicit',
|
|
16385
16415
|
available: true,
|
|
@@ -16511,6 +16541,8 @@ function getCapabilities() {
|
|
|
16511
16541
|
verifiedOnDevice: false,
|
|
16512
16542
|
},
|
|
16513
16543
|
sessionLogs: {
|
|
16544
|
+
evidenceDirectoryLocations: ['outside-project', 'project-root-.ttmg'],
|
|
16545
|
+
unsafeEvidenceDirectoryError: 'DEV_EVIDENCE_DIRECTORY_UNSAFE',
|
|
16514
16546
|
sources: ['live', 'archive'],
|
|
16515
16547
|
defaultSource: 'live',
|
|
16516
16548
|
runtimeSessionOption: '--runtime-session',
|
|
@@ -17054,6 +17086,13 @@ withVerboseOption(program.command('logout').description(t('cli.command.logout.de
|
|
|
17054
17086
|
await logout();
|
|
17055
17087
|
});
|
|
17056
17088
|
const authCmd = program.command('auth').description(t('cli.command.auth.desc'));
|
|
17089
|
+
withVerboseOption(authCmd.command('open')
|
|
17090
|
+
.description(t('cli.command.auth.open.desc'))
|
|
17091
|
+
.option('--page-url <url>', t('cli.command.auth.open.pageUrl'))
|
|
17092
|
+
.option('--format <format>', t('cli.command.upload.format'), 'text')).action(async (cmd) => {
|
|
17093
|
+
const { vibeAuthOpen } = await Promise.resolve().then(function () { return authOpen; });
|
|
17094
|
+
await vibeAuthOpen(cmd);
|
|
17095
|
+
});
|
|
17057
17096
|
withVerboseOption(authCmd
|
|
17058
17097
|
.command('status')
|
|
17059
17098
|
.description(t('cli.command.auth.status.desc'))
|
|
@@ -17539,6 +17578,115 @@ class VibeError extends Error {
|
|
|
17539
17578
|
}
|
|
17540
17579
|
}
|
|
17541
17580
|
|
|
17581
|
+
// Only the backend-owned authorization page can be a browser redirect target.
|
|
17582
|
+
// Never return its import_token to Skill stdout or put it in a CLI argument.
|
|
17583
|
+
function browserLandingUrl(value) {
|
|
17584
|
+
if (typeof value !== 'string' || value.length > 16384 || /[\r\n]/.test(value))
|
|
17585
|
+
return;
|
|
17586
|
+
try {
|
|
17587
|
+
const url = new URL(value);
|
|
17588
|
+
if (url.origin !== 'https://developers.tiktok.com' || url.username || url.password || url.hash ||
|
|
17589
|
+
url.pathname !== '/portal_h5/minis_import' ||
|
|
17590
|
+
url.searchParams.getAll('task_id').length !== 1 ||
|
|
17591
|
+
url.searchParams.getAll('import_token').length !== 1 ||
|
|
17592
|
+
!/^\d{1,19}$/.test(url.searchParams.get('task_id') || '') ||
|
|
17593
|
+
!url.searchParams.get('import_token'))
|
|
17594
|
+
return;
|
|
17595
|
+
return url.href;
|
|
17596
|
+
}
|
|
17597
|
+
catch {
|
|
17598
|
+
return;
|
|
17599
|
+
}
|
|
17600
|
+
}
|
|
17601
|
+
function authorizationOpenUrl(pageUrl) {
|
|
17602
|
+
try {
|
|
17603
|
+
if (typeof pageUrl !== 'string')
|
|
17604
|
+
throw new Error();
|
|
17605
|
+
const url = new URL(pageUrl);
|
|
17606
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port ||
|
|
17607
|
+
url.username || url.password || url.search || url.hash ||
|
|
17608
|
+
!/^\/[a-f0-9]{48}$/.test(url.pathname))
|
|
17609
|
+
throw new Error();
|
|
17610
|
+
return `${url.origin}${url.pathname}/authorize`;
|
|
17611
|
+
}
|
|
17612
|
+
catch {
|
|
17613
|
+
throw new VibeError('VIBE_INVALID_PAGE_URL');
|
|
17614
|
+
}
|
|
17615
|
+
}
|
|
17616
|
+
function checkAuthorizationPage(url) {
|
|
17617
|
+
return new Promise((resolve, reject) => {
|
|
17618
|
+
// HEAD never follows the redirect or requests the platform. The browser GET
|
|
17619
|
+
// rechecks readiness, covering state changes between the probe and opening.
|
|
17620
|
+
const req = http.request(url, { method: 'HEAD' }, res => {
|
|
17621
|
+
res.resume();
|
|
17622
|
+
if (res.statusCode === 204 && res.headers['x-ttmg-authorization'] === 'ready')
|
|
17623
|
+
resolve();
|
|
17624
|
+
else
|
|
17625
|
+
reject(new VibeError('VIBE_AUTH_PAGE_UNAVAILABLE'));
|
|
17626
|
+
});
|
|
17627
|
+
const timer = setTimeout(() => req.destroy(new Error('timeout')), 5000);
|
|
17628
|
+
req.once('close', () => clearTimeout(timer));
|
|
17629
|
+
req.once('error', () => reject(new VibeError('VIBE_AUTH_PAGE_UNAVAILABLE')));
|
|
17630
|
+
req.end();
|
|
17631
|
+
});
|
|
17632
|
+
}
|
|
17633
|
+
async function executeVibeAuthOpen(options, deps = { open: async (url) => {
|
|
17634
|
+
const child = await (await import('open')).default(url);
|
|
17635
|
+
return new Promise((resolve, reject) => {
|
|
17636
|
+
child.once('error', reject);
|
|
17637
|
+
if (child.pid)
|
|
17638
|
+
resolve(child);
|
|
17639
|
+
else
|
|
17640
|
+
child.once('spawn', () => resolve(child));
|
|
17641
|
+
});
|
|
17642
|
+
} }) {
|
|
17643
|
+
const result = {
|
|
17644
|
+
schemaVersion: '1', command: 'auth open', operationId: `auth_open_${crypto.randomUUID()}`,
|
|
17645
|
+
status: 'failed', platformWrite: false, browserOpenRequested: false,
|
|
17646
|
+
errorCode: null, error: null,
|
|
17647
|
+
};
|
|
17648
|
+
try {
|
|
17649
|
+
if (!['text', 'json', 'ndjson'].includes(options.format || 'text'))
|
|
17650
|
+
throw new VibeError('VIBE_INVALID_FORMAT');
|
|
17651
|
+
const url = authorizationOpenUrl(options.pageUrl);
|
|
17652
|
+
await checkAuthorizationPage(url);
|
|
17653
|
+
try {
|
|
17654
|
+
await deps.open(url);
|
|
17655
|
+
}
|
|
17656
|
+
catch {
|
|
17657
|
+
throw new VibeError('VIBE_BROWSER_OPEN_FAILED');
|
|
17658
|
+
}
|
|
17659
|
+
result.status = 'open_requested';
|
|
17660
|
+
result.browserOpenRequested = true;
|
|
17661
|
+
}
|
|
17662
|
+
catch (error) {
|
|
17663
|
+
result.errorCode = error instanceof VibeError ? error.code : 'VIBE_BROWSER_OPEN_FAILED';
|
|
17664
|
+
const key = result.errorCode === 'VIBE_INVALID_PAGE_URL' ? 'invalidPage'
|
|
17665
|
+
: result.errorCode === 'VIBE_AUTH_PAGE_UNAVAILABLE' ? 'unavailable'
|
|
17666
|
+
: result.errorCode === 'VIBE_INVALID_FORMAT' ? 'invalidFormat' : 'failed';
|
|
17667
|
+
result.error = t(`cli.command.auth.open.${key}`);
|
|
17668
|
+
}
|
|
17669
|
+
return result;
|
|
17670
|
+
}
|
|
17671
|
+
async function vibeAuthOpen(options) {
|
|
17672
|
+
const result = await executeVibeAuthOpen(options);
|
|
17673
|
+
if (options.format === 'json' || options.format === 'ndjson')
|
|
17674
|
+
console.log(JSON.stringify(result));
|
|
17675
|
+
else
|
|
17676
|
+
console.log(result.error || t('cli.command.auth.open.requested'));
|
|
17677
|
+
if (result.errorCode)
|
|
17678
|
+
process.exitCode = 1;
|
|
17679
|
+
return result;
|
|
17680
|
+
}
|
|
17681
|
+
|
|
17682
|
+
var authOpen = /*#__PURE__*/Object.freeze({
|
|
17683
|
+
__proto__: null,
|
|
17684
|
+
authorizationOpenUrl: authorizationOpenUrl,
|
|
17685
|
+
browserLandingUrl: browserLandingUrl,
|
|
17686
|
+
executeVibeAuthOpen: executeVibeAuthOpen,
|
|
17687
|
+
vibeAuthOpen: vibeAuthOpen
|
|
17688
|
+
});
|
|
17689
|
+
|
|
17542
17690
|
// BFF IDL: feat-minis-import. PPE task creation/polling only; no production adapter.
|
|
17543
17691
|
const prefix = '/bff/app_info/minis_import/';
|
|
17544
17692
|
const MINIS_IMPORT_PATHS = {
|
|
@@ -18427,9 +18575,27 @@ async function renderVibePage(input) {
|
|
|
18427
18575
|
async function createVibePageServer() {
|
|
18428
18576
|
const route = `/${crypto.randomBytes(24).toString('hex')}`;
|
|
18429
18577
|
let html = 'Starting…';
|
|
18578
|
+
let landingUrl;
|
|
18430
18579
|
const server = http.createServer((req, res) => {
|
|
18431
18580
|
const address = server.address();
|
|
18432
18581
|
const host = address && typeof address !== 'string' ? `127.0.0.1:${address.port}` : '';
|
|
18582
|
+
if (req.url === `${route}/authorize` && req.headers.host === host &&
|
|
18583
|
+
['GET', 'HEAD'].includes(req.method || '') &&
|
|
18584
|
+
(!req.headers.origin || req.headers.origin === `http://${host}`) &&
|
|
18585
|
+
req.headers['sec-fetch-site'] !== 'cross-site') {
|
|
18586
|
+
const headers = { 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' };
|
|
18587
|
+
if (!landingUrl) {
|
|
18588
|
+
res.writeHead(410, { ...headers, 'Content-Type': 'text/plain; charset=utf-8' });
|
|
18589
|
+
res.end('Authorization is no longer available. Return to the upload process.');
|
|
18590
|
+
}
|
|
18591
|
+
else if (req.method === 'HEAD') {
|
|
18592
|
+
res.writeHead(204, { ...headers, 'X-TTMG-Authorization': 'ready' }).end();
|
|
18593
|
+
}
|
|
18594
|
+
else {
|
|
18595
|
+
res.writeHead(302, { ...headers, Location: landingUrl }).end();
|
|
18596
|
+
}
|
|
18597
|
+
return;
|
|
18598
|
+
}
|
|
18433
18599
|
if (req.method !== 'GET' ||
|
|
18434
18600
|
req.url !== route ||
|
|
18435
18601
|
req.headers.host !== host) {
|
|
@@ -18452,8 +18618,9 @@ async function createVibePageServer() {
|
|
|
18452
18618
|
const address = server.address();
|
|
18453
18619
|
return {
|
|
18454
18620
|
url: `http://127.0.0.1:${address.port}${route}`,
|
|
18455
|
-
update: (value) => {
|
|
18621
|
+
update: (value, authorization) => {
|
|
18456
18622
|
html = value;
|
|
18623
|
+
landingUrl = browserLandingUrl(authorization);
|
|
18457
18624
|
},
|
|
18458
18625
|
close: () => server.close(),
|
|
18459
18626
|
};
|
|
@@ -18475,7 +18642,7 @@ async function runVibePageWorker() {
|
|
|
18475
18642
|
if (message?.type === 'render' &&
|
|
18476
18643
|
typeof message.html === 'string' &&
|
|
18477
18644
|
message.html.length < VIBE_PAGE_HTML_LIMIT) {
|
|
18478
|
-
server.update(message.html);
|
|
18645
|
+
server.update(message.html, message.browserLandingUrl);
|
|
18479
18646
|
process.send?.({ type: 'rendered', id: message.id });
|
|
18480
18647
|
}
|
|
18481
18648
|
if (message?.type === 'close') {
|
|
@@ -18523,7 +18690,8 @@ async function startVibePage() {
|
|
|
18523
18690
|
const html = await renderVibePage(input);
|
|
18524
18691
|
const current = ++id;
|
|
18525
18692
|
const ack = receive(m => m?.type === 'rendered' && m.id === current);
|
|
18526
|
-
|
|
18693
|
+
const landingUrl = input.phase === 'binding' ? browserLandingUrl(input.browserLandingUrl) : undefined;
|
|
18694
|
+
child.send({ type: 'render', id: current, html, browserLandingUrl: landingUrl }, error => {
|
|
18527
18695
|
if (error)
|
|
18528
18696
|
child.emit('error', error);
|
|
18529
18697
|
});
|
|
@@ -18802,12 +18970,15 @@ async function executeVibeUpload(options, onEvent = () => { }, dependencies = {}
|
|
|
18802
18970
|
simulation: result.simulation,
|
|
18803
18971
|
message: bindingMessage,
|
|
18804
18972
|
qrUrl: authorizationUrl,
|
|
18973
|
+
browserLandingUrl: task.landingUrl,
|
|
18805
18974
|
purpose: 'authorization_binding',
|
|
18806
18975
|
});
|
|
18807
18976
|
ensureActive();
|
|
18808
18977
|
emit('waiting_authorization', {
|
|
18809
18978
|
message: bindingMessage,
|
|
18810
18979
|
pageUrl: page.url,
|
|
18980
|
+
authorizationOpenCommand: browserLandingUrl(task.landingUrl)
|
|
18981
|
+
? `ttmg auth open --page-url ${page.url} --format json` : undefined,
|
|
18811
18982
|
qrPurpose: 'authorization_binding',
|
|
18812
18983
|
nextAction: result.simulation ? 'wait' : 'scan_qr',
|
|
18813
18984
|
readyForInput: false,
|
|
@@ -19913,7 +20084,12 @@ async function watch(options = {}) {
|
|
|
19913
20084
|
/**
|
|
19914
20085
|
* 忽略 wasmcode/cache 目录
|
|
19915
20086
|
*/
|
|
19916
|
-
ignored:
|
|
20087
|
+
ignored: file => {
|
|
20088
|
+
if (/(^|[\/\\])(\.git|node_modules|__TTMG_TEMP__)/.test(file))
|
|
20089
|
+
return true;
|
|
20090
|
+
const relative = path$1.relative(process.cwd(), file);
|
|
20091
|
+
return relative === '.ttmg' || relative.startsWith(`.ttmg${path$1.sep}`);
|
|
20092
|
+
},
|
|
19917
20093
|
ignoreInitial: true, // 忽略初始添加事件
|
|
19918
20094
|
persistent: true,
|
|
19919
20095
|
awaitWriteFinish: {
|
|
@@ -27737,7 +27913,7 @@ function requireOperationId(value) {
|
|
|
27737
27913
|
async function snapshotProject(source, signal) {
|
|
27738
27914
|
const directory = await fsp.mkdtemp(path$1.join(os$1.tmpdir(), 'ttmg-iteration-'));
|
|
27739
27915
|
const hash = crypto$1.createHash('sha256');
|
|
27740
|
-
const ignored = new Set(['.git', '
|
|
27916
|
+
const ignored = new Set(['.git', 'node_modules', '__TTMG_TEMP__']);
|
|
27741
27917
|
const dispose = () => fs$1.rmSync(directory, { recursive: true, force: true });
|
|
27742
27918
|
try {
|
|
27743
27919
|
async function copy(relative) {
|
|
@@ -27746,7 +27922,7 @@ async function snapshotProject(source, signal) {
|
|
|
27746
27922
|
});
|
|
27747
27923
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
27748
27924
|
signal?.throwIfAborted();
|
|
27749
|
-
if (ignored.has(entry.name))
|
|
27925
|
+
if (ignored.has(entry.name) || (!relative && entry.name === '.ttmg'))
|
|
27750
27926
|
continue;
|
|
27751
27927
|
const name = path$1.join(relative, entry.name);
|
|
27752
27928
|
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile()))
|
|
@@ -28047,6 +28223,24 @@ class PreviewWorkflow {
|
|
|
28047
28223
|
}
|
|
28048
28224
|
}
|
|
28049
28225
|
|
|
28226
|
+
function physicalPath(directory) {
|
|
28227
|
+
const absolute = path$1.resolve(directory);
|
|
28228
|
+
if (fs$1.existsSync(absolute))
|
|
28229
|
+
return fs$1.realpathSync(absolute);
|
|
28230
|
+
const parent = path$1.dirname(absolute);
|
|
28231
|
+
if (parent === absolute)
|
|
28232
|
+
return absolute;
|
|
28233
|
+
return path$1.join(physicalPath(parent), path$1.basename(absolute));
|
|
28234
|
+
}
|
|
28235
|
+
// Packaging reserves only the project-root .ttmg directory. Never silently put
|
|
28236
|
+
// session output into an ordinary game asset directory (including via symlinks).
|
|
28237
|
+
function isSafeEvidenceDirectory(project, directory) {
|
|
28238
|
+
const relative = path$1.relative(physicalPath(project), physicalPath(directory));
|
|
28239
|
+
return path$1.isAbsolute(relative) || relative === '..' ||
|
|
28240
|
+
relative.startsWith(`..${path$1.sep}`) || relative === '.ttmg' ||
|
|
28241
|
+
relative.startsWith(`.ttmg${path$1.sep}`);
|
|
28242
|
+
}
|
|
28243
|
+
|
|
28050
28244
|
function normalizeMode(mode) {
|
|
28051
28245
|
const value = (mode || 'default').toLowerCase();
|
|
28052
28246
|
if (value !== 'default' && value !== 'client-preview') {
|
|
@@ -28116,6 +28310,17 @@ async function dev(options = {}) {
|
|
|
28116
28310
|
}
|
|
28117
28311
|
const humanPreview = mode === 'client-preview' && !structured;
|
|
28118
28312
|
const managed = structured || humanPreview;
|
|
28313
|
+
if (managed && !isSafeEvidenceDirectory(process.cwd(), options.evidenceDir || path$1.join('.ttmg', 'sessions'))) {
|
|
28314
|
+
const failed = { schemaVersion: '1', command: 'dev', status: 'failed',
|
|
28315
|
+
sessionId: null, platformWrite: false, errorCode: 'DEV_EVIDENCE_DIRECTORY_UNSAFE',
|
|
28316
|
+
error: t('dev.evidence.unsafeDirectory') };
|
|
28317
|
+
if (structured)
|
|
28318
|
+
console.log(JSON.stringify(failed));
|
|
28319
|
+
else
|
|
28320
|
+
console.error(failed.error);
|
|
28321
|
+
process.exitCode = 1;
|
|
28322
|
+
return failed;
|
|
28323
|
+
}
|
|
28119
28324
|
const humanOutput = humanPreview ? new HumanPreviewOutput() : null;
|
|
28120
28325
|
const scanPage = mode === 'client-preview' ? new ClientPreviewScanPage() : null;
|
|
28121
28326
|
const timeoutMs = normalizeTimeout(options.timeout);
|
|
@@ -29993,6 +30198,8 @@ async function startBackgroundPreview(options) {
|
|
|
29993
30198
|
const clientKey = options.clientKey?.trim();
|
|
29994
30199
|
if (!clientKey)
|
|
29995
30200
|
throw workflowError('DEV_ITERATION_INVALID_OPTIONS', 'Pass --client-key when starting a background preview.');
|
|
30201
|
+
if (!isSafeEvidenceDirectory(process.cwd(), path$1.dirname(lock)))
|
|
30202
|
+
throw workflowError('DEV_EVIDENCE_DIRECTORY_UNSAFE', 'The project .ttmg directory must not resolve to a game asset directory.');
|
|
29996
30203
|
fs$1.mkdirSync(path$1.dirname(lock), { recursive: true });
|
|
29997
30204
|
try {
|
|
29998
30205
|
fs$1.mkdirSync(lock, { mode: 0o700 });
|