@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/CHANGELOG.md +516 -0
- package/README.md +5 -13
- package/dist/index.js +198 -530
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/scripts/vibe-upload-smoke.js +30 -22
- package/package.json +1 -1
- package/src/commands/common/game/categories.test.ts +38 -0
- package/src/commands/common/game/categories.ts +48 -0
- package/src/commands/common/game/categoryCatalog.json +522 -0
- package/vibe-upload.md +19 -28
package/dist/package.json
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
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',
|
|
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
|
|
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
|
|
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
|
@@ -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
|
+
}
|