@playdrop/playdrop-cli 0.10.20 → 0.11.0

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.
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.10.20",
2
+ "version": "0.11.0",
3
3
  "build": 1,
4
4
  "runtimeSdkVersion": "0.10.0",
5
5
  "clients": {
6
6
  "all": {
7
7
  "minimumVersion": "0.7.4"
8
+ },
9
+ "windows-playdrop": {
10
+ "minimumVersion": "0.9.6",
11
+ "minimumBuild": 1
8
12
  }
9
13
  }
10
14
  }
package/dist/appUrls.d.ts CHANGED
@@ -7,6 +7,9 @@ export type AppUrlInput = {
7
7
  export declare function normalizePlaydropWebBase(webBase?: string | null): string;
8
8
  export declare function getAppTypeSlug(type: AppType | string | null | undefined): string;
9
9
  export declare function buildPlatformPlayUrl(webBase: string | null | undefined, input: AppUrlInput): string;
10
+ export declare function buildPlatformPrivateDraftPlayUrl(webBase: string | null | undefined, input: AppUrlInput & {
11
+ version: string;
12
+ }): string;
10
13
  export declare function buildPlatformDevUrl(webBase: string | null | undefined, input: AppUrlInput & {
11
14
  devAuth?: 'prompt' | 'anonymous' | 'viewer' | 'player';
12
15
  player?: string | null;
package/dist/appUrls.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizePlaydropWebBase = normalizePlaydropWebBase;
4
4
  exports.getAppTypeSlug = getAppTypeSlug;
5
5
  exports.buildPlatformPlayUrl = buildPlatformPlayUrl;
6
+ exports.buildPlatformPrivateDraftPlayUrl = buildPlatformPrivateDraftPlayUrl;
6
7
  exports.buildPlatformDevUrl = buildPlatformDevUrl;
7
8
  exports.buildUploadLaunchCheckUrl = buildUploadLaunchCheckUrl;
8
9
  const DEFAULT_WEB_BASE = 'https://www.playdrop.ai';
@@ -33,6 +34,17 @@ function buildPlatformPlayUrl(webBase, input) {
33
34
  const appName = encodeURIComponent(input.appName);
34
35
  return `${base}/creators/${creator}/apps/${typeSlug}/${appName}/play`;
35
36
  }
37
+ function buildPlatformPrivateDraftPlayUrl(webBase, input) {
38
+ const base = normalizePlaydropWebBase(webBase);
39
+ const creator = encodeURIComponent(input.creatorUsername);
40
+ const typeSlug = getAppTypeSlug(input.appType);
41
+ const appName = encodeURIComponent(input.appName);
42
+ const version = encodeURIComponent(input.version.trim());
43
+ if (!version) {
44
+ throw new Error('invalid_private_draft_play_version');
45
+ }
46
+ return `${base}/_auth/creators/${creator}/apps/${typeSlug}/${appName}/play?v=${version}`;
47
+ }
36
48
  function buildPlatformDevUrl(webBase, input) {
37
49
  const base = normalizePlaydropWebBase(webBase);
38
50
  const creator = encodeURIComponent(input.creatorUsername);
@@ -33,9 +33,10 @@ const projectBuildCache = new Map();
33
33
  const typescriptSourceCache = new Map();
34
34
  async function runNpmScript(projectDir, script) {
35
35
  await (0, promises_1.mkdir)(projectDir, { recursive: true });
36
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
36
+ const npmCommand = process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : 'npm';
37
+ const npmArgs = process.platform === 'win32' ? ['/d', '/s', '/c', `npm run ${script}`] : ['run', script];
37
38
  await new Promise((resolveRun, rejectRun) => {
38
- const child = (0, node_child_process_1.spawn)(npmCommand, ['run', script], {
39
+ const child = (0, node_child_process_1.spawn)(npmCommand, npmArgs, {
39
40
  cwd: projectDir,
40
41
  stdio: 'inherit',
41
42
  env: { ...process.env },
@@ -8,7 +8,7 @@ export type HostedLaunchCheckResult = {
8
8
  checkedAt: string;
9
9
  artifactFingerprint: string | null;
10
10
  };
11
- export declare function formatHostedLaunchCheckFailure(taskName: string, result: HostedLaunchCheckResult, scope?: 'local' | 'staged'): string;
11
+ export declare function formatHostedLaunchCheckFailure(taskName: string, result: HostedLaunchCheckResult, scope?: 'local' | 'staged' | 'final'): string;
12
12
  export declare function runLocalHostedLaunchCheck(input: {
13
13
  client: ApiClient;
14
14
  apiBase: string;
@@ -30,3 +30,10 @@ export declare function runUploadedHostedLaunchCheck(input: {
30
30
  token?: string | null;
31
31
  currentUser?: UserResponse | null;
32
32
  }): Promise<HostedLaunchCheckResult>;
33
+ export declare function runPublishedHostedPlayCheck(input: {
34
+ targetUrl: string;
35
+ surfaceTargets?: unknown;
36
+ timeoutMs?: number;
37
+ token?: string | null;
38
+ currentUser?: UserResponse | null;
39
+ }): Promise<HostedLaunchCheckResult>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatHostedLaunchCheckFailure = formatHostedLaunchCheckFailure;
4
4
  exports.runLocalHostedLaunchCheck = runLocalHostedLaunchCheck;
5
5
  exports.runUploadedHostedLaunchCheck = runUploadedHostedLaunchCheck;
6
+ exports.runPublishedHostedPlayCheck = runPublishedHostedPlayCheck;
6
7
  const appUrls_1 = require("../appUrls");
7
8
  const captureRuntime_1 = require("../captureRuntime");
8
9
  const devShared_1 = require("../commands/devShared");
@@ -13,13 +14,67 @@ const devRuntimeAssets_1 = require("../commands/devRuntimeAssets");
13
14
  const registration_1 = require("./registration");
14
15
  const DEFAULT_HOSTED_LAUNCH_TIMEOUT_MS = 15000;
15
16
  const POST_READY_SETTLE_MS = 750;
16
- const CAPTURE_SURFACE = 'DESKTOP';
17
+ const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
18
+ const LAUNCH_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
19
+ const LAUNCH_CHECK_SURFACE_CONTEXT_OPTIONS = {
20
+ DESKTOP: {
21
+ viewport: { width: 1280, height: 720 },
22
+ deviceScaleFactor: 1,
23
+ isMobile: false,
24
+ hasTouch: false,
25
+ },
26
+ MOBILE_LANDSCAPE: {
27
+ viewport: { width: 896, height: 414 },
28
+ deviceScaleFactor: 2,
29
+ isMobile: true,
30
+ hasTouch: true,
31
+ userAgent: MOBILE_USER_AGENT,
32
+ },
33
+ MOBILE_PORTRAIT: {
34
+ viewport: { width: 414, height: 896 },
35
+ deviceScaleFactor: 2,
36
+ isMobile: true,
37
+ hasTouch: true,
38
+ userAgent: MOBILE_USER_AGENT,
39
+ },
40
+ };
41
+ function normalizeLaunchCheckSurface(value) {
42
+ if (typeof value !== 'string') {
43
+ return null;
44
+ }
45
+ const normalized = value.trim().toUpperCase();
46
+ return LAUNCH_CHECK_SURFACE_ORDER.includes(normalized)
47
+ ? normalized
48
+ : null;
49
+ }
50
+ function normalizeLaunchCheckSurfaceTargets(value) {
51
+ if (!Array.isArray(value)) {
52
+ return [];
53
+ }
54
+ return Array.from(new Set(value.map(normalizeLaunchCheckSurface).filter((entry) => Boolean(entry))));
55
+ }
56
+ function resolveLaunchCheckSurface(surfaceTargets) {
57
+ const targets = normalizeLaunchCheckSurfaceTargets(surfaceTargets);
58
+ for (const surface of LAUNCH_CHECK_SURFACE_ORDER) {
59
+ if (targets.includes(surface)) {
60
+ return surface;
61
+ }
62
+ }
63
+ return 'DESKTOP';
64
+ }
65
+ function cloneLaunchCheckContextOptions(surface) {
66
+ const source = LAUNCH_CHECK_SURFACE_CONTEXT_OPTIONS[surface];
67
+ return {
68
+ ...source,
69
+ viewport: source.viewport ? { ...source.viewport } : source.viewport,
70
+ };
71
+ }
17
72
  function resolvePostAuthHostedLaunchState(controllerMode) {
18
73
  return controllerMode === 'REQUIRED' ? 'controller_required' : 'ready';
19
74
  }
20
75
  function resolveAnonymousHostedLaunchState(input) {
21
- const surfaceTargets = Array.isArray(input.surfaceTargets) ? input.surfaceTargets : [];
22
- if (!surfaceTargets.includes(CAPTURE_SURFACE)) {
76
+ const surfaceTargets = normalizeLaunchCheckSurfaceTargets(input.surfaceTargets);
77
+ if (!surfaceTargets.includes(input.captureSurface)) {
23
78
  return 'surface_unsupported';
24
79
  }
25
80
  if (input.authMode === 'REQUIRED') {
@@ -64,7 +119,11 @@ function formatCaptureErrors(errors) {
64
119
  return ` First error${normalized.length === 1 ? '' : 's'}: ${normalized.join(' | ')}`;
65
120
  }
66
121
  function formatHostedLaunchCheckFailure(taskName, result, scope = 'local') {
67
- const scopeLabel = scope === 'staged' ? 'staged upload preview' : 'local hosted preview';
122
+ const scopeLabel = scope === 'final'
123
+ ? 'final owner play route'
124
+ : scope === 'staged'
125
+ ? 'staged upload preview'
126
+ : 'local hosted preview';
68
127
  const errorCode = result.errorCode?.trim() || 'startup_failure';
69
128
  const message = normalizeLaunchCheckMessage(result.message);
70
129
  return `hosted_launch_check_failed: ${taskName} failed the ${scopeLabel} launch-check (${errorCode}). ${message}`;
@@ -102,13 +161,10 @@ function buildRegistrationRequiredLaunchCheckFailure(taskName) {
102
161
  return buildFailedLaunchCheckResult('app_registration_required_for_auth_validation', `Auth-required hosted app "${taskName}" must be registered on PlayDrop before viewer launch validation can run. Run "playdrop project create app ${taskName}" and try again.`, null);
103
162
  }
104
163
  function appendLaunchCheckParams(targetUrl, devAuth) {
105
- const withAuth = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(targetUrl, {
164
+ return (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(targetUrl, {
106
165
  devAuth,
107
166
  player: null,
108
167
  });
109
- const nextUrl = new URL(withAuth);
110
- nextUrl.searchParams.set('launchCheck', '1');
111
- return nextUrl.toString();
112
168
  }
113
169
  async function runHostedLaunchCheckWithCapture(options) {
114
170
  const expectedHostedLaunchState = options.expectedHostedLaunchState ?? 'ready';
@@ -125,6 +181,7 @@ async function runHostedLaunchCheckWithCapture(options) {
125
181
  token: options.token ?? null,
126
182
  user: options.user ?? null,
127
183
  savedSessionBootstrap: options.savedSessionBootstrap ?? false,
184
+ contextOptions: options.contextOptions,
128
185
  });
129
186
  if (result.errorCount > 0) {
130
187
  return buildFailedLaunchCheckResult('startup_failure', `Hosted app emitted ${result.errorCount} console, page, or network error(s) during startup.${formatCaptureErrors(result.errors)}`, options.artifactFingerprint ?? null);
@@ -210,7 +267,12 @@ async function runLocalHostedLaunchCheck(input) {
210
267
  ? null
211
268
  : input.devRouterPort;
212
269
  const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
213
- const anonymousExpectedState = resolveAnonymousHostedLaunchState(localAppMetadata);
270
+ const captureSurface = resolveLaunchCheckSurface(localAppMetadata.surfaceTargets);
271
+ const contextOptions = cloneLaunchCheckContextOptions(captureSurface);
272
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
273
+ ...localAppMetadata,
274
+ captureSurface,
275
+ });
214
276
  if (anonymousExpectedState === 'login_required') {
215
277
  const registeredApp = await (0, registration_1.fetchRegisteredAppShell)(input.client, input.creatorUsername, input.task.name);
216
278
  const canLaunchUnregisteredViewer = input.allowUnregisteredViewerLaunch === true
@@ -230,6 +292,7 @@ async function runLocalHostedLaunchCheck(input) {
230
292
  }),
231
293
  timeoutMs: input.timeoutMs,
232
294
  expectedHostedLaunchState: anonymousExpectedState,
295
+ contextOptions,
233
296
  });
234
297
  if (anonymousGateResult.status !== 'PASSED') {
235
298
  return anonymousGateResult;
@@ -248,6 +311,7 @@ async function runLocalHostedLaunchCheck(input) {
248
311
  token: input.token ?? null,
249
312
  user: input.currentUser ?? null,
250
313
  savedSessionBootstrap: true,
314
+ contextOptions,
251
315
  });
252
316
  }
253
317
  return await runHostedLaunchCheckWithCapture({
@@ -261,6 +325,7 @@ async function runLocalHostedLaunchCheck(input) {
261
325
  }),
262
326
  timeoutMs: input.timeoutMs,
263
327
  expectedHostedLaunchState: anonymousExpectedState,
328
+ contextOptions,
264
329
  });
265
330
  }
266
331
  finally {
@@ -271,7 +336,12 @@ async function runLocalHostedLaunchCheck(input) {
271
336
  }
272
337
  async function runUploadedHostedLaunchCheck(input) {
273
338
  const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
274
- const anonymousExpectedState = resolveAnonymousHostedLaunchState(preview.localAppMetadata);
339
+ const captureSurface = resolveLaunchCheckSurface(preview.localAppMetadata.surfaceTargets);
340
+ const contextOptions = cloneLaunchCheckContextOptions(captureSurface);
341
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
342
+ ...preview.localAppMetadata,
343
+ captureSurface,
344
+ });
275
345
  let result;
276
346
  if (anonymousExpectedState === 'login_required') {
277
347
  if (!preview.app?.id) {
@@ -283,6 +353,7 @@ async function runUploadedHostedLaunchCheck(input) {
283
353
  artifactFingerprint: preview.session.manifestHash,
284
354
  timeoutMs: input.timeoutMs,
285
355
  expectedHostedLaunchState: anonymousExpectedState,
356
+ contextOptions,
286
357
  });
287
358
  if (anonymousGateResult.status !== 'PASSED') {
288
359
  result = anonymousGateResult;
@@ -296,6 +367,7 @@ async function runUploadedHostedLaunchCheck(input) {
296
367
  token: input.token ?? null,
297
368
  user: input.currentUser ?? null,
298
369
  savedSessionBootstrap: true,
370
+ contextOptions,
299
371
  });
300
372
  }
301
373
  }
@@ -306,6 +378,7 @@ async function runUploadedHostedLaunchCheck(input) {
306
378
  artifactFingerprint: preview.session.manifestHash,
307
379
  timeoutMs: input.timeoutMs,
308
380
  expectedHostedLaunchState: anonymousExpectedState,
381
+ contextOptions,
309
382
  });
310
383
  }
311
384
  const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
@@ -326,3 +399,16 @@ async function runUploadedHostedLaunchCheck(input) {
326
399
  artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
327
400
  };
328
401
  }
402
+ async function runPublishedHostedPlayCheck(input) {
403
+ const captureSurface = resolveLaunchCheckSurface(input.surfaceTargets);
404
+ return runHostedLaunchCheckWithCapture({
405
+ targetUrl: input.targetUrl,
406
+ artifactFingerprint: null,
407
+ timeoutMs: input.timeoutMs,
408
+ expectedHostedLaunchState: 'ready',
409
+ token: input.token ?? null,
410
+ user: input.currentUser ?? null,
411
+ savedSessionBootstrap: true,
412
+ contextOptions: cloneLaunchCheckContextOptions(captureSurface),
413
+ });
414
+ }
@@ -253,6 +253,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
253
253
  })),
254
254
  usesPacks: task.uses.packs,
255
255
  tags: task.tags,
256
+ design: task.design,
256
257
  clearTags: options?.clearTags,
257
258
  surfaceTargets: task.surfaceTargets,
258
259
  assetSpecSupport: (task.assetSpecSupport ?? []).map((row) => ({
@@ -409,6 +410,10 @@ async function uploadAppVersion(client, task, artifacts, options) {
409
410
  + `Delete one old version: playdrop creations apps versions delete ${task.name} <version>`);
410
411
  }
411
412
  if (unknownError.status === 409 && unknownError.code === 'version_exists') {
413
+ if (options?.agentTaskId !== undefined) {
414
+ throw new Error(`Version ${task.version} already exists for app "${task.name}".\n` +
415
+ 'This task has a fixed outputVersion; do not bump catalogue.json. The required task version has already been consumed by an upload attempt, so fail the task with the operational reason.');
416
+ }
412
417
  throw new Error(`Version ${task.version} already exists for app "${task.name}".\n` +
413
418
  `Hint: Update "version" in catalogue.json to a new version (e.g., bump to the next patch).`);
414
419
  }
@@ -1,4 +1,4 @@
1
- import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type ContentLicense } from '@playdrop/types';
1
+ import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppVersionDesign, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type ContentLicense } from '@playdrop/types';
2
2
  import { type AssetSpecSupportDeclaration } from './assetSpecs';
3
3
  export type CatalogueJson = {
4
4
  apps?: Array<Record<string, unknown>>;
@@ -41,6 +41,7 @@ export type AppCatalogueEntry = {
41
41
  remixSource?: string | null;
42
42
  remix?: string;
43
43
  surfaceTargets?: Record<string, unknown> | AppSurface[] | null;
44
+ design?: unknown;
44
45
  version?: string;
45
46
  releaseNotes?: string;
46
47
  visibility?: string;
@@ -177,6 +178,7 @@ export type AppTask = {
177
178
  emoji?: string | null;
178
179
  color?: string | null;
179
180
  surfaceTargets: AppSurface[];
181
+ design?: AppVersionDesign;
180
182
  missingMetadata: string[];
181
183
  ecsPath?: string;
182
184
  serverPath?: string;
package/dist/catalogue.js CHANGED
@@ -1300,6 +1300,17 @@ function buildAppTasks(rootDir, catalogues, options) {
1300
1300
  if (errors.length > errorCountBeforeDependencies) {
1301
1301
  continue;
1302
1302
  }
1303
+ let design;
1304
+ if (Object.prototype.hasOwnProperty.call(entry, 'design')) {
1305
+ const parsedDesign = (0, types_1.normalizeAppVersionDesign)(entry.design, {
1306
+ allowedPackRefs: usesPacks,
1307
+ });
1308
+ if (!parsedDesign.ok) {
1309
+ errors.push(`[${label}] App "${rawName}" ${parsedDesign.error}`);
1310
+ continue;
1311
+ }
1312
+ design = parsedDesign.value;
1313
+ }
1303
1314
  const errorCountBeforeTagMetadata = errors.length;
1304
1315
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1305
1316
  const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
@@ -1500,6 +1511,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1500
1511
  emoji: metadata.emoji === undefined ? undefined : metadata.emoji,
1501
1512
  color: metadata.color === undefined ? undefined : metadata.color,
1502
1513
  surfaceTargets: metadata.surfaceTargets,
1514
+ design,
1503
1515
  missingMetadata: metadata.missingFields,
1504
1516
  ecsPath,
1505
1517
  serverPath,
@@ -88,11 +88,16 @@ async function detectCli(command, runShell) {
88
88
  if (pathProbe.exitCode !== 0 || pathProbe.output.trim().length === 0) {
89
89
  return { installed: false };
90
90
  }
91
+ const path = pathProbe.output.split('\n')[0]?.trim();
91
92
  const versionProbe = await runShell(`${command} --version`);
93
+ const version = versionProbe.output.split('\n')[0]?.trim() || undefined;
94
+ if (versionProbe.exitCode !== 0 && !/\d+\.\d+\.\d+/.test(versionProbe.output)) {
95
+ return { installed: false, path };
96
+ }
92
97
  return {
93
98
  installed: true,
94
- path: pathProbe.output.split('\n')[0]?.trim(),
95
- version: versionProbe.exitCode === 0 ? versionProbe.output.split('\n')[0]?.trim() : undefined,
99
+ path,
100
+ version,
96
101
  };
97
102
  }
98
103
  function detectApp(appNames, homeDir, appExists) {
@@ -285,6 +290,9 @@ function printAgentsStatus(status) {
285
290
  }
286
291
  }
287
292
  function runCodexPluginAction(action, runCommand) {
293
+ if (!ensureAgentCliUsable('codex', runCommand)) {
294
+ return;
295
+ }
288
296
  const marketplaceCommand = action === 'install-plugin'
289
297
  ? ['plugin', 'marketplace', 'add', PLAYDROP_PLUGIN_MARKETPLACE]
290
298
  : ['plugin', 'marketplace', 'upgrade', 'playdrop'];
@@ -340,6 +348,9 @@ function runCodexPluginAction(action, runCommand) {
340
348
  console.log('Open Codex and run /plugins, then install or enable PlayDrop.');
341
349
  }
342
350
  function runClaudePluginAction(action, runCommand) {
351
+ if (!ensureAgentCliUsable('claude', runCommand)) {
352
+ return;
353
+ }
343
354
  if (action === 'install-plugin') {
344
355
  const marketplace = runCommand('claude', ['plugin', 'marketplace', 'add', PLAYDROP_PLUGIN_MARKETPLACE]);
345
356
  if (marketplace.status !== 0) {
@@ -379,6 +390,21 @@ function runClaudePluginAction(action, runCommand) {
379
390
  console.log('Updated the PlayDrop plugin for Claude.');
380
391
  console.log('Run /reload-plugins in Claude Code or restart Claude Code to apply it.');
381
392
  }
393
+ function ensureAgentCliUsable(command, runCommand) {
394
+ const result = runCommand(command, ['--version']);
395
+ const output = commandOutput(result).trim();
396
+ if (result.status === 0 || /\d+\.\d+\.\d+/.test(output)) {
397
+ return true;
398
+ }
399
+ const label = command === 'codex' ? 'Codex' : 'Claude';
400
+ console.error(`${label} CLI is missing or not runnable.`);
401
+ console.error(`${command} --version failed with code ${result.status ?? 'unknown'}.`);
402
+ if (output) {
403
+ console.error(`Output: ${output}`);
404
+ }
405
+ process.exitCode = 1;
406
+ return false;
407
+ }
382
408
  function readInstalledPlayDropPlugin(command, runCommand) {
383
409
  const list = runCommand(command, ['plugin', 'list', '--json']);
384
410
  if (list.status !== 0) {
@@ -477,9 +503,10 @@ async function runLoginShell(command) {
477
503
  }
478
504
  function runCommandSync(command, args) {
479
505
  const result = (0, node_child_process_1.spawnSync)(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
506
+ const spawnError = result.error instanceof Error ? result.error.message : '';
480
507
  return {
481
508
  status: result.status,
482
509
  stdout: result.stdout ?? '',
483
- stderr: result.stderr ?? '',
510
+ stderr: [result.stderr ?? '', spawnError].filter(Boolean).join('\n'),
484
511
  };
485
512
  }
@@ -17,6 +17,9 @@ const output_1 = require("../output");
17
17
  const versionCompare_1 = require("../versionCompare");
18
18
  const MAC_APP_PATH = '/Applications/PlayDrop.app';
19
19
  const MAC_CLIENT_LATEST_URL = 'https://www.playdrop.ai/api/mac-client/latest';
20
+ const WINDOWS_CLIENT_EXECUTABLE = 'PlayDrop.exe';
21
+ const WINDOWS_CLIENT_INSTALL_HINT = 'Install or update PlayDrop for Windows with the Windows installer or the in-app updater.';
22
+ const WINDOWS_CLIENT_PATH_ENV = 'PLAYDROP_WINDOWS_CLIENT_PATH';
20
23
  async function showClientStatus(options = {}) {
21
24
  const status = await getClientStatus(options);
22
25
  if (options.json) {
@@ -29,6 +32,9 @@ async function showClientStatus(options = {}) {
29
32
  }
30
33
  async function getClientStatus(options = {}) {
31
34
  const platform = options.platform ?? process.platform;
35
+ if (platform === 'win32') {
36
+ return getWindowsClientStatus(options);
37
+ }
32
38
  if (platform !== 'darwin') {
33
39
  return {
34
40
  platform,
@@ -65,6 +71,24 @@ async function updateClient(options = {}) {
65
71
  }
66
72
  async function launchClient(options = {}) {
67
73
  const platform = options.platform ?? process.platform;
74
+ if (platform === 'win32') {
75
+ const status = await getWindowsClientStatus(options);
76
+ if (!status.installed || !status.appPath) {
77
+ console.error('PlayDrop for Windows is not installed.');
78
+ console.error(WINDOWS_CLIENT_INSTALL_HINT);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ const runCommand = options.runCommand ?? runCommandSync;
83
+ const result = runCommand(status.appPath, []);
84
+ if (result.status !== 0) {
85
+ console.error(`Failed to launch PlayDrop from ${status.appPath}.`);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ console.log('Launched PlayDrop.');
90
+ return;
91
+ }
68
92
  if (platform !== 'darwin') {
69
93
  printUnsupportedPlatform(platform);
70
94
  return;
@@ -87,6 +111,12 @@ async function launchClient(options = {}) {
87
111
  }
88
112
  async function installOrUpdateClient(action, options) {
89
113
  const platform = options.platform ?? process.platform;
114
+ if (platform === 'win32') {
115
+ console.error(`PlayDrop for Windows ${action} is not available through the CLI yet.`);
116
+ console.error(WINDOWS_CLIENT_INSTALL_HINT);
117
+ process.exitCode = 1;
118
+ return;
119
+ }
90
120
  if (platform !== 'darwin') {
91
121
  printUnsupportedPlatform(platform);
92
122
  return;
@@ -129,7 +159,12 @@ function printClientStatus(status) {
129
159
  }
130
160
  if (!status.installed) {
131
161
  console.log('PlayDrop client: missing');
132
- console.log('Run: playdrop clients install');
162
+ if (status.nextAction) {
163
+ console.log(`Run: ${status.nextAction}`);
164
+ }
165
+ else if (status.reason) {
166
+ console.log(status.reason);
167
+ }
133
168
  return;
134
169
  }
135
170
  const version = status.version ? ` ${status.version}` : '';
@@ -141,6 +176,68 @@ function printClientStatus(status) {
141
176
  console.log('Run: playdrop clients update');
142
177
  }
143
178
  }
179
+ async function getWindowsClientStatus(options) {
180
+ const appPath = resolveWindowsClientPath(options.appPath);
181
+ if (!appPath) {
182
+ return {
183
+ platform: 'win32',
184
+ supported: true,
185
+ status: 'missing',
186
+ installed: false,
187
+ reason: WINDOWS_CLIENT_INSTALL_HINT,
188
+ };
189
+ }
190
+ const bundle = readWindowsClientInfo(appPath);
191
+ return {
192
+ platform: 'win32',
193
+ supported: true,
194
+ status: 'installed',
195
+ installed: true,
196
+ appPath,
197
+ version: bundle.version,
198
+ build: bundle.build,
199
+ updateAvailable: false,
200
+ };
201
+ }
202
+ function resolveWindowsClientPath(explicitPath) {
203
+ const candidates = windowsClientPathCandidates(explicitPath);
204
+ return candidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
205
+ }
206
+ function windowsClientPathCandidates(explicitPath) {
207
+ const candidates = [];
208
+ const add = (value) => {
209
+ if (value && value.trim().length > 0) {
210
+ candidates.push(value.trim());
211
+ }
212
+ };
213
+ add(explicitPath);
214
+ add(process.env[WINDOWS_CLIENT_PATH_ENV]);
215
+ const localAppData = process.env.LOCALAPPDATA;
216
+ if (localAppData) {
217
+ add((0, node_path_1.join)(localAppData, 'PlayDrop.Windows', 'current', WINDOWS_CLIENT_EXECUTABLE));
218
+ add((0, node_path_1.join)(localAppData, 'PlayDrop', 'current', WINDOWS_CLIENT_EXECUTABLE));
219
+ }
220
+ add(process.env.ProgramFiles ? (0, node_path_1.join)(process.env.ProgramFiles, 'PlayDrop', WINDOWS_CLIENT_EXECUTABLE) : undefined);
221
+ add(process.env['ProgramFiles(x86)'] ? (0, node_path_1.join)(process.env['ProgramFiles(x86)'], 'PlayDrop', WINDOWS_CLIENT_EXECUTABLE) : undefined);
222
+ return [...new Set(candidates)];
223
+ }
224
+ function readWindowsClientInfo(appPath) {
225
+ for (const versionPath of [
226
+ (0, node_path_1.join)((0, node_path_1.dirname)(appPath), 'sq.version'),
227
+ (0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.dirname)(appPath)), 'sq.version'),
228
+ ]) {
229
+ try {
230
+ const version = (0, node_fs_1.readFileSync)(versionPath, 'utf8').trim();
231
+ if (version.length > 0) {
232
+ return { version };
233
+ }
234
+ }
235
+ catch {
236
+ // Development and portable builds may not have a Velopack version file.
237
+ }
238
+ }
239
+ return {};
240
+ }
144
241
  async function fetchLatestMacClientManifest() {
145
242
  try {
146
243
  const response = await fetch(MAC_CLIENT_LATEST_URL, { headers: { Accept: 'application/json' } });
@@ -22,6 +22,14 @@ interface CliHealth {
22
22
  updateStatus: 'current' | 'update-available' | 'unknown';
23
23
  updateAvailable?: boolean;
24
24
  nextAction?: string;
25
+ command?: CliCommandHealth;
26
+ }
27
+ interface CliCommandHealth {
28
+ available: boolean;
29
+ path?: string;
30
+ npmGlobalBin?: string;
31
+ npmShimPath?: string;
32
+ issue?: 'not-on-path' | 'shim-missing';
25
33
  }
26
34
  export interface DoctorReport {
27
35
  cli: CliHealth;