@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/README.md +88 -5
- package/dist/index.js +4287 -424
- package/dist/index.js.map +1 -1
- package/dist/package.json +14 -4
- package/dist/scripts/dev-client-preview-control-smoke.js +322 -0
- package/dist/scripts/verify-agent-contract.js +24 -0
- package/dist/scripts/vibe-ppe-test-preload.cjs +23 -0
- package/dist/scripts/vibe-upload-demo.js +72 -0
- package/dist/scripts/vibe-upload-smoke.js +341 -0
- package/package.json +14 -4
- package/vibe-upload.md +183 -0
|
@@ -0,0 +1,341 @@
|
|
|
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 { spawn } = require('node:child_process');
|
|
6
|
+
const archiver = require('archiver');
|
|
7
|
+
const http = require('node:http');
|
|
8
|
+
|
|
9
|
+
const entry = path.resolve(__dirname, '../dist/index.js');
|
|
10
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ttmg-vibe-process-'));
|
|
11
|
+
const icon = path.join(temp, 'avatar.png');
|
|
12
|
+
const zip = path.join(temp, 'game.zip');
|
|
13
|
+
const input = [
|
|
14
|
+
'upload',
|
|
15
|
+
'--vibe',
|
|
16
|
+
'--title',
|
|
17
|
+
'Local test game',
|
|
18
|
+
'--icon',
|
|
19
|
+
icon,
|
|
20
|
+
'--package',
|
|
21
|
+
zip,
|
|
22
|
+
'--no-open',
|
|
23
|
+
];
|
|
24
|
+
const png =
|
|
25
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXyoAAAAASUVORK5CYII=';
|
|
26
|
+
const children = new Set();
|
|
27
|
+
let fixtureServer;
|
|
28
|
+
const requests = [];
|
|
29
|
+
let failStatus = false;
|
|
30
|
+
const env = {
|
|
31
|
+
...process.env,
|
|
32
|
+
HOME: temp,
|
|
33
|
+
USERPROFILE: temp,
|
|
34
|
+
TTMGRC_PATH: path.join(temp, '.ttmgrc'),
|
|
35
|
+
TTMG_VIBE_PAGE_TTL_MS: '7000',
|
|
36
|
+
TTMG_VIBE_BACKEND: '',
|
|
37
|
+
TTMG_VIBE_API: '',
|
|
38
|
+
TTMG_VIBE_PPE: '',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function run(args, onEvent, extraEnv = {}) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
44
|
+
cwd: temp,
|
|
45
|
+
env: { ...env, ...extraEnv },
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
children.add(child);
|
|
49
|
+
let stdout = '';
|
|
50
|
+
let stderr = '';
|
|
51
|
+
let pending = '';
|
|
52
|
+
let callbackError;
|
|
53
|
+
const callbacks = [];
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
child.kill('SIGKILL');
|
|
56
|
+
reject(new Error('CLI process timed out'));
|
|
57
|
+
}, 12000);
|
|
58
|
+
child.stdout.on('data', chunk => {
|
|
59
|
+
stdout += chunk;
|
|
60
|
+
pending += chunk;
|
|
61
|
+
let newline;
|
|
62
|
+
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
63
|
+
const line = pending.slice(0, newline);
|
|
64
|
+
pending = pending.slice(newline + 1);
|
|
65
|
+
if (onEvent && line.trim()) {
|
|
66
|
+
try {
|
|
67
|
+
callbacks.push(
|
|
68
|
+
Promise.resolve(onEvent(JSON.parse(line), child)).catch(error => {
|
|
69
|
+
callbackError = error;
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
callbackError = error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
child.stderr.on('data', chunk => {
|
|
79
|
+
stderr += chunk;
|
|
80
|
+
});
|
|
81
|
+
child.once('error', reject);
|
|
82
|
+
child.once('close', async (code, signal) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
children.delete(child);
|
|
85
|
+
await Promise.all(callbacks);
|
|
86
|
+
if (callbackError) reject(callbackError);
|
|
87
|
+
else resolve({ code, signal, stdout, stderr });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function events(result) {
|
|
92
|
+
assert.doesNotMatch(
|
|
93
|
+
result.stdout,
|
|
94
|
+
/upload_uuid|upload_token|import_token|importToken|mock-secret|mock-authorization/,
|
|
95
|
+
);
|
|
96
|
+
return result.stdout
|
|
97
|
+
.trim()
|
|
98
|
+
.split('\n')
|
|
99
|
+
.map(line => JSON.parse(line));
|
|
100
|
+
}
|
|
101
|
+
async function main() {
|
|
102
|
+
fixtureServer = http.createServer((req, res) => {
|
|
103
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
104
|
+
requests.push({ path: url.pathname, method: req.method, headers: req.headers });
|
|
105
|
+
req.resume();
|
|
106
|
+
const create = url.pathname.endsWith('/create_minis_import_task_for_agent/');
|
|
107
|
+
const status = url.pathname.endsWith('/get_minis_import_task_status_for_agent/');
|
|
108
|
+
assert.ok(create || status, 'Unexpected real upload endpoint');
|
|
109
|
+
assert.equal(req.headers['x-use-ppe'], '1');
|
|
110
|
+
assert.equal(req.headers['x-tt-env'], 'ppe_op_vc');
|
|
111
|
+
assert.equal(req.headers.cookie, undefined);
|
|
112
|
+
if (status) {
|
|
113
|
+
assert.equal(url.searchParams.get('task_id'), '7684370037737553941');
|
|
114
|
+
assert.equal(url.searchParams.get('import_token'), 'process-test-secret');
|
|
115
|
+
}
|
|
116
|
+
const data = create ? {
|
|
117
|
+
task_id: '7684370037737553941', import_token: 'process-test-secret',
|
|
118
|
+
landing_url: 'https://developers.tiktok.com/portal_h5/minis_import?import_token=process-test-secret',
|
|
119
|
+
poll_interval_seconds: 0.01,
|
|
120
|
+
} : { status: 1, ready_for_input: false };
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ ...data, error_info: { code: failStatus && status ? 420500 : 0 } }));
|
|
123
|
+
});
|
|
124
|
+
await new Promise(resolve => fixtureServer.listen(0, '127.0.0.1', resolve));
|
|
125
|
+
env.TTMG_VIBE_TEST_PORT = String(fixtureServer.address().port);
|
|
126
|
+
env.NODE_OPTIONS = `--require ${JSON.stringify(path.join(__dirname, 'vibe-ppe-test-preload.cjs'))}`;
|
|
127
|
+
fs.writeFileSync(icon, Buffer.from(png, 'base64'));
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
const out = fs.createWriteStream(zip);
|
|
130
|
+
const archive = archiver('zip');
|
|
131
|
+
out.once('close', resolve);
|
|
132
|
+
out.once('error', reject);
|
|
133
|
+
archive.once('error', reject);
|
|
134
|
+
archive.pipe(out);
|
|
135
|
+
archive.append('console.log("local fixture")', { name: 'game.js' });
|
|
136
|
+
archive.finalize().catch(reject);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let bindingHtml;
|
|
140
|
+
const success = await run(
|
|
141
|
+
[...input, '--mock', '--format', 'ndjson', '--poll-interval', '0.2'],
|
|
142
|
+
async event => {
|
|
143
|
+
if (event.status === 'waiting_authorization')
|
|
144
|
+
bindingHtml = await (await fetch(event.pageUrl)).text();
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
assert.equal(success.code, 0, success.stderr);
|
|
148
|
+
assert.match(bindingHtml, /Upload to TikTok/);
|
|
149
|
+
assert.match(bindingHtml, /class="game-avatar"/);
|
|
150
|
+
assert.match(bindingHtml, /PPE \+ Mock/);
|
|
151
|
+
assert.match(bindingHtml, /data-phase="binding"/);
|
|
152
|
+
assert.match(bindingHtml, /Phone authorization is not required/);
|
|
153
|
+
assert.doesNotMatch(bindingHtml, /<form|<input/);
|
|
154
|
+
const sequence = events(success);
|
|
155
|
+
const final = sequence[sequence.length - 1];
|
|
156
|
+
assert.equal(final.schemaVersion, '2');
|
|
157
|
+
assert.equal(final.status, 'uploaded');
|
|
158
|
+
assert.equal(final.terminal, true);
|
|
159
|
+
assert.equal(final.nextAction, 'none');
|
|
160
|
+
assert.equal(sequence.filter(e => e.terminal).length, 1);
|
|
161
|
+
assert.equal(final.uploaded, true);
|
|
162
|
+
assert.equal(final.platformWrite, true);
|
|
163
|
+
assert.equal(final.backend, 'ppe-mock');
|
|
164
|
+
assert.equal(final.taskStatusRaw, 1);
|
|
165
|
+
assert.equal(final.readyForInput, false);
|
|
166
|
+
assert.equal(final.simulatedReadyForInput, true);
|
|
167
|
+
assert.equal(final.simulatedTaskStatusRaw, 3);
|
|
168
|
+
assert.equal(requests.length, 4);
|
|
169
|
+
assert.equal(requests[0].method, 'POST');
|
|
170
|
+
assert.ok(requests.slice(1).every(r => r.method === 'GET'));
|
|
171
|
+
assert.doesNotMatch(success.stdout, /process-test-secret/);
|
|
172
|
+
assert.equal(final.simulation, true);
|
|
173
|
+
const progress = sequence.filter(
|
|
174
|
+
e => e.status === 'uploading' && e.loadedBytes !== undefined,
|
|
175
|
+
);
|
|
176
|
+
assert.equal(progress[0].loadedBytes, 0);
|
|
177
|
+
assert.equal(
|
|
178
|
+
progress[progress.length - 1].loadedBytes,
|
|
179
|
+
progress[progress.length - 1].totalBytes,
|
|
180
|
+
);
|
|
181
|
+
assert.ok(progress.length >= 6);
|
|
182
|
+
assert.equal(
|
|
183
|
+
sequence.filter(e => e.status === 'waiting_authorization').length,
|
|
184
|
+
1,
|
|
185
|
+
);
|
|
186
|
+
const previewHtml = await (await fetch(final.pageUrl)).text();
|
|
187
|
+
assert.match(previewHtml, /Simulated upload complete/);
|
|
188
|
+
assert.match(previewHtml, /data-phase="complete"/);
|
|
189
|
+
assert.doesNotMatch(previewHtml, /QR|preview|review|Publish to TikTok/);
|
|
190
|
+
|
|
191
|
+
const json = await run(
|
|
192
|
+
[...input, '--format', 'json', '--poll-interval', '0.01'],
|
|
193
|
+
undefined,
|
|
194
|
+
{ TTMG_VIBE_BACKEND: 'mock' },
|
|
195
|
+
);
|
|
196
|
+
assert.equal(json.code, 0, json.stderr);
|
|
197
|
+
assert.equal(events(json).length, 1);
|
|
198
|
+
assert.equal(JSON.parse(json.stdout).status, 'uploaded');
|
|
199
|
+
assert.equal(JSON.parse(json.stdout).simulation, true);
|
|
200
|
+
assert.match(json.stderr, /http:\/\/127\.0\.0\.1:/);
|
|
201
|
+
|
|
202
|
+
const missingPpe = await run([...input, '--format', 'json'], undefined, {
|
|
203
|
+
TTMG_VIBE_BACKEND: 'ppe',
|
|
204
|
+
});
|
|
205
|
+
assert.equal(missingPpe.code, 1);
|
|
206
|
+
assert.equal(events(missingPpe)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
|
|
207
|
+
assert.equal(events(missingPpe)[0].platformWrite, false);
|
|
208
|
+
const dryPpe = await run([...input, '--dry-run', '--format', 'json'], undefined, {
|
|
209
|
+
TTMG_VIBE_BACKEND: 'ppe', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
210
|
+
});
|
|
211
|
+
assert.equal(dryPpe.code, 0, dryPpe.stderr);
|
|
212
|
+
assert.equal(events(dryPpe)[0].status, 'prepared');
|
|
213
|
+
assert.equal(events(dryPpe)[0].platformWrite, false);
|
|
214
|
+
const missingHybrid = await run([...input, '--format', 'json'], undefined, {
|
|
215
|
+
TTMG_VIBE_BACKEND: 'ppe-mock',
|
|
216
|
+
});
|
|
217
|
+
assert.equal(events(missingHybrid)[0].errorCode, 'VIBE_INVALID_PPE_ENV');
|
|
218
|
+
assert.equal(events(missingHybrid)[0].platformWrite, false);
|
|
219
|
+
const dryHybrid = await run([...input, '--dry-run', '--format', 'json'], undefined, {
|
|
220
|
+
TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
221
|
+
});
|
|
222
|
+
assert.equal(dryHybrid.code, 0, dryHybrid.stderr);
|
|
223
|
+
assert.equal(events(dryHybrid)[0].status, 'prepared');
|
|
224
|
+
assert.equal(events(dryHybrid)[0].platformWrite, false);
|
|
225
|
+
const hybridMockOverride = await run([...input, '--mock', '--poll-interval', '0.01', '--format', 'json'], undefined, {
|
|
226
|
+
TTMG_VIBE_BACKEND: 'ppe-mock', TTMG_VIBE_PPE: 'ppe_op_vc',
|
|
227
|
+
});
|
|
228
|
+
assert.equal(hybridMockOverride.code, 0, hybridMockOverride.stderr);
|
|
229
|
+
assert.equal(events(hybridMockOverride)[0].backend, 'ppe-mock');
|
|
230
|
+
assert.equal(events(hybridMockOverride)[0].platformWrite, true);
|
|
231
|
+
|
|
232
|
+
failStatus = true;
|
|
233
|
+
const realFailure = await run([...input, '--mock', '--format', 'json']);
|
|
234
|
+
assert.equal(realFailure.code, 1, realFailure.stderr);
|
|
235
|
+
assert.equal(events(realFailure)[0].errorCode, 'VIBE_BACKEND_REJECTED');
|
|
236
|
+
assert.equal(events(realFailure)[0].uploadAttempted, false);
|
|
237
|
+
assert.equal(events(realFailure)[0].uploaded, false);
|
|
238
|
+
failStatus = false;
|
|
239
|
+
|
|
240
|
+
for (const [scenario, errorCode, attempted] of [
|
|
241
|
+
['denied', 'VIBE_AUTH_DENIED', false],
|
|
242
|
+
['upload-failed', 'VIBE_BACKEND_REQUEST_FAILED', true],
|
|
243
|
+
]) {
|
|
244
|
+
const failed = await run([
|
|
245
|
+
...input,
|
|
246
|
+
'--mock',
|
|
247
|
+
'--mock-scenario',
|
|
248
|
+
scenario,
|
|
249
|
+
'--poll-interval',
|
|
250
|
+
'0.01',
|
|
251
|
+
'--format',
|
|
252
|
+
'json',
|
|
253
|
+
]);
|
|
254
|
+
assert.equal(failed.code, 1, failed.stderr);
|
|
255
|
+
const result = events(failed)[0];
|
|
256
|
+
assert.equal(result.status, 'failed');
|
|
257
|
+
assert.equal(result.errorCode, errorCode);
|
|
258
|
+
assert.equal(result.uploadAttempted, attempted);
|
|
259
|
+
assert.equal(result.uploaded, false);
|
|
260
|
+
assert.equal(result.terminal, true);
|
|
261
|
+
const html = await (await fetch(result.pageUrl)).text();
|
|
262
|
+
assert.match(html, /data-phase="failed"/);
|
|
263
|
+
assert.doesNotMatch(html, /alt="Authorization QR"|data-phase="complete"/);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const dryRun = await run([...input, '--dry-run', '--format', 'json']);
|
|
267
|
+
assert.equal(dryRun.code, 0, dryRun.stderr);
|
|
268
|
+
const prepared = events(dryRun)[0];
|
|
269
|
+
assert.equal(prepared.status, 'prepared');
|
|
270
|
+
assert.equal(prepared.backend, 'none');
|
|
271
|
+
assert.equal(prepared.pageUrl, undefined);
|
|
272
|
+
assert.equal(prepared.uploadAttempted, false);
|
|
273
|
+
const beforeDryMock = requests.length;
|
|
274
|
+
const dryMock = await run([...input, '--mock', '--dry-run', '--format', 'json']);
|
|
275
|
+
assert.equal(dryMock.code, 0);
|
|
276
|
+
assert.equal(events(dryMock)[0].platformWrite, false);
|
|
277
|
+
assert.equal(requests.length, beforeDryMock);
|
|
278
|
+
|
|
279
|
+
const project = path.join(temp, 'project');
|
|
280
|
+
fs.mkdirSync(project);
|
|
281
|
+
fs.writeFileSync(path.join(project, 'game.js'), 'console.log("game");\n');
|
|
282
|
+
fs.writeFileSync(path.join(project, 'game.json'), '{}\n');
|
|
283
|
+
const projectRun = await run([
|
|
284
|
+
'upload',
|
|
285
|
+
'--vibe',
|
|
286
|
+
'--title',
|
|
287
|
+
'Source game',
|
|
288
|
+
'--icon',
|
|
289
|
+
icon,
|
|
290
|
+
'--dir',
|
|
291
|
+
project,
|
|
292
|
+
'--dry-run',
|
|
293
|
+
'--format',
|
|
294
|
+
'json',
|
|
295
|
+
]);
|
|
296
|
+
assert.equal(projectRun.code, 0, projectRun.stdout + projectRun.stderr);
|
|
297
|
+
assert.equal(events(projectRun)[0].status, 'prepared');
|
|
298
|
+
assert.ok(events(projectRun)[0].packageSizeBytes > 0);
|
|
299
|
+
|
|
300
|
+
const missingBackend = await run([...input, '--format', 'json']);
|
|
301
|
+
assert.equal(missingBackend.code, 1);
|
|
302
|
+
assert.equal(
|
|
303
|
+
events(missingBackend)[0].errorCode,
|
|
304
|
+
'VIBE_BACKEND_NOT_CONFIGURED',
|
|
305
|
+
);
|
|
306
|
+
const badMode = await run(['upload', '--title', 'x', '--format', 'json']);
|
|
307
|
+
assert.equal(badMode.code, 1);
|
|
308
|
+
assert.equal(events(badMode)[0].errorCode, 'VIBE_OPTION_REQUIRES_VIBE');
|
|
309
|
+
const help = await run(['upload', '--help']);
|
|
310
|
+
assert.equal(help.code, 0);
|
|
311
|
+
assert.match(help.stdout, /--vibe/);
|
|
312
|
+
assert.match(help.stdout, /--client-key/);
|
|
313
|
+
|
|
314
|
+
let cancelledPage;
|
|
315
|
+
const cancelled = await run(
|
|
316
|
+
[...input, '--mock', '--mock-scenario', 'timeout', '--format', 'ndjson'],
|
|
317
|
+
(event, child) => {
|
|
318
|
+
if (event.status === 'waiting_authorization') {
|
|
319
|
+
cancelledPage = event.pageUrl;
|
|
320
|
+
child.kill('SIGINT');
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
assert.equal(cancelled.code, 130, cancelled.stderr);
|
|
325
|
+
assert.equal(events(cancelled).slice(-1)[0].status, 'cancelled');
|
|
326
|
+
await assert.rejects(fetch(cancelledPage));
|
|
327
|
+
// Detached success pages expire without leaving a permanent background worker.
|
|
328
|
+
await new Promise(resolve => setTimeout(resolve, 2600));
|
|
329
|
+
await assert.rejects(fetch(final.pageUrl));
|
|
330
|
+
console.log('vibe upload process smoke passed');
|
|
331
|
+
}
|
|
332
|
+
main()
|
|
333
|
+
.catch(error => {
|
|
334
|
+
console.error(error);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
})
|
|
337
|
+
.finally(() => {
|
|
338
|
+
for (const child of children) child.kill('SIGKILL');
|
|
339
|
+
fixtureServer?.close();
|
|
340
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
341
|
+
});
|
package/package.json
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttmg/cli",
|
|
3
|
-
"version": "0.4.6-vibe-beta.
|
|
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",
|
package/vibe-upload.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Vibe Binding and Upload — Skill Integration
|
|
2
|
+
|
|
3
|
+
## 中文
|
|
4
|
+
|
|
5
|
+
本次范围:本地准备 → 扫码绑定 → 上传头像 → 提交名称和头像信息 → 上传代码包 → 上传完成。不包含预览、审核或正式发布。当前创建/查询接 PPE,授权和素材上传仍为模拟。
|
|
6
|
+
|
|
7
|
+
### 博华如何接入
|
|
8
|
+
|
|
9
|
+
继续使用 `--mock`,无需本地环境配置。CLI 内置 `ppe_op_vc` 创建任务,展示授权二维码,并查询三次真实状态;之后模拟授权和头像/信息/代码包上传。二维码可用于查看端内授权页,不要求完成授权。真实请求失败仍报错,素材不会上传到 PPE。上线时由 CLI 版本去掉 PPE 路由,Skill 不负责切换环境。
|
|
10
|
+
|
|
11
|
+
Skill 只引导开发者准备游戏名称、本地头像和工程,然后调用 CLI、读取结构化状态。不调用平台接口、不保存 Token、不自行轮询、不增加名称头像表单。
|
|
12
|
+
|
|
13
|
+
Skill 固定调用:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
ttmg upload --vibe --mock --dir ./game --title "星环守卫" --icon ./avatar.png --format ndjson
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
未安装本地构建时,以 `node /绝对路径/cli/dist/index.js` 替换 `ttmg`。已调通的步骤接真实接口、未调通的步骤继续模拟,由 CLI 内部维护;Skill 不跟着 HTTP 字段和联调环境变化。
|
|
20
|
+
|
|
21
|
+
可先检查素材,不创建 task、不打开页面:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
ttmg upload --vibe --dir ./game --title "星环守卫" --icon ./avatar.png --dry-run --format json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
已有 ZIP 时,用 `--package ./game.zip` 替换 `--dir ./game`。名称和头像必须由开发者提供;演示脚本中的临时样本不作为产品默认素材。
|
|
28
|
+
|
|
29
|
+
### Skill 读取什么
|
|
30
|
+
|
|
31
|
+
输出 schema 为 `"2"`。每行 NDJSON 是同一 `operationId` 的状态快照;只有最后一行 `terminal=true`。`--format json` 仅在结束时向 stdout 输出一个对象,等待授权的本地页面地址写到 stderr。不要解析中文/英文文案判断状态。
|
|
32
|
+
|
|
33
|
+
| status | Skill 处理 |
|
|
34
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
35
|
+
| precheck / packaging / prepared | 正在检查或准备素材;dry-run 的 prepared 为终态 |
|
|
36
|
+
| creating_task | 正在准备授权绑定 |
|
|
37
|
+
| waiting_authorization | 打开 pageUrl;nextAction=scan_qr 时提示使用 TikTok 扫码,nextAction=wait 时仅等待模拟推进 |
|
|
38
|
+
| uploading | 根据 uploadStep 显示 avatar、app_info、package 的进度,不再询问授权 |
|
|
39
|
+
| uploaded | terminal=true 且 uploaded=true:本次上传完成;不是审核通过或正式发布 |
|
|
40
|
+
| failed / cancelled | 根据 errorCode、message 和 nextAction 提示处理,不自动重传 |
|
|
41
|
+
|
|
42
|
+
固定字段:`schemaVersion`、`command`、`mode`、`operationId`、`ts`、`status`、`terminal`、`nextAction`、`message`、`backend`、`simulation`、`platformWrite`、`uploadAttempted`、`uploaded`、`errorCode`、`error`。
|
|
43
|
+
|
|
44
|
+
从真实创建任务请求发起前开始 `platformWrite=true`,表示已尝试平台写入,不代表创建或上传成功。只有内部离线测试、本机 HTTP 和 dry-run 保持 `platformWrite=false`。
|
|
45
|
+
|
|
46
|
+
`backend=ppe-mock` 时 `simulation=true`、`simulationStartsAt=authorization`;创建任务前开始 `platformWrite=true`。`taskStatusRaw`、`readyForInput` 始终保留最后一次真实查询值,模拟状态单独放在 `simulatedTaskStatusRaw`、`simulatedReadyForInput`。终态仍为 `uploaded`,但只表示模拟上传完成,不代表真实授权或上传成功;Skill 必须保留模拟提示。
|
|
47
|
+
|
|
48
|
+
按阶段补充:`taskId`(字符串,禁止转成 JS Number)、`pollIntervalSeconds`、`readyForInput`、`taskStatusRaw`(仅诊断)、`uploadStep`、`loadedBytes`、`totalBytes`、`pageUrl`、`qrPurpose=authorization_binding`、素材大小和包 SHA256。代码包字节进度达到 100% 不代表成功,必须等待最终上传回执。
|
|
49
|
+
|
|
50
|
+
`nextAction` 取值:`wait`、`scan_qr`、`fix_input`、`restart_authorization`、`check_task`、`none`。已开始上传后发生失败/超时,先检查 task,不能直接再次创建并重传。若上传已完成但页面失败,仍保留 `uploaded=true`,同时返回错误;Skill 提示上传已完成但展示失败。
|
|
51
|
+
|
|
52
|
+
退出码:成功/dry-run 为 0,失败为 1,Ctrl+C 为 130。进程或宿主意外终止导致没有终态时,不得认定上传成功。模拟必须显示为模拟:即使 `uploaded=true`,`simulation=true` 时也只表示模拟步骤完成。纯 Mock 的 `platformWrite=false`;混合联调创建过真实任务时为 true,但都不代表真实上传。
|
|
53
|
+
|
|
54
|
+
### Mock 覆盖与演示
|
|
55
|
+
|
|
56
|
+
`npm run demo:vibe` 会构建并用临时工程和头像运行 CLI;真实检查、ZIP 打包和 PPE 创建/查询,模拟授权与上传。需要后端可访问,无需完成手机授权。结果页在进程结束后继续保留至到期。
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --mock --format ndjson
|
|
60
|
+
ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --mock --mock-scenario denied --format ndjson
|
|
61
|
+
ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --mock --mock-scenario timeout --timeout 5 --format ndjson
|
|
62
|
+
ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --mock --mock-scenario upload-failed --format ndjson
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| 场景 | 结果 |
|
|
66
|
+
| ----------------------------------------- | -------------------------------------------------------------------- |
|
|
67
|
+
| success | 第三次轮询允许上传,头像/信息/代码包依次完成 |
|
|
68
|
+
| denied | 授权取消,VIBE_AUTH_DENIED,不上传 |
|
|
69
|
+
| expired | 授权过期,VIBE_AUTH_EXPIRED,不上传 |
|
|
70
|
+
| timeout | 持续等待,直到整体超时 VIBE_TIMEOUT |
|
|
71
|
+
| icon-failed / info-failed / upload-failed | 在对应上传步骤失败,uploadStep 指明位置 |
|
|
72
|
+
| processing | 包已上传,后端状态仍为 PROCESSING;CLI 返回 uploaded,不等 SUCCEEDED |
|
|
73
|
+
|
|
74
|
+
Mock 的 denied 是交互测试场景,真实端内拒绝授权如何映射错误仍待后端确认。Mock 不请求任何远端,不更改平台登录态,不创建真实应用。
|
|
75
|
+
|
|
76
|
+
轮询间隔使用创建任务返回的 `poll_interval_seconds`,第三次真实查询后模拟授权。`--poll-interval` 仅用于覆盖测试间隔。`--timeout` 是整体超时,默认 300 秒。不自动重试写接口、不实现跨进程续传。
|
|
77
|
+
|
|
78
|
+
### 页面与素材
|
|
79
|
+
|
|
80
|
+
同一电脑本地页依次显示授权绑定码、上传进度、上传完成;失败时清除授权码并显示失败原因。没有完成后的预览码、发布按钮或名称头像表单。结构化输出不自动打开浏览器,Skill 用 pageUrl 打开 Codex 右侧预览或浏览器;文本输出默认打开,`--no-open` 可关闭。
|
|
81
|
+
|
|
82
|
+
页面只监听 127.0.0.1,二维码才由手机扫描,手机不访问 pageUrl。默认 15 分钟后关闭,内容驻留内存;测试可设置 `TTMG_VIBE_PAGE_TTL_MS`(1—900000)。取消进程会关闭页面;成功与可显示的失败页面保留至到期。不要分享本地页面地址。
|
|
83
|
+
|
|
84
|
+
真实执行的部分包括本地项目检查和 ZIP 打包。文件直接输入只检查非空、大小与文件头,不代表完整图像解码、ZIP 内容检查或平台审核。现有 10 MiB 头像、100 MiB ZIP 为本地保护阈值,不是后端已确认限制。图片识别 PNG/JPEG/WebP。
|
|
85
|
+
|
|
86
|
+
准备完成后固定本次素材字节,授权期间不会重新读取。打包排除 .ttmg、.env\*、PEM 和 KEY 等文件,但不保证识别所有业务秘密。
|
|
87
|
+
|
|
88
|
+
CLI 在工程目录的 `.ttmg/vibe-project.json` 保存并复用非敏感 source_project_id;使用 --package 时保存在 ZIP 所在目录。不要将多个独立游戏的 ZIP 放在同一身份目录。dry-run 不创建此文件。每次运行使用新的 task_idempotency_id,当前不支持中断续传。凭证只驻留进程内存,不进入事件、文件、日志或错误信息。
|
|
89
|
+
|
|
90
|
+
### HTTP 适配边界
|
|
91
|
+
|
|
92
|
+
Mock 与本地 HTTP 共用请求构造、业务响应校验及状态映射,基于 2026-09-12 的 Minis Import BFF IDL:
|
|
93
|
+
|
|
94
|
+
| 方法 / 接口(统一前缀 /bff/app_info/minis_import/) | 请求 | 响应 |
|
|
95
|
+
| --------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
96
|
+
| POST create_minis_import_task_for_agent/ | source_type=1、source_project_id、task_idempotency_id、minis_type=MINI_GAME | task_id、import_token、landing_url、poll_interval_seconds |
|
|
97
|
+
| GET get_minis_import_task_status_for_agent/ | Query:task_id、import_token | status、ready_for_input |
|
|
98
|
+
| POST upload_minis_import_app_icon_for_agent/ | Multipart:task_id、import_token、file | icon_uri |
|
|
99
|
+
| POST submit_minis_import_info_for_agent/ | JSON:task_id、import_token、app_info.app_name、app_info.icon_uri | error_info |
|
|
100
|
+
| POST upload_minis_import_package_for_agent/ | Multipart:task_id、import_token、asset_type=2、file | package_uri、status |
|
|
101
|
+
|
|
102
|
+
所有响应必须业务 `error_info.code=0`。最后一步还要求非空 package_uri 和合法非失败状态;完成后不再轮询。HTTP 200、进度100% 都不能单独作为成功判定。19位 task_id 在 CLI 内保留字符串,提交 i64 JSON 时无损编码。
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
TTMG_VIBE_BACKEND=local-http TTMG_VIBE_API=http://127.0.0.1:3100 ttmg upload --vibe --package ./game.zip --title "测试游戏" --icon ./avatar.png --format ndjson
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
维护者可使用 `--vibe-api http://127.0.0.1:3100` 验证本机 HTTP 合同,只接受回环 IP,不跟随重定向、不读取 Cookie;本地服务需实现上表协议。博华试用无需此选项,直接 `--mock`,PPE 路由由 CLI 内置。正式环境未开放。
|
|
109
|
+
|
|
110
|
+
**v1 → v2 兼容性变化**:旧 UUID 换 Token 的三接口协议废弃,`--vibe-authorize-url` 返回明确错误;授权地址来自 landing_url。不再输出 preview_ready、game_preview、uploadId、reviewStatus。Skill 必须校验 schemaVersion=2,并按新终态处理。普通不带 --vibe 的 upload 不变。
|
|
111
|
+
|
|
112
|
+
仍待真实联调确认:头像/代码包规则、Token 有效期与重试规则、真实拒绝授权映射、端内 PPE Cookie 问题。后端接口调整留在适配层处理;若用户动作或成功语义改变,应协商升级 CLI schema,不能承诺 Skill 永远无需调整。
|
|
113
|
+
|
|
114
|
+
### 验证
|
|
115
|
+
|
|
116
|
+
`npm run test:vibe`:Mock、HTTP 合同、请求顺序、i64、失败、取消、输入和页面测试。
|
|
117
|
+
|
|
118
|
+
`npm run test:vibe-process`:构建后的真实命令、JSON/NDJSON、环境配置、页面生命周期与退出码。完整 CLI 回归仍为 `npm run test:agent-contract`。测试通过不等于真实绑定上传已验证。
|
|
119
|
+
|
|
120
|
+
## English
|
|
121
|
+
|
|
122
|
+
### Skill entry
|
|
123
|
+
|
|
124
|
+
Scope: local preparation → authorization binding → avatar upload → app-info submission → package upload → **upload complete**. No preview, review or production release. Mock and loopback HTTP cover this contract. PPE additionally supports real task creation, polling and the authorization QR only; material upload and production access are not enabled.
|
|
125
|
+
|
|
126
|
+
Keep the existing --mock command; no environment setup is required:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
ttmg upload --vibe --mock --dir ./game --title "My game" --icon ./avatar.png --format ndjson
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use `--package ./game.zip` instead of --dir for a prepared ZIP. Use `--dry-run --format json` for local preparation only. When running a source build, replace ttmg with `node /absolute/path/cli/dist/index.js`. Explicit --mock or --vibe-api overrides environment selection; an unconfigured backend fails instead of silently simulating.
|
|
133
|
+
|
|
134
|
+
The Skill gathers the title and user-selected local icon in conversation. It does not call backend APIs, store credentials, poll services, or introduce a metadata form. The CLI internally connects working steps to PPE and simulates the remaining steps. Removing PPE at production launch is a CLI release change, not a Skill configuration step.
|
|
135
|
+
|
|
136
|
+
### Output and decisions
|
|
137
|
+
|
|
138
|
+
Schema **2** provides NDJSON snapshots under one operationId, with exactly one final `terminal=true` event. JSON mode emits one final object on stdout and the authorization page address on stderr. Structured modes do not open the browser automatically; open pageUrl in the AI workspace preview or browser. Text mode opens it unless --no-open.
|
|
139
|
+
|
|
140
|
+
| status | Skill action |
|
|
141
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
142
|
+
| precheck / packaging / prepared | Wait for input preparation; prepared is terminal only for dry-run |
|
|
143
|
+
| creating_task | Preparing binding |
|
|
144
|
+
| waiting_authorization | Open pageUrl; scan_qr requests a phone scan, wait means simulation advances automatically |
|
|
145
|
+
| uploading | Follow uploadStep: avatar, app_info, package |
|
|
146
|
+
| uploaded | terminal=true and uploaded=true mean upload complete, not release |
|
|
147
|
+
| failed / cancelled | Read errorCode, message and nextAction; do not blindly repeat uploads |
|
|
148
|
+
|
|
149
|
+
Stable fields: schemaVersion, command, mode, operationId, ts, status, terminal, nextAction, message, backend, simulation, platformWrite, uploadAttempted, uploaded, errorCode, error. Optional evidence: taskId (string), taskStatusRaw (diagnostics only), pollIntervalSeconds, readyForInput, uploadStep, loadedBytes, totalBytes, pageUrl, qrPurpose=authorization_binding, sizes and packageSha256.
|
|
150
|
+
|
|
151
|
+
platformWrite becomes true immediately before attempting real task creation, not as proof of successful creation or upload. Only injected offline tests, loopback HTTP and dry-run keep platformWrite=false.
|
|
152
|
+
|
|
153
|
+
For backend=ppe-mock, simulation=true and simulationStartsAt=authorization. platformWrite becomes true before real task creation. taskStatusRaw and readyForInput retain the last real observation; simulatedTaskStatusRaw and simulatedReadyForInput describe the local demo separately. The uploaded terminal state means simulated completion only; the Skill must retain the simulation label.
|
|
154
|
+
|
|
155
|
+
nextAction is wait, scan_qr, fix_input, restart_authorization, check_task or none. If upload was attempted, verify the task before retrying. If a result page fails after confirmed upload, uploaded remains true alongside the error. Progress at 100% is not a receipt. Missing terminal output after a killed process is not success. Exit codes: 0 success/dry-run, 1 failure, 130 cancellation.
|
|
156
|
+
|
|
157
|
+
Pure Mock reports simulation=true and platformWrite=false. Hybrid PPE + Mock reports platformWrite=true once real task creation is attempted, but uploaded=true still means simulated completion only. No backend exposes credentials or raw HTTP errors. Backend integration must preserve this public contract; changes to user actions or completion semantics require an explicit schema migration.
|
|
158
|
+
|
|
159
|
+
### Mock, UI and inputs
|
|
160
|
+
|
|
161
|
+
Run `npm run demo:vibe` for the CLI with temporary game/icon fixtures. Local checking, ZIP creation and PPE task creation/polling are real; authorization and uploads are simulated. Backend access is required, phone authorization is not. Scenarios: success, denied, expired, timeout, upload-failed, processing, icon-failed, info-failed. Use --mock-scenario and --timeout to test failures. Denial is a mock interaction scenario, not a confirmed backend error mapping.
|
|
162
|
+
|
|
163
|
+
The same local page changes from binding QR to upload progress and completion, without preview QR or metadata form. Failures clear the binding QR. It listens only on 127.0.0.1, retains content in memory, and expires after 15 minutes; the phone scans the QR rather than visiting pageUrl. Success/failure pages remain after CLI completion; cancellation closes the page. Test TTL: TTMG_VIBE_PAGE_TTL_MS=1..900000.
|
|
164
|
+
|
|
165
|
+
The trial follows poll_interval_seconds and simulates input readiness after the third real query; --poll-interval is a testing override. The overall --timeout defaults to 300 seconds. No automatic write retries or cross-process resume.
|
|
166
|
+
|
|
167
|
+
The supplied title/icon/package bytes are fixed before authorization. Direct files are checked for size and signatures, not decoded or fully inspected. PNG/JPEG/WebP and 10 MiB icon / 100 MiB ZIP limits are provisional local guards, not confirmed platform rules. Project packaging excludes .ttmg, .env\*, PEM and KEY files, not every possible secret.
|
|
168
|
+
|
|
169
|
+
A non-secret source_project_id is persisted in .ttmg/vibe-project.json under the project, or the ZIP parent for --package. Keep distinct games in separate directories. Dry-run does not create identity metadata. Each invocation has a new task_idempotency_id; credentials are held only in memory.
|
|
170
|
+
|
|
171
|
+
### Adapter and verification
|
|
172
|
+
|
|
173
|
+
The five endpoint contracts are listed in the table above. Mock and loopback HTTP share request construction and response validation. The HTTP adapter implements POST create → GET status with task_id/import_token query parameters → Multipart icon → JSON app_info → Multipart package with asset_type=2 (Native). Multipart Content-Type includes its generated boundary; JSON i64 task IDs are encoded losslessly.
|
|
174
|
+
|
|
175
|
+
A business-success response requires error_info.code=0. Package completion additionally requires package_uri and a valid non-failure status; no polling until SUCCEEDED. No platform Cookies or redirects are used. Configure TTMG_VIBE_BACKEND=local-http plus TTMG_VIBE_API=http://127.0.0.1:3100, or pass --vibe-api explicitly; this option rejects remote hosts.
|
|
176
|
+
|
|
177
|
+
For internal PPE integration, set TTMG_VIBE_BACKEND=ppe and TTMG_VIBE_PPE=ppe_op_vc without changing the Skill command. This uses real creation/polling on developers.tiktok.com and encodes the QR as aweme://webview?url=<encodeURIComponent(landing_url)>. The phone must use the same lane. OAuth is not bypassed or simulated. Complete upload inputs are not integrated yet: after authorization, VIBE_REAL_UPLOAD_NOT_READY stops the flow before any material upload. Production is not enabled.
|
|
178
|
+
|
|
179
|
+
The normal --mock trial internally uses ppe_op_vc without environment variables. Task creation, QR and three polls are real; authorization, icon, app-info and package uploads are simulated with separate in-memory credentials. This changes --mock from an offline run into a PPE integration trial: platformWrite becomes true when task creation is attempted, while simulation remains true. No real material-upload endpoint is called, and real request failures still fail. --mock-scenario controls only simulated steps. Offline unit/process tests use injected transports, not a separate developer configuration.
|
|
180
|
+
|
|
181
|
+
Schema-v1 UUID endpoints, --vibe-authorize-url, preview_ready, game_preview, uploadId and reviewStatus are retired. The deprecated option returns an explicit error. Ordinary upload without --vibe is unchanged.
|
|
182
|
+
|
|
183
|
+
Unconfirmed: platform file limits, token expiry/retry rules, actual denial mapping and in-client PPE Cookie behavior. Run test:vibe, built test:vibe-process, and test:agent-contract. Local contract tests do not establish real platform or device results.
|