@playdrop/playdrop-cli 0.12.42 → 0.13.1

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.
Files changed (110) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/index.d.ts +2 -0
  3. package/dist/apps/index.js +2 -1
  4. package/dist/apps/loadCheck.d.ts +4 -1
  5. package/dist/apps/loadCheck.js +40 -10
  6. package/dist/apps/staticHtml.d.ts +28 -0
  7. package/dist/apps/staticHtml.js +343 -0
  8. package/dist/apps/validate.js +2 -1
  9. package/dist/commands/captureListing.d.ts +1 -1
  10. package/dist/commands/captureListing.js +4 -3
  11. package/dist/commands/devServer.js +19 -0
  12. package/dist/commands/feedback.d.ts +2 -0
  13. package/dist/commands/feedback.js +44 -2
  14. package/dist/commands/upload.d.ts +24 -0
  15. package/dist/commands/upload.js +122 -4
  16. package/dist/commands/worker/instrument-evidence.js +1 -1
  17. package/dist/commands/worker/runtime.d.ts +15 -0
  18. package/dist/commands/worker/runtime.js +121 -11
  19. package/dist/commands/worker.d.ts +28 -2
  20. package/dist/commands/worker.js +843 -170
  21. package/dist/index.js +7 -0
  22. package/dist/listingPreflight.js +15 -4
  23. package/dist/recorderRelease.d.ts +18 -0
  24. package/dist/recorderRelease.js +90 -0
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
  26. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -2
  28. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  29. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +13 -2
  30. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  32. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +2 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/me.js +11 -0
  35. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -3
  36. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  37. package/node_modules/@playdrop/api-client/dist/index.js +14 -2
  38. package/node_modules/@playdrop/config/client-meta.json +2 -2
  39. package/node_modules/@playdrop/config/dist/src/constants.d.ts +1 -0
  40. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  41. package/node_modules/@playdrop/config/dist/src/constants.js +1 -0
  42. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  43. package/node_modules/@playdrop/types/dist/api.d.ts +195 -4
  44. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  45. package/node_modules/@playdrop/types/dist/api.js +25 -2
  46. package/node_modules/entities/decode-codepoint.d.ts +19 -0
  47. package/node_modules/entities/decode-codepoint.d.ts.map +1 -0
  48. package/node_modules/entities/decode-codepoint.js +77 -0
  49. package/node_modules/entities/decode-codepoint.js.map +1 -0
  50. package/node_modules/entities/decode.d.ts +209 -0
  51. package/node_modules/entities/decode.d.ts.map +1 -0
  52. package/node_modules/entities/decode.js +511 -0
  53. package/node_modules/entities/decode.js.map +1 -0
  54. package/node_modules/entities/encode.d.ts +22 -0
  55. package/node_modules/entities/encode.d.ts.map +1 -0
  56. package/node_modules/entities/encode.js +73 -0
  57. package/node_modules/entities/encode.js.map +1 -0
  58. package/node_modules/entities/escape.d.ts +43 -0
  59. package/node_modules/entities/escape.d.ts.map +1 -0
  60. package/node_modules/entities/escape.js +121 -0
  61. package/node_modules/entities/escape.js.map +1 -0
  62. package/node_modules/entities/generated/decode-data-html.d.ts +2 -0
  63. package/node_modules/entities/generated/decode-data-html.d.ts.map +1 -0
  64. package/node_modules/entities/generated/decode-data-html.js +10 -0
  65. package/node_modules/entities/generated/decode-data-html.js.map +1 -0
  66. package/node_modules/entities/generated/decode-data-xml.d.ts +2 -0
  67. package/node_modules/entities/generated/decode-data-xml.d.ts.map +1 -0
  68. package/node_modules/entities/generated/decode-data-xml.js +10 -0
  69. package/node_modules/entities/generated/decode-data-xml.js.map +1 -0
  70. package/node_modules/entities/generated/encode-html.d.ts +8 -0
  71. package/node_modules/entities/generated/encode-html.d.ts.map +1 -0
  72. package/node_modules/entities/generated/encode-html.js +13 -0
  73. package/node_modules/entities/generated/encode-html.js.map +1 -0
  74. package/node_modules/entities/index.d.ts +96 -0
  75. package/node_modules/entities/index.d.ts.map +1 -0
  76. package/node_modules/entities/index.js +131 -0
  77. package/node_modules/entities/index.js.map +1 -0
  78. package/node_modules/entities/package.json +3 -0
  79. package/node_modules/parse5/common/doctype.d.ts +4 -0
  80. package/node_modules/parse5/common/doctype.js +118 -0
  81. package/node_modules/parse5/common/error-codes.d.ts +67 -0
  82. package/node_modules/parse5/common/error-codes.js +66 -0
  83. package/node_modules/parse5/common/foreign-content.d.ts +9 -0
  84. package/node_modules/parse5/common/foreign-content.js +237 -0
  85. package/node_modules/parse5/common/html.d.ts +289 -0
  86. package/node_modules/parse5/common/html.js +528 -0
  87. package/node_modules/parse5/common/token.d.ts +84 -0
  88. package/node_modules/parse5/common/token.js +24 -0
  89. package/node_modules/parse5/common/unicode.d.ts +42 -0
  90. package/node_modules/parse5/common/unicode.js +70 -0
  91. package/node_modules/parse5/index.d.ts +71 -0
  92. package/node_modules/parse5/index.js +54 -0
  93. package/node_modules/parse5/package.json +1 -0
  94. package/node_modules/parse5/parser/formatting-element-list.d.ts +36 -0
  95. package/node_modules/parse5/parser/formatting-element-list.js +114 -0
  96. package/node_modules/parse5/parser/index.d.ts +221 -0
  97. package/node_modules/parse5/parser/index.js +3240 -0
  98. package/node_modules/parse5/parser/open-element-stack.d.ts +53 -0
  99. package/node_modules/parse5/parser/open-element-stack.js +328 -0
  100. package/node_modules/parse5/serializer/index.d.ts +60 -0
  101. package/node_modules/parse5/serializer/index.js +172 -0
  102. package/node_modules/parse5/tokenizer/index.d.ts +247 -0
  103. package/node_modules/parse5/tokenizer/index.js +2714 -0
  104. package/node_modules/parse5/tokenizer/preprocessor.d.ts +36 -0
  105. package/node_modules/parse5/tokenizer/preprocessor.js +200 -0
  106. package/node_modules/parse5/tree-adapters/default.d.ts +84 -0
  107. package/node_modules/parse5/tree-adapters/default.js +176 -0
  108. package/node_modules/parse5/tree-adapters/interface.d.ts +255 -0
  109. package/node_modules/parse5/tree-adapters/interface.js +2 -0
  110. package/package.json +8 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.12.42",
2
+ "version": "0.13.1",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.12.42",
4
+ "runtimeSdkVersion": "0.13.1",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -19,6 +19,7 @@ export type AppPipelineOptions = {
19
19
  token?: string | null;
20
20
  user?: UserResponse | null;
21
21
  runLocalLoadCheck?: boolean;
22
+ devRouterPort?: number;
22
23
  runStagedUploadLoadCheck?: boolean;
23
24
  loadCheckTimeoutMs?: number;
24
25
  ensureRegisteredAppShell?: boolean;
@@ -27,6 +28,7 @@ export type AppPipelineOptions = {
27
28
  mediaCaptureRequired?: boolean;
28
29
  machineCausalGateRequired?: boolean;
29
30
  captureSession?: TaskCaptureSession | null;
31
+ artifacts?: AppBuildArtifacts;
30
32
  };
31
33
  export declare function runAppPipeline(client: ApiClient, task: AppTask, options?: AppPipelineOptions): Promise<AppPipelineResult>;
32
34
  export { collectAppValidationWarnings, validateAppTask, buildApp, uploadApp, runFormatScript };
@@ -29,7 +29,7 @@ async function runAppPipeline(client, task, options) {
29
29
  agentTaskId: options.agentTaskId,
30
30
  });
31
31
  }
32
- const artifacts = isExternal ? null : await (0, build_1.buildApp)(task);
32
+ const artifacts = isExternal ? null : options?.artifacts ?? await (0, build_1.buildApp)(task);
33
33
  const warnings = (0, validate_1.collectAppValidationWarnings)(task, isExternal ? 'source' : 'bundle');
34
34
  if (!isExternal && options?.runLocalLoadCheck) {
35
35
  const creatorUsername = typeof options.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
@@ -46,6 +46,7 @@ async function runAppPipeline(client, task, options) {
46
46
  webBase: options.webBase,
47
47
  creatorUsername,
48
48
  task,
49
+ devRouterPort: options.devRouterPort,
49
50
  timeoutMs: options.loadCheckTimeoutMs,
50
51
  token: options.token ?? null,
51
52
  currentUser: options.user ?? null,
@@ -90,7 +90,10 @@ export declare function runUploadedHostedLoadCheck(input: {
90
90
  postReadyWaitMs?: number;
91
91
  }): Promise<HostedLoadCheckResult>;
92
92
  export declare function sliceFirstPrimaryVerbAction(tape: AppPlaytestTape): AppPlaytestTape;
93
- export declare function compareCausalFrames(firstPath: string, secondPath: string): Promise<AppUploadCausalComparison>;
93
+ export declare function compareCausalFrames(firstPath: string, secondPath: string, thresholds?: {
94
+ meanDeltaMin: number;
95
+ changedPixelRatioMin: number;
96
+ }): Promise<AppUploadCausalComparison>;
94
97
  export declare function runUploadedHostedCausalCheck(input: {
95
98
  client: ApiClient;
96
99
  creatorUsername: string;
@@ -41,6 +41,10 @@ const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
41
41
  const POST_READY_SETTLE_MS = 750;
42
42
  const CAUSAL_FRAME_MEAN_DELTA_MIN = 0.025;
43
43
  const CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.12;
44
+ const CAUSAL_START_FRAME_MEAN_DELTA_MIN = 0.02;
45
+ const CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.08;
46
+ const CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN = 0.012;
47
+ const CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.05;
44
48
  const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
45
49
  const GOOGLE_TELEMETRY_HOSTS = [
46
50
  'analytics.google.com',
@@ -1124,7 +1128,10 @@ function sliceFirstPrimaryVerbAction(tape) {
1124
1128
  async function hashFile(path) {
1125
1129
  return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(path)).digest('hex');
1126
1130
  }
1127
- async function compareCausalFrames(firstPath, secondPath) {
1131
+ async function compareCausalFrames(firstPath, secondPath, thresholds = {
1132
+ meanDeltaMin: CAUSAL_FRAME_MEAN_DELTA_MIN,
1133
+ changedPixelRatioMin: CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN,
1134
+ }) {
1128
1135
  const readPixels = async (path) => {
1129
1136
  return await (0, sharp_1.default)(path)
1130
1137
  .resize(64, 64, { fit: 'fill' })
@@ -1149,7 +1156,7 @@ async function compareCausalFrames(firstPath, secondPath) {
1149
1156
  return {
1150
1157
  meanDelta,
1151
1158
  changedPixelRatio,
1152
- passed: meanDelta >= CAUSAL_FRAME_MEAN_DELTA_MIN || changedPixelRatio >= CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN,
1159
+ passed: meanDelta >= thresholds.meanDeltaMin || changedPixelRatio >= thresholds.changedPixelRatioMin,
1153
1160
  };
1154
1161
  }
1155
1162
  async function runUploadedHostedCausalCheck(input) {
@@ -1218,20 +1225,43 @@ async function runUploadedHostedCausalCheck(input) {
1218
1225
  throw new Error('agent_task_machine_causal_gate_artifact_changed_during_check');
1219
1226
  }
1220
1227
  const comparisons = {
1221
- startOnlyVsReady: await compareCausalFrames(paths.startOnlyReady, paths.startOnly),
1222
- startOnlyVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.startOnly),
1223
- fullTapeVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.fullTape),
1224
- fullTapeVsStartOnly: await compareCausalFrames(paths.startOnly, paths.fullTape),
1225
- fullTapeVsFirstAction: await compareCausalFrames(paths.firstAction, paths.fullTape),
1228
+ startOnlyVsReady: await compareCausalFrames(paths.startOnlyReady, paths.startOnly, {
1229
+ meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
1230
+ changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
1231
+ }),
1232
+ startOnlyVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.startOnly, {
1233
+ meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
1234
+ changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
1235
+ }),
1236
+ fullTapeVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.fullTape, {
1237
+ meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1238
+ changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1239
+ }),
1240
+ fullTapeVsStartOnly: await compareCausalFrames(paths.startOnly, paths.fullTape, {
1241
+ meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1242
+ changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1243
+ }),
1244
+ fullTapeVsFirstAction: await compareCausalFrames(paths.firstAction, paths.fullTape, {
1245
+ meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1246
+ changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1247
+ }),
1226
1248
  };
1227
1249
  if (!comparisons.startOnlyVsReady.passed || !comparisons.startOnlyVsZeroInput.passed) {
1228
- throw new Error('agent_task_machine_causal_gate_failed:start-only did not provably leave the title state. Fix the start action or ready-relative timing.');
1250
+ throw new Error(`agent_task_machine_causal_gate_failed:start-only did not provably leave the title state. `
1251
+ + `ready(mean=${comparisons.startOnlyVsReady.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsReady.changedPixelRatio.toFixed(4)}); `
1252
+ + `zero(mean=${comparisons.startOnlyVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsZeroInput.changedPixelRatio.toFixed(4)}). `
1253
+ + 'Fix the start action, visible start-only outcome, or ready-relative timing.');
1229
1254
  }
1230
1255
  if (!comparisons.fullTapeVsZeroInput.passed || !comparisons.fullTapeVsStartOnly.passed) {
1231
- throw new Error('agent_task_machine_causal_gate_failed:full tape did not meaningfully beat zero-input and start-only controls. Fix the game or tape before upload.');
1256
+ throw new Error(`agent_task_machine_causal_gate_failed:full tape did not meaningfully beat zero-input and start-only controls. `
1257
+ + `zero(mean=${comparisons.fullTapeVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsZeroInput.changedPixelRatio.toFixed(4)}); `
1258
+ + `start(mean=${comparisons.fullTapeVsStartOnly.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsStartOnly.changedPixelRatio.toFixed(4)}). `
1259
+ + 'Fix the game or tape before upload.');
1232
1260
  }
1233
1261
  if (!comparisons.fullTapeVsFirstAction.passed) {
1234
- throw new Error('agent_task_machine_causal_gate_failed:first primary action trivially reached the full-tape outcome. Add a sustained causal loop before upload.');
1262
+ throw new Error(`agent_task_machine_causal_gate_failed:first primary action trivially reached the full-tape outcome. `
1263
+ + `action(mean=${comparisons.fullTapeVsFirstAction.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsFirstAction.changedPixelRatio.toFixed(4)}). `
1264
+ + 'Add a sustained causal loop before upload.');
1235
1265
  }
1236
1266
  const teachingText = input.teachingText.trim();
1237
1267
  const evidence = {
@@ -0,0 +1,28 @@
1
+ import type { AppSurface } from '@playdrop/types';
2
+ import type { AppTask } from '../catalogue';
3
+ import { type AppBuildArtifacts } from './build';
4
+ export type StaticHtmlMetadata = {
5
+ title: string;
6
+ description: string | null;
7
+ imagePath: string | null;
8
+ warnings: string[];
9
+ };
10
+ export type PreparedStaticHtmlProject = {
11
+ sourceTask: AppTask;
12
+ runtimeTask: AppTask;
13
+ artifacts: AppBuildArtifacts;
14
+ sourceHash: string;
15
+ runtimeHash: string;
16
+ metadata: StaticHtmlMetadata;
17
+ runtimeDir: string;
18
+ cleanup: () => Promise<void>;
19
+ };
20
+ export declare function parseStaticHtmlMetadata(projectDir: string, entryPoint?: string): StaticHtmlMetadata;
21
+ export declare function buildStaticGameSlug(title: string, taskId?: number): string;
22
+ export declare function prepareStaticHtmlProject(input: {
23
+ projectDir: string;
24
+ taskId?: number;
25
+ appName?: string;
26
+ primarySurface: AppSurface;
27
+ }): Promise<PreparedStaticHtmlProject>;
28
+ export declare function sha256File(filePath: string): string;
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseStaticHtmlMetadata = parseStaticHtmlMetadata;
4
+ exports.buildStaticGameSlug = buildStaticGameSlug;
5
+ exports.prepareStaticHtmlProject = prepareStaticHtmlProject;
6
+ exports.sha256File = sha256File;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_fs_1 = require("node:fs");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ const parse5_1 = require("parse5");
13
+ const build_1 = require("./build");
14
+ const STATIC_GAME_SURFACES = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
15
+ const STATIC_SOURCE_EXCLUDES = ['AGENTS.md', 'CLAUDE.md', '.playdrop/', 'bin/playdrop'];
16
+ const STATIC_BUNDLE_EXCLUDES = [...STATIC_SOURCE_EXCLUDES];
17
+ function attribute(node, name) {
18
+ return node.attrs?.find((entry) => entry.name.toLowerCase() === name)?.value.trim() ?? null;
19
+ }
20
+ function walk(node, visit) {
21
+ visit(node);
22
+ for (const child of node.childNodes ?? []) {
23
+ walk(child, visit);
24
+ }
25
+ }
26
+ function findElement(node, tagName) {
27
+ if (node.tagName === tagName)
28
+ return node;
29
+ for (const child of node.childNodes ?? []) {
30
+ const match = findElement(child, tagName);
31
+ if (match)
32
+ return match;
33
+ }
34
+ return null;
35
+ }
36
+ function textContent(node) {
37
+ if (typeof node.value === 'string') {
38
+ return node.value;
39
+ }
40
+ return (node.childNodes ?? []).map(textContent).join('');
41
+ }
42
+ function isPngFile(filePath) {
43
+ const signature = (0, node_fs_1.readFileSync)(filePath).subarray(0, 8);
44
+ return signature.length === 8
45
+ && signature.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
46
+ }
47
+ function isInside(root, candidate) {
48
+ const rel = (0, node_path_1.relative)(root, candidate);
49
+ return rel === '' || (!rel.startsWith(`..${node_path_1.sep}`) && rel !== '..' && !(0, node_path_1.isAbsolute)(rel));
50
+ }
51
+ function localReferencePath(projectDir, rawReference, code) {
52
+ const reference = rawReference.trim();
53
+ if (!reference || reference.startsWith('#') || reference.startsWith('data:') || reference.startsWith('blob:')) {
54
+ return null;
55
+ }
56
+ if (/^(?:https?:)?\/\//i.test(reference) || /^[a-z][a-z0-9+.-]*:/i.test(reference)) {
57
+ throw new Error(`${code}:remote_reference:${reference}`);
58
+ }
59
+ if (reference.startsWith('/')) {
60
+ throw new Error(`${code}:root_reference:${reference}`);
61
+ }
62
+ let decoded;
63
+ try {
64
+ decoded = decodeURIComponent(reference.split(/[?#]/, 1)[0] ?? '');
65
+ }
66
+ catch {
67
+ throw new Error(`${code}:invalid_reference:${reference}`);
68
+ }
69
+ const absoluteRoot = (0, node_path_1.resolve)(projectDir);
70
+ const resolvedPath = (0, node_path_1.resolve)(absoluteRoot, decoded);
71
+ const realRoot = (0, node_fs_1.realpathSync)(absoluteRoot);
72
+ if (!isInside(absoluteRoot, resolvedPath) || !(0, node_fs_1.existsSync)(resolvedPath) || !(0, node_fs_1.statSync)(resolvedPath).isFile()) {
73
+ throw new Error(`${code}:missing_or_outside:${reference}`);
74
+ }
75
+ const realFile = (0, node_fs_1.realpathSync)(resolvedPath);
76
+ if (!isInside(realRoot, realFile)) {
77
+ throw new Error(`${code}:outside_project:${reference}`);
78
+ }
79
+ return realFile;
80
+ }
81
+ function validateHtmlRuntimeReferences(projectDir, document) {
82
+ walk(document, (node) => {
83
+ const tag = node.tagName?.toLowerCase();
84
+ if (!tag)
85
+ return;
86
+ if (tag === 'base') {
87
+ throw new Error('static_game_artifact_base_tag_unsupported');
88
+ }
89
+ const rel = attribute(node, 'rel')?.toLowerCase().split(/\s+/) ?? [];
90
+ const reference = tag === 'script'
91
+ ? attribute(node, 'src')
92
+ : tag === 'link' && (rel.includes('stylesheet') || rel.includes('icon') || rel.includes('preload'))
93
+ ? attribute(node, 'href')
94
+ : ['img', 'audio', 'video', 'source', 'track', 'embed', 'object', 'iframe'].includes(tag)
95
+ ? attribute(node, tag === 'object' ? 'data' : 'src')
96
+ : null;
97
+ if (reference) {
98
+ localReferencePath(projectDir, reference, 'static_game_artifact_invalid_local_path');
99
+ }
100
+ const srcset = tag === 'img' || tag === 'source' ? attribute(node, 'srcset') : null;
101
+ for (const candidate of srcset?.split(',') ?? []) {
102
+ const referenceValue = candidate.trim().split(/\s+/, 1)[0];
103
+ if (referenceValue) {
104
+ localReferencePath(projectDir, referenceValue, 'static_game_artifact_invalid_local_path');
105
+ }
106
+ }
107
+ });
108
+ }
109
+ function parseStaticHtmlProject(projectDir, entryPoint = 'index.html') {
110
+ const root = (0, node_path_1.resolve)(projectDir);
111
+ const entryPath = (0, node_path_1.resolve)(root, entryPoint);
112
+ if (!isInside(root, entryPath) || (0, node_path_1.basename)(entryPath).toLowerCase() !== 'index.html') {
113
+ throw new Error('static_game_artifact_invalid_entry_point');
114
+ }
115
+ if (!(0, node_fs_1.existsSync)(entryPath) || !(0, node_fs_1.statSync)(entryPath).isFile()) {
116
+ throw new Error('static_game_artifact_index_missing');
117
+ }
118
+ const html = (0, node_fs_1.readFileSync)(entryPath, 'utf8');
119
+ if (html.includes('data-playdrop-static-sdk') || html.includes('data-playdrop-static-bootstrap')) {
120
+ throw new Error('static_game_artifact_injected_sdk_already_present');
121
+ }
122
+ const document = (0, parse5_1.parse)(html, { sourceCodeLocationInfo: true });
123
+ const head = findElement(document, 'head');
124
+ const body = findElement(document, 'body');
125
+ if (!head) {
126
+ throw new Error('static_game_artifact_head_missing');
127
+ }
128
+ const titles = [];
129
+ const metas = [];
130
+ let headEndOffset = null;
131
+ walk(head, (node) => {
132
+ if (node.tagName === 'title')
133
+ titles.push(node);
134
+ if (node.tagName === 'meta')
135
+ metas.push(node);
136
+ if (node.tagName === 'head' && Number.isInteger(node.sourceCodeLocation?.endTag?.startOffset)) {
137
+ headEndOffset = Number(node.sourceCodeLocation.endTag.startOffset);
138
+ }
139
+ });
140
+ if (titles.length !== 1) {
141
+ throw new Error(`static_game_artifact_title_count:${titles.length}`);
142
+ }
143
+ const title = textContent(titles[0]).replace(/\s+/g, ' ').trim();
144
+ if (!title || title.length > 120) {
145
+ throw new Error('static_game_artifact_title_invalid');
146
+ }
147
+ if (headEndOffset === null && body) {
148
+ const explicitBodyStart = body.sourceCodeLocation?.startTag?.startOffset
149
+ ?? body.sourceCodeLocation?.startOffset;
150
+ const firstBodyChildStart = (body.childNodes ?? [])
151
+ .map((node) => node.sourceCodeLocation?.startOffset)
152
+ .find((offset) => Number.isInteger(offset));
153
+ const finalHeadChildEnd = [...(head.childNodes ?? [])]
154
+ .reverse()
155
+ .map((node) => node.sourceCodeLocation?.endOffset)
156
+ .find((offset) => Number.isInteger(offset));
157
+ headEndOffset = explicitBodyStart ?? firstBodyChildStart ?? finalHeadChildEnd ?? null;
158
+ }
159
+ if (headEndOffset === null) {
160
+ throw new Error('static_game_artifact_html_injection_point_missing');
161
+ }
162
+ for (const meta of metas) {
163
+ const httpEquiv = attribute(meta, 'http-equiv')?.toLowerCase();
164
+ if (httpEquiv === 'content-security-policy') {
165
+ throw new Error('static_game_artifact_blocking_csp');
166
+ }
167
+ }
168
+ validateHtmlRuntimeReferences(root, document);
169
+ const readMeta = (...keys) => {
170
+ for (const key of keys) {
171
+ const match = metas.find((node) => (attribute(node, 'name')?.toLowerCase() === key
172
+ || attribute(node, 'property')?.toLowerCase() === key));
173
+ const value = match ? attribute(match, 'content') : null;
174
+ if (value)
175
+ return value;
176
+ }
177
+ return null;
178
+ };
179
+ const description = readMeta('description', 'og:description');
180
+ const image = readMeta('og:image', 'twitter:image');
181
+ const warnings = [];
182
+ let imagePath = null;
183
+ if (image) {
184
+ try {
185
+ const resolvedImage = localReferencePath(root, image, 'static_game_artifact_image_invalid');
186
+ if (resolvedImage && (0, node_path_1.extname)(resolvedImage).toLowerCase() === '.png' && isPngFile(resolvedImage)) {
187
+ imagePath = resolvedImage;
188
+ }
189
+ else {
190
+ warnings.push('Optional HTML image metadata was ignored because it is not a local PNG.');
191
+ }
192
+ }
193
+ catch {
194
+ warnings.push('Optional HTML image metadata was ignored because it does not resolve to a local PNG.');
195
+ }
196
+ }
197
+ return {
198
+ html,
199
+ headEndOffset,
200
+ metadata: { title, description, imagePath, warnings },
201
+ };
202
+ }
203
+ function parseStaticHtmlMetadata(projectDir, entryPoint = 'index.html') {
204
+ return parseStaticHtmlProject(projectDir, entryPoint).metadata;
205
+ }
206
+ function buildStaticGameSlug(title, taskId) {
207
+ const base = title.normalize('NFKD')
208
+ .replace(/[\u0300-\u036f]/g, '')
209
+ .toLowerCase()
210
+ .replace(/[^a-z0-9]+/g, '-')
211
+ .replace(/^-+|-+$/g, '') || 'game';
212
+ const suffix = taskId === undefined ? '' : `-t${taskId}`;
213
+ return `${base.slice(0, 128 - suffix.length).replace(/-+$/g, '')}${suffix}`;
214
+ }
215
+ function buildListing(imagePath, primarySurface) {
216
+ if (!imagePath)
217
+ return undefined;
218
+ return {
219
+ ...(primarySurface === 'MOBILE_PORTRAIT'
220
+ ? { heroPortraitPath: imagePath }
221
+ : { heroLandscapePath: imagePath }),
222
+ screenshotPortraitPaths: [],
223
+ screenshotLandscapePaths: [],
224
+ videoPortraitPaths: [],
225
+ videoLandscapePaths: [],
226
+ };
227
+ }
228
+ function buildStaticAppTask(input) {
229
+ return {
230
+ kind: 'app',
231
+ name: input.name,
232
+ cataloguePath: input.entryPath,
233
+ catalogueAbsolutePath: input.entryPath,
234
+ catalogueDir: input.projectDir,
235
+ filePath: input.entryPath,
236
+ relativeFile: 'index.html',
237
+ projectDir: input.projectDir,
238
+ packageJsonPath: null,
239
+ isTypescriptProject: false,
240
+ hasValidateScript: false,
241
+ hasFormatScript: false,
242
+ displayName: input.metadata.title,
243
+ ...(input.metadata.description ? { description: input.metadata.description } : {}),
244
+ type: 'GAME',
245
+ surfaceTargets: [...STATIC_GAME_SURFACES],
246
+ primarySurface: input.primarySurface,
247
+ missingMetadata: [],
248
+ version: '1.0.0',
249
+ releaseNotes: 'Initial static game upload',
250
+ versionVisibility: 'PRIVATE',
251
+ sourceArchiveExcludeRelativeFiles: STATIC_SOURCE_EXCLUDES,
252
+ bundleArchiveExcludeRelativeFiles: STATIC_BUNDLE_EXCLUDES,
253
+ license: 'CLOSED',
254
+ tags: [],
255
+ hostingMode: 'HOSTED',
256
+ authMode: 'NONE',
257
+ previewable: true,
258
+ ...(buildListing(input.metadata.imagePath, input.primarySurface)
259
+ ? { listing: buildListing(input.metadata.imagePath, input.primarySurface) }
260
+ : {}),
261
+ achievements: [],
262
+ leaderboards: [],
263
+ ownedAssets: [],
264
+ assetSpecSupport: [],
265
+ uses: { assets: [], packs: [] },
266
+ graph: { relations: [] },
267
+ };
268
+ }
269
+ const STATIC_SDK_BOOTSTRAP = [
270
+ '<script src="/sdk/playdrop.js" data-playdrop-static-sdk></script>',
271
+ '<script data-playdrop-static-bootstrap>',
272
+ 'window.addEventListener("load", async () => {',
273
+ ' const sdk = await window.playdrop.init();',
274
+ ' await sdk.host.ready();',
275
+ '}, { once: true });',
276
+ '</script>',
277
+ ].join('\n');
278
+ async function prepareStaticHtmlProject(input) {
279
+ const sourceRoot = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(input.projectDir));
280
+ const sourceEntryPath = (0, node_path_1.join)(sourceRoot, 'index.html');
281
+ const parsedProject = parseStaticHtmlProject(sourceRoot);
282
+ const metadata = parsedProject.metadata;
283
+ const requestedAppName = input.appName?.trim() ?? '';
284
+ if (requestedAppName && (requestedAppName.length > 128 || !/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(requestedAppName))) {
285
+ throw new Error('static_game_artifact_claimed_slug_invalid');
286
+ }
287
+ const name = requestedAppName || buildStaticGameSlug(metadata.title, input.taskId);
288
+ const sourceTask = buildStaticAppTask({
289
+ projectDir: sourceRoot,
290
+ entryPath: sourceEntryPath,
291
+ name,
292
+ metadata,
293
+ primarySurface: input.primarySurface,
294
+ });
295
+ const sourceArtifacts = await (0, build_1.buildApp)(sourceTask);
296
+ const runtimeDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), `playdrop-static-${name}-`));
297
+ try {
298
+ await (0, promises_1.cp)(sourceRoot, runtimeDir, {
299
+ recursive: true,
300
+ filter: (source) => {
301
+ const rel = (0, node_path_1.relative)(sourceRoot, source).replace(/\\/g, '/');
302
+ return !rel || !STATIC_SOURCE_EXCLUDES.some((excluded) => (excluded.endsWith('/') ? rel === excluded.slice(0, -1) || rel.startsWith(excluded) : rel === excluded));
303
+ },
304
+ });
305
+ const runtimeEntryPath = (0, node_path_1.join)(runtimeDir, 'index.html');
306
+ const sourceHtml = parsedProject.html;
307
+ const runtimeHtml = `${sourceHtml.slice(0, parsedProject.headEndOffset)}${STATIC_SDK_BOOTSTRAP}\n${sourceHtml.slice(parsedProject.headEndOffset)}`;
308
+ await (0, promises_1.writeFile)(runtimeEntryPath, runtimeHtml, 'utf8');
309
+ const runtimeTask = buildStaticAppTask({
310
+ projectDir: runtimeDir,
311
+ entryPath: runtimeEntryPath,
312
+ name,
313
+ metadata: {
314
+ ...metadata,
315
+ imagePath: metadata.imagePath ? (0, node_path_1.join)(runtimeDir, (0, node_path_1.relative)(sourceRoot, metadata.imagePath)) : null,
316
+ },
317
+ primarySurface: input.primarySurface,
318
+ });
319
+ const runtimeArtifacts = await (0, build_1.buildApp)(runtimeTask);
320
+ const artifacts = {
321
+ ...runtimeArtifacts,
322
+ sourceFile: sourceArtifacts.sourceFile,
323
+ sourceHash: sourceArtifacts.sourceHash,
324
+ };
325
+ return {
326
+ sourceTask,
327
+ runtimeTask,
328
+ artifacts,
329
+ sourceHash: sourceArtifacts.sourceHash,
330
+ runtimeHash: runtimeArtifacts.bundleHash,
331
+ metadata,
332
+ runtimeDir,
333
+ cleanup: async () => { await (0, promises_1.rm)(runtimeDir, { recursive: true, force: true }); },
334
+ };
335
+ }
336
+ catch (error) {
337
+ await (0, promises_1.rm)(runtimeDir, { recursive: true, force: true });
338
+ throw error;
339
+ }
340
+ }
341
+ function sha256File(filePath) {
342
+ return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(filePath)).digest('hex');
343
+ }
@@ -43,6 +43,7 @@ const LEGACY_SDK_SYMBOL_PATTERNS = [
43
43
  ];
44
44
  const SDK_REFERENCE_PATTERNS = [
45
45
  /https:\/\/assets\.playdrop\.ai\/sdk\/playdrop\.js/g,
46
+ /["']\/sdk\/playdrop\.js(?:[?"'])/g,
46
47
  /@playdrop\/sdk(?:\/browser)?/g,
47
48
  ];
48
49
  const SDK_INIT_PATTERNS = [
@@ -487,7 +488,7 @@ function collectAppValidationWarnings(task, mode = 'source') {
487
488
  const hasSdkReady = primaryDetection.hasSdkReady || sourceDetection.hasSdkReady;
488
489
  const warnings = [];
489
490
  if (!hasSdkReference) {
490
- warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find https://assets.playdrop.ai/sdk/playdrop.js or @playdrop/sdk in the app files, so it might not work once uploaded.`);
491
+ warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find /sdk/playdrop.js, https://assets.playdrop.ai/sdk/playdrop.js, or @playdrop/sdk in the app files, so it might not work once uploaded.`);
491
492
  }
492
493
  if (!hasSdkInit) {
493
494
  warnings.push(`[apps][validate] Could not detect Playdrop SDK initialization for ${task.name}. We could not find playdrop.init() in the app files, so it might not work once uploaded.`);
@@ -85,7 +85,7 @@ export declare function buildListingCaptureFrameUrl({ webBase, currentUsername,
85
85
  export declare function resolveListingCaptureSceneId(width: number, height: number): 'listing-landscape' | 'listing-portrait';
86
86
  export declare function parseCaptureListingOptions(targetArg: string | undefined, options?: CaptureListingOptions): ParsedCaptureListingOptions;
87
87
  export declare function assertSupportedListingEnvironment(platform?: NodeJS.Platform, macosVersion?: string): void;
88
- export declare function resolveListingRecorderPath(baseDir?: string): string;
88
+ export declare function resolveListingRecorderPath(workerHome?: string): string;
89
89
  export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppSurface, useSurfacePrefix: boolean): {
90
90
  rawVideoPath: string;
91
91
  metadataPath: string;
@@ -59,6 +59,7 @@ const commandContext_1 = require("../commandContext");
59
59
  const http_1 = require("../http");
60
60
  const messages_1 = require("../messages");
61
61
  const playwright_1 = require("../playwright");
62
+ const recorderRelease_1 = require("../recorderRelease");
62
63
  const listingCaptureLease_1 = require("../listingCaptureLease");
63
64
  const dev_1 = require("./dev");
64
65
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
@@ -204,8 +205,8 @@ function commandExists(command) {
204
205
  async function ensureExecutableFile(filePath) {
205
206
  await (0, promises_1.access)(filePath);
206
207
  }
207
- function resolveListingRecorderPath(baseDir = __dirname) {
208
- return (0, node_path_1.resolve)(baseDir, '..', '..', '..', '..', 'clients', 'apple', 'playdrop-listing-recorder', '.build', 'release', 'playdrop-listing-recorder');
208
+ function resolveListingRecorderPath(workerHome) {
209
+ return (0, recorderRelease_1.resolveMacWorkerRecorderPath)(workerHome);
209
210
  }
210
211
  function buildTimestampFolder(date = new Date()) {
211
212
  const iso = date.toISOString();
@@ -755,7 +756,7 @@ async function ensureListingRecorderAvailable(recorderPath) {
755
756
  return true;
756
757
  }
757
758
  catch {
758
- (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture' });
759
+ (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Start the PlayDrop worker once on this Mac to install the recorder from the PlayDrop CDN.'], { command: 'project capture' });
759
760
  process.exitCode = 1;
760
761
  return false;
761
762
  }
@@ -651,6 +651,25 @@ function createDevRouterServer(initialPort = exports.DEV_ROUTER_PORT) {
651
651
  });
652
652
  return;
653
653
  }
654
+ if (pathname.startsWith('/sdk/') && (method === 'GET' || method === 'HEAD')) {
655
+ const assetsBase = process.env.ASSETS_BASE_URL?.trim();
656
+ if (!assetsBase) {
657
+ respondWithText(res, method, 500, 'ASSETS_BASE_URL is required to serve the development SDK.');
658
+ return;
659
+ }
660
+ let sdkUrl;
661
+ try {
662
+ sdkUrl = new URL(`${pathname}${requestUrl.search}`, `${assetsBase.replace(/\/$/, '')}/`);
663
+ }
664
+ catch {
665
+ respondWithText(res, method, 500, 'ASSETS_BASE_URL is invalid.');
666
+ return;
667
+ }
668
+ res.statusCode = 307;
669
+ res.setHeader('Location', sdkUrl.toString());
670
+ res.end();
671
+ return;
672
+ }
654
673
  if (pathname.startsWith(`${CONTROL_PREFIX}/apps/dev/`) && pathname.endsWith('/runtime-assets') && method === 'GET') {
655
674
  cleanupStaleRouterMounts();
656
675
  const segments = pathname.split('/').filter(Boolean);
@@ -6,9 +6,11 @@ type FeedbackSendOptions = {
6
6
  json?: boolean;
7
7
  };
8
8
  type FeedbackBrowseOptions = {
9
+ admin?: boolean;
9
10
  limit?: string | number;
10
11
  offset?: string | number;
11
12
  client?: string;
13
+ sinceHours?: string | number;
12
14
  userId?: string | number;
13
15
  json?: boolean;
14
16
  };