@playdrop/playdrop-cli 0.14.6 → 0.14.8

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 (57) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/upload.js +1 -0
  3. package/dist/catalogue.d.ts +3 -1
  4. package/dist/catalogue.js +10 -0
  5. package/dist/commands/dev.js +9 -0
  6. package/dist/commands/devRuntimeAssets.d.ts +2 -1
  7. package/dist/commands/devRuntimeAssets.js +3 -0
  8. package/dist/commands/notes.d.ts +7 -0
  9. package/dist/commands/notes.js +107 -0
  10. package/dist/commands/tweaks.d.ts +6 -0
  11. package/dist/commands/tweaks.js +93 -0
  12. package/dist/commands/validate.js +14 -0
  13. package/dist/commands/worker.d.ts +1 -1
  14. package/dist/commands/worker.js +29 -8
  15. package/dist/index.js +40 -1
  16. package/dist/listingPreflight.d.ts +4 -0
  17. package/dist/listingPreflight.js +17 -0
  18. package/dist/tweakStaleness.d.ts +7 -0
  19. package/dist/tweakStaleness.js +36 -0
  20. package/node_modules/@playdrop/api-client/dist/client.d.ts +25 -4
  21. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/client.js +40 -0
  23. package/node_modules/@playdrop/api-client/dist/core/request.d.ts +1 -0
  24. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/core/request.js +2 -1
  26. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/admin.js +27 -0
  29. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +18 -4
  30. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/apps.js +76 -6
  32. package/node_modules/@playdrop/api-client/dist/index.d.ts +35 -4
  33. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/index.js +79 -4
  35. package/node_modules/@playdrop/config/client-meta.json +2 -2
  36. package/node_modules/@playdrop/config/dist/src/game-arena.d.ts +2 -2
  37. package/node_modules/@playdrop/config/dist/src/game-arena.js +2 -2
  38. package/node_modules/@playdrop/config/dist/test/game-arena.test.js +4 -3
  39. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  40. package/node_modules/@playdrop/types/dist/api.d.ts +76 -2
  41. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  42. package/node_modules/@playdrop/types/dist/api.js +39 -2
  43. package/node_modules/@playdrop/types/dist/app-review.d.ts +11 -0
  44. package/node_modules/@playdrop/types/dist/app-review.d.ts.map +1 -0
  45. package/node_modules/@playdrop/types/dist/app-review.js +56 -0
  46. package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
  47. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  48. package/node_modules/@playdrop/types/dist/index.js +6 -0
  49. package/node_modules/@playdrop/types/dist/playtest-notes.d.ts +69 -0
  50. package/node_modules/@playdrop/types/dist/playtest-notes.d.ts.map +1 -0
  51. package/node_modules/@playdrop/types/dist/playtest-notes.js +30 -0
  52. package/node_modules/@playdrop/types/dist/tweaks.d.ts +48 -0
  53. package/node_modules/@playdrop/types/dist/tweaks.d.ts.map +1 -0
  54. package/node_modules/@playdrop/types/dist/tweaks.js +156 -0
  55. package/node_modules/@playdrop/types/dist/version.d.ts +17 -0
  56. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  57. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.14.6",
2
+ "version": "0.14.8",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.14.6",
4
+ "runtimeSdkVersion": "0.14.8",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -245,6 +245,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
245
245
  usesPacks: task.uses.packs,
246
246
  tags: task.tags,
247
247
  design: task.design,
248
+ tweaks: task.tweaks,
248
249
  clearTags: options?.clearTags,
249
250
  surfaceTargets: task.surfaceTargets,
250
251
  primarySurface: task.primarySurface,
@@ -1,4 +1,4 @@
1
- import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppListingMediaMetadata, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppSurface, type AppType, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
1
+ import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppListingMediaMetadata, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppSurface, type AppType, type AppTweaksDeclaration, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
2
2
  import { type AssetSpecSupportDeclaration } from './assetSpecs';
3
3
  export type CatalogueJson = {
4
4
  apps?: Array<Record<string, unknown>>;
@@ -54,6 +54,7 @@ export type AppCatalogueEntry = {
54
54
  primarySurface?: string | null;
55
55
  playtestTapes?: unknown;
56
56
  design?: unknown;
57
+ tweaks?: unknown;
57
58
  version?: string;
58
59
  releaseNotes?: string;
59
60
  visibility?: string;
@@ -196,6 +197,7 @@ export type AppTask = {
196
197
  primarySurface?: AppSurface;
197
198
  playtestTapes?: AppPlaytestTapes;
198
199
  design?: AppVersionDesign;
200
+ tweaks?: AppTweaksDeclaration;
199
201
  missingMetadata: string[];
200
202
  version?: string;
201
203
  releaseNotes?: string;
package/dist/catalogue.js CHANGED
@@ -1375,6 +1375,15 @@ function buildAppTasks(rootDir, catalogues, options) {
1375
1375
  }
1376
1376
  design = parsedDesign.value;
1377
1377
  }
1378
+ let tweaks;
1379
+ if (Object.prototype.hasOwnProperty.call(entry, 'tweaks')) {
1380
+ const parsedTweaks = (0, types_1.normalizeAppTweaks)(entry.tweaks);
1381
+ if (!parsedTweaks.ok) {
1382
+ errors.push(`[${label}] App "${rawName}" ${parsedTweaks.error}`);
1383
+ continue;
1384
+ }
1385
+ tweaks = parsedTweaks.value;
1386
+ }
1378
1387
  const errorCountBeforeTagMetadata = errors.length;
1379
1388
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1380
1389
  const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
@@ -1579,6 +1588,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1579
1588
  primarySurface: metadata.primarySurface,
1580
1589
  playtestTapes: metadata.playtestTapes,
1581
1590
  design,
1591
+ tweaks,
1582
1592
  missingMetadata: metadata.missingFields,
1583
1593
  // Versioning fields
1584
1594
  version,
@@ -16,6 +16,7 @@ const http_1 = require("../http");
16
16
  const messages_1 = require("../messages");
17
17
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
18
18
  const devServer_1 = require("./devServer");
19
+ const tweakStaleness_1 = require("../tweakStaleness");
19
20
  const devShared_1 = require("./devShared");
20
21
  function formatDevRuntimeAssetManifestFailure(error) {
21
22
  const rawMessage = error instanceof Error ? error.message : String(error ?? 'unknown_error');
@@ -301,6 +302,14 @@ async function dev(targetArg, appOption, devOptions = {}) {
301
302
  });
302
303
  let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
303
304
  if (taskLookup.task) {
305
+ try {
306
+ await (0, tweakStaleness_1.fetchAndAssertTweakState)(client, currentUsername, taskLookup.task);
307
+ }
308
+ catch (error) {
309
+ (0, messages_1.printErrorWithHelp)(error instanceof Error ? error.message : 'Could not verify the current tweak state.', [], { command: 'project dev' });
310
+ process.exitCode = 1;
311
+ return;
312
+ }
304
313
  try {
305
314
  runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
306
315
  client,
@@ -1,5 +1,5 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import { type AppAuthMode, type AppControllerMode, type AppMetadataAssetSpecSupport, type AppRuntimeAssetsResponse, type AppSurface, type AppType } from '@playdrop/types';
2
+ import { type AppAuthMode, type AppControllerMode, type AppMetadataAssetSpecSupport, type AppRuntimeAssetsResponse, type AppSurface, type AppType, type TweakValues } from '@playdrop/types';
3
3
  import type { AppTask } from '../catalogue';
4
4
  export type LocalOwnedRuntimeAssetFileRecord = {
5
5
  absolutePath: string;
@@ -14,6 +14,7 @@ export type LocalDevAppMetadata = {
14
14
  previewable: boolean;
15
15
  assetSpecSupport: AppMetadataAssetSpecSupport[];
16
16
  version: string | null;
17
+ tweakValues?: TweakValues;
17
18
  };
18
19
  export type DevRuntimeAssetsResponse = AppRuntimeAssetsResponse & {
19
20
  localAppMetadata: LocalDevAppMetadata;
@@ -124,6 +124,9 @@ function buildLocalAppMetadata(task) {
124
124
  capabilities: Array.from(entry.capabilities),
125
125
  })),
126
126
  version: typeof task.version === 'string' && task.version.trim().length > 0 ? task.version.trim() : null,
127
+ ...(task.tweaks && task.tweaks.removed !== true
128
+ ? { tweakValues: { ...task.tweaks.defaults } }
129
+ : {}),
127
130
  };
128
131
  }
129
132
  function assertCustomAssetVersionSupported(task, assetRef, version) {
@@ -0,0 +1,7 @@
1
+ export declare function browseNotes(rawRef: string | undefined, options?: {
2
+ json?: boolean;
3
+ output?: string;
4
+ }): Promise<void>;
5
+ export declare function clearNotes(rawRef: string | undefined, rawIds: string[], options?: {
6
+ json?: boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.browseNotes = browseNotes;
4
+ exports.clearNotes = clearNotes;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const types_1 = require("@playdrop/types");
8
+ const commandContext_1 = require("../commandContext");
9
+ const errors_1 = require("../errors");
10
+ const messages_1 = require("../messages");
11
+ const output_1 = require("../output");
12
+ function notesApiMessage(error) {
13
+ if (error.code === 'playtest_notes_changed') {
14
+ const missingIds = Array.isArray(error.details?.['missingIds']) ? error.details['missingIds'] : [];
15
+ return {
16
+ problem: `The note inbox changed before it could be cleared${missingIds.length > 0 ? `: ${missingIds.join(', ')}` : '.'}`,
17
+ suggestions: ['Run notes browse again, then clear the current IDs.'],
18
+ };
19
+ }
20
+ return { problem: `PlayDrop rejected the notes request: ${error.code ?? error.message} (HTTP ${error.status}).` };
21
+ }
22
+ function parseAppVersionRef(raw, command) {
23
+ const ref = (0, types_1.parseContentVersionRef)(raw ?? '');
24
+ if (!ref || ref.kind !== 'app') {
25
+ (0, messages_1.printErrorWithHelp)('An exact canonical app-version ref is required.', ['Use app:creator/name@x.y.z.', `Example: playdrop ${command} app:olivier/platformer@1.4.0`], { command });
26
+ process.exitCode = 1;
27
+ return null;
28
+ }
29
+ return ref;
30
+ }
31
+ async function browseNotes(rawRef, options = {}) {
32
+ const ref = parseAppVersionRef(rawRef, 'notes browse');
33
+ if (!ref)
34
+ return;
35
+ await (0, commandContext_1.withEnvironment)('notes browse', 'Reading playtest notes', async ({ client }) => {
36
+ try {
37
+ const response = await client.listPlaytestNotes(ref.creatorUsername, ref.name, ref.version);
38
+ let output = response;
39
+ if (options.output) {
40
+ const outputDirectory = (0, node_path_1.resolve)(options.output);
41
+ (0, node_fs_1.mkdirSync)(outputDirectory, { recursive: true });
42
+ const notes = [];
43
+ for (const note of response.notes) {
44
+ if (!note.attachment) {
45
+ notes.push(note);
46
+ continue;
47
+ }
48
+ const localPath = (0, node_path_1.resolve)(outputDirectory, `${note.id}-${note.attachment.fileName}`);
49
+ const blob = await client.downloadPlaytestNoteAttachment(ref.creatorUsername, ref.name, ref.version, note.id);
50
+ (0, node_fs_1.writeFileSync)(localPath, Buffer.from(await blob.arrayBuffer()));
51
+ notes.push({ ...note, localPath });
52
+ }
53
+ output = { notes };
54
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(outputDirectory, 'notes.json'), `${JSON.stringify(output, null, 2)}\n`, 'utf8');
55
+ }
56
+ if (options.json) {
57
+ (0, output_1.printJson)(output);
58
+ return;
59
+ }
60
+ if (options.output) {
61
+ (0, output_1.printSuccess)(`Materialized ${output.notes.length} playtest notes in ${(0, node_path_1.resolve)(options.output)}.`);
62
+ return;
63
+ }
64
+ if (output.notes.length === 0) {
65
+ console.log('No pending playtest notes.');
66
+ return;
67
+ }
68
+ for (const note of output.notes) {
69
+ const summary = note.kind === 'TEXT'
70
+ ? note.text
71
+ : note.kind === 'ASSET'
72
+ ? `${note.assetRef}: ${note.comment}`
73
+ : note.attachment?.fileName;
74
+ console.log(`${note.id}\t${note.kind}\tv${note.authoredVersion}\t${summary ?? ''}`);
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (!(0, errors_1.handleCommandFailure)(error, 'notes browse', 'Read playtest notes', { apiMessage: notesApiMessage }))
79
+ throw error;
80
+ }
81
+ });
82
+ }
83
+ async function clearNotes(rawRef, rawIds, options = {}) {
84
+ const ref = parseAppVersionRef(rawRef, 'notes clear');
85
+ if (!ref)
86
+ return;
87
+ const ids = rawIds.map(Number);
88
+ if (ids.length === 0 || ids.some((id) => !Number.isInteger(id) || id <= 0) || new Set(ids).size !== ids.length) {
89
+ (0, messages_1.printErrorWithHelp)('One or more explicit note IDs are required.', ['Example: playdrop notes clear app:olivier/platformer@1.4.0 42 43'], { command: 'notes clear' });
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ await (0, commandContext_1.withEnvironment)('notes clear', 'Clearing playtest notes', async ({ client }) => {
94
+ try {
95
+ const result = await client.clearPlaytestNotes(ref.creatorUsername, ref.name, ref.version, ids);
96
+ if (options.json) {
97
+ (0, output_1.printJson)(result);
98
+ return;
99
+ }
100
+ (0, output_1.printSuccess)(`Cleared ${result.cleared.length} playtest notes. ${result.count} pending.`);
101
+ }
102
+ catch (error) {
103
+ if (!(0, errors_1.handleCommandFailure)(error, 'notes clear', 'Clear playtest notes', { apiMessage: notesApiMessage }))
104
+ throw error;
105
+ }
106
+ });
107
+ }
@@ -0,0 +1,6 @@
1
+ type TweaksOptions = {
2
+ json?: boolean;
3
+ };
4
+ export declare function getTweaks(rawRef: string | undefined, options?: TweaksOptions): Promise<void>;
5
+ export declare function replaceTweaks(rawRef: string | undefined, file: string | undefined, options?: TweaksOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTweaks = getTweaks;
4
+ exports.replaceTweaks = replaceTweaks;
5
+ const node_fs_1 = require("node:fs");
6
+ const types_1 = require("@playdrop/types");
7
+ const commandContext_1 = require("../commandContext");
8
+ const errors_1 = require("../errors");
9
+ const messages_1 = require("../messages");
10
+ const output_1 = require("../output");
11
+ function tweakApiMessage(error) {
12
+ if (error.code === 'creator_latest_private_version_required') {
13
+ const latestVersion = typeof error.details?.['latestVersion'] === 'string' ? error.details['latestVersion'] : null;
14
+ const latestId = typeof error.details?.['latestId'] === 'string' ? error.details['latestId'] : null;
15
+ const latestVisibility = typeof error.details?.['latestVisibility'] === 'string' ? error.details['latestVisibility'] : null;
16
+ return {
17
+ problem: latestVersion && latestVisibility === 'PUBLIC'
18
+ ? `The latest tweak base, ${latestVersion}, is live and cannot be edited.`
19
+ : latestVersion
20
+ ? `Tweaks can only be changed on the latest editable version, ${latestVersion}.`
21
+ : 'This app has no editable tweak base version.',
22
+ suggestions: latestVersion && latestVisibility === 'PUBLIC'
23
+ ? [`Create a new private version based on ${latestId ?? 'the latest tweak state'}, then tune that version.`]
24
+ : latestId ? [`Latest tweak state: ${latestId}`] : [],
25
+ };
26
+ }
27
+ return { problem: `PlayDrop rejected the tweak request: ${error.code ?? error.message} (HTTP ${error.status}).` };
28
+ }
29
+ function parseAppVersionRef(raw, command) {
30
+ const ref = (0, types_1.parseContentVersionRef)(raw ?? '');
31
+ if (!ref || ref.kind !== 'app') {
32
+ (0, messages_1.printErrorWithHelp)('An exact canonical app-version ref is required.', ['Use app:creator/name@x.y.z.', `Example: playdrop ${command} app:olivier/platformer@1.4.0`], { command });
33
+ process.exitCode = 1;
34
+ return null;
35
+ }
36
+ return ref;
37
+ }
38
+ async function getTweaks(rawRef, options = {}) {
39
+ const ref = parseAppVersionRef(rawRef, 'tweaks get');
40
+ if (!ref)
41
+ return;
42
+ await (0, commandContext_1.withEnvironment)('tweaks get', 'Reading tweaks', async ({ client }) => {
43
+ try {
44
+ const snapshot = await client.getAppTweaks(ref.creatorUsername, ref.name, ref.version);
45
+ if (options.json) {
46
+ (0, output_1.printJson)(snapshot);
47
+ return;
48
+ }
49
+ console.log(`Tweak state: ${snapshot.id}`);
50
+ console.log(JSON.stringify(snapshot.values, null, 2));
51
+ }
52
+ catch (error) {
53
+ if (!(0, errors_1.handleCommandFailure)(error, 'tweaks get', 'Read tweaks', { apiMessage: tweakApiMessage }))
54
+ throw error;
55
+ }
56
+ });
57
+ }
58
+ async function replaceTweaks(rawRef, file, options = {}) {
59
+ const ref = parseAppVersionRef(rawRef, 'tweaks replace');
60
+ if (!ref)
61
+ return;
62
+ if (!file?.trim()) {
63
+ (0, messages_1.printErrorWithHelp)('A JSON file is required.', ['Pass --file <path>.'], { command: 'tweaks replace' });
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ let values;
68
+ try {
69
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
70
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
71
+ throw new Error('not_object');
72
+ values = parsed;
73
+ }
74
+ catch {
75
+ (0, messages_1.printErrorWithHelp)(`Could not read a JSON object from ${file}.`, [], { command: 'tweaks replace' });
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ await (0, commandContext_1.withEnvironment)('tweaks replace', 'Replacing tweaks', async ({ client }) => {
80
+ try {
81
+ const snapshot = await client.replaceAppTweaks(ref.creatorUsername, ref.name, ref.version, values);
82
+ if (options.json) {
83
+ (0, output_1.printJson)(snapshot);
84
+ return;
85
+ }
86
+ (0, output_1.printSuccess)(`Tweaks replaced. New state: ${snapshot.id}`);
87
+ }
88
+ catch (error) {
89
+ if (!(0, errors_1.handleCommandFailure)(error, 'tweaks replace', 'Replace tweaks', { apiMessage: tweakApiMessage }))
90
+ throw error;
91
+ }
92
+ });
93
+ }
@@ -9,6 +9,7 @@ const externalAssetPackValidation_1 = require("../externalAssetPackValidation");
9
9
  const taskSelection_1 = require("../taskSelection");
10
10
  const taskUtils_1 = require("../taskUtils");
11
11
  const uploadLog_1 = require("../uploadLog");
12
+ const tweakStaleness_1 = require("../tweakStaleness");
12
13
  const listingPreflight_1 = require("../listingPreflight");
13
14
  const upload_content_1 = require("./upload-content");
14
15
  const upload_1 = require("./upload");
@@ -189,11 +190,24 @@ async function validate(pathOrName, options = {}) {
189
190
  const assetSpecTasks = tasks.filter((task) => task.kind === 'asset-spec');
190
191
  const assetSpecLookups = buildLocalAssetSpecLookups(assetSpecTasks);
191
192
  const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
193
+ const tweakValidationContext = await loadAuthenticatedValidationContext(workspacePath, options.env);
192
194
  for (const task of sortedTasks) {
193
195
  const entityId = task.name;
194
196
  try {
195
197
  if (task.kind === 'app') {
196
198
  const workerContext = (0, listingPreflight_1.readWorkerUploadPreflightContext)(task.projectDir);
199
+ if (tweakValidationContext) {
200
+ const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, tweakValidationContext.username, tweakValidationContext.currentUserRole);
201
+ if (creatorResult.creatorTargetError)
202
+ throw new Error(creatorResult.creatorTargetError);
203
+ await (0, tweakStaleness_1.fetchAndAssertTweakState)(tweakValidationContext.client, creatorResult.taskCreator, task, { allowAppNotFound: true });
204
+ }
205
+ else if (workerContext?.tweaks) {
206
+ (0, tweakStaleness_1.assertTweakStateMatches)(task, workerContext.tweaks);
207
+ }
208
+ else if (task.tweaks !== undefined) {
209
+ warnings.add(`App "${task.name}" tweak basedOn was validated locally; remote staleness was not checked.`);
210
+ }
197
211
  if (workerContext) {
198
212
  await (0, upload_1.assertWorkerAppLocalUploadPreflight)({
199
213
  task,
@@ -346,7 +346,7 @@ type WorkerRuntimeState = {
346
346
  target: AgentExecutionTarget;
347
347
  startedAt: string;
348
348
  };
349
- export type WorkerSupervisorType = 'app' | 'terminal';
349
+ export type WorkerSupervisorType = 'app' | 'launchagent' | 'terminal';
350
350
  export type WorkerSupervisorLockOwner = {
351
351
  schemaVersion: 1;
352
352
  supervisorType: WorkerSupervisorType;
@@ -151,7 +151,7 @@ const CLAIM_BACKOFF_JITTER_MS = 500;
151
151
  const FAIL_REPORT_RETRY_DELAY_MS = 2000;
152
152
  const WORKER_TRANSCRIPT_CHUNK_BATCH_SIZE = 100;
153
153
  const SLACK_API_BASE_URL = 'https://slack.com/api';
154
- const WORKER_WRAPPER_CONTRACT_VERSION = 2;
154
+ const WORKER_WRAPPER_CONTRACT_VERSION = 3;
155
155
  const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_REVIEW', 'GAME_EVAL'];
156
156
  const GAME_CREATION_TASK_KINDS = new Set(['NEW_GAME', 'STATIC_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
157
157
  const AGENT_MANAGED_UPLOAD_TASK_KINDS = new Set(['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE']);
@@ -3238,8 +3238,8 @@ function resolveWorkerStateDir() {
3238
3238
  }
3239
3239
  function normalizeWorkerSupervisorType(value) {
3240
3240
  const normalized = value?.trim().toLowerCase() || 'terminal';
3241
- if (normalized !== 'app' && normalized !== 'terminal') {
3242
- throw new Error(`worker_supervisor_type_invalid: expected app or terminal, received ${value ?? ''}.`);
3241
+ if (normalized !== 'app' && normalized !== 'launchagent' && normalized !== 'terminal') {
3242
+ throw new Error(`worker_supervisor_type_invalid: expected app, launchagent, or terminal, received ${value ?? ''}.`);
3243
3243
  }
3244
3244
  return normalized;
3245
3245
  }
@@ -3299,7 +3299,7 @@ function readWorkerSupervisorLockOwner(file) {
3299
3299
  throw new Error(`worker_supervisor_lock_invalid: ${file} does not contain valid owner metadata.`);
3300
3300
  }
3301
3301
  if (parsed.schemaVersion !== 1
3302
- || (parsed.supervisorType !== 'app' && parsed.supervisorType !== 'terminal')
3302
+ || (parsed.supervisorType !== 'app' && parsed.supervisorType !== 'launchagent' && parsed.supervisorType !== 'terminal')
3303
3303
  || !Number.isInteger(parsed.pid)
3304
3304
  || (parsed.pid ?? 0) <= 0
3305
3305
  || typeof parsed.processStartIdentity !== 'string'
@@ -3323,7 +3323,11 @@ function workerSupervisorOwnerIsLive(owner) {
3323
3323
  return currentIdentity === owner.processStartIdentity;
3324
3324
  }
3325
3325
  function workerSupervisorHolderDescription(owner) {
3326
- const supervisor = owner.supervisorType === 'app' ? 'Local Agent app' : 'terminal';
3326
+ const supervisor = owner.supervisorType === 'app'
3327
+ ? 'Local Agent app'
3328
+ : owner.supervisorType === 'launchagent'
3329
+ ? 'LaunchAgent'
3330
+ : 'terminal';
3327
3331
  return `${supervisor} worker "${owner.workerName}" (PID ${owner.pid})`;
3328
3332
  }
3329
3333
  function workerSupervisorOwnersMatch(left, right) {
@@ -3384,7 +3388,12 @@ function acquireWorkerSupervisorLock(input) {
3384
3388
  }
3385
3389
  const holder = readWorkerSupervisorLockOwner(file);
3386
3390
  if (workerSupervisorOwnerIsLive(holder)) {
3387
- throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(holder)} already owns ${file}. Stop that worker before starting ${input.supervisorType === 'app' ? 'the Local Agent app worker' : 'a terminal worker'}.`);
3391
+ const requestedSupervisor = input.supervisorType === 'app'
3392
+ ? 'the Local Agent app worker'
3393
+ : input.supervisorType === 'launchagent'
3394
+ ? 'the LaunchAgent worker'
3395
+ : 'a terminal worker';
3396
+ throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(holder)} already owns ${file}. Stop that worker before starting ${requestedSupervisor}.`);
3388
3397
  }
3389
3398
  removeStaleWorkerSupervisorLockIfUnchanged(file, holder);
3390
3399
  }
@@ -3885,7 +3894,7 @@ if [ "$TARGET_UPDATE_FAILED" -eq 0 ]; then
3885
3894
  rm -f "$FAILURE_FILE"
3886
3895
  fi
3887
3896
 
3888
- exec /usr/bin/caffeinate -dimsu playdrop worker start --env ${shellQuote(input.env)} --name ${shellQuote(input.workerName)}
3897
+ exec /usr/bin/caffeinate -dimsu playdrop worker start --env ${shellQuote(input.env)} --name ${shellQuote(input.workerName)} --supervisor launchagent
3889
3898
  `;
3890
3899
  await (0, promises_1.writeFile)(wrapperPath, script, 'utf8');
3891
3900
  await (0, promises_1.chmod)(wrapperPath, 0o755);
@@ -4191,6 +4200,19 @@ async function startWorker(options = {}) {
4191
4200
  throw new Error(`worker_update_policy_target_mismatch: expected ${target}, received ${initialWorkerPolicy.executionTarget}.`);
4192
4201
  }
4193
4202
  const workerName = options.name?.trim() || `${username} - ${node_os_1.default.hostname()}`;
4203
+ const workerKey = (0, config_1.getOrCreateWorkerKey)();
4204
+ const runningWrapperVersion = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION', WORKER_WRAPPER_CONTRACT_VERSION);
4205
+ if (target === 'FIRST_PARTY'
4206
+ && node_process_1.default.platform === 'darwin'
4207
+ && options.env?.trim()
4208
+ && node_process_1.default.env.PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION
4209
+ && runningWrapperVersion < WORKER_WRAPPER_CONTRACT_VERSION) {
4210
+ verifyNpmGlobalInstallPreconditions();
4211
+ const { wrapperPath } = await writeManagedWorkerWrapper({ env, workerName, workerKey });
4212
+ await writeManagedWorkerLaunchAgent({ env, workerName, wrapperPath });
4213
+ console.log(`Worker repaired LaunchAgent wrapper contract ${runningWrapperVersion} -> ${WORKER_WRAPPER_CONTRACT_VERSION}. Restarting under LaunchAgent ownership.`);
4214
+ return;
4215
+ }
4194
4216
  const supervisorType = normalizeWorkerSupervisorType(options.supervisor);
4195
4217
  const supervisorLock = acquireWorkerSupervisorLock({ supervisorType, workerName });
4196
4218
  try {
@@ -4221,7 +4243,6 @@ async function startWorker(options = {}) {
4221
4243
  chromiumInstalled: true,
4222
4244
  ...(local.playwright.executablePath ? { executablePath: local.playwright.executablePath } : {}),
4223
4245
  };
4224
- const workerKey = (0, config_1.getOrCreateWorkerKey)();
4225
4246
  const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
4226
4247
  let capabilities = local.capabilities;
4227
4248
  let lastCapabilityRefreshAt = Date.now();
package/dist/index.js CHANGED
@@ -44,6 +44,8 @@ const feedback_1 = require("./commands/feedback");
44
44
  const changelog_1 = require("./commands/changelog");
45
45
  const publicPages_1 = require("./commands/publicPages");
46
46
  const upgrade_1 = require("./commands/upgrade");
47
+ const tweaks_1 = require("./commands/tweaks");
48
+ const notes_1 = require("./commands/notes");
47
49
  const worker_1 = require("./commands/worker");
48
50
  const messages_1 = require("./messages");
49
51
  // Restricted shim: inside a worker task workspace only task-scoped commands,
@@ -177,7 +179,7 @@ worker
177
179
  .option('--env <env>', 'Resolve this command against one logged-in environment')
178
180
  .option('--once', 'Process at most one task then exit')
179
181
  .option('--name <name>', 'Worker display name override')
180
- .option('--supervisor <type>', 'Worker supervisor type: app or terminal', 'terminal')
182
+ .option('--supervisor <type>', 'Worker supervisor type: app, launchagent, or terminal', 'terminal')
181
183
  .option('--supervise-stdin', 'Exit with a bounded shutdown when stdin closes')
182
184
  .option('--dev-plugin-working-tree <path>', 'DEV ONLY: stage task plugin files from this working tree')
183
185
  .action(async (opts) => {
@@ -577,6 +579,43 @@ versions
577
579
  .action(async (ref, opts) => {
578
580
  await (0, versionsBrowse_1.browseVersions)(ref, opts);
579
581
  });
582
+ const tweaks = program.command('tweaks').description('Read and replace version-scoped game tweaks');
583
+ tweaks
584
+ .command('get')
585
+ .description('Read the current tweak state for an exact app version')
586
+ .argument('<ref>', 'Exact ref: app:creator/name@x.y.z')
587
+ .option('--json', 'Output JSON')
588
+ .action(async (ref, options) => {
589
+ await (0, tweaks_1.getTweaks)(ref, options);
590
+ });
591
+ tweaks
592
+ .command('replace')
593
+ .description('Replace tweaks on the latest private app version')
594
+ .argument('<ref>', 'Exact ref: app:creator/name@x.y.z')
595
+ .requiredOption('--file <path>', 'JSON object containing the complete tweak values')
596
+ .option('--json', 'Output JSON')
597
+ .action(async (ref, options) => {
598
+ await (0, tweaks_1.replaceTweaks)(ref, options.file, options);
599
+ });
600
+ const notes = program.command('notes').description('Read and clear pending creator playtest notes');
601
+ notes
602
+ .command('browse')
603
+ .description('Browse pending playtest notes for an app')
604
+ .argument('<ref>', 'Exact ref: app:creator/name@x.y.z')
605
+ .option('--json', 'Output JSON')
606
+ .option('--output <directory>', 'Materialize attachments and a notes.json manifest')
607
+ .action(async (ref, options) => {
608
+ await (0, notes_1.browseNotes)(ref, options);
609
+ });
610
+ notes
611
+ .command('clear')
612
+ .description('Clear explicit note IDs after incorporating them')
613
+ .argument('<ref>', 'Exact ref: app:creator/name@x.y.z')
614
+ .argument('<ids...>', 'One or more note IDs')
615
+ .option('--json', 'Output JSON')
616
+ .action(async (ref, ids, options) => {
617
+ await (0, notes_1.clearNotes)(ref, ids, options);
618
+ });
580
619
  const credits = program.command('credits').description('Credit balance and history');
581
620
  credits
582
621
  .command('balance')
@@ -9,6 +9,10 @@ export type WorkerUploadPreflightContext = {
9
9
  target: 'FIRST_PARTY' | 'PERSONAL';
10
10
  creatorUsername: string;
11
11
  creatorRequest: string;
12
+ tweaks?: {
13
+ id: string;
14
+ values: Record<string, string | number | boolean>;
15
+ };
12
16
  };
13
17
  export declare function readMediaCaptureProof(input: {
14
18
  task: AppTask;
@@ -303,6 +303,15 @@ function readWorkerUploadPreflightContext(projectDir) {
303
303
  target,
304
304
  creatorUsername: typeof playdrop.creatorUsername === 'string' ? playdrop.creatorUsername.trim() : '',
305
305
  creatorRequest: typeof request.prompt === 'string' ? request.prompt.trim() : '',
306
+ ...(playdrop.tweaks
307
+ && typeof playdrop.tweaks === 'object'
308
+ && !Array.isArray(playdrop.tweaks)
309
+ && typeof playdrop.tweaks.id === 'string'
310
+ && playdrop.tweaks.values
311
+ && typeof playdrop.tweaks.values === 'object'
312
+ && !Array.isArray(playdrop.tweaks.values)
313
+ ? { tweaks: { id: playdrop.tweaks.id, values: playdrop.tweaks.values } }
314
+ : {}),
306
315
  };
307
316
  }
308
317
  if (parsed?.protocolVersion !== undefined) {
@@ -319,6 +328,14 @@ function readWorkerUploadPreflightContext(projectDir) {
319
328
  target: parsed.target,
320
329
  creatorUsername: typeof parsed.creatorUsername === 'string' ? parsed.creatorUsername.trim() : '',
321
330
  creatorRequest: typeof parsed.creatorRequest === 'string' ? parsed.creatorRequest.trim() : '',
331
+ ...(parsed.tweaks
332
+ && typeof parsed.tweaks === 'object'
333
+ && typeof parsed.tweaks.id === 'string'
334
+ && parsed.tweaks.values
335
+ && typeof parsed.tweaks.values === 'object'
336
+ && !Array.isArray(parsed.tweaks.values)
337
+ ? { tweaks: parsed.tweaks }
338
+ : {}),
322
339
  };
323
340
  }
324
341
  function assertAgentGameHeroListingPreflight(task) {
@@ -0,0 +1,7 @@
1
+ import { type AppTweaksSnapshot } from '@playdrop/types';
2
+ import type { ApiClient } from '@playdrop/api-client';
3
+ import type { AppTask } from './catalogue';
4
+ export declare function assertTweakStateMatches(task: AppTask, latest: AppTweaksSnapshot | null): void;
5
+ export declare function fetchAndAssertTweakState(client: ApiClient, creatorUsername: string, task: AppTask, options?: {
6
+ allowAppNotFound?: boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertTweakStateMatches = assertTweakStateMatches;
4
+ exports.fetchAndAssertTweakState = fetchAndAssertTweakState;
5
+ const types_1 = require("@playdrop/types");
6
+ function assertTweakStateMatches(task, latest) {
7
+ const basedOn = task.tweaks?.basedOn ?? null;
8
+ const latestId = latest?.id ?? null;
9
+ if (task.tweaks === undefined && latestId === null)
10
+ return;
11
+ if (task.tweaks !== undefined && basedOn === latestId)
12
+ return;
13
+ throw new Error([
14
+ 'Tweak definition is out of date.',
15
+ '',
16
+ `Code is based on: ${basedOn ?? 'none'}`,
17
+ `Latest state is: ${latestId ?? 'none'}`,
18
+ '',
19
+ 'Latest values:',
20
+ JSON.stringify(latest?.values ?? null, null, 2),
21
+ ].join('\n'));
22
+ }
23
+ async function fetchAndAssertTweakState(client, creatorUsername, task, options = {}) {
24
+ let latest = null;
25
+ try {
26
+ latest = await client.getLatestAppTweaks(creatorUsername, task.name);
27
+ }
28
+ catch (error) {
29
+ if (!(error instanceof types_1.ApiError))
30
+ throw error;
31
+ if (error.code !== 'tweaks_not_declared' && !(options.allowAppNotFound && error.code === 'app_not_found')) {
32
+ throw error;
33
+ }
34
+ }
35
+ assertTweakStateMatches(task, latest);
36
+ }