@ttmg/cli 0.4.6-vibe-beta.22 → 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.22",
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,14 +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);
340
330
  const waitingPage = savedEvents.find(e => e.status === 'waiting_authorization').pageUrl;
341
- assert.deepEqual(saved.stderr.split(/\r?\n/).filter(line => line.startsWith('page: ')), [`page: ${waitingPage}`]);
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/);
342
353
  const savedFinal = savedEvents.at(-1);
343
354
  assert.equal(savedFinal.status, 'uploaded', JSON.stringify(savedEvents.map(e => ({ status: e.status, terminal: e.terminal, step: e.uploadStep, uploaded: e.uploaded }))));
344
355
  assert.equal(savedFinal.uploaded, true);
@@ -350,7 +361,7 @@ async function main() {
350
361
  'upload_minis_import_package_for_agent',
351
362
  'get_minis_import_task_detail_for_agent',
352
363
  ]);
353
- assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret|test-package/);
364
+ assert.doesNotMatch(saved.stdout + saved.stderr, /receipt-test-secret/);
354
365
  } finally {
355
366
  uploadServer.close();
356
367
  }
@@ -403,10 +414,7 @@ async function main() {
403
414
  const previewHtml = await (await fetch(final.pageUrl)).text();
404
415
  assert.match(previewHtml, /Simulated upload complete/);
405
416
  assert.match(previewHtml, /data-phase="complete"/);
406
- assert.doesNotMatch(previewHtml.split('<body>')[1], /QR|preview|review|Publish to TikTok/);
407
- const localizedPage = await fetch(final.pageUrl, {headers: {'Accept-Language': 'zh-CN'}});
408
- assert.equal(localizedPage.headers.get('vary'), 'Accept-Language');
409
- assert.match(await localizedPage.text(), /<html lang="zh-CN">[\s\S]*模拟上传完成/);
417
+ assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
410
418
 
411
419
  const beforeWeb = requests.length;
412
420
  const webConflict = await run([...input, '--web', '--mock', '--format', 'json']);
@@ -495,9 +503,7 @@ async function main() {
495
503
  assert.equal(output.at(-1).codeAssetId, '7686101169281894418');
496
504
  assert.equal(output.at(-1).qrPurpose, 'game_preview');
497
505
  const readyPage = await (await fetch(output.at(-1).pageUrl)).text();
498
- assert.match(readyPage, /Progress QR/);
499
506
  assert.match(readyPage, /Game preview QR/);
500
- assert.ok(readyPage.indexOf('alt="Progress QR"') < readyPage.indexOf('alt="Game preview QR"'));
501
507
  assert.match(readyPage, /data-phase="preview"/);
502
508
  assert.equal(output.filter(e => e.terminal).length, 1);
503
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.22",
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
+ }