@ttmg/cli 0.4.6-vibe-beta.5 → 0.4.6-vibe-beta.7

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,19 +1,23 @@
1
1
  {
2
2
  "name": "@ttmg/cli",
3
- "version": "0.4.6-vibe-beta.5",
3
+ "version": "0.4.6-vibe-beta.7",
4
4
  "description": "TikTok Mini Game Command Line Tool",
5
5
  "license": "ISC",
6
6
  "bin": {
7
7
  "ttmg": "dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "vibe-upload.md"
11
12
  ],
12
13
  "publishConfig": {
13
14
  "access": "public",
14
15
  "registry": "https://registry.npmjs.org/"
15
16
  },
16
17
  "scripts": {
18
+ "demo:vibe": "npm run build && node scripts/vibe-upload-demo.js",
19
+ "test:vibe": "node scripts/run-ts-test.js src/commands/common/vibe/vibe.test.ts",
20
+ "test:vibe-process": "node scripts/vibe-upload-smoke.js",
17
21
  "prepack": "npm run build && npm run verify:dist-version && npm run verify:agent-contract",
18
22
  "build": "node scripts/build.js",
19
23
  "verify:dist-version": "node scripts/verify-dist-version.js",
@@ -30,14 +34,20 @@
30
34
  "test:dev-session": "node scripts/run-ts-test.js src/commands/native/dev/agentSession.test.ts",
31
35
  "test:dev-session-command": "node scripts/run-ts-test.js src/commands/native/dev/session.test.ts",
32
36
  "test:dev-runtime-event": "node scripts/run-ts-test.js src/commands/native/dev/utils/scoket.test.ts",
37
+ "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-runtime-session-follow": "node scripts/run-ts-test.js src/commands/native/dev/runtime/runtimeSessionFollow.test.ts",
40
+ "test:dev-client-preview-orchestrator": "node scripts/run-ts-test.js src/commands/native/dev/runtime/clientPreviewOrchestrator.test.ts",
41
+ "test:dev-watch-change": "node scripts/run-ts-test.js src/commands/native/dev/utils/watchChange.test.ts",
33
42
  "test:dev-game-ready-process": "npm run build && node scripts/dev-game-ready-smoke.js",
34
43
  "test:dev-default-process": "npm run build && node scripts/dev-default-smoke.js",
44
+ "test:dev-client-preview-control-process": "npm run build && node scripts/dev-client-preview-control-smoke.js",
35
45
  "test:check": "node scripts/run-ts-test.js src/commands/native/check/check.test.ts",
36
46
  "test:check-ai": "node scripts/run-ts-test.js src/commands/native/check/ai.test.ts",
37
47
  "test:build-structured": "node scripts/run-ts-test.js src/commands/native/build/build.test.ts",
38
48
  "test:doctor": "node scripts/run-ts-test.js src/commands/native/doctor/doctor.test.ts",
39
- "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:network && npx tsc --noEmit && npm run build && npm run test:preview-page-process && npm run test:preview-page-detached && npm run verify:dist-version && npm run verify:agent-contract",
40
- "test:agent-contract:full": "npm run test:agent-contract && npm run test:dev-default-process && npm run test:dev-game-ready-process",
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",
41
51
  "publish:beta": "npm publish --registry=https://registry.npmjs.org/ --access public --tag beta",
42
52
  "setup": "node scripts/setup.js",
43
53
  "reset:local-state": "node scripts/resetLocalState.js",
@@ -0,0 +1,322 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const net = require('node:net');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+
8
+ function runCli(args, cwd) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(
11
+ process.execPath,
12
+ [path.resolve(__dirname, '../dist/index.js'), ...args],
13
+ {
14
+ cwd,
15
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ },
18
+ );
19
+ let stdout = '';
20
+ let stderr = '';
21
+ child.stdout.on('data', chunk => {
22
+ stdout += chunk;
23
+ });
24
+ child.stderr.on('data', chunk => {
25
+ stderr += chunk;
26
+ });
27
+ child.once('error', reject);
28
+ child.once('close', status => resolve({ status, stdout, stderr }));
29
+ });
30
+ }
31
+
32
+ function write(socket, value) {
33
+ socket.write(`${JSON.stringify(value)}\n`);
34
+ }
35
+
36
+ async function run() {
37
+ const shortTmp = process.platform === 'win32' ? os.tmpdir() : '/tmp';
38
+ const root = fs.mkdtempSync(path.join(shortTmp, 'ttmg-preview-control-'));
39
+ const gameDirectory = path.join(root, 'game');
40
+ const previewGameDirectory = path.join(
41
+ root,
42
+ `client-preview-project-${'x'.repeat(80)}`,
43
+ );
44
+ const sessionId = 'dev_control_smoke';
45
+ const sessionDirectory = path.join(
46
+ gameDirectory,
47
+ '.ttmg',
48
+ 'sessions',
49
+ sessionId,
50
+ );
51
+ const screenshotsDirectory = path.join(sessionDirectory, 'screenshots');
52
+ fs.mkdirSync(screenshotsDirectory, { recursive: true });
53
+ fs.mkdirSync(previewGameDirectory, { recursive: true });
54
+ fs.writeFileSync(
55
+ path.join(previewGameDirectory, 'game.js'),
56
+ 'console.log("game");\n',
57
+ );
58
+ fs.writeFileSync(path.join(previewGameDirectory, 'game.json'), '{}\n');
59
+ const control =
60
+ process.platform === 'win32'
61
+ ? {
62
+ transport: 'named-pipe',
63
+ path: `\\\\.\\pipe\\ttmg-${sessionId}`,
64
+ }
65
+ : {
66
+ transport: 'unix',
67
+ path: path.join(sessionDirectory, 'ipc'),
68
+ };
69
+ fs.writeFileSync(
70
+ path.join(sessionDirectory, 'manifest.json'),
71
+ JSON.stringify({
72
+ schemaVersion: '1',
73
+ command: 'dev',
74
+ platformWrite: false,
75
+ sessionId,
76
+ status: 'ready',
77
+ mode: 'client-preview',
78
+ project: { clientKey: 'smoke-client', directory: gameDirectory },
79
+ process: { pid: process.pid, startedAt: Date.now(), command: 'ttmg dev' },
80
+ runtime: {
81
+ status: 'ready',
82
+ runtimeSessionId: 'runtime-control-smoke',
83
+ protocolVersion: '1.0',
84
+ capabilities: { logs: true, screenshot: true },
85
+ },
86
+ control,
87
+ }),
88
+ );
89
+ const server = net.createServer(socket => {
90
+ let buffer = '';
91
+ socket.on('data', chunk => {
92
+ buffer += chunk.toString();
93
+ if (!buffer.includes('\n')) return;
94
+ const request = JSON.parse(buffer.slice(0, buffer.indexOf('\n')));
95
+ if (request.operation === 'logs.snapshot') {
96
+ write(socket, {
97
+ requestId: request.requestId,
98
+ type: 'result',
99
+ result: {
100
+ records: [
101
+ {
102
+ sequence: '90071992547409930002',
103
+ timestamp_ms: 2,
104
+ severity: 'info',
105
+ kind: 'text',
106
+ source: 'native_runtime',
107
+ message: 'snapshot',
108
+ formatted_line: '[I][native_runtime] snapshot',
109
+ },
110
+ ],
111
+ nextCursor: '90071992547409930002',
112
+ oldestCursor: '90071992547409930001',
113
+ snapshotCursor: '90071992547409930002',
114
+ hasMore: false,
115
+ cursorExpired: false,
116
+ },
117
+ });
118
+ socket.end();
119
+ return;
120
+ }
121
+ if (request.operation === 'logs.follow') {
122
+ write(socket, {
123
+ requestId: request.requestId,
124
+ type: 'result',
125
+ result: {
126
+ records: [],
127
+ nextCursor: request.params.after,
128
+ oldestCursor: '1',
129
+ snapshotCursor: request.params.after,
130
+ hasMore: false,
131
+ cursorExpired: false,
132
+ },
133
+ });
134
+ write(socket, {
135
+ requestId: request.requestId,
136
+ type: 'event',
137
+ records: [
138
+ {
139
+ sequence: '90071992547409930003',
140
+ timestamp_ms: 3,
141
+ severity: 'error',
142
+ kind: 'text',
143
+ source: 'structured_trace',
144
+ message: 'live',
145
+ formatted_line: '[E][structured_trace] live',
146
+ },
147
+ ],
148
+ cursor: '90071992547409930003',
149
+ });
150
+ write(socket, {
151
+ requestId: request.requestId,
152
+ type: 'end',
153
+ cursor: '90071992547409930003',
154
+ reason: 'timeout',
155
+ });
156
+ socket.end();
157
+ return;
158
+ }
159
+ if (request.operation === 'screenshot.capture') {
160
+ assert.equal(request.params.format, 'jpeg');
161
+ assert.equal(request.params.quality, 0.8);
162
+ const output = request.params.output;
163
+ fs.writeFileSync(output, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
164
+ write(socket, {
165
+ requestId: request.requestId,
166
+ type: 'result',
167
+ result: {
168
+ path: output,
169
+ mime: 'image/jpeg',
170
+ size: 4,
171
+ width: 1,
172
+ height: 1,
173
+ captureTime: '1787700000789',
174
+ sha256: 'smoke-hash',
175
+ },
176
+ });
177
+ socket.end();
178
+ }
179
+ });
180
+ });
181
+
182
+ try {
183
+ const previewRun = await runCli(
184
+ [
185
+ 'dev',
186
+ '--mode',
187
+ 'client-preview',
188
+ '--client-key',
189
+ 'smoke-client',
190
+ '--events',
191
+ 'ndjson',
192
+ '--no-open',
193
+ '--timeout',
194
+ '1',
195
+ ],
196
+ previewGameDirectory,
197
+ );
198
+ assert.equal(previewRun.status, 2, previewRun.stderr || previewRun.stdout);
199
+ const previewEvents = previewRun.stdout
200
+ .trim()
201
+ .split(/\r?\n/)
202
+ .filter(Boolean)
203
+ .map(JSON.parse);
204
+ const readyForScan = previewEvents.find(
205
+ event =>
206
+ event.type === 'lifecycle' &&
207
+ event.payload?.state === 'READY_FOR_SCAN',
208
+ );
209
+ assert.ok(readyForScan, previewRun.stderr || previewRun.stdout);
210
+ assert.equal(fs.existsSync(readyForScan.payload.qrPngPath), true);
211
+ const previewManifest = JSON.parse(
212
+ fs.readFileSync(readyForScan.payload.manifest, 'utf8'),
213
+ );
214
+ if (previewManifest.control?.transport === 'unix') {
215
+ assert.equal(Buffer.byteLength(previewManifest.control.path) <= 100, true);
216
+ }
217
+ assert.match(previewRun.stderr, /QR image:/);
218
+ assert.equal(
219
+ previewEvents.filter(
220
+ event =>
221
+ event.type === 'compile' && event.payload?.state === 'succeeded',
222
+ ).length,
223
+ 1,
224
+ 'session evidence must not trigger an endless recompilation loop',
225
+ );
226
+
227
+ await new Promise((resolve, reject) => {
228
+ server.once('error', reject);
229
+ server.listen(control.path, resolve);
230
+ });
231
+ if (control.transport === 'unix') fs.chmodSync(control.path, 0o600);
232
+
233
+ const snapshotRun = await runCli(
234
+ [
235
+ 'dev',
236
+ 'session',
237
+ 'logs',
238
+ sessionId,
239
+ '--after',
240
+ '90071992547409930001',
241
+ '--format',
242
+ 'json',
243
+ ],
244
+ gameDirectory,
245
+ );
246
+ assert.equal(
247
+ snapshotRun.status,
248
+ 0,
249
+ snapshotRun.stderr || snapshotRun.stdout,
250
+ );
251
+ const snapshotLines = snapshotRun.stdout.trim().split(/\r?\n/);
252
+ assert.equal(snapshotLines.length, 1);
253
+ const snapshot = JSON.parse(snapshotLines[0]);
254
+ assert.equal(snapshot.command, 'dev.session.logs');
255
+ assert.equal(snapshot.records[0].sequence, '90071992547409930002');
256
+ assert.equal(typeof snapshot.records[0].sequence, 'string');
257
+
258
+ const followRun = await runCli(
259
+ [
260
+ 'dev',
261
+ 'session',
262
+ 'logs',
263
+ sessionId,
264
+ '--after',
265
+ '90071992547409930002',
266
+ '--follow',
267
+ '--timeout',
268
+ '1',
269
+ '--format',
270
+ 'ndjson',
271
+ ],
272
+ gameDirectory,
273
+ );
274
+ assert.equal(followRun.status, 0, followRun.stderr || followRun.stdout);
275
+ const frames = followRun.stdout.trim().split(/\r?\n/).map(JSON.parse);
276
+ assert.deepEqual(
277
+ frames.map(frame => frame.type),
278
+ ['log_snapshot', 'log_batch', 'follow_end'],
279
+ );
280
+ assert.equal(
281
+ frames.every(frame => frame.platformWrite === false),
282
+ true,
283
+ );
284
+
285
+ const screenshotOutput = path.join(root, 'screen.jpeg');
286
+ const screenshotRun = await runCli(
287
+ [
288
+ 'dev',
289
+ 'session',
290
+ 'screenshot',
291
+ sessionId,
292
+ '--output',
293
+ screenshotOutput,
294
+ '--format',
295
+ 'json',
296
+ ],
297
+ gameDirectory,
298
+ );
299
+ assert.equal(
300
+ screenshotRun.status,
301
+ 0,
302
+ screenshotRun.stderr || screenshotRun.stdout,
303
+ );
304
+ const screenshotLines = screenshotRun.stdout.trim().split(/\r?\n/);
305
+ assert.equal(screenshotLines.length, 1);
306
+ const screenshot = JSON.parse(screenshotLines[0]);
307
+ assert.equal(screenshot.path, screenshotOutput);
308
+ assert.equal(screenshot.mime, 'image/jpeg');
309
+ assert.equal('data' in screenshot, false);
310
+ assert.equal(fs.readFileSync(screenshotOutput).toString('hex'), 'ffd8ffd9');
311
+ } finally {
312
+ await new Promise(resolve => server.close(resolve));
313
+ fs.rmSync(root, { recursive: true, force: true });
314
+ }
315
+
316
+ console.log('client preview control process smoke passed');
317
+ }
318
+
319
+ run().catch(error => {
320
+ console.error(error);
321
+ process.exitCode = 1;
322
+ });
@@ -103,6 +103,13 @@ assert.equal(
103
103
  'ttmg dev --events ndjson --no-open',
104
104
  );
105
105
  assert.equal(capabilities.commands.dev.outputModesMutuallyExclusive, true);
106
+ assert.equal(capabilities.commands.dev.clientPreview.available, true);
107
+ assert.equal(capabilities.commands.dev.clientPreview.protocolVersion, '1.0');
108
+ assert.equal(capabilities.commands.dev.clientPreview.humanScanRequired, true);
109
+ assert.equal(capabilities.commands.dev.clientPreview.deviceLaunch, false);
110
+ assert.equal(capabilities.commands.dev.clientPreview.verifiedOnDevice, false);
111
+ assert.equal(capabilities.commands.dev.sessionLogs.available, true);
112
+ assert.equal(capabilities.commands.dev.sessionScreenshot.available, true);
106
113
  assert.equal(capabilities.commands.uploadPreview.platformWrite, true);
107
114
  assert.equal(
108
115
  capabilities.commands.uploadPreview.previewPage.option,
@@ -153,6 +160,10 @@ assert.equal(
153
160
  );
154
161
  assert.equal(capabilities.dev.gameReadyProtocol, true);
155
162
  assert.equal(capabilities.dev.gameReady, false);
163
+ assert.equal(capabilities.dev.runtimeLogs, false);
164
+ assert.equal(capabilities.dev.screenshot, false);
165
+ assert.equal(capabilities.dev.clientPreviewProtocol.runtimeLogsVerified, false);
166
+ assert.equal(capabilities.dev.clientPreviewProtocol.screenshotVerified, false);
156
167
  assertNoSecretKeys(capabilities, 'capabilities');
157
168
 
158
169
  const conflictingDevOutput = runCli([
@@ -174,6 +185,19 @@ assert.match(
174
185
  /output modes cannot be combined|输出模式不能同时使用/,
175
186
  );
176
187
 
188
+ const devHelp = runCli(['dev', '--help']);
189
+ assertExit(devHelp, 0, 'dev help');
190
+ assert.match(devHelp.stdout, /--mode <mode>/);
191
+
192
+ const logsHelp = runCli(['dev', 'session', 'logs', '--help']);
193
+ assertExit(logsHelp, 0, 'dev session logs help');
194
+ assert.match(logsHelp.stdout, /--follow\b/);
195
+ assert.match(logsHelp.stdout, /--until <cursor>/);
196
+
197
+ const screenshotHelp = runCli(['dev', 'session', 'screenshot', '--help']);
198
+ assertExit(screenshotHelp, 0, 'dev session screenshot help');
199
+ assert.match(screenshotHelp.stdout, /--image-format <format>/);
200
+
177
201
  const checkHelp = runCli(['check', '--help']);
178
202
  assertExit(checkHelp, 0, 'check help');
179
203
  assert.match(checkHelp.stdout, /Usage: ttmg check \[options\]/);
@@ -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
+ });