@ttmg/cli 0.4.6-vibe-beta.6 → 0.4.6-vibe-beta.8

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.
@@ -4,6 +4,7 @@ const os = require('node:os');
4
4
  const path = require('node:path');
5
5
  const { spawn } = require('node:child_process');
6
6
  const archiver = require('archiver');
7
+ const http = require('node:http');
7
8
 
8
9
  const entry = path.resolve(__dirname, '../dist/index.js');
9
10
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-vibe-process-'));
@@ -23,6 +24,11 @@ const input = [
23
24
  const png =
24
25
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXyoAAAAASUVORK5CYII=';
25
26
  const children = new Set();
27
+ let fixtureServer;
28
+ const requests = [];
29
+ let failStatus = false;
30
+ let webMode = false;
31
+ let webPolls = 0;
26
32
  const env = {
27
33
  ...process.env,
28
34
  HOME: temp,
@@ -31,6 +37,7 @@ const env = {
31
37
  TTMG_VIBE_PAGE_TTL_MS: '7000',
32
38
  TTMG_VIBE_BACKEND: '',
33
39
  TTMG_VIBE_API: '',
40
+ TTMG_VIBE_PPE: '',
34
41
  };
35
42
 
36
43
  function run(args, onEvent, extraEnv = {}) {
@@ -94,6 +101,84 @@ function events(result) {
94
101
  .map(line => JSON.parse(line));
95
102
  }
96
103
  async function main() {
104
+ // Exercise the built worker IPC and new command without creating a platform task.
105
+ const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
106
+ env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
107
+ });
108
+ children.add(authWorker);
109
+ const workerMessage = predicate => new Promise((resolve, reject) => {
110
+ const timer = setTimeout(() => {authWorker.off('message', receive); reject(new Error('worker timeout'));}, 5000);
111
+ function receive(message) {
112
+ if (predicate(message)) {clearTimeout(timer); authWorker.off('message', receive); resolve(message);}
113
+ }
114
+ authWorker.on('message', receive);
115
+ });
116
+ const ready = await workerMessage(message => message.type === 'ready');
117
+ const landing = 'https://developers.tiktok.com/portal_h5/minis_import?task_id=123&import_token=process-private-token';
118
+ let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
119
+ authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
120
+ await rendered;
121
+ const browserReceipt = path.join(temp, 'browser-open.ndjson');
122
+ const browserEnv = {
123
+ NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
124
+ TTMG_AUTH_OPEN_TEST_RECEIPT: browserReceipt,
125
+ };
126
+ for (const format of ['json', 'ndjson']) {
127
+ const opened = await run(['auth', 'open', '--page-url', ready.url, '--format', format], undefined, browserEnv);
128
+ assert.equal(opened.code, 0, opened.stderr);
129
+ const result = JSON.parse(opened.stdout);
130
+ assert.equal(result.status, 'open_requested');
131
+ assert.equal(result.platformWrite, false);
132
+ assert.doesNotMatch(opened.stdout + opened.stderr, /import_token|process-private-token/);
133
+ }
134
+ const launches = fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').map(JSON.parse);
135
+ assert.equal(launches.length, 2);
136
+ assert.ok(launches.every(item => item.args.includes(ready.url + '/authorize')));
137
+ const redirect = await fetch(ready.url + '/authorize', {redirect: 'manual'});
138
+ assert.equal(redirect.headers.get('location'), landing);
139
+ assert.equal(redirect.status, 302);
140
+ rendered = workerMessage(message => message.type === 'rendered' && message.id === 2);
141
+ authWorker.send({type: 'render', id: 2, html: '<h1>Complete</h1>'});
142
+ await rendered;
143
+ const stale = await run(['auth', 'open', '--page-url', ready.url, '--format', 'json'], undefined, browserEnv);
144
+ assert.equal(stale.code, 1);
145
+ assert.equal(JSON.parse(stale.stdout).errorCode, 'VIBE_AUTH_PAGE_UNAVAILABLE');
146
+ for (const args of [[], ['--page-url', landing]]) {
147
+ const invalid = await run(['auth', 'open', ...args, '--format', 'json'], undefined, browserEnv);
148
+ assert.equal(invalid.code, 1);
149
+ assert.equal(JSON.parse(invalid.stdout).errorCode, 'VIBE_INVALID_PAGE_URL');
150
+ assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
151
+ }
152
+ assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
153
+ authWorker.kill();
154
+ children.delete(authWorker);
155
+ fixtureServer = http.createServer((req, res) => {
156
+ const url = new URL(req.url, 'http://127.0.0.1');
157
+ requests.push({ path: url.pathname, method: req.method, headers: req.headers });
158
+ req.resume();
159
+ const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
160
+ const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
161
+ if (create) webPolls = 0;
162
+ if (status && webMode) webPolls++;
163
+ assert.ok(create || status, 'Unexpected real upload endpoint');
164
+ assert.equal(req.headers['x-use-ppe'], '1');
165
+ assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
166
+ assert.equal(req.headers.cookie, undefined);
167
+ if (status) {
168
+ assert.equal(url.searchParams.get('task_id'), '7684370037737553941');
169
+ assert.equal(url.searchParams.get('import_token'), 'process-test-secret');
170
+ }
171
+ const data = create ? {
172
+ task_id: '7684370037737553941', import_token: 'process-test-secret',
173
+ landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?task_id=7684370037737553941&import_token=process-test-secret',
174
+ poll_interval_seconds: 0.01,
175
+ } : { status: webMode && webPolls >= 5 ? 2 : 1, ready_for_input: webMode && webPolls >= 5 };
176
+ res.writeHead(200, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ ...data, error_info: { code: failStatus && status ? 420500 : 0 } }));
178
+ });
179
+ await new Promise(resolve => fixtureServer.listen(0, '127.0.0.1', resolve));
180
+ env.TTMG_VIBE_TEST_PORT = String(fixtureServer.address().port);
181
+ env.NODE_OPTIONS = `--require ${JSON.stringify(path.join(__dirname, 'vibe-ppe-test-preload.cjs'))}`;
97
182
  fs.writeFileSync(icon, Buffer.from(png, 'base64'));
98
183
  await new Promise((resolve, reject) => {
99
184
  const out = fs.createWriteStream(zip);
@@ -107,6 +192,77 @@ async function main() {
107
192
  });
108
193
 
109
194
  let bindingHtml;
195
+ const appInfoPath = path.join(temp, 'app-info.json');
196
+ fs.writeFileSync(appInfoPath, JSON.stringify({
197
+ description: 'Process contract fixture', category: 'Casual', subcategory: 'Puzzle',
198
+ terms_of_service: 'https://example.invalid/terms', privacy_policy: 'https://example.invalid/privacy',
199
+ copyright_confirmation: true,
200
+ }));
201
+ const badIcon = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
202
+ assert.equal(badIcon.code, 1);
203
+ assert.equal(events(badIcon)[0].errorCode, 'VIBE_PLATFORM_ICON_INVALID');
204
+ assert.equal(events(badIcon)[0].platformWrite, false);
205
+ assert.equal(requests.length, 0, 'Invalid input must not create a task');
206
+ assert.equal(fs.existsSync(path.join(temp, '.ttmg/vibe-project.json')), false);
207
+ const platformHeader = Buffer.from(png, 'base64');
208
+ platformHeader.writeUInt32BE(1024, 16);
209
+ platformHeader.writeUInt32BE(1024, 20);
210
+ fs.writeFileSync(icon, platformHeader); // Header preflight fixture, not a decoded image.
211
+ const completeDry = await run([...input, '--app-info', appInfoPath, '--dry-run', '--format', 'json']);
212
+ assert.equal(completeDry.code, 0, completeDry.stderr);
213
+ assert.equal(events(completeDry)[0].status, 'prepared');
214
+ assert.equal(requests.length, 0);
215
+ fs.writeFileSync(appInfoPath, '{private-token');
216
+ const badInfo = await run([...input, '--mock', '--app-info', appInfoPath, '--format', 'json']);
217
+ assert.equal(events(badInfo)[0].errorCode, 'VIBE_INVALID_APP_INFO');
218
+ assert.doesNotMatch(badInfo.stdout + badInfo.stderr, /private-token/);
219
+ assert.equal(requests.length, 0);
220
+ const legacyInfo = await run(['upload', '--app-info', appInfoPath, '--format', 'json']);
221
+ assert.equal(events(legacyInfo)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
222
+ assert.equal(events(legacyInfo)[0].platformWrite, false);
223
+ fs.writeFileSync(icon, Buffer.from(png, 'base64'));
224
+ // Replay the live package receipt through the built CLI without PPE writes.
225
+ const uploadPaths = [];
226
+ const uploadServer = http.createServer((req, res) => {
227
+ const url = new URL(req.url, 'http://127.0.0.1');
228
+ const endpoint = url.pathname.split('/').filter(Boolean).at(-1);
229
+ uploadPaths.push(endpoint);
230
+ req.resume();
231
+ const replies = {
232
+ create_minis_import_task_for_agent: {
233
+ task_id: '7684370037737619477', import_token: 'receipt-test-secret',
234
+ landing_url: 'https://example.invalid/auth', poll_interval_seconds: 0.01,
235
+ },
236
+ get_minis_import_task_status_for_agent: { status: 2, ready_for_input: true },
237
+ upload_minis_import_app_icon_for_agent: { icon_uri: 'test-icon' },
238
+ submit_minis_import_info_for_agent: {},
239
+ upload_minis_import_package_for_agent: { status: 2, package_uri: 'test-package' },
240
+ };
241
+ res.writeHead(endpoint in replies ? 200 : 404, { 'Content-Type': 'application/json' });
242
+ res.end(JSON.stringify({ error_info: { code: 0 }, ...replies[endpoint] }));
243
+ });
244
+ await new Promise(resolve => uploadServer.listen(0, '127.0.0.1', resolve));
245
+ try {
246
+ const saved = await run([...input, '--format', 'ndjson'], undefined, {
247
+ TTMG_VIBE_BACKEND: 'local-http',
248
+ TTMG_VIBE_API: `http://127.0.0.1:${uploadServer.address().port}`,
249
+ });
250
+ assert.equal(saved.code, 0, saved.stderr);
251
+ const savedEvents = events(saved);
252
+ const savedFinal = savedEvents.at(-1);
253
+ assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
254
+ assert.equal(savedFinal.uploaded, true);
255
+ assert.equal(savedFinal.taskStatusRaw, 2);
256
+ assert.equal(savedEvents.filter(e => e.terminal).length, 1);
257
+ assert.deepEqual(uploadPaths, [
258
+ 'create_minis_import_task_for_agent', 'get_minis_import_task_status_for_agent',
259
+ 'upload_minis_import_app_icon_for_agent', 'submit_minis_import_info_for_agent',
260
+ 'upload_minis_import_package_for_agent',
261
+ ]);
262
+ assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret|test-package/);
263
+ } finally {
264
+ uploadServer.close();
265
+ }
110
266
  const success = await run(
111
267
  [...input, '--mock', '--format', 'ndjson', '--poll-interval', '0.2'],
112
268
  async event => {
@@ -117,9 +273,9 @@ async function main() {
117
273
  assert.equal(success.code, 0, success.stderr);
118
274
  assert.match(bindingHtml, /Upload to TikTok/);
119
275
  assert.match(bindingHtml, /class="game-avatar"/);
120
- assert.match(bindingHtml, /Mock only/);
276
+ assert.match(bindingHtml, /PPE \+ Mock/);
121
277
  assert.match(bindingHtml, /data-phase="binding"/);
122
- assert.match(bindingHtml, /无需扫码|no scan needed/);
278
+ assert.match(bindingHtml, /Phone authorization is not required/);
123
279
  assert.doesNotMatch(bindingHtml, /<form|<input/);
124
280
  const sequence = events(success);
125
281
  const final = sequence[sequence.length - 1];
@@ -129,7 +285,16 @@ async function main() {
129
285
  assert.equal(final.nextAction, 'none');
130
286
  assert.equal(sequence.filter(e => e.terminal).length, 1);
131
287
  assert.equal(final.uploaded, true);
132
- assert.equal(final.platformWrite, false);
288
+ assert.equal(final.platformWrite, true);
289
+ assert.equal(final.backend, 'ppe-mock');
290
+ assert.equal(final.taskStatusRaw, 1);
291
+ assert.equal(final.readyForInput, false);
292
+ assert.equal(final.simulatedReadyForInput, true);
293
+ assert.equal(final.simulatedTaskStatusRaw, 3);
294
+ assert.equal(requests.length, 4);
295
+ assert.equal(requests[0].method, 'POST');
296
+ assert.ok(requests.slice(1).every(r => r.method === 'GET'));
297
+ assert.doesNotMatch(success.stdout, /process-test-secret/);
133
298
  assert.equal(final.simulation, true);
134
299
  const progress = sequence.filter(
135
300
  e => e.status === 'uploading' && e.loadedBytes !== undefined,
@@ -145,10 +310,56 @@ async function main() {
145
310
  1,
146
311
  );
147
312
  const previewHtml = await (await fetch(final.pageUrl)).text();
148
- assert.match(previewHtml, /Upload complete/);
313
+ assert.match(previewHtml, /Simulated upload complete/);
149
314
  assert.match(previewHtml, /data-phase="complete"/);
150
315
  assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
151
316
 
317
+ const beforeWeb = requests.length;
318
+ const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
319
+ assert.equal(events(webConflict)[0].errorCode, 'VIBE_WEB_OPTION_CONFLICT');
320
+ const legacyWeb = await run(['upload', '--web', '--format', 'json']);
321
+ assert.equal(events(legacyWeb)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
322
+ assert.equal(requests.length, beforeWeb);
323
+ webMode = true;
324
+ for (const format of ['json', 'ndjson']) {
325
+ const receipt = path.join(temp, `upload-web-${format}.ndjson`);
326
+ const web = await run([...input.filter(v => v !== '--no-open'), '--web', '--format', format, '--poll-interval', '0.2'], async event => {
327
+ if (event.status === 'waiting_authorization') {
328
+ assert.equal(event.authorizationMode, 'web');
329
+ assert.equal(event.browserOpenRequested, true);
330
+ assert.equal(event.simulation, false);
331
+ assert.equal(event.nextAction, 'open_browser');
332
+ assert.equal(event.qrPurpose, undefined);
333
+ const response = await fetch(event.pageUrl + '/authorize', {redirect: 'manual'});
334
+ assert.equal(response.status, 302);
335
+ assert.match(response.headers.get('location'), /^https:\/\/developers\.tiktok\.com\/portal_h5\/minis_import\?/);
336
+ const html = await (await fetch(event.pageUrl)).text();
337
+ assert.match(html, /Authorize in your browser/);
338
+ assert.doesNotMatch(html, /alt="Authorization QR"|Scan the QR code/);
339
+ }
340
+ }, {
341
+ NODE_OPTIONS: env.NODE_OPTIONS + ' --require=' + path.join(__dirname, 'vibe-auth-open-test-preload.cjs'),
342
+ TTMG_AUTH_OPEN_TEST_RECEIPT: receipt,
343
+ // Explicit --web must not inherit an old Mock backend setting.
344
+ TTMG_VIBE_BACKEND: 'mock',
345
+ });
346
+ assert.equal(web.code, 1, web.stderr);
347
+ const output = events(web);
348
+ assert.equal(output.at(-1).errorCode, 'VIBE_REAL_UPLOAD_NOT_READY');
349
+ assert.equal(output.at(-1).simulation, false);
350
+ assert.equal(output.at(-1).backend, 'ppe');
351
+ assert.equal(output.at(-1).readyForInput, true);
352
+ assert.equal(output.at(-1).uploadAttempted, false);
353
+ assert.equal(output.at(-1).uploaded, false);
354
+ assert.equal(webPolls, 5, 'Only true backend readiness unlocks authorization');
355
+ if (format === 'json') assert.equal(output.length, 1);
356
+ const opens = fs.readFileSync(receipt, 'utf8').trim().split('\n').map(line => JSON.parse(line));
357
+ assert.equal(opens.length, 1);
358
+ assert.ok(opens[0].args.some(arg => /^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{48}\/authorize$/.test(arg)));
359
+ assert.doesNotMatch(JSON.stringify(opens) + web.stdout + web.stderr, /process-test-secret|import_token|aweme:\/\//);
360
+ }
361
+ webMode = false;
362
+
152
363
  const json = await run(
153
364
  [...input, '--format', 'json', '--poll-interval', '0.01'],
154
365
  undefined,
@@ -160,6 +371,44 @@ async function main() {
160
371
  assert.equal(JSON.parse(json.stdout).simulation, true);
161
372
  assert.match(json.stderr, /http:\/\/127\.0\.0\.1:/);
162
373
 
374
+ const missingPpe = await run([...input, '--format', 'json'], undefined, {
375
+ TTMG_VIBE_BACKEND: 'ppe',
376
+ });
377
+ assert.equal(missingPpe.code, 1);
378
+ assert.equal(events(missingPpe)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
379
+ assert.equal(events(missingPpe)[0].platformWrite, false);
380
+ const dryPpe = await run([...input, '--dry-run', '--format', 'json'], undefined, {
381
+ TTMG_VIBE_BACKEND: 'ppe', TTMG_VIBE_PPE: 'ppe_op_vc',
382
+ });
383
+ assert.equal(dryPpe.code, 0, dryPpe.stderr);
384
+ assert.equal(events(dryPpe)[0].status, 'prepared');
385
+ assert.equal(events(dryPpe)[0].platformWrite, false);
386
+ const missingHybrid = await run([...input, '--format', 'json'], undefined, {
387
+ TTMG_VIBE_BACKEND: 'ppe-mock',
388
+ });
389
+ assert.equal(events(missingHybrid)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
390
+ assert.equal(events(missingHybrid)[0].platformWrite, false);
391
+ const dryHybrid = await run([...input, '--dry-run', '--format', 'json'], undefined, {
392
+ TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
393
+ });
394
+ assert.equal(dryHybrid.code, 0, dryHybrid.stderr);
395
+ assert.equal(events(dryHybrid)[0].status, 'prepared');
396
+ assert.equal(events(dryHybrid)[0].platformWrite, false);
397
+ const hybridMockOverride = await run([...input, '--mock', '--poll-interval', '0.01', '--format', 'json'], undefined, {
398
+ TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
399
+ });
400
+ assert.equal(hybridMockOverride.code, 0, hybridMockOverride.stderr);
401
+ assert.equal(events(hybridMockOverride)[0].backend, 'ppe-mock');
402
+ assert.equal(events(hybridMockOverride)[0].platformWrite, true);
403
+
404
+ failStatus = true;
405
+ const realFailure = await run([...input, '--mock', '--format', 'json']);
406
+ assert.equal(realFailure.code, 1, realFailure.stderr);
407
+ assert.equal(events(realFailure)[0].errorCode, 'VIBE_BACKEND_REJECTED');
408
+ assert.equal(events(realFailure)[0].uploadAttempted, false);
409
+ assert.equal(events(realFailure)[0].uploaded, false);
410
+ failStatus = false;
411
+
163
412
  for (const [scenario, errorCode, attempted] of [
164
413
  ['denied', 'VIBE_AUTH_DENIED', false],
165
414
  ['upload-failed', 'VIBE_BACKEND_REQUEST_FAILED', true],
@@ -193,6 +442,11 @@ async function main() {
193
442
  assert.equal(prepared.backend, 'none');
194
443
  assert.equal(prepared.pageUrl, undefined);
195
444
  assert.equal(prepared.uploadAttempted, false);
445
+ const beforeDryMock = requests.length;
446
+ const dryMock = await run([...input, '--mock', '--dry-run', '--format', 'json']);
447
+ assert.equal(dryMock.code, 0);
448
+ assert.equal(events(dryMock)[0].platformWrite, false);
449
+ assert.equal(requests.length, beforeDryMock);
196
450
 
197
451
  const project = path.join(temp, 'project');
198
452
  fs.mkdirSync(project);
@@ -228,6 +482,9 @@ async function main() {
228
482
  assert.equal(help.code, 0);
229
483
  assert.match(help.stdout, /--vibe/);
230
484
  assert.match(help.stdout, /--client-key/);
485
+ const authHelp = await run(['auth', 'open', '--help']);
486
+ assert.equal(authHelp.code, 0);
487
+ assert.match(authHelp.stdout, /--page-url/);
231
488
 
232
489
  let cancelledPage;
233
490
  const cancelled = await run(
@@ -254,5 +511,6 @@ main()
254
511
  })
255
512
  .finally(() => {
256
513
  for (const child of children) child.kill('SIGKILL');
514
+ fixtureServer?.close();
257
515
  fs.rmSync(temp, { recursive: true, force: true });
258
516
  });
@@ -0,0 +1,53 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const { spawnSync } = require('node:child_process');
7
+ const entry = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '../dist/index.js');
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-wasm-smoke-'));
9
+ const input = path.join(root, 'original.wasm');
10
+ const wasm = Buffer.from('0061736d010000000105016000017f0304030000000404017000030707010372756e00000909010041000b030001020a1003040010010b0400412a0b040041070b', 'hex');
11
+ const sha = crypto.createHash('sha256').update(wasm).digest('hex');
12
+ function run(args, env = {}) {
13
+ const result = spawnSync(process.execPath, [entry, ...args, '--format', 'json'], {
14
+ cwd: root, encoding: 'utf8', timeout: 15000,
15
+ env: { ...process.env, TTMGRC_PATH: path.join(root, 'rc.json'), ...env },
16
+ });
17
+ assert.ifError(result.error);
18
+ assert.equal(result.stdout.trim().split('\n').length, 1, result.stdout + result.stderr);
19
+ const body = JSON.parse(result.stdout);
20
+ assert.equal(result.status, body.status === 'failed' ? 1 : 0, result.stderr);
21
+ return body;
22
+ }
23
+ try {
24
+ fs.writeFileSync(input, wasm);
25
+ const stub = path.join(root, 'missing-binding.js');
26
+ fs.writeFileSync(stub, "const Module=require('node:module'); const load=Module._load; Module._load=function(name,...args){if(name==='@anrans001/ttmg-wasmtool')throw new Error('fixture unavailable');return load.call(this,name,...args)};");
27
+ const unavailable = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'missing')], { NODE_OPTIONS: '--require=' + stub });
28
+ assert.equal(unavailable.errorCode, 'WASM_BINDING_UNAVAILABLE');
29
+ assert.equal(fs.existsSync(path.join(root, 'missing')), false);
30
+ const preflight = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'dry'), '--dry-run'], { NODE_OPTIONS: '--require=' + stub });
31
+ assert.equal(preflight.status, 'ready');
32
+ assert.equal(preflight.nativeBindingChecked, false);
33
+ assert.equal(fs.existsSync(path.join(root, 'dry')), false);
34
+ const collection = path.join(root, 'collection.json');
35
+ fs.writeFileSync(collection, JSON.stringify({ schemaVersion: '1', wasmSha256: sha, funcIds: [0, 1], bootFuncIds: [0] }));
36
+ const prepared = run(['wasm', 'prepare', '--input', input, '--output', path.join(root, 'prepared')]);
37
+ if (prepared.errorCode === 'WASM_BINDING_UNAVAILABLE') {
38
+ console.log('wasm process contract passed; native execution unavailable on this host, not verified');
39
+ } else {
40
+ assert.equal(prepared.status, 'succeeded', JSON.stringify(prepared));
41
+ const split = run(['wasm', 'split', '--input', input, '--collection', collection, '--output', path.join(root, 'split')]);
42
+ assert.equal(split.status, 'succeeded', JSON.stringify(split));
43
+ assert.equal(split.detail.totalWasmCount, 3);
44
+ assert.equal(split.detail.mainWasmCount, 2);
45
+ assert.equal(split.gameValidation, 'not_evaluated');
46
+ assert.equal(split.input.wasmSha256, sha);
47
+ assert.deepEqual(JSON.parse(fs.readFileSync(split.manifest, 'utf8')), split);
48
+ for (const artifact of split.artifacts) assert.equal(crypto.createHash('sha256').update(fs.readFileSync(artifact.path)).digest('hex'), artifact.sha256);
49
+ assert.deepEqual(fs.readFileSync(input), wasm);
50
+ assert.equal(fs.existsSync(path.join(root, 'rc.json')), false);
51
+ console.log(`wasm process contract passed: native prepare/split on ${process.platform}/${process.arch}, immutable input and verified artifacts`);
52
+ }
53
+ } finally { fs.rmSync(root, { recursive: true, force: true }); }
@@ -0,0 +1,63 @@
1
+ // Native Wasm operations run in a disposable process so a deadline can stop
2
+ // synchronous native code without blocking or corrupting the CLI owner.
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const zlib = require('node:zlib');
6
+
7
+ process.once('message', request => {
8
+ let response;
9
+ try {
10
+ let binding;
11
+ try {
12
+ binding = require('@anrans001/ttmg-wasmtool');
13
+ } catch {
14
+ throw Object.assign(new Error('Wasm tool binding is unavailable for this OS/architecture.'), {
15
+ code: 'WASM_BINDING_UNAVAILABLE',
16
+ });
17
+ }
18
+ if (request.operation === 'prepare') {
19
+ const output = path.join(request.output, 'prepared.wasm');
20
+ const result = binding.prepare(request.input, output);
21
+ if (!fs.existsSync(output) || result.outputSize <= 0)
22
+ throw new Error('Wasm preparation produced no output.');
23
+ fs.writeFileSync(output + '.br', zlib.brotliCompressSync(fs.readFileSync(output), {
24
+ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 9 },
25
+ }));
26
+ response = { ok: true, detail: { outputSize: result.outputSize, timeCost: result.timeCost } };
27
+ } else if (request.operation === 'split') {
28
+ const result = binding.split({
29
+ input: request.input,
30
+ outputDir: request.output,
31
+ funcIds: request.funcIds,
32
+ alwaysInclude: request.bootFuncIds,
33
+ archive: true,
34
+ compress: true,
35
+ quality: 9,
36
+ callClosure: true,
37
+ callIndirectClosure: false,
38
+ });
39
+ if (result.code !== 0) throw new Error(result.errMsg || 'Wasm split failed.');
40
+ if (![...request.funcIds, ...request.bootFuncIds].every(id => id < result.totalWasmCount))
41
+ throw Object.assign(new Error('Collection contains function IDs outside this Wasm module.'), {
42
+ code: 'WASM_COLLECTION_INVALID',
43
+ });
44
+ const detail = {};
45
+ for (const key of ['totalWasmCount', 'mainWasmCount', 'tableSize', 'timeCost',
46
+ 'globalVarList', 'collectFuncCount', 'alwaysIncludeAdded', 'closureAdded',
47
+ 'indirectClosureAdded', 'indirectClosureTypes', 'exportAdded']) detail[key] = result[key];
48
+ detail.outputFiles = [result.mainWasmPath, result.subWasmPath, result.funcMetaPath, result.archivePath]
49
+ .filter(Boolean).map(file => {
50
+ if (path.dirname(path.resolve(file)) !== path.resolve(request.output))
51
+ throw new Error('Native output escaped its assigned directory.');
52
+ return path.basename(file);
53
+ });
54
+ response = { ok: true, detail };
55
+ } else throw new Error('Unknown Wasm operation.');
56
+ response.detail.bindingVersion = require('@anrans001/ttmg-wasmtool/package.json').version;
57
+ response.detail.platform = process.platform;
58
+ response.detail.arch = process.arch;
59
+ } catch (error) {
60
+ response = { ok: false, errorCode: error.code || 'WASM_OPERATION_FAILED', error: error.message };
61
+ }
62
+ process.send(response, () => process.disconnect());
63
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.6",
3
+ "version": "0.4.6-vibe-beta.8",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -31,11 +31,12 @@
31
31
  "test:auth": "node scripts/run-ts-test.js src/commands/common/auth.test.ts",
32
32
  "test:capabilities": "node scripts/run-ts-test.js src/commands/common/capabilities.test.ts",
33
33
  "test:test-account": "node scripts/run-ts-test.js src/commands/common/testAccount.test.ts",
34
- "test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
35
- "test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts",
34
+ "test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts && npm run test:packaging-compatibility",
35
+ "test:packaging-compatibility": "node scripts/run-ts-test.js src/commands/native/dev/packagingCompatibility.test.ts",
36
+ "test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts && node scripts/run-ts-test.js src/commands/native/dev/projectSession.test.ts && node scripts/run-ts-test.js src/commands/native/dev/scanPage.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/logArchive.test.ts && node scripts/run-ts-test.js src/commands/native/dev/capture.test.ts",
36
37
  "test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
37
38
  "test:dev-runtime-client": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtime.test.ts",
38
- "test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts",
39
+ "test:dev-session-control": "node scripts/run-ts-test.js src/commands/native/dev/runtime/sessionControl.test.ts && node scripts/run-ts-test.js src/commands/native/dev/runtime/previewWorkflow.test.ts",
39
40
  "test:dev-runtime-session-follow": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtimeSessionFollow.test.ts",
40
41
  "test:dev-client-preview-orchestrator": "node scripts/run-ts-test.js src/commands/native/dev/runtime/clientPreviewOrchestrator.test.ts",
41
42
  "test:dev-watch-change": "node scripts/run-ts-test.js src/commands/native/dev/utils/watchChange.test.ts",
@@ -46,9 +47,10 @@
46
47
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
47
48
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
48
49
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
49
- "test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
50
- "test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process",
51
- "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
50
+ "test:wasm": "node scripts/run-ts-test.js src/commands/native/wasm/wasm.test.ts && node scripts/acceptance-fixture.test.js",
51
+ "test:agent-contract": "npm run test:login && npm run test:auth && npm run test:capabilities && npm run test:test-account && npm run test:upload-preview && npm run test:preview-page && npm run test:check && npm run test:check-ai && npm run test:build-structured && npm run test:doctor && npm run test:wasm && npm run test:dev-session && npm run test:dev-session-command && npm run test:dev-runtime-event && npm run test:dev-runtime-client && npm run test:dev-session-control && npm run test:dev-runtime-session-follow && npm run test:dev-client-preview-orchestrator && npm run test:dev-watch-change && npm run test:network && npm run test:vibe && npx tsc --noEmit && npm run build && npm run test:vibe-process && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
52
+ "test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process && npm run test:dev-client-preview-control-process && node scripts/dev-workflow-smoke.js",
53
+ "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag vibe-beta",
52
54
  "setup": "node scripts/setup.js",
53
55
  "reset:local-state": "node scripts/resetLocalState.js",
54
56
  "dev:link": "npm run build && npm link",
@@ -60,7 +62,7 @@
60
62
  ],
61
63
  "author": "zhanghongyang.mocha",
62
64
  "dependencies": {
63
- "@anrans001/ttmg-wasmtool": "^0.3.2",
65
+ "@anrans001/ttmg-wasmtool": "^0.3.3",
64
66
  "acorn": "^8.15.0",
65
67
  "archiver": "^7.0.1",
66
68
  "axios": "^1.11.0",
@@ -93,7 +95,7 @@
93
95
  "qs": "6.12.1",
94
96
  "semver": "^7.7.2",
95
97
  "socks-proxy-agent": "^9.0.0",
96
- "ttmg-pack": "0.4.13-beta.2",
98
+ "ttmg-pack": "0.4.13",
97
99
  "ws": "^8.18.3"
98
100
  },
99
101
  "devDependencies": {