@ttmg/cli 0.4.5 → 0.4.6-agent-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +644 -2
  2. package/dist/index.js +22502 -6070
  3. package/dist/index.js.map +1 -1
  4. package/dist/package.json +36 -4
  5. package/dist/public/assets/{Card-BAsimNoh.js → Card-CN2lxY0Y.js} +1 -1
  6. package/dist/public/assets/{Detail-rVxbEDgh.js → Detail-CELZZ-4h.js} +1 -1
  7. package/dist/public/assets/Detail-CELZZ-4h.js.br +0 -0
  8. package/dist/public/assets/{MonetizationMode-B20TAE9o.js → MonetizationMode-C7DNqium.js} +1 -1
  9. package/dist/public/assets/{MonetizationModeSummary-CnHqXyco.js → MonetizationModeSummary-DYE7XUq1.js} +1 -1
  10. package/dist/public/assets/{SectionHeader-Cn3FnOqh.js → SectionHeader-BKc3BWXP.js} +1 -1
  11. package/dist/public/assets/{baseForm-kBTxn8h8.js → baseForm-I9suIiQn.js} +1 -1
  12. package/dist/public/assets/baseForm-I9suIiQn.js.br +0 -0
  13. package/dist/public/assets/{col-VN3Ivd-x.js → col-w9Q-2mgM.js} +1 -1
  14. package/dist/public/assets/{compass-BabZVjAl.js → compass-D6waJ52O.js} +1 -1
  15. package/dist/public/assets/{index-DHdJsD10.js → index-8sDAOAcS.js} +1 -1
  16. package/dist/public/assets/index-8sDAOAcS.js.br +0 -0
  17. package/dist/public/assets/{index-DK4IvdRl.js → index-B6hY5GdS.js} +1 -1
  18. package/dist/public/assets/{index-Cl0_vMWE.js → index-BIrFkB6r.js} +1 -1
  19. package/dist/public/assets/index-BIrFkB6r.js.br +0 -0
  20. package/dist/public/assets/{index-BjQlJrX7.js → index-BLWgpW64.js} +1 -1
  21. package/dist/public/assets/{index-odEI7YQF.js → index-BY3eQmfP.js} +1 -1
  22. package/dist/public/assets/index-BY3eQmfP.js.br +0 -0
  23. package/dist/public/assets/{index-Bjn0ctEd.js → index-BegU9LBT.js} +2 -2
  24. package/dist/public/assets/index-BegU9LBT.js.br +0 -0
  25. package/dist/public/assets/{index-1WjGwqIV.js → index-BqwflQml.js} +1 -1
  26. package/dist/public/assets/{index-DXDZCuG4.js → index-BrOFdVbv.js} +1 -1
  27. package/dist/public/assets/{index-BxsB5E3u.js → index-BvPolBSn.js} +1 -1
  28. package/dist/public/assets/{index-BnOeYQAd.js → index-BwrZjkCc.js} +1 -1
  29. package/dist/public/assets/{index-CUuX90R9.js → index-CLKKui8G.js} +1 -1
  30. package/dist/public/assets/{index-Dohibgdp.js → index-Cb4Ja0tZ.js} +1 -1
  31. package/dist/public/assets/{index-C83QHLmD.js → index-CcUgN29Y.js} +1 -1
  32. package/dist/public/assets/{index-aSl6Wdrp.js → index-CgXOkYrX.js} +1 -1
  33. package/dist/public/assets/index-CgXOkYrX.js.br +0 -0
  34. package/dist/public/assets/{index-DulANwbq.js → index-CsiIrLKY.js} +1 -1
  35. package/dist/public/assets/{index-Ba-JWe4X.js → index-Csmg5oyu.js} +1 -1
  36. package/dist/public/assets/{index-DnnfZ7H_.js → index-DDecPuod.js} +1 -1
  37. package/dist/public/assets/{index-Clq0hTDI.js → index-DDy4PkWT.js} +1 -1
  38. package/dist/public/assets/{index-DIcz5ciL.js → index-DEQ2hVPA.js} +1 -1
  39. package/dist/public/assets/index-DEQ2hVPA.js.br +0 -0
  40. package/dist/public/assets/{index-CpM8t743.js → index-DGkgqtgJ.js} +1 -1
  41. package/dist/public/assets/{index-QNLvejKK.js → index-FVHabbDB.js} +1 -1
  42. package/dist/public/assets/{index-CzXMNZL3.js → index-M8jf-YAZ.js} +1 -1
  43. package/dist/public/assets/{index-BxokHznc.js → index-Q0GC8Irj.js} +1 -1
  44. package/dist/public/assets/index-Q0GC8Irj.js.br +0 -0
  45. package/dist/public/assets/{index--6iHQzho.js → index-YVB9ZW00.js} +1 -1
  46. package/dist/public/assets/{index-O_L1tU2b.js → index-mjEeEQcC.js} +1 -1
  47. package/dist/public/assets/index-mjEeEQcC.js.br +0 -0
  48. package/dist/public/assets/{index-BFFsJ47B.js → index-rLkONFpx.js} +1 -1
  49. package/dist/public/assets/{layers-B4zhx14P.js → layers-Dbwq9AyL.js} +1 -1
  50. package/dist/public/assets/{list-checks-Br6imjXC.js → list-checks-og1usOZf.js} +1 -1
  51. package/dist/public/assets/{sparkles-BjIkcbAi.js → sparkles-p-dZbjGb.js} +1 -1
  52. package/dist/public/assets/{triangle-alert-CgKAc3w5.js → triangle-alert-BZVAEUiL.js} +1 -1
  53. package/dist/public/assets/{zap-DFHw5liD.js → zap-BAhZUPPa.js} +1 -1
  54. package/dist/public/index.html +1 -1
  55. package/dist/scripts/acceptance-fixture.test.js +33 -0
  56. package/dist/scripts/dev-capture-smoke.js +76 -0
  57. package/dist/scripts/dev-client-preview-control-smoke.js +606 -0
  58. package/dist/scripts/dev-default-smoke.js +67 -0
  59. package/dist/scripts/dev-game-ready-smoke.js +155 -0
  60. package/dist/scripts/dev-workflow-smoke.js +260 -0
  61. package/dist/scripts/prepare-client-preview-acceptance.js +65 -0
  62. package/dist/scripts/preview-page-server-smoke.js +63 -0
  63. package/dist/scripts/run-ts-test.js +41 -0
  64. package/dist/scripts/verify-agent-contract.js +410 -0
  65. package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
  66. package/dist/scripts/vibe-upload-demo.js +72 -0
  67. package/dist/scripts/vibe-upload-smoke.js +412 -0
  68. package/dist/scripts/wasm-smoke.js +53 -0
  69. package/dist/scripts/wasm-worker.js +63 -0
  70. package/dist/scripts/worker.js +28 -22
  71. package/package.json +36 -4
  72. package/CHANGELOG.md +0 -277
  73. package/dist/public/assets/Detail-rVxbEDgh.js.br +0 -0
  74. package/dist/public/assets/baseForm-kBTxn8h8.js.br +0 -0
  75. package/dist/public/assets/index-Bjn0ctEd.js.br +0 -0
  76. package/dist/public/assets/index-BxokHznc.js.br +0 -0
  77. package/dist/public/assets/index-Cl0_vMWE.js.br +0 -0
  78. package/dist/public/assets/index-DHdJsD10.js.br +0 -0
  79. package/dist/public/assets/index-DIcz5ciL.js.br +0 -0
  80. package/dist/public/assets/index-O_L1tU2b.js.br +0 -0
  81. package/dist/public/assets/index-aSl6Wdrp.js.br +0 -0
  82. package/dist/public/assets/index-odEI7YQF.js.br +0 -0
@@ -0,0 +1,410 @@
1
+ const assert = require('node:assert/strict');
2
+ const { spawnSync } = require('node:child_process');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const cliRoot = path.resolve(__dirname, '..');
8
+ const cliEntry = path.join(cliRoot, 'dist', 'index.js');
9
+ const packageJson = JSON.parse(
10
+ fs.readFileSync(path.join(cliRoot, 'package.json'), 'utf8'),
11
+ );
12
+
13
+ function runCli(args, environment = {}) {
14
+ const env = {
15
+ ...process.env,
16
+ NO_COLOR: '1',
17
+ FORCE_COLOR: '0',
18
+ ...environment,
19
+ };
20
+ delete env.TTMG_LOGIN_EMAIL;
21
+ delete env.TTMG_LOGIN_PASSWORD;
22
+ return spawnSync(process.execPath, [cliEntry, ...args], {
23
+ cwd: cliRoot,
24
+ env,
25
+ encoding: 'utf8',
26
+ });
27
+ }
28
+
29
+ function assertExit(result, expected, label) {
30
+ assert.equal(
31
+ result.status,
32
+ expected,
33
+ `${label} exited ${result.status}: ${result.stderr || result.stdout}`,
34
+ );
35
+ }
36
+
37
+ // Exercise the shipped bundle with an unavailable native binding, regardless
38
+ // of the host OS. This catches bundler-hoisted imports as well as source imports.
39
+ function runWithoutWasmBinding(args) {
40
+ const bootstrap = `
41
+ const Module = require('node:module');
42
+ const originalLoad = Module._load;
43
+ Module._load = function (request, ...rest) {
44
+ if (request === '@anrans001/ttmg-wasmtool') {
45
+ throw new Error('WASM_BINDING_MUST_NOT_LOAD_FOR_THIS_COMMAND');
46
+ }
47
+ return originalLoad.call(this, request, ...rest);
48
+ };
49
+ const [entry, ...args] = process.argv.slice(1);
50
+ process.argv = [process.execPath, entry, ...args];
51
+ require(entry);
52
+ `;
53
+ return spawnSync(process.execPath, ['-e', bootstrap, cliEntry, ...args], {
54
+ cwd: cliRoot,
55
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
56
+ encoding: 'utf8',
57
+ timeout: 10000,
58
+ });
59
+ }
60
+
61
+ function parseSingleJson(result, label) {
62
+ const output = result.stdout.trim();
63
+ assert.ok(output, `${label} did not write a JSON result to stdout`);
64
+ const lines = output.split(/\r?\n/);
65
+ assert.equal(
66
+ lines.length,
67
+ 1,
68
+ `${label} contaminated structured stdout with ${lines.length} lines`,
69
+ );
70
+ return JSON.parse(lines[0]);
71
+ }
72
+
73
+ function assertNoSecretKeys(value, label) {
74
+ const blocked = new Set([
75
+ 'password',
76
+ 'cookie',
77
+ 'authorization',
78
+ 'otp',
79
+ 'proxy',
80
+ ]);
81
+ const pending = [value];
82
+ while (pending.length > 0) {
83
+ const current = pending.pop();
84
+ if (!current || typeof current !== 'object') continue;
85
+ for (const [key, child] of Object.entries(current)) {
86
+ assert.equal(
87
+ blocked.has(key.toLowerCase()),
88
+ false,
89
+ `${label} exposed blocked key: ${key}`,
90
+ );
91
+ pending.push(child);
92
+ }
93
+ }
94
+ }
95
+
96
+ assert.ok(fs.existsSync(cliEntry), 'dist/index.js is missing; run build first');
97
+ assertExit(runWithoutWasmBinding(['--help']), 0, 'help without Wasm binding');
98
+ const withoutWasmCapabilities = runWithoutWasmBinding([
99
+ 'capabilities', '--format', 'json',
100
+ ]);
101
+ assertExit(withoutWasmCapabilities, 0, 'capabilities without Wasm binding');
102
+ assert.equal(
103
+ parseSingleJson(withoutWasmCapabilities, 'capabilities without Wasm binding').command,
104
+ 'capabilities',
105
+ );
106
+ const withoutWasmBuild = runWithoutWasmBinding([
107
+ 'build', '--dir', __filename, '--format', 'json',
108
+ ]);
109
+ assertExit(withoutWasmBuild, 1, 'build validation without Wasm binding');
110
+ assert.equal(
111
+ parseSingleJson(withoutWasmBuild, 'build validation without Wasm binding').errorCode,
112
+ 'BUILD_DIRECTORY_NOT_FOUND',
113
+ );
114
+ assert.match(
115
+ packageJson.scripts.prepack,
116
+ /verify:dist-version/,
117
+ 'prepack must verify the built CLI version',
118
+ );
119
+ assert.match(
120
+ packageJson.scripts.prepack,
121
+ /verify:agent-contract/,
122
+ 'prepack must verify the built CLI agent contract',
123
+ );
124
+
125
+ const capabilitiesResult = runCli(['capabilities', '--format', 'json']);
126
+ assertExit(capabilitiesResult, 0, 'capabilities');
127
+ const capabilities = parseSingleJson(capabilitiesResult, 'capabilities');
128
+ assert.equal(capabilities.command, 'capabilities');
129
+ assert.equal(capabilities.structuredOutput.stdoutProtocolOnly, true);
130
+ assert.equal(capabilities.commands.check.platformWrite, false);
131
+ assert.equal(capabilities.commands.check.ai.option, '--ai');
132
+ assert.equal(
133
+ capabilities.commands.check.ai.command,
134
+ 'ttmg check --ai --format json',
135
+ );
136
+ assert.equal(capabilities.commands.build.platformWrite, false);
137
+ assert.equal(capabilities.commands.dev.platformWrite, false);
138
+ assert.equal(
139
+ capabilities.commands.dev.command,
140
+ 'ttmg dev --format json --no-open',
141
+ );
142
+ assert.equal(
143
+ capabilities.commands.dev.eventsCommand,
144
+ 'ttmg dev --events ndjson --no-open',
145
+ );
146
+ assert.equal(capabilities.commands.dev.outputModesMutuallyExclusive, true);
147
+ assert.equal(capabilities.commands.dev.clientPreview.available, true);
148
+ assert.equal(capabilities.commands.dev.clientPreview.textOutput, true);
149
+ assert.equal(
150
+ capabilities.commands.dev.clientPreview.scanPage.urlField,
151
+ 'launch.scanPageUrl',
152
+ );
153
+ assert.equal(
154
+ capabilities.commands.dev.clientPreview.scanPage.structuredModeAutoOpen,
155
+ false,
156
+ );
157
+ assert.equal(capabilities.commands.dev.sessionLogs.shortcut, 'ttmg dev logs');
158
+ assert.equal(
159
+ capabilities.commands.dev.sessionScreenshot.shortcut,
160
+ 'ttmg dev screenshot',
161
+ );
162
+ assert.equal(capabilities.commands.dev.clientPreview.protocolVersion, '1.0');
163
+ assert.equal(capabilities.commands.dev.clientPreview.humanScanRequired, true);
164
+ assert.equal(capabilities.commands.dev.clientPreview.deviceLaunch, false);
165
+ assert.equal(capabilities.commands.dev.clientPreview.verifiedOnDevice, false);
166
+ assert.equal(capabilities.commands.dev.sessionLogs.available, true);
167
+ assert.equal(capabilities.commands.dev.sessionScreenshot.available, true);
168
+ assert.equal(capabilities.commands.uploadPreview.platformWrite, true);
169
+ assert.equal(
170
+ capabilities.commands.uploadPreview.previewPage.option,
171
+ '--serve-preview-page',
172
+ );
173
+ assert.equal(capabilities.commands.uploadPreview.previewPage.inMemory, true);
174
+ assert.equal(
175
+ capabilities.commands.uploadPreview.previewPage.reuseScope,
176
+ 'clientKey',
177
+ );
178
+ assert.equal(capabilities.commands.uploadPreview.previewPage.latestOnly, true);
179
+ assert.equal(
180
+ capabilities.commands.uploadPreview.previewPage.renewsTtlOnUpload,
181
+ true,
182
+ );
183
+ assert.equal(
184
+ capabilities.commands.uploadPreview.previewPage.textModeAutoOpen,
185
+ true,
186
+ );
187
+ assert.equal(
188
+ capabilities.commands.uploadPreview.previewPage.structuredModeAutoOpen,
189
+ false,
190
+ );
191
+ assert.equal(
192
+ capabilities.commands.uploadPreview.previewPage.noOpenOption,
193
+ '--no-open',
194
+ );
195
+ assert.equal(
196
+ capabilities.commands.uploadPreview.previewPage.presentation.preferredAction,
197
+ 'open_url',
198
+ );
199
+ assert.equal(
200
+ capabilities.commands.uploadPreview.previewPage.presentation.terminalInline,
201
+ false,
202
+ );
203
+ assert.equal(
204
+ capabilities.commands.uploadPreview.previewPage.presentation.sameHostRequired,
205
+ true,
206
+ );
207
+ assert.equal(capabilities.commands.uploadPreview.previewPage.ttlSeconds, 3600);
208
+ assert.equal(
209
+ capabilities.commands.uploadPreview.previewPage.expiredNoticeTtlSeconds,
210
+ 3600,
211
+ );
212
+ assert.equal(
213
+ capabilities.commands.uploadPreview.previewPage.localhostOnly,
214
+ true,
215
+ );
216
+ assert.equal(capabilities.dev.gameReadyProtocol, true);
217
+ assert.equal(capabilities.dev.gameReady, false);
218
+ assert.equal(capabilities.dev.runtimeLogs, false);
219
+ assert.equal(capabilities.dev.screenshot, false);
220
+ assert.equal(capabilities.dev.clientPreviewProtocol.runtimeLogsVerified, false);
221
+ assert.equal(capabilities.dev.clientPreviewProtocol.screenshotVerified, false);
222
+ assertNoSecretKeys(capabilities, 'capabilities');
223
+
224
+ const conflictingDevOutput = runCli([
225
+ 'dev',
226
+ '--format',
227
+ 'json',
228
+ '--events',
229
+ 'ndjson',
230
+ '--no-open',
231
+ ]);
232
+ assertExit(conflictingDevOutput, 1, 'conflicting dev output modes');
233
+ assert.equal(
234
+ conflictingDevOutput.stdout,
235
+ '',
236
+ 'conflicting dev output modes must not write protocol data to stdout',
237
+ );
238
+ assert.match(
239
+ conflictingDevOutput.stderr,
240
+ /output modes cannot be combined|输出模式不能同时使用/,
241
+ );
242
+
243
+ const devHelp = runCli(['dev', '--help']);
244
+ assertExit(devHelp, 0, 'dev help');
245
+ assert.match(devHelp.stdout, /--mode <mode>/);
246
+ assert.match(devHelp.stdout, /--record-logs\b/);
247
+ for (const shortcut of ['logs', 'screenshot']) {
248
+ const help = runCli(['dev', shortcut, '--help']);
249
+ assertExit(help, 0, `${shortcut} shortcut help`);
250
+ assert.match(help.stdout, /--session <sessionId>/);
251
+ assert.match(help.stdout, /--sessions-dir <path>/);
252
+ assert.match(help.stdout, /--format <format>/);
253
+ }
254
+
255
+ const logsHelp = runCli(['dev', 'session', 'logs', '--help']);
256
+ assert.equal(capabilities.commands.dev.iterationWorkflow.runtimeBuildVerified, false);
257
+ assert.match(runCli(['dev', '--help']).stdout, /--background\b/);
258
+ assert.match(runCli(['dev', '--help']).stdout, /--update-mode <mode>/);
259
+ for (const action of ['sync', 'observe']) {
260
+ const help = runCli(['dev', action, '--help']);
261
+ assertExit(help, 0, `dev ${action} help`);
262
+ const missing = runCli(['dev', action, '--format', 'json']);
263
+ assertExit(missing, 1, `dev ${action} missing identifier`);
264
+ assert.equal(parseSingleJson(missing, action).errorCode, 'DEV_ITERATION_INVALID_ID');
265
+ assert.notEqual(runCli(['dev', action, '--unknown-option']).status, 0);
266
+ }
267
+ const invalidBackground = runCli(['dev', '--background', '--format', 'json']);
268
+ assertExit(invalidBackground, 1, 'background mode validation');
269
+ assert.equal(parseSingleJson(invalidBackground, 'background').errorCode, 'DEV_ITERATION_INVALID_OPTIONS');
270
+ const invalidManual = runCli(['dev', '--update-mode', 'invalid', '--format', 'json']);
271
+ assertExit(invalidManual, 1, 'update mode validation');
272
+ assert.equal(parseSingleJson(invalidManual, 'manual').errorCode, 'DEV_ITERATION_INVALID_OPTIONS');
273
+ // Both formats perform the same local build validation; the former text placeholder is retired.
274
+ const missingBuildDirectory = path.join(os.tmpdir(), 'ttmg-build-dir-that-does-not-exist-' + process.pid);
275
+ for (const format of ['text', 'json']) {
276
+ const result = runCli(['build', '--dir', missingBuildDirectory, '--format', format]);
277
+ assertExit(result, 1, `build ${format} invalid directory`);
278
+ if (format === 'json') assert.equal(parseSingleJson(result, 'build').errorCode, 'BUILD_DIRECTORY_NOT_FOUND');
279
+ }
280
+ assertExit(logsHelp, 0, 'dev session logs help');
281
+ assert.match(logsHelp.stdout, /--follow\b/);
282
+ assert.match(logsHelp.stdout, /--until <cursor>/);
283
+ assert.match(logsHelp.stdout, /--source <source>/);
284
+ assert.match(logsHelp.stdout, /--runtime-session <id>/);
285
+ assert.equal(
286
+ capabilities.commands.dev.sessionLogs.archive.defaultEnabled,
287
+ false,
288
+ );
289
+ assert.equal(capabilities.commands.dev.sessionLogs.nextQuery, true);
290
+ const invalidRecording = runCli([
291
+ 'dev',
292
+ '--record-logs',
293
+ '--format',
294
+ 'json',
295
+ '--no-open',
296
+ ]);
297
+ assertExit(invalidRecording, 1, 'log recording requires client-preview');
298
+ assert.equal(
299
+ parseSingleJson(invalidRecording, 'invalid log recording').errorCode,
300
+ 'DEV_LOG_INVALID_OPTIONS',
301
+ );
302
+ const invalidLogSource = runCli([
303
+ 'dev',
304
+ 'logs',
305
+ '--source',
306
+ 'unknown',
307
+ '--format',
308
+ 'json',
309
+ ]);
310
+ assertExit(invalidLogSource, 1, 'invalid log source');
311
+ assert.equal(
312
+ parseSingleJson(invalidLogSource, 'invalid log source').errorCode,
313
+ 'DEV_LOG_INVALID_OPTIONS',
314
+ );
315
+
316
+ const screenshotHelp = runCli(['dev', 'session', 'screenshot', '--help']);
317
+ assertExit(screenshotHelp, 0, 'dev session screenshot help');
318
+ assert.match(screenshotHelp.stdout, /--image-format <format>/);
319
+ assert.match(screenshotHelp.stdout, /--runtime-session <id>/);
320
+ assert.match(
321
+ runCli(['dev', 'screenshot', '--help']).stdout,
322
+ /--runtime-session <id>/,
323
+ );
324
+ for (const command of [
325
+ ['dev', 'screenshot'],
326
+ ['dev', 'session', 'screenshot', 'missing-session'],
327
+ ]) {
328
+ const unknownOption = runCli([...command, '--runtime-sesion', 'unknown']);
329
+ assert.notEqual(
330
+ unknownOption.status,
331
+ 0,
332
+ 'unknown screenshot option must fail',
333
+ );
334
+ }
335
+
336
+ const checkHelp = runCli(['check', '--help']);
337
+ assertExit(checkHelp, 0, 'check help');
338
+ assert.match(checkHelp.stdout, /Usage: ttmg check \[options\]/);
339
+ assert.match(checkHelp.stdout, /--ai\b/);
340
+ assert.doesNotMatch(checkHelp.stdout, /check \[profile\]/);
341
+
342
+ const unsupportedCheckProfile = runCli(['check', 'ai']);
343
+ assert.notEqual(
344
+ unsupportedCheckProfile.status,
345
+ 0,
346
+ 'ttmg check ai must remain unsupported; use the --ai option',
347
+ );
348
+
349
+ const uploadHelp = runCli(['upload', '--help']);
350
+ assertExit(uploadHelp, 0, 'upload help');
351
+ assert.match(uploadHelp.stdout, /--preview\b/);
352
+ assert.match(uploadHelp.stdout, /--serve-preview-page\b/);
353
+ assert.match(uploadHelp.stdout, /--no-open\b/);
354
+ assert.doesNotMatch(uploadHelp.stdout, /--allow-upload\b/);
355
+
356
+ const tempDirectory = fs.mkdtempSync(
357
+ path.join(os.tmpdir(), 'ttmg-agent-contract-'),
358
+ );
359
+ try {
360
+ const configPath = path.join(tempDirectory, '.ttmgrc');
361
+ const authResult = runCli(['auth', 'status', '--format', 'json'], {
362
+ TTMGRC_PATH: configPath,
363
+ });
364
+ assertExit(authResult, 0, 'auth status');
365
+ const auth = parseSingleJson(authResult, 'auth status');
366
+ assert.equal(auth.command, 'auth.status');
367
+ assert.equal(auth.status, 'not_authenticated');
368
+ assert.equal(auth.authenticated, false);
369
+ assertNoSecretKeys(auth, 'auth status');
370
+
371
+ const loginResult = runCli(['login', '--implicit', '--verbose'], {
372
+ TTMGRC_PATH: configPath,
373
+ });
374
+ assertExit(loginResult, 1, 'implicit login without credentials');
375
+ const login = parseSingleJson(loginResult, 'implicit login');
376
+ assert.equal(login.operation, 'login');
377
+ assert.equal(login.status, 'failed');
378
+ assert.equal(login.errorCode, 'CREDENTIALS_REQUIRED');
379
+ assert.equal(login.cookieStored, false);
380
+ assertNoSecretKeys(login, 'implicit login');
381
+ assert.equal(fs.existsSync(configPath), false);
382
+ } finally {
383
+ fs.rmSync(tempDirectory, { recursive: true, force: true });
384
+ }
385
+
386
+ for (const operation of ['prepare', 'split']) {
387
+ assertExit(runWithoutWasmBinding(['wasm', operation, '--help']), 0, `wasm ${operation} help without binding`);
388
+ const invalid = runCli(['wasm', operation, '--format', 'json']);
389
+ assertExit(invalid, 1, `wasm ${operation} missing input`);
390
+ assert.equal(parseSingleJson(invalid, `wasm ${operation}`).errorCode, 'WASM_INVALID_OPTIONS');
391
+ }
392
+ const wasmSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/wasm-smoke.js'), cliEntry], {
393
+ cwd: cliRoot, encoding: 'utf8', timeout: 60000,
394
+ });
395
+ assertExit(wasmSmoke, 0, 'Wasm built-artifact process smoke');
396
+ console.log(wasmSmoke.stdout.trim());
397
+ const captureHelp = runCli(['dev', 'capture', '--help']);
398
+ assertExit(captureHelp, 0, 'capture help');
399
+ assert.match(captureHelp.stdout, /--timeout/);
400
+ const invalidCapture = runCli(['dev', 'capture', '--timeout', '0', '--format', 'json']);
401
+ assertExit(invalidCapture, 1, 'capture invalid timeout');
402
+ assert.equal(parseSingleJson(invalidCapture, 'capture invalid timeout').errorCode, 'DEV_CAPTURE_INVALID_OPTIONS');
403
+ assertExit(runCli(['dev', 'capture', '--format', 'ndjson']), 1, 'capture invalid format inherited from dev');
404
+ assertExit(runCli(['dev', 'capture', '--unknown-option']), 1, 'capture unknown option');
405
+ const captureSmoke = spawnSync(process.execPath, [path.join(cliRoot, 'scripts/dev-capture-smoke.js'), cliEntry], {
406
+ cwd: cliRoot, encoding: 'utf8', timeout: 45000,
407
+ });
408
+ assertExit(captureSmoke, 0, 'capture built-artifact process smoke');
409
+ console.log(captureSmoke.stdout.trim());
410
+ console.log('built CLI agent contract verified');
@@ -0,0 +1,23 @@
1
+ // Explicit test-process preload only; never loaded by the CLI. Replace HTTPS
2
+ // transport with a local fixture so --mock regressions cannot create real tasks.
3
+ const assert = require('node:assert/strict');
4
+ const http = require('node:http');
5
+ const https = require('node:https');
6
+ const port = Number(process.env.TTMG_VIBE_TEST_PORT);
7
+ assert.ok(Number.isInteger(port) && port > 0 && port < 65536);
8
+ https.request = (options, callback) => {
9
+ assert.equal(typeof options, 'object');
10
+ assert.equal(options.hostname, 'developers.tiktok.com');
11
+ assert.equal(options.protocol, 'https:');
12
+ assert.match(options.path, /^\/bff\/app_info\/minis_import\/(create_minis_import_task_for_agent|get_minis_import_task_status_for_agent)\//);
13
+ return http.request({
14
+ ...options,
15
+ protocol: 'http:', hostname: '127.0.0.1', host: undefined, port,
16
+ agent: undefined, servername: undefined,
17
+ }, callback);
18
+ };
19
+ https.get = (options, callback) => {
20
+ const request = https.request(options, callback);
21
+ request.end();
22
+ return request;
23
+ };
@@ -0,0 +1,72 @@
1
+ // A local fixture runner, not a second upload implementation.
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+
7
+ const entry = path.resolve(__dirname, '../dist/index.js');
8
+ if (!fs.existsSync(entry)) throw new Error('Run npm run build first.');
9
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-vibe-demo-'));
10
+ const project = path.join(directory, 'game');
11
+ fs.mkdirSync(project);
12
+ fs.writeFileSync(
13
+ path.join(project, 'game.js'),
14
+ 'console.log("Vibe mock demo");\n',
15
+ );
16
+ fs.writeFileSync(path.join(project, 'game.json'), '{}\n');
17
+ // Localize this Chinese fixture demo without changing the user's CLI settings.
18
+ fs.writeFileSync(
19
+ path.join(directory, '.ttmgrc'),
20
+ JSON.stringify({ lang: 'zh-CN' }),
21
+ );
22
+ const icon = path.join(directory, 'fixture-icon.png');
23
+ // Tiny valid PNG fixture; normal usage still takes the user's own local image.
24
+ fs.writeFileSync(
25
+ icon,
26
+ Buffer.from(
27
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXyoAAAAASUVORK5CYII=',
28
+ 'base64',
29
+ ),
30
+ );
31
+ console.error(
32
+ 'Integration demo: creates a real PPE task; authorization and uploads are simulated. No environment setup or phone authorization required.',
33
+ );
34
+ const child = spawn(
35
+ process.execPath,
36
+ [
37
+ entry,
38
+ 'upload',
39
+ '--vibe',
40
+ '--mock',
41
+ '--title',
42
+ '星环守卫',
43
+ '--icon',
44
+ icon,
45
+ '--dir',
46
+ project,
47
+ ...process.argv.slice(2),
48
+ ],
49
+ {
50
+ cwd: directory,
51
+ stdio: 'inherit',
52
+ env: {
53
+ ...process.env,
54
+ HOME: directory,
55
+ USERPROFILE: directory,
56
+ TTMGRC_PATH: path.join(directory, '.ttmgrc'),
57
+ },
58
+ },
59
+ );
60
+ const cancel = () => child.kill('SIGINT');
61
+ process.once('SIGINT', cancel);
62
+ process.once('SIGTERM', cancel);
63
+ child.once('error', () => {
64
+ console.error('Unable to start CLI demo.');
65
+ process.exitCode = 1;
66
+ });
67
+ child.once('close', code => {
68
+ process.off('SIGINT', cancel);
69
+ process.off('SIGTERM', cancel);
70
+ process.exitCode = code ?? 1;
71
+ fs.rmSync(directory, { recursive: true, force: true });
72
+ });