@ttmg/cli 0.4.6-vibe-beta.21 → 0.4.6-vibe-beta.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.21",
3
+ "version": "0.4.6-vibe-beta.23",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -115,7 +115,7 @@ function events(result) {
115
115
  async function main() {
116
116
  // Exercise the built worker IPC and new command without creating a platform task.
117
117
  const authWorker = spawn(process.execPath, [entry, '__vibe-page-server'], {
118
- env: {...env, TTMG_VIBE_PAGE_TTL_MS: '500'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
118
+ env: {...env, TTMG_VIBE_PAGE_TTL_MS: '30000'}, stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
119
119
  });
120
120
  children.add(authWorker);
121
121
  const workerMessage = predicate => new Promise((resolve, reject) => {
@@ -130,9 +130,6 @@ async function main() {
130
130
  let rendered = workerMessage(message => message.type === 'rendered' && message.id === 1);
131
131
  authWorker.send({type: 'render', id: 1, html: '<h1>Waiting</h1>', browserLandingUrl: landing});
132
132
  await rendered;
133
- await new Promise(resolve => setTimeout(resolve, 700));
134
- assert.equal(await (await fetch(ready.url)).text(), '<h1>Waiting</h1>',
135
- 'Retention TTL must not close the page while the CLI is still polling');
136
133
  const browserReceipt = path.join(temp, 'browser-open.ndjson');
137
134
  const browserEnv = {
138
135
  NODE_OPTIONS: `--require=${path.join(__dirname, 'vibe-auth-open-test-preload.cjs')}`,
@@ -165,15 +162,7 @@ async function main() {
165
162
  assert.doesNotMatch(invalid.stdout + invalid.stderr, /process-private-token/);
166
163
  }
167
164
  assert.equal(fs.readFileSync(browserReceipt, 'utf8').trim().split('\n').length, 2);
168
- const workerExited = new Promise((resolve, reject) => {
169
- const timer = setTimeout(() => reject(new Error('Detached worker did not expire')), 4000);
170
- authWorker.once('exit', code => { clearTimeout(timer); resolve(code); });
171
- });
172
- authWorker.disconnect();
173
- assert.equal(await (await fetch(ready.url)).text(), '<h1>Complete</h1>',
174
- 'A completed page remains readable after CLI detaches');
175
- assert.equal(await workerExited, 0);
176
- await assert.rejects(fetch(ready.url), 'Detached page must close when retention expires');
165
+ authWorker.kill();
177
166
  children.delete(authWorker);
178
167
  fixtureServer = http.createServer(async (req, res) => {
179
168
  const url = new URL(req.url, 'http://127.0.0.1');
@@ -319,7 +308,8 @@ async function main() {
319
308
  const replies = {
320
309
  create_minis_import_task_for_agent: {
321
310
  task_id: '7684370037737619477', import_token: 'receipt-test-secret',
322
- landing_url: 'https://example.invalid/auth', poll_interval_seconds: 0.01,
311
+ landing_url: 'https://example.invalid/auth?task_id=7684370037737619477&import_token=receipt-test-secret',
312
+ poll_interval_seconds: 0.01,
323
313
  },
324
314
  get_minis_import_task_detail_for_agent: { status: 2, ready_for_input: true, preview_url: 'https://www.tiktok.com/minis/receipt-preview' },
325
315
  upload_minis_import_app_icon_for_agent: { icon_uri: 'test-icon' },
@@ -331,12 +321,35 @@ async function main() {
331
321
  });
332
322
  await new Promise(resolve => uploadServer.listen(0, '127.0.0.1', resolve));
333
323
  try {
334
- const saved = await run([...input, '--format', 'ndjson'], undefined, {
324
+ const saved = await run([...input, '--format', 'ndjson', '--verbose'], undefined, {
335
325
  TTMG_VIBE_BACKEND: 'local-http',
336
326
  TTMG_VIBE_API: `http://127.0.0.1:${uploadServer.address().port}`,
337
327
  });
338
328
  assert.equal(saved.code, 0, saved.stderr);
339
329
  const savedEvents = events(saved);
330
+ const waitingPage = savedEvents.find(e => e.status === 'waiting_authorization').pageUrl;
331
+ const savedStderr = saved.stderr.split(/\r?\n/).filter(Boolean);
332
+ assert.deepEqual(savedStderr.filter(line => line.startsWith('page: ')), [`page: ${waitingPage}`]);
333
+ const httpTraces = savedStderr.filter(line => line.startsWith('{')).map(JSON.parse);
334
+ assert.deepEqual(httpTraces.map(trace => trace.event), [
335
+ 'http_request', 'http_response',
336
+ 'http_request', 'http_response',
337
+ 'http_request', 'http_response',
338
+ 'http_request', 'http_response',
339
+ 'http_request', 'http_response',
340
+ 'http_request', 'http_response',
341
+ ]);
342
+ const createRequest = httpTraces.find(trace => trace.event === 'http_request' && trace.url.endsWith('/create_minis_import_task_for_agent/'));
343
+ const createResponse = httpTraces.find(trace => trace.event === 'http_response' && trace.url.endsWith('/create_minis_import_task_for_agent/'));
344
+ assert.equal(createRequest.request.source_project_id.length, 36);
345
+ assert.equal(createRequest.request.task_idempotency_id.length, 36);
346
+ assert.equal(createResponse.response.task_id, '7684370037737619477');
347
+ assert.equal(createResponse.response.import_token, '[REDACTED]');
348
+ assert.match(createResponse.response.landing_url, /import_token=%5BREDACTED%5D/);
349
+ const pollRequests = httpTraces.filter(trace => trace.event === 'http_request' && trace.url.endsWith('/get_minis_import_task_detail_for_agent/'));
350
+ assert.ok(pollRequests.length >= 2);
351
+ assert.ok(pollRequests.every(trace => trace.request.task_id === '7684370037737619477' && trace.request.import_token === '[REDACTED]'));
352
+ assert.doesNotMatch(saved.stderr, /receipt-test-secret/);
340
353
  const savedFinal = savedEvents.at(-1);
341
354
  assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
342
355
  assert.equal(savedFinal.uploaded, true);
@@ -348,7 +361,7 @@ async function main() {
348
361
  'upload_minis_import_package_for_agent',
349
362
  'get_minis_import_task_detail_for_agent',
350
363
  ]);
351
- assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret|test-package/);
364
+ assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret/);
352
365
  } finally {
353
366
  uploadServer.close();
354
367
  }
@@ -401,10 +414,7 @@ async function main() {
401
414
  const previewHtml = await (await fetch(final.pageUrl)).text();
402
415
  assert.match(previewHtml, /Simulated upload complete/);
403
416
  assert.match(previewHtml, /data-phase="complete"/);
404
- assert.doesNotMatch(previewHtml.split('<body>')[1], /QR|preview|review|Publish to TikTok/);
405
- const localizedPage = await fetch(final.pageUrl, {headers: {'Accept-Language': 'zh-CN'}});
406
- assert.equal(localizedPage.headers.get('vary'), 'Accept-Language');
407
- assert.match(await localizedPage.text(), /<html lang="zh-CN">[\s\S]*模拟上传完成/);
417
+ assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
408
418
 
409
419
  const beforeWeb = requests.length;
410
420
  const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
@@ -493,9 +503,7 @@ async function main() {
493
503
  assert.equal(output.at(-1).codeAssetId, '7686101169281894418');
494
504
  assert.equal(output.at(-1).qrPurpose, 'game_preview');
495
505
  const readyPage = await (await fetch(output.at(-1).pageUrl)).text();
496
- assert.match(readyPage, /Progress QR/);
497
506
  assert.match(readyPage, /Game preview QR/);
498
- assert.ok(readyPage.indexOf('alt="Progress QR"') < readyPage.indexOf('alt="Game preview QR"'));
499
507
  assert.match(readyPage, /data-phase="preview"/);
500
508
  assert.equal(output.filter(e => e.terminal).length, 1);
501
509
  assert.equal(webPolls, 7, 'Keep polling the same task until preview URL is available');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.21",
3
+ "version": "0.4.6-vibe-beta.23",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'crypto';
3
+ import { GAME_CATEGORIES, GAME_CATEGORY_CATALOG_VERSION, getGameCategories, isGameCategoryPair } from './categories';
4
+ import { applyVibeCategory, VIBE_PPE_DEFAULT_APP_INFO } from '../vibe/appInfo';
5
+
6
+ const result = getGameCategories({ format: 'json' });
7
+ assert.equal(result.platformWrite, false);
8
+ assert.equal(result.categoryCount, 12);
9
+ assert.equal(result.subcategoryCount, 114);
10
+ assert.equal(result.errorCode, null);
11
+ assert.equal(GAME_CATEGORY_CATALOG_VERSION, 'sha256:' + createHash('sha256').update(JSON.stringify(GAME_CATEGORIES)).digest('hex'));
12
+ assert.equal(new Set(GAME_CATEGORIES.map(item => item.value)).size, 12);
13
+ for (const category of GAME_CATEGORIES) {
14
+ assert.equal(new Set(category.subcategories.map(item => item.value)).size, category.subcategories.length);
15
+ for (const item of category.subcategories) assert.ok(item.description.trim());
16
+ }
17
+ assert.deepEqual(getGameCategories({ category: 'Puzzle' }).categories.map(item => item.value), ['Puzzle']);
18
+ assert.equal(getGameCategories({ category: 'puzzle' }).errorCode, 'GAME_CATEGORY_UNKNOWN');
19
+ assert.equal(getGameCategories({ category: '' }).errorCode, 'GAME_CATEGORY_UNKNOWN');
20
+ assert.equal(getGameCategories({ format: 'xml' }).errorCode, 'GAME_CATEGORIES_INVALID_FORMAT');
21
+ assert.ok(isGameCategoryPair('Action', "Shoot'em Up"));
22
+ assert.ok(isGameCategoryPair('Action', "Beat 'em up"));
23
+ assert.ok(!isGameCategoryPair('Other', 'Physics'));
24
+ assert.ok(!isGameCategoryPair('Casino', 'Bingo'));
25
+
26
+ assert.equal(applyVibeCategory({}), undefined);
27
+ const legacy = { ...VIBE_PPE_DEFAULT_APP_INFO, category: 'Casual', subcategory: 'Puzzle', description: 'Legacy description' };
28
+ assert.equal(applyVibeCategory({}, legacy), legacy, 'Old file callers remain unchanged when new flags are absent');
29
+ const pair = { category: 'Puzzle', subcategory: 'Physics' };
30
+ assert.deepEqual(applyVibeCategory(pair), { ...VIBE_PPE_DEFAULT_APP_INFO, ...pair });
31
+ assert.deepEqual(applyVibeCategory(pair, legacy), { ...legacy, ...pair });
32
+ assert.equal(VIBE_PPE_DEFAULT_APP_INFO.category, 'Other');
33
+ assert.equal(legacy.category, 'Casual', 'Overrides must not mutate old input');
34
+ for (const options of [{category:'Puzzle'}, {subcategory:'Physics'}])
35
+ assert.throws(() => applyVibeCategory(options), /VIBE_CATEGORY_PAIR_REQUIRED/);
36
+ for (const options of [{category:'Other',subcategory:'Physics'}, {category:'puzzle',subcategory:'Physics'}, {category:'',subcategory:''}])
37
+ assert.throws(() => applyVibeCategory(options), /VIBE_INVALID_CATEGORY/);
38
+ console.log('game category catalog and compatibility tests passed');
@@ -0,0 +1,48 @@
1
+ import { randomUUID } from 'crypto';
2
+ import catalog from './categoryCatalog.json';
3
+ import { t } from '../../../libs/i18n';
4
+
5
+ export const GAME_CATEGORY_CATALOG_VERSION = catalog.catalogVersion;
6
+ export const GAME_CATEGORIES = catalog.categories;
7
+
8
+ export function isGameCategoryPair(category: string, subcategory: string) {
9
+ return GAME_CATEGORIES.some(item => item.value === category &&
10
+ item.subcategories.some(child => child.value === subcategory));
11
+ }
12
+
13
+ export function getGameCategories(options: { category?: string; format?: string } = {}) {
14
+ const errorCode = !['text', 'json', 'ndjson'].includes(options.format || 'text')
15
+ ? 'GAME_CATEGORIES_INVALID_FORMAT'
16
+ : options.category !== undefined && !GAME_CATEGORIES.some(item => item.value === options.category)
17
+ ? 'GAME_CATEGORY_UNKNOWN' : null;
18
+ const categories = errorCode ? [] : GAME_CATEGORIES.filter(item =>
19
+ options.category === undefined || item.value === options.category);
20
+ return {
21
+ schemaVersion: '1',
22
+ command: 'game categories',
23
+ operationId: `game_categories_${randomUUID()}`,
24
+ status: errorCode ? 'failed' : 'ready',
25
+ terminal: true,
26
+ platformWrite: false,
27
+ catalogVersion: GAME_CATEGORY_CATALOG_VERSION,
28
+ source: catalog.source,
29
+ descriptionLanguage: 'en',
30
+ categoryCount: categories.length,
31
+ subcategoryCount: categories.reduce((count, item) => count + item.subcategories.length, 0),
32
+ categories,
33
+ errorCode,
34
+ error: errorCode ? t(errorCode === 'GAME_CATEGORY_UNKNOWN'
35
+ ? 'cli.command.game.categories.unknown' : 'cli.command.game.categories.invalidFormat') : null,
36
+ };
37
+ }
38
+
39
+ export function gameCategories(options: { category?: string; format?: string }) {
40
+ const result = getGameCategories(options);
41
+ if (options.format && options.format !== 'text') console.log(JSON.stringify(result));
42
+ else if (result.error) console.error(result.error);
43
+ else for (const category of result.categories) {
44
+ console.log(category.value);
45
+ for (const item of category.subcategories) console.log(` ${item.value}: ${item.description}`);
46
+ }
47
+ if (result.errorCode) process.exitCode = 1;
48
+ }