@playdrop/playdrop-cli 0.12.2 → 0.12.4

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.
@@ -4,6 +4,7 @@ exports.createDevError = createDevError;
4
4
  exports.isNetworkError = isNetworkError;
5
5
  exports.fetchDevUser = fetchDevUser;
6
6
  exports.fetchDevUsername = fetchDevUsername;
7
+ exports.resolveDevCreatorUsername = resolveDevCreatorUsername;
7
8
  exports.assertAppRegistered = assertAppRegistered;
8
9
  exports.resolveDevTarget = resolveDevTarget;
9
10
  exports.findProjectInfo = findProjectInfo;
@@ -54,6 +55,14 @@ async function fetchDevUsername(client) {
54
55
  const user = await fetchDevUser(client);
55
56
  return user.username;
56
57
  }
58
+ async function resolveDevCreatorUsername(input) {
59
+ const workspaceOwnerUsername = input.workspaceOwnerUsername?.trim() ?? '';
60
+ if (workspaceOwnerUsername) {
61
+ return workspaceOwnerUsername;
62
+ }
63
+ const accountUsername = input.accountUsername?.trim() ?? '';
64
+ return accountUsername || await fetchDevUsername(input.client);
65
+ }
57
66
  async function assertAppRegistered(client, creatorUsername, appName) {
58
67
  const response = await client.fetchAppBySlug(creatorUsername, appName);
59
68
  if (!response?.app) {
@@ -1,3 +1,4 @@
1
+ import type { Readable } from 'stream';
1
2
  import { resetOpenBrowserForTests, setOpenBrowserForTests } from '../browser';
2
3
  export { resetOpenBrowserForTests, setOpenBrowserForTests, };
3
4
  type LoginOptions = {
@@ -5,6 +6,17 @@ type LoginOptions = {
5
6
  password?: string;
6
7
  key?: string;
7
8
  handoffToken?: string;
9
+ handoffTokenStdin?: boolean;
8
10
  json?: boolean;
9
11
  };
10
- export declare function login(env: string, options?: LoginOptions): Promise<void>;
12
+ type LoginRuntime = {
13
+ stdin?: Readable;
14
+ };
15
+ export declare const HANDOFF_TOKEN_STDIN_MAX_BYTES: number;
16
+ type HandoffTokenStdinErrorCode = 'empty' | 'too_large' | 'read_failed';
17
+ export declare class HandoffTokenStdinError extends Error {
18
+ readonly code: HandoffTokenStdinErrorCode;
19
+ constructor(code: HandoffTokenStdinErrorCode);
20
+ }
21
+ export declare function readHandoffTokenFromStdin(input?: Readable): Promise<string>;
22
+ export declare function login(env: string, options?: LoginOptions, runtime?: LoginRuntime): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setOpenBrowserForTests = exports.resetOpenBrowserForTests = void 0;
3
+ exports.HandoffTokenStdinError = exports.HANDOFF_TOKEN_STDIN_MAX_BYTES = exports.setOpenBrowserForTests = exports.resetOpenBrowserForTests = void 0;
4
+ exports.readHandoffTokenFromStdin = readHandoffTokenFromStdin;
4
5
  exports.login = login;
5
6
  const types_1 = require("@playdrop/types");
6
7
  const config_1 = require("../config");
@@ -12,6 +13,40 @@ Object.defineProperty(exports, "resetOpenBrowserForTests", { enumerable: true, g
12
13
  Object.defineProperty(exports, "setOpenBrowserForTests", { enumerable: true, get: function () { return browser_1.setOpenBrowserForTests; } });
13
14
  const output_1 = require("../output");
14
15
  const messages_1 = require("../messages");
16
+ exports.HANDOFF_TOKEN_STDIN_MAX_BYTES = 8 * 1024;
17
+ class HandoffTokenStdinError extends Error {
18
+ constructor(code) {
19
+ super(`handoff_token_stdin_${code}`);
20
+ this.name = 'HandoffTokenStdinError';
21
+ this.code = code;
22
+ }
23
+ }
24
+ exports.HandoffTokenStdinError = HandoffTokenStdinError;
25
+ async function readHandoffTokenFromStdin(input = process.stdin) {
26
+ const chunks = [];
27
+ let byteCount = 0;
28
+ try {
29
+ for await (const chunk of input) {
30
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
31
+ byteCount += buffer.byteLength;
32
+ if (byteCount > exports.HANDOFF_TOKEN_STDIN_MAX_BYTES) {
33
+ throw new HandoffTokenStdinError('too_large');
34
+ }
35
+ chunks.push(buffer);
36
+ }
37
+ }
38
+ catch (error) {
39
+ if (error instanceof HandoffTokenStdinError) {
40
+ throw error;
41
+ }
42
+ throw new HandoffTokenStdinError('read_failed');
43
+ }
44
+ const token = Buffer.concat(chunks, byteCount).toString('utf8').trim();
45
+ if (!token) {
46
+ throw new HandoffTokenStdinError('empty');
47
+ }
48
+ return token;
49
+ }
15
50
  function sleep(ms) {
16
51
  return new Promise((resolve) => setTimeout(resolve, ms));
17
52
  }
@@ -193,7 +228,7 @@ async function loginInBrowser(env, baseUrl, options = {}) {
193
228
  ], { command: 'login' });
194
229
  process.exitCode = 1;
195
230
  }
196
- async function login(env, options = {}) {
231
+ async function login(env, options = {}, runtime = {}) {
197
232
  const envConfig = (0, environment_1.resolveEnvironmentConfig)(env);
198
233
  if (!envConfig) {
199
234
  const choices = (0, environment_1.formatEnvironmentList)();
@@ -207,12 +242,16 @@ async function login(env, options = {}) {
207
242
  const username = options.username?.trim();
208
243
  const password = options.password;
209
244
  const apiKey = options.key?.trim();
210
- const handoffToken = options.handoffToken?.trim();
245
+ const argvHandoffToken = options.handoffToken?.trim();
246
+ const hasApiKeyArgument = options.key !== undefined;
247
+ const hasHandoffTokenArgument = options.handoffToken !== undefined;
248
+ const readsHandoffTokenFromStdin = options.handoffTokenStdin === true;
211
249
  const hasDirectCredentials = Boolean(username || password);
212
250
  const explicitLoginMethods = [
213
- apiKey ? 'key' : null,
251
+ hasApiKeyArgument ? 'key' : null,
214
252
  hasDirectCredentials ? 'credentials' : null,
215
- handoffToken ? 'handoff' : null,
253
+ hasHandoffTokenArgument ? 'handoff' : null,
254
+ readsHandoffTokenFromStdin ? 'handoff-stdin' : null,
216
255
  ].filter(Boolean);
217
256
  if (explicitLoginMethods.length > 1) {
218
257
  (0, messages_1.printErrorWithHelp)('Choose exactly one login method.', [
@@ -220,6 +259,7 @@ async function login(env, options = {}) {
220
259
  'Use "--username" with "--password" for direct login.',
221
260
  'Use "--key" by itself for API key login.',
222
261
  'Use "--handoff-token" by itself for native app handoff login.',
262
+ 'Use "--handoff-token-stdin" by itself to read a native app handoff token from stdin.',
223
263
  ], { command: 'login' });
224
264
  process.exitCode = 1;
225
265
  return;
@@ -232,6 +272,33 @@ async function login(env, options = {}) {
232
272
  process.exitCode = 1;
233
273
  return;
234
274
  }
275
+ if (hasApiKeyArgument && !apiKey) {
276
+ (0, messages_1.printErrorWithHelp)('The API key cannot be empty.', ['Provide a non-empty value to "--key".'], { command: 'login' });
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+ if (hasHandoffTokenArgument && !argvHandoffToken) {
281
+ (0, messages_1.printErrorWithHelp)('The native app handoff token cannot be empty.', ['Request a fresh handoff token from the PlayDrop app and retry.'], { command: 'login' });
282
+ process.exitCode = 1;
283
+ return;
284
+ }
285
+ let handoffToken = argvHandoffToken;
286
+ if (readsHandoffTokenFromStdin) {
287
+ try {
288
+ handoffToken = await readHandoffTokenFromStdin(runtime.stdin ?? process.stdin);
289
+ }
290
+ catch (error) {
291
+ const inputError = error instanceof HandoffTokenStdinError ? error : null;
292
+ const message = inputError?.code === 'too_large'
293
+ ? `Native app handoff input exceeded the ${exports.HANDOFF_TOKEN_STDIN_MAX_BYTES}-byte limit.`
294
+ : inputError?.code === 'empty'
295
+ ? 'Native app handoff input was empty.'
296
+ : 'Could not read native app handoff input from stdin.';
297
+ (0, messages_1.printErrorWithHelp)(message, ['Request a fresh handoff token from the PlayDrop app and retry.'], { command: 'login' });
298
+ process.exitCode = 1;
299
+ return;
300
+ }
301
+ }
235
302
  try {
236
303
  if (apiKey) {
237
304
  await loginWithKey(env, envConfig.apiBase, apiKey, { json: options.json });
@@ -0,0 +1,4 @@
1
+ import type { AppResponse } from '@playdrop/types';
2
+ import type { TaskCaptureSession } from '../appUrls';
3
+ import type { ResolvedWorkspaceAuthConfig } from '../workspaceAuth';
4
+ export declare function resolveTaskCaptureSession(workspaceAuth: ResolvedWorkspaceAuthConfig | null, app: AppResponse): TaskCaptureSession | null;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTaskCaptureSession = resolveTaskCaptureSession;
4
+ function resolveTaskCaptureSession(workspaceAuth, app) {
5
+ const taskId = workspaceAuth?.config.taskId;
6
+ const taskToken = workspaceAuth?.config.taskToken?.trim() ?? '';
7
+ if (taskId === undefined && !taskToken) {
8
+ return null;
9
+ }
10
+ if (!Number.isInteger(taskId) || Number(taskId) <= 0 || !taskToken) {
11
+ throw new Error('worker_capture_session_incomplete');
12
+ }
13
+ if (!Number.isInteger(app.id) || Number(app.id) <= 0) {
14
+ throw new Error('registered_app_id_missing');
15
+ }
16
+ return {
17
+ appId: Number(app.id),
18
+ taskId: Number(taskId),
19
+ taskToken,
20
+ };
21
+ }
@@ -9,6 +9,7 @@ export declare function upload(pathOrName: string, options?: UploadCommandOption
9
9
  export type WorkerAppPublishInput = {
10
10
  client: ApiClient;
11
11
  taskId: number;
12
+ taskToken: string;
12
13
  kind: 'NEW_GAME' | 'REMIX_GAME' | 'GAME_UPDATE';
13
14
  executionTarget?: AgentExecutionTarget;
14
15
  expectedAppName?: string | null;
@@ -1225,6 +1225,11 @@ const WORKER_SUPERVISOR_SOURCE_EXCLUDES = [
1225
1225
  '.playdrop-task-events/',
1226
1226
  'bin/playdrop',
1227
1227
  ];
1228
+ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
1229
+ ...WORKER_SUPERVISOR_SOURCE_EXCLUDES,
1230
+ 'assets/marketing/',
1231
+ 'listing/',
1232
+ ];
1228
1233
  const MIN_HERO_PORTRAIT_WIDTH = 512;
1229
1234
  const MIN_HERO_PORTRAIT_HEIGHT = 768;
1230
1235
  const MIN_HERO_LANDSCAPE_WIDTH = 768;
@@ -2273,7 +2278,7 @@ async function publishWorkerAppProject(input) {
2273
2278
  ...appTask,
2274
2279
  versionVisibility: 'PRIVATE',
2275
2280
  sourceArchiveExcludeRelativeFiles: WORKER_SUPERVISOR_SOURCE_EXCLUDES,
2276
- bundleArchiveExcludeRelativeFiles: WORKER_SUPERVISOR_SOURCE_EXCLUDES,
2281
+ bundleArchiveExcludeRelativeFiles: WORKER_SUPERVISOR_BUNDLE_EXCLUDES,
2277
2282
  };
2278
2283
  if (input.kind === 'REMIX_GAME') {
2279
2284
  const expectedRemixRef = input.remixSourceRef?.trim() ?? '';
@@ -2331,6 +2336,15 @@ async function publishWorkerAppProject(input) {
2331
2336
  if (typeof app.id !== 'number' || !Number.isInteger(app.id) || app.id <= 0) {
2332
2337
  throw new Error(`App "${task.name}" registration did not return an app id.`);
2333
2338
  }
2339
+ const taskToken = input.taskToken.trim();
2340
+ if (!taskToken) {
2341
+ throw new Error('agent_task_token_missing');
2342
+ }
2343
+ const captureSession = {
2344
+ appId: app.id,
2345
+ taskId: input.taskId,
2346
+ taskToken,
2347
+ };
2334
2348
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2335
2349
  assertNewGameListingScreenshots(task);
2336
2350
  assertTaskPlaytestEvidenceManifest(task);
@@ -2346,6 +2360,7 @@ async function publishWorkerAppProject(input) {
2346
2360
  ensureRegisteredAppShell: true,
2347
2361
  agentTaskId: input.taskId,
2348
2362
  mediaCaptureRequired: input.executionTarget === 'FIRST_PARTY',
2363
+ captureSession,
2349
2364
  });
2350
2365
  if (!uploadResult.versionCreated || !uploadResult.version) {
2351
2366
  throw new Error(`App "${task.name}" upload did not return a created version.`);
@@ -187,11 +187,13 @@ export declare function classifyWorkerHeartbeatError(error: unknown): 'session_e
187
187
  export declare function assertWorkerClaimErrorRetryable(error: unknown): void;
188
188
  export declare function resolvePlaydropAssetRequirementFromText(value: unknown): WorkerPlaydropAssetRequirement | null;
189
189
  export declare function loadWorkerEnvFile(envName: string | undefined, cwd?: string): string | null;
190
- export declare function buildWorkerCapabilities(input: string | {
190
+ export declare function buildWorkerCapabilities(input: {
191
191
  codexVersion?: string | null;
192
192
  codexAuthenticated?: boolean | null;
193
+ codexModels?: string[] | null;
193
194
  claudeVersion?: string | null;
194
195
  claudeAuthenticated?: boolean | null;
196
+ claudeModels?: string[] | null;
195
197
  cursorVersion?: string | null;
196
198
  cursorAuthenticated?: boolean | null;
197
199
  npmVersion?: string | null;
@@ -233,6 +235,8 @@ export declare function createLocalPlaydropShim(input: {
233
235
  attempt: number;
234
236
  devPort: number;
235
237
  }): Promise<string>;
238
+ export declare function parseCodexModelCatalog(output: string): string[];
239
+ export declare function parseClaudeModelCatalog(output: string): string[];
236
240
  export declare function buildWorkerSetupActions(input: {
237
241
  degradedReasons: string[];
238
242
  playwright?: {