shotops-mcp 0.9.7 → 0.9.9
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/README.md +4 -3
- package/dist/local.js +2073 -598
- package/harness-dist/assets/index-B8oQTxzE.js +1 -0
- package/harness-dist/assets/{three-addons-CvX38LO6.js → three-addons-zF8BveTt.js} +1 -1
- package/harness-dist/assets/{three-webgl-DCw804Ic.js → three-webgl-BAyyruEx.js} +2 -2
- package/harness-dist/index.html +3 -3
- package/package.json +1 -1
- package/widget-dist/bundle.js +3 -3
- package/widget-dist/panels.js +2 -2
- package/harness-dist/assets/index--FFK3psO.js +0 -1
package/dist/local.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
await visit('')
|
|
26
26
|
return hash.digest('hex')
|
|
27
|
-
})(sourceDirectory) !== "
|
|
27
|
+
})(sourceDirectory) !== "726565c5a58fb0cf9d366865195f22440699f6a55c8789edcd92577fc682ebbf") {
|
|
28
28
|
console.error('shotops-mcp: dist/local.js is stale; run npm run build in shotops-mcp.');
|
|
29
29
|
}
|
|
30
30
|
} catch { /* An unreadable checkout must not break an otherwise working CLI. */ }
|
|
@@ -44,6 +44,27 @@ var __export = (target, all) => {
|
|
|
44
44
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
// ../mockup-engine/appstore/presets.ts
|
|
48
|
+
function presetById(id) {
|
|
49
|
+
return PANEL_PRESETS.find((p) => p.id === id) ?? PANEL_PRESETS[0];
|
|
50
|
+
}
|
|
51
|
+
var PANEL_PRESETS;
|
|
52
|
+
var init_presets = __esm({
|
|
53
|
+
"../mockup-engine/appstore/presets.ts"() {
|
|
54
|
+
"use strict";
|
|
55
|
+
PANEL_PRESETS = [
|
|
56
|
+
{ id: "r69", label: "6.9\u2033 iPhone \xB7 1320\xD72868", width: 1320, height: 2868 },
|
|
57
|
+
{ id: "r65", label: "6.5\u2033 iPhone \xB7 1242\xD72688", width: 1242, height: 2688 },
|
|
58
|
+
{ id: "r55", label: "5.5\u2033 iPhone \xB7 1242\xD72208", width: 1242, height: 2208 },
|
|
59
|
+
// Pixel 10 Pro — a tall marketing canvas matching the 6.9″ iPhone's dimensions so
|
|
60
|
+
// the strip layout + caption scale are identical; only the 3D body differs.
|
|
61
|
+
{ id: "pixel69", label: "Pixel 10 Pro \xB7 1320\xD72868", width: 1320, height: 2868 },
|
|
62
|
+
{ id: "ipad13", label: "13\u2033 iPad \xB7 2064\xD72752", width: 2064, height: 2752 },
|
|
63
|
+
{ id: "ipad129", label: "12.9\u2033 iPad \xB7 2048\xD72732", width: 2048, height: 2732 }
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
47
68
|
// ../mockup-engine/appstore/devices.ts
|
|
48
69
|
function deviceById(id) {
|
|
49
70
|
return DEVICES.find((d) => d.id === id);
|
|
@@ -70,9 +91,23 @@ function deriveOutputs(explicit, panelPresetId2) {
|
|
|
70
91
|
}
|
|
71
92
|
return [DEFAULT_OUTPUT_DEVICE_ID];
|
|
72
93
|
}
|
|
94
|
+
function activeDeviceId(outputs, panelPresetId2) {
|
|
95
|
+
const match = outputs.find((id) => deviceById(id)?.panelPresetId === panelPresetId2);
|
|
96
|
+
return match ?? outputs[0];
|
|
97
|
+
}
|
|
98
|
+
function projectOutputAxis(projectFile) {
|
|
99
|
+
const file = projectFile ?? {};
|
|
100
|
+
const explicit = Array.isArray(file.outputs) ? file.outputs.filter((id) => typeof id === "string") : void 0;
|
|
101
|
+
const outputs = deriveOutputs(explicit, typeof file.panelPresetId === "string" ? file.panelPresetId : void 0);
|
|
102
|
+
const panelPresetId2 = presetById(typeof file.panelPresetId === "string" ? file.panelPresetId : "").id;
|
|
103
|
+
return { outputs, panelPresetId: panelPresetId2, activeDeviceId: activeDeviceId(outputs, panelPresetId2) };
|
|
104
|
+
}
|
|
73
105
|
function screenFamilyForDevice(deviceId) {
|
|
74
106
|
return deviceById(deviceId)?.screenFamily ?? BASE_SCREEN_FAMILY;
|
|
75
107
|
}
|
|
108
|
+
function screenFamilyForPreset(panelPresetId2) {
|
|
109
|
+
return DEVICES.find((d) => d.panelPresetId === panelPresetId2)?.screenFamily ?? BASE_SCREEN_FAMILY;
|
|
110
|
+
}
|
|
76
111
|
function outputScreenFamilies(outputs) {
|
|
77
112
|
const seen = /* @__PURE__ */ new Set();
|
|
78
113
|
const out = [];
|
|
@@ -89,6 +124,7 @@ var IPHONE_MODEL_ID, PIXEL_MODEL_ID, DEVICES, DEFAULT_OUTPUT_DEVICE_ID, BASE_SCR
|
|
|
89
124
|
var init_devices = __esm({
|
|
90
125
|
"../mockup-engine/appstore/devices.ts"() {
|
|
91
126
|
"use strict";
|
|
127
|
+
init_presets();
|
|
92
128
|
IPHONE_MODEL_ID = "iphone-17-pro";
|
|
93
129
|
PIXEL_MODEL_ID = "pixel-10-pro";
|
|
94
130
|
DEVICES = [
|
|
@@ -104,10 +140,15 @@ var init_devices = __esm({
|
|
|
104
140
|
});
|
|
105
141
|
|
|
106
142
|
// ../mockup-engine/appstore/catalog/caption.ts
|
|
107
|
-
var CAPTION_STYLE_BOUNDS;
|
|
143
|
+
var CAPTION_STYLE_DEFAULTS, CAPTION_STYLE_BOUNDS;
|
|
108
144
|
var init_caption = __esm({
|
|
109
145
|
"../mockup-engine/appstore/catalog/caption.ts"() {
|
|
110
146
|
"use strict";
|
|
147
|
+
CAPTION_STYLE_DEFAULTS = {
|
|
148
|
+
lineHeight: 1.2,
|
|
149
|
+
bandInset: 0.06,
|
|
150
|
+
bandCenter: 0.5
|
|
151
|
+
};
|
|
111
152
|
CAPTION_STYLE_BOUNDS = {
|
|
112
153
|
sizePt: { min: 20, max: 120 },
|
|
113
154
|
maxWidth: { min: 0.1, max: 1 },
|
|
@@ -120,7 +161,10 @@ var init_caption = __esm({
|
|
|
120
161
|
});
|
|
121
162
|
|
|
122
163
|
// ../mockup-engine/appstore/catalog/defaults.ts
|
|
123
|
-
|
|
164
|
+
function deviceSizeFromLegacyPhoneHeight(phoneHeight) {
|
|
165
|
+
return Math.round(phoneHeight / DEVICE_SIZE_BASE_FRACTION * 100) / 100;
|
|
166
|
+
}
|
|
167
|
+
var DEFAULT_LOOK, SHOT_LOOK_DOMAIN, SHOT_LOOK_RANGES, DEVICE_SIZE_BASE_FRACTION, DEFAULT_CAPTION_ANCHOR, DEFAULT_CAPTION_STYLE;
|
|
124
168
|
var init_defaults = __esm({
|
|
125
169
|
"../mockup-engine/appstore/catalog/defaults.ts"() {
|
|
126
170
|
"use strict";
|
|
@@ -138,19 +182,24 @@ var init_defaults = __esm({
|
|
|
138
182
|
// pushPhonesBelowCaptionBands() solved per-render before e0f587c ("Stop captions moving
|
|
139
183
|
// devices") deleted it: captions must never move a device, so the clearance has to be baked
|
|
140
184
|
// into the default instead.
|
|
141
|
-
// deviceTop(v) = (1 -
|
|
185
|
+
// deviceTop(v) = (1 - deviceHeightFraction(deviceSize))/2 + (v/100)/2 [layoutStrip, +y = down]
|
|
142
186
|
// bandBottom(n) = CAPTION_BAND_TOP + n·LINE_HEIGHT·sizePt/PANEL_LOGICAL_HEIGHT_PT
|
|
143
|
-
// At 82% height and a 32pt caption, deviceTop = 18.0% and the device sits FLUSH to the bottom
|
|
187
|
+
// At deviceSize 100 (82% of panel height) and a 32pt caption, deviceTop = 18.0% and the device sits FLUSH to the bottom
|
|
144
188
|
// edge (18.0 + 82 = 100.0). A full 2-line headline's ink ends at 13.6%, so the gap is 4.4%;
|
|
145
189
|
// a 3-line headline still clears by 0.3%. Four lines overlap — that is what Bleed is for, and
|
|
146
190
|
// it buys the room by running the device 10% off the bottom edge. Re-derive via
|
|
147
|
-
// catalog/layouts.ts if sizePt or
|
|
148
|
-
|
|
191
|
+
// catalog/layouts.ts if sizePt or deviceSize changes; its tests are the guard.
|
|
192
|
+
deviceSize: "100",
|
|
149
193
|
hOffset: "0",
|
|
150
194
|
vOffset: "18",
|
|
151
195
|
material: "real",
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
// Deep Blue on the iPhone, Moonstone on the Pixel (which binds no `blue`, so it falls back to its
|
|
197
|
+
// first finish — deviceFinishes.ts). A product call, not a derived one: the blue body is the one
|
|
198
|
+
// that reads as a device against the widest range of strip backgrounds, where Silver disappeared
|
|
199
|
+
// into a light one. `customColor` starts at the same hex so opening the custom well doesn't jump
|
|
200
|
+
// the phone to a different colour before anyone has picked anything.
|
|
201
|
+
colorway: "blue",
|
|
202
|
+
customColor: "32374A",
|
|
154
203
|
finish: "0",
|
|
155
204
|
clearcoat: "0",
|
|
156
205
|
clayTone: "grey",
|
|
@@ -164,7 +213,7 @@ var init_defaults = __esm({
|
|
|
164
213
|
angle: { valueKind: "angle", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.angle, persistence: "always" },
|
|
165
214
|
cameraPos: { valueKind: "camera-position", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.cameraPos, persistence: "always" },
|
|
166
215
|
roll: { valueKind: "numeric-string", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.roll, persistence: "always" },
|
|
167
|
-
|
|
216
|
+
deviceSize: { valueKind: "numeric-string", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.deviceSize, persistence: "always" },
|
|
168
217
|
hOffset: { valueKind: "numeric-string", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.hOffset, persistence: "always" },
|
|
169
218
|
vOffset: { valueKind: "numeric-string", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.vOffset, persistence: "always" },
|
|
170
219
|
material: { valueKind: "material", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.material, persistence: "always" },
|
|
@@ -176,21 +225,27 @@ var init_defaults = __esm({
|
|
|
176
225
|
clayCustom: { valueKind: "string", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.clayCustom, persistence: "always" },
|
|
177
226
|
flatScreen: { valueKind: "boolean", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.flatScreen, persistence: "always" },
|
|
178
227
|
clipToFrame: { valueKind: "boolean", required: false, defaultSource: "explicit", defaultValue: false, persistence: "omit-default" },
|
|
228
|
+
deviceFrame: { valueKind: "boolean", required: false, defaultSource: "explicit", defaultValue: true, persistence: "omit-default" },
|
|
229
|
+
screenCornerRadius: { valueKind: "numeric-string", required: false, defaultSource: "explicit", defaultValue: "12", persistence: "omit-default" },
|
|
179
230
|
glare: { valueKind: "boolean", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.glare, persistence: "always" },
|
|
180
231
|
lighting: { valueKind: "boolean", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.lighting, persistence: "always" },
|
|
181
232
|
reflections: { valueKind: "boolean", required: true, defaultSource: "default-look", defaultValue: DEFAULT_LOOK.reflections, persistence: "always" }
|
|
182
233
|
};
|
|
183
234
|
SHOT_LOOK_RANGES = {
|
|
184
235
|
roll: { min: -45, max: 45 },
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// exactly what the Bleed layout template trades headline
|
|
188
|
-
|
|
236
|
+
// Percent of the default device size (see DEVICE_SIZE_BASE_FRACTION): 25–240 is 20–200% of the
|
|
237
|
+
// panel's height. Past ~122 the device is TALLER than the panel and bleeds off an edge — a
|
|
238
|
+
// legitimate, shipped composition, and exactly what the Bleed layout template trades headline
|
|
239
|
+
// room for.
|
|
240
|
+
deviceSize: { min: 25, max: 240 },
|
|
189
241
|
hOffset: { min: -100, max: 100 },
|
|
190
242
|
vOffset: { min: -200, max: 200 },
|
|
191
243
|
finish: { min: 0, max: 1 },
|
|
192
|
-
clearcoat: { min: 0, max: 1 }
|
|
244
|
+
clearcoat: { min: 0, max: 1 },
|
|
245
|
+
// Percent of the flat card's WIDTH (#907); 50 turns the short edges into semicircles.
|
|
246
|
+
screenCornerRadius: { min: 0, max: 50 }
|
|
193
247
|
};
|
|
248
|
+
DEVICE_SIZE_BASE_FRACTION = 0.82;
|
|
194
249
|
DEFAULT_CAPTION_ANCHOR = { x: 0.5, y: 0.06 };
|
|
195
250
|
DEFAULT_CAPTION_STYLE = {
|
|
196
251
|
fontId: "inter",
|
|
@@ -362,6 +417,9 @@ function variantKey(variant) {
|
|
|
362
417
|
if (variant.locale !== void 0) parts.push(`locale=${variant.locale}`);
|
|
363
418
|
return parts.join("&");
|
|
364
419
|
}
|
|
420
|
+
function isBaseVariant(variant) {
|
|
421
|
+
return variantKey(variant) === "";
|
|
422
|
+
}
|
|
365
423
|
function variantAxes(variant) {
|
|
366
424
|
const out = [];
|
|
367
425
|
if (variant.family !== void 0) out.push("family");
|
|
@@ -540,9 +598,9 @@ function sha256Hex(text3) {
|
|
|
540
598
|
const words = [h0, h1, h2, h3, h4, h5, h6, h7];
|
|
541
599
|
let out = "";
|
|
542
600
|
for (let i = 0; i < words.length; i += 1) {
|
|
543
|
-
let
|
|
544
|
-
while (
|
|
545
|
-
out +=
|
|
601
|
+
let hex2 = (words[i] >>> 0).toString(16);
|
|
602
|
+
while (hex2.length < 8) hex2 = `0${hex2}`;
|
|
603
|
+
out += hex2;
|
|
546
604
|
}
|
|
547
605
|
return out;
|
|
548
606
|
}
|
|
@@ -1855,7 +1913,7 @@ var init_resultContract = __esm({
|
|
|
1855
1913
|
{ code: "render_failed", test: /^render failed|^renderer returned / },
|
|
1856
1914
|
{ code: "payload_too_large", test: /inline-response ceiling|is too large/ },
|
|
1857
1915
|
{ code: "storage_failed", test: /could not store the rendered panels|could not create share link/ },
|
|
1858
|
-
{ code: "asset_not_found", test: /failed to read panel|no preview image available|does not belong to this account/ },
|
|
1916
|
+
{ code: "asset_not_found", test: /failed to read panel|no preview image available|does not belong to this account|no screenshot was ever uploaded for ref/ },
|
|
1859
1917
|
{ code: "persistence_failed", test: /could not build the project record|no project record was produced/ },
|
|
1860
1918
|
{ code: "quota_exhausted", test: /not enough credits|credit balance|wallet is empty/ },
|
|
1861
1919
|
// Input-shape complaints raised by a HANDLER rather than by the schema. Last, and each anchored
|
|
@@ -1945,7 +2003,7 @@ var init_refineIntent = __esm({
|
|
|
1945
2003
|
init_defaults();
|
|
1946
2004
|
init_fonts();
|
|
1947
2005
|
MAX_REFINE_PROMPT_LENGTH = 1200;
|
|
1948
|
-
AI_COLORWAY_IDS = ["orange", "blue", "silver", "green"];
|
|
2006
|
+
AI_COLORWAY_IDS = ["orange", "blue", "ice", "silver", "red", "black", "green"];
|
|
1949
2007
|
AI_CLAY_TONES = ["grey", "white", "charcoal"];
|
|
1950
2008
|
AI_SHOT_RANGES = {
|
|
1951
2009
|
orbit: { min: -180, max: 180 },
|
|
@@ -1982,7 +2040,7 @@ var init_shotLookProjection = __esm({
|
|
|
1982
2040
|
valueKind: "camera-position",
|
|
1983
2041
|
validation: { wireKind: "nullable-camera-position" },
|
|
1984
2042
|
schemaDescription: "Manual camera position override; null/omit = use the angle preset.",
|
|
1985
|
-
catalogDescription: "{ x, y, z } rotates the camera around the device to refine the angle beyond the presets, or null to use the preset. It does NOT zoom or resize \u2014 device SIZE is set ONLY by
|
|
2043
|
+
catalogDescription: "{ x, y, z } rotates the camera around the device to refine the angle beyond the presets, or null to use the preset. It does NOT zoom or resize \u2014 device SIZE is set ONLY by deviceSize (the export alpha-crops the device tight, so camera distance is discarded)."
|
|
1986
2044
|
},
|
|
1987
2045
|
roll: {
|
|
1988
2046
|
exposure: "public",
|
|
@@ -1991,12 +2049,12 @@ var init_shotLookProjection = __esm({
|
|
|
1991
2049
|
schemaDescription: `${rangeText("roll")}\xB0 clock-hand tilt. Default ${defaultValue("roll")}.`,
|
|
1992
2050
|
catalogDescription: `${catalogRange("roll")} (degrees, clock-hand tilt; continuous, unlike angle)`
|
|
1993
2051
|
},
|
|
1994
|
-
|
|
2052
|
+
deviceSize: {
|
|
1995
2053
|
exposure: "public",
|
|
1996
2054
|
valueKind: "numeric-string",
|
|
1997
2055
|
validation: { wireKind: "bounded-numeric" },
|
|
1998
|
-
schemaDescription: `
|
|
1999
|
-
catalogDescription: `${catalogRange("
|
|
2056
|
+
schemaDescription: `Device size, ${rangeText("deviceSize")} (percent of the default device size; about 122 fills the panel height and more bleeds off an edge). Default ${defaultValue("deviceSize")} (the Standard layout template; prefer \`style.layout\` over setting this by hand).`,
|
|
2057
|
+
catalogDescription: `${catalogRange("deviceSize")} (percent of the default device size, which is 82% of the panel height) \u2014 the ONLY device-size control. Past about 122 the device is taller than the panel and bleeds off an edge. Default ${defaultValue("deviceSize")} (the Standard layout template; prefer style.layout over setting this by hand).`
|
|
2000
2058
|
},
|
|
2001
2059
|
hOffset: {
|
|
2002
2060
|
exposure: "public",
|
|
@@ -2022,9 +2080,9 @@ var init_shotLookProjection = __esm({
|
|
|
2022
2080
|
colorway: {
|
|
2023
2081
|
exposure: "public",
|
|
2024
2082
|
valueKind: "colorway",
|
|
2025
|
-
validation: { wireKind: "enum", values: ["orange", "blue", "silver", "green", "custom"] },
|
|
2026
|
-
schemaDescription: `Body colour (material "real"). Default ${defaultValue("colorway")}. These are finish SLOTS \u2014 each device binds its own real finish
|
|
2027
|
-
catalogDescription: "orange | blue | silver | green | custom (with customColor hex) \u2014 finish SLOTS, each device binds its own real finish
|
|
2083
|
+
validation: { wireKind: "enum", values: ["orange", "blue", "ice", "silver", "red", "black", "green", "custom"] },
|
|
2084
|
+
schemaDescription: `Body colour (material "real"). Default ${defaultValue("colorway")}. These are finish SLOTS \u2014 each device binds its own real finish. On iPhone: blue = Deep Blue, silver = Silver, orange = Cosmic Orange, ice = Glacier, red = Burgundy, black = Black. A slot a device does not bind falls back to its first finish \u2014 "green" is Pixel-only (Jade).`,
|
|
2085
|
+
catalogDescription: "orange | blue | ice | silver | red | black | green | custom (with customColor hex) \u2014 finish SLOTS, each device binds its own real finish. On iPhone: blue = Deep Blue, silver = Silver, orange = Cosmic Orange, ice = Glacier, red = Burgundy, black = Black. A slot a device does not bind falls back to its first finish; green is Pixel-only (Jade)."
|
|
2028
2086
|
},
|
|
2029
2087
|
customColor: {
|
|
2030
2088
|
exposure: "public",
|
|
@@ -2095,6 +2153,20 @@ var init_shotLookProjection = __esm({
|
|
|
2095
2153
|
validation: { wireKind: "boolean" },
|
|
2096
2154
|
schemaDescription: `Clip this complete device composite to its owning frame. Default ${defaultValue("clipToFrame")} (overflow allowed).`,
|
|
2097
2155
|
catalogDescription: "boolean \u2014 clip this device, its screen, shadow and reflection to its owning frame; default false"
|
|
2156
|
+
},
|
|
2157
|
+
deviceFrame: {
|
|
2158
|
+
exposure: "public",
|
|
2159
|
+
valueKind: "boolean",
|
|
2160
|
+
validation: { wireKind: "boolean" },
|
|
2161
|
+
schemaDescription: `Render the 3D device frame around the screen. Default ${defaultValue("deviceFrame")}. Set false for a flat rounded screenshot card with no device chrome \u2014 screenCornerRadius then sets its corner rounding, and roll still rotates the flat card exactly as it tilts a framed device.`,
|
|
2162
|
+
catalogDescription: `boolean \u2014 false renders a flat rounded screenshot card instead of the 3D device; screenCornerRadius then sets its corner rounding, and roll still rotates the flat card. Default true.`
|
|
2163
|
+
},
|
|
2164
|
+
screenCornerRadius: {
|
|
2165
|
+
exposure: "public",
|
|
2166
|
+
valueKind: "numeric-string",
|
|
2167
|
+
validation: { wireKind: "bounded-numeric" },
|
|
2168
|
+
schemaDescription: `${rangeText("screenCornerRadius")} (percent of the flat card's width). Default ${defaultValue("screenCornerRadius")}. Applies only when deviceFrame is false \u2014 ignored on a framed device, whose corners come from the device model.`,
|
|
2169
|
+
catalogDescription: `${catalogRange("screenCornerRadius")} (percent of the flat card's width) \u2014 applies only when deviceFrame is false; ignored on a framed device.`
|
|
2098
2170
|
}
|
|
2099
2171
|
};
|
|
2100
2172
|
MCP_SHOT_LOOK_PUBLIC_ENTRIES = Object.entries(MCP_SHOT_LOOK_PROJECTION).filter((entry) => entry[1].exposure === "public");
|
|
@@ -2104,24 +2176,12 @@ var init_shotLookProjection = __esm({
|
|
|
2104
2176
|
}
|
|
2105
2177
|
});
|
|
2106
2178
|
|
|
2107
|
-
// ../mockup-engine/
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
var PANEL_PRESETS;
|
|
2112
|
-
var init_presets = __esm({
|
|
2113
|
-
"../mockup-engine/appstore/presets.ts"() {
|
|
2179
|
+
// ../mockup-engine/outputDeviceIds.ts
|
|
2180
|
+
var OUTPUT_DEVICE_IDS;
|
|
2181
|
+
var init_outputDeviceIds = __esm({
|
|
2182
|
+
"../mockup-engine/outputDeviceIds.ts"() {
|
|
2114
2183
|
"use strict";
|
|
2115
|
-
|
|
2116
|
-
{ id: "r69", label: "6.9\u2033 iPhone \xB7 1290\xD72796", width: 1290, height: 2796 },
|
|
2117
|
-
{ id: "r65", label: "6.5\u2033 iPhone \xB7 1242\xD72688", width: 1242, height: 2688 },
|
|
2118
|
-
{ id: "r55", label: "5.5\u2033 iPhone \xB7 1242\xD72208", width: 1242, height: 2208 },
|
|
2119
|
-
// Pixel 10 Pro — a tall marketing canvas matching the 6.9″ iPhone's dimensions so
|
|
2120
|
-
// the strip layout + caption scale are identical; only the 3D body differs.
|
|
2121
|
-
{ id: "pixel69", label: "Pixel 10 Pro \xB7 1290\xD72796", width: 1290, height: 2796 },
|
|
2122
|
-
{ id: "ipad13", label: "13\u2033 iPad \xB7 2064\xD72752", width: 2064, height: 2752 },
|
|
2123
|
-
{ id: "ipad129", label: "12.9\u2033 iPad \xB7 2048\xD72732", width: 2048, height: 2732 }
|
|
2124
|
-
];
|
|
2184
|
+
OUTPUT_DEVICE_IDS = ["iphone-6-9", "iphone-6-5"];
|
|
2125
2185
|
}
|
|
2126
2186
|
});
|
|
2127
2187
|
|
|
@@ -2157,20 +2217,33 @@ function panelLabelFor(presetId, width, height) {
|
|
|
2157
2217
|
function panelDimensionsFor(presetId) {
|
|
2158
2218
|
return PANEL_DIMENSIONS[presetId] ?? PANEL_DIMENSIONS.r69;
|
|
2159
2219
|
}
|
|
2160
|
-
var MCP_UNKNOWN_PANEL_PRESET_IDS, MCP_PANEL_PRESETS, PANEL_PRESET_IDS, RENDERABLE_PANEL_PRESET_IDS,
|
|
2220
|
+
var MCP_UNRENDERED_DEVICE_IDS, MCP_UNKNOWN_PANEL_PRESET_IDS, MCP_PANEL_PRESETS, PANEL_PRESET_IDS, RENDERABLE_PANEL_PRESET_IDS, OUTPUT_DEVICES, NOT_COVERED_BY_THIS_CALL, SIZE_SUFFIX, PANEL_LABELS, PANEL_DIMENSIONS;
|
|
2161
2221
|
var init_panelCatalog = __esm({
|
|
2162
2222
|
"src/render/panelCatalog.ts"() {
|
|
2163
2223
|
"use strict";
|
|
2164
2224
|
init_presets();
|
|
2165
|
-
|
|
2225
|
+
init_devices();
|
|
2226
|
+
init_outputDeviceIds();
|
|
2227
|
+
init_outputDeviceIds();
|
|
2228
|
+
MCP_UNRENDERED_DEVICE_IDS = ["pixel-10-pro"];
|
|
2229
|
+
MCP_UNKNOWN_PANEL_PRESET_IDS = MCP_UNRENDERED_DEVICE_IDS.map(
|
|
2230
|
+
(id) => deviceById(id)?.panelPresetId ?? id
|
|
2231
|
+
);
|
|
2166
2232
|
MCP_PANEL_PRESETS = PANEL_PRESETS.filter((preset) => !MCP_UNKNOWN_PANEL_PRESET_IDS.includes(preset.id));
|
|
2167
2233
|
PANEL_PRESET_IDS = MCP_PANEL_PRESETS.map((preset) => preset.id);
|
|
2168
2234
|
RENDERABLE_PANEL_PRESET_IDS = ["r69", "r65", "r55"];
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2235
|
+
OUTPUT_DEVICES = OUTPUT_DEVICE_IDS.map((id) => {
|
|
2236
|
+
const device = deviceById(id);
|
|
2237
|
+
if (!device || device.comingSoon) {
|
|
2238
|
+
throw new Error(`output device "${id}" is not a selectable device in the engine catalog`);
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
id,
|
|
2242
|
+
label: device.label,
|
|
2243
|
+
panelPresetId: device.panelPresetId,
|
|
2244
|
+
ascDeviceSet: device.ascDeviceSet
|
|
2245
|
+
};
|
|
2246
|
+
});
|
|
2174
2247
|
NOT_COVERED_BY_THIS_CALL = "not covered by this bundle";
|
|
2175
2248
|
SIZE_SUFFIX = / · \d+×\d+$/;
|
|
2176
2249
|
PANEL_LABELS = Object.fromEntries(
|
|
@@ -2219,7 +2292,7 @@ var init_layouts = __esm({
|
|
|
2219
2292
|
blurb: "Headline on top, whole device below it, nothing cropped. The safe default.",
|
|
2220
2293
|
character: "safe",
|
|
2221
2294
|
bleedEdge: "none",
|
|
2222
|
-
shotLook: {
|
|
2295
|
+
shotLook: { deviceSize: "100", hOffset: "0", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2223
2296
|
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2224
2297
|
},
|
|
2225
2298
|
{
|
|
@@ -2228,7 +2301,7 @@ var init_layouts = __esm({
|
|
|
2228
2301
|
blurb: "Room for a longer headline, device running off the bottom edge. What the market looks like.",
|
|
2229
2302
|
character: "conventional",
|
|
2230
2303
|
bleedEdge: "bottom",
|
|
2231
|
-
shotLook: {
|
|
2304
|
+
shotLook: { deviceSize: "100", hOffset: "0", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2232
2305
|
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, align: "center", bandCenter: 0.5 }
|
|
2233
2306
|
},
|
|
2234
2307
|
{
|
|
@@ -2237,7 +2310,7 @@ var init_layouts = __esm({
|
|
|
2237
2310
|
blurb: "Device running off the TOP edge, headline underneath it. Inverts the usual reading order.",
|
|
2238
2311
|
character: "expressive",
|
|
2239
2312
|
bleedEdge: "top",
|
|
2240
|
-
shotLook: {
|
|
2313
|
+
shotLook: { deviceSize: "100", hOffset: "0", vOffset: "-38", angle: "front", cameraPos: null, roll: "0" },
|
|
2241
2314
|
// The only entry that moves the band. Its caption sits BELOW the device, which the engine
|
|
2242
2315
|
// could not express at all until #218 gave placeCaptions a bottom-anchored band — before that
|
|
2243
2316
|
// the only way down was pinning an anchor, which silently costs reflow, stacking and the
|
|
@@ -2256,7 +2329,7 @@ var init_layouts = __esm({
|
|
|
2256
2329
|
blurb: "Device pushed off the RIGHT edge, headline in a column down the left. Reads as one continuous swipe.",
|
|
2257
2330
|
character: "expressive",
|
|
2258
2331
|
bleedEdge: "right",
|
|
2259
|
-
shotLook: {
|
|
2332
|
+
shotLook: { deviceSize: "100", hOffset: "46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2260
2333
|
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "left", bandCenter: 0.375 }
|
|
2261
2334
|
},
|
|
2262
2335
|
{
|
|
@@ -2265,7 +2338,7 @@ var init_layouts = __esm({
|
|
|
2265
2338
|
blurb: "Device pushed off the LEFT edge, headline in a column down the right. The mirror of Lean right.",
|
|
2266
2339
|
character: "expressive",
|
|
2267
2340
|
bleedEdge: "left",
|
|
2268
|
-
shotLook: {
|
|
2341
|
+
shotLook: { deviceSize: "100", hOffset: "-46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2269
2342
|
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "right", bandCenter: 0.625 }
|
|
2270
2343
|
},
|
|
2271
2344
|
{
|
|
@@ -2279,7 +2352,7 @@ var init_layouts = __esm({
|
|
|
2279
2352
|
// too — which makes this template claim `bottom-right` and collide with Corner bleed, the only
|
|
2280
2353
|
// other entry on that edge. The horizontal push is the whole idea here; the vertical stays out
|
|
2281
2354
|
// of its way. Both halves are guarded (clearance, and the declared-edge check).
|
|
2282
|
-
shotLook: {
|
|
2355
|
+
shotLook: { deviceSize: "100", hOffset: "78", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2283
2356
|
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2284
2357
|
},
|
|
2285
2358
|
{
|
|
@@ -2288,7 +2361,7 @@ var init_layouts = __esm({
|
|
|
2288
2361
|
blurb: "Off the bottom AND the right at once, headline in a column down the left. The most opinionated panel here.",
|
|
2289
2362
|
character: "expressive",
|
|
2290
2363
|
bleedEdge: "bottom-right",
|
|
2291
|
-
shotLook: {
|
|
2364
|
+
shotLook: { deviceSize: "100", hOffset: "46", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2292
2365
|
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 4, align: "left", bandCenter: 0.375 }
|
|
2293
2366
|
}
|
|
2294
2367
|
];
|
|
@@ -2302,7 +2375,7 @@ var init_layouts = __esm({
|
|
|
2302
2375
|
align: "center",
|
|
2303
2376
|
bandCenter: 0.5
|
|
2304
2377
|
};
|
|
2305
|
-
LAYOUT_POSE_FIELDS = ["
|
|
2378
|
+
LAYOUT_POSE_FIELDS = ["deviceSize", "hOffset", "vOffset"];
|
|
2306
2379
|
LAYOUT_CAMERA_FIELDS = ["angle", "cameraPos", "roll"];
|
|
2307
2380
|
LAYOUT_CAPTION_FIELDS = Array.from(
|
|
2308
2381
|
new Set(LAYOUT_TEMPLATE_CATALOG.flatMap((t) => Object.keys(t.caption)))
|
|
@@ -2314,7 +2387,8 @@ var init_layouts = __esm({
|
|
|
2314
2387
|
function layoutTemplateAgentSummary(template) {
|
|
2315
2388
|
const reserveLines = template.caption.reserveLines;
|
|
2316
2389
|
const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${Math.round(reserveLines * 18 * (template.caption.maxWidth ?? 0.86) / 0.86)} characters).` : "";
|
|
2317
|
-
|
|
2390
|
+
const headline = (template.caption.maxWidth ?? 0.86) < 0.86 || (template.caption.align ?? "center") !== "center" ? " Restyles the headline into a narrower column." : " Leaves the headline full-width and centred.";
|
|
2391
|
+
return `${template.blurb}${capacity}${headline} Character: [${template.character}]. Bleeds: [${template.bleedEdge}].`;
|
|
2318
2392
|
}
|
|
2319
2393
|
function layoutTemplateById(id) {
|
|
2320
2394
|
return LAYOUT_TEMPLATES.find((t) => t.id === id);
|
|
@@ -2406,12 +2480,20 @@ var init_layoutTemplates = __esm({
|
|
|
2406
2480
|
'A request that names a composition outright ("device running off the bottom edge", "headline under the phone") beats the character.',
|
|
2407
2481
|
"The EDGE beats the character too, and is checked first: a request that says which way the phone should go has already chosen, whatever quality words sit around it.",
|
|
2408
2482
|
"When several templates share the right character AND the right edge, take the one whose reserved headline lines fit the LONGEST caption in the set. Under-reserving truncates; over-reserving leaves a visible empty band.",
|
|
2409
|
-
"A side bleed
|
|
2483
|
+
"A side bleed may be given to the whole set or to named frames, but never to a frame whose NEIGHBOUR leans the other way. Every panel leaning the same way reads as one swipe; mirrored neighbours (lean-right beside lean-left) overlap by most of a panel width and read as a mistake. Alternate a lean with `standard`, never with its mirror.",
|
|
2484
|
+
"A template sets the HEADLINE as well as the phone, so choosing one restyles the captions on every frame it reaches. When the user asked for a composition and asked NOT to touch the captions, take the template on the right edge whose headline treatment already matches theirs, and say in the interpretation what it does to the headline."
|
|
2410
2485
|
];
|
|
2411
2486
|
valuesEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2412
2487
|
}
|
|
2413
2488
|
});
|
|
2414
2489
|
|
|
2490
|
+
// ../mockup-engine/hexColor.ts
|
|
2491
|
+
var init_hexColor = __esm({
|
|
2492
|
+
"../mockup-engine/hexColor.ts"() {
|
|
2493
|
+
"use strict";
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2415
2497
|
// ../mockup-engine/gradient.ts
|
|
2416
2498
|
function angleFromDirection(dir) {
|
|
2417
2499
|
return dir === "horizontal" ? HORIZONTAL_ANGLE : VERTICAL_ANGLE;
|
|
@@ -2470,6 +2552,7 @@ var HORIZONTAL_ANGLE, VERTICAL_ANGLE, FALLBACK_GRADIENT_FROM, FALLBACK_GRADIENT_
|
|
|
2470
2552
|
var init_gradient = __esm({
|
|
2471
2553
|
"../mockup-engine/gradient.ts"() {
|
|
2472
2554
|
"use strict";
|
|
2555
|
+
init_hexColor();
|
|
2473
2556
|
HORIZONTAL_ANGLE = 90;
|
|
2474
2557
|
VERTICAL_ANGLE = 180;
|
|
2475
2558
|
FALLBACK_GRADIENT_FROM = "#1b1b2e";
|
|
@@ -2766,7 +2849,7 @@ var init_toolContract = __esm({
|
|
|
2766
2849
|
"device, if the project targets several), so the Big-jobs timing warning applies unchanged.",
|
|
2767
2850
|
"",
|
|
2768
2851
|
"## Outputs \u2014 one project, several device SIZES",
|
|
2769
|
-
"A project targets one or more DEVICES: iphone-6-9 (
|
|
2852
|
+
"A project targets one or more DEVICES: iphone-6-9 (1320\xD72868) and/or iphone-6-5 (1242\xD72688).",
|
|
2770
2853
|
"You almost never need to name them. render_project renders EVERY device the project targets \u2014",
|
|
2771
2854
|
"the top-level panels are the previewed device, and an `outputs` array carries each device's own",
|
|
2772
2855
|
"panels when there is more than one. emit_bundle does the same for the zip: with a `project` and",
|
|
@@ -2815,7 +2898,7 @@ var init_toolContract = __esm({
|
|
|
2815
2898
|
"COMPOSE \u2014 every real App Store strip needs BOTH varied devices AND headlines, and that is ONE",
|
|
2816
2899
|
"call: pass `look` (devices + background) and `style` (style.captions gives the words) together.",
|
|
2817
2900
|
"A style.shotLook/style.background alongside a look is redundant \u2014 the look wins, with a note.",
|
|
2818
|
-
"panelPresetId picks the App Store size: r69 (6.9\u2033, default,
|
|
2901
|
+
"panelPresetId picks the App Store size: r69 (6.9\u2033, default, 1320\xD72868) | r65 | r55 \u2014 iPhone only.",
|
|
2819
2902
|
"Match it to your source screenshots (1320\xD72868 or 1290\xD72796 \u2192 r69).",
|
|
2820
2903
|
"Continuous scene: panels have no hard divider, so a phone pushed past its edge with a large",
|
|
2821
2904
|
"hOffset overflows into the NEIGHBOURING panel and the strip reads as one swipe-through image.",
|
|
@@ -2901,6 +2984,21 @@ var init_toolContract = __esm({
|
|
|
2901
2984
|
"Skip the names and the record falls back to positional filenames (screen-1.png, screen-2.png\u2026)",
|
|
2902
2985
|
"the user's folder can't match \u2014 the single worst create-project failure mode. Pass the real filenames.",
|
|
2903
2986
|
"",
|
|
2987
|
+
"## Link the project to the real app (search_app_listing)",
|
|
2988
|
+
"When the user says which app these screenshots are for, find it instead of typing its name:",
|
|
2989
|
+
'search_app_listing({ term: "Duolingo" }) returns matching App Store apps with their exact name,',
|
|
2990
|
+
"seller, icon, bundle ID and numeric app ID, plus the App Store Connect languages and iPhone",
|
|
2991
|
+
"devices ShotOps would apply. Show the candidates and let the USER say which one is theirs \u2014 the",
|
|
2992
|
+
"icon and the seller are what prove it \u2014 then pass that result's `appId` (or its `storeUrl`, which",
|
|
2993
|
+
"also carries the storefront) as save_project({ appListing }). The created project then takes the",
|
|
2994
|
+
"app's name, bundle ID, language list and device set; anything you state yourself \u2014 `projectName`,",
|
|
2995
|
+
"`outputs`, `locale` \u2014 wins, and the app is still linked. Passing `appListing` with an existing",
|
|
2996
|
+
"`project` re-links that project and rewrites nothing else. read_project reports the linked app as",
|
|
2997
|
+
"`linkedApp`. Both are free reads over Apple's public listing data: no Apple or store credential is",
|
|
2998
|
+
"involved, and a public listing proves nothing about who owns the app \u2014 the bundle ID stays",
|
|
2999
|
+
"editable. Both need an account; an unsigned local server says so and the save still works without",
|
|
3000
|
+
"the reference.",
|
|
3001
|
+
"",
|
|
2904
3002
|
'## "Any updates?" \u2014 read back what a designer changed (read_project)',
|
|
2905
3003
|
"read_look returns only STYLING. To see what a designer edited in the web app \u2014 frame ORDER, the",
|
|
2906
3004
|
"per-locale caption WORDS, the locale list, structural changes \u2014 call read_project. It returns the",
|
|
@@ -3027,7 +3125,7 @@ var init_designIntake = __esm({
|
|
|
3027
3125
|
{ id: "continuous", label: "One continuous scene across panels", blurb: "Large hOffset values let a phone straddle the seam so the strip reads as one swipe-through image. Preview it \u2014 placement is fiddly." },
|
|
3028
3126
|
// #223 — the bound is read from the engine's range table, not re-typed: this blurb said
|
|
3029
3127
|
// 20–100 while the render clamped to 200 and both sliders offered it.
|
|
3030
|
-
{ id: "bigger", label: "Make the phones larger or smaller", blurb: `
|
|
3128
|
+
{ id: "bigger", label: "Make the phones larger or smaller", blurb: `deviceSize, ${SHOT_LOOK_RANGES.deviceSize.min}\u2013${SHOT_LOOK_RANGES.deviceSize.max} (percent of the default device size; about 122 fills the panel height and more bleeds off an edge), default ${DEFAULT_LOOK.deviceSize}. It is the ONLY device-size control \u2014 cameraPos does not resize. Prefer style.layout over hand-setting it.` }
|
|
3031
3129
|
],
|
|
3032
3130
|
apply: `The layout answer is one field: style.layout: ${LAYOUT_TEMPLATE_IDS.map((id) => `"${id}"`).join(" | ")} (see describe_look \`layouts\`). It expands FIRST into ` + LAYOUT_DEVICE_PATHS + " + " + LAYOUT_CAPTION_PATHS + ', and any of those fields you pass explicitly still overrides it \u2014 so "bleed, but a bit lower" is { layout: "bleed", shotLook: { vOffset: "45" } }. Nothing stores the template id. Anything past "uniform" on the device half means passing a `look` alongside `style`. They compose in one render_strip call: the look supplies per-device styling + background, style.captions the words.'
|
|
3033
3131
|
}
|
|
@@ -3086,7 +3184,7 @@ function clampCaptionBounds(input) {
|
|
|
3086
3184
|
}
|
|
3087
3185
|
function captionClampNote(clamps) {
|
|
3088
3186
|
if (clamps.length === 0) return void 0;
|
|
3089
|
-
const said = clamps.map((
|
|
3187
|
+
const said = clamps.map((clamp2) => `${clamp2.field} ${clamp2.submitted} \u2192 ${clamp2.stored}`).join("; ");
|
|
3090
3188
|
return `Caption values outside the supported range were stored at the bound (${said}) \u2014 sizePt is ${captionBoundText("sizePt")} and maxWidth ${captionBoundText("maxWidth")}, the same range the ShotOps caption controls offer, so nothing here is unreachable in the app.`;
|
|
3091
3189
|
}
|
|
3092
3190
|
function rangeIssueMessage(field, raw) {
|
|
@@ -3137,11 +3235,12 @@ var init_lookSchemas = __esm({
|
|
|
3137
3235
|
rangeText2 = (field) => `${SHOT_LOOK_RANGES[field].min}\u2013${SHOT_LOOK_RANGES[field].max}`;
|
|
3138
3236
|
RANGE_UNITS = {
|
|
3139
3237
|
roll: "degrees of clock-hand tilt",
|
|
3140
|
-
|
|
3238
|
+
deviceSize: "percent of the default device size, 100 = default",
|
|
3141
3239
|
hOffset: "horizontal position, 0 = centre",
|
|
3142
3240
|
vOffset: "vertical position, 0 = centre, positive = lower",
|
|
3143
3241
|
finish: "0 = matte \u2026 1 = glossy",
|
|
3144
|
-
clearcoat: "0 = none \u2026 1 = full clear-coat gloss"
|
|
3242
|
+
clearcoat: "0 = none \u2026 1 = full clear-coat gloss",
|
|
3243
|
+
screenCornerRadius: "percent of the flat card width"
|
|
3145
3244
|
};
|
|
3146
3245
|
PLACEMENT_FIELDS = LAYOUT_POSE_FIELDS;
|
|
3147
3246
|
layoutAlternative = () => ` For a ready-made composition pass \`style.layout\`: ${LAYOUT_TEMPLATE_IDS.map((id) => `"${id}"`).join(" | ")} \u2014 it sets ${LAYOUT_POSE_FIELDS.join("/")} together, resets camera pose + roll, and sets the headline region they were solved against), and any field you also pass explicitly still wins over it.`;
|
|
@@ -3264,7 +3363,7 @@ var init_lookSchemas = __esm({
|
|
|
3264
3363
|
// write door — saveLookForProject in api/_lib/projectLookStore.ts — so this field is purely
|
|
3265
3364
|
// informational on the wire.
|
|
3266
3365
|
schema: z3.number().optional().describe("Look schema version. The server stamps it on every save (save_look); read_look returns it. Any input value is ignored \u2014 never set it yourself."),
|
|
3267
|
-
panelPresetId: z3.string().optional().describe("Canvas size preset, e.g. r69 =
|
|
3366
|
+
panelPresetId: z3.string().optional().describe("Canvas size preset, e.g. r69 = 1320\xD72868. Default r69."),
|
|
3268
3367
|
bgMode: z3.enum(["gradient", "perPanel"]).optional().describe("One gradient across the strip, or a flat colour per panel. Default gradient."),
|
|
3269
3368
|
// #460 decision 6 — UNLIKE render_strip's `style.background` above, the PERSISTED look keeps
|
|
3270
3369
|
// both forms: `gradientStops`/`gradientAngle` are canonical (win when present), and
|
|
@@ -3543,8 +3642,8 @@ var init_describeLookCatalog = __esm({
|
|
|
3543
3642
|
gradientFrom: "#3E3D42",
|
|
3544
3643
|
gradientTo: "#1E1D22",
|
|
3545
3644
|
shots: [
|
|
3546
|
-
{ panelId: "panel-1", look: { material: "real", colorway: "blue", angle: "left", roll: "-19",
|
|
3547
|
-
{ panelId: "panel-1", look: { material: "real", colorway: "orange", angle: "right", roll: "8",
|
|
3645
|
+
{ panelId: "panel-1", look: { material: "real", colorway: "blue", angle: "left", roll: "-19", deviceSize: "90" } },
|
|
3646
|
+
{ panelId: "panel-1", look: { material: "real", colorway: "orange", angle: "right", roll: "8", deviceSize: "105" } }
|
|
3548
3647
|
]
|
|
3549
3648
|
}
|
|
3550
3649
|
},
|
|
@@ -3570,7 +3669,7 @@ var init_describeLookCatalog = __esm({
|
|
|
3570
3669
|
"style.layout PRECEDENCE: the template expands first, then any explicit shotLook composition field or caption layout field you also pass overrides it. Nothing stores the template id \u2014 save_project persists the expanded values and read_look returns them unchanged, so a layout is a starting point exactly as a preset is.",
|
|
3571
3670
|
"style.shotLook is ONE shared style for every phone. To vary ANY devices \u2014 including two phones inside the SAME panel \u2014 pass a `look` instead. `look.shots[]` has one entry per screenshot/device in flattened panel order; repeat panelId for devices that share a panel. See the `look` example.",
|
|
3572
3671
|
"COMPOSE (every real App Store strip): to style devices independently AND caption their panels, pass BOTH `look` (per-device styling via shots[] + background) and `style` (captions via style.captions) in ONE render_strip/emit_bundle call \u2014 they combine. The look supplies the styling; if you also pass style.shotLook/style.background alongside a look they are ignored (the look wins) and a note tells you.",
|
|
3573
|
-
"Fidelity ceilings: `angle` is preset-only (front|left|right) and `roll` is continuous (\xB145\xB0), but there is NO free yaw \u2014 an arbitrary 3-axis source rotation can only be approximated. Device SIZE is set only by `
|
|
3672
|
+
"Fidelity ceilings: `angle` is preset-only (front|left|right) and `roll` is continuous (\xB145\xB0), but there is NO free yaw \u2014 an arbitrary 3-axis source rotation can only be approximated. Device SIZE is set only by `deviceSize` (there is no separate scale, and cameraPos does not resize). Fonts are the 5 in `style.captions.fonts` \u2014 pick the closest; an exact non-bundled typeface match is not possible.",
|
|
3574
3673
|
"save_look appends a look version; hold_look sets which saved version agents render by default (or Hold one in ShotOps\u2019s Version history), and render_strip `version` renders a specific one."
|
|
3575
3674
|
]
|
|
3576
3675
|
};
|
|
@@ -3579,7 +3678,7 @@ var init_describeLookCatalog = __esm({
|
|
|
3579
3678
|
|
|
3580
3679
|
// src/contracts/outputSchemas.ts
|
|
3581
3680
|
import { z as z4 } from "zod";
|
|
3582
|
-
var projectSummary, storedAssetOutput, panelDeliveryOutput, panelOutput, lookHistoryAccess, noteField, creditsField, layoutFindingOutput, layoutAdvisoryOutput, contractField, disclosureField, operationStateOutputSchema, durableRenderFinalOutputSchema, durableBundleFinalOutputSchema, renderStripOutputSchema, optionalShape, operationStateShape, renderStripToolOutputSchema, emitBundleOutputSchema, emitBundleToolOutputSchema, saveProjectOutputSchema, layoutFieldDiff, layoutReportShape, layoutReportOutput, readLookOutputSchema, storedScreenshots, readProjectOutputSchema, agentOutcomeOutput, refineProjectOutputSchema, renderProjectOutputSchema, renderProjectToolOutputSchema, productionOperationOutputSchema, saveLookOutputSchema, holdLookOutputSchema, releaseLookOutputSchema, describeLookOutputSchema, accountCreditBalanceSchema, accountStatusOutputSchema, requestScreenshotUploadOutputSchema, importedScreenshotOutput, importScreenshotOutputSchema, deleteAssetsOutputSchema, advertisedWithFailure, refineProjectToolOutputSchema, saveProjectToolOutputSchema, readLookToolOutputSchema, readProjectToolOutputSchema, saveLookToolOutputSchema, holdLookToolOutputSchema, releaseLookToolOutputSchema, requestScreenshotUploadToolOutputSchema, importScreenshotToolOutputSchema, deleteAssetsToolOutputSchema;
|
|
3681
|
+
var projectSummary, storedAssetOutput, panelDeliveryOutput, panelOutput, lookHistoryAccess, noteField, creditsField, layoutFindingOutput, layoutAdvisoryOutput, contractField, disclosureField, operationStateOutputSchema, durableRenderFinalOutputSchema, durableBundleFinalOutputSchema, renderStripOutputSchema, optionalShape, operationStateShape, renderStripToolOutputSchema, emitBundleOutputSchema, emitBundleToolOutputSchema, appListingOutput, saveProjectOutputSchema, layoutFieldDiff, layoutReportShape, layoutReportOutput, readLookOutputSchema, storedScreenshots, searchAppListingOutputSchema, readProjectOutputSchema, agentOutcomeOutput, refineProjectOutputSchema, renderProjectOutputSchema, renderProjectToolOutputSchema, productionOperationOutputSchema, saveLookOutputSchema, holdLookOutputSchema, releaseLookOutputSchema, describeLookOutputSchema, accountCreditBalanceSchema, accountStatusOutputSchema, requestScreenshotUploadOutputSchema, importedScreenshotOutput, importScreenshotOutputSchema, deleteAssetsOutputSchema, advertisedWithFailure, refineProjectToolOutputSchema, saveProjectToolOutputSchema, readLookToolOutputSchema, readProjectToolOutputSchema, searchAppListingToolOutputSchema, saveLookToolOutputSchema, holdLookToolOutputSchema, releaseLookToolOutputSchema, requestScreenshotUploadToolOutputSchema, importScreenshotToolOutputSchema, deleteAssetsToolOutputSchema;
|
|
3583
3682
|
var init_outputSchemas = __esm({
|
|
3584
3683
|
"src/contracts/outputSchemas.ts"() {
|
|
3585
3684
|
"use strict";
|
|
@@ -3758,6 +3857,20 @@ var init_outputSchemas = __esm({
|
|
|
3758
3857
|
zipRef: z4.string().optional(),
|
|
3759
3858
|
ok: z4.boolean()
|
|
3760
3859
|
}).passthrough();
|
|
3860
|
+
appListingOutput = z4.object({
|
|
3861
|
+
appId: z4.string().describe("Apple\u2019s numeric app ID, as a string. Pass it as save_project\u2019s `appListing`."),
|
|
3862
|
+
bundleId: z4.string(),
|
|
3863
|
+
name: z4.string(),
|
|
3864
|
+
seller: z4.string(),
|
|
3865
|
+
storeUrl: z4.string().optional().describe("The listing\u2019s own App Store page. Carries the storefront, so it is the reference to reuse for a non-US app."),
|
|
3866
|
+
iconUrl: z4.string().optional().describe("The app icon on Apple\u2019s CDN. Shown as project chrome; never downloaded and never rendered into a strip."),
|
|
3867
|
+
locales: z4.array(z4.string()).describe("The App Store Connect locales this listing\u2019s languages map to, in catalog order."),
|
|
3868
|
+
baseLocale: z4.string().describe("The caption base the other locales inherit from. Derived \u2014 Apple never says which language is primary."),
|
|
3869
|
+
outputs: z4.array(z4.string()).describe("The ShotOps output devices this listing fills. iPhone only, and EMPTY for an iPad-only app."),
|
|
3870
|
+
supportsIpad: z4.boolean().describe("The listing supports iPad. Recorded, and applied to no output until iPad panels ship."),
|
|
3871
|
+
storefront: z4.string().describe("The two-letter storefront this was read from. It decides `baseLocale`."),
|
|
3872
|
+
droppedLanguageCodes: z4.array(z4.string()).optional().describe("Apple language codes this build maps no App Store Connect locale for. Omitted when nothing was dropped.")
|
|
3873
|
+
}).passthrough();
|
|
3761
3874
|
saveProjectOutputSchema = z4.object({
|
|
3762
3875
|
ok: z4.boolean(),
|
|
3763
3876
|
// Direct authenticated saves return projectId. Unsigned local saves instead return an
|
|
@@ -3776,6 +3889,10 @@ var init_outputSchemas = __esm({
|
|
|
3776
3889
|
// response stays byte-identical, and a declared-but-absent field never fails the SDK's
|
|
3777
3890
|
// structuredContent validation (the #196 lesson — an output field is never made required).
|
|
3778
3891
|
outputs: z4.array(z4.string()).optional(),
|
|
3892
|
+
// #1004 — the App Store app this save linked the project to, echoed so the caller can see the
|
|
3893
|
+
// identity it now carries without a second read. Emitted ONLY when the call passed an
|
|
3894
|
+
// `appListing` reference; a save that did not link one answers exactly as it did before.
|
|
3895
|
+
linkedApp: appListingOutput.optional(),
|
|
3779
3896
|
// #224 — save_project used to DROP this entirely: it destructured `{ look, style }` out of
|
|
3780
3897
|
// resolveLook and never forwarded the note, on the one tool where a wrong composition is
|
|
3781
3898
|
// PERSISTED rather than merely rendered.
|
|
@@ -3835,13 +3952,21 @@ var init_outputSchemas = __esm({
|
|
|
3835
3952
|
"One row per shot \xD7 target-family \xD7 locale. `resolvedAt` names the manifest cell used; `inherited` says which requested axes fell back. This is the authoritative coverage report."
|
|
3836
3953
|
)
|
|
3837
3954
|
}).passthrough().optional();
|
|
3955
|
+
searchAppListingOutputSchema = z4.object({
|
|
3956
|
+
ok: z4.boolean(),
|
|
3957
|
+
results: z4.array(appListingOutput),
|
|
3958
|
+
message: z4.string()
|
|
3959
|
+
}).passthrough();
|
|
3838
3960
|
readProjectOutputSchema = z4.object({
|
|
3839
3961
|
saved: z4.boolean(),
|
|
3840
3962
|
project: projectSummary.optional(),
|
|
3841
3963
|
message: z4.string().optional(),
|
|
3842
3964
|
layout: layoutReportOutput,
|
|
3843
3965
|
screenshots: storedScreenshots,
|
|
3844
|
-
history: lookHistoryAccess
|
|
3966
|
+
history: lookHistoryAccess,
|
|
3967
|
+
// #1004 — the App Store app this project is linked to. Omitted entirely for a project that
|
|
3968
|
+
// carries none, which is every project created without one.
|
|
3969
|
+
linkedApp: appListingOutput.optional()
|
|
3845
3970
|
}).passthrough();
|
|
3846
3971
|
agentOutcomeOutput = z4.object({
|
|
3847
3972
|
status: z4.enum([
|
|
@@ -4063,6 +4188,7 @@ var init_outputSchemas = __esm({
|
|
|
4063
4188
|
saveProjectToolOutputSchema = advertisedWithFailure(saveProjectOutputSchema);
|
|
4064
4189
|
readLookToolOutputSchema = advertisedWithFailure(readLookOutputSchema);
|
|
4065
4190
|
readProjectToolOutputSchema = advertisedWithFailure(readProjectOutputSchema);
|
|
4191
|
+
searchAppListingToolOutputSchema = advertisedWithFailure(searchAppListingOutputSchema);
|
|
4066
4192
|
saveLookToolOutputSchema = advertisedWithFailure(saveLookOutputSchema);
|
|
4067
4193
|
holdLookToolOutputSchema = advertisedWithFailure(holdLookOutputSchema);
|
|
4068
4194
|
releaseLookToolOutputSchema = advertisedWithFailure(releaseLookOutputSchema);
|
|
@@ -4074,7 +4200,7 @@ var init_outputSchemas = __esm({
|
|
|
4074
4200
|
|
|
4075
4201
|
// src/contracts/schemas.ts
|
|
4076
4202
|
import { z as z5 } from "zod";
|
|
4077
|
-
var nameField, inlineBase64Entry, refEntry, urlEntry, pathEntry, singleScreenshotEntry, localeScreenshotEntry, screenshotEntry, screenshotsDescription, slot, flatScreenshots, groupedScreenshots, screenshots, screenshotsForSaveDescription, screenshotsForSave, screenshotsOptionalDescription, screenshotsOptional, panelRef, pathPanel, panelEntry, localePanelRef, panelEntryUnion, panelsDescription, panelRefs, output, outputDeviceArray, outputDeviceList, outputsDescription, outputsField, projectOutputsDescription, projectOutputsField, clip, preview, idempotencyKey, panelPresetId, useSavedLook, locale, localesField, project, version, createProjectFlag, projectName, sourceDir, waiverShape, waiversField, renderStripShape, emitBundleShape, saveProjectShape, readLookShape, readProjectShape, refineProjectFocus, refineProjectShape, projectRefEntry, projectUrlEntry, projectPathEntry, projectScreenshotEntry, screenshotFileListDescription, screenshotFileList, renderProjectShape, productionOperationShape, SAVE_LOOK_LOOK_DESCRIPTION, saveLookShape, heldVersion, holdLookShape, releaseLookShape, describeLookShape, accountStatusShape, requestScreenshotUploadShape, deleteAssetsShape, FILE_URL_DESCRIPTION, FILE_ID_DESCRIPTION, FILE_MIME_DESCRIPTION, FILE_NAME_DESCRIPTION, FILE_DESCRIPTION, chatGptFile, importSourceName, importUrlSource, importFileSource, importPathSource, importSourceEntry, importScreenshotsDescription, importScreenshotShape, advertisedChatGptFile, advertisedOutputsField, advertisedProjectOutputsField, renderStripAdvertisedShape, emitBundleAdvertisedShape, saveProjectAdvertisedShape, saveLookAdvertisedShape, renderProjectAdvertisedShape, importScreenshotAdvertisedShape, screenshotSlotGuide, screenshotFileGuide, panelGuide, chatGptFileGuide, importSourceGuide, outputDevicesGuide, TOOL_INPUT_GUIDES;
|
|
4203
|
+
var nameField, inlineBase64Entry, refEntry, urlEntry, pathEntry, singleScreenshotEntry, localeScreenshotEntry, screenshotEntry, screenshotsDescription, slot, flatScreenshots, groupedScreenshots, screenshots, screenshotsForSaveDescription, screenshotsForSave, screenshotsOptionalDescription, screenshotsOptional, panelRef, pathPanel, panelEntry, localePanelRef, panelEntryUnion, panelsDescription, panelRefs, output, outputDeviceArray, outputDeviceList, outputsDescription, outputsField, projectOutputsDescription, projectOutputsField, clip, preview, idempotencyKey, panelPresetId, useSavedLook, locale, localesField, project, version, createProjectFlag, projectName, sourceDir, waiverShape, waiversField, renderStripShape, emitBundleShape, appListingReference, saveProjectShape, searchAppListingShape, readLookShape, readProjectShape, refineProjectFocus, refineProjectShape, projectRefEntry, projectUrlEntry, projectPathEntry, projectScreenshotEntry, screenshotFileListDescription, screenshotFileList, renderProjectShape, productionOperationShape, SAVE_LOOK_LOOK_DESCRIPTION, saveLookShape, heldVersion, holdLookShape, releaseLookShape, describeLookShape, accountStatusShape, requestScreenshotUploadShape, deleteAssetsShape, FILE_URL_DESCRIPTION, FILE_ID_DESCRIPTION, FILE_MIME_DESCRIPTION, FILE_NAME_DESCRIPTION, FILE_DESCRIPTION, chatGptFile, importSourceName, importUrlSource, importFileSource, importPathSource, importSourceEntry, importScreenshotsDescription, importScreenshotShape, advertisedChatGptFile, advertisedOutputsField, advertisedProjectOutputsField, renderStripAdvertisedShape, emitBundleAdvertisedShape, saveProjectAdvertisedShape, saveLookAdvertisedShape, renderProjectAdvertisedShape, importScreenshotAdvertisedShape, screenshotSlotGuide, screenshotFileGuide, panelGuide, chatGptFileGuide, importSourceGuide, outputDevicesGuide, TOOL_INPUT_GUIDES;
|
|
4078
4204
|
var init_schemas = __esm({
|
|
4079
4205
|
"src/contracts/schemas.ts"() {
|
|
4080
4206
|
"use strict";
|
|
@@ -4108,7 +4234,7 @@ var init_schemas = __esm({
|
|
|
4108
4234
|
flatScreenshots = z5.array(screenshotEntry).min(1).max(10).describe("Preferred: one screenshot entry per App Store panel, in display order.");
|
|
4109
4235
|
groupedScreenshots = z5.array(slot).min(1).max(10).describe("Advanced: one nested slot per panel; put several screenshot entries in a slot for a multi-phone panel.");
|
|
4110
4236
|
screenshots = z5.union([flatScreenshots, groupedScreenshots]).describe(screenshotsDescription);
|
|
4111
|
-
screenshotsForSaveDescription = `${screenshotsDescription} NOTE: save_project does not render
|
|
4237
|
+
screenshotsForSaveDescription = `${screenshotsDescription} NOTE: save_project does not render and never fetches image bytes. It uses each slot's SHAPE (phones per panel), each entry's \`name\` (source filename, for re-load matching) and each { "ref" }, per-locale ones included, which it records on that entry's own frame so the project reopens with that screenshot. A ref that is not an upload under this account refuses the whole save, naming it, and nothing is written. Pass the same entries you rendered with (esp. their \`name\`s).`;
|
|
4112
4238
|
screenshotsForSave = screenshots.describe(screenshotsForSaveDescription);
|
|
4113
4239
|
screenshotsOptionalDescription = `${screenshotsDescription} Required unless \`panels\` (pre-rendered refs) is given instead.`;
|
|
4114
4240
|
screenshotsOptional = screenshots.optional().describe(screenshotsOptionalDescription);
|
|
@@ -4140,9 +4266,9 @@ var init_schemas = __esm({
|
|
|
4140
4266
|
seen.add(id);
|
|
4141
4267
|
});
|
|
4142
4268
|
});
|
|
4143
|
-
outputsDescription = "Target device outputs to render into one screenshot bundle. Supported now: iphone-6-9 (
|
|
4269
|
+
outputsDescription = "Target device outputs to render into one screenshot bundle. Supported now: iphone-6-9 (1320\xD72868 / APP_IPHONE_67) and iphone-6-5 (1242\xD72688 / APP_IPHONE_65). Each is rendered from the same Look and inputs; omit this field to keep the legacy single panelPresetId form unchanged. With a `project` and no list, the project's OWN saved outputs are used. Not supported with pre-rendered `panels` refs.";
|
|
4144
4270
|
outputsField = outputDeviceList.optional().describe(outputsDescription);
|
|
4145
|
-
projectOutputsDescription = "The target DEVICES this project is for \u2014 iphone-6-9 (
|
|
4271
|
+
projectOutputsDescription = "The target DEVICES this project is for \u2014 iphone-6-9 (1320\xD72868) and/or iphone-6-5 (1242\xD72688), in the order they should be offered. On save_project this is saved ON the project (the same Output list the web app shows) and the first entry becomes the previewed size unless `panelPresetId` says otherwise; on render_project it OVERRIDES the saved set for this call. Omit it and the project's saved outputs are used unchanged.";
|
|
4146
4272
|
projectOutputsField = outputDeviceList.optional().describe(projectOutputsDescription);
|
|
4147
4273
|
clip = z5.enum(["strip", "panel"]).optional().describe(MCP_TOP_LEVEL_CLIP_PRECEDENCE);
|
|
4148
4274
|
preview = z5.boolean().optional().describe(
|
|
@@ -4151,7 +4277,7 @@ var init_schemas = __esm({
|
|
|
4151
4277
|
idempotencyKey = z5.string().min(1).max(200).optional().describe(
|
|
4152
4278
|
"Stable retry key for paid Hosted production. Reuse it only for the identical request; a new key deliberately requests a new render. Omit it for deterministic fingerprint retry safety."
|
|
4153
4279
|
);
|
|
4154
|
-
panelPresetId = z5.enum(RENDERABLE_PANEL_PRESET_IDS).optional().describe("App Store screenshot size. Default r69 (6.9\u2033 iPhone,
|
|
4280
|
+
panelPresetId = z5.enum(RENDERABLE_PANEL_PRESET_IDS).optional().describe("App Store screenshot size. Default r69 (6.9\u2033 iPhone, 1320\xD72868). iPhone sizes only \u2014 iPad is not rendered yet.");
|
|
4155
4281
|
useSavedLook = z5.boolean().optional().describe("If true, style the strip with the project's saved look (ignored when `look`/`style` is given).");
|
|
4156
4282
|
locale = z5.string().optional().describe(
|
|
4157
4283
|
'App Store locale, e.g. "de-DE" (default en-US). Labels the render, selects which { "locales": \u2026 } screenshot variants render, and, for emit_bundle, selects the fastlane screenshots folder. Does not select caption text \u2014 pass the copy for this locale yourself via `style.captions[].text` (see the README\'s `captions.<locale>.json` convention).'
|
|
@@ -4228,6 +4354,9 @@ var init_schemas = __esm({
|
|
|
4228
4354
|
waivers: waiversField,
|
|
4229
4355
|
idempotencyKey
|
|
4230
4356
|
};
|
|
4357
|
+
appListingReference = z5.string().trim().min(1).max(500).optional().describe(
|
|
4358
|
+
'Link this project to its App Store listing: an App Store URL ("https://apps.apple.com/us/app/duolingo/id570060128"), a numeric app ID ("570060128"), or a bundle ID ("com.duolingo.DuolingoMobile"). The server looks the app up and the created project takes its name, bundle ID, languages and iPhone device set. Anything YOU state wins: an explicit `projectName`, `outputs` or `locale` is applied as given and the app is still linked. On an EXISTING `project` it re-links that project and rewrites nothing else \u2014 the language list, the frames and every caption stay exactly as the designer left them. Use the full store URL when the app is listed outside the US storefront; a bare ID is looked up in the US. Needs an account \u2014 an unsigned local server refuses it. Not the same field as `bundleId`, which only pre-fills a fastlane Deliverfile.'
|
|
4359
|
+
);
|
|
4231
4360
|
saveProjectShape = {
|
|
4232
4361
|
screenshots: screenshotsForSave,
|
|
4233
4362
|
panelPresetId,
|
|
@@ -4241,8 +4370,17 @@ var init_schemas = __esm({
|
|
|
4241
4370
|
version,
|
|
4242
4371
|
locale,
|
|
4243
4372
|
projectName,
|
|
4373
|
+
appListing: appListingReference,
|
|
4244
4374
|
sourceDir
|
|
4245
4375
|
};
|
|
4376
|
+
searchAppListingShape = {
|
|
4377
|
+
term: z5.string().trim().min(1).max(200).describe(
|
|
4378
|
+
"What to search the App Store for \u2014 the app name as the user said it (\"Duolingo\"), or the developer's name. Each result carries the app ID to pass as save_project's `appListing`."
|
|
4379
|
+
),
|
|
4380
|
+
country: z5.string().regex(/^[A-Za-z]{2}$/).optional().describe(
|
|
4381
|
+
`The two-letter App Store storefront to search, e.g. "dk". Defaults to "us". An app released in one country only is invisible to a search of any other, so pass the user's own storefront when they say the app is not showing up.`
|
|
4382
|
+
)
|
|
4383
|
+
};
|
|
4246
4384
|
readLookShape = { project };
|
|
4247
4385
|
readProjectShape = { project };
|
|
4248
4386
|
refineProjectFocus = z5.discriminatedUnion("kind", [
|
|
@@ -5195,6 +5333,7 @@ var init_toolRegistry = __esm({
|
|
|
5195
5333
|
"save_project",
|
|
5196
5334
|
"read_look",
|
|
5197
5335
|
"read_project",
|
|
5336
|
+
"search_app_listing",
|
|
5198
5337
|
"render_project",
|
|
5199
5338
|
"refine_project",
|
|
5200
5339
|
"save_look",
|
|
@@ -5213,6 +5352,7 @@ var init_toolRegistry = __esm({
|
|
|
5213
5352
|
save_project: "mutate",
|
|
5214
5353
|
read_look: "read",
|
|
5215
5354
|
read_project: "read",
|
|
5355
|
+
search_app_listing: "read",
|
|
5216
5356
|
render_project: "render",
|
|
5217
5357
|
refine_project: "mutate",
|
|
5218
5358
|
save_look: "mutate",
|
|
@@ -5330,11 +5470,11 @@ function previewUsage(deps, snapshot) {
|
|
|
5330
5470
|
...credits ? publicCreditBalance(credits) : {}
|
|
5331
5471
|
};
|
|
5332
5472
|
}
|
|
5333
|
-
function statusMessage(deps, snapshot,
|
|
5334
|
-
if (
|
|
5473
|
+
function statusMessage(deps, snapshot, step2) {
|
|
5474
|
+
if (step2 === "none") {
|
|
5335
5475
|
return "This connection can produce store-ready output: full-resolution render_strip and render_project, and every emit_bundle form." + (isLocal(deps) ? " Local rendering spends no cloud credits." : "");
|
|
5336
5476
|
}
|
|
5337
|
-
if (
|
|
5477
|
+
if (step2 === "sign_in" && !snapshot.authenticated && deps.anonymous) {
|
|
5338
5478
|
return `This connection is not signed in. It can import screenshots and render up to ${ANONYMOUS.attempts} preview strips of up to ${ANONYMOUS.maxPanels} panels each per ${Math.round(ANONYMOUS.windowSeconds / 86400)} days. ` + productionDenialMessage(snapshot.productionExport, deps);
|
|
5339
5479
|
}
|
|
5340
5480
|
return productionDenialMessage(snapshot.productionExport, deps);
|
|
@@ -5471,6 +5611,11 @@ function operationSupported(deps, tool, account) {
|
|
|
5471
5611
|
return deps.deleteAssets ? true : "hosted_connection";
|
|
5472
5612
|
case "refine_project":
|
|
5473
5613
|
return deps.refineProject ? true : "account";
|
|
5614
|
+
// #1004 — Apple is reached through the control plane on both doors, and an unsigned caller has
|
|
5615
|
+
// no door there. Derived from the bound dep AND the account, so an anonymous hosted connection
|
|
5616
|
+
// is told 'account' rather than being promised a search its own security scheme refuses.
|
|
5617
|
+
case "search_app_listing":
|
|
5618
|
+
return deps.searchAppListing && account ? true : "account";
|
|
5474
5619
|
case "save_project":
|
|
5475
5620
|
return local || account ? true : "account";
|
|
5476
5621
|
default:
|
|
@@ -5480,7 +5625,7 @@ function operationSupported(deps, tool, account) {
|
|
|
5480
5625
|
async function handleAccountStatus(deps) {
|
|
5481
5626
|
const snapshot = await readSnapshot(deps);
|
|
5482
5627
|
const decision = snapshot.productionExport;
|
|
5483
|
-
const
|
|
5628
|
+
const step2 = accountNextStep(decision);
|
|
5484
5629
|
return textResult({
|
|
5485
5630
|
ok: true,
|
|
5486
5631
|
connection: isLocal(deps) ? "local" : "hosted",
|
|
@@ -5506,8 +5651,8 @@ async function handleAccountStatus(deps) {
|
|
|
5506
5651
|
...decision.allowed ? {} : { reason: decision.reason },
|
|
5507
5652
|
...decision.allowed || !decision.requiredPlan ? {} : { requiredPlan: decision.requiredPlan }
|
|
5508
5653
|
},
|
|
5509
|
-
nextStep:
|
|
5510
|
-
message: statusMessage(deps, snapshot,
|
|
5654
|
+
nextStep: step2,
|
|
5655
|
+
message: statusMessage(deps, snapshot, step2),
|
|
5511
5656
|
// #662 — the capability half: accepted inputs, delivery modes, retention classes, operation
|
|
5512
5657
|
// support, the cost model and the closed failure catalog. Additive; every field above is
|
|
5513
5658
|
// exactly what it was.
|
|
@@ -5528,6 +5673,108 @@ var init_toolsAccount = __esm({
|
|
|
5528
5673
|
}
|
|
5529
5674
|
});
|
|
5530
5675
|
|
|
5676
|
+
// ../mockup-engine/appStoreLocales.ts
|
|
5677
|
+
function appStoreLocale(code) {
|
|
5678
|
+
return BY_CODE[code];
|
|
5679
|
+
}
|
|
5680
|
+
var APP_STORE_LOCALE_CATALOG, APP_STORE_LOCALES, BY_CODE;
|
|
5681
|
+
var init_appStoreLocales = __esm({
|
|
5682
|
+
"../mockup-engine/appStoreLocales.ts"() {
|
|
5683
|
+
"use strict";
|
|
5684
|
+
APP_STORE_LOCALE_CATALOG = [
|
|
5685
|
+
{ code: "ar-SA", name: "Arabic", script: "rtl", dir: "rtl" },
|
|
5686
|
+
{ code: "bn-BD", name: "Bengali", script: "indic", dir: "ltr" },
|
|
5687
|
+
{ code: "ca", name: "Catalan", script: "latin", dir: "ltr" },
|
|
5688
|
+
{ code: "cs", name: "Czech", script: "latin", dir: "ltr" },
|
|
5689
|
+
{ code: "da", name: "Danish", script: "latin", dir: "ltr" },
|
|
5690
|
+
{ code: "de-DE", name: "German", script: "latin", dir: "ltr" },
|
|
5691
|
+
{ code: "el", name: "Greek", script: "greek", dir: "ltr" },
|
|
5692
|
+
{ code: "en-AU", name: "English (Australia)", script: "latin", dir: "ltr" },
|
|
5693
|
+
{ code: "en-CA", name: "English (Canada)", script: "latin", dir: "ltr" },
|
|
5694
|
+
{ code: "en-GB", name: "English (U.K.)", script: "latin", dir: "ltr" },
|
|
5695
|
+
{ code: "en-US", name: "English (U.S.)", script: "latin", dir: "ltr" },
|
|
5696
|
+
{ code: "es-ES", name: "Spanish (Spain)", script: "latin", dir: "ltr" },
|
|
5697
|
+
{ code: "es-MX", name: "Spanish (Mexico)", script: "latin", dir: "ltr" },
|
|
5698
|
+
{ code: "fi", name: "Finnish", script: "latin", dir: "ltr" },
|
|
5699
|
+
{ code: "fr-CA", name: "French (Canada)", script: "latin", dir: "ltr" },
|
|
5700
|
+
{ code: "fr-FR", name: "French", script: "latin", dir: "ltr" },
|
|
5701
|
+
{ code: "gu-IN", name: "Gujarati", script: "indic", dir: "ltr" },
|
|
5702
|
+
{ code: "he", name: "Hebrew", script: "rtl", dir: "rtl" },
|
|
5703
|
+
{ code: "hi", name: "Hindi", script: "indic", dir: "ltr" },
|
|
5704
|
+
{ code: "hr", name: "Croatian", script: "latin", dir: "ltr" },
|
|
5705
|
+
{ code: "hu", name: "Hungarian", script: "latin", dir: "ltr" },
|
|
5706
|
+
{ code: "id", name: "Indonesian", script: "latin", dir: "ltr" },
|
|
5707
|
+
{ code: "it", name: "Italian", script: "latin", dir: "ltr" },
|
|
5708
|
+
{ code: "ja", name: "Japanese", script: "cjk", dir: "ltr" },
|
|
5709
|
+
{ code: "kn-IN", name: "Kannada", script: "indic", dir: "ltr" },
|
|
5710
|
+
{ code: "ko", name: "Korean", script: "cjk", dir: "ltr" },
|
|
5711
|
+
{ code: "ml-IN", name: "Malayalam", script: "indic", dir: "ltr" },
|
|
5712
|
+
{ code: "mr-IN", name: "Marathi", script: "indic", dir: "ltr" },
|
|
5713
|
+
{ code: "ms", name: "Malay", script: "latin", dir: "ltr" },
|
|
5714
|
+
{ code: "nl-NL", name: "Dutch", script: "latin", dir: "ltr" },
|
|
5715
|
+
{ code: "no", name: "Norwegian", script: "latin", dir: "ltr" },
|
|
5716
|
+
{ code: "or-IN", name: "Odia", script: "indic", dir: "ltr" },
|
|
5717
|
+
{ code: "pa-IN", name: "Punjabi", script: "indic", dir: "ltr" },
|
|
5718
|
+
{ code: "pl", name: "Polish", script: "latin", dir: "ltr" },
|
|
5719
|
+
{ code: "pt-BR", name: "Portuguese (Brazil)", script: "latin", dir: "ltr" },
|
|
5720
|
+
{ code: "pt-PT", name: "Portuguese (Portugal)", script: "latin", dir: "ltr" },
|
|
5721
|
+
{ code: "ro", name: "Romanian", script: "latin", dir: "ltr" },
|
|
5722
|
+
{ code: "ru", name: "Russian", script: "cyrillic", dir: "ltr" },
|
|
5723
|
+
{ code: "sk", name: "Slovak", script: "latin", dir: "ltr" },
|
|
5724
|
+
{ code: "sl-SI", name: "Slovenian", script: "latin", dir: "ltr" },
|
|
5725
|
+
{ code: "sv", name: "Swedish", script: "latin", dir: "ltr" },
|
|
5726
|
+
{ code: "ta-IN", name: "Tamil", script: "indic", dir: "ltr" },
|
|
5727
|
+
{ code: "te-IN", name: "Telugu", script: "indic", dir: "ltr" },
|
|
5728
|
+
{ code: "th", name: "Thai", script: "thai", dir: "ltr" },
|
|
5729
|
+
{ code: "tr", name: "Turkish", script: "latin", dir: "ltr" },
|
|
5730
|
+
{ code: "uk", name: "Ukrainian", script: "cyrillic", dir: "ltr" },
|
|
5731
|
+
{ code: "ur-PK", name: "Urdu", script: "rtl", dir: "rtl" },
|
|
5732
|
+
{ code: "vi", name: "Vietnamese", script: "latin", dir: "ltr" },
|
|
5733
|
+
{ code: "zh-Hans", name: "Chinese (Simplified)", script: "cjk", dir: "ltr" },
|
|
5734
|
+
{ code: "zh-Hant", name: "Chinese (Traditional)", script: "cjk", dir: "ltr" }
|
|
5735
|
+
];
|
|
5736
|
+
APP_STORE_LOCALES = APP_STORE_LOCALE_CATALOG.map((l) => l.code);
|
|
5737
|
+
BY_CODE = Object.fromEntries(
|
|
5738
|
+
APP_STORE_LOCALE_CATALOG.map((l) => [l.code, l])
|
|
5739
|
+
);
|
|
5740
|
+
}
|
|
5741
|
+
});
|
|
5742
|
+
|
|
5743
|
+
// ../mockup-engine/appstore/appListing.ts
|
|
5744
|
+
function readAppListing(envelope) {
|
|
5745
|
+
if (envelope == null || typeof envelope !== "object" || Array.isArray(envelope)) return null;
|
|
5746
|
+
const stored = envelope[APP_LISTING_ENVELOPE_KEY];
|
|
5747
|
+
if (stored == null || typeof stored !== "object" || Array.isArray(stored)) return null;
|
|
5748
|
+
const value = stored;
|
|
5749
|
+
for (const key of ["trackId", "bundleId", "trackName", "sellerName", "storefront", "fetchedAt"]) {
|
|
5750
|
+
if (typeof value[key] !== "string") return null;
|
|
5751
|
+
}
|
|
5752
|
+
if (!Array.isArray(value.languageCodesISO2A) || !Array.isArray(value.supportedDevices)) return null;
|
|
5753
|
+
if (value.applied == null || typeof value.applied !== "object" || Array.isArray(value.applied)) return null;
|
|
5754
|
+
const applied = value.applied;
|
|
5755
|
+
if (typeof applied.projectName !== "string" || typeof applied.bundleId !== "string") return null;
|
|
5756
|
+
if (typeof applied.baseLocale !== "string" || typeof applied.supportsIpad !== "boolean") return null;
|
|
5757
|
+
for (const key of ["locales", "outputs", "droppedLanguageCodes"]) {
|
|
5758
|
+
if (!Array.isArray(applied[key])) return null;
|
|
5759
|
+
}
|
|
5760
|
+
return stored;
|
|
5761
|
+
}
|
|
5762
|
+
function appListingRewrite(listing2) {
|
|
5763
|
+
return { [APP_LISTING_ENVELOPE_KEY]: listing2 ?? void 0 };
|
|
5764
|
+
}
|
|
5765
|
+
var CATALOG_ORDER, APP_LISTING_ENVELOPE_KEY;
|
|
5766
|
+
var init_appListing = __esm({
|
|
5767
|
+
"../mockup-engine/appstore/appListing.ts"() {
|
|
5768
|
+
"use strict";
|
|
5769
|
+
init_appStoreLocales();
|
|
5770
|
+
init_devices();
|
|
5771
|
+
CATALOG_ORDER = Object.fromEntries(
|
|
5772
|
+
APP_STORE_LOCALE_CATALOG.map((locale2, index) => [locale2.code, index])
|
|
5773
|
+
);
|
|
5774
|
+
APP_LISTING_ENVELOPE_KEY = "appListing";
|
|
5775
|
+
}
|
|
5776
|
+
});
|
|
5777
|
+
|
|
5531
5778
|
// ../mockup-engine/fastlaneBundle.ts
|
|
5532
5779
|
import { strToU8, zipSync } from "fflate";
|
|
5533
5780
|
function render(template, vars) {
|
|
@@ -5850,8 +6097,17 @@ function canonicalPersistence(look2) {
|
|
|
5850
6097
|
}
|
|
5851
6098
|
return canonical;
|
|
5852
6099
|
}
|
|
6100
|
+
function upgradeLegacyShotLook(input) {
|
|
6101
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || !("phoneHeight" in input)) return input;
|
|
6102
|
+
const { phoneHeight, ...rest } = input;
|
|
6103
|
+
if (rest.deviceSize === void 0) {
|
|
6104
|
+
const parsed = typeof phoneHeight === "number" ? phoneHeight : Number.parseFloat(String(phoneHeight ?? ""));
|
|
6105
|
+
if (Number.isFinite(parsed)) rest.deviceSize = String(deviceSizeFromLegacyPhoneHeight(parsed));
|
|
6106
|
+
}
|
|
6107
|
+
return rest;
|
|
6108
|
+
}
|
|
5853
6109
|
function normalizeShotLook(input) {
|
|
5854
|
-
return canonicalPersistence({ ...DEFAULT_LOOK, ...input });
|
|
6110
|
+
return canonicalPersistence({ ...DEFAULT_LOOK, ...upgradeLegacyShotLook(input) });
|
|
5855
6111
|
}
|
|
5856
6112
|
function serializeShotLook(input) {
|
|
5857
6113
|
return canonicalPersistence(normalizeShotLook(input));
|
|
@@ -5862,7 +6118,7 @@ function comparableShotLook(input) {
|
|
|
5862
6118
|
function shotLookNumber(field, raw) {
|
|
5863
6119
|
const { min, max } = SHOT_LOOK_RANGES[field];
|
|
5864
6120
|
const parsed = typeof raw === "number" ? raw : Number.parseFloat(raw ?? "");
|
|
5865
|
-
const value = Number.isFinite(parsed) ? parsed : Number.parseFloat(
|
|
6121
|
+
const value = Number.isFinite(parsed) ? parsed : Number.parseFloat(String(SHOT_LOOK_DOMAIN[field].defaultValue));
|
|
5866
6122
|
return Math.max(min, Math.min(max, value));
|
|
5867
6123
|
}
|
|
5868
6124
|
var init_shotLook = __esm({
|
|
@@ -5916,12 +6172,178 @@ var init_panel = __esm({
|
|
|
5916
6172
|
}
|
|
5917
6173
|
});
|
|
5918
6174
|
|
|
6175
|
+
// ../mockup-engine/storagePath.ts
|
|
6176
|
+
function isSafeStoragePath(path) {
|
|
6177
|
+
if (path.length === 0 || path.startsWith("/") || UNSAFE_CHARACTER.test(path)) return false;
|
|
6178
|
+
const body = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
6179
|
+
return body.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
6180
|
+
}
|
|
6181
|
+
var UNSAFE_CHARACTER;
|
|
6182
|
+
var init_storagePath = __esm({
|
|
6183
|
+
"../mockup-engine/storagePath.ts"() {
|
|
6184
|
+
"use strict";
|
|
6185
|
+
UNSAFE_CHARACTER = /[%\\?#\u0000-\u001f\u007f]/;
|
|
6186
|
+
}
|
|
6187
|
+
});
|
|
6188
|
+
|
|
6189
|
+
// ../mockup-engine/appstore/fingerprint.ts
|
|
6190
|
+
function isScreenshotFingerprint(value) {
|
|
6191
|
+
return typeof value === "string" && SCREENSHOT_FINGERPRINT.test(value);
|
|
6192
|
+
}
|
|
6193
|
+
async function screenshotFingerprint(bytes) {
|
|
6194
|
+
const subtle = globalThis.crypto?.subtle;
|
|
6195
|
+
if (subtle) return hex(new Uint8Array(await subtle.digest("SHA-256", bytes)));
|
|
6196
|
+
return sha256(bytes);
|
|
6197
|
+
}
|
|
6198
|
+
function sha256(input) {
|
|
6199
|
+
const length = input.length;
|
|
6200
|
+
const padded = new Uint8Array(length + 72 >> 6 << 6);
|
|
6201
|
+
padded.set(input);
|
|
6202
|
+
padded[length] = 128;
|
|
6203
|
+
const bits = length * 8;
|
|
6204
|
+
const view = new DataView(padded.buffer);
|
|
6205
|
+
view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
|
|
6206
|
+
view.setUint32(padded.length - 4, bits >>> 0);
|
|
6207
|
+
const h = new Uint32Array([
|
|
6208
|
+
1779033703,
|
|
6209
|
+
3144134277,
|
|
6210
|
+
1013904242,
|
|
6211
|
+
2773480762,
|
|
6212
|
+
1359893119,
|
|
6213
|
+
2600822924,
|
|
6214
|
+
528734635,
|
|
6215
|
+
1541459225
|
|
6216
|
+
]);
|
|
6217
|
+
const w = new Uint32Array(64);
|
|
6218
|
+
for (let block = 0; block < padded.length; block += 64) {
|
|
6219
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(block + i * 4);
|
|
6220
|
+
for (let i = 16; i < 64; i++) {
|
|
6221
|
+
const a2 = w[i - 15];
|
|
6222
|
+
const b2 = w[i - 2];
|
|
6223
|
+
const s0 = rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3;
|
|
6224
|
+
const s1 = rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10;
|
|
6225
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
6226
|
+
}
|
|
6227
|
+
let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
|
|
6228
|
+
for (let i = 0; i < 64; i++) {
|
|
6229
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
6230
|
+
const choose = e & f ^ ~e & g;
|
|
6231
|
+
const t1 = hh + s1 + choose + K[i] + w[i] >>> 0;
|
|
6232
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
6233
|
+
const majority = a & b ^ a & c ^ b & c;
|
|
6234
|
+
const t2 = s0 + majority >>> 0;
|
|
6235
|
+
hh = g;
|
|
6236
|
+
g = f;
|
|
6237
|
+
f = e;
|
|
6238
|
+
e = d + t1 >>> 0;
|
|
6239
|
+
d = c;
|
|
6240
|
+
c = b;
|
|
6241
|
+
b = a;
|
|
6242
|
+
a = t1 + t2 >>> 0;
|
|
6243
|
+
}
|
|
6244
|
+
h[0] = h[0] + a >>> 0;
|
|
6245
|
+
h[1] = h[1] + b >>> 0;
|
|
6246
|
+
h[2] = h[2] + c >>> 0;
|
|
6247
|
+
h[3] = h[3] + d >>> 0;
|
|
6248
|
+
h[4] = h[4] + e >>> 0;
|
|
6249
|
+
h[5] = h[5] + f >>> 0;
|
|
6250
|
+
h[6] = h[6] + g >>> 0;
|
|
6251
|
+
h[7] = h[7] + hh >>> 0;
|
|
6252
|
+
}
|
|
6253
|
+
return hex(bigEndian(h));
|
|
6254
|
+
}
|
|
6255
|
+
function bigEndian(words) {
|
|
6256
|
+
const out = new Uint8Array(words.length * 4);
|
|
6257
|
+
const view = new DataView(out.buffer);
|
|
6258
|
+
for (let i = 0; i < words.length; i++) view.setUint32(i * 4, words[i]);
|
|
6259
|
+
return out;
|
|
6260
|
+
}
|
|
6261
|
+
var SCREENSHOT_FINGERPRINT, hex, K, rotr;
|
|
6262
|
+
var init_fingerprint = __esm({
|
|
6263
|
+
"../mockup-engine/appstore/fingerprint.ts"() {
|
|
6264
|
+
"use strict";
|
|
6265
|
+
SCREENSHOT_FINGERPRINT = /^[a-f0-9]{64}$/;
|
|
6266
|
+
hex = (bytes) => {
|
|
6267
|
+
let out = "";
|
|
6268
|
+
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
|
|
6269
|
+
return out;
|
|
6270
|
+
};
|
|
6271
|
+
K = new Uint32Array([
|
|
6272
|
+
1116352408,
|
|
6273
|
+
1899447441,
|
|
6274
|
+
3049323471,
|
|
6275
|
+
3921009573,
|
|
6276
|
+
961987163,
|
|
6277
|
+
1508970993,
|
|
6278
|
+
2453635748,
|
|
6279
|
+
2870763221,
|
|
6280
|
+
3624381080,
|
|
6281
|
+
310598401,
|
|
6282
|
+
607225278,
|
|
6283
|
+
1426881987,
|
|
6284
|
+
1925078388,
|
|
6285
|
+
2162078206,
|
|
6286
|
+
2614888103,
|
|
6287
|
+
3248222580,
|
|
6288
|
+
3835390401,
|
|
6289
|
+
4022224774,
|
|
6290
|
+
264347078,
|
|
6291
|
+
604807628,
|
|
6292
|
+
770255983,
|
|
6293
|
+
1249150122,
|
|
6294
|
+
1555081692,
|
|
6295
|
+
1996064986,
|
|
6296
|
+
2554220882,
|
|
6297
|
+
2821834349,
|
|
6298
|
+
2952996808,
|
|
6299
|
+
3210313671,
|
|
6300
|
+
3336571891,
|
|
6301
|
+
3584528711,
|
|
6302
|
+
113926993,
|
|
6303
|
+
338241895,
|
|
6304
|
+
666307205,
|
|
6305
|
+
773529912,
|
|
6306
|
+
1294757372,
|
|
6307
|
+
1396182291,
|
|
6308
|
+
1695183700,
|
|
6309
|
+
1986661051,
|
|
6310
|
+
2177026350,
|
|
6311
|
+
2456956037,
|
|
6312
|
+
2730485921,
|
|
6313
|
+
2820302411,
|
|
6314
|
+
3259730800,
|
|
6315
|
+
3345764771,
|
|
6316
|
+
3516065817,
|
|
6317
|
+
3600352804,
|
|
6318
|
+
4094571909,
|
|
6319
|
+
275423344,
|
|
6320
|
+
430227734,
|
|
6321
|
+
506948616,
|
|
6322
|
+
659060556,
|
|
6323
|
+
883997877,
|
|
6324
|
+
958139571,
|
|
6325
|
+
1322822218,
|
|
6326
|
+
1537002063,
|
|
6327
|
+
1747873779,
|
|
6328
|
+
1955562222,
|
|
6329
|
+
2024104815,
|
|
6330
|
+
2227730452,
|
|
6331
|
+
2361852424,
|
|
6332
|
+
2428436474,
|
|
6333
|
+
2756734187,
|
|
6334
|
+
3204031479,
|
|
6335
|
+
3329325298
|
|
6336
|
+
]);
|
|
6337
|
+
rotr = (value, by) => value >>> by | value << 32 - by;
|
|
6338
|
+
}
|
|
6339
|
+
});
|
|
6340
|
+
|
|
5919
6341
|
// ../mockup-engine/appstore/screenManifest.ts
|
|
5920
6342
|
function isShotManifestEntry(entry) {
|
|
5921
6343
|
return typeof entry.shotId === "string";
|
|
5922
6344
|
}
|
|
5923
6345
|
function manifestRefUploader(ref) {
|
|
5924
|
-
if (ref
|
|
6346
|
+
if (!isSafeStoragePath(ref)) return null;
|
|
5925
6347
|
const match = UPLOAD_REF_RE.exec(ref);
|
|
5926
6348
|
if (!match || !UUID_RE.test(match[1])) return null;
|
|
5927
6349
|
return match[1];
|
|
@@ -6019,7 +6441,7 @@ function parseList(raw, mode, baseLocale) {
|
|
|
6019
6441
|
const shotId = e.shotId === void 0 || e.shotId === null ? void 0 : e.shotId;
|
|
6020
6442
|
const slot2 = e.slot === void 0 || e.slot === null ? void 0 : e.slot;
|
|
6021
6443
|
const identified = typeof shotId === "string" && shotId.length > 0 && slot2 === void 0 || typeof slot2 === "string" && slot2.length > 0 && shotId === void 0;
|
|
6022
|
-
if (!identified || typeof e.ref !== "string" || e.ref.length === 0 || variant === null || hash !== void 0 && (
|
|
6444
|
+
if (!identified || typeof e.ref !== "string" || e.ref.length === 0 || variant === null || hash !== void 0 && !isScreenshotFingerprint(hash)) {
|
|
6023
6445
|
if (mode === "strict") return null;
|
|
6024
6446
|
continue;
|
|
6025
6447
|
}
|
|
@@ -6167,25 +6589,13 @@ function rebindManifestToShots(cells, shots) {
|
|
|
6167
6589
|
}
|
|
6168
6590
|
return { manifest, findings };
|
|
6169
6591
|
}
|
|
6170
|
-
function manifestFromNamedRefs(refs, shots) {
|
|
6171
|
-
const idsByFrameName = shotIdsByFrameName(shots);
|
|
6172
|
-
const out = [];
|
|
6173
|
-
for (const [name, ref] of Object.entries(refs)) {
|
|
6174
|
-
const canonical = canonicalizeManifestRef(ref);
|
|
6175
|
-
const ids = idsByFrameName[name];
|
|
6176
|
-
if (ids === void 0) out.push({ slot: name, variant: {}, ref: canonical });
|
|
6177
|
-
else for (const id of ids) out.push({ shotId: id, variant: {}, ref: canonical });
|
|
6178
|
-
}
|
|
6179
|
-
return out;
|
|
6180
|
-
}
|
|
6181
|
-
function nonBaseCellEntries(manifest) {
|
|
6182
|
-
return manifest.filter((e) => variantKey(e.variant) !== "");
|
|
6183
|
-
}
|
|
6184
6592
|
var UPLOAD_REF_RE, UUID_RE, TRUNCATED_NAMED_UPLOAD_REF, EMPTY;
|
|
6185
6593
|
var init_screenManifest = __esm({
|
|
6186
6594
|
"../mockup-engine/appstore/screenManifest.ts"() {
|
|
6187
6595
|
"use strict";
|
|
6596
|
+
init_storagePath();
|
|
6188
6597
|
init_variant();
|
|
6598
|
+
init_fingerprint();
|
|
6189
6599
|
UPLOAD_REF_RE = /^uploads\/([^/]+)\/([^/]+(?:\/[^/]+)*)$/;
|
|
6190
6600
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6191
6601
|
TRUNCATED_NAMED_UPLOAD_REF = /^uploads\/[^/]+\/[^/]+\/\d+__[A-Za-z0-9_-]+$/;
|
|
@@ -6193,6 +6603,23 @@ var init_screenManifest = __esm({
|
|
|
6193
6603
|
}
|
|
6194
6604
|
});
|
|
6195
6605
|
|
|
6606
|
+
// ../mockup-engine/appstore/screenUploadPlan.ts
|
|
6607
|
+
function planScreenUploads(current, manifest, shots) {
|
|
6608
|
+
const byCell = resolveScreenManifest(manifest, shots).byShotCell;
|
|
6609
|
+
const toUpload = [];
|
|
6610
|
+
for (const c of current) {
|
|
6611
|
+
const stored = byCell[manifestShotCellKey(c.shotId, c.variant)];
|
|
6612
|
+
if (stored === void 0 || stored.hash !== c.hash) toUpload.push(c);
|
|
6613
|
+
}
|
|
6614
|
+
return { toUpload, kept: manifest };
|
|
6615
|
+
}
|
|
6616
|
+
var init_screenUploadPlan = __esm({
|
|
6617
|
+
"../mockup-engine/appstore/screenUploadPlan.ts"() {
|
|
6618
|
+
"use strict";
|
|
6619
|
+
init_screenManifest();
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
6622
|
+
|
|
6196
6623
|
// ../mockup-engine/appstore/localeAxis.ts
|
|
6197
6624
|
function deriveLocales(captionText, explicit, baseLocale = DEFAULT_LOCALE) {
|
|
6198
6625
|
const dedupe2 = (list) => {
|
|
@@ -6252,9 +6679,20 @@ function normalizeLegacyLocales(captionText, locales) {
|
|
|
6252
6679
|
}
|
|
6253
6680
|
return { captionText: nextCaption, locales: locales?.map((c) => LEGACY_LOCALE_ALIASES[c] ?? c) };
|
|
6254
6681
|
}
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6682
|
+
function projectLocaleAxis(projectFile) {
|
|
6683
|
+
const file = projectFile ?? {};
|
|
6684
|
+
const baseLocaleRaw = normalizeBaseLocale(file.baseLocale);
|
|
6685
|
+
const explicit = Array.isArray(file.locales) ? file.locales.filter((locale2) => typeof locale2 === "string" && locale2.length > 0) : void 0;
|
|
6686
|
+
const { captionText, locales } = normalizeLegacyLocales(
|
|
6687
|
+
normalizeCaptionText(file.captionText, file.captionStyles, baseLocaleRaw),
|
|
6688
|
+
explicit
|
|
6689
|
+
);
|
|
6690
|
+
const baseLocale = normalizeLegacyLocales({ [baseLocaleRaw]: {} }, [baseLocaleRaw]).locales?.[0] ?? baseLocaleRaw;
|
|
6691
|
+
return { captionText, locales: deriveLocales(captionText, locales, baseLocale), baseLocale };
|
|
6692
|
+
}
|
|
6693
|
+
var LEGACY_LOCALE_ALIASES;
|
|
6694
|
+
var init_localeAxis = __esm({
|
|
6695
|
+
"../mockup-engine/appstore/localeAxis.ts"() {
|
|
6258
6696
|
"use strict";
|
|
6259
6697
|
init_captionModel();
|
|
6260
6698
|
LEGACY_LOCALE_ALIASES = {
|
|
@@ -6552,6 +6990,16 @@ var init_layout = __esm({
|
|
|
6552
6990
|
}
|
|
6553
6991
|
});
|
|
6554
6992
|
|
|
6993
|
+
// ../mockup-engine/appstore/framelessCard.ts
|
|
6994
|
+
var FRAMELESS_BLANK_ASPECT;
|
|
6995
|
+
var init_framelessCard = __esm({
|
|
6996
|
+
"../mockup-engine/appstore/framelessCard.ts"() {
|
|
6997
|
+
"use strict";
|
|
6998
|
+
init_shotLook();
|
|
6999
|
+
FRAMELESS_BLANK_ASPECT = 9 / 19.5;
|
|
7000
|
+
}
|
|
7001
|
+
});
|
|
7002
|
+
|
|
6555
7003
|
// ../mockup-engine/appstore/serialize.ts
|
|
6556
7004
|
function extractStripStyle(state, captionText, baseLocale = DEFAULT_LOCALE) {
|
|
6557
7005
|
const gradient = stripGradientOf(state);
|
|
@@ -6821,20 +7269,15 @@ function restoreProject(json) {
|
|
|
6821
7269
|
const panels = file.panels.map((p) => ({ id: p.id }));
|
|
6822
7270
|
reseedIdCounters(shots.map((s) => s.id), panels.map((p) => p.id));
|
|
6823
7271
|
const selectedShotId = file.selectedShotId && shots.some((s) => s.id === file.selectedShotId) ? file.selectedShotId : shots[0]?.id ?? null;
|
|
6824
|
-
const
|
|
6825
|
-
const
|
|
6826
|
-
normalizeCaptionText(file.captionText, file.captionStyles, baseLocaleRaw),
|
|
6827
|
-
file.locales
|
|
6828
|
-
);
|
|
6829
|
-
const baseLocale = normalizeLegacyLocales({ [baseLocaleRaw]: {} }, [baseLocaleRaw]).locales?.[0] ?? baseLocaleRaw;
|
|
6830
|
-
const locales = deriveLocales(captionText, normalizedLocales, baseLocale);
|
|
7272
|
+
const { captionText, locales, baseLocale } = projectLocaleAxis(file);
|
|
7273
|
+
const output2 = projectOutputAxis(file);
|
|
6831
7274
|
const captionSlotIds2 = normalizeCaptionSlotIds(file.captionSlotIds, captionText, panels.map((panel) => panel.id));
|
|
6832
7275
|
const canonicalGradient = normalizeGradient(file.gradientStops, file.gradientAngle);
|
|
6833
7276
|
return {
|
|
6834
7277
|
shots: shotsInPanelOrder(shots, panels),
|
|
6835
7278
|
panels,
|
|
6836
7279
|
selectedShotId,
|
|
6837
|
-
panelPresetId:
|
|
7280
|
+
panelPresetId: output2.panelPresetId,
|
|
6838
7281
|
bgMode: file.bgMode === "perPanel" ? "perPanel" : "gradient",
|
|
6839
7282
|
// Canonical first, and spread so an old record carries neither key rather than two undefineds.
|
|
6840
7283
|
// #556 — when it IS there, the mirror is DERIVED from it rather than copied off the record: a
|
|
@@ -6868,7 +7311,7 @@ function restoreProject(json) {
|
|
|
6868
7311
|
),
|
|
6869
7312
|
// Stored device list trusted verbatim (unknown/comingSoon ids dropped); an old record with no
|
|
6870
7313
|
// `outputs` migrates from its panelPresetId (deriveOutputs) so it loads with a coherent device.
|
|
6871
|
-
outputs:
|
|
7314
|
+
outputs: output2.outputs,
|
|
6872
7315
|
// The record carries no per-locale bytes (rule 1) — the studio re-joins them from the byte-cache
|
|
6873
7316
|
// by frameName after restore (Phase 3). {} = the pre-join shape.
|
|
6874
7317
|
localeScreens: {},
|
|
@@ -6940,7 +7383,6 @@ function serializeProject(state) {
|
|
|
6940
7383
|
var init_project = __esm({
|
|
6941
7384
|
"../mockup-engine/appstore/project.ts"() {
|
|
6942
7385
|
"use strict";
|
|
6943
|
-
init_presets();
|
|
6944
7386
|
init_devices();
|
|
6945
7387
|
init_shotLook();
|
|
6946
7388
|
init_panel();
|
|
@@ -7818,9 +8260,19 @@ function applicationOutcome(facts) {
|
|
|
7818
8260
|
}
|
|
7819
8261
|
function headlineForChanges(changed) {
|
|
7820
8262
|
const first = changed[0] ?? "Updated this project.";
|
|
8263
|
+
const whole = changed.join(" \xB7 ");
|
|
8264
|
+
if (changed.length <= 1 || whole.length <= AGENT_OUTCOME_BOUNDS.headline) {
|
|
8265
|
+
return boundOutcomeText(changed.length > 0 ? whole : first, AGENT_OUTCOME_BOUNDS.headline);
|
|
8266
|
+
}
|
|
8267
|
+
let kept = changed.length - 1;
|
|
8268
|
+
while (kept > 1) {
|
|
8269
|
+
const rest2 = changed.length - kept;
|
|
8270
|
+
const candidate = `${changed.slice(0, kept).join(" \xB7 ")} (+${rest2} more change${rest2 === 1 ? "" : "s"})`;
|
|
8271
|
+
if (candidate.length <= AGENT_OUTCOME_BOUNDS.headline) return candidate;
|
|
8272
|
+
kept -= 1;
|
|
8273
|
+
}
|
|
7821
8274
|
const rest = changed.length - 1;
|
|
7822
|
-
|
|
7823
|
-
return boundOutcomeText(headline, AGENT_OUTCOME_BOUNDS.headline);
|
|
8275
|
+
return boundOutcomeText(`${first} (+${rest} more change${rest === 1 ? "" : "s"})`, AGENT_OUTCOME_BOUNDS.headline);
|
|
7824
8276
|
}
|
|
7825
8277
|
function failureOutcome(reason, headline) {
|
|
7826
8278
|
return {
|
|
@@ -7970,7 +8422,7 @@ function lookFromAiShotPatch(current, patch, allowHOffset) {
|
|
|
7970
8422
|
}
|
|
7971
8423
|
if (patch.orbit !== void 0) next.cameraPos = posFromAzimuthDeg(normalizedAzimuth(patch.orbit));
|
|
7972
8424
|
if (patch.roll !== void 0) next.roll = String(shotLookNumber("roll", patch.roll));
|
|
7973
|
-
if (patch.
|
|
8425
|
+
if (patch.deviceSize !== void 0) next.deviceSize = String(shotLookNumber("deviceSize", patch.deviceSize));
|
|
7974
8426
|
if (patch.hOffset !== void 0 && allowHOffset) next.hOffset = String(shotLookNumber("hOffset", patch.hOffset));
|
|
7975
8427
|
if (patch.vOffset !== void 0) next.vOffset = String(shotLookNumber("vOffset", patch.vOffset));
|
|
7976
8428
|
if (patch.material !== void 0) next.material = patch.material;
|
|
@@ -7992,6 +8444,13 @@ function lookFromAiShotPatch(current, patch, allowHOffset) {
|
|
|
7992
8444
|
if (patch.reflections !== void 0) next.reflections = patch.reflections;
|
|
7993
8445
|
if (patch.clipToFrame === true) next.clipToFrame = true;
|
|
7994
8446
|
else if (patch.clipToFrame === false) delete next.clipToFrame;
|
|
8447
|
+
if (patch.deviceFrame === false) next.deviceFrame = false;
|
|
8448
|
+
else if (patch.deviceFrame === true) delete next.deviceFrame;
|
|
8449
|
+
if (patch.screenCornerRadius !== void 0) {
|
|
8450
|
+
const radius = String(shotLookNumber("screenCornerRadius", patch.screenCornerRadius));
|
|
8451
|
+
if (radius === SHOT_LOOK_DOMAIN.screenCornerRadius.defaultValue) delete next.screenCornerRadius;
|
|
8452
|
+
else next.screenCornerRadius = radius;
|
|
8453
|
+
}
|
|
7995
8454
|
return JSON.stringify(next) === JSON.stringify(current) ? current : next;
|
|
7996
8455
|
}
|
|
7997
8456
|
function resolveCaptionNumber(input, current, range) {
|
|
@@ -8041,6 +8500,7 @@ var init_refinementValues = __esm({
|
|
|
8041
8500
|
init_gradient();
|
|
8042
8501
|
init_panel();
|
|
8043
8502
|
init_refineIntent();
|
|
8503
|
+
init_defaults();
|
|
8044
8504
|
init_shotLook();
|
|
8045
8505
|
AI_SHOT_LOOK_PROJECTION = {
|
|
8046
8506
|
angle: {
|
|
@@ -8060,10 +8520,10 @@ var init_refinementValues = __esm({
|
|
|
8060
8520
|
reason: "numeric-string",
|
|
8061
8521
|
project: (look2) => ({ roll: shotLookNumber("roll", look2.roll) })
|
|
8062
8522
|
},
|
|
8063
|
-
|
|
8523
|
+
deviceSize: {
|
|
8064
8524
|
disposition: "transformed",
|
|
8065
8525
|
reason: "numeric-string",
|
|
8066
|
-
project: (look2) => ({
|
|
8526
|
+
project: (look2) => ({ deviceSize: shotLookNumber("deviceSize", look2.deviceSize) })
|
|
8067
8527
|
},
|
|
8068
8528
|
hOffset: {
|
|
8069
8529
|
disposition: "transformed",
|
|
@@ -8120,6 +8580,16 @@ var init_refinementValues = __esm({
|
|
|
8120
8580
|
reason: "false-is-absence",
|
|
8121
8581
|
project: (look2) => ({ clipToFrame: look2.clipToFrame === true })
|
|
8122
8582
|
},
|
|
8583
|
+
deviceFrame: {
|
|
8584
|
+
disposition: "transformed",
|
|
8585
|
+
reason: "true-is-default",
|
|
8586
|
+
project: (look2) => ({ deviceFrame: look2.deviceFrame !== false })
|
|
8587
|
+
},
|
|
8588
|
+
screenCornerRadius: {
|
|
8589
|
+
disposition: "transformed",
|
|
8590
|
+
reason: "numeric-string",
|
|
8591
|
+
project: (look2) => ({ screenCornerRadius: shotLookNumber("screenCornerRadius", look2.screenCornerRadius) })
|
|
8592
|
+
},
|
|
8123
8593
|
glare: {
|
|
8124
8594
|
disposition: "direct",
|
|
8125
8595
|
reason: "same-value",
|
|
@@ -8841,7 +9311,7 @@ var TEMPLATE_OWNED_SHOT_FIELDS;
|
|
|
8841
9311
|
var init_templatePoseGuard = __esm({
|
|
8842
9312
|
"../mockup-engine/appstore/templatePoseGuard.ts"() {
|
|
8843
9313
|
"use strict";
|
|
8844
|
-
TEMPLATE_OWNED_SHOT_FIELDS = ["orbit", "roll", "angle", "
|
|
9314
|
+
TEMPLATE_OWNED_SHOT_FIELDS = ["orbit", "roll", "angle", "deviceSize", "hOffset", "vOffset"];
|
|
8845
9315
|
}
|
|
8846
9316
|
});
|
|
8847
9317
|
|
|
@@ -9117,6 +9587,33 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
9117
9587
|
).patch;
|
|
9118
9588
|
}
|
|
9119
9589
|
}
|
|
9590
|
+
const layoutFrames = patch.frames?.length === positionalCount ? patch.frames : void 0;
|
|
9591
|
+
if (layoutFrames?.some((frame) => frame?.layout)) {
|
|
9592
|
+
const panelsByTemplate = {};
|
|
9593
|
+
const order = [];
|
|
9594
|
+
requestedPanelIds.forEach((panelId, index) => {
|
|
9595
|
+
const id = layoutFrames[index]?.layout;
|
|
9596
|
+
if (!id || !surviving.has(panelId)) return;
|
|
9597
|
+
if (!panelsByTemplate[id]) {
|
|
9598
|
+
panelsByTemplate[id] = [];
|
|
9599
|
+
order.push(id);
|
|
9600
|
+
}
|
|
9601
|
+
panelsByTemplate[id].push(panelId);
|
|
9602
|
+
});
|
|
9603
|
+
for (const id of order) {
|
|
9604
|
+
const panelIds2 = panelsByTemplate[id];
|
|
9605
|
+
next = applyLayout(next, id, panelIds2);
|
|
9606
|
+
if (layoutTemplateById(id)) {
|
|
9607
|
+
const shots = next.shots;
|
|
9608
|
+
patch = dropTemplatePoses(
|
|
9609
|
+
patch,
|
|
9610
|
+
new Set(panelIds2),
|
|
9611
|
+
requestedPanelIds,
|
|
9612
|
+
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
9613
|
+
).patch;
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9120
9617
|
if (patch.shots?.length === positionalCount) {
|
|
9121
9618
|
const entries = new Map(requestedPanelIds.map((panelId, index) => [panelId, patch.shots[index]]));
|
|
9122
9619
|
const shared = new Set(
|
|
@@ -9178,11 +9675,16 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
9178
9675
|
...stripBackground.floorReflection !== void 0 ? { floorReflection: stripBackground.floorReflection } : {}
|
|
9179
9676
|
};
|
|
9180
9677
|
}
|
|
9181
|
-
if (frameEntries?.some((frame) => frame.background && Object.keys(frame.background).length > 0)) {
|
|
9678
|
+
if (frameEntries?.some((frame) => frame.clearBackground || frame.background && Object.keys(frame.background).length > 0)) {
|
|
9182
9679
|
const inherited = panelBackgroundFromGradientPatch(stripGradientOf(next));
|
|
9183
9680
|
const panelBackgrounds = { ...next.panelBackgrounds };
|
|
9184
9681
|
let changed = false;
|
|
9185
9682
|
requestedPanelIds.forEach((panelId, index) => {
|
|
9683
|
+
if (frameEntries[index]?.clearBackground && surviving.has(panelId)) {
|
|
9684
|
+
delete panelBackgrounds[panelId];
|
|
9685
|
+
changed = true;
|
|
9686
|
+
return;
|
|
9687
|
+
}
|
|
9186
9688
|
const entry = frameEntries[index]?.background;
|
|
9187
9689
|
if (!entry || Object.keys(entry).length === 0 || !surviving.has(panelId)) return;
|
|
9188
9690
|
const gradient = gradientFromAiBackground(backgroundGradient(panelBackgrounds[panelId] ?? inherited), entry);
|
|
@@ -9193,7 +9695,11 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
9193
9695
|
if (changed) next = { ...next, panelBackgrounds };
|
|
9194
9696
|
}
|
|
9195
9697
|
if (patch.outputs) {
|
|
9196
|
-
const
|
|
9698
|
+
const valid = patch.outputs.filter((id, index, all) => {
|
|
9699
|
+
const device = deviceById(id);
|
|
9700
|
+
return device && !device.comingSoon && all.indexOf(id) === index;
|
|
9701
|
+
});
|
|
9702
|
+
const wanted = valid.length > 0 ? valid : next.outputs;
|
|
9197
9703
|
const outputCaptions = pruneOutputFamilies(next.outputCaptions, wanted);
|
|
9198
9704
|
const outputScreens = next.outputScreens === void 0 ? void 0 : pruneOutputFamilies(next.outputScreens, wanted);
|
|
9199
9705
|
const previewPresetId = next.previewPresetId === void 0 ? void 0 : wanted.some((id) => deviceById(id)?.panelPresetId === next.previewPresetId) ? next.previewPresetId : deviceById(wanted[0])?.panelPresetId ?? next.previewPresetId;
|
|
@@ -9259,73 +9765,6 @@ var init_refinement = __esm({
|
|
|
9259
9765
|
}
|
|
9260
9766
|
});
|
|
9261
9767
|
|
|
9262
|
-
// ../mockup-engine/appStoreLocales.ts
|
|
9263
|
-
function appStoreLocale(code) {
|
|
9264
|
-
return BY_CODE[code];
|
|
9265
|
-
}
|
|
9266
|
-
var APP_STORE_LOCALE_CATALOG, APP_STORE_LOCALES, BY_CODE;
|
|
9267
|
-
var init_appStoreLocales = __esm({
|
|
9268
|
-
"../mockup-engine/appStoreLocales.ts"() {
|
|
9269
|
-
"use strict";
|
|
9270
|
-
APP_STORE_LOCALE_CATALOG = [
|
|
9271
|
-
{ code: "ar-SA", name: "Arabic", script: "rtl", dir: "rtl" },
|
|
9272
|
-
{ code: "bn-BD", name: "Bengali", script: "indic", dir: "ltr" },
|
|
9273
|
-
{ code: "ca", name: "Catalan", script: "latin", dir: "ltr" },
|
|
9274
|
-
{ code: "cs", name: "Czech", script: "latin", dir: "ltr" },
|
|
9275
|
-
{ code: "da", name: "Danish", script: "latin", dir: "ltr" },
|
|
9276
|
-
{ code: "de-DE", name: "German", script: "latin", dir: "ltr" },
|
|
9277
|
-
{ code: "el", name: "Greek", script: "greek", dir: "ltr" },
|
|
9278
|
-
{ code: "en-AU", name: "English (Australia)", script: "latin", dir: "ltr" },
|
|
9279
|
-
{ code: "en-CA", name: "English (Canada)", script: "latin", dir: "ltr" },
|
|
9280
|
-
{ code: "en-GB", name: "English (U.K.)", script: "latin", dir: "ltr" },
|
|
9281
|
-
{ code: "en-US", name: "English (U.S.)", script: "latin", dir: "ltr" },
|
|
9282
|
-
{ code: "es-ES", name: "Spanish (Spain)", script: "latin", dir: "ltr" },
|
|
9283
|
-
{ code: "es-MX", name: "Spanish (Mexico)", script: "latin", dir: "ltr" },
|
|
9284
|
-
{ code: "fi", name: "Finnish", script: "latin", dir: "ltr" },
|
|
9285
|
-
{ code: "fr-CA", name: "French (Canada)", script: "latin", dir: "ltr" },
|
|
9286
|
-
{ code: "fr-FR", name: "French", script: "latin", dir: "ltr" },
|
|
9287
|
-
{ code: "gu-IN", name: "Gujarati", script: "indic", dir: "ltr" },
|
|
9288
|
-
{ code: "he", name: "Hebrew", script: "rtl", dir: "rtl" },
|
|
9289
|
-
{ code: "hi", name: "Hindi", script: "indic", dir: "ltr" },
|
|
9290
|
-
{ code: "hr", name: "Croatian", script: "latin", dir: "ltr" },
|
|
9291
|
-
{ code: "hu", name: "Hungarian", script: "latin", dir: "ltr" },
|
|
9292
|
-
{ code: "id", name: "Indonesian", script: "latin", dir: "ltr" },
|
|
9293
|
-
{ code: "it", name: "Italian", script: "latin", dir: "ltr" },
|
|
9294
|
-
{ code: "ja", name: "Japanese", script: "cjk", dir: "ltr" },
|
|
9295
|
-
{ code: "kn-IN", name: "Kannada", script: "indic", dir: "ltr" },
|
|
9296
|
-
{ code: "ko", name: "Korean", script: "cjk", dir: "ltr" },
|
|
9297
|
-
{ code: "ml-IN", name: "Malayalam", script: "indic", dir: "ltr" },
|
|
9298
|
-
{ code: "mr-IN", name: "Marathi", script: "indic", dir: "ltr" },
|
|
9299
|
-
{ code: "ms", name: "Malay", script: "latin", dir: "ltr" },
|
|
9300
|
-
{ code: "nl-NL", name: "Dutch", script: "latin", dir: "ltr" },
|
|
9301
|
-
{ code: "no", name: "Norwegian", script: "latin", dir: "ltr" },
|
|
9302
|
-
{ code: "or-IN", name: "Odia", script: "indic", dir: "ltr" },
|
|
9303
|
-
{ code: "pa-IN", name: "Punjabi", script: "indic", dir: "ltr" },
|
|
9304
|
-
{ code: "pl", name: "Polish", script: "latin", dir: "ltr" },
|
|
9305
|
-
{ code: "pt-BR", name: "Portuguese (Brazil)", script: "latin", dir: "ltr" },
|
|
9306
|
-
{ code: "pt-PT", name: "Portuguese (Portugal)", script: "latin", dir: "ltr" },
|
|
9307
|
-
{ code: "ro", name: "Romanian", script: "latin", dir: "ltr" },
|
|
9308
|
-
{ code: "ru", name: "Russian", script: "cyrillic", dir: "ltr" },
|
|
9309
|
-
{ code: "sk", name: "Slovak", script: "latin", dir: "ltr" },
|
|
9310
|
-
{ code: "sl-SI", name: "Slovenian", script: "latin", dir: "ltr" },
|
|
9311
|
-
{ code: "sv", name: "Swedish", script: "latin", dir: "ltr" },
|
|
9312
|
-
{ code: "ta-IN", name: "Tamil", script: "indic", dir: "ltr" },
|
|
9313
|
-
{ code: "te-IN", name: "Telugu", script: "indic", dir: "ltr" },
|
|
9314
|
-
{ code: "th", name: "Thai", script: "thai", dir: "ltr" },
|
|
9315
|
-
{ code: "tr", name: "Turkish", script: "latin", dir: "ltr" },
|
|
9316
|
-
{ code: "uk", name: "Ukrainian", script: "cyrillic", dir: "ltr" },
|
|
9317
|
-
{ code: "ur-PK", name: "Urdu", script: "rtl", dir: "rtl" },
|
|
9318
|
-
{ code: "vi", name: "Vietnamese", script: "latin", dir: "ltr" },
|
|
9319
|
-
{ code: "zh-Hans", name: "Chinese (Simplified)", script: "cjk", dir: "ltr" },
|
|
9320
|
-
{ code: "zh-Hant", name: "Chinese (Traditional)", script: "cjk", dir: "ltr" }
|
|
9321
|
-
];
|
|
9322
|
-
APP_STORE_LOCALES = APP_STORE_LOCALE_CATALOG.map((l) => l.code);
|
|
9323
|
-
BY_CODE = Object.fromEntries(
|
|
9324
|
-
APP_STORE_LOCALE_CATALOG.map((l) => [l.code, l])
|
|
9325
|
-
);
|
|
9326
|
-
}
|
|
9327
|
-
});
|
|
9328
|
-
|
|
9329
9768
|
// ../mockup-engine/appstore/agentTurnRepetition.ts
|
|
9330
9769
|
function appliedOperationOf(operation) {
|
|
9331
9770
|
if (!NON_REPEATABLE_OPS.has(operation.op)) return null;
|
|
@@ -9400,7 +9839,360 @@ var init_agentTurnRepetition = __esm({
|
|
|
9400
9839
|
}
|
|
9401
9840
|
});
|
|
9402
9841
|
|
|
9842
|
+
// ../mockup-engine/appstore/agentTurnAdjustments.ts
|
|
9843
|
+
function step(field, from, to) {
|
|
9844
|
+
if (field !== "orbit") return to - from;
|
|
9845
|
+
return ((to - from) % 360 + 540) % 360 - 180;
|
|
9846
|
+
}
|
|
9847
|
+
function pushesFurther(field, start, now, proposed) {
|
|
9848
|
+
if (typeof start !== "number" || typeof now !== "number" || typeof proposed !== "number") return false;
|
|
9849
|
+
const moved = step(field, start, now);
|
|
9850
|
+
const next = step(field, now, proposed);
|
|
9851
|
+
return moved !== 0 && next !== 0 && Math.sign(moved) === Math.sign(next);
|
|
9852
|
+
}
|
|
9853
|
+
function deviceLooks(state) {
|
|
9854
|
+
const looks = {};
|
|
9855
|
+
for (const shot of state.shots) looks[shot.id] = aiShotPatchFromLook(shot.look);
|
|
9856
|
+
return looks;
|
|
9857
|
+
}
|
|
9858
|
+
function deviceRepeats(entry, deviceId, start, now) {
|
|
9859
|
+
const from = start[deviceId];
|
|
9860
|
+
const at = now[deviceId];
|
|
9861
|
+
if (!from || !at) return [];
|
|
9862
|
+
return ADJUSTABLE_DEVICE_FIELDS.filter((field) => pushesFurther(field, from[field], at[field], entry[field]));
|
|
9863
|
+
}
|
|
9864
|
+
function withoutFields(entry, fields2) {
|
|
9865
|
+
const kept = { ...entry };
|
|
9866
|
+
for (const field of fields2) delete kept[field];
|
|
9867
|
+
return kept;
|
|
9868
|
+
}
|
|
9869
|
+
function baseLayers(state, panelId) {
|
|
9870
|
+
return state.captionText[state.baseLocale]?.[panelId];
|
|
9871
|
+
}
|
|
9872
|
+
function effectiveCaptionValue(layer, field) {
|
|
9873
|
+
const value = layer[field];
|
|
9874
|
+
if (typeof value === "number") return value;
|
|
9875
|
+
switch (field) {
|
|
9876
|
+
case "lineHeight":
|
|
9877
|
+
case "bandInset":
|
|
9878
|
+
case "bandCenter":
|
|
9879
|
+
return CAPTION_STYLE_DEFAULTS[field];
|
|
9880
|
+
case "reserveLines":
|
|
9881
|
+
return layer.text.split("\n").length;
|
|
9882
|
+
default:
|
|
9883
|
+
return void 0;
|
|
9884
|
+
}
|
|
9885
|
+
}
|
|
9886
|
+
function captionRepeats(style2, start, now) {
|
|
9887
|
+
if (!start || !now || start.length !== now.length || now.length === 0) return [];
|
|
9888
|
+
return ADJUSTABLE_CAPTION_FIELDS.filter((field) => style2[field] !== void 0 && now.some((layer, index) => {
|
|
9889
|
+
const applied = captionLayerFromAiPatch(layer, { [field]: style2[field] });
|
|
9890
|
+
if (applied === layer) return false;
|
|
9891
|
+
return pushesFurther(
|
|
9892
|
+
field,
|
|
9893
|
+
effectiveCaptionValue(start[index], field),
|
|
9894
|
+
effectiveCaptionValue(layer, field),
|
|
9895
|
+
effectiveCaptionValue(applied, field)
|
|
9896
|
+
);
|
|
9897
|
+
}));
|
|
9898
|
+
}
|
|
9899
|
+
function dropRepeatedAdjustments(patch, context) {
|
|
9900
|
+
const dropped = [];
|
|
9901
|
+
const start = deviceLooks(context.start);
|
|
9902
|
+
const now = deviceLooks(context.now);
|
|
9903
|
+
const repairing = (panelId) => panelId !== void 0 && (context.repairing?.has(panelId) ?? false);
|
|
9904
|
+
const panelOfDevice = (deviceId) => context.now.shots.find((shot) => shot.id === deviceId)?.panelId;
|
|
9905
|
+
let next = patch;
|
|
9906
|
+
if (patch.devices) {
|
|
9907
|
+
const devices = [];
|
|
9908
|
+
let changed = false;
|
|
9909
|
+
for (const entry of patch.devices) {
|
|
9910
|
+
const repeats = repairing(panelOfDevice(entry.deviceId)) ? [] : deviceRepeats(entry.patch, entry.deviceId, start, now);
|
|
9911
|
+
if (repeats.length === 0) {
|
|
9912
|
+
devices.push(entry);
|
|
9913
|
+
continue;
|
|
9914
|
+
}
|
|
9915
|
+
changed = true;
|
|
9916
|
+
for (const field of repeats) dropped.push({ target: entry.deviceId, field });
|
|
9917
|
+
const kept = withoutFields(entry.patch, repeats);
|
|
9918
|
+
if (Object.keys(kept).length > 0) devices.push({ ...entry, patch: kept });
|
|
9919
|
+
}
|
|
9920
|
+
if (changed) {
|
|
9921
|
+
const { devices: _replaced, ...rest } = next;
|
|
9922
|
+
void _replaced;
|
|
9923
|
+
next = devices.length > 0 ? { ...rest, devices } : rest;
|
|
9924
|
+
}
|
|
9925
|
+
}
|
|
9926
|
+
if (patch.shots && patch.shots.length === context.panelIds.length) {
|
|
9927
|
+
let changed = false;
|
|
9928
|
+
const shots = patch.shots.map((entry, index) => {
|
|
9929
|
+
const panelId = context.panelIds[index];
|
|
9930
|
+
if (repairing(panelId)) return entry;
|
|
9931
|
+
const repeats = [];
|
|
9932
|
+
for (const shot of context.now.shots) {
|
|
9933
|
+
if (shot.panelId !== panelId) continue;
|
|
9934
|
+
for (const field of deviceRepeats(entry, shot.id, start, now)) {
|
|
9935
|
+
if (repeats.indexOf(field) < 0) repeats.push(field);
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
if (repeats.length === 0) return entry;
|
|
9939
|
+
changed = true;
|
|
9940
|
+
for (const field of repeats) dropped.push({ target: `${panelId}#devices`, field });
|
|
9941
|
+
return withoutFields(entry, repeats);
|
|
9942
|
+
});
|
|
9943
|
+
if (changed) next = { ...next, shots };
|
|
9944
|
+
}
|
|
9945
|
+
const frames = patch.frames && patch.frames.length === context.panelIds.length ? patch.frames : void 0;
|
|
9946
|
+
if (frames) {
|
|
9947
|
+
let changed = false;
|
|
9948
|
+
const kept = frames.map((entry, index) => {
|
|
9949
|
+
const style2 = entry.captionStyle;
|
|
9950
|
+
const panelId = context.panelIds[index];
|
|
9951
|
+
if (!style2 || repairing(panelId)) return entry;
|
|
9952
|
+
const repeats = captionRepeats(style2, baseLayers(context.start, panelId), baseLayers(context.now, panelId));
|
|
9953
|
+
if (repeats.length === 0) return entry;
|
|
9954
|
+
changed = true;
|
|
9955
|
+
for (const field of repeats) dropped.push({ target: `${panelId}#captions`, field });
|
|
9956
|
+
const captionStyle = withoutFields(style2, repeats);
|
|
9957
|
+
const { captionStyle: _replaced, ...rest } = entry;
|
|
9958
|
+
void _replaced;
|
|
9959
|
+
return Object.keys(captionStyle).length > 0 ? { ...rest, captionStyle } : rest;
|
|
9960
|
+
});
|
|
9961
|
+
if (changed) next = { ...next, frames: kept };
|
|
9962
|
+
}
|
|
9963
|
+
if (patch.captionStyle && !context.panelIds.some(repairing)) {
|
|
9964
|
+
const stripWide = patch.captionStyle;
|
|
9965
|
+
const repeats = [];
|
|
9966
|
+
context.panelIds.forEach((panelId, index) => {
|
|
9967
|
+
const own = frames?.[index]?.captionStyle ?? {};
|
|
9968
|
+
const reaching = withoutFields(stripWide, Object.keys(own));
|
|
9969
|
+
const found = captionRepeats(reaching, baseLayers(context.start, panelId), baseLayers(context.now, panelId));
|
|
9970
|
+
for (const field of found) {
|
|
9971
|
+
if (repeats.indexOf(field) < 0) repeats.push(field);
|
|
9972
|
+
}
|
|
9973
|
+
});
|
|
9974
|
+
if (repeats.length > 0) {
|
|
9975
|
+
for (const field of repeats) dropped.push({ target: "strip#captions", field });
|
|
9976
|
+
const captionStyle = withoutFields(stripWide, repeats);
|
|
9977
|
+
const { captionStyle: _replaced, ...rest } = next;
|
|
9978
|
+
void _replaced;
|
|
9979
|
+
next = Object.keys(captionStyle).length > 0 ? { ...rest, captionStyle } : rest;
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9982
|
+
return { patch: dropped.length > 0 ? next : patch, dropped };
|
|
9983
|
+
}
|
|
9984
|
+
function carriesSomething(value) {
|
|
9985
|
+
if (value === void 0 || value === null) return false;
|
|
9986
|
+
if (Array.isArray(value)) return value.some(carriesSomething);
|
|
9987
|
+
if (typeof value === "object") return Object.keys(value).some((key) => carriesSomething(value[key]));
|
|
9988
|
+
return true;
|
|
9989
|
+
}
|
|
9990
|
+
function patchChangesSomething(patch) {
|
|
9991
|
+
return Object.keys(patch).some((key) => key !== "summary" && carriesSomething(patch[key]));
|
|
9992
|
+
}
|
|
9993
|
+
var ADJUSTABLE_DEVICE_FIELDS, ADJUSTABLE_CAPTION_FIELDS;
|
|
9994
|
+
var init_agentTurnAdjustments = __esm({
|
|
9995
|
+
"../mockup-engine/appstore/agentTurnAdjustments.ts"() {
|
|
9996
|
+
"use strict";
|
|
9997
|
+
init_refinementValues();
|
|
9998
|
+
init_caption();
|
|
9999
|
+
ADJUSTABLE_DEVICE_FIELDS = [
|
|
10000
|
+
"orbit",
|
|
10001
|
+
"roll",
|
|
10002
|
+
"deviceSize",
|
|
10003
|
+
"hOffset",
|
|
10004
|
+
"vOffset",
|
|
10005
|
+
"finish",
|
|
10006
|
+
"clearcoat",
|
|
10007
|
+
"screenCornerRadius"
|
|
10008
|
+
];
|
|
10009
|
+
ADJUSTABLE_CAPTION_FIELDS = ["sizePt", "maxWidth", "lineHeight", "bandInset", "bandCenter", "reserveLines"];
|
|
10010
|
+
}
|
|
10011
|
+
});
|
|
10012
|
+
|
|
10013
|
+
// ../mockup-engine/appstore/agentTurnLanded.ts
|
|
10014
|
+
function sameDeviceValue(field, proposed, applied) {
|
|
10015
|
+
if (field !== "orbit") return near(proposed, applied);
|
|
10016
|
+
const wrapped = ((proposed + 180) % 360 + 360) % 360 - 180;
|
|
10017
|
+
return near(wrapped, applied, 0.5) || near(Math.abs(wrapped), 180, 0.5) && near(Math.abs(applied), 180, 0.5);
|
|
10018
|
+
}
|
|
10019
|
+
function deviceDifference(field, proposed, applied, sharedOffset) {
|
|
10020
|
+
if (sameDeviceValue(field, proposed, applied)) return null;
|
|
10021
|
+
if (field === "hOffset" && sharedOffset) return "shared_offset";
|
|
10022
|
+
const range = field === "orbit" ? void 0 : SHOT_LOOK_RANGES[field];
|
|
10023
|
+
if (range && (proposed < range.min || proposed > range.max) && near(clamp(proposed, range), applied)) return "held_at_limit";
|
|
10024
|
+
return "set_differently";
|
|
10025
|
+
}
|
|
10026
|
+
function deviceDifferences(entry, applied, sharedOffset, out) {
|
|
10027
|
+
for (const field of DEVICE_NUMBERS) {
|
|
10028
|
+
const proposed = entry[field];
|
|
10029
|
+
const landed = applied[field];
|
|
10030
|
+
if (typeof proposed !== "number" || typeof landed !== "number") continue;
|
|
10031
|
+
const difference = deviceDifference(field, proposed, landed, sharedOffset);
|
|
10032
|
+
if (difference) out.add(difference);
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
10035
|
+
function expectedCaptionValue(field, proposed, current) {
|
|
10036
|
+
const raw = typeof proposed === "number" ? proposed : current === void 0 ? void 0 : current * clamp(proposed.scale, AI_SCALE_RANGE);
|
|
10037
|
+
return raw !== void 0 && field === "reserveLines" ? Math.round(raw) : raw;
|
|
10038
|
+
}
|
|
10039
|
+
function captionDifferences(style2, before, after, alignedByPosition, out) {
|
|
10040
|
+
const stated = CAPTION_NUMBERS.filter((field) => style2[field] !== void 0);
|
|
10041
|
+
if (stated.length === 0) return;
|
|
10042
|
+
if (!after || after.length === 0) {
|
|
10043
|
+
out.add("set_differently");
|
|
10044
|
+
return;
|
|
10045
|
+
}
|
|
10046
|
+
for (const field of stated) {
|
|
10047
|
+
const proposed = style2[field];
|
|
10048
|
+
if (typeof proposed !== "number" && !alignedByPosition) continue;
|
|
10049
|
+
after.forEach((layer, index) => {
|
|
10050
|
+
const expected = expectedCaptionValue(field, proposed, typeof proposed === "number" ? void 0 : before?.[index]?.[field]);
|
|
10051
|
+
const landed = layer[field];
|
|
10052
|
+
if (expected === void 0) {
|
|
10053
|
+
out.add("set_differently");
|
|
10054
|
+
return;
|
|
10055
|
+
}
|
|
10056
|
+
if (typeof landed === "number" && near(landed, expected)) return;
|
|
10057
|
+
const range = CAPTION_STYLE_BOUNDS[field];
|
|
10058
|
+
const limited = field === "reserveLines" ? Math.round(clamp(expected, range)) : clamp(expected, range);
|
|
10059
|
+
out.add(typeof landed === "number" && !near(limited, expected) && near(landed, limited) ? "held_at_limit" : "set_differently");
|
|
10060
|
+
});
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
function landedDifferences(before, after, patch, panelIds) {
|
|
10064
|
+
const out = /* @__PURE__ */ new Set();
|
|
10065
|
+
const lookOf = (shotId) => {
|
|
10066
|
+
const shot = after.shots.find((candidate) => candidate.id === shotId);
|
|
10067
|
+
return shot ? aiShotPatchFromLook(shot.look) : null;
|
|
10068
|
+
};
|
|
10069
|
+
for (const entry of patch.devices ?? []) {
|
|
10070
|
+
const applied = lookOf(entry.deviceId);
|
|
10071
|
+
if (applied) deviceDifferences(entry.patch, applied, false, out);
|
|
10072
|
+
}
|
|
10073
|
+
if (patch.shots && patch.shots.length === panelIds.length) {
|
|
10074
|
+
patch.shots.forEach((entry, index) => {
|
|
10075
|
+
const shots = after.shots.filter((shot) => shot.panelId === panelIds[index]);
|
|
10076
|
+
for (const shot of shots) deviceDifferences(entry, aiShotPatchFromLook(shot.look), shots.length > 1, out);
|
|
10077
|
+
});
|
|
10078
|
+
}
|
|
10079
|
+
const frames = patch.frames && patch.frames.length === panelIds.length ? patch.frames : void 0;
|
|
10080
|
+
if (patch.captionStyle || frames) {
|
|
10081
|
+
const beforeSlots = normalizeCaptionSlotIds(before.captionSlotIds, before.captionText, before.panels.map((panel) => panel.id));
|
|
10082
|
+
const afterSlots = normalizeCaptionSlotIds(after.captionSlotIds, after.captionText, after.panels.map((panel) => panel.id));
|
|
10083
|
+
panelIds.forEach((panelId, index) => {
|
|
10084
|
+
const style2 = { ...patch.captionStyle, ...frames?.[index]?.captionStyle };
|
|
10085
|
+
const aligned = (beforeSlots[panelId] ?? []).join("\0") === (afterSlots[panelId] ?? []).join("\0");
|
|
10086
|
+
captionDifferences(
|
|
10087
|
+
style2,
|
|
10088
|
+
before.captionText[before.baseLocale]?.[panelId],
|
|
10089
|
+
after.captionText[after.baseLocale]?.[panelId],
|
|
10090
|
+
aligned,
|
|
10091
|
+
out
|
|
10092
|
+
);
|
|
10093
|
+
});
|
|
10094
|
+
}
|
|
10095
|
+
return out;
|
|
10096
|
+
}
|
|
10097
|
+
var DEVICE_NUMBERS, CAPTION_NUMBERS, clamp, near;
|
|
10098
|
+
var init_agentTurnLanded = __esm({
|
|
10099
|
+
"../mockup-engine/appstore/agentTurnLanded.ts"() {
|
|
10100
|
+
"use strict";
|
|
10101
|
+
init_caption();
|
|
10102
|
+
init_defaults();
|
|
10103
|
+
init_frameTemplates();
|
|
10104
|
+
init_refineIntent();
|
|
10105
|
+
init_refinementValues();
|
|
10106
|
+
DEVICE_NUMBERS = ["orbit", "roll", "deviceSize", "hOffset", "vOffset", "finish", "clearcoat", "screenCornerRadius"];
|
|
10107
|
+
CAPTION_NUMBERS = ["sizePt", "maxWidth", "lineHeight", "bandInset", "bandCenter", "reserveLines"];
|
|
10108
|
+
clamp = (value, range) => Math.min(Math.max(value, range.min), range.max);
|
|
10109
|
+
near = (a, b, tolerance = 1e-9) => Math.abs(a - b) <= tolerance;
|
|
10110
|
+
}
|
|
10111
|
+
});
|
|
10112
|
+
|
|
9403
10113
|
// ../mockup-engine/appstore/agentTurnCoordinator.ts
|
|
10114
|
+
function passEffect(working, application, patch, panelIds, notApplied, focus) {
|
|
10115
|
+
const landed = landedDifferences(working, application.state, patch, panelIds);
|
|
10116
|
+
if (notApplied.size === 0 && landed.size === 0) return null;
|
|
10117
|
+
const applied = [...describeRefinementEffect(working, application.state), ...describeAssetBindings(application.assetBindings)];
|
|
10118
|
+
const lines = [applied.length > 0 ? `${applied.join(" \xB7 ")}.` : "Nothing changed in this pass."];
|
|
10119
|
+
if (notApplied.size > 0) lines.push(`Not applied: ${Array.from(notApplied).map((reason) => NOT_APPLIED[reason]).join("; ")}.`);
|
|
10120
|
+
if (landed.size > 0) lines.push(`Not applied as proposed: ${Array.from(landed).map((reason) => LANDED_DIFFERENTLY[reason]).join("; ")}.`);
|
|
10121
|
+
return {
|
|
10122
|
+
summary: lines.join(" "),
|
|
10123
|
+
reachesOutsideFocus: changedOutsideFocus(working, application.state, focus)
|
|
10124
|
+
};
|
|
10125
|
+
}
|
|
10126
|
+
function emptiedPassEffect(notApplied) {
|
|
10127
|
+
return {
|
|
10128
|
+
summary: `Nothing changed in this pass. Not applied: ${Array.from(notApplied).map((reason) => NOT_APPLIED[reason]).join("; ")}.`,
|
|
10129
|
+
reachesOutsideFocus: false
|
|
10130
|
+
};
|
|
10131
|
+
}
|
|
10132
|
+
function changedOutsideFocus(before, after, focus) {
|
|
10133
|
+
if (focus.kind === "project") return false;
|
|
10134
|
+
const frameId = focus.frameId;
|
|
10135
|
+
const ownsFrame = focus.kind === "frame";
|
|
10136
|
+
const slotsOf = (state) => normalizeCaptionSlotIds(state.captionSlotIds, state.captionText, state.panels.map((panel) => panel.id));
|
|
10137
|
+
const beforeSlots = slotsOf(before);
|
|
10138
|
+
const focusedSlot = focus.kind === "caption" ? beforeSlots[frameId]?.[focus.captionIndex] : void 0;
|
|
10139
|
+
const shotsOutside = (state) => state.shots.filter((shot) => !(ownsFrame && shot.panelId === frameId)).map((shot) => focus.kind === "device" && shot.id === focus.deviceId ? { id: shot.id, panelId: shot.panelId } : { id: shot.id, panelId: shot.panelId, frameName: shot.frameName, frameNodeId: shot.frameNodeId, look: shot.look });
|
|
10140
|
+
const bytesOutside = (state) => state.shots.filter((shot) => !(ownsFrame && shot.panelId === frameId) && !(focus.kind === "device" && shot.id === focus.deviceId)).map((shot) => shot.bytes);
|
|
10141
|
+
const perFrame = (map) => {
|
|
10142
|
+
if (!map || !ownsFrame) return map;
|
|
10143
|
+
const { [frameId]: _owned, ...rest } = map;
|
|
10144
|
+
void _owned;
|
|
10145
|
+
return rest;
|
|
10146
|
+
};
|
|
10147
|
+
const outside = (state) => {
|
|
10148
|
+
const {
|
|
10149
|
+
selection: _selection,
|
|
10150
|
+
activeLocale: _activeLocale,
|
|
10151
|
+
previewPresetId: _preview,
|
|
10152
|
+
outputScreens: _outputScreens,
|
|
10153
|
+
localeScreens: _localeScreens,
|
|
10154
|
+
selectedShotId: _selectedShotId,
|
|
10155
|
+
shots: _shots,
|
|
10156
|
+
captionSlotIds: _rawSlots,
|
|
10157
|
+
captionText,
|
|
10158
|
+
outputCaptions,
|
|
10159
|
+
panelColors,
|
|
10160
|
+
panelBackgrounds,
|
|
10161
|
+
staleCaptionTranslations,
|
|
10162
|
+
frameTemplates,
|
|
10163
|
+
...rest
|
|
10164
|
+
} = state;
|
|
10165
|
+
void [_selection, _activeLocale, _preview, _outputScreens, _localeScreens, _selectedShotId, _shots, _rawSlots];
|
|
10166
|
+
const slots = slotsOf(state);
|
|
10167
|
+
const slotAt = (panelId, index) => slots[panelId]?.[index] ?? `${panelId}#${index}`;
|
|
10168
|
+
const owned = (panelId, index) => ownsFrame ? panelId === frameId : focusedSlot !== void 0 && slotAt(panelId, index) === focusedSlot;
|
|
10169
|
+
const keyed = (panelId, layers) => layers.flatMap((layer, index) => owned(panelId, index) ? [] : [[slotAt(panelId, index), layer]]);
|
|
10170
|
+
const byFrame = (frames) => frames && Object.keys(frames).map((panelId) => [panelId, keyed(panelId, frames[panelId])]).filter(([, entries]) => entries.length > 0);
|
|
10171
|
+
const byLocale = (locales) => locales && Object.keys(locales).map((locale2) => [locale2, byFrame(locales[locale2])]);
|
|
10172
|
+
return JSON.stringify({
|
|
10173
|
+
rest,
|
|
10174
|
+
shots: shotsOutside(state),
|
|
10175
|
+
captionText: byLocale(captionText),
|
|
10176
|
+
outputCaptions: outputCaptions && Object.keys(outputCaptions).map((family) => [family, byLocale(outputCaptions[family])]),
|
|
10177
|
+
captionSlots: Object.keys(slots).map((panelId) => [panelId, slots[panelId].filter((_, index) => !owned(panelId, index))]).filter(([, ids]) => ids.length > 0),
|
|
10178
|
+
staleCaptionTranslations: staleCaptionTranslations && Object.keys(staleCaptionTranslations).map((locale2) => [
|
|
10179
|
+
locale2,
|
|
10180
|
+
staleCaptionTranslations[locale2].flatMap((key) => {
|
|
10181
|
+
const cut = key.lastIndexOf(":");
|
|
10182
|
+
const panelId = key.slice(0, cut);
|
|
10183
|
+
const index = Number(key.slice(cut + 1));
|
|
10184
|
+
return owned(panelId, index) ? [] : [`${panelId}:${slotAt(panelId, index)}`];
|
|
10185
|
+
}).sort()
|
|
10186
|
+
]),
|
|
10187
|
+
panelColors: perFrame(panelColors),
|
|
10188
|
+
panelBackgrounds: perFrame(panelBackgrounds),
|
|
10189
|
+
frameTemplates: perFrame(frameTemplates)
|
|
10190
|
+
});
|
|
10191
|
+
};
|
|
10192
|
+
const beforeBytes = bytesOutside(before);
|
|
10193
|
+
const afterBytes = bytesOutside(after);
|
|
10194
|
+
return outside(before) !== outside(after) || beforeBytes.length !== afterBytes.length || beforeBytes.some((bytes, index) => bytes !== afterBytes[index]);
|
|
10195
|
+
}
|
|
9404
10196
|
function phaseFor(cycle) {
|
|
9405
10197
|
return cycle === 1 ? "applying" : cycle === 2 ? "inspecting" : "adjusting";
|
|
9406
10198
|
}
|
|
@@ -9469,6 +10261,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9469
10261
|
const appliedOperations = [];
|
|
9470
10262
|
const touchedCapabilities = /* @__PURE__ */ new Set();
|
|
9471
10263
|
const templatedPanels = /* @__PURE__ */ new Set();
|
|
10264
|
+
let instructionSatisfied = false;
|
|
9472
10265
|
let terminalOutcome = null;
|
|
9473
10266
|
let invalid2 = false;
|
|
9474
10267
|
let cancelled = false;
|
|
@@ -9521,10 +10314,55 @@ async function runAgentTurn(input, ports) {
|
|
|
9521
10314
|
}
|
|
9522
10315
|
interpretation ||= authorization.interpretation?.trim() || instruction;
|
|
9523
10316
|
inspection = authorization.inspection?.trim() || "Inspected the current composed result.";
|
|
10317
|
+
const notApplied = /* @__PURE__ */ new Set();
|
|
10318
|
+
if (!authorization.failed && !authorization.terminalOutcome) {
|
|
10319
|
+
if (authorization.patch && cycle > 1) {
|
|
10320
|
+
const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
|
|
10321
|
+
if (guarded.dropped.length > 0) {
|
|
10322
|
+
authorization = { ...authorization, patch: guarded.patch };
|
|
10323
|
+
notApplied.add("repeated_step");
|
|
10324
|
+
}
|
|
10325
|
+
}
|
|
10326
|
+
if (authorization.patch && instructionSatisfied) {
|
|
10327
|
+
const context = { start: before, now: working, panelIds: authorization.panelIds };
|
|
10328
|
+
let guarded = dropRepeatedAdjustments(authorization.patch, context);
|
|
10329
|
+
if (guarded.dropped.length > 0 && layoutRepairable && renderedState) {
|
|
10330
|
+
const repairing = /* @__PURE__ */ new Set();
|
|
10331
|
+
for (const index of ports.layoutCollisionFrames?.(rendered) ?? []) {
|
|
10332
|
+
const panelId = renderedState.panels[index]?.id;
|
|
10333
|
+
if (panelId) repairing.add(panelId);
|
|
10334
|
+
}
|
|
10335
|
+
if (repairing.size > 0) guarded = dropRepeatedAdjustments(authorization.patch, { ...context, repairing });
|
|
10336
|
+
}
|
|
10337
|
+
if (guarded.dropped.length > 0) {
|
|
10338
|
+
authorization = {
|
|
10339
|
+
...authorization,
|
|
10340
|
+
patch: patchChangesSomething(guarded.patch) ? guarded.patch : void 0
|
|
10341
|
+
};
|
|
10342
|
+
notApplied.add("repeated_adjustment");
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
if (authorization.patch && templatedPanels.size > 0) {
|
|
10346
|
+
const shots = working.shots;
|
|
10347
|
+
const guarded = dropTemplatePoses(
|
|
10348
|
+
authorization.patch,
|
|
10349
|
+
templatedPanels,
|
|
10350
|
+
authorization.panelIds,
|
|
10351
|
+
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
10352
|
+
);
|
|
10353
|
+
if (guarded.dropped) {
|
|
10354
|
+
authorization = { ...authorization, patch: guarded.patch };
|
|
10355
|
+
notApplied.add("template_pose");
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
}
|
|
10359
|
+
const planned = !authorization.failed && !authorization.terminalOutcome;
|
|
10360
|
+
const application = planned && authorization.patch ? applyProjectRefinementWithAssets(working, authorization.patch, authorization.panelIds, input.assets ?? []) : null;
|
|
10361
|
+
const effect = !planned ? null : application && authorization.patch ? passEffect(working, application, authorization.patch, authorization.panelIds, notApplied, input.focus) : notApplied.size > 0 && !authorization.patch ? emptiedPassEffect(notApplied) : null;
|
|
9524
10362
|
const pass = {
|
|
9525
10363
|
phase,
|
|
9526
10364
|
generationId: authorization.generationId,
|
|
9527
|
-
summary: authorization.failed ? FAILED_PASS_SUMMARY[phase] : inspection,
|
|
10365
|
+
summary: authorization.failed ? FAILED_PASS_SUMMARY[phase] : effect?.summary ?? inspection,
|
|
9528
10366
|
usage: authorization.usage,
|
|
9529
10367
|
createdAt: now()
|
|
9530
10368
|
};
|
|
@@ -9545,29 +10383,11 @@ async function runAgentTurn(input, ports) {
|
|
|
9545
10383
|
break;
|
|
9546
10384
|
}
|
|
9547
10385
|
remainingMismatch = authorization.remainingMismatch?.trim() || authorization.followUp?.trim() || null;
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
|
|
9551
|
-
if (guarded.dropped.length > 0) authorization = { ...authorization, patch: guarded.patch };
|
|
9552
|
-
}
|
|
9553
|
-
if (authorization.patch && templatedPanels.size > 0) {
|
|
9554
|
-
const shots = working.shots;
|
|
9555
|
-
const guarded = dropTemplatePoses(
|
|
9556
|
-
authorization.patch,
|
|
9557
|
-
templatedPanels,
|
|
9558
|
-
authorization.panelIds,
|
|
9559
|
-
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
9560
|
-
);
|
|
9561
|
-
if (guarded.dropped) authorization = { ...authorization, patch: guarded.patch };
|
|
10386
|
+
if (authorization.patch && (!effect || effect.reachesOutsideFocus)) {
|
|
10387
|
+
for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
|
|
9562
10388
|
}
|
|
9563
10389
|
if (authorization.patch) {
|
|
9564
10390
|
for (const id of capabilitiesForPatch(authorization.patch)) touchedCapabilities.add(id);
|
|
9565
|
-
const application = applyProjectRefinementWithAssets(
|
|
9566
|
-
working,
|
|
9567
|
-
authorization.patch,
|
|
9568
|
-
authorization.panelIds,
|
|
9569
|
-
input.assets ?? []
|
|
9570
|
-
);
|
|
9571
10391
|
if (!application) {
|
|
9572
10392
|
invalid2 = true;
|
|
9573
10393
|
break;
|
|
@@ -9593,6 +10413,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9593
10413
|
assetBindings: workingAssetBindings
|
|
9594
10414
|
};
|
|
9595
10415
|
selected = checkpoint;
|
|
10416
|
+
if (authorization.complete === true) instructionSatisfied = true;
|
|
9596
10417
|
if (checkpoint.changed.length > 0 && (!best || checkpoint.quality >= best.quality)) best = checkpoint;
|
|
9597
10418
|
await emit({
|
|
9598
10419
|
type: "draft_applied",
|
|
@@ -9719,7 +10540,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9719
10540
|
void input.requestedCycle;
|
|
9720
10541
|
return result;
|
|
9721
10542
|
}
|
|
9722
|
-
var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
|
|
10543
|
+
var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY, NOT_APPLIED, LANDED_DIFFERENTLY;
|
|
9723
10544
|
var init_agentTurnCoordinator = __esm({
|
|
9724
10545
|
"../mockup-engine/appstore/agentTurnCoordinator.ts"() {
|
|
9725
10546
|
"use strict";
|
|
@@ -9731,8 +10552,11 @@ var init_agentTurnCoordinator = __esm({
|
|
|
9731
10552
|
init_appStoreLocales();
|
|
9732
10553
|
init_agentAssetCustody();
|
|
9733
10554
|
init_agentTurnRepetition();
|
|
10555
|
+
init_agentTurnAdjustments();
|
|
9734
10556
|
init_templatePoseGuard();
|
|
9735
10557
|
init_layoutTemplates();
|
|
10558
|
+
init_frameTemplates();
|
|
10559
|
+
init_agentTurnLanded();
|
|
9736
10560
|
AGENT_TURN_PASS_CAP = 3;
|
|
9737
10561
|
AgentTurnAuthorizationError = class extends Error {
|
|
9738
10562
|
constructor(outcome) {
|
|
@@ -9756,6 +10580,16 @@ var init_agentTurnCoordinator = __esm({
|
|
|
9756
10580
|
inspecting: "The inspection pass did not complete.",
|
|
9757
10581
|
adjusting: "The correction pass did not complete."
|
|
9758
10582
|
};
|
|
10583
|
+
NOT_APPLIED = {
|
|
10584
|
+
repeated_step: "a step this turn had already taken",
|
|
10585
|
+
repeated_adjustment: "a further change to what this turn had already adjusted",
|
|
10586
|
+
template_pose: "a new pose for a device its layout template had placed"
|
|
10587
|
+
};
|
|
10588
|
+
LANDED_DIFFERENTLY = {
|
|
10589
|
+
held_at_limit: "a value past its limit, which was held at the limit",
|
|
10590
|
+
shared_offset: "a sideways offset for each phone of a frame that holds several, which stays shared",
|
|
10591
|
+
set_differently: "a value the project took differently"
|
|
10592
|
+
};
|
|
9759
10593
|
}
|
|
9760
10594
|
});
|
|
9761
10595
|
|
|
@@ -9866,6 +10700,13 @@ var init_agentTurnConformance = __esm({
|
|
|
9866
10700
|
summary: "Restyled 1 device",
|
|
9867
10701
|
broaderChanges: [],
|
|
9868
10702
|
remainingMismatch: null
|
|
10703
|
+
},
|
|
10704
|
+
receipt: {
|
|
10705
|
+
committed: true,
|
|
10706
|
+
status: "applied",
|
|
10707
|
+
generationIds: ["generation-1", "generation-2"],
|
|
10708
|
+
recordVersion: 8,
|
|
10709
|
+
usage: { inputTokens: 20, cacheReadTokens: 4, imageTokens: null, outputTokens: 10, totalTokens: 30 }
|
|
9869
10710
|
}
|
|
9870
10711
|
}
|
|
9871
10712
|
};
|
|
@@ -9968,6 +10809,8 @@ var init_appstore = __esm({
|
|
|
9968
10809
|
init_captionModel();
|
|
9969
10810
|
init_variant();
|
|
9970
10811
|
init_screenManifest();
|
|
10812
|
+
init_fingerprint();
|
|
10813
|
+
init_screenUploadPlan();
|
|
9971
10814
|
init_productionInput();
|
|
9972
10815
|
init_productionReadiness();
|
|
9973
10816
|
init_localeAxis();
|
|
@@ -9977,6 +10820,7 @@ var init_appstore = __esm({
|
|
|
9977
10820
|
init_localeVariants();
|
|
9978
10821
|
init_shot();
|
|
9979
10822
|
init_layout();
|
|
10823
|
+
init_framelessCard();
|
|
9980
10824
|
init_serialize();
|
|
9981
10825
|
init_project();
|
|
9982
10826
|
init_look();
|
|
@@ -10283,6 +11127,70 @@ var init_lookResolution = __esm({
|
|
|
10283
11127
|
}
|
|
10284
11128
|
});
|
|
10285
11129
|
|
|
11130
|
+
// ../mockup-engine/appstore/recordEnvelope.ts
|
|
11131
|
+
function isEnvelope(value) {
|
|
11132
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
11133
|
+
}
|
|
11134
|
+
function isBlankRecord(value) {
|
|
11135
|
+
return isEnvelope(value) && value.record === 0 && value.project === void 0;
|
|
11136
|
+
}
|
|
11137
|
+
function rebuildEnvelope(stored, next) {
|
|
11138
|
+
const out = isEnvelope(stored) ? { ...stored } : {};
|
|
11139
|
+
const { project: project2, manifest, ...explicit } = next;
|
|
11140
|
+
out.record = SAVED_RECORD_VERSION;
|
|
11141
|
+
out.project = project2;
|
|
11142
|
+
if ("manifest" in next) {
|
|
11143
|
+
const folded = manifest ? mergeManifest([], manifest) : [];
|
|
11144
|
+
if (folded.length > 0) out.handoffRefs = folded;
|
|
11145
|
+
else delete out.handoffRefs;
|
|
11146
|
+
}
|
|
11147
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
11148
|
+
if (value === void 0) delete out[key];
|
|
11149
|
+
else out[key] = value;
|
|
11150
|
+
}
|
|
11151
|
+
return out;
|
|
11152
|
+
}
|
|
11153
|
+
function custodyUnion(...manifests) {
|
|
11154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11155
|
+
const out = [];
|
|
11156
|
+
for (const manifest of manifests) {
|
|
11157
|
+
for (const entry of manifest) {
|
|
11158
|
+
if (seen.has(entry.ref)) continue;
|
|
11159
|
+
seen.add(entry.ref);
|
|
11160
|
+
out.push(entry);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
return out;
|
|
11164
|
+
}
|
|
11165
|
+
var SAVED_RECORD_VERSION, ENVELOPE_KEYS_A_SAVE_OWNS;
|
|
11166
|
+
var init_recordEnvelope = __esm({
|
|
11167
|
+
"../mockup-engine/appstore/recordEnvelope.ts"() {
|
|
11168
|
+
"use strict";
|
|
11169
|
+
init_screenManifest();
|
|
11170
|
+
SAVED_RECORD_VERSION = 2;
|
|
11171
|
+
ENVELOPE_KEYS_A_SAVE_OWNS = [
|
|
11172
|
+
"record",
|
|
11173
|
+
"project",
|
|
11174
|
+
"fastlaneBundleId",
|
|
11175
|
+
"fastlaneLocale",
|
|
11176
|
+
"lastBundleExportAt",
|
|
11177
|
+
"lastShareAt",
|
|
11178
|
+
"latestVersion",
|
|
11179
|
+
"heldVersion",
|
|
11180
|
+
"source",
|
|
11181
|
+
"baseline",
|
|
11182
|
+
"reach",
|
|
11183
|
+
"handoffRefs",
|
|
11184
|
+
// The App Store listing a project is linked to (#1001). OWNED, because owned is the only kind a
|
|
11185
|
+
// DETACH can delete — an unowned key could never be removed. That does not oblige a save to carry
|
|
11186
|
+
// it: the rule above already says an unnamed key survives. Naming it is the hazard here, so no
|
|
11187
|
+
// ordinary save path may mention `appListing` — only a deliberate attach (a value) or detach
|
|
11188
|
+
// (`undefined` through rebuildEnvelope, `null` through patchRecord).
|
|
11189
|
+
"appListing"
|
|
11190
|
+
];
|
|
11191
|
+
}
|
|
11192
|
+
});
|
|
11193
|
+
|
|
10286
11194
|
// ../api/_lib/projectRecord.ts
|
|
10287
11195
|
function parseScreenPurgeRefs(value) {
|
|
10288
11196
|
if (!Array.isArray(value)) {
|
|
@@ -10299,13 +11207,15 @@ function parseScreenPurgeRefs(value) {
|
|
|
10299
11207
|
}
|
|
10300
11208
|
return entries;
|
|
10301
11209
|
}
|
|
10302
|
-
var PROJECT_RECORD_MAX_BYTES;
|
|
11210
|
+
var PROJECT_RECORD_MAX_BYTES, WRITABLE_ENVELOPE_KEYS;
|
|
10303
11211
|
var init_projectRecord = __esm({
|
|
10304
11212
|
"../api/_lib/projectRecord.ts"() {
|
|
10305
11213
|
"use strict";
|
|
10306
11214
|
init_projectFileGuard();
|
|
11215
|
+
init_recordEnvelope();
|
|
10307
11216
|
init_screenManifest();
|
|
10308
11217
|
PROJECT_RECORD_MAX_BYTES = 2 * 1024 * 1024;
|
|
11218
|
+
WRITABLE_ENVELOPE_KEYS = new Set(ENVELOPE_KEYS_A_SAVE_OWNS);
|
|
10309
11219
|
}
|
|
10310
11220
|
});
|
|
10311
11221
|
|
|
@@ -10447,10 +11357,14 @@ var init_supabaseRest = __esm({
|
|
|
10447
11357
|
});
|
|
10448
11358
|
|
|
10449
11359
|
// ../api/_lib/shareStorage.ts
|
|
11360
|
+
function safePath(path) {
|
|
11361
|
+
if (!isSafeStoragePath(path)) throw new Error(`storage: refusing unsafe object path ${JSON.stringify(path)}`);
|
|
11362
|
+
return path;
|
|
11363
|
+
}
|
|
10450
11364
|
async function createSignedUploadUrl(path, options = {}) {
|
|
10451
11365
|
const headers = serviceHeaders({ "Content-Type": "application/json" });
|
|
10452
11366
|
if (options.upsert) headers["x-upsert"] = "true";
|
|
10453
|
-
const res = await fetch(`${storageBase()}/object/upload/sign/${BUCKET}/${path}`, {
|
|
11367
|
+
const res = await fetch(`${storageBase()}/object/upload/sign/${BUCKET}/${safePath(path)}`, {
|
|
10454
11368
|
method: "POST",
|
|
10455
11369
|
headers,
|
|
10456
11370
|
body: JSON.stringify({})
|
|
@@ -10463,7 +11377,7 @@ async function createSignedUploadUrl(path, options = {}) {
|
|
|
10463
11377
|
return { uploadUrl: `${storageBase()}${data.url}` };
|
|
10464
11378
|
}
|
|
10465
11379
|
async function createSignedDownloadUrl(path, expiresInSeconds, downloadFilename) {
|
|
10466
|
-
const res = await fetch(`${storageBase()}/object/sign/${BUCKET}/${path}`, {
|
|
11380
|
+
const res = await fetch(`${storageBase()}/object/sign/${BUCKET}/${safePath(path)}`, {
|
|
10467
11381
|
method: "POST",
|
|
10468
11382
|
headers: serviceHeaders({ "Content-Type": "application/json" }),
|
|
10469
11383
|
body: JSON.stringify({ expiresIn: expiresInSeconds })
|
|
@@ -10488,7 +11402,7 @@ async function putBytes(uploadUrl, bytes, contentType) {
|
|
|
10488
11402
|
}
|
|
10489
11403
|
}
|
|
10490
11404
|
async function downloadObject(path) {
|
|
10491
|
-
const res = await fetch(`${storageBase()}/object/${BUCKET}/${path}`, { headers: serviceHeaders() });
|
|
11405
|
+
const res = await fetch(`${storageBase()}/object/${BUCKET}/${safePath(path)}`, { headers: serviceHeaders() });
|
|
10492
11406
|
if (!res.ok) {
|
|
10493
11407
|
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
10494
11408
|
if (res.status === 404 || /"statusCode"\s*:\s*"?404"?/.test(body) || /not_found/.test(body)) {
|
|
@@ -10499,7 +11413,7 @@ async function downloadObject(path) {
|
|
|
10499
11413
|
return new Uint8Array(await res.arrayBuffer());
|
|
10500
11414
|
}
|
|
10501
11415
|
async function getObjectInfo(path) {
|
|
10502
|
-
const res = await fetch(`${storageBase()}/object/info/${BUCKET}/${path}`, { headers: serviceHeaders() });
|
|
11416
|
+
const res = await fetch(`${storageBase()}/object/info/${BUCKET}/${safePath(path)}`, { headers: serviceHeaders() });
|
|
10503
11417
|
if (!res.ok) {
|
|
10504
11418
|
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
10505
11419
|
if (res.status === 404 || /"statusCode"\s*:\s*"?404"?/.test(body) || /not_found/.test(body)) {
|
|
@@ -10518,6 +11432,7 @@ var BUCKET, StorageObjectNotFoundError;
|
|
|
10518
11432
|
var init_shareStorage = __esm({
|
|
10519
11433
|
"../api/_lib/shareStorage.ts"() {
|
|
10520
11434
|
"use strict";
|
|
11435
|
+
init_storagePath();
|
|
10521
11436
|
init_supabaseRest();
|
|
10522
11437
|
BUCKET = "share-bundles";
|
|
10523
11438
|
StorageObjectNotFoundError = class extends Error {
|
|
@@ -10872,7 +11787,7 @@ function uploadCustody(userId) {
|
|
|
10872
11787
|
}
|
|
10873
11788
|
function refBelongsToUser(ref, userId, now = Date.now()) {
|
|
10874
11789
|
const prefix = `uploads/${userId}/`;
|
|
10875
|
-
if (!ref.startsWith(prefix) || ref
|
|
11790
|
+
if (!ref.startsWith(prefix) || !isSafeStoragePath(ref)) return false;
|
|
10876
11791
|
if (!userId.startsWith(ANONYMOUS_USER_PREFIX)) return true;
|
|
10877
11792
|
const dir = ref.slice(prefix.length).split("/")[0];
|
|
10878
11793
|
const expiry = ANONYMOUS_DIR_EXPIRY_RE.exec(dir);
|
|
@@ -10886,17 +11801,16 @@ async function fetchUploadedRef(ref, userId) {
|
|
|
10886
11801
|
await adoptLegacyAssetOnAuthorizedRead(ref, userId).catch(() => void 0);
|
|
10887
11802
|
return await downloadObject(ref);
|
|
10888
11803
|
} catch (err) {
|
|
10889
|
-
if (err instanceof StorageObjectNotFoundError)
|
|
10890
|
-
throw new Error(
|
|
10891
|
-
`no screenshot was ever uploaded for ref "${ref}". request_screenshot_upload only MINTS an upload slot \u2014 the raw PNG bytes must still be PUT to its uploadUrl before rendering (curl -T screenshot.png "$UPLOAD_URL"). If that PUT failed or was skipped, mint a fresh slot and retry it; slots are single-use and short-lived. Alternatively pass the screenshot as { "url": "https://\u2026" } and this server will fetch it for you. ` + // #180: on ChatGPT mobile the PUT can NEVER succeed (the sandbox cannot resolve the
|
|
10892
|
-
// storage host), so "retry it" is not always advice an agent can take. Name the door
|
|
10893
|
-
// that has no upload step at all.
|
|
10894
|
-
BROWSER_FIRST_ROUTE
|
|
10895
|
-
);
|
|
10896
|
-
}
|
|
11804
|
+
if (err instanceof StorageObjectNotFoundError) throw new Error(missingUploadMessage(ref));
|
|
10897
11805
|
throw err;
|
|
10898
11806
|
}
|
|
10899
11807
|
}
|
|
11808
|
+
function missingUploadMessage(ref) {
|
|
11809
|
+
return `no screenshot was ever uploaded for ref "${ref}". request_screenshot_upload only MINTS an upload slot \u2014 the raw PNG bytes must still be PUT to its uploadUrl before rendering (curl -T screenshot.png "$UPLOAD_URL"). If that PUT failed or was skipped, mint a fresh slot and retry it; slots are single-use and short-lived. Alternatively pass the screenshot as { "url": "https://\u2026" } and this server will fetch it for you. ` + // #180: on ChatGPT mobile the PUT can NEVER succeed (the sandbox cannot resolve the
|
|
11810
|
+
// storage host), so "retry it" is not always advice an agent can take. Name the door
|
|
11811
|
+
// that has no upload step at all.
|
|
11812
|
+
BROWSER_FIRST_ROUTE;
|
|
11813
|
+
}
|
|
10900
11814
|
async function fetchPanelRef(ref, userId) {
|
|
10901
11815
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref)) {
|
|
10902
11816
|
return fetchUploadedRef(ref, userId);
|
|
@@ -11070,6 +11984,7 @@ var init_uploads = __esm({
|
|
|
11070
11984
|
init_shareStorage();
|
|
11071
11985
|
init_assetRegistry();
|
|
11072
11986
|
init_supabaseRest();
|
|
11987
|
+
init_storagePath();
|
|
11073
11988
|
init_retentionPolicy();
|
|
11074
11989
|
init_toolContract();
|
|
11075
11990
|
init_pngOptimizer();
|
|
@@ -11152,7 +12067,12 @@ async function readLocalPathAsBase64(path) {
|
|
|
11152
12067
|
const { bytes } = await readLocalScreenshotFile(path);
|
|
11153
12068
|
return (await optimizeScreenshotPng(bytes, `The file at "${path}"`)).toString("base64");
|
|
11154
12069
|
}
|
|
11155
|
-
async function
|
|
12070
|
+
async function readOneUpload(readUploads, ref) {
|
|
12071
|
+
const bytes = (await readUploads([ref])).get(ref);
|
|
12072
|
+
if (!bytes) throw new Error(`the screenshot for ref "${ref}" could not be read.`);
|
|
12073
|
+
return bytes;
|
|
12074
|
+
}
|
|
12075
|
+
async function resolvePanelBytes(entry, userId, allowLocalPath, readUploads) {
|
|
11156
12076
|
if ("path" in entry) {
|
|
11157
12077
|
if (!allowLocalPath) {
|
|
11158
12078
|
throw new Error(
|
|
@@ -11165,14 +12085,20 @@ async function resolvePanelBytes(entry, userId, allowLocalPath) {
|
|
|
11165
12085
|
throw new Error(`could not read local panel at "${entry.path}": ${err.message}`);
|
|
11166
12086
|
}
|
|
11167
12087
|
}
|
|
12088
|
+
if (readUploads) {
|
|
12089
|
+
if (PANEL_ID_RE.test(entry.ref)) throw new HostedOnlyPanelError(entry.ref);
|
|
12090
|
+
return readOneUpload(readUploads, entry.ref);
|
|
12091
|
+
}
|
|
11168
12092
|
return entry.ref.includes("/") ? fetchUploadedRef(entry.ref, userId) : fetchPanelRef(entry.ref, userId);
|
|
11169
12093
|
}
|
|
11170
|
-
async function resolveEntry(entry, userId, allowLocalPath) {
|
|
12094
|
+
async function resolveEntry(entry, userId, allowLocalPath, uploaded) {
|
|
11171
12095
|
if (typeof entry === "string") {
|
|
11172
12096
|
return (await optimizeScreenshotPng(Buffer.from(entry, "base64"), "Inline screenshot")).toString("base64");
|
|
11173
12097
|
}
|
|
11174
12098
|
if ("ref" in entry) {
|
|
11175
|
-
const
|
|
12099
|
+
const read = uploaded?.get(entry.ref);
|
|
12100
|
+
if (uploaded && !read) throw new Error(`the screenshot for ref "${entry.ref}" could not be read.`);
|
|
12101
|
+
const bytes = read ?? await fetchUploadedRef(entry.ref, userId);
|
|
11176
12102
|
return (await optimizeScreenshotPng(bytes, `Screenshot ref "${entry.ref}"`)).toString("base64");
|
|
11177
12103
|
}
|
|
11178
12104
|
if ("path" in entry) {
|
|
@@ -11188,10 +12114,13 @@ async function resolveEntry(entry, userId, allowLocalPath) {
|
|
|
11188
12114
|
}
|
|
11189
12115
|
return fetchUrlAsBase64(entry.url);
|
|
11190
12116
|
}
|
|
11191
|
-
async function resolveScreenshots(slots, userId, allowLocalPath = false) {
|
|
12117
|
+
async function resolveScreenshots(slots, userId, allowLocalPath = false, readUploads) {
|
|
11192
12118
|
assertInlinePayloadSizeNested(slots);
|
|
11193
|
-
|
|
12119
|
+
const refs = Array.from(new Set(slots.flat().flatMap((entry) => entry && typeof entry === "object" && "ref" in entry && typeof entry.ref === "string" ? [entry.ref] : [])));
|
|
12120
|
+
const uploaded = readUploads && refs.length > 0 ? await readUploads(refs) : null;
|
|
12121
|
+
return Promise.all(slots.map((slot2) => Promise.all(slot2.map((entry) => resolveEntry(entry, userId, allowLocalPath, uploaded)))));
|
|
11194
12122
|
}
|
|
12123
|
+
var PANEL_ID_RE, HostedOnlyPanelError;
|
|
11195
12124
|
var init_screenshotInput = __esm({
|
|
11196
12125
|
"src/render/screenshotInput.ts"() {
|
|
11197
12126
|
"use strict";
|
|
@@ -11199,6 +12128,15 @@ var init_screenshotInput = __esm({
|
|
|
11199
12128
|
init_uploads();
|
|
11200
12129
|
init_pngOptimizer();
|
|
11201
12130
|
init_toolContract();
|
|
12131
|
+
PANEL_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12132
|
+
HostedOnlyPanelError = class extends Error {
|
|
12133
|
+
constructor(ref) {
|
|
12134
|
+
super(
|
|
12135
|
+
`Rendered panel ids are hosted-only \u2014 this local server cannot read panel "${ref}". Pass the rendered PNG as { "path": "\u2026" } instead.`
|
|
12136
|
+
);
|
|
12137
|
+
this.name = "HostedOnlyPanelError";
|
|
12138
|
+
}
|
|
12139
|
+
};
|
|
11202
12140
|
}
|
|
11203
12141
|
});
|
|
11204
12142
|
|
|
@@ -11208,22 +12146,96 @@ function screenNamesFromRecord(record8) {
|
|
|
11208
12146
|
if (!Array.isArray(shots)) return [];
|
|
11209
12147
|
return shots.map((s) => s?.frameName).filter((n) => typeof n === "string" && n.length > 0);
|
|
11210
12148
|
}
|
|
11211
|
-
function
|
|
11212
|
-
if (!
|
|
11213
|
-
const
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
12149
|
+
function refOf(entry) {
|
|
12150
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
12151
|
+
const ref = entry.ref;
|
|
12152
|
+
return typeof ref === "string" && ref.length > 0 ? canonicalizeUploadRef(ref) : null;
|
|
12153
|
+
}
|
|
12154
|
+
function introducedScreenCells(screenshots2, projectFile) {
|
|
12155
|
+
const out = { manifest: [], refs: [], unaddressed: [] };
|
|
12156
|
+
if (!Array.isArray(screenshots2) || screenshots2.length === 0) return out;
|
|
12157
|
+
const entries = normalizeScreenshotSlots(screenshots2).flat();
|
|
12158
|
+
const shots = shotRefsFromProjectFile(projectFile);
|
|
12159
|
+
const { baseLocale } = projectLocaleAxis(projectFile);
|
|
12160
|
+
entries.forEach((entry, i) => {
|
|
12161
|
+
const shot = entries.length === shots.length ? shots[i] : void 0;
|
|
12162
|
+
const cells = [];
|
|
12163
|
+
if (isLocaleVariants(entry)) {
|
|
12164
|
+
const base = refOf(resolveLocaleVariant(entry, baseLocale, baseLocale));
|
|
12165
|
+
if (base) cells.push({ ref: base, variant: {} });
|
|
12166
|
+
for (const [locale2, variant] of Object.entries(entry.locales)) {
|
|
12167
|
+
const ref = locale2 === baseLocale ? null : refOf(variant);
|
|
12168
|
+
if (ref) cells.push({ ref, variant: canonicalVariant({ locale: locale2 }, baseLocale), locale: locale2 });
|
|
12169
|
+
}
|
|
12170
|
+
} else {
|
|
12171
|
+
const ref = refOf(entry);
|
|
12172
|
+
if (ref) cells.push({ ref, variant: {} });
|
|
11223
12173
|
}
|
|
11224
|
-
|
|
12174
|
+
for (const cell of cells) {
|
|
12175
|
+
const named = {
|
|
12176
|
+
ref: cell.ref,
|
|
12177
|
+
frame: i + 1,
|
|
12178
|
+
frameName: shot?.frameName ?? null,
|
|
12179
|
+
...cell.locale ? { locale: cell.locale } : {}
|
|
12180
|
+
};
|
|
12181
|
+
out.refs.push(named);
|
|
12182
|
+
if (!shot) out.unaddressed.push(named);
|
|
12183
|
+
else out.manifest.push({ shotId: shot.id, variant: cell.variant, ref: cell.ref });
|
|
12184
|
+
}
|
|
12185
|
+
});
|
|
11225
12186
|
return out;
|
|
11226
12187
|
}
|
|
12188
|
+
function describeUnsavedRefs(refs) {
|
|
12189
|
+
return refs.map((entry) => {
|
|
12190
|
+
const where = [
|
|
12191
|
+
`frame ${entry.frame}`,
|
|
12192
|
+
entry.frameName ? `"${entry.frameName}"` : null,
|
|
12193
|
+
entry.locale ? `locale ${entry.locale}` : null
|
|
12194
|
+
].filter(Boolean).join(", ");
|
|
12195
|
+
return `${entry.ref} (${where})`;
|
|
12196
|
+
}).join("; ");
|
|
12197
|
+
}
|
|
12198
|
+
function refusedRefs(kind, refs) {
|
|
12199
|
+
const list = describeUnsavedRefs(refs);
|
|
12200
|
+
const details = { frames: refs.map(({ ref: _ref, ...where }) => where) };
|
|
12201
|
+
switch (kind) {
|
|
12202
|
+
case "no_account":
|
|
12203
|
+
return {
|
|
12204
|
+
code: "authentication_required",
|
|
12205
|
+
details,
|
|
12206
|
+
error: `This local server has no ShotOps account, so it cannot save { "ref" } screenshots \u2014 a ref points at an upload inside an account: ${list}. Pass each screenshot as a local { "path": "\u2026" } PNG, or sign in (npx shotops-mcp login) and save again. Nothing was saved.`
|
|
12207
|
+
};
|
|
12208
|
+
case "account_unknown":
|
|
12209
|
+
return {
|
|
12210
|
+
code: "upstream_unavailable",
|
|
12211
|
+
details,
|
|
12212
|
+
error: `Could not confirm which ShotOps account this server\u2019s token signs in as, so these { "ref" } screenshots could not be checked: ${list}. Nothing was saved \u2014 try again in a moment.`
|
|
12213
|
+
};
|
|
12214
|
+
case "foreign":
|
|
12215
|
+
return {
|
|
12216
|
+
code: "asset_not_found",
|
|
12217
|
+
details,
|
|
12218
|
+
error: `These { "ref" } screenshots do not belong to this account: ${list}. A ref has to be an upload under the account this save writes to. Pass those screenshots again from a source this account owns \u2014 a ref from import_screenshot or request_screenshot_upload, or a local { "path": "\u2026" } on the local server. Nothing was saved.`
|
|
12219
|
+
};
|
|
12220
|
+
case "unaddressed":
|
|
12221
|
+
return {
|
|
12222
|
+
code: "internal_error",
|
|
12223
|
+
details,
|
|
12224
|
+
error: `These { "ref" } screenshots could not be matched to a frame of the project being saved: ${list}. Nothing was saved.`
|
|
12225
|
+
};
|
|
12226
|
+
}
|
|
12227
|
+
}
|
|
12228
|
+
async function unsavableRefRefusal(deps, introduced) {
|
|
12229
|
+
if (introduced.refs.length === 0) return null;
|
|
12230
|
+
if (introduced.unaddressed.length > 0) return refusedRefs("unaddressed", introduced.unaddressed);
|
|
12231
|
+
if (deps.createPendingProjectClaim) return refusedRefs("no_account", introduced.refs);
|
|
12232
|
+
let owner = deps.userId;
|
|
12233
|
+
if (deps.accountUserId) owner = await deps.accountUserId().catch(() => null);
|
|
12234
|
+
if (!owner) return refusedRefs("account_unknown", introduced.refs);
|
|
12235
|
+
const account = owner;
|
|
12236
|
+
const foreign = introduced.refs.filter((entry) => !refBelongsToUser(entry.ref, account));
|
|
12237
|
+
return foreign.length > 0 ? refusedRefs("foreign", foreign) : null;
|
|
12238
|
+
}
|
|
11227
12239
|
function projectShotRefsFromRecord(record8) {
|
|
11228
12240
|
return shotRefsFromProjectFile(record8?.project);
|
|
11229
12241
|
}
|
|
@@ -11241,14 +12253,6 @@ function preservedCells(record8, nextProjectFile) {
|
|
|
11241
12253
|
}
|
|
11242
12254
|
return { manifest: rebound.manifest, findings };
|
|
11243
12255
|
}
|
|
11244
|
-
function screenCustodyUnion(...manifests) {
|
|
11245
|
-
const seen = /* @__PURE__ */ new Set();
|
|
11246
|
-
return manifests.flatMap((manifest) => manifest.filter((entry) => {
|
|
11247
|
-
if (seen.has(entry.ref)) return false;
|
|
11248
|
-
seen.add(entry.ref);
|
|
11249
|
-
return true;
|
|
11250
|
-
}));
|
|
11251
|
-
}
|
|
11252
12256
|
function screenCustodyFromRecord(record8) {
|
|
11253
12257
|
return parseScreenPurgeRefs(record8?.screenPurgeRefs) ?? [];
|
|
11254
12258
|
}
|
|
@@ -11256,44 +12260,40 @@ function allLiveScreenRefs(record8) {
|
|
|
11256
12260
|
const parsed = parseScreenManifest(record8?.handoffRefs);
|
|
11257
12261
|
return parsed ? allManifestEntries(parsed) : [];
|
|
11258
12262
|
}
|
|
11259
|
-
|
|
12263
|
+
function projectSaveEnvelope(stored, projectFile, manifest, source, listing2) {
|
|
12264
|
+
const custody = custodyUnion(screenCustodyFromRecord(stored), allLiveScreenRefs(stored), manifest);
|
|
12265
|
+
const storedSource = stored != null && typeof stored === "object" && !Array.isArray(stored) ? stored.source : void 0;
|
|
12266
|
+
const storedKind = storedSource != null && typeof storedSource === "object" ? storedSource.kind : void 0;
|
|
12267
|
+
const mcpOrigin = storedKind === "agent" || storedKind === "local-path";
|
|
12268
|
+
const sourceForEnvelope = storedSource != null && !mcpOrigin ? storedSource : source ?? storedSource;
|
|
12269
|
+
return rebuildEnvelope(stored, {
|
|
12270
|
+
project: projectFile,
|
|
12271
|
+
manifest,
|
|
12272
|
+
source: sourceForEnvelope ?? void 0,
|
|
12273
|
+
reach: manifest.length > 0 ? "account" : void 0,
|
|
12274
|
+
screenPurgeRefs: custody.length > 0 ? custody : void 0,
|
|
12275
|
+
// #1004 — the App Store listing, and the ONE shape in which this door may name that key.
|
|
12276
|
+
// SPREAD, never a ternary yielding `undefined`: `rebuildEnvelope` deletes on `undefined`, so
|
|
12277
|
+
// `appListing: listing ?? undefined` would wipe a stored association on every ordinary save —
|
|
12278
|
+
// which is exactly the hazard `recordEnvelope.ts` warns about. A save with no reference names
|
|
12279
|
+
// no key at all, and the stored snapshot survives because nothing touched it.
|
|
12280
|
+
...listing2 ? { ...appListingRewrite(listing2), fastlaneBundleId: listing2.applied.bundleId } : {}
|
|
12281
|
+
});
|
|
12282
|
+
}
|
|
12283
|
+
async function persistProject(deps, args, record8, claimPolicy = "none", listing2) {
|
|
11260
12284
|
const names = screenNamesFromRecord(record8);
|
|
11261
12285
|
const source = names.length === 0 ? void 0 : args.sourceDir ? { kind: "local-path", ref: { dir: args.sourceDir, names } } : { kind: "agent", ref: { names } };
|
|
11262
|
-
const
|
|
12286
|
+
const introduced = introducedScreenCells(args.screenshots, record8);
|
|
12287
|
+
const refusal = await unsavableRefRefusal(deps, introduced);
|
|
12288
|
+
if (refusal) return refusal;
|
|
11263
12289
|
let screenNote;
|
|
11264
|
-
const wrapWith = (
|
|
12290
|
+
const wrapWith = (existingRecord) => {
|
|
11265
12291
|
const preserved = preservedCells(existingRecord, record8);
|
|
11266
12292
|
if (preserved.findings.length > 0) {
|
|
11267
12293
|
screenNote = preserved.findings.map((finding) => `The stored screenshot filed under "${finding.frameName}" was not carried over: that filename names ${finding.shotIds.length} devices (${finding.shotIds.join(", ")}), so no single device owns it. Its object is retained; give those devices distinct filenames, or pass their screenshots explicitly.`).join(" ");
|
|
11268
12294
|
}
|
|
11269
|
-
const manifest = mergeManifest(
|
|
11270
|
-
|
|
11271
|
-
manifestFromNamedRefs(handoffRefs, shotRefsFromProjectFile(record8))
|
|
11272
|
-
);
|
|
11273
|
-
const custody = screenCustodyUnion(
|
|
11274
|
-
screenCustodyFromRecord(existingRecord),
|
|
11275
|
-
allLiveScreenRefs(existingRecord),
|
|
11276
|
-
manifest
|
|
11277
|
-
);
|
|
11278
|
-
const stored = existingRecord != null && typeof existingRecord === "object" && !Array.isArray(existingRecord) ? { ...existingRecord } : {};
|
|
11279
|
-
delete stored.record;
|
|
11280
|
-
delete stored.project;
|
|
11281
|
-
delete stored.handoffRefs;
|
|
11282
|
-
delete stored.reach;
|
|
11283
|
-
delete stored.screenPurgeRefs;
|
|
11284
|
-
const storedSource = stored.source;
|
|
11285
|
-
delete stored.source;
|
|
11286
|
-
const storedKind = storedSource != null && typeof storedSource === "object" ? storedSource.kind : void 0;
|
|
11287
|
-
const mcpOrigin = storedKind === "agent" || storedKind === "local-path";
|
|
11288
|
-
const sourceForEnvelope = storedSource != null && !mcpOrigin ? storedSource : source ?? storedSource;
|
|
11289
|
-
return {
|
|
11290
|
-
...stored,
|
|
11291
|
-
record: SAVED_RECORD_VERSION,
|
|
11292
|
-
project: record8,
|
|
11293
|
-
...sourceForEnvelope != null ? { source: sourceForEnvelope } : {},
|
|
11294
|
-
...manifest.length > 0 ? { handoffRefs: manifest, reach: "account" } : {},
|
|
11295
|
-
...custody.length > 0 ? { screenPurgeRefs: custody } : {}
|
|
11296
|
-
};
|
|
12295
|
+
const manifest = mergeManifest(preserved.manifest, introduced.manifest);
|
|
12296
|
+
return projectSaveEnvelope(existingRecord, record8, manifest, source, listing2);
|
|
11297
12297
|
};
|
|
11298
12298
|
const ctx = {
|
|
11299
12299
|
actorUserId: deps.userId,
|
|
@@ -11318,7 +12318,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none") {
|
|
|
11318
12318
|
};
|
|
11319
12319
|
}
|
|
11320
12320
|
const name = args.projectName?.trim() || "ShotOps render";
|
|
11321
|
-
return await deps.createPendingProjectClaim(name, wrapWith(
|
|
12321
|
+
return await deps.createPendingProjectClaim(name, wrapWith(), ctx);
|
|
11322
12322
|
}
|
|
11323
12323
|
let projectId;
|
|
11324
12324
|
if (args.project) {
|
|
@@ -11334,7 +12334,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none") {
|
|
|
11334
12334
|
if (denial) return { error: denial };
|
|
11335
12335
|
const existing = await deps.readProjectRecord(project2.id);
|
|
11336
12336
|
if (!existing) return { error: `Project "${project2.name}" has no readable saved record.` };
|
|
11337
|
-
await deps.updateProjectRecord(project2.id, wrapWith(
|
|
12337
|
+
await deps.updateProjectRecord(project2.id, wrapWith(existing.record), existing.recordVersion ?? 1, ctx);
|
|
11338
12338
|
projectId = project2.id;
|
|
11339
12339
|
} else {
|
|
11340
12340
|
const name = args.projectName?.trim() || "ShotOps render";
|
|
@@ -11347,7 +12347,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none") {
|
|
|
11347
12347
|
const denial = authorizationMessage(decision);
|
|
11348
12348
|
if (denial) return { error: denial };
|
|
11349
12349
|
}
|
|
11350
|
-
const created = await deps.createProject(deps.userId, name, wrapWith(
|
|
12350
|
+
const created = await deps.createProject(deps.userId, name, wrapWith(), ctx);
|
|
11351
12351
|
projectId = created.id;
|
|
11352
12352
|
}
|
|
11353
12353
|
return { projectId, openUrl: `${deps.studioOrigin}/?project=${projectId}`, ...screenNote ? { screenNote } : {} };
|
|
@@ -11359,7 +12359,6 @@ function projectFileFromRecord(record8) {
|
|
|
11359
12359
|
const inner = record8?.project;
|
|
11360
12360
|
return inner != null && typeof inner === "object" ? inner : record8;
|
|
11361
12361
|
}
|
|
11362
|
-
var SAVED_RECORD_VERSION;
|
|
11363
12362
|
var init_projectPersistence = __esm({
|
|
11364
12363
|
"src/project/projectPersistence.ts"() {
|
|
11365
12364
|
"use strict";
|
|
@@ -11367,12 +12366,16 @@ var init_projectPersistence = __esm({
|
|
|
11367
12366
|
init_projectRecord();
|
|
11368
12367
|
init_uploadRef();
|
|
11369
12368
|
init_screenManifest();
|
|
12369
|
+
init_recordEnvelope();
|
|
12370
|
+
init_appListing();
|
|
12371
|
+
init_localeAxis();
|
|
12372
|
+
init_localeVariants();
|
|
12373
|
+
init_variant();
|
|
11370
12374
|
init_authorization();
|
|
11371
12375
|
init_projectResolution();
|
|
11372
12376
|
init_schemas();
|
|
11373
12377
|
init_screenshotInput();
|
|
11374
12378
|
init_uploads();
|
|
11375
|
-
SAVED_RECORD_VERSION = 2;
|
|
11376
12379
|
}
|
|
11377
12380
|
});
|
|
11378
12381
|
|
|
@@ -11658,7 +12661,6 @@ var init_projectAccessStore = __esm({
|
|
|
11658
12661
|
});
|
|
11659
12662
|
|
|
11660
12663
|
// ../api/_lib/projectLookStore.ts
|
|
11661
|
-
var FREE_EDITABLE_PROJECT_LIMIT;
|
|
11662
12664
|
var init_projectLookStore = __esm({
|
|
11663
12665
|
"../api/_lib/projectLookStore.ts"() {
|
|
11664
12666
|
"use strict";
|
|
@@ -11666,11 +12668,17 @@ var init_projectLookStore = __esm({
|
|
|
11666
12668
|
init_look();
|
|
11667
12669
|
init_lookDivergence();
|
|
11668
12670
|
init_projectFileGuard();
|
|
12671
|
+
init_shotLook();
|
|
11669
12672
|
init_supabaseRest();
|
|
11670
12673
|
init_capabilities();
|
|
11671
|
-
init_monetizationPolicy();
|
|
11672
12674
|
init_projectAccessStore();
|
|
11673
|
-
|
|
12675
|
+
}
|
|
12676
|
+
});
|
|
12677
|
+
|
|
12678
|
+
// ../mockup-engine/access/projectActions.ts
|
|
12679
|
+
var init_projectActions = __esm({
|
|
12680
|
+
"../mockup-engine/access/projectActions.ts"() {
|
|
12681
|
+
"use strict";
|
|
11674
12682
|
}
|
|
11675
12683
|
});
|
|
11676
12684
|
|
|
@@ -11678,6 +12686,8 @@ var init_projectLookStore = __esm({
|
|
|
11678
12686
|
var init_policy = __esm({
|
|
11679
12687
|
"../api/_lib/authorization/policy.ts"() {
|
|
11680
12688
|
"use strict";
|
|
12689
|
+
init_projectActions();
|
|
12690
|
+
init_monetizationPolicy();
|
|
11681
12691
|
}
|
|
11682
12692
|
});
|
|
11683
12693
|
|
|
@@ -11697,6 +12707,7 @@ var init_production = __esm({
|
|
|
11697
12707
|
init_entitlement();
|
|
11698
12708
|
init_monetizationPolicy();
|
|
11699
12709
|
init_projectLookStore();
|
|
12710
|
+
init_projectActions();
|
|
11700
12711
|
init_authorizeCapability();
|
|
11701
12712
|
init_capabilities();
|
|
11702
12713
|
init_policy();
|
|
@@ -12407,7 +13418,7 @@ var init_package = __esm({
|
|
|
12407
13418
|
"package.json"() {
|
|
12408
13419
|
package_default = {
|
|
12409
13420
|
name: "shotops-mcp",
|
|
12410
|
-
version: "0.9.
|
|
13421
|
+
version: "0.9.9",
|
|
12411
13422
|
private: false,
|
|
12412
13423
|
type: "module",
|
|
12413
13424
|
description: "The bundle-emitting MCP server over the @engine/@sync spine. Exposes ShotOps tools to ChatGPT, Claude Code, Cursor, and CI over Streamable HTTP with OAuth or personal API tokens. Hosted and MCP tool paths never accept, store, or forward a store-signing credential; the explicit local release CLI validates the user's existing key directly with Apple and records only its path. Renders via headless Playwright Chromium, the engine's native non-browser habitat (same path as mockup-mcp). Also ships as a free LOCAL stdio server (`npx shotops-mcp`) \u2014 same render, on your own machine, no account needed.",
|
|
@@ -12595,8 +13606,7 @@ function projectStructure(projectFile) {
|
|
|
12595
13606
|
}
|
|
12596
13607
|
function projectCopyLocales(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
12597
13608
|
const file = projectFile;
|
|
12598
|
-
const
|
|
12599
|
-
const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
|
|
13609
|
+
const { captionText, baseLocale } = projectLocaleAxis(projectFile);
|
|
12600
13610
|
const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
|
|
12601
13611
|
const { panelIds } = projectStructure(projectFile);
|
|
12602
13612
|
const out = [];
|
|
@@ -12613,8 +13623,7 @@ function projectCopyLocales(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
|
12613
13623
|
}
|
|
12614
13624
|
function projectCopyGaps(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
12615
13625
|
const file = projectFile;
|
|
12616
|
-
const
|
|
12617
|
-
const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
|
|
13626
|
+
const { captionText, baseLocale } = projectLocaleAxis(projectFile);
|
|
12618
13627
|
const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
|
|
12619
13628
|
const base = resolveOutputCaptionText(captionText, outputCaptions, family, baseLocale, baseLocale);
|
|
12620
13629
|
if (!Object.values(base).some((layers) => layers.some((layer) => layer.text.trim().length > 0))) return [];
|
|
@@ -12879,7 +13888,7 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
12879
13888
|
if (durableHostedProductionAvailable(deps) && args.preview !== true) {
|
|
12880
13889
|
let durableScreenshots;
|
|
12881
13890
|
try {
|
|
12882
|
-
durableScreenshots = await resolveScreenshots(entries, deps.userId, false);
|
|
13891
|
+
durableScreenshots = await resolveScreenshots(entries, deps.userId, false, deps.readUploadedRefs);
|
|
12883
13892
|
} catch (err) {
|
|
12884
13893
|
return errorResult(err.message);
|
|
12885
13894
|
}
|
|
@@ -12932,7 +13941,7 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
12932
13941
|
if (refused) return errorResult(refused.error);
|
|
12933
13942
|
let screenshots2;
|
|
12934
13943
|
try {
|
|
12935
|
-
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
13944
|
+
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
12936
13945
|
} catch (err) {
|
|
12937
13946
|
return errorResult(err.message);
|
|
12938
13947
|
}
|
|
@@ -13072,8 +14081,7 @@ function onePanelStyle(value, panelIndex2) {
|
|
|
13072
14081
|
}
|
|
13073
14082
|
function projectPanelCaptionLayers(projectFile, panelId, variant) {
|
|
13074
14083
|
const file = record3(projectFile);
|
|
13075
|
-
const baseLocale =
|
|
13076
|
-
const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
|
|
14084
|
+
const { captionText, baseLocale } = projectLocaleAxis(projectFile);
|
|
13077
14085
|
const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
|
|
13078
14086
|
const resolved = resolveOutputCaptionText(
|
|
13079
14087
|
captionText,
|
|
@@ -13087,6 +14095,7 @@ function projectPanelCaptionLayers(projectFile, panelId, variant) {
|
|
|
13087
14095
|
function projectPanelRecipes(projectFile, screens, targets) {
|
|
13088
14096
|
const structure = projectStructure(projectFile);
|
|
13089
14097
|
const file = record3(projectFile);
|
|
14098
|
+
const recordPreset = projectOutputAxis(projectFile).panelPresetId;
|
|
13090
14099
|
const shots = Array.isArray(file?.shots) ? file.shots : [];
|
|
13091
14100
|
const slotSizes = structure.panelIds.map(
|
|
13092
14101
|
(panelId) => shots.filter((shot) => record3(shot)?.panelId === panelId).length
|
|
@@ -13095,14 +14104,14 @@ function projectPanelRecipes(projectFile, screens, targets) {
|
|
|
13095
14104
|
const variant = projectRenderVariant(projectFile, target.locale, target.panelPresetId);
|
|
13096
14105
|
return [screens.resolve(variant).then((resolved) => {
|
|
13097
14106
|
const byShot = new Map(resolved.screenManifest.map((entry) => [entry.shotId, entry.bytes]));
|
|
13098
|
-
const dimensions = panelDimensionsFor(target.panelPresetId ??
|
|
14107
|
+
const dimensions = panelDimensionsFor(target.panelPresetId ?? recordPreset);
|
|
13099
14108
|
return structure.panelIds.flatMap((panelId, panelIndex2) => {
|
|
13100
14109
|
const screenshots2 = shots.filter((shot) => record3(shot)?.panelId === panelId).map((shot) => byShot.get(String(record3(shot)?.id))).filter((bytes) => typeof bytes === "string");
|
|
13101
14110
|
return [{
|
|
13102
14111
|
key: [target.output, target.locale, `panel-${String(panelIndex2 + 1).padStart(4, "0")}`].filter(Boolean).join("-"),
|
|
13103
14112
|
panelIndex: panelIndex2,
|
|
13104
14113
|
screenshots: screenshots2,
|
|
13105
|
-
panelPresetId: target.panelPresetId ??
|
|
14114
|
+
panelPresetId: target.panelPresetId ?? recordPreset,
|
|
13106
14115
|
look: onePanelLook(projectFile, panelIndex2, slotSizes, panelId),
|
|
13107
14116
|
captionLayers: projectPanelCaptionLayers(projectFile, panelId, variant),
|
|
13108
14117
|
backgroundViewport: { panelIndex: panelIndex2, panelCount: structure.panelIds.length },
|
|
@@ -13166,10 +14175,6 @@ function frameNamesFromProjectFile(projectFile) {
|
|
|
13166
14175
|
if (!Array.isArray(shots)) return [];
|
|
13167
14176
|
return shots.map((s) => s?.frameName).filter((n) => typeof n === "string" && n.length > 0);
|
|
13168
14177
|
}
|
|
13169
|
-
function panelPresetFromProjectFile(projectFile) {
|
|
13170
|
-
const preset = projectFile?.panelPresetId;
|
|
13171
|
-
return typeof preset === "string" && preset.length > 0 ? preset : void 0;
|
|
13172
|
-
}
|
|
13173
14178
|
function outputDevicesFromProjectFile(projectFile) {
|
|
13174
14179
|
const outputs = projectFile?.outputs;
|
|
13175
14180
|
if (!Array.isArray(outputs)) return { devices: [], dropped: [] };
|
|
@@ -13180,7 +14185,7 @@ async function loadProjectRecord(deps, projectId) {
|
|
|
13180
14185
|
const project2 = resolvedProject(resolution);
|
|
13181
14186
|
if (!project2) return { error: describeMiss(resolution, projectId) };
|
|
13182
14187
|
const rec = await deps.readProjectRecord(project2.id);
|
|
13183
|
-
if (!rec) {
|
|
14188
|
+
if (!rec || isBlankRecord(rec.record)) {
|
|
13184
14189
|
return {
|
|
13185
14190
|
error: `Project "${project2.name}" has no saved record yet \u2014 open it in ShotOps and save a strip first.`
|
|
13186
14191
|
};
|
|
@@ -13193,34 +14198,29 @@ async function loadProjectRecord(deps, projectId) {
|
|
|
13193
14198
|
};
|
|
13194
14199
|
}
|
|
13195
14200
|
function projectBaseLocale(projectFile) {
|
|
13196
|
-
|
|
13197
|
-
if (typeof file?.baseLocale === "string" && file.baseLocale) return file.baseLocale;
|
|
13198
|
-
const first = Array.isArray(file?.locales) ? file.locales[0] : void 0;
|
|
13199
|
-
return typeof first === "string" && first ? first : "en-US";
|
|
13200
|
-
}
|
|
13201
|
-
function projectLocales(projectFile) {
|
|
13202
|
-
const file = projectFile;
|
|
13203
|
-
const authored = Array.isArray(file?.locales) ? file.locales.filter((locale2) => typeof locale2 === "string" && locale2.length > 0) : [];
|
|
13204
|
-
const captionLocales = file?.captionText && typeof file.captionText === "object" ? Object.keys(file.captionText) : [];
|
|
13205
|
-
return Array.from(/* @__PURE__ */ new Set([projectBaseLocale(projectFile), ...authored, ...captionLocales]));
|
|
14201
|
+
return projectLocaleAxis(projectFile).baseLocale;
|
|
13206
14202
|
}
|
|
13207
14203
|
function projectRenderLocale(projectFile, requested) {
|
|
14204
|
+
const { locales, baseLocale } = projectLocaleAxis(projectFile);
|
|
13208
14205
|
const candidate = requested?.trim();
|
|
13209
|
-
return matchAuthoredLocale(
|
|
14206
|
+
return matchAuthoredLocale(locales, candidate) ?? candidate ?? baseLocale;
|
|
14207
|
+
}
|
|
14208
|
+
function activeRenderDevice(projectFile, renderable) {
|
|
14209
|
+
const active = projectOutputAxis(projectFile).activeDeviceId;
|
|
14210
|
+
return renderable.find((device) => device.id === active) ?? renderable[0];
|
|
13210
14211
|
}
|
|
13211
14212
|
function projectRenderVariant(projectFile, locale2, panelPresetId2) {
|
|
13212
|
-
const
|
|
13213
|
-
const preset = panelPresetId2 ?? (typeof file?.panelPresetId === "string" ? file.panelPresetId : void 0);
|
|
13214
|
-
const family = DEVICES.find((device) => device.panelPresetId === preset)?.screenFamily ?? BASE_SCREEN_FAMILY;
|
|
14213
|
+
const preset = panelPresetId2 ?? projectOutputAxis(projectFile).panelPresetId;
|
|
13215
14214
|
const baseLocale = projectBaseLocale(projectFile);
|
|
13216
|
-
|
|
14215
|
+
const resolved = projectRenderLocale(projectFile, locale2);
|
|
14216
|
+
return canonicalVariant({ family: screenFamilyForPreset(preset), locale: resolved }, baseLocale);
|
|
13217
14217
|
}
|
|
13218
14218
|
async function resolveProjectScreens(deps, loaded, supplied) {
|
|
13219
14219
|
const baseLocale = projectBaseLocale(loaded.projectFile);
|
|
13220
14220
|
const nested = supplied.map((e) => [e]);
|
|
13221
14221
|
let resolvedBytes;
|
|
13222
14222
|
try {
|
|
13223
|
-
resolvedBytes = await resolveScreenshots(nested, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
14223
|
+
resolvedBytes = await resolveScreenshots(nested, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
13224
14224
|
} catch (err) {
|
|
13225
14225
|
return { error: err.message };
|
|
13226
14226
|
}
|
|
@@ -13262,7 +14262,7 @@ async function resolveProjectScreens(deps, loaded, supplied) {
|
|
|
13262
14262
|
"lenient",
|
|
13263
14263
|
baseLocale
|
|
13264
14264
|
);
|
|
13265
|
-
const
|
|
14265
|
+
const storedManifest2 = parsed ? allManifestEntries(parsed) : [];
|
|
13266
14266
|
const repairFindings = [];
|
|
13267
14267
|
let canReadStored = Boolean(deps.fetchHandoffBytes || deps.readProjectScreens);
|
|
13268
14268
|
const storedSources = [];
|
|
@@ -13292,7 +14292,7 @@ async function resolveProjectScreens(deps, loaded, supplied) {
|
|
|
13292
14292
|
canReadStored = Boolean(deps.fetchHandoffBytes);
|
|
13293
14293
|
}
|
|
13294
14294
|
} else if (deps.fetchHandoffBytes) {
|
|
13295
|
-
const resolvedManifest = resolveScreenManifest(
|
|
14295
|
+
const resolvedManifest = resolveScreenManifest(storedManifest2, shots);
|
|
13296
14296
|
repairFindings.push(...resolvedManifest.findings);
|
|
13297
14297
|
for (const cell of resolvedManifest.cells) {
|
|
13298
14298
|
let pending;
|
|
@@ -13375,7 +14375,7 @@ async function resolveProjectScreens(deps, loaded, supplied) {
|
|
|
13375
14375
|
}
|
|
13376
14376
|
return { screenManifest, missing };
|
|
13377
14377
|
};
|
|
13378
|
-
return { expected, unnamed, storedCount:
|
|
14378
|
+
return { expected, unnamed, storedCount: storedManifest2.length, canReadStored, resolve: resolve14, plan, repairFindings };
|
|
13379
14379
|
}
|
|
13380
14380
|
function noProjectScreensMessage(screens, suppliedCount, toolName) {
|
|
13381
14381
|
let lead;
|
|
@@ -13393,7 +14393,7 @@ function noProjectScreensMessage(screens, suppliedCount, toolName) {
|
|
|
13393
14393
|
return `${lead} This project expects: ${screens.expected.join(", ")}. Pass each raw screenshot with its original \`name\` and optional \`variant\`, in any order \u2014 ${toolName} matches by name + coordinate.` + route;
|
|
13394
14394
|
}
|
|
13395
14395
|
function coordinateLabel(variant) {
|
|
13396
|
-
return !variant ||
|
|
14396
|
+
return !variant || isBaseVariant(variant) ? "base" : variantKey(variant);
|
|
13397
14397
|
}
|
|
13398
14398
|
function projectScreenResolutionNote(shots) {
|
|
13399
14399
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -13473,8 +14473,8 @@ async function renderProject(deps, args, scope, attempt, signal) {
|
|
|
13473
14473
|
const requested = resolveOutputDevices(args.outputs);
|
|
13474
14474
|
const chosen = requested.devices.length > 0 ? requested : outputDevicesFromProjectFile(projectFile);
|
|
13475
14475
|
const droppedNote = droppedOutputsNote(chosen.dropped, renderedSavedSet(chosen.devices));
|
|
13476
|
-
const recordPreset =
|
|
13477
|
-
const active =
|
|
14476
|
+
const recordPreset = projectOutputAxis(projectFile).panelPresetId;
|
|
14477
|
+
const active = activeRenderDevice(projectFile, chosen.devices);
|
|
13478
14478
|
const ordered = active ? [active, ...chosen.devices.filter((d) => d.id !== active.id)] : [];
|
|
13479
14479
|
const mayOverridePreset = requested.devices.length > 0 || chosen.devices.length > 1;
|
|
13480
14480
|
const requestedLocale = args.locale?.trim() || void 0;
|
|
@@ -13498,7 +14498,7 @@ async function renderProject(deps, args, scope, attempt, signal) {
|
|
|
13498
14498
|
locales: [snapshotLocale],
|
|
13499
14499
|
outputs: ordered.map((device) => device.id),
|
|
13500
14500
|
unsupportedOutputs: chosen.dropped,
|
|
13501
|
-
panelPresetId: active?.panelPresetId ?? recordPreset
|
|
14501
|
+
panelPresetId: active?.panelPresetId ?? recordPreset
|
|
13502
14502
|
},
|
|
13503
14503
|
baseLocale: projectBaseLocale(projectFile),
|
|
13504
14504
|
gaps: repairGaps(screens.repairFindings)
|
|
@@ -13519,9 +14519,9 @@ async function renderProject(deps, args, scope, attempt, signal) {
|
|
|
13519
14519
|
});
|
|
13520
14520
|
};
|
|
13521
14521
|
if (durableHostedProductionAvailable(deps) && args.preview !== true) {
|
|
13522
|
-
const activePreset = active && mayOverridePreset ? active.panelPresetId : recordPreset
|
|
14522
|
+
const activePreset = active && mayOverridePreset ? active.panelPresetId : recordPreset;
|
|
13523
14523
|
const dimensions = panelDimensionsFor(activePreset);
|
|
13524
|
-
const targets = ordered.length > 0 ? ordered.map((device) => ({ locale: snapshotLocale, panelPresetId: device.panelPresetId, output: device.id })) : [{ locale: snapshotLocale,
|
|
14524
|
+
const targets = ordered.length > 0 ? ordered.map((device) => ({ locale: snapshotLocale, panelPresetId: device.panelPresetId, output: device.id })) : [{ locale: snapshotLocale, panelPresetId: recordPreset }];
|
|
13525
14525
|
const recipes = await projectPanelRecipes(projectFile, screens, targets);
|
|
13526
14526
|
const panelCount = projectStructure(projectFile).panelIds.length || recipes.length;
|
|
13527
14527
|
const durableResolved = await screens.resolve(activeVariant);
|
|
@@ -13768,6 +14768,7 @@ var init_toolsRender = __esm({
|
|
|
13768
14768
|
init_productionRenderAccess();
|
|
13769
14769
|
init_outputMode();
|
|
13770
14770
|
init_appstore();
|
|
14771
|
+
init_recordEnvelope();
|
|
13771
14772
|
init_screenshotInput();
|
|
13772
14773
|
init_uploads();
|
|
13773
14774
|
init_resultContract();
|
|
@@ -13847,6 +14848,83 @@ var init_refineProjectReceipt = __esm({
|
|
|
13847
14848
|
}
|
|
13848
14849
|
});
|
|
13849
14850
|
|
|
14851
|
+
// ../mockup-engine/recordFromStrip.ts
|
|
14852
|
+
function normalizeSourceName(raw) {
|
|
14853
|
+
const name = raw?.trim();
|
|
14854
|
+
if (!name) return null;
|
|
14855
|
+
return /\.(png|jpe?g|webp)$/i.test(name) ? name : `${name}.png`;
|
|
14856
|
+
}
|
|
14857
|
+
function frameNamesFor(shots, shotNames) {
|
|
14858
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14859
|
+
return shots.map((_, i) => {
|
|
14860
|
+
const real = normalizeSourceName(shotNames?.[i]);
|
|
14861
|
+
if (!real) return `screen-${i + 1}.png`;
|
|
14862
|
+
const count = seen.get(real) ?? 0;
|
|
14863
|
+
seen.set(real, count + 1);
|
|
14864
|
+
if (count === 0) return real;
|
|
14865
|
+
const dot = real.lastIndexOf(".");
|
|
14866
|
+
return `${real.slice(0, dot)}-${count + 1}${real.slice(dot)}`;
|
|
14867
|
+
});
|
|
14868
|
+
}
|
|
14869
|
+
function recordFromStrip(input) {
|
|
14870
|
+
const { look: look2, panelIds, captionText, locale: locale2, shotNames, outputs, locales, baseLocale } = input;
|
|
14871
|
+
const panels = panelIds.map((id) => ({ id }));
|
|
14872
|
+
const frameNames = frameNamesFor(look2.shots, shotNames);
|
|
14873
|
+
const shots = look2.shots.map((s, i) => ({
|
|
14874
|
+
id: `shot-${i + 1}`,
|
|
14875
|
+
panelId: s.panelId,
|
|
14876
|
+
frameNodeId: frameNames[i],
|
|
14877
|
+
frameName: frameNames[i],
|
|
14878
|
+
bytes: null,
|
|
14879
|
+
look: s.look,
|
|
14880
|
+
thumb: null,
|
|
14881
|
+
thumbStale: true
|
|
14882
|
+
}));
|
|
14883
|
+
return serializeProject({
|
|
14884
|
+
shots,
|
|
14885
|
+
panels,
|
|
14886
|
+
selectedShotId: shots[0]?.id ?? null,
|
|
14887
|
+
panelPresetId: look2.panelPresetId,
|
|
14888
|
+
bgMode: look2.bgMode,
|
|
14889
|
+
// #556 — the CANONICAL gradient, forwarded alongside the mirror. `stripFromSlots` genuinely
|
|
14890
|
+
// resolves stops+angle (it runs every styling source through `stripGradientOf`), so omitting
|
|
14891
|
+
// them here was live data loss: a three-stop backdrop at 37° was persisted as its two-stop
|
|
14892
|
+
// mirror on the nearest axis, and the middle stops were gone from the record for good. The
|
|
14893
|
+
// sibling render path (`buildStrip.ts`, #463) always got this right.
|
|
14894
|
+
gradientStops: look2.gradientStops,
|
|
14895
|
+
gradientAngle: look2.gradientAngle,
|
|
14896
|
+
gradientFrom: look2.gradientFrom,
|
|
14897
|
+
gradientTo: look2.gradientTo,
|
|
14898
|
+
gradientDir: look2.gradientDir,
|
|
14899
|
+
panelColors: look2.panelColors,
|
|
14900
|
+
shadow: look2.shadow,
|
|
14901
|
+
// #564 — the same drop, one field away and not a mirror bug at all: `serialize.ts` turns an
|
|
14902
|
+
// absent `floorReflection` into `false`, so a render that showed a floor reflection saved a
|
|
14903
|
+
// project that did not have one (the `studio` design preset is one of the two that turn it on).
|
|
14904
|
+
floorReflection: look2.floorReflection,
|
|
14905
|
+
panelBackgrounds: look2.panelBackgrounds,
|
|
14906
|
+
captionStyles: look2.captionStyles,
|
|
14907
|
+
// #40 — the selected target devices. Passed straight to serializeProject, which runs it
|
|
14908
|
+
// through deriveOutputs: an undefined list migrates to the single device matching
|
|
14909
|
+
// `panelPresetId` (the pre-#40 record, unchanged), a supplied one is trusted verbatim minus
|
|
14910
|
+
// unknown/coming-soon ids.
|
|
14911
|
+
outputs,
|
|
14912
|
+
// #1001 — the selected languages, when the caller knows them. Omitted by every render path, and
|
|
14913
|
+
// `serializeProject` then derives the list exactly as it always has (caption keys ∪ the base).
|
|
14914
|
+
...locales ? { locales } : {},
|
|
14915
|
+
...baseLocale ? { baseLocale } : {},
|
|
14916
|
+
// Nest this render's flat caption text under its locale (default en-US) so the stored blob
|
|
14917
|
+
// is locale-correct; serializeProject's normalizeCaptionText passes a nested map through.
|
|
14918
|
+
captionText: captionText && Object.keys(captionText).length > 0 ? { [locale2 || DEFAULT_LOCALE]: captionText } : {}
|
|
14919
|
+
});
|
|
14920
|
+
}
|
|
14921
|
+
var init_recordFromStrip = __esm({
|
|
14922
|
+
"../mockup-engine/recordFromStrip.ts"() {
|
|
14923
|
+
"use strict";
|
|
14924
|
+
init_appstore();
|
|
14925
|
+
}
|
|
14926
|
+
});
|
|
14927
|
+
|
|
13850
14928
|
// ../mockup-engine/stripFromSlots.ts
|
|
13851
14929
|
function staggeredHOffset(k) {
|
|
13852
14930
|
return String(Math.max(-100, Math.min(100, k * 35)));
|
|
@@ -13982,111 +15060,282 @@ function stripFromSlots(input) {
|
|
|
13982
15060
|
}
|
|
13983
15061
|
}
|
|
13984
15062
|
});
|
|
13985
|
-
const localeLayers = normalizeCaptionText({ [DEFAULT_LOCALE]: inner }, normalizeCaptionStyles(rawStyles));
|
|
13986
|
-
captionStyles = captionStylesFromLayers(localeLayers);
|
|
13987
|
-
captionLayers = resolveCaptionText(localeLayers);
|
|
15063
|
+
const localeLayers = normalizeCaptionText({ [DEFAULT_LOCALE]: inner }, normalizeCaptionStyles(rawStyles));
|
|
15064
|
+
captionStyles = captionStylesFromLayers(localeLayers);
|
|
15065
|
+
captionLayers = resolveCaptionText(localeLayers);
|
|
15066
|
+
}
|
|
15067
|
+
const panelPresetId2 = input.panelPresetId ?? (typeof opaque?.panelPresetId === "string" ? opaque.panelPresetId : void 0) ?? "r69";
|
|
15068
|
+
return {
|
|
15069
|
+
look: {
|
|
15070
|
+
panelPresetId: panelPresetId2,
|
|
15071
|
+
bgMode,
|
|
15072
|
+
gradientStops,
|
|
15073
|
+
gradientAngle,
|
|
15074
|
+
gradientFrom,
|
|
15075
|
+
gradientTo,
|
|
15076
|
+
gradientDir,
|
|
15077
|
+
panelColors,
|
|
15078
|
+
shadow,
|
|
15079
|
+
floorReflection,
|
|
15080
|
+
panelBackgrounds,
|
|
15081
|
+
captionStyles,
|
|
15082
|
+
shots
|
|
15083
|
+
},
|
|
15084
|
+
panelIds,
|
|
15085
|
+
captionText: captionLayers
|
|
15086
|
+
};
|
|
15087
|
+
}
|
|
15088
|
+
var NUMERIC_STRING_FIELDS;
|
|
15089
|
+
var init_stripFromSlots = __esm({
|
|
15090
|
+
"../mockup-engine/stripFromSlots.ts"() {
|
|
15091
|
+
"use strict";
|
|
15092
|
+
init_appstore();
|
|
15093
|
+
init_gradient();
|
|
15094
|
+
NUMERIC_STRING_FIELDS = ["roll", "deviceSize", "hOffset", "vOffset", "finish", "clearcoat"];
|
|
15095
|
+
}
|
|
15096
|
+
});
|
|
15097
|
+
|
|
15098
|
+
// ../mockup-engine/appstore/importScreens.ts
|
|
15099
|
+
var init_importScreens = __esm({
|
|
15100
|
+
"../mockup-engine/appstore/importScreens.ts"() {
|
|
15101
|
+
"use strict";
|
|
15102
|
+
init_defaults();
|
|
15103
|
+
init_recordFromStrip();
|
|
15104
|
+
init_stripFromSlots();
|
|
15105
|
+
init_projectFileGuard();
|
|
15106
|
+
init_recordEnvelope();
|
|
15107
|
+
init_screenManifest();
|
|
15108
|
+
}
|
|
15109
|
+
});
|
|
15110
|
+
|
|
15111
|
+
// ../mockup-engine/appstore/reach.ts
|
|
15112
|
+
function reachUploadsScreens(reach) {
|
|
15113
|
+
return reach === "account";
|
|
15114
|
+
}
|
|
15115
|
+
function readReach(value) {
|
|
15116
|
+
if (value === "device" || value === "account") return value;
|
|
15117
|
+
if (value === "shared") return "account";
|
|
15118
|
+
return null;
|
|
15119
|
+
}
|
|
15120
|
+
var init_reach = __esm({
|
|
15121
|
+
"../mockup-engine/appstore/reach.ts"() {
|
|
15122
|
+
"use strict";
|
|
15123
|
+
}
|
|
15124
|
+
});
|
|
15125
|
+
|
|
15126
|
+
// ../mockup-engine/ops/persistenceOps.ts
|
|
15127
|
+
function envelopeOf(record8) {
|
|
15128
|
+
return record8 != null && typeof record8 === "object" && !Array.isArray(record8) ? record8 : {};
|
|
15129
|
+
}
|
|
15130
|
+
function storedManifest(record8) {
|
|
15131
|
+
const parsed = parseScreenManifest(envelopeOf(record8).handoffRefs);
|
|
15132
|
+
return parsed ? allManifestEntries(parsed) : [];
|
|
15133
|
+
}
|
|
15134
|
+
function storedCustody(record8) {
|
|
15135
|
+
const parsed = parseScreenManifest(envelopeOf(record8).screenPurgeRefs);
|
|
15136
|
+
return parsed ? allManifestEntries(parsed) : [];
|
|
15137
|
+
}
|
|
15138
|
+
function writeReach(record8, override) {
|
|
15139
|
+
if (override) return override;
|
|
15140
|
+
return readReach(envelopeOf(record8).reach) ?? "account";
|
|
15141
|
+
}
|
|
15142
|
+
function planScreenSave(args) {
|
|
15143
|
+
const kept = storedManifest(args.record);
|
|
15144
|
+
const reach = writeReach(args.record, args.reach);
|
|
15145
|
+
if (!reachUploadsScreens(reach)) {
|
|
15146
|
+
return { toUpload: [], kept, alreadyDurable: [], nothingToUpload: true };
|
|
15147
|
+
}
|
|
15148
|
+
const { toUpload } = planScreenUploads(args.cells, kept, args.shots);
|
|
15149
|
+
const going = new Set(toUpload.map((cell) => manifestShotCellKey(cell.shotId, cell.variant)));
|
|
15150
|
+
const alreadyDurable = args.cells.filter(
|
|
15151
|
+
(cell) => !going.has(manifestShotCellKey(cell.shotId, cell.variant))
|
|
15152
|
+
);
|
|
15153
|
+
return {
|
|
15154
|
+
toUpload: toUpload.map(({ shotId, variant, hash }) => ({ shotId, variant, hash })),
|
|
15155
|
+
kept,
|
|
15156
|
+
alreadyDurable: alreadyDurable.map(({ shotId, variant, hash }) => ({ shotId, variant, hash })),
|
|
15157
|
+
nothingToUpload: toUpload.length === 0
|
|
15158
|
+
};
|
|
15159
|
+
}
|
|
15160
|
+
function reserveUploads(cells, slots) {
|
|
15161
|
+
const byCell = new Map(cells.map((cell) => [manifestShotCellKey(cell.shotId, cell.variant), cell]));
|
|
15162
|
+
const reserved = [];
|
|
15163
|
+
for (const slot2 of slots) {
|
|
15164
|
+
const cell = byCell.get(manifestShotCellKey(slot2.shotId, slot2.variant));
|
|
15165
|
+
if (!cell) continue;
|
|
15166
|
+
reserved.push({
|
|
15167
|
+
cell,
|
|
15168
|
+
slot: slot2,
|
|
15169
|
+
entry: { shotId: slot2.shotId, variant: slot2.variant, hash: cell.hash, ref: slot2.ref }
|
|
15170
|
+
});
|
|
13988
15171
|
}
|
|
13989
|
-
|
|
15172
|
+
return reserved;
|
|
15173
|
+
}
|
|
15174
|
+
function recordScreenSave(args) {
|
|
15175
|
+
const reach = writeReach(args.record, args.reach);
|
|
15176
|
+
const manifest = reachUploadsScreens(reach) ? mergeManifest(args.kept, args.uploaded) : [];
|
|
15177
|
+
const custody = custodyUnion(storedCustody(args.record), args.heldCustody ?? [], args.kept, args.uploaded);
|
|
15178
|
+
const envelope = envelopeOf(args.record);
|
|
13990
15179
|
return {
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
shadow,
|
|
14001
|
-
floorReflection,
|
|
14002
|
-
panelBackgrounds,
|
|
14003
|
-
captionStyles,
|
|
14004
|
-
shots
|
|
14005
|
-
},
|
|
14006
|
-
panelIds,
|
|
14007
|
-
captionText: captionLayers
|
|
15180
|
+
record: rebuildEnvelope(args.record, {
|
|
15181
|
+
project: envelope.project,
|
|
15182
|
+
manifest,
|
|
15183
|
+
reach,
|
|
15184
|
+
...custody.length > 0 ? { screenPurgeRefs: custody } : {}
|
|
15185
|
+
}),
|
|
15186
|
+
manifest,
|
|
15187
|
+
custody,
|
|
15188
|
+
reach
|
|
14008
15189
|
};
|
|
14009
15190
|
}
|
|
14010
|
-
|
|
14011
|
-
|
|
14012
|
-
|
|
15191
|
+
async function saveProjectScreens(request, transport) {
|
|
15192
|
+
const reach = writeReach(request.record, request.reach);
|
|
15193
|
+
const plan = planScreenSave({
|
|
15194
|
+
record: request.record,
|
|
15195
|
+
shots: request.shots,
|
|
15196
|
+
cells: request.cells,
|
|
15197
|
+
reach
|
|
15198
|
+
});
|
|
15199
|
+
const build = request.rebuild ?? ((rewrite) => recordScreenSave({
|
|
15200
|
+
record: request.record,
|
|
15201
|
+
kept: rewrite.manifest,
|
|
15202
|
+
uploaded: [],
|
|
15203
|
+
heldCustody: rewrite.custody,
|
|
15204
|
+
reach: rewrite.reach
|
|
15205
|
+
}).record);
|
|
15206
|
+
const overtaken = () => transport.overtaken?.() === true;
|
|
15207
|
+
const held = custodyUnion(storedCustody(request.record), request.heldCustody ?? []);
|
|
15208
|
+
const settled = (uploaded2, custody2, projectId2, stopped) => {
|
|
15209
|
+
const manifest = reachUploadsScreens(reach) ? mergeManifest(plan.kept, uploaded2) : [];
|
|
15210
|
+
return {
|
|
15211
|
+
projectId: projectId2,
|
|
15212
|
+
record: build({ manifest, custody: custody2, reach }),
|
|
15213
|
+
manifest,
|
|
15214
|
+
custody: custody2,
|
|
15215
|
+
uploaded: uploaded2,
|
|
15216
|
+
stopped
|
|
15217
|
+
};
|
|
15218
|
+
};
|
|
15219
|
+
let projectId = request.projectId;
|
|
15220
|
+
if (projectId === null) {
|
|
15221
|
+
if (!transport.insertRow) throw new Error("This project has no row yet and the transport cannot insert one");
|
|
15222
|
+
const custody2 = custodyUnion(held, plan.kept);
|
|
15223
|
+
const created = await transport.insertRow({ record: build({ manifest: plan.kept, custody: custody2, reach }) });
|
|
15224
|
+
projectId = created.projectId;
|
|
15225
|
+
if (overtaken()) return settled([], custody2, projectId, true);
|
|
15226
|
+
}
|
|
15227
|
+
if (plan.nothingToUpload) {
|
|
15228
|
+
return settled([], custodyUnion(held, plan.kept), projectId, false);
|
|
15229
|
+
}
|
|
15230
|
+
const slots = await transport.mintUploadSlots({ projectId, cells: plan.toUpload });
|
|
15231
|
+
if (overtaken()) return settled([], custodyUnion(held, plan.kept), projectId, true);
|
|
15232
|
+
const reserved = reserveUploads(request.cells, slots);
|
|
15233
|
+
const custody = custodyUnion(held, plan.kept, reserved.map(({ entry }) => entry));
|
|
15234
|
+
if (reserved.length === 0) return settled([], custody, projectId, false);
|
|
15235
|
+
await transport.saveRecord({ projectId, record: build({ manifest: plan.kept, custody, reach }) });
|
|
15236
|
+
if (overtaken()) return settled([], custody, projectId, true);
|
|
15237
|
+
const uploaded = [];
|
|
15238
|
+
for (const { entry, slot: slot2, cell } of reserved) {
|
|
15239
|
+
await transport.putBytes({ slot: slot2, bytes: cell.bytes });
|
|
15240
|
+
if (overtaken()) return settled([], custody, projectId, true);
|
|
15241
|
+
uploaded.push(entry);
|
|
15242
|
+
}
|
|
15243
|
+
const done = settled(uploaded, custody, projectId, false);
|
|
15244
|
+
await transport.saveRecord({ projectId, record: done.record });
|
|
15245
|
+
return done;
|
|
15246
|
+
}
|
|
15247
|
+
var init_persistenceOps = __esm({
|
|
15248
|
+
"../mockup-engine/ops/persistenceOps.ts"() {
|
|
14013
15249
|
"use strict";
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
15250
|
+
init_recordEnvelope();
|
|
15251
|
+
init_importScreens();
|
|
15252
|
+
init_screenManifest();
|
|
15253
|
+
init_screenUploadPlan();
|
|
15254
|
+
init_variant();
|
|
15255
|
+
init_reach();
|
|
15256
|
+
init_reach();
|
|
14017
15257
|
}
|
|
14018
15258
|
});
|
|
14019
15259
|
|
|
14020
|
-
// ../mockup-engine/
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
15260
|
+
// ../mockup-engine/appstore/blankProject.ts
|
|
15261
|
+
var init_blankProject = __esm({
|
|
15262
|
+
"../mockup-engine/appstore/blankProject.ts"() {
|
|
15263
|
+
"use strict";
|
|
15264
|
+
init_recordFromStrip();
|
|
15265
|
+
init_stripFromSlots();
|
|
15266
|
+
init_recordEnvelope();
|
|
15267
|
+
}
|
|
15268
|
+
});
|
|
15269
|
+
|
|
15270
|
+
// ../mockup-engine/appstore/captionEdit.ts
|
|
15271
|
+
var init_captionEdit = __esm({
|
|
15272
|
+
"../mockup-engine/appstore/captionEdit.ts"() {
|
|
15273
|
+
"use strict";
|
|
15274
|
+
init_defaults();
|
|
15275
|
+
init_captionModel();
|
|
15276
|
+
init_recordTransforms();
|
|
15277
|
+
}
|
|
15278
|
+
});
|
|
15279
|
+
|
|
15280
|
+
// ../mockup-engine/captionTranslationPlan.ts
|
|
15281
|
+
var init_captionTranslationPlan = __esm({
|
|
15282
|
+
"../mockup-engine/captionTranslationPlan.ts"() {
|
|
15283
|
+
"use strict";
|
|
15284
|
+
}
|
|
15285
|
+
});
|
|
15286
|
+
|
|
15287
|
+
// ../mockup-engine/appstore/translationTable.ts
|
|
15288
|
+
var init_translationTable = __esm({
|
|
15289
|
+
"../mockup-engine/appstore/translationTable.ts"() {
|
|
15290
|
+
"use strict";
|
|
15291
|
+
init_appStoreLocales();
|
|
15292
|
+
init_captionEdit();
|
|
15293
|
+
init_captionModel();
|
|
15294
|
+
init_localeAxis();
|
|
15295
|
+
init_captionTranslationPlan();
|
|
15296
|
+
}
|
|
15297
|
+
});
|
|
15298
|
+
|
|
15299
|
+
// ../mockup-engine/ops/editOps.ts
|
|
15300
|
+
function buildRecord(input) {
|
|
15301
|
+
const { look: look2, panelIds, captionText } = stripFromSlots({
|
|
15302
|
+
slotSizes: input.slotSizes,
|
|
15303
|
+
panelPresetId: input.panelPresetId,
|
|
15304
|
+
look: input.look ?? void 0,
|
|
15305
|
+
style: input.style ?? void 0
|
|
14036
15306
|
});
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
frameName: frameNames[i],
|
|
14047
|
-
bytes: null,
|
|
14048
|
-
look: s.look,
|
|
14049
|
-
thumb: null,
|
|
14050
|
-
thumbStale: true
|
|
14051
|
-
}));
|
|
14052
|
-
return serializeProject({
|
|
14053
|
-
shots,
|
|
14054
|
-
panels,
|
|
14055
|
-
selectedShotId: shots[0]?.id ?? null,
|
|
14056
|
-
panelPresetId: look2.panelPresetId,
|
|
14057
|
-
bgMode: look2.bgMode,
|
|
14058
|
-
// #556 — the CANONICAL gradient, forwarded alongside the mirror. `stripFromSlots` genuinely
|
|
14059
|
-
// resolves stops+angle (it runs every styling source through `stripGradientOf`), so omitting
|
|
14060
|
-
// them here was live data loss: a three-stop backdrop at 37° was persisted as its two-stop
|
|
14061
|
-
// mirror on the nearest axis, and the middle stops were gone from the record for good. The
|
|
14062
|
-
// sibling render path (`buildStrip.ts`, #463) always got this right.
|
|
14063
|
-
gradientStops: look2.gradientStops,
|
|
14064
|
-
gradientAngle: look2.gradientAngle,
|
|
14065
|
-
gradientFrom: look2.gradientFrom,
|
|
14066
|
-
gradientTo: look2.gradientTo,
|
|
14067
|
-
gradientDir: look2.gradientDir,
|
|
14068
|
-
panelColors: look2.panelColors,
|
|
14069
|
-
shadow: look2.shadow,
|
|
14070
|
-
// #564 — the same drop, one field away and not a mirror bug at all: `serialize.ts` turns an
|
|
14071
|
-
// absent `floorReflection` into `false`, so a render that showed a floor reflection saved a
|
|
14072
|
-
// project that did not have one (the `studio` design preset is one of the two that turn it on).
|
|
14073
|
-
floorReflection: look2.floorReflection,
|
|
14074
|
-
panelBackgrounds: look2.panelBackgrounds,
|
|
14075
|
-
captionStyles: look2.captionStyles,
|
|
14076
|
-
// #40 — the selected target devices. Passed straight to serializeProject, which runs it
|
|
14077
|
-
// through deriveOutputs: an undefined list migrates to the single device matching
|
|
14078
|
-
// `panelPresetId` (the pre-#40 record, unchanged), a supplied one is trusted verbatim minus
|
|
14079
|
-
// unknown/coming-soon ids.
|
|
14080
|
-
outputs,
|
|
14081
|
-
// Nest this render's flat caption text under its locale (default en-US) so the stored blob
|
|
14082
|
-
// is locale-correct; serializeProject's normalizeCaptionText passes a nested map through.
|
|
14083
|
-
captionText: captionText && Object.keys(captionText).length > 0 ? { [locale2 || DEFAULT_LOCALE]: captionText } : {}
|
|
15307
|
+
return recordFromStrip({
|
|
15308
|
+
look: look2,
|
|
15309
|
+
panelIds,
|
|
15310
|
+
captionText,
|
|
15311
|
+
locale: input.locale,
|
|
15312
|
+
shotNames: input.screenshotNames,
|
|
15313
|
+
outputs: input.outputs,
|
|
15314
|
+
...input.locales ? { locales: input.locales } : {},
|
|
15315
|
+
...input.baseLocale ? { baseLocale: input.baseLocale } : {}
|
|
14084
15316
|
});
|
|
14085
15317
|
}
|
|
14086
|
-
var
|
|
14087
|
-
|
|
15318
|
+
var STRIP_PREVIEW;
|
|
15319
|
+
var init_editOps = __esm({
|
|
15320
|
+
"../mockup-engine/ops/editOps.ts"() {
|
|
14088
15321
|
"use strict";
|
|
15322
|
+
init_gradient();
|
|
15323
|
+
init_variant();
|
|
15324
|
+
init_persistenceOps();
|
|
14089
15325
|
init_appstore();
|
|
15326
|
+
init_appListing();
|
|
15327
|
+
init_blankProject();
|
|
15328
|
+
init_captionEdit();
|
|
15329
|
+
init_recordEnvelope();
|
|
15330
|
+
init_translationTable();
|
|
15331
|
+
init_recordFromStrip();
|
|
15332
|
+
init_stripFromSlots();
|
|
15333
|
+
STRIP_PREVIEW = {
|
|
15334
|
+
/** The band's aspect, width ÷ height. */
|
|
15335
|
+
bandRatio: 25 / 11,
|
|
15336
|
+
/** How many frames fill the band before it starts scrolling. */
|
|
15337
|
+
framesAcross: 5
|
|
15338
|
+
};
|
|
14090
15339
|
}
|
|
14091
15340
|
});
|
|
14092
15341
|
|
|
@@ -14158,8 +15407,7 @@ var init_renderer = __esm({
|
|
|
14158
15407
|
"src/render/renderer.ts"() {
|
|
14159
15408
|
"use strict";
|
|
14160
15409
|
init_packagePaths();
|
|
14161
|
-
|
|
14162
|
-
init_recordFromStrip();
|
|
15410
|
+
init_editOps();
|
|
14163
15411
|
init_log();
|
|
14164
15412
|
init_staticServer();
|
|
14165
15413
|
init_browserInstall();
|
|
@@ -14245,23 +15493,20 @@ var init_renderer = __esm({
|
|
|
14245
15493
|
// buildRecord: the SAME styling seam the render uses (stripFromSlots → recordFromStrip), run
|
|
14246
15494
|
// in Node with NO browser. Both are pure, browser-free @engine functions and a record carries
|
|
14247
15495
|
// no bytes, so save_project persists an editable project without a multi-second Chromium render
|
|
14248
|
-
// — the fix for the Fly proxy timeout on that path.
|
|
14249
|
-
//
|
|
14250
|
-
//
|
|
15496
|
+
// — the fix for the Fly proxy timeout on that path. It is the engine's `buildRecord` op, the one
|
|
15497
|
+
// the phone's bridge calls too, so a save_project record, a render({buildRecord:true}) record and
|
|
15498
|
+
// a record built on the phone are the same record rather than three copies kept aligned by hand.
|
|
14251
15499
|
async buildRecord(input) {
|
|
14252
|
-
|
|
15500
|
+
return buildRecord({
|
|
14253
15501
|
slotSizes: input.slotSizes,
|
|
14254
15502
|
panelPresetId: input.panelPresetId,
|
|
14255
15503
|
look: input.look ?? void 0,
|
|
14256
|
-
style: input.style ?? void 0
|
|
14257
|
-
});
|
|
14258
|
-
return recordFromStrip({
|
|
14259
|
-
look: look2,
|
|
14260
|
-
panelIds,
|
|
14261
|
-
captionText,
|
|
15504
|
+
style: input.style ?? void 0,
|
|
14262
15505
|
locale: input.locale,
|
|
14263
|
-
|
|
14264
|
-
outputs: input.outputs
|
|
15506
|
+
screenshotNames: input.screenshotNames,
|
|
15507
|
+
outputs: input.outputs,
|
|
15508
|
+
locales: input.locales,
|
|
15509
|
+
baseLocale: input.baseLocale
|
|
14265
15510
|
});
|
|
14266
15511
|
}
|
|
14267
15512
|
async render(input) {
|
|
@@ -14398,6 +15643,9 @@ function parseControlPlaneResponse(op, value) {
|
|
|
14398
15643
|
case "list_projects":
|
|
14399
15644
|
if (!Array.isArray(body.projects)) throw new Error("control plane response is missing projects");
|
|
14400
15645
|
if (!object(body.ownedProjects)) throw new Error("control plane response is missing ownedProjects");
|
|
15646
|
+
for (const row2 of body.projects) {
|
|
15647
|
+
if (!object(object(row2)?.actions)) throw new Error("control plane response project row is missing actions");
|
|
15648
|
+
}
|
|
14401
15649
|
break;
|
|
14402
15650
|
case "read_look":
|
|
14403
15651
|
if (!Object.prototype.hasOwnProperty.call(body, "look")) throw new Error("control plane response is missing look");
|
|
@@ -14422,6 +15670,10 @@ function parseControlPlaneResponse(op, value) {
|
|
|
14422
15670
|
case "read_attachment_screens":
|
|
14423
15671
|
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
14424
15672
|
break;
|
|
15673
|
+
case "read_upload_screens":
|
|
15674
|
+
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
15675
|
+
if (body.missing !== void 0 && !Array.isArray(body.missing)) throw new Error("control plane response has malformed missing");
|
|
15676
|
+
break;
|
|
14425
15677
|
case "refine_project":
|
|
14426
15678
|
if (typeof body.ok !== "boolean") throw new Error("control plane response is missing refine_project result");
|
|
14427
15679
|
if (body.ok) {
|
|
@@ -14462,6 +15714,16 @@ function parseControlPlaneResponse(op, value) {
|
|
|
14462
15714
|
throw new Error("control plane response is missing clear_look result");
|
|
14463
15715
|
}
|
|
14464
15716
|
break;
|
|
15717
|
+
case "search_app_listing":
|
|
15718
|
+
if (typeof body.error !== "string" && !Array.isArray(body.results)) {
|
|
15719
|
+
throw new Error("control plane response is missing results");
|
|
15720
|
+
}
|
|
15721
|
+
break;
|
|
15722
|
+
case "resolve_app_listing":
|
|
15723
|
+
if (typeof body.error !== "string" && !Object.prototype.hasOwnProperty.call(body, "listing")) {
|
|
15724
|
+
throw new Error("control plane response is missing listing");
|
|
15725
|
+
}
|
|
15726
|
+
break;
|
|
14465
15727
|
case "create_project":
|
|
14466
15728
|
case "duplicate_project":
|
|
14467
15729
|
requiredString(body.id, "id");
|
|
@@ -14618,8 +15880,25 @@ async function readAccountSnapshot(token2) {
|
|
|
14618
15880
|
if (!body) return unavailable2("billing_unavailable");
|
|
14619
15881
|
return snapshotFromEntitlementBody(body);
|
|
14620
15882
|
}
|
|
15883
|
+
async function readAccountUserId(token2) {
|
|
15884
|
+
try {
|
|
15885
|
+
const res = await fetch(`${studioOrigin()}/api/billing-entitlement`, {
|
|
15886
|
+
headers: { Authorization: `Bearer ${token2}`, [SHOTOPS_CLIENT_HEADER]: `shotops-mcp/${VERSION}` }
|
|
15887
|
+
});
|
|
15888
|
+
if (!res.ok) return null;
|
|
15889
|
+
const body = await res.json().catch(() => null);
|
|
15890
|
+
return typeof body?.userId === "string" && body.userId.length > 0 ? body.userId : null;
|
|
15891
|
+
} catch {
|
|
15892
|
+
return null;
|
|
15893
|
+
}
|
|
15894
|
+
}
|
|
14621
15895
|
function connectHostedBridge(token2) {
|
|
15896
|
+
let accountId = null;
|
|
14622
15897
|
return {
|
|
15898
|
+
async accountUserId() {
|
|
15899
|
+
accountId ??= await readAccountUserId(token2);
|
|
15900
|
+
return accountId;
|
|
15901
|
+
},
|
|
14623
15902
|
async productionOperation(action, operationId) {
|
|
14624
15903
|
const response = await callOp(token2, "production_operation", { action, operationId });
|
|
14625
15904
|
return response.operation;
|
|
@@ -14663,6 +15942,18 @@ function connectHostedBridge(token2) {
|
|
|
14663
15942
|
const res = await callOp(token2, "read_attachment_screens", { project: projectId, refs: [...refs] });
|
|
14664
15943
|
return res.screens;
|
|
14665
15944
|
},
|
|
15945
|
+
async readUploadScreens(refs) {
|
|
15946
|
+
try {
|
|
15947
|
+
const res = await callOp(token2, "read_upload_screens", { refs: [...refs] });
|
|
15948
|
+
return { screens: res.screens, missing: res.missing ?? [] };
|
|
15949
|
+
} catch (err) {
|
|
15950
|
+
if (err instanceof ControlPlaneOperationError && err.code === "foreign_refs") {
|
|
15951
|
+
const named = err.refs.length > 0 ? err.refs : [...refs];
|
|
15952
|
+
throw new Error(`ref does not belong to this account: ${named.join(", ")}`);
|
|
15953
|
+
}
|
|
15954
|
+
throw err;
|
|
15955
|
+
}
|
|
15956
|
+
},
|
|
14666
15957
|
async refineProject(input) {
|
|
14667
15958
|
return callOp(token2, "refine_project", input);
|
|
14668
15959
|
},
|
|
@@ -14698,6 +15989,8 @@ function connectHostedBridge(token2) {
|
|
|
14698
15989
|
const res = await callOp(token2, "save_look", { project: projectId, look: look2, ...sourceName ? { sourceName } : {} });
|
|
14699
15990
|
return { version: res.version };
|
|
14700
15991
|
},
|
|
15992
|
+
searchAppListing: (term, country) => callOp(token2, "search_app_listing", { term, ...country ? { country } : {} }),
|
|
15993
|
+
resolveAppListing: (reference, country) => callOp(token2, "resolve_app_listing", { reference, ...country ? { country } : {} }),
|
|
14701
15994
|
async createProject(_userId, name, record8) {
|
|
14702
15995
|
const res = await callOp(token2, "create_project", { name, record: record8 });
|
|
14703
15996
|
return { id: res.id, recordVersion: res.recordVersion };
|
|
@@ -14736,6 +16029,8 @@ var init_hostedBridge = __esm({
|
|
|
14736
16029
|
code;
|
|
14737
16030
|
capability;
|
|
14738
16031
|
requiredPlan;
|
|
16032
|
+
/** The refs a `read_upload_screens` refusal named (#1014); empty on every other refusal. */
|
|
16033
|
+
refs;
|
|
14739
16034
|
constructor(op, status, body) {
|
|
14740
16035
|
const code = typeof body?.error === "string" ? body.error : null;
|
|
14741
16036
|
super(`${op} failed (${status}${code ? ` ${code}` : ""})`);
|
|
@@ -14744,6 +16039,7 @@ var init_hostedBridge = __esm({
|
|
|
14744
16039
|
this.code = code;
|
|
14745
16040
|
this.capability = typeof body?.capability === "string" ? body.capability : null;
|
|
14746
16041
|
this.requiredPlan = typeof body?.requiredPlan === "string" ? body.requiredPlan : null;
|
|
16042
|
+
this.refs = Array.isArray(body?.refs) ? body.refs.filter((ref) => typeof ref === "string") : [];
|
|
14747
16043
|
}
|
|
14748
16044
|
};
|
|
14749
16045
|
RATE_LIMIT_RETRIES = 3;
|
|
@@ -14751,7 +16047,7 @@ var init_hostedBridge = __esm({
|
|
|
14751
16047
|
}
|
|
14752
16048
|
});
|
|
14753
16049
|
|
|
14754
|
-
// ../mockup-engine/
|
|
16050
|
+
// ../mockup-engine/layoutFindingSentence.ts
|
|
14755
16051
|
function describeUnresolvedLayout(findings) {
|
|
14756
16052
|
if (!findings || findings.length === 0) return null;
|
|
14757
16053
|
const frames = Array.from(new Set(findings.flatMap((finding) => finding.frames))).sort((left, right) => left - right).map((index) => `Frame ${index + 1}`);
|
|
@@ -14762,9 +16058,50 @@ function describeUnresolvedLayout(findings) {
|
|
|
14762
16058
|
}
|
|
14763
16059
|
return `${findings.length} layout overlaps still remain${where}.`;
|
|
14764
16060
|
}
|
|
14765
|
-
var
|
|
14766
|
-
"../mockup-engine/
|
|
16061
|
+
var init_layoutFindingSentence = __esm({
|
|
16062
|
+
"../mockup-engine/layoutFindingSentence.ts"() {
|
|
16063
|
+
"use strict";
|
|
16064
|
+
}
|
|
16065
|
+
});
|
|
16066
|
+
|
|
16067
|
+
// ../mockup-engine/ops/agentTurnOps.ts
|
|
16068
|
+
function createAgentTurnOp(sources) {
|
|
16069
|
+
const assetTable = mintAgentAssets(sources);
|
|
16070
|
+
return {
|
|
16071
|
+
assetTable,
|
|
16072
|
+
run: (input, ports) => runAgentTurn({ ...input, assets: assetTable.descriptors }, {
|
|
16073
|
+
loadProject: ports.loadProject,
|
|
16074
|
+
renderComposed: ports.renderComposed,
|
|
16075
|
+
authorize: ports.authorize,
|
|
16076
|
+
emit: ports.emit,
|
|
16077
|
+
unresolvedLayout: (rendered) => describeUnresolvedLayout(ports.layoutFindings(rendered)),
|
|
16078
|
+
layoutCollisionFrames: (rendered) => ports.layoutFindings(rendered)?.flatMap((finding) => finding.frames) ?? null,
|
|
16079
|
+
commit: async ({ after, assetBindings, expectedVersion }) => {
|
|
16080
|
+
const current = ports.readCustody();
|
|
16081
|
+
const custody = assetBindings.length > 0 ? bindAgentAssets(
|
|
16082
|
+
assetTable,
|
|
16083
|
+
assetBindings,
|
|
16084
|
+
current.handoffRefs,
|
|
16085
|
+
after.shots.map((shot) => ({ id: shot.id, frameName: shot.frameName }))
|
|
16086
|
+
) : null;
|
|
16087
|
+
return ports.commitProject({
|
|
16088
|
+
after,
|
|
16089
|
+
expectedVersion,
|
|
16090
|
+
custody: custody && {
|
|
16091
|
+
handoffRefs: custody.handoffRefs,
|
|
16092
|
+
screenPurgeRefs: foldDisplacedRefs(current.screenPurgeRefs, custody.displaced)
|
|
16093
|
+
}
|
|
16094
|
+
});
|
|
16095
|
+
}
|
|
16096
|
+
})
|
|
16097
|
+
};
|
|
16098
|
+
}
|
|
16099
|
+
var init_agentTurnOps = __esm({
|
|
16100
|
+
"../mockup-engine/ops/agentTurnOps.ts"() {
|
|
14767
16101
|
"use strict";
|
|
16102
|
+
init_layoutFindingSentence();
|
|
16103
|
+
init_agentAssetCustody();
|
|
16104
|
+
init_agentTurnCoordinator();
|
|
14768
16105
|
}
|
|
14769
16106
|
});
|
|
14770
16107
|
|
|
@@ -14825,10 +16162,10 @@ var init_aiShotProjection = __esm({
|
|
|
14825
16162
|
schema: z9.enum(["front", "left", "right"]).optional(),
|
|
14826
16163
|
prompt: "angle: front | left | right \u2014 a camera PRESET. Setting it discards any orbit value on that frame, so send orbit OR angle, never both."
|
|
14827
16164
|
},
|
|
14828
|
-
|
|
16165
|
+
deviceSize: {
|
|
14829
16166
|
exposure: "included",
|
|
14830
|
-
schema: z9.number().min(AI_SHOT_RANGES.
|
|
14831
|
-
prompt: `
|
|
16167
|
+
schema: z9.number().min(AI_SHOT_RANGES.deviceSize.min).max(AI_SHOT_RANGES.deviceSize.max).optional(),
|
|
16168
|
+
prompt: `deviceSize: ${AI_SHOT_RANGES.deviceSize.min}\u2013${AI_SHOT_RANGES.deviceSize.max} (percent of the default device size: 100 is the default, about 122 fills the panel height, more bleeds off an edge)`
|
|
14832
16169
|
},
|
|
14833
16170
|
hOffset: {
|
|
14834
16171
|
exposure: "included",
|
|
@@ -14899,6 +16236,16 @@ var init_aiShotProjection = __esm({
|
|
|
14899
16236
|
exposure: "included",
|
|
14900
16237
|
schema: z9.boolean().optional(),
|
|
14901
16238
|
prompt: "flatScreen, glare, lighting, reflections, clipToFrame: true | false."
|
|
16239
|
+
},
|
|
16240
|
+
deviceFrame: {
|
|
16241
|
+
exposure: "included",
|
|
16242
|
+
schema: z9.boolean().optional(),
|
|
16243
|
+
prompt: 'deviceFrame: true | false, default true. False renders a flat rounded screenshot card instead of the 3D device \u2014 no device chrome. "remove the phone frame"/"make it borderless" means deviceFrame: false. roll still rotates the flat card the same way it tilts a framed device.'
|
|
16244
|
+
},
|
|
16245
|
+
screenCornerRadius: {
|
|
16246
|
+
exposure: "included",
|
|
16247
|
+
schema: z9.number().min(AI_SHOT_RANGES.screenCornerRadius.min).max(AI_SHOT_RANGES.screenCornerRadius.max).optional(),
|
|
16248
|
+
prompt: `screenCornerRadius: ${AI_SHOT_RANGES.screenCornerRadius.min}\u2013${AI_SHOT_RANGES.screenCornerRadius.max} (percent of the flat card's width). Applies only when deviceFrame is false.`
|
|
14902
16249
|
}
|
|
14903
16250
|
};
|
|
14904
16251
|
API_AI_SHOT_INCLUDED_ENTRIES = Object.entries(API_AI_SHOT_PROJECTION).filter((entry) => entry[1].exposure === "included");
|
|
@@ -14979,7 +16326,11 @@ var init_refineProjectContracts = __esm({
|
|
|
14979
16326
|
});
|
|
14980
16327
|
framePatchSchema = z10.object({
|
|
14981
16328
|
captionStyle: captionStylePatchSchema.optional(),
|
|
14982
|
-
background: backgroundPatchSchema.optional()
|
|
16329
|
+
background: backgroundPatchSchema.optional(),
|
|
16330
|
+
// The per-frame half of the strip-wide `layout`, on the same two-scope footing as `captionStyle`
|
|
16331
|
+
// and `background` above (#266). Without it a composition asked for on SOME frames had to be
|
|
16332
|
+
// spent on ALL of them.
|
|
16333
|
+
layout: z10.enum(START_LAYOUT_IDS).optional()
|
|
14983
16334
|
});
|
|
14984
16335
|
currentCaptionStyleSchema = captionStylePatchSchema.extend({
|
|
14985
16336
|
color: currentColorSchema.optional()
|
|
@@ -15418,15 +16769,13 @@ function recordCustody(existing) {
|
|
|
15418
16769
|
};
|
|
15419
16770
|
}
|
|
15420
16771
|
function recordEnvelope(existing, state, custody) {
|
|
15421
|
-
|
|
15422
|
-
return {
|
|
15423
|
-
...stored,
|
|
16772
|
+
return rebuildEnvelope(existing, {
|
|
15424
16773
|
project: serializeProject(state),
|
|
15425
16774
|
...custody ? {
|
|
15426
|
-
|
|
16775
|
+
manifest: custody.handoffRefs,
|
|
15427
16776
|
...custody.screenPurgeRefs.length > 0 ? { screenPurgeRefs: custody.screenPurgeRefs } : {}
|
|
15428
16777
|
} : {}
|
|
15429
|
-
};
|
|
16778
|
+
});
|
|
15430
16779
|
}
|
|
15431
16780
|
function aiCaptionStyleFromLayer(layer) {
|
|
15432
16781
|
if (!layer) return {};
|
|
@@ -15748,13 +17097,14 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15748
17097
|
return { ...INVALID_ATTACHMENT };
|
|
15749
17098
|
}
|
|
15750
17099
|
}
|
|
15751
|
-
const
|
|
17100
|
+
const turnOp = createAgentTurnOp(attachments.map((attachment) => ({
|
|
15752
17101
|
ref: attachment.ref,
|
|
15753
17102
|
label: attachment.label,
|
|
15754
17103
|
...attachment.width !== void 0 ? { width: attachment.width } : {},
|
|
15755
17104
|
...attachment.height !== void 0 ? { height: attachment.height } : {},
|
|
15756
17105
|
singleUse: false
|
|
15757
17106
|
})));
|
|
17107
|
+
const assetTable = turnOp.assetTable;
|
|
15758
17108
|
const turnId = randomUUID3();
|
|
15759
17109
|
const turnKey = `turn_${turnId}`;
|
|
15760
17110
|
const startedAt = input.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -15783,11 +17133,10 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15783
17133
|
assets: assetTable.descriptors,
|
|
15784
17134
|
assetTable
|
|
15785
17135
|
};
|
|
15786
|
-
const result = await
|
|
17136
|
+
const result = await turnOp.run({
|
|
15787
17137
|
turnId,
|
|
15788
17138
|
instruction: input.instruction,
|
|
15789
17139
|
focus: input.focus,
|
|
15790
|
-
assets: assetTable.descriptors,
|
|
15791
17140
|
requestedCycle: input.requestedCycle,
|
|
15792
17141
|
startedAt,
|
|
15793
17142
|
cancelled: input.cancelled
|
|
@@ -15819,7 +17168,7 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15819
17168
|
const resolved = await withBoundAttachments(stored, state, variant, boundAssets, assetTable, readAttachmentBytes);
|
|
15820
17169
|
return renderedTurn(deps, state, resolved);
|
|
15821
17170
|
},
|
|
15822
|
-
|
|
17171
|
+
layoutFindings: (rendered) => rendered.layoutFindings,
|
|
15823
17172
|
authorize: async ({ cycle, state, rendered, previous }) => {
|
|
15824
17173
|
const request = requestForPass(context, { cycle, state, rendered, previous });
|
|
15825
17174
|
const authorization = await authorizePass(
|
|
@@ -15834,21 +17183,12 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15834
17183
|
const { patch: _refused, ...rest } = authorization;
|
|
15835
17184
|
return { ...rest, terminalOutcome: failureOutcome("invalid_plan", handles.failure.message) };
|
|
15836
17185
|
},
|
|
15837
|
-
|
|
15838
|
-
|
|
15839
|
-
const bound = assetBindings.length > 0 ? bindAgentAssets(
|
|
15840
|
-
assetTable,
|
|
15841
|
-
assetBindings,
|
|
15842
|
-
custody.handoffRefs,
|
|
15843
|
-
after.shots.map((shot) => ({ id: shot.id, frameName: shot.frameName }))
|
|
15844
|
-
) : null;
|
|
17186
|
+
readCustody: () => recordCustody(storedRecord),
|
|
17187
|
+
commitProject: async ({ after, expectedVersion, custody }) => {
|
|
15845
17188
|
const write = await runtime.writeProjectRecord(
|
|
15846
17189
|
project2.id,
|
|
15847
17190
|
expectedVersion,
|
|
15848
|
-
recordEnvelope(storedRecord, after,
|
|
15849
|
-
handoffRefs: bound.handoffRefs,
|
|
15850
|
-
screenPurgeRefs: foldDisplacedRefs(custody.screenPurgeRefs, bound.displaced)
|
|
15851
|
-
} : void 0),
|
|
17191
|
+
recordEnvelope(storedRecord, after, custody ?? void 0),
|
|
15852
17192
|
{ actorUserId: deps.userId }
|
|
15853
17193
|
);
|
|
15854
17194
|
if (write.ok) return { ok: true, data: { projectId: project2.id, recordVersion: write.recordVersion } };
|
|
@@ -15921,10 +17261,11 @@ var init_agentTurnAdapter = __esm({
|
|
|
15921
17261
|
"src/project/agentTurnAdapter.ts"() {
|
|
15922
17262
|
"use strict";
|
|
15923
17263
|
init_appstore();
|
|
17264
|
+
init_agentTurnOps();
|
|
17265
|
+
init_recordEnvelope();
|
|
15924
17266
|
init_agentOutcome();
|
|
15925
17267
|
init_projectRecord();
|
|
15926
17268
|
init_gradient();
|
|
15927
|
-
init_layoutFindings();
|
|
15928
17269
|
init_capabilities();
|
|
15929
17270
|
init_invalidRequest();
|
|
15930
17271
|
init_refineProjectContracts();
|
|
@@ -16084,8 +17425,8 @@ async function postClaim(body) {
|
|
|
16084
17425
|
const parsed = await res.json().catch(() => null);
|
|
16085
17426
|
return { status: res.status, body: parsed ?? {} };
|
|
16086
17427
|
}
|
|
16087
|
-
function claimFailure(
|
|
16088
|
-
const detail = status == null ?
|
|
17428
|
+
function claimFailure(step2, status, code) {
|
|
17429
|
+
const detail = status == null ? step2 : `${step2} (${status}${typeof code === "string" && code ? ` ${code}` : ""})`;
|
|
16089
17430
|
return new Error(
|
|
16090
17431
|
`Images delivered, but the editable project was not created: pending-claim ${detail}. The local render/export is still usable; retry save_project to create a fresh private claim link.`
|
|
16091
17432
|
);
|
|
@@ -16180,6 +17521,16 @@ var init_projectClaimClient = __esm({
|
|
|
16180
17521
|
|
|
16181
17522
|
// src/runtime/localDeps.ts
|
|
16182
17523
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
17524
|
+
function refsNeedAccount(refs) {
|
|
17525
|
+
throw new Error(
|
|
17526
|
+
`A { "ref" } screenshot needs a signed-in ShotOps account, and this local server has none (${refs.join(", ")}). Pass each screenshot as a local { "path": "\u2026" } PNG, or sign in (npx shotops-mcp login) and try again.`
|
|
17527
|
+
);
|
|
17528
|
+
}
|
|
17529
|
+
async function readUploadsThroughBridge(bridge, refs) {
|
|
17530
|
+
const { screens, missing } = await bridge.readUploadScreens(refs);
|
|
17531
|
+
if (missing.length > 0) throw new Error(missingUploadMessage(missing[0]));
|
|
17532
|
+
return new Map(await Promise.all(screens.map(async ({ ref, url }) => [ref, new Uint8Array(await downloadPng(url, `Screenshot ref "${ref}"`))])));
|
|
17533
|
+
}
|
|
16183
17534
|
function localToolDeps(token2, options = {}) {
|
|
16184
17535
|
const base = {
|
|
16185
17536
|
// #889 — the first render on a machine with no Playwright Chromium downloads it once, here only.
|
|
@@ -16238,6 +17589,7 @@ function localToolDeps(token2, options = {}) {
|
|
|
16238
17589
|
throw new Error(`Saving an editable project is ${SIGN_IN}`);
|
|
16239
17590
|
},
|
|
16240
17591
|
createPendingProjectClaim: (name, record8, ctx) => createAnonymousProjectClaim(name, record8, ctx?.screenshots),
|
|
17592
|
+
readUploadedRefs: async (refs) => refsNeedAccount(refs),
|
|
16241
17593
|
updateProjectRecord: async () => {
|
|
16242
17594
|
throw new Error(`Updating a saved project is ${SIGN_IN}`);
|
|
16243
17595
|
},
|
|
@@ -16259,10 +17611,22 @@ function localToolDeps(token2, options = {}) {
|
|
|
16259
17611
|
// Read FRESH on every production request and never cached: that is what makes a revoked,
|
|
16260
17612
|
// expired or downgraded token take effect on the next call rather than at the next restart.
|
|
16261
17613
|
readAccountSnapshot: () => bridge.readAccountSnapshot(),
|
|
17614
|
+
// #1014 — `userId` is 'local' on this tier, so the account a `{ ref }` must belong to comes
|
|
17615
|
+
// from the token. Without this every ref a save was handed failed the ownership test and was
|
|
17616
|
+
// dropped from the project it reported saving.
|
|
17617
|
+
accountUserId: () => bridge.accountUserId(),
|
|
17618
|
+
// #1014 — the same placeholder `userId` made every render path refuse the caller's own refs as
|
|
17619
|
+
// "not this account's". The control plane checks them against the token's account instead.
|
|
17620
|
+
readUploadedRefs: (refs) => readUploadsThroughBridge(bridge, refs),
|
|
16262
17621
|
productionOperation: {
|
|
16263
17622
|
execute: (_userId, action, operationId) => bridge.productionOperation(action, operationId)
|
|
16264
17623
|
},
|
|
16265
17624
|
saveLook: (projectId, look2, sourceName) => bridge.saveLook(projectId, look2, sourceName),
|
|
17625
|
+
// #1004 — the App Store listing doors, through the bridge and nowhere else. This tier runs in
|
|
17626
|
+
// the USER's own process, so a direct Apple client here would be a second copy of the locale
|
|
17627
|
+
// and device tables on their machine; the control plane keeps exactly one.
|
|
17628
|
+
searchAppListing: (_userId, term, country) => bridgedListingCall(() => bridge.searchAppListing(term, country)),
|
|
17629
|
+
resolveAppListing: (_userId, reference, country) => bridgedListingCall(() => bridge.resolveAppListing(reference, country)),
|
|
16266
17630
|
// `ctx` is NOT forwarded as tool args — it existed so the REMOTE save_project tool could
|
|
16267
17631
|
// rebuild the record, and the control plane is a dumb writer, so the record built HERE is the
|
|
16268
17632
|
// one that gets stored. It is still READ, for one thing only: the local file paths whose
|
|
@@ -16290,83 +17654,84 @@ function localToolDeps(token2, options = {}) {
|
|
|
16290
17654
|
bridged.refineProject = (userId, input) => runLocalRefineTurn({ ...bridged, userId }, bridge, input, options.emit ? { emit: options.emit } : void 0);
|
|
16291
17655
|
return bridged;
|
|
16292
17656
|
}
|
|
17657
|
+
async function bridgedListingCall(call) {
|
|
17658
|
+
try {
|
|
17659
|
+
return await call();
|
|
17660
|
+
} catch (err) {
|
|
17661
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17662
|
+
const throttled = err instanceof ControlPlaneOperationError && err.status === 429 || /refusing new requests/i.test(message);
|
|
17663
|
+
return { error: throttled ? "throttled" : "unavailable", message };
|
|
17664
|
+
}
|
|
17665
|
+
}
|
|
16293
17666
|
async function pushScreens(bridge, projectId, record8, recordVersion, screenshots2) {
|
|
16294
17667
|
const local = localScreenPaths(screenshots2);
|
|
16295
17668
|
if (local.length === 0) return;
|
|
16296
17669
|
try {
|
|
16297
|
-
const slots = await bridge.mintScreenUploads(
|
|
16298
|
-
projectId,
|
|
16299
|
-
local.map((s) => s.name)
|
|
16300
|
-
);
|
|
16301
|
-
const uploadable = await Promise.all(slots.map(async (slot2) => {
|
|
16302
|
-
const source = local.find((s) => s.name === slot2.name);
|
|
16303
|
-
if (!source) return null;
|
|
16304
|
-
return { slot: slot2, bytes: await readFile4(source.path) };
|
|
16305
|
-
}));
|
|
16306
|
-
const prepared = uploadable.filter((item) => item !== null);
|
|
16307
|
-
if (prepared.length === 0) return;
|
|
16308
17670
|
const envelope = record8 && typeof record8 === "object" ? record8 : {};
|
|
16309
|
-
const parsedLive = parseScreenManifest(envelope.handoffRefs);
|
|
16310
|
-
const existingLive = parsedLive ? allManifestEntries(parsedLive) : [];
|
|
16311
|
-
const existingCustody = parseScreenPurgeRefs(envelope.screenPurgeRefs) ?? [];
|
|
16312
17671
|
const cloudShots = shotRefsFromProjectFile(envelope.project);
|
|
16313
|
-
const
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
16321
|
-
screenPurgeRefs: custody
|
|
16322
|
-
}, recordVersion);
|
|
16323
|
-
const registeredVersion = registered?.recordVersion ?? recordVersion + 1;
|
|
16324
|
-
const outcomes = await Promise.allSettled(
|
|
16325
|
-
prepared.map(async ({ slot: slot2, bytes }) => {
|
|
16326
|
-
const res = await fetch(slot2.uploadUrl, {
|
|
16327
|
-
method: "PUT",
|
|
16328
|
-
headers: { "content-type": "image/png" },
|
|
16329
|
-
body: new Uint8Array(bytes)
|
|
16330
|
-
});
|
|
16331
|
-
if (!res.ok) throw new Error(`upload ${slot2.name} -> ${res.status}`);
|
|
16332
|
-
return slot2;
|
|
16333
|
-
})
|
|
16334
|
-
);
|
|
16335
|
-
const successfulRefs = {};
|
|
16336
|
-
for (const outcome of outcomes) {
|
|
16337
|
-
if (outcome.status === "fulfilled" && outcome.value.name) {
|
|
16338
|
-
successfulRefs[outcome.value.name] = outcome.value.ref;
|
|
17672
|
+
const idsByFrameName = shotIdsByFrameName(cloudShots);
|
|
17673
|
+
const cells = [];
|
|
17674
|
+
const unaddressed = [];
|
|
17675
|
+
for (const source of local) {
|
|
17676
|
+
const ids = idsByFrameName[source.name];
|
|
17677
|
+
if (ids === void 0) {
|
|
17678
|
+
unaddressed.push(source.name);
|
|
17679
|
+
continue;
|
|
16339
17680
|
}
|
|
17681
|
+
const bytes = await readFile4(source.path);
|
|
17682
|
+
const hash = await screenshotFingerprint(bytes);
|
|
17683
|
+
for (const id of ids) cells.push({ shotId: id, variant: {}, hash, bytes: new Uint8Array(bytes) });
|
|
16340
17684
|
}
|
|
16341
|
-
if (
|
|
16342
|
-
|
|
16343
|
-
throw failed2?.reason ?? new Error("no screenshot upload completed");
|
|
17685
|
+
if (unaddressed.length > 0) {
|
|
17686
|
+
logEvent("warn", "screen_upload_unaddressed", { names: unaddressed.join(", ") });
|
|
16344
17687
|
}
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
17688
|
+
if (cells.length === 0) return;
|
|
17689
|
+
let version2 = recordVersion;
|
|
17690
|
+
await saveProjectScreens(
|
|
17691
|
+
{
|
|
17692
|
+
projectId,
|
|
17693
|
+
record: envelope,
|
|
17694
|
+
shots: cloudShots,
|
|
17695
|
+
cells,
|
|
17696
|
+
// #178: refs and `reach` travel together. Studio only fetches bytes from refs when the
|
|
17697
|
+
// record says the screens reach the account, so writing refs without it leaves the screens
|
|
17698
|
+
// unreachable and the "Screens missing" banner firing anyway.
|
|
17699
|
+
reach: "account",
|
|
17700
|
+
rebuild: ({ manifest, custody }) => ({
|
|
17701
|
+
...envelope,
|
|
17702
|
+
...manifest.length > 0 ? { handoffRefs: manifest, reach: "account" } : {},
|
|
17703
|
+
screenPurgeRefs: custody
|
|
17704
|
+
})
|
|
17705
|
+
},
|
|
17706
|
+
{
|
|
17707
|
+
async saveRecord({ record: next }) {
|
|
17708
|
+
const written = await bridge.updateProjectRecord(projectId, next, version2);
|
|
17709
|
+
version2 = written?.recordVersion ?? version2 + 1;
|
|
17710
|
+
},
|
|
17711
|
+
async mintUploadSlots({ cells: wanted }) {
|
|
17712
|
+
const nameById = {};
|
|
17713
|
+
for (const shot of cloudShots) nameById[shot.id] = shot.frameName;
|
|
17714
|
+
const names = wanted.map((cell) => nameById[cell.shotId]).filter((name) => !!name);
|
|
17715
|
+
const slots = await bridge.mintScreenUploads(projectId, names);
|
|
17716
|
+
return wanted.flatMap((cell) => {
|
|
17717
|
+
const slot2 = slots.find((minted) => minted.name === nameById[cell.shotId]);
|
|
17718
|
+
return slot2 ? [{ shotId: cell.shotId, variant: cell.variant, ref: slot2.ref, uploadUrl: slot2.uploadUrl }] : [];
|
|
17719
|
+
});
|
|
17720
|
+
},
|
|
17721
|
+
async putBytes({ slot: slot2, bytes }) {
|
|
17722
|
+
const res = await fetch(slot2.uploadUrl, {
|
|
17723
|
+
method: "PUT",
|
|
17724
|
+
headers: { "content-type": "image/png" },
|
|
17725
|
+
body: new Uint8Array(bytes)
|
|
17726
|
+
});
|
|
17727
|
+
if (!res.ok) throw new Error(`upload ${slot2.ref} -> ${res.status}`);
|
|
17728
|
+
}
|
|
17729
|
+
}
|
|
16348
17730
|
);
|
|
16349
|
-
const preserved = preservedEntries.filter((entry) => preservedKeep.has(entry));
|
|
16350
|
-
await bridge.updateProjectRecord(projectId, {
|
|
16351
|
-
...envelope,
|
|
16352
|
-
handoffRefs: mergeManifest(preserved, manifestFromNamedRefs(successfulRefs, cloudShots)),
|
|
16353
|
-
screenPurgeRefs: custody,
|
|
16354
|
-
reach: "account"
|
|
16355
|
-
}, registeredVersion);
|
|
16356
|
-
const failed = outcomes.find((outcome) => outcome.status === "rejected");
|
|
16357
|
-
if (failed) throw failed.reason;
|
|
16358
17731
|
} catch (err) {
|
|
16359
17732
|
logEvent("error", "screen_upload_failed", { message: err.message });
|
|
16360
17733
|
}
|
|
16361
17734
|
}
|
|
16362
|
-
function custodyUnion(...manifests) {
|
|
16363
|
-
const seen = /* @__PURE__ */ new Set();
|
|
16364
|
-
return manifests.flatMap((manifest) => manifest.filter((entry) => {
|
|
16365
|
-
if (seen.has(entry.ref)) return false;
|
|
16366
|
-
seen.add(entry.ref);
|
|
16367
|
-
return true;
|
|
16368
|
-
}));
|
|
16369
|
-
}
|
|
16370
17735
|
var SIGN_IN, SHARE_LINK_NOT_BRIDGED;
|
|
16371
17736
|
var init_localDeps = __esm({
|
|
16372
17737
|
"src/runtime/localDeps.ts"() {
|
|
@@ -16379,8 +17744,10 @@ var init_localDeps = __esm({
|
|
|
16379
17744
|
init_projectClaimClient();
|
|
16380
17745
|
init_log();
|
|
16381
17746
|
init_capabilities();
|
|
16382
|
-
init_projectRecord();
|
|
16383
17747
|
init_screenManifest();
|
|
17748
|
+
init_fingerprint();
|
|
17749
|
+
init_persistenceOps();
|
|
17750
|
+
init_uploads();
|
|
16384
17751
|
SIGN_IN = "not available in local (free) mode \u2014 connect the hosted ShotOps MCP and sign in for saved looks, editable projects, and share links.";
|
|
16385
17752
|
SHARE_LINK_NOT_BRIDGED = `Creating a share link is ${SIGN_IN} (bridging this needs a hosted rendering step, which local mode deliberately avoids \u2014 connect directly to the hosted MCP for share links.)`;
|
|
16386
17753
|
}
|
|
@@ -16496,7 +17863,7 @@ function canonicalJson(value) {
|
|
|
16496
17863
|
return `${JSON.stringify(stableValue(value), null, 2)}
|
|
16497
17864
|
`;
|
|
16498
17865
|
}
|
|
16499
|
-
function
|
|
17866
|
+
function sha2562(value) {
|
|
16500
17867
|
return createHash5("sha256").update(value).digest("hex");
|
|
16501
17868
|
}
|
|
16502
17869
|
async function sha256File(path) {
|
|
@@ -17238,7 +18605,7 @@ function desiredReleasePayload(manifest) {
|
|
|
17238
18605
|
return payload;
|
|
17239
18606
|
}
|
|
17240
18607
|
function computeDesiredId(payload) {
|
|
17241
|
-
return
|
|
18608
|
+
return sha2562(canonicalJson(payload));
|
|
17242
18609
|
}
|
|
17243
18610
|
function parseTextValue(value, label) {
|
|
17244
18611
|
const item = record4(value, label);
|
|
@@ -18519,8 +19886,8 @@ async function assembleDesiredRelease(input) {
|
|
|
18519
19886
|
};
|
|
18520
19887
|
const recordBytes = Buffer.from(canonicalString(project2.record), "utf8");
|
|
18521
19888
|
const projectBlob = {
|
|
18522
|
-
path: seal(
|
|
18523
|
-
sha256:
|
|
19889
|
+
path: seal(sha2562(recordBytes), { bytes: recordBytes }),
|
|
19890
|
+
sha256: sha2562(recordBytes),
|
|
18524
19891
|
bytes: recordBytes.byteLength,
|
|
18525
19892
|
mediaType: "application/json"
|
|
18526
19893
|
};
|
|
@@ -18567,7 +19934,7 @@ async function assembleDesiredRelease(input) {
|
|
|
18567
19934
|
throw new Error(`renderer returned a non-PNG panel for ${output2.id}/${locale2} at position ${index + 1}`);
|
|
18568
19935
|
}
|
|
18569
19936
|
const position = index + 1;
|
|
18570
|
-
const digest4 =
|
|
19937
|
+
const digest4 = sha2562(bytes);
|
|
18571
19938
|
artifacts.push({
|
|
18572
19939
|
role: "rendered_screenshot",
|
|
18573
19940
|
custody: "sealed",
|
|
@@ -18780,7 +20147,7 @@ function screenshotSetProjection(set) {
|
|
|
18780
20147
|
return set ? { id: set.id, deviceSet: set.deviceSet, items: mediaProjection(set.items) } : null;
|
|
18781
20148
|
}
|
|
18782
20149
|
function screenshotSetToken(set) {
|
|
18783
|
-
return
|
|
20150
|
+
return sha2562(canonicalJson(screenshotSetProjection(set)));
|
|
18784
20151
|
}
|
|
18785
20152
|
function previewTypeFor(deviceSet) {
|
|
18786
20153
|
return deviceSet.replace(/^APP_/, "");
|
|
@@ -18836,7 +20203,7 @@ function projectRemoteResource(remote, scope) {
|
|
|
18836
20203
|
}
|
|
18837
20204
|
}
|
|
18838
20205
|
function resourceToken(remote, scope) {
|
|
18839
|
-
return
|
|
20206
|
+
return sha2562(canonicalJson(projectRemoteResource(remote, scope)));
|
|
18840
20207
|
}
|
|
18841
20208
|
function scopeKey(scope) {
|
|
18842
20209
|
return scope.key === null ? scope.resource : `${scope.resource}:${scope.key}`;
|
|
@@ -18919,10 +20286,10 @@ var init_releaseDrift = __esm({
|
|
|
18919
20286
|
|
|
18920
20287
|
// src/release/releaseOperations.ts
|
|
18921
20288
|
function operationValueHash(value) {
|
|
18922
|
-
return
|
|
20289
|
+
return sha2562(canonicalJson(value));
|
|
18923
20290
|
}
|
|
18924
20291
|
function computeOperationId(input) {
|
|
18925
|
-
return
|
|
20292
|
+
return sha2562(canonicalJson({
|
|
18926
20293
|
kind: input.kind,
|
|
18927
20294
|
target: input.target,
|
|
18928
20295
|
precondition: input.precondition,
|
|
@@ -19537,7 +20904,7 @@ function assertSha(value, label) {
|
|
|
19537
20904
|
if (typeof value !== "string" || !SHA2562.test(value)) throw new Error(`${label} is not a SHA-256 digest`);
|
|
19538
20905
|
}
|
|
19539
20906
|
function valueHash(value) {
|
|
19540
|
-
return
|
|
20907
|
+
return sha2562(canonicalJson(value));
|
|
19541
20908
|
}
|
|
19542
20909
|
function assertExactReleasePlan(value) {
|
|
19543
20910
|
if (!isRecord5(value) || value.kind !== "shotops.release-plan") {
|
|
@@ -19636,7 +21003,7 @@ async function readScreenshot(root, reference) {
|
|
|
19636
21003
|
if (!bytes.subarray(0, PNG_SIGNATURE3.length).equals(PNG_SIGNATURE3)) {
|
|
19637
21004
|
throw new Error(`release screenshot is not a PNG: ${reference.source}`);
|
|
19638
21005
|
}
|
|
19639
|
-
if (
|
|
21006
|
+
if (sha2562(bytes) !== reference.sha256) throw new Error(`release screenshot content changed: ${reference.source}`);
|
|
19640
21007
|
return bytes;
|
|
19641
21008
|
}
|
|
19642
21009
|
async function atomicPrivateWrite(path, bytes) {
|
|
@@ -19748,7 +21115,7 @@ async function loadArtifact(path, now) {
|
|
|
19748
21115
|
throw new Error("release review artifact screenshot manifest does not match the plan");
|
|
19749
21116
|
}
|
|
19750
21117
|
const bytes = entries[raw.path];
|
|
19751
|
-
if (!bytes || bytes.byteLength !== raw.bytes || bytes.byteLength !== expected.get(raw.sha256).bytes || !Buffer.from(bytes.subarray(0, PNG_SIGNATURE3.length)).equals(PNG_SIGNATURE3) ||
|
|
21118
|
+
if (!bytes || bytes.byteLength !== raw.bytes || bytes.byteLength !== expected.get(raw.sha256).bytes || !Buffer.from(bytes.subarray(0, PNG_SIGNATURE3.length)).equals(PNG_SIGNATURE3) || sha2562(bytes) !== raw.sha256) {
|
|
19752
21119
|
throw new Error("release review artifact screenshot content does not match the plan");
|
|
19753
21120
|
}
|
|
19754
21121
|
screenshots2.set(raw.sha256, bytes);
|
|
@@ -19854,7 +21221,7 @@ function assertRecord(record8) {
|
|
|
19854
21221
|
assertDetail(record8.detail);
|
|
19855
21222
|
}
|
|
19856
21223
|
function chain(previous, payload) {
|
|
19857
|
-
return
|
|
21224
|
+
return sha2562(`${previous}\0${canonicalJson(payload)}`);
|
|
19858
21225
|
}
|
|
19859
21226
|
function journalFileName(attemptId) {
|
|
19860
21227
|
return `attempt-${attemptId}.ndjson`;
|
|
@@ -20129,7 +21496,7 @@ async function writePartialReceipt(path, receipt) {
|
|
|
20129
21496
|
await rename4(temporary, path);
|
|
20130
21497
|
}
|
|
20131
21498
|
function receiptDigest(payload) {
|
|
20132
|
-
return
|
|
21499
|
+
return sha2562(canonicalJson(payload));
|
|
20133
21500
|
}
|
|
20134
21501
|
var RELEASE_JOURNAL_SCHEMA, RELEASE_JOURNAL_KIND, MAX_DETAIL_TEXT, SHA2563, RELEASE_JOURNAL_STAGES, RELEASE_JOURNAL_PHASES, RELEASE_JOURNAL_STATUSES, ReleaseJournalError, SAFE_ID, JOURNAL_FILE, ATTEMPT_DIRECTORY_NAME;
|
|
20135
21502
|
var init_releaseJournal = __esm({
|
|
@@ -20983,7 +22350,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20983
22350
|
let workspace = null;
|
|
20984
22351
|
try {
|
|
20985
22352
|
workspace = await verifyReleaseWorkspace(workspacePath);
|
|
20986
|
-
if (workspace.manifest.desiredId !== plan.desired.desiredId ||
|
|
22353
|
+
if (workspace.manifest.desiredId !== plan.desired.desiredId || sha2562(workspace.text) !== plan.desired.manifestSha256) {
|
|
20987
22354
|
diagnostics.push("unsupported_evidence");
|
|
20988
22355
|
}
|
|
20989
22356
|
} catch {
|
|
@@ -21283,7 +22650,7 @@ async function inspectApple(evidence, deps, diagnostics) {
|
|
|
21283
22650
|
diagnostics.push("credentials_missing");
|
|
21284
22651
|
return { editable: false, url: null, context: null };
|
|
21285
22652
|
}
|
|
21286
|
-
if (
|
|
22653
|
+
if (sha2562(setup.issuerId) !== evidence.plan.owner.issuerIdSha256 || sha2562(setup.keyId) !== evidence.plan.owner.keyIdSha256) {
|
|
21287
22654
|
diagnostics.push("credentials_missing");
|
|
21288
22655
|
return { editable: false, url: null, context: null };
|
|
21289
22656
|
}
|
|
@@ -22198,7 +23565,7 @@ async function runUpload(request, client, deps, ids, record8) {
|
|
|
22198
23565
|
} catch {
|
|
22199
23566
|
return { state: "not-applied", reason: "validation_failed" };
|
|
22200
23567
|
}
|
|
22201
|
-
if (bytes.byteLength !== request.payload.bytes ||
|
|
23568
|
+
if (bytes.byteLength !== request.payload.bytes || sha2562(bytes) !== request.payload.sha256) {
|
|
22202
23569
|
return { state: "not-applied", reason: "validation_failed" };
|
|
22203
23570
|
}
|
|
22204
23571
|
let relationshipId;
|
|
@@ -23087,7 +24454,7 @@ async function sealBackup(destination, manifest, blobs) {
|
|
|
23087
24454
|
await writeFile6(file, bytes, { mode: 384 });
|
|
23088
24455
|
const written = await stat5(file);
|
|
23089
24456
|
if (written.size !== bytes.byteLength) fail4("a recovery backup image changed size while it was being written");
|
|
23090
|
-
if (await sha256File(file) !==
|
|
24457
|
+
if (await sha256File(file) !== sha2562(bytes)) fail4("a recovery backup image changed content while it was being written");
|
|
23091
24458
|
await fsyncPath2(file, false);
|
|
23092
24459
|
}
|
|
23093
24460
|
const manifestPath = join15(temporary, BACKUP_FILENAME);
|
|
@@ -23227,7 +24594,7 @@ async function readSealedImages(workspace, manifest, selected) {
|
|
|
23227
24594
|
if (!info?.isFile() || info.isSymbolicLink()) fail4(`the sealed image for ${set.key} is missing or unsafe`);
|
|
23228
24595
|
if (info.size !== payload.bytes) fail4(`the sealed image for ${set.key} changed size since the plan was approved`);
|
|
23229
24596
|
const bytes = await readFile12(file);
|
|
23230
|
-
if (
|
|
24597
|
+
if (sha2562(bytes) !== payload.sha256) fail4(`the sealed image for ${set.key} changed since the plan was approved`);
|
|
23231
24598
|
const md5 = md5Hex(bytes);
|
|
23232
24599
|
if (md5 !== payload.md5) fail4(`the sealed image for ${set.key} does not match the checksum the plan approved`);
|
|
23233
24600
|
sealed.set(payload.sha256, { bytes, md5 });
|
|
@@ -23307,7 +24674,7 @@ async function captureBackup(client, plan, manifest, bound, destination, capture
|
|
|
23307
24674
|
fail4(`App Store Connect will not release the bytes of ${image.fileName} in ${item.selected.target.locale}/${item.selected.target.deviceSet}; nothing was deleted`);
|
|
23308
24675
|
}
|
|
23309
24676
|
const bytes = await client.download(image.asset);
|
|
23310
|
-
const digest4 =
|
|
24677
|
+
const digest4 = sha2562(bytes);
|
|
23311
24678
|
const md5 = md5Hex(bytes);
|
|
23312
24679
|
if (image.sourceFileChecksum && image.sourceFileChecksum !== md5) {
|
|
23313
24680
|
fail4(`the bytes App Store Connect returned for ${image.fileName} do not match its own checksum; nothing was deleted`);
|
|
@@ -23704,7 +25071,7 @@ function assetsFingerprint(assets) {
|
|
|
23704
25071
|
});
|
|
23705
25072
|
}
|
|
23706
25073
|
function valueHash2(value) {
|
|
23707
|
-
return
|
|
25074
|
+
return sha2562(canonicalJson(value));
|
|
23708
25075
|
}
|
|
23709
25076
|
function payloadOf(asset, role) {
|
|
23710
25077
|
return { role, source: asset.source, sha256: asset.sha256, md5: asset.md5, bytes: asset.bytes };
|
|
@@ -24317,7 +25684,7 @@ async function createReleasePlan(input, deps = {}) {
|
|
|
24317
25684
|
const approvalExpiresAt = new Date(now.getTime() + APPROVAL_TTL_MS).toISOString();
|
|
24318
25685
|
const policySha256 = valueHash2(RELEASE_POLICY);
|
|
24319
25686
|
const remoteSnapshotSha256 = valueHash2(remote);
|
|
24320
|
-
const manifestSha256 =
|
|
25687
|
+
const manifestSha256 = sha2562(loaded.text);
|
|
24321
25688
|
const commitment = mintCommitmentKey(deps.randomSource);
|
|
24322
25689
|
const payload = {
|
|
24323
25690
|
schema: PLAN_SCHEMA2,
|
|
@@ -24339,7 +25706,7 @@ async function createReleasePlan(input, deps = {}) {
|
|
|
24339
25706
|
buildSha256: loaded.manifest.build?.sha256 ?? null,
|
|
24340
25707
|
scope: loaded.manifest.scope
|
|
24341
25708
|
},
|
|
24342
|
-
owner: { issuerIdSha256:
|
|
25709
|
+
owner: { issuerIdSha256: sha2562(setup.issuerId), keyIdSha256: sha2562(setup.keyId) },
|
|
24343
25710
|
target: {
|
|
24344
25711
|
appId: remote.app.id,
|
|
24345
25712
|
appInfoId: remote.appInfo.id,
|
|
@@ -25036,7 +26403,7 @@ async function runPreflight(input, deps, recovery) {
|
|
|
25036
26403
|
if (plan.desired.desiredId !== workspace.manifest.desiredId) {
|
|
25037
26404
|
refuse2("plan_mismatch", "this plan was built from a different desired release than the workspace holds");
|
|
25038
26405
|
}
|
|
25039
|
-
if (plan.desired.manifestSha256 !==
|
|
26406
|
+
if (plan.desired.manifestSha256 !== sha2562(workspace.text)) {
|
|
25040
26407
|
refuse2("plan_mismatch", "the sealed desired release changed after this plan was built; re-plan before committing");
|
|
25041
26408
|
}
|
|
25042
26409
|
if (!EDITABLE_VERSION_STATES.has(plan.target.appStoreState)) {
|
|
@@ -25052,7 +26419,7 @@ async function runPreflight(input, deps, recovery) {
|
|
|
25052
26419
|
} catch (error) {
|
|
25053
26420
|
refuse2("setup_unavailable", error.message);
|
|
25054
26421
|
}
|
|
25055
|
-
if (
|
|
26422
|
+
if (sha2562(setup.issuerId) !== plan.owner.issuerIdSha256 || sha2562(setup.keyId) !== plan.owner.keyIdSha256) {
|
|
25056
26423
|
refuse2("owner_mismatch", "this machine's App Store Connect setup is not the one this plan was built with");
|
|
25057
26424
|
}
|
|
25058
26425
|
let appleToken;
|
|
@@ -25255,7 +26622,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
|
|
|
25255
26622
|
function readSealedPayload(workspacePath) {
|
|
25256
26623
|
return async (reference) => {
|
|
25257
26624
|
const bytes = await readFile13(join17(workspacePath, ...reference.source.split("/")));
|
|
25258
|
-
if (bytes.byteLength !== reference.bytes ||
|
|
26625
|
+
if (bytes.byteLength !== reference.bytes || sha2562(bytes) !== reference.sha256) {
|
|
25259
26626
|
throw new Error("a sealed payload changed since this plan was approved");
|
|
25260
26627
|
}
|
|
25261
26628
|
return bytes;
|
|
@@ -25861,23 +27228,20 @@ function parsePrepareArgs(argv2) {
|
|
|
25861
27228
|
};
|
|
25862
27229
|
}
|
|
25863
27230
|
function authoredLocales(projectFile) {
|
|
25864
|
-
const
|
|
25865
|
-
|
|
25866
|
-
const fallback = typeof file.baseLocale === "string" && file.baseLocale.trim() ? file.baseLocale.trim() : "en-US";
|
|
25867
|
-
const result = Array.from(new Set((locales.length ? locales : [fallback]).map((locale2) => locale2.trim())));
|
|
25868
|
-
for (const locale2 of result) {
|
|
27231
|
+
const locales = projectLocaleAxis(projectFile).locales;
|
|
27232
|
+
for (const locale2 of locales) {
|
|
25869
27233
|
if (!/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(locale2)) throw new Error(`project contains an unsafe locale identifier: ${locale2}`);
|
|
25870
27234
|
}
|
|
25871
|
-
return
|
|
27235
|
+
return locales;
|
|
25872
27236
|
}
|
|
25873
27237
|
function selectedOutputs(projectFile) {
|
|
25874
27238
|
const resolved = outputDevicesFromProjectFile(projectFile);
|
|
25875
27239
|
const file = projectFile;
|
|
25876
27240
|
const declared = Array.isArray(file.outputs) ? file.outputs.filter((id) => typeof id === "string" && id.length > 0) : [];
|
|
25877
27241
|
if (declared.length > 0) return { outputs: [...resolved.devices], unsupported: resolved.dropped };
|
|
25878
|
-
const preset =
|
|
27242
|
+
const preset = projectOutputAxis(projectFile).panelPresetId;
|
|
25879
27243
|
const fallback = OUTPUT_DEVICES.find((output2) => output2.panelPresetId === preset);
|
|
25880
|
-
if (!fallback) throw new Error(`project output is unavailable or unsupported (panel preset ${preset
|
|
27244
|
+
if (!fallback) throw new Error(`project output is unavailable or unsupported (panel preset ${preset})`);
|
|
25881
27245
|
return { outputs: [fallback], unsupported: [] };
|
|
25882
27246
|
}
|
|
25883
27247
|
function localReleaseDoor(token2) {
|
|
@@ -26028,6 +27392,7 @@ var RELEASE_PREPARE_USAGE, RELEASE_GC_USAGE, LOCAL_ACTOR;
|
|
|
26028
27392
|
var init_releasePrepare = __esm({
|
|
26029
27393
|
"src/release/releasePrepare.ts"() {
|
|
26030
27394
|
"use strict";
|
|
27395
|
+
init_appstore();
|
|
26031
27396
|
init_env();
|
|
26032
27397
|
init_productionRenderAccess();
|
|
26033
27398
|
init_projectResolution();
|
|
@@ -26278,7 +27643,7 @@ function pageCss() {
|
|
|
26278
27643
|
*{box-sizing:border-box}html{background:var(--paper);color:var(--ink);font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:16px;line-height:1.5}body{margin:0}main{max-width:1240px;margin:0 auto;padding:44px 32px 96px}
|
|
26279
27644
|
.eyebrow,.section-heading>p,.integrity-lead>p{margin:0 0 8px;text-transform:uppercase;letter-spacing:.14em;font-size:.72rem;font-weight:750;color:var(--green)}h1,h2{font-family:Georgia,"Times New Roman",serif;font-weight:500;letter-spacing:-.025em}h1{font-size:clamp(2.7rem,7vw,6.4rem);line-height:.9;margin:20px 0 24px;max-width:900px}h2{font-size:2rem;line-height:1.05;margin:0}h3{font-size:1rem;margin:0}.masthead{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(260px,.55fr);gap:56px;padding:10px 0 42px;border-bottom:1px solid var(--ink)}.masthead-copy>p:last-child{max-width:700px;color:var(--muted);font-size:1.08rem}.identity{align-self:end;border-left:4px solid var(--green);padding:4px 0 4px 20px}.identity dl{margin:0}.identity div{padding:8px 0;border-bottom:1px solid var(--line)}dt{text-transform:uppercase;letter-spacing:.08em;font-size:.67rem;font-weight:750;color:var(--muted)}dd{margin:2px 0 0;font-weight:650;overflow-wrap:anywhere}.warnings{position:sticky;top:0;z-index:5;display:grid;gap:1px;margin:0;background:var(--ink);box-shadow:0 6px 18px #161b1d22}.warning{display:flex;gap:20px;align-items:baseline;padding:14px 32px;color:white}.warning strong{min-width:150px;text-transform:uppercase;letter-spacing:.1em;font-size:.7rem}.warning--partial{background:var(--red)}.warning--build{background:var(--amber)}.warning--expired{background:var(--ink)}
|
|
26280
27645
|
.integrity{display:grid;grid-template-columns:minmax(240px,.55fr) 1.45fr;margin:48px 0;background:var(--ink);color:var(--paper);border-radius:2px;overflow:hidden}.integrity-lead{padding:32px;border-right:1px solid #ffffff2a}.integrity-lead p{color:#9dd0b5}.integrity-lead span{display:block;color:#c9c4ba;margin-top:16px}.integrity dl{display:grid;grid-template-columns:1fr 1fr;margin:0}.integrity dl div{min-width:0;padding:18px 22px;border-right:1px solid #ffffff1c;border-bottom:1px solid #ffffff1c}.integrity dt{color:#aaa59c}.integrity code{font-size:.73rem;color:#fff;overflow-wrap:anywhere}.scope-card{display:grid;grid-template-columns:1.1fr 1fr 1fr 1fr;border:1px solid var(--line);background:var(--white);margin-bottom:64px}.scope-card>div{padding:24px;border-right:1px solid var(--line)}.scope-card>div:last-child{border:0}.scope-status span{display:inline-block;padding:4px 8px;background:var(--green);color:white;text-transform:uppercase;letter-spacing:.08em;font-size:.66rem;font-weight:750}.scope-card[data-scope="partial"] .scope-status span{background:var(--red)}.scope-status strong{display:block;margin-top:18px;font-family:Georgia,"Times New Roman",serif;font-size:1.5rem;line-height:1.1}.scope-card h3{text-transform:uppercase;letter-spacing:.08em;font-size:.7rem}.scope-card ul{list-style:none;margin:12px 0 0;padding:0}.scope-card li{padding:3px 0}.muted{color:var(--muted)}
|
|
26281
|
-
.operation-section,.forbidden{display:grid;grid-template-columns:230px 1fr;gap:34px;margin:64px 0}.section-heading{position:sticky;top:82px;align-self:start}.section-heading>span{display:block;margin-top:12px;color:var(--muted);font-size:.85rem}.operation-list{display:grid;gap:14px}.operation{background:var(--white);border:1px solid var(--line)}.operation>header{display:grid;grid-template-columns:42px 1fr auto;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line);align-items:start}.sequence{font-family:Georgia,"Times New Roman",serif;font-size:1.3rem;color:var(--muted)}.action{padding:4px 8px;border:1px solid currentColor;text-transform:uppercase;letter-spacing:.08em;font-size:.65rem;font-weight:800;color:var(--green)}.action--clear,.action--replace{color:var(--red)}.action--noop{color:var(--muted)}.action--attach,.action--upload{color:var(--amber)}.target{display:flex;flex-wrap:wrap;gap:6px 16px;margin:7px 0 0}.target div{display:flex;gap:5px}.target dt{font-size:.6rem}.target dd{font-size:.72rem;margin:0;font-weight:600}.diff{display:grid;grid-template-columns:1fr 1fr}.value{min-width:0;padding:18px 20px}.value:first-child{border-right:1px solid var(--line)}.value>span{display:block;text-transform:uppercase;letter-spacing:.09em;font-size:.62rem;font-weight:750;color:var(--muted);margin-bottom:7px}.value pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font:500 .82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.value--after{background:#f8f5ef}.value--empty pre{color:var(--red);font-style:italic}.verdict{font-size:1.5rem}.gallery-groups{display:grid;gap:24px}.gallery .shot-grid{grid-template-columns:repeat(auto-fill,minmax(150px,190px));gap:12px;background:none}.gallery .shot-grid figure{border:1px solid var(--line)}.gallery-group h3{text-transform:uppercase;letter-spacing:.08em;font-size:.7rem;color:var(--muted);margin:0 0 10px}.fold{border:1px solid var(--line);background:var(--white)}.fold summary{cursor:pointer;padding:16px 20px;font-weight:650;list-style:none}.fold summary::-webkit-details-marker{display:none}.fold summary::before{content:'\u25B8 ';color:var(--muted)}.fold[open] summary::before{content:'\u25BE '}.fold-list{list-style:none;margin:0;padding:0 20px 14px;display:grid;gap:6px}.operation--folded{display:flex;gap:12px;border:0;background:none;font-size:.9rem}.operation--folded .sequence{font-size:.9rem}.operation>footer{display:flex;justify-content:space-between;gap:20px;padding:10px 20px;border-top:1px solid var(--line);color:var(--muted);font-size:.68rem}.operation>footer code{overflow:hidden;text-overflow:ellipsis}.reason{font-weight:700;color:var(--amber);white-space:nowrap}.shot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;background:var(--line)}.shot-grid figure{margin:0;background:var(--white);padding:14px}.shot-frame{aspect-ratio:
|
|
27646
|
+
.operation-section,.forbidden{display:grid;grid-template-columns:230px 1fr;gap:34px;margin:64px 0}.section-heading{position:sticky;top:82px;align-self:start}.section-heading>span{display:block;margin-top:12px;color:var(--muted);font-size:.85rem}.operation-list{display:grid;gap:14px}.operation{background:var(--white);border:1px solid var(--line)}.operation>header{display:grid;grid-template-columns:42px 1fr auto;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line);align-items:start}.sequence{font-family:Georgia,"Times New Roman",serif;font-size:1.3rem;color:var(--muted)}.action{padding:4px 8px;border:1px solid currentColor;text-transform:uppercase;letter-spacing:.08em;font-size:.65rem;font-weight:800;color:var(--green)}.action--clear,.action--replace{color:var(--red)}.action--noop{color:var(--muted)}.action--attach,.action--upload{color:var(--amber)}.target{display:flex;flex-wrap:wrap;gap:6px 16px;margin:7px 0 0}.target div{display:flex;gap:5px}.target dt{font-size:.6rem}.target dd{font-size:.72rem;margin:0;font-weight:600}.diff{display:grid;grid-template-columns:1fr 1fr}.value{min-width:0;padding:18px 20px}.value:first-child{border-right:1px solid var(--line)}.value>span{display:block;text-transform:uppercase;letter-spacing:.09em;font-size:.62rem;font-weight:750;color:var(--muted);margin-bottom:7px}.value pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font:500 .82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.value--after{background:#f8f5ef}.value--empty pre{color:var(--red);font-style:italic}.verdict{font-size:1.5rem}.gallery-groups{display:grid;gap:24px}.gallery .shot-grid{grid-template-columns:repeat(auto-fill,minmax(150px,190px));gap:12px;background:none}.gallery .shot-grid figure{border:1px solid var(--line)}.gallery-group h3{text-transform:uppercase;letter-spacing:.08em;font-size:.7rem;color:var(--muted);margin:0 0 10px}.fold{border:1px solid var(--line);background:var(--white)}.fold summary{cursor:pointer;padding:16px 20px;font-weight:650;list-style:none}.fold summary::-webkit-details-marker{display:none}.fold summary::before{content:'\u25B8 ';color:var(--muted)}.fold[open] summary::before{content:'\u25BE '}.fold-list{list-style:none;margin:0;padding:0 20px 14px;display:grid;gap:6px}.operation--folded{display:flex;gap:12px;border:0;background:none;font-size:.9rem}.operation--folded .sequence{font-size:.9rem}.operation>footer{display:flex;justify-content:space-between;gap:20px;padding:10px 20px;border-top:1px solid var(--line);color:var(--muted);font-size:.68rem}.operation>footer code{overflow:hidden;text-overflow:ellipsis}.reason{font-weight:700;color:var(--amber);white-space:nowrap}.shot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1px;background:var(--line)}.shot-grid figure{margin:0;background:var(--white);padding:14px}.shot-frame{aspect-ratio:1320/2868;background:var(--wash);display:flex;align-items:center;justify-content:center;overflow:hidden}.shot-frame img{display:block;width:100%;height:100%;object-fit:contain}.shot-grid figcaption{display:grid;gap:2px;margin-top:10px}.shot-grid figcaption span,.shot-grid figcaption small{font-size:.68rem;overflow-wrap:anywhere;color:var(--muted)}.empty-section>p{margin:0;padding:28px;background:var(--wash);color:var(--muted)}
|
|
26282
27647
|
.approval .approve-body{border:1px solid var(--line);background:var(--white);padding:26px}.approval[data-approval-state="approved"] .approve-body{border-left:4px solid var(--green)}.approval[data-approval-state="expired"] .approve-body{border-left:4px solid var(--red)}.approve-body>p{margin:0 0 16px}.approve-code{display:flex;flex-wrap:wrap;align-items:baseline;gap:12px;margin:0 0 18px;padding:14px 18px;background:var(--ink);color:var(--paper)}.approve-code span{text-transform:uppercase;letter-spacing:.1em;font-size:.66rem;font-weight:750;color:#9dd0b5}.approve-code code{font:700 1.25rem/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.08em}.approve-facts{display:grid;grid-template-columns:1fr 1fr;gap:12px 24px;margin:22px 0 0}.approve-facts code{font-size:.73rem;overflow-wrap:anywhere}.approve-note{margin:20px 0 0;color:var(--muted);font-size:.82rem}
|
|
26283
27648
|
.forbidden{padding-top:54px;border-top:1px solid var(--ink)}.forbidden ul{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line)}.forbidden li{display:flex;gap:14px;padding:22px;background:var(--white)}.forbidden li>span{color:var(--red);font-size:1.4rem;line-height:1}.forbidden p{margin:5px 0 0;color:var(--muted);font-size:.82rem}.page-footer{display:flex;justify-content:space-between;gap:30px;margin-top:72px;padding-top:20px;border-top:1px solid var(--ink);color:var(--muted);font-size:.76rem}.page-footer code{overflow-wrap:anywhere}
|
|
26284
27649
|
@media(max-width:900px){main{padding:28px 20px 70px}.masthead{grid-template-columns:1fr;gap:24px}.identity{align-self:auto}.warning{padding-inline:20px}.integrity{grid-template-columns:1fr}.integrity-lead{border-right:0;border-bottom:1px solid #ffffff2a}.scope-card{grid-template-columns:1fr 1fr}.scope-card>div:nth-child(2){border-right:0}.scope-card>div:nth-child(-n+2){border-bottom:1px solid var(--line)}.operation-section,.forbidden{grid-template-columns:1fr;gap:20px}.section-heading{position:static}.forbidden ul{grid-template-columns:1fr}}
|
|
@@ -26455,7 +27820,7 @@ function serializeReleaseReviewPayload(payload) {
|
|
|
26455
27820
|
if (json.length > RELEASE_REVIEW_PAYLOAD_LIMITS.maxPayloadBytes) {
|
|
26456
27821
|
throw new Error("this plan\u2019s review content is too large to upload for remote review");
|
|
26457
27822
|
}
|
|
26458
|
-
return { bytes: new TextEncoder().encode(json), sha256:
|
|
27823
|
+
return { bytes: new TextEncoder().encode(json), sha256: sha2562(json) };
|
|
26459
27824
|
}
|
|
26460
27825
|
var init_releaseReviewPayload2 = __esm({
|
|
26461
27826
|
"src/release/releaseReviewPayload.ts"() {
|
|
@@ -27423,6 +28788,66 @@ init_toolContract();
|
|
|
27423
28788
|
init_toolResults();
|
|
27424
28789
|
init_toolsAccount();
|
|
27425
28790
|
|
|
28791
|
+
// src/tools/toolsAppListing.ts
|
|
28792
|
+
init_toolResults();
|
|
28793
|
+
|
|
28794
|
+
// src/project/appListingLink.ts
|
|
28795
|
+
init_appListing();
|
|
28796
|
+
function appListingView(listing2) {
|
|
28797
|
+
const { applied } = listing2;
|
|
28798
|
+
return {
|
|
28799
|
+
appId: listing2.trackId,
|
|
28800
|
+
bundleId: listing2.bundleId,
|
|
28801
|
+
name: listing2.trackName,
|
|
28802
|
+
seller: listing2.sellerName,
|
|
28803
|
+
...listing2.trackViewUrl ? { storeUrl: listing2.trackViewUrl } : {},
|
|
28804
|
+
...applied.iconUrl ? { iconUrl: applied.iconUrl } : {},
|
|
28805
|
+
locales: applied.locales,
|
|
28806
|
+
baseLocale: applied.baseLocale,
|
|
28807
|
+
outputs: applied.outputs,
|
|
28808
|
+
supportsIpad: applied.supportsIpad,
|
|
28809
|
+
storefront: listing2.storefront,
|
|
28810
|
+
...applied.droppedLanguageCodes.length > 0 ? { droppedLanguageCodes: applied.droppedLanguageCodes } : {}
|
|
28811
|
+
};
|
|
28812
|
+
}
|
|
28813
|
+
function linkedAppFromRecord(record8) {
|
|
28814
|
+
const listing2 = readAppListing(record8);
|
|
28815
|
+
return listing2 ? appListingView(listing2) : null;
|
|
28816
|
+
}
|
|
28817
|
+
function appListingFailureCode(kind) {
|
|
28818
|
+
const codes = {
|
|
28819
|
+
// Both are the caller's to fix with a different reference: one could not be parsed, the other
|
|
28820
|
+
// named no app. Neither is a ShotOps asset, so `asset_not_found` would point at the wrong door.
|
|
28821
|
+
invalid_reference: "invalid_input",
|
|
28822
|
+
not_found: "invalid_input",
|
|
28823
|
+
throttled: "rate_limited",
|
|
28824
|
+
unavailable: "upstream_unavailable"
|
|
28825
|
+
};
|
|
28826
|
+
return codes[kind];
|
|
28827
|
+
}
|
|
28828
|
+
|
|
28829
|
+
// src/tools/toolsAppListing.ts
|
|
28830
|
+
var APP_LISTING_SIGN_IN = "Searching the App Store needs a ShotOps account. Sign in at https://shotops.dev, or configure SHOTOPS_TOKEN on this server, and try again. You can still name the app yourself \u2014 pass `projectName` on save_project.";
|
|
28831
|
+
function failureKind(answer) {
|
|
28832
|
+
const error = answer?.error;
|
|
28833
|
+
return typeof error === "string" ? error : null;
|
|
28834
|
+
}
|
|
28835
|
+
async function handleSearchAppListing(deps, args) {
|
|
28836
|
+
if (!deps.searchAppListing) return errorResult(APP_LISTING_SIGN_IN, "authentication_required");
|
|
28837
|
+
const answer = await deps.searchAppListing(deps.userId, args.term, args.country);
|
|
28838
|
+
const kind = failureKind(answer);
|
|
28839
|
+
if (kind) {
|
|
28840
|
+
return errorResult(answer.message, appListingFailureCode(kind));
|
|
28841
|
+
}
|
|
28842
|
+
const results = answer.results.map(appListingView);
|
|
28843
|
+
const storefront = args.country?.toLowerCase() ?? "us";
|
|
28844
|
+
return textResult({
|
|
28845
|
+
ok: true,
|
|
28846
|
+
results,
|
|
28847
|
+
message: results.length === 0 ? `No App Store app matched "${args.term}" in the ${storefront} storefront. Try the exact name as it appears on the App Store, or another storefront with \`country\` \u2014 an app released in one country is invisible to a search of any other.` : `${results.length} App Store ${results.length === 1 ? "app" : "apps"} matched "${args.term}" in the ${storefront} storefront. Confirm with the user WHICH app is theirs \u2014 the icon and seller are what prove it \u2014 then pass that result\u2019s \`appId\` (or \`storeUrl\`) as \`appListing\` on save_project, and the project it creates takes the app\u2019s name, bundle ID, languages and iPhone devices. \`locales\` and \`outputs\` on each result are what the listing would apply; \`outputs\` is empty for an iPad-only app, because ShotOps renders iPhone panels only. This says nothing about who owns the app.`
|
|
28848
|
+
});
|
|
28849
|
+
}
|
|
28850
|
+
|
|
27426
28851
|
// src/tools/toolsBundle.ts
|
|
27427
28852
|
init_fastlaneBundle();
|
|
27428
28853
|
init_appstore();
|
|
@@ -27442,6 +28867,10 @@ init_renderCredits();
|
|
|
27442
28867
|
init_browserInstall();
|
|
27443
28868
|
init_appstore();
|
|
27444
28869
|
init_productionOperationStart();
|
|
28870
|
+
function panelReadFailure(err) {
|
|
28871
|
+
if (err instanceof HostedOnlyPanelError) return errorResult(`failed to read panel: ${err.message}`, "unsupported_input");
|
|
28872
|
+
return errorResult(`failed to read panel: ${err.message}`);
|
|
28873
|
+
}
|
|
27445
28874
|
function dedupeLocales(list) {
|
|
27446
28875
|
if (!Array.isArray(list)) return [];
|
|
27447
28876
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -27456,10 +28885,7 @@ function dedupeLocales(list) {
|
|
|
27456
28885
|
return out;
|
|
27457
28886
|
}
|
|
27458
28887
|
function projectBaseLocale2(projectFile) {
|
|
27459
|
-
|
|
27460
|
-
if (typeof file?.baseLocale === "string" && file.baseLocale) return file.baseLocale;
|
|
27461
|
-
const first = Array.isArray(file?.locales) ? file.locales[0] : void 0;
|
|
27462
|
-
return typeof first === "string" && first ? first : void 0;
|
|
28888
|
+
return projectLocaleAxis(projectFile).baseLocale;
|
|
27463
28889
|
}
|
|
27464
28890
|
var PROJECT_SOURCE_CREATE_PROJECT_ERROR = "createProject is not supported when the screenshots come from a saved `project` \u2014 that project already exists. Drop createProject to bundle it, or pass `screenshots` to render (and save) a new one.";
|
|
27465
28891
|
function joinNotes(...parts) {
|
|
@@ -27485,7 +28911,7 @@ async function directDurableBundlePanels(input) {
|
|
|
27485
28911
|
input.slots,
|
|
27486
28912
|
locale2
|
|
27487
28913
|
);
|
|
27488
|
-
const screenshots2 = await resolveScreenshots(entries, input.deps.userId, false);
|
|
28914
|
+
const screenshots2 = await resolveScreenshots(entries, input.deps.userId, false, input.deps.readUploadedRefs);
|
|
27489
28915
|
const preset = output2?.panelPresetId ?? input.panelPresetId ?? "r69";
|
|
27490
28916
|
const dimensions = panelDimensionsFor(preset);
|
|
27491
28917
|
const slotSizes = screenshots2.map((slot2) => slot2.length);
|
|
@@ -27824,7 +29250,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
27824
29250
|
}
|
|
27825
29251
|
let screenshots2;
|
|
27826
29252
|
try {
|
|
27827
|
-
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
29253
|
+
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
27828
29254
|
} catch (err) {
|
|
27829
29255
|
return { ok: false, error: err.message };
|
|
27830
29256
|
}
|
|
@@ -28039,9 +29465,9 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
28039
29465
|
readinessReport = resolvedInput.report;
|
|
28040
29466
|
let bytesList;
|
|
28041
29467
|
try {
|
|
28042
|
-
bytesList = await Promise.all(panelEntries.map((p) => resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false)));
|
|
29468
|
+
bytesList = await Promise.all(panelEntries.map((p) => resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs)));
|
|
28043
29469
|
} catch (err) {
|
|
28044
|
-
return
|
|
29470
|
+
return panelReadFailure(err);
|
|
28045
29471
|
}
|
|
28046
29472
|
slots = bytesList.map((bytes, i) => ({ displayPosition: i + 1, bytes: Buffer.from(bytes) }));
|
|
28047
29473
|
previewPng = bytesList[0] ? Buffer.from(bytesList[0]) : null;
|
|
@@ -28181,7 +29607,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
28181
29607
|
if (refused) return errorResult(refused.error);
|
|
28182
29608
|
let screenshots2;
|
|
28183
29609
|
try {
|
|
28184
|
-
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
29610
|
+
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
28185
29611
|
} catch (err) {
|
|
28186
29612
|
return errorResult(err.message);
|
|
28187
29613
|
}
|
|
@@ -28521,7 +29947,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
28521
29947
|
const key = "path" in p ? `path:${p.path}` : `ref:${p.ref}`;
|
|
28522
29948
|
let pending = bytesCache.get(key);
|
|
28523
29949
|
if (!pending) {
|
|
28524
|
-
pending = resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
29950
|
+
pending = resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
28525
29951
|
bytesCache.set(key, pending);
|
|
28526
29952
|
}
|
|
28527
29953
|
return pending;
|
|
@@ -28537,7 +29963,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
28537
29963
|
});
|
|
28538
29964
|
}
|
|
28539
29965
|
} catch (err) {
|
|
28540
|
-
return
|
|
29966
|
+
return panelReadFailure(err);
|
|
28541
29967
|
}
|
|
28542
29968
|
const firstPanel = localeSlots[0]?.slots[0];
|
|
28543
29969
|
previewPng = firstPanel ? Buffer.from(firstPanel.bytes) : null;
|
|
@@ -28559,7 +29985,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
28559
29985
|
}
|
|
28560
29986
|
let screenshots2;
|
|
28561
29987
|
try {
|
|
28562
|
-
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
|
|
29988
|
+
screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
|
|
28563
29989
|
} catch (err) {
|
|
28564
29990
|
return errorResult(err.message);
|
|
28565
29991
|
}
|
|
@@ -28807,7 +30233,7 @@ function readPose(look2) {
|
|
|
28807
30233
|
const angle = look2.angle === "left" || look2.angle === "right" || look2.angle === "front" ? look2.angle : DEFAULT_LOOK.angle;
|
|
28808
30234
|
const cameraPos = isRecord2(look2.cameraPos) && typeof look2.cameraPos.x === "number" && Number.isFinite(look2.cameraPos.x) && typeof look2.cameraPos.y === "number" && Number.isFinite(look2.cameraPos.y) && typeof look2.cameraPos.z === "number" && Number.isFinite(look2.cameraPos.z) ? { x: look2.cameraPos.x, y: look2.cameraPos.y, z: look2.cameraPos.z } : null;
|
|
28809
30235
|
return {
|
|
28810
|
-
|
|
30236
|
+
deviceSize: normalizePose(look2.deviceSize, "deviceSize"),
|
|
28811
30237
|
hOffset: normalizePose(look2.hOffset, "hOffset"),
|
|
28812
30238
|
vOffset: normalizePose(look2.vOffset, "vOffset"),
|
|
28813
30239
|
angle,
|
|
@@ -29118,12 +30544,28 @@ init_toolContract();
|
|
|
29118
30544
|
init_capabilities();
|
|
29119
30545
|
init_authorization();
|
|
29120
30546
|
init_productionInput2();
|
|
30547
|
+
var SAVE_APP_LISTING_SIGN_IN = "Linking a project to its App Store listing needs a ShotOps account. Sign in at https://shotops.dev, or configure SHOTOPS_TOKEN on this server, and try again \u2014 or drop `appListing` and pass `projectName` to name the project yourself. Nothing was saved.";
|
|
29121
30548
|
async function handleSaveProject(deps, args) {
|
|
29122
30549
|
const looked = await resolveLook(deps, args);
|
|
29123
30550
|
if (isLookFailure(looked)) return productionInputErrorResult(looked.failure);
|
|
29124
30551
|
const { look: look2, style: style2, note: resolveNote } = looked;
|
|
29125
30552
|
const baseNote = [resolveNote, composeConflictNote(look2, style2)].filter(Boolean).join(" ") || void 0;
|
|
29126
|
-
const
|
|
30553
|
+
const creating = !args.project;
|
|
30554
|
+
let listing2 = null;
|
|
30555
|
+
if (args.appListing) {
|
|
30556
|
+
if (!deps.resolveAppListing) return errorResult(SAVE_APP_LISTING_SIGN_IN, "authentication_required");
|
|
30557
|
+
const answer = await deps.resolveAppListing(deps.userId, args.appListing);
|
|
30558
|
+
const kind = typeof answer.error === "string" ? answer.error : null;
|
|
30559
|
+
if (kind) return errorResult(answer.message, appListingFailureCode(kind));
|
|
30560
|
+
listing2 = answer.listing;
|
|
30561
|
+
if (!listing2) {
|
|
30562
|
+
return errorResult(
|
|
30563
|
+
`No App Store app matches "${args.appListing}". Search for it with search_app_listing and pass a result\u2019s \`appId\`, or drop \`appListing\` and name the project yourself. Nothing was saved.`,
|
|
30564
|
+
"invalid_input"
|
|
30565
|
+
);
|
|
30566
|
+
}
|
|
30567
|
+
}
|
|
30568
|
+
const locale2 = args.locale?.trim() || (creating && listing2 ? listing2.applied.baseLocale : "en-US");
|
|
29127
30569
|
if (!Array.isArray(args.screenshots) || args.screenshots.length === 0) {
|
|
29128
30570
|
return errorResult(
|
|
29129
30571
|
"save_project needs a non-empty `screenshots` array (its slot shape + names define the project structure).",
|
|
@@ -29142,7 +30584,8 @@ async function handleSaveProject(deps, args) {
|
|
|
29142
30584
|
return errorResult(err.message);
|
|
29143
30585
|
}
|
|
29144
30586
|
const screenshotNames = resolveScreenshotNames(entries).flat();
|
|
29145
|
-
const
|
|
30587
|
+
const listedOutputs = creating && listing2 && !args.panelPresetId && listing2.applied.outputs.length > 0 ? listing2.applied.outputs : void 0;
|
|
30588
|
+
const { devices: outputDevices } = resolveOutputDevices(args.outputs ?? listedOutputs);
|
|
29146
30589
|
let panelPresetId2 = args.panelPresetId;
|
|
29147
30590
|
if (outputDevices.length > 0) {
|
|
29148
30591
|
if (panelPresetId2 && !outputDevices.some((d) => d.panelPresetId === panelPresetId2)) {
|
|
@@ -29164,7 +30607,11 @@ async function handleSaveProject(deps, args) {
|
|
|
29164
30607
|
screenshotNames,
|
|
29165
30608
|
// Undefined when the caller passed no `outputs` — the record then migrates its single
|
|
29166
30609
|
// output from panelPresetId exactly as it did before this field existed.
|
|
29167
|
-
outputs: outputDevices.length > 0 ? outputDevices.map((d) => d.id) : void 0
|
|
30610
|
+
outputs: outputDevices.length > 0 ? outputDevices.map((d) => d.id) : void 0,
|
|
30611
|
+
// #1004 — the listing's language set, on a CREATE only. On an update these stay undefined and
|
|
30612
|
+
// `serializeProject` derives the list exactly as it always has, so re-linking an app leaves
|
|
30613
|
+
// `locales`, `baseLocale` and every per-locale caption on the record untouched.
|
|
30614
|
+
...creating && listing2 ? { locales: listing2.applied.locales, baseLocale: locale2 } : {}
|
|
29168
30615
|
});
|
|
29169
30616
|
} catch (err) {
|
|
29170
30617
|
return errorResult(`could not build the project record: ${err.message}`, "persistence_failed");
|
|
@@ -29172,8 +30619,14 @@ async function handleSaveProject(deps, args) {
|
|
|
29172
30619
|
if (record8 == null) {
|
|
29173
30620
|
return errorResult("no project record was produced \u2014 not saved.", "persistence_failed");
|
|
29174
30621
|
}
|
|
29175
|
-
const p = await persistProject(
|
|
29176
|
-
|
|
30622
|
+
const p = await persistProject(
|
|
30623
|
+
deps,
|
|
30624
|
+
{ ...args, projectName: args.projectName?.trim() || listing2?.applied.projectName },
|
|
30625
|
+
record8,
|
|
30626
|
+
"unsigned-new",
|
|
30627
|
+
listing2
|
|
30628
|
+
);
|
|
30629
|
+
if ("error" in p) return errorResult(p.error, p.code, p.details);
|
|
29177
30630
|
const note = ["screenNote" in p && p.screenNote ? p.screenNote : void 0, baseNote].filter(Boolean).join(" ") || void 0;
|
|
29178
30631
|
if ("status" in p) {
|
|
29179
30632
|
return textResult({
|
|
@@ -29186,6 +30639,7 @@ async function handleSaveProject(deps, args) {
|
|
|
29186
30639
|
panelPresetId: panelPresetId2 ?? "r69",
|
|
29187
30640
|
locale: locale2,
|
|
29188
30641
|
...outputDevices.length > 0 ? { outputs: outputDevices.map((d) => d.id) } : {},
|
|
30642
|
+
...listing2 ? { linkedApp: appListingView(listing2) } : {},
|
|
29189
30643
|
...note ? { note } : {},
|
|
29190
30644
|
// The one local path that moves bytes off this machine. Stated with the result, from the
|
|
29191
30645
|
// retention policy's own number, rather than left for the user to discover when the claim
|
|
@@ -29209,6 +30663,9 @@ async function handleSaveProject(deps, args) {
|
|
|
29209
30663
|
// #40: echoed ONLY when the caller asked for a device set — a legacy save's response body is
|
|
29210
30664
|
// byte-identical to before.
|
|
29211
30665
|
...outputDevices.length > 0 ? { outputs: outputDevices.map((d) => d.id) } : {},
|
|
30666
|
+
// #1004 — the app this save linked, echoed so the caller sees the identity the project now
|
|
30667
|
+
// carries. Absent on every save that named no `appListing`.
|
|
30668
|
+
...listing2 ? { linkedApp: appListingView(listing2) } : {},
|
|
29212
30669
|
...note ? { note } : {},
|
|
29213
30670
|
// #662 — this is the ONE tool in the contract whose job is a project write, and the envelope
|
|
29214
30671
|
// says so: `projectMutation: true` here, false on every render. That pair is what makes
|
|
@@ -29230,17 +30687,12 @@ function reachFromRecord(record8) {
|
|
|
29230
30687
|
function storedScreenshotReport(deps, record8, projectFile) {
|
|
29231
30688
|
const frames = frameNamesFromProjectFile(projectFile);
|
|
29232
30689
|
const file = projectFile;
|
|
29233
|
-
const
|
|
29234
|
-
const locales = Array.from(
|
|
29235
|
-
/* @__PURE__ */ new Set([
|
|
29236
|
-
baseLocale,
|
|
29237
|
-
...Array.isArray(file.locales) ? file.locales.filter((locale2) => typeof locale2 === "string") : []
|
|
29238
|
-
])
|
|
29239
|
-
);
|
|
30690
|
+
const { locales, baseLocale } = projectLocaleAxis(projectFile);
|
|
29240
30691
|
const outputIds = Array.isArray(file.outputs) ? file.outputs.filter((id) => typeof id === "string") : [];
|
|
29241
|
-
const presetFamily = typeof file.panelPresetId === "string" ? DEVICES.find((device) => device.panelPresetId === file.panelPresetId)?.screenFamily : void 0;
|
|
29242
30692
|
const families = Array.from(
|
|
29243
|
-
new Set(
|
|
30693
|
+
new Set(
|
|
30694
|
+
outputIds.length > 0 ? outputIds.map(screenFamilyForDevice) : [screenFamilyForPreset(projectOutputAxis(projectFile).panelPresetId)]
|
|
30695
|
+
)
|
|
29244
30696
|
);
|
|
29245
30697
|
const parsed = parseScreenManifest(record8?.handoffRefs, "lenient", baseLocale);
|
|
29246
30698
|
const manifest = parsed ? allManifestEntries(parsed) : [];
|
|
@@ -29316,6 +30768,7 @@ async function handleReadProject(deps, args) {
|
|
|
29316
30768
|
const projectFile = projectFileFromRecord(rec.record);
|
|
29317
30769
|
const layout = deriveLayoutReport(projectFile);
|
|
29318
30770
|
const screenshots2 = storedScreenshotReport(deps, rec.record, projectFile);
|
|
30771
|
+
const linkedApp = linkedAppFromRecord(rec.record);
|
|
29319
30772
|
const historyDecision = await deps.authorizeCapability({
|
|
29320
30773
|
subject: { kind: "account", userId: deps.userId, mode: "api_token" },
|
|
29321
30774
|
capability: CAPABILITIES.LOOK_HISTORY_READ,
|
|
@@ -29338,9 +30791,13 @@ async function handleReadProject(deps, args) {
|
|
|
29338
30791
|
...historyDenial ? {} : { heldVersion: project2.heldVersion, versions },
|
|
29339
30792
|
history: historyDenial ?? { available: true, capability: CAPABILITIES.LOOK_HISTORY_READ },
|
|
29340
30793
|
screenshots: screenshots2,
|
|
30794
|
+
// #1004 — the App Store app this project is linked to, read off the stored envelope. Omitted
|
|
30795
|
+
// entirely for a project that carries none, so a project created without one reads exactly as
|
|
30796
|
+
// it did before.
|
|
30797
|
+
...linkedApp ? { linkedApp } : {},
|
|
29341
30798
|
project_file: projectFile,
|
|
29342
30799
|
...layout ? { layout } : {},
|
|
29343
|
-
message: `Full project for "${project2.name}". project_file is the designer's current strip (frame order in \`panels\`, per-locale words in \`captionText\`, the language list in \`locales\`, the caption base the untranslated locales inherit in \`baseLocale\`, styling). To detect changes, remember updatedAt and compare on your next read_project \u2014 a newer value means the designer edited it. \`layout\` names the composition template the strip is on, derived from its values \u2014 nothing stores it, so it is never stale. ${storedScreenshotSentence(screenshots2, project2.id)
|
|
30800
|
+
message: `Full project for "${project2.name}". project_file is the designer's current strip (frame order in \`panels\`, per-locale words in \`captionText\`, the language list in \`locales\`, the caption base the untranslated locales inherit in \`baseLocale\`, styling). To detect changes, remember updatedAt and compare on your next read_project \u2014 a newer value means the designer edited it. \`layout\` names the composition template the strip is on, derived from its values \u2014 nothing stores it, so it is never stale. ` + (linkedApp ? `This project is linked to the App Store app "${linkedApp.name}" by ${linkedApp.seller} (${linkedApp.bundleId}) \u2014 \`linkedApp\` carries its identity, artwork and the languages and devices the listing mapped. Advisory: it says nothing about who owns the app. ` : "") + storedScreenshotSentence(screenshots2, project2.id)
|
|
29344
30801
|
});
|
|
29345
30802
|
}
|
|
29346
30803
|
|
|
@@ -29573,6 +31030,12 @@ var DESTRUCTIVE_WRITE = {
|
|
|
29573
31030
|
idempotentHint: false,
|
|
29574
31031
|
openWorldHint: false
|
|
29575
31032
|
};
|
|
31033
|
+
var OPEN_WORLD_READ = {
|
|
31034
|
+
readOnlyHint: true,
|
|
31035
|
+
destructiveHint: false,
|
|
31036
|
+
idempotentHint: true,
|
|
31037
|
+
openWorldHint: true
|
|
31038
|
+
};
|
|
29576
31039
|
var OPEN_WORLD_DESTRUCTIVE_WRITE = {
|
|
29577
31040
|
readOnlyHint: false,
|
|
29578
31041
|
destructiveHint: true,
|
|
@@ -29607,7 +31070,7 @@ function registerTools(server, deps) {
|
|
|
29607
31070
|
"render_strip",
|
|
29608
31071
|
{
|
|
29609
31072
|
title: "Render App Store screenshots",
|
|
29610
|
-
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the seven layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` ONCE, BEFORE ANYTHING ELSE \u2014 off the EDGE the user asked for if they named one, otherwise off the LONGEST HEADLINE in the set: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The side bleeds carry the device into the NEIGHBOURING screenshot so the set reads as one swipe \u2014 "lean-right" / "lean-left" put the headline in a narrow column beside it (~26 characters), "swipe" pushes a third of the device across and keeps the full-width headline, "corner-bleed" goes off the bottom and the right at once. A side bleed is for the WHOLE set: apply one to every panel or none, and never mirror it on neighbouring panels \u2014 two devices leaning toward each other overlap. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw
|
|
31073
|
+
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the seven layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` ONCE, BEFORE ANYTHING ELSE \u2014 off the EDGE the user asked for if they named one, otherwise off the LONGEST HEADLINE in the set: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The side bleeds carry the device into the NEIGHBOURING screenshot so the set reads as one swipe \u2014 "lean-right" / "lean-left" put the headline in a narrow column beside it (~26 characters), "swipe" pushes a third of the device across and keeps the full-width headline, "corner-bleed" goes off the bottom and the right at once. A side bleed is for the WHOLE set: apply one to every panel or none, and never mirror it on neighbouring panels \u2014 two devices leaning toward each other overlap. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw deviceSize/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so \u2014 `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.',
|
|
29611
31074
|
inputSchema: renderStripAdvertisedShape,
|
|
29612
31075
|
outputSchema: renderStripToolOutputSchema,
|
|
29613
31076
|
// ADDITIVE, not destructive: this can mint private generated assets but cannot change a
|
|
@@ -29681,6 +31144,18 @@ function registerTools(server, deps) {
|
|
|
29681
31144
|
},
|
|
29682
31145
|
seam("read_project", readProjectShape, (args) => handleReadProject(deps, args))
|
|
29683
31146
|
);
|
|
31147
|
+
server.registerTool(
|
|
31148
|
+
"search_app_listing",
|
|
31149
|
+
{
|
|
31150
|
+
title: "Find an app on the App Store",
|
|
31151
|
+
description: "Find the user\u2019s app on the App Store so the project you save carries its real identity instead of a name you typed: search by app or developer name and every result comes back with the app\u2019s exact name, its seller, its icon, its bundle ID and its numeric app ID, plus the App Store Connect languages and the iPhone device set ShotOps would apply. Let the USER confirm which one is theirs \u2014 the icon and the seller name are what prove it \u2014 then pass that result\u2019s `appId` (or `storeUrl`) as `appListing` on save_project. Pass `country` to search another storefront; an app released in one country is invisible to a search of any other. Free and unmetered: it renders nothing, saves nothing and changes no project. Finding a public listing proves nothing about who owns the app, and no Apple or store credential is involved. Needs an account.",
|
|
31152
|
+
inputSchema: searchAppListingShape,
|
|
31153
|
+
outputSchema: searchAppListingToolOutputSchema,
|
|
31154
|
+
annotations: OPEN_WORLD_READ,
|
|
31155
|
+
_meta: toolMeta(deps, "search_app_listing", "Searching the App Store\u2026", "App Store searched")
|
|
31156
|
+
},
|
|
31157
|
+
seam("search_app_listing", searchAppListingShape, (args) => handleSearchAppListing(deps, args))
|
|
31158
|
+
);
|
|
29684
31159
|
server.registerTool(
|
|
29685
31160
|
"render_project",
|
|
29686
31161
|
{
|