@playdrop/playdrop-cli 0.12.41 → 0.13.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.
Files changed (116) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/index.d.ts +3 -0
  3. package/dist/apps/index.js +3 -1
  4. package/dist/apps/loadCheck.d.ts +21 -1
  5. package/dist/apps/loadCheck.js +253 -1
  6. package/dist/apps/playtestContract.d.ts +2 -0
  7. package/dist/apps/playtestContract.js +77 -0
  8. package/dist/apps/staticHtml.d.ts +28 -0
  9. package/dist/apps/staticHtml.js +343 -0
  10. package/dist/apps/upload.d.ts +1 -0
  11. package/dist/apps/upload.js +17 -0
  12. package/dist/apps/validate.js +2 -1
  13. package/dist/commands/devServer.js +19 -0
  14. package/dist/commands/feedback.d.ts +2 -0
  15. package/dist/commands/feedback.js +44 -2
  16. package/dist/commands/upload.d.ts +24 -0
  17. package/dist/commands/upload.js +131 -60
  18. package/dist/commands/worker/instrument-evidence.js +1 -1
  19. package/dist/commands/worker/runtime.d.ts +14 -0
  20. package/dist/commands/worker/runtime.js +97 -8
  21. package/dist/commands/worker.d.ts +27 -2
  22. package/dist/commands/worker.js +688 -146
  23. package/dist/index.js +7 -0
  24. package/dist/listingPreflight.js +15 -4
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts +6 -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 +3 -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/apps.js +11 -0
  33. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +2 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  35. package/node_modules/@playdrop/api-client/dist/domains/me.js +11 -0
  36. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -3
  37. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  38. package/node_modules/@playdrop/api-client/dist/index.js +14 -2
  39. package/node_modules/@playdrop/config/client-meta.json +2 -2
  40. package/node_modules/@playdrop/config/dist/src/constants.d.ts +1 -0
  41. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  42. package/node_modules/@playdrop/config/dist/src/constants.js +1 -0
  43. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  44. package/node_modules/@playdrop/types/dist/api.d.ts +195 -4
  45. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  46. package/node_modules/@playdrop/types/dist/api.js +25 -2
  47. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts +4 -0
  48. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -1
  49. package/node_modules/@playdrop/types/dist/app-playtest-tape.js +56 -0
  50. package/node_modules/@playdrop/types/dist/version.d.ts +49 -1
  51. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  52. package/node_modules/entities/decode-codepoint.d.ts +19 -0
  53. package/node_modules/entities/decode-codepoint.d.ts.map +1 -0
  54. package/node_modules/entities/decode-codepoint.js +77 -0
  55. package/node_modules/entities/decode-codepoint.js.map +1 -0
  56. package/node_modules/entities/decode.d.ts +209 -0
  57. package/node_modules/entities/decode.d.ts.map +1 -0
  58. package/node_modules/entities/decode.js +511 -0
  59. package/node_modules/entities/decode.js.map +1 -0
  60. package/node_modules/entities/encode.d.ts +22 -0
  61. package/node_modules/entities/encode.d.ts.map +1 -0
  62. package/node_modules/entities/encode.js +73 -0
  63. package/node_modules/entities/encode.js.map +1 -0
  64. package/node_modules/entities/escape.d.ts +43 -0
  65. package/node_modules/entities/escape.d.ts.map +1 -0
  66. package/node_modules/entities/escape.js +121 -0
  67. package/node_modules/entities/escape.js.map +1 -0
  68. package/node_modules/entities/generated/decode-data-html.d.ts +2 -0
  69. package/node_modules/entities/generated/decode-data-html.d.ts.map +1 -0
  70. package/node_modules/entities/generated/decode-data-html.js +10 -0
  71. package/node_modules/entities/generated/decode-data-html.js.map +1 -0
  72. package/node_modules/entities/generated/decode-data-xml.d.ts +2 -0
  73. package/node_modules/entities/generated/decode-data-xml.d.ts.map +1 -0
  74. package/node_modules/entities/generated/decode-data-xml.js +10 -0
  75. package/node_modules/entities/generated/decode-data-xml.js.map +1 -0
  76. package/node_modules/entities/generated/encode-html.d.ts +8 -0
  77. package/node_modules/entities/generated/encode-html.d.ts.map +1 -0
  78. package/node_modules/entities/generated/encode-html.js +13 -0
  79. package/node_modules/entities/generated/encode-html.js.map +1 -0
  80. package/node_modules/entities/index.d.ts +96 -0
  81. package/node_modules/entities/index.d.ts.map +1 -0
  82. package/node_modules/entities/index.js +131 -0
  83. package/node_modules/entities/index.js.map +1 -0
  84. package/node_modules/entities/package.json +3 -0
  85. package/node_modules/parse5/common/doctype.d.ts +4 -0
  86. package/node_modules/parse5/common/doctype.js +118 -0
  87. package/node_modules/parse5/common/error-codes.d.ts +67 -0
  88. package/node_modules/parse5/common/error-codes.js +66 -0
  89. package/node_modules/parse5/common/foreign-content.d.ts +9 -0
  90. package/node_modules/parse5/common/foreign-content.js +237 -0
  91. package/node_modules/parse5/common/html.d.ts +289 -0
  92. package/node_modules/parse5/common/html.js +528 -0
  93. package/node_modules/parse5/common/token.d.ts +84 -0
  94. package/node_modules/parse5/common/token.js +24 -0
  95. package/node_modules/parse5/common/unicode.d.ts +42 -0
  96. package/node_modules/parse5/common/unicode.js +70 -0
  97. package/node_modules/parse5/index.d.ts +71 -0
  98. package/node_modules/parse5/index.js +54 -0
  99. package/node_modules/parse5/package.json +1 -0
  100. package/node_modules/parse5/parser/formatting-element-list.d.ts +36 -0
  101. package/node_modules/parse5/parser/formatting-element-list.js +114 -0
  102. package/node_modules/parse5/parser/index.d.ts +221 -0
  103. package/node_modules/parse5/parser/index.js +3240 -0
  104. package/node_modules/parse5/parser/open-element-stack.d.ts +53 -0
  105. package/node_modules/parse5/parser/open-element-stack.js +328 -0
  106. package/node_modules/parse5/serializer/index.d.ts +60 -0
  107. package/node_modules/parse5/serializer/index.js +172 -0
  108. package/node_modules/parse5/tokenizer/index.d.ts +247 -0
  109. package/node_modules/parse5/tokenizer/index.js +2714 -0
  110. package/node_modules/parse5/tokenizer/preprocessor.d.ts +36 -0
  111. package/node_modules/parse5/tokenizer/preprocessor.js +200 -0
  112. package/node_modules/parse5/tree-adapters/default.d.ts +84 -0
  113. package/node_modules/parse5/tree-adapters/default.js +176 -0
  114. package/node_modules/parse5/tree-adapters/interface.d.ts +255 -0
  115. package/node_modules/parse5/tree-adapters/interface.js +2 -0
  116. package/package.json +7 -1
@@ -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
+ }
@@ -23,6 +23,7 @@ export type AppUploadOptions = {
23
23
  agentTaskId?: number;
24
24
  playtestProof?: AgentTaskPlaytestProof;
25
25
  mediaCaptureRequired?: boolean;
26
+ machineCausalGateRequired?: boolean;
26
27
  captureSession?: TaskCaptureSession | null;
27
28
  };
28
29
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
@@ -9,6 +9,7 @@ const node_path_1 = require("node:path");
9
9
  const http_1 = require("../http");
10
10
  const loadCheck_1 = require("./loadCheck");
11
11
  const listingPreflight_1 = require("../listingPreflight");
12
+ const playtestContract_1 = require("./playtestContract");
12
13
  const EXTENSION_TO_MIME = {
13
14
  '.png': 'image/png',
14
15
  '.jpg': 'image/jpeg',
@@ -353,6 +354,22 @@ async function uploadAppVersion(client, task, artifacts, options) {
353
354
  await client.recordAppUploadMediaCapture(creatorUsername, task.name, sessionId, mediaCaptureProof);
354
355
  }
355
356
  if (task.hostingMode !== 'EXTERNAL'
357
+ && !task.externalUrl
358
+ && options?.machineCausalGateRequired) {
359
+ const teachingText = (0, playtestContract_1.assertPrimaryVerbTeachingParity)(task);
360
+ await (0, loadCheck_1.runUploadedHostedCausalCheck)({
361
+ client,
362
+ creatorUsername,
363
+ task,
364
+ sessionId,
365
+ teachingText,
366
+ timeoutMs: options.loadCheckTimeoutMs,
367
+ token: options?.token ?? null,
368
+ currentUser: options?.user ?? null,
369
+ captureSession: options?.captureSession,
370
+ });
371
+ }
372
+ else if (task.hostingMode !== 'EXTERNAL'
356
373
  && !task.externalUrl
357
374
  && options?.runStagedUploadLoadCheck) {
358
375
  const loadCheck = await (0, loadCheck_1.runUploadedHostedLoadCheck)({
@@ -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.`);
@@ -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
  };
@@ -16,7 +16,7 @@ const output_1 = require("../output");
16
16
  const clientInfo_1 = require("../clientInfo");
17
17
  const MAX_FEEDBACK_TITLE_LENGTH = 70;
18
18
  const MAX_FEEDBACK_COMMENT_LENGTH = 10000;
19
- const FEEDBACK_CATEGORY_SET = new Set(types_1.FEEDBACK_CATEGORY_VALUES);
19
+ const FEEDBACK_CATEGORY_SET = new Set(types_1.MANUAL_FEEDBACK_CATEGORY_VALUES);
20
20
  function parsePositiveInteger(raw, label, command, options = {}) {
21
21
  if (raw === undefined) {
22
22
  return null;
@@ -32,7 +32,7 @@ function parsePositiveInteger(raw, label, command, options = {}) {
32
32
  function parseFeedbackCategory(raw, command) {
33
33
  const normalized = typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : 'other';
34
34
  if (!FEEDBACK_CATEGORY_SET.has(normalized)) {
35
- (0, messages_1.printErrorWithHelp)(`Feedback category must be one of: ${types_1.FEEDBACK_CATEGORY_VALUES.join(', ')}.`, [], { command });
35
+ (0, messages_1.printErrorWithHelp)(`Feedback category must be one of: ${types_1.MANUAL_FEEDBACK_CATEGORY_VALUES.join(', ')}.`, [], { command });
36
36
  process.exitCode = 1;
37
37
  return null;
38
38
  }
@@ -137,8 +137,50 @@ async function browseFeedback(options = {}) {
137
137
  process.exitCode = 1;
138
138
  return;
139
139
  }
140
+ const sinceHours = options.sinceHours !== undefined
141
+ ? parsePositiveInteger(options.sinceHours, '--since-hours', 'feedback browse')
142
+ : null;
143
+ if (options.sinceHours !== undefined && sinceHours === null) {
144
+ return;
145
+ }
140
146
  await (0, commandContext_1.withEnvironment)('feedback browse', 'Browsing feedback', async ({ client }) => {
141
147
  try {
148
+ if (options.admin) {
149
+ const cutoff = sinceHours === null ? null : Date.now() - sinceHours * 60 * 60 * 1000;
150
+ const items = [];
151
+ let nextOffset = offset ?? 0;
152
+ let hasMore = true;
153
+ while (hasMore && (limit === null || items.length < limit)) {
154
+ const page = await client.fetchAdminFeedback({ limit: 100, offset: nextOffset });
155
+ for (const item of page.feedback ?? []) {
156
+ if (cutoff !== null && new Date(item.createdAt).getTime() < cutoff) {
157
+ hasMore = false;
158
+ break;
159
+ }
160
+ items.push(item);
161
+ if (limit !== null && items.length >= limit) {
162
+ break;
163
+ }
164
+ }
165
+ nextOffset += page.feedback?.length ?? 0;
166
+ hasMore = hasMore && Boolean(page.pagination?.hasMore) && (page.feedback?.length ?? 0) > 0;
167
+ }
168
+ if (options.json) {
169
+ (0, output_1.printJson)({ items, sinceHours, count: items.length });
170
+ return;
171
+ }
172
+ if (items.length === 0) {
173
+ console.log('No feedback entries found.');
174
+ return;
175
+ }
176
+ console.log('Feedback:\n');
177
+ for (const item of items) {
178
+ console.log(`#${item.id} | ${item.category} | ${item.client} | ${(0, output_1.formatTimestamp)(item.createdAt)}`);
179
+ console.log(` ${item.title}`);
180
+ console.log(` ${item.comment}`);
181
+ }
182
+ return;
183
+ }
142
184
  const response = await client.listFeedback({
143
185
  limit: limit ?? undefined,
144
186
  offset: offset ?? undefined,
@@ -5,6 +5,7 @@ export type UploadCommandOptions = {
5
5
  env?: string;
6
6
  skipReview?: boolean;
7
7
  clearTags?: boolean;
8
+ json?: boolean;
8
9
  };
9
10
  export declare function resolveUploadOwner(actorUsername: string | null | undefined, workspaceOwnerUsername: string | null | undefined): string;
10
11
  export declare function resolveDefaultUploadCreator(owner: string | null | undefined, actorUsername: string | null | undefined): string;
@@ -35,6 +36,28 @@ export type WorkerAppPublishResult = {
35
36
  creatorUsername: string;
36
37
  warnings: string[];
37
38
  };
39
+ export type StaticHtmlPublishInput = {
40
+ client: ApiClient;
41
+ projectDir: string;
42
+ creatorUsername: string;
43
+ primarySurface: 'DESKTOP' | 'MOBILE_LANDSCAPE' | 'MOBILE_PORTRAIT';
44
+ apiBase: string;
45
+ webBase?: string | null;
46
+ token: string;
47
+ user: UserResponse;
48
+ taskId?: number;
49
+ taskToken?: string;
50
+ claimedAppName?: string | null;
51
+ devRouterPort?: number;
52
+ };
53
+ export type StaticHtmlPublishResult = WorkerAppPublishResult & {
54
+ sourceHash: string;
55
+ runtimeHash: string;
56
+ playUrl: string;
57
+ sourceContainsInjectedSdk: false;
58
+ runtimeContainsInjectedSdk: true;
59
+ hostedLoadCheckPassed: true;
60
+ };
38
61
  export type WorkerPlaydropAssetRequirement = 'PACK' | 'ASSET_OR_PACK';
39
62
  export declare function resolveWorkerPlaydropAssetRequirement(value: unknown): WorkerPlaydropAssetRequirement | null;
40
63
  export declare function assertAgentNewGamePlaytestContract(task: AppTask): void;
@@ -50,3 +73,4 @@ export declare function assertWorkerAppLocalUploadPreflight(input: {
50
73
  creatorRequest?: string | null;
51
74
  }): Promise<AgentTaskPlaytestProof | null>;
52
75
  export declare function publishWorkerAppProject(input: WorkerAppPublishInput): Promise<WorkerAppPublishResult>;
76
+ export declare function publishStaticHtmlProject(input: StaticHtmlPublishInput): Promise<StaticHtmlPublishResult>;