@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.
- package/config/client-meta.json +2 -2
- package/dist/apps/upload.js +1 -0
- package/dist/catalogue.d.ts +3 -1
- package/dist/catalogue.js +10 -0
- package/dist/commands/dev.js +9 -0
- package/dist/commands/devRuntimeAssets.d.ts +2 -1
- package/dist/commands/devRuntimeAssets.js +3 -0
- package/dist/commands/notes.d.ts +7 -0
- package/dist/commands/notes.js +107 -0
- package/dist/commands/tweaks.d.ts +6 -0
- package/dist/commands/tweaks.js +93 -0
- package/dist/commands/validate.js +14 -0
- package/dist/commands/worker.d.ts +1 -1
- package/dist/commands/worker.js +29 -8
- package/dist/index.js +40 -1
- package/dist/listingPreflight.d.ts +4 -0
- package/dist/listingPreflight.js +17 -0
- package/dist/tweakStaleness.d.ts +7 -0
- package/dist/tweakStaleness.js +36 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +25 -4
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/client.js +40 -0
- package/node_modules/@playdrop/api-client/dist/core/request.d.ts +1 -0
- package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/core/request.js +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +27 -0
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +18 -4
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +76 -6
- package/node_modules/@playdrop/api-client/dist/index.d.ts +35 -4
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +79 -4
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/config/dist/src/game-arena.d.ts +2 -2
- package/node_modules/@playdrop/config/dist/src/game-arena.js +2 -2
- package/node_modules/@playdrop/config/dist/test/game-arena.test.js +4 -3
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +76 -2
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +39 -2
- package/node_modules/@playdrop/types/dist/app-review.d.ts +11 -0
- package/node_modules/@playdrop/types/dist/app-review.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/app-review.js +56 -0
- package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +6 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.d.ts +69 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.js +30 -0
- package/node_modules/@playdrop/types/dist/tweaks.d.ts +48 -0
- package/node_modules/@playdrop/types/dist/tweaks.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/tweaks.js +156 -0
- package/node_modules/@playdrop/types/dist/version.d.ts +17 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/upload.js
CHANGED
|
@@ -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,
|
package/dist/catalogue.d.ts
CHANGED
|
@@ -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,
|
package/dist/commands/dev.js
CHANGED
|
@@ -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,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;
|
package/dist/commands/worker.js
CHANGED
|
@@ -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 =
|
|
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'
|
|
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
|
-
|
|
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;
|
package/dist/listingPreflight.js
CHANGED
|
@@ -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
|
+
}
|