shotops-mcp 0.9.6 → 0.9.7
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 +27 -10
- package/dist/local.js +696 -215
- package/harness-dist/assets/index--FFK3psO.js +1 -0
- package/harness-dist/index.html +1 -1
- package/package.json +5 -2
- package/widget-dist/bundle.js +4 -4
- package/widget-dist/panels.js +4 -4
- package/harness-dist/assets/index-MiEp7Ev8.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) !== "1b56a11440edcc23083bfa4a283fe2d972acfb3a17820247bc951bcd5cc3810d") {
|
|
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. */ }
|
|
@@ -2208,7 +2208,7 @@ var init_screenVariants = __esm({
|
|
|
2208
2208
|
});
|
|
2209
2209
|
|
|
2210
2210
|
// ../mockup-engine/appstore/catalog/layouts.ts
|
|
2211
|
-
var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
|
|
2211
|
+
var LAYOUT_TEMPLATE_CATALOG, LAYOUT_TEMPLATES, TEMPLATE_IDS, LAYOUT_TEMPLATE_IDS, LAYOUT_CAPTION_ABSENT_DEFAULTS, LAYOUT_POSE_FIELDS, LAYOUT_CAMERA_FIELDS, LAYOUT_CAPTION_FIELDS;
|
|
2212
2212
|
var init_layouts = __esm({
|
|
2213
2213
|
"../mockup-engine/appstore/catalog/layouts.ts"() {
|
|
2214
2214
|
"use strict";
|
|
@@ -2218,28 +2218,78 @@ var init_layouts = __esm({
|
|
|
2218
2218
|
label: "Standard",
|
|
2219
2219
|
blurb: "Headline on top, whole device below it, nothing cropped. The safe default.",
|
|
2220
2220
|
character: "safe",
|
|
2221
|
+
bleedEdge: "none",
|
|
2221
2222
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2222
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2 }
|
|
2223
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2223
2224
|
},
|
|
2224
2225
|
{
|
|
2225
2226
|
id: "bleed",
|
|
2226
2227
|
label: "Bleed",
|
|
2227
2228
|
blurb: "Room for a longer headline, device running off the bottom edge. What the market looks like.",
|
|
2228
2229
|
character: "conventional",
|
|
2230
|
+
bleedEdge: "bottom",
|
|
2229
2231
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2230
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4 }
|
|
2232
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, align: "center", bandCenter: 0.5 }
|
|
2231
2233
|
},
|
|
2232
2234
|
{
|
|
2233
2235
|
id: "top-bleed",
|
|
2234
2236
|
label: "Top bleed",
|
|
2235
2237
|
blurb: "Device running off the TOP edge, headline underneath it. Inverts the usual reading order.",
|
|
2236
2238
|
character: "expressive",
|
|
2239
|
+
bleedEdge: "top",
|
|
2237
2240
|
shotLook: { phoneHeight: "82", hOffset: "0", vOffset: "-38", angle: "front", cameraPos: null, roll: "0" },
|
|
2238
2241
|
// The only entry that moves the band. Its caption sits BELOW the device, which the engine
|
|
2239
2242
|
// could not express at all until #218 gave placeCaptions a bottom-anchored band — before that
|
|
2240
2243
|
// the only way down was pinning an anchor, which silently costs reflow, stacking and the
|
|
2241
2244
|
// legibility advisory. `band: 'bottom'` keeps the anchor ABSENT, so it stays genuinely auto.
|
|
2242
|
-
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom" }
|
|
2245
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 4, band: "bottom", align: "center", bandCenter: 0.5 }
|
|
2246
|
+
},
|
|
2247
|
+
// ── The side bleeds. Facts 5–7 in the essay above own every number here. ────────────────────
|
|
2248
|
+
//
|
|
2249
|
+
// What they buy that the vertical three cannot: the headline gets a COLUMN instead of a band the
|
|
2250
|
+
// device sits under. Narrowing `maxWidth` to 0.61 and moving `bandCenter` off centre is what
|
|
2251
|
+
// makes that column, and it is why the catalog owns `align`/`bandCenter` at all — see the note
|
|
2252
|
+
// on LayoutCaptionField below for what that costs every other template.
|
|
2253
|
+
{
|
|
2254
|
+
id: "lean-right",
|
|
2255
|
+
label: "Lean right",
|
|
2256
|
+
blurb: "Device pushed off the RIGHT edge, headline in a column down the left. Reads as one continuous swipe.",
|
|
2257
|
+
character: "expressive",
|
|
2258
|
+
bleedEdge: "right",
|
|
2259
|
+
shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2260
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "left", bandCenter: 0.375 }
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
id: "lean-left",
|
|
2264
|
+
label: "Lean left",
|
|
2265
|
+
blurb: "Device pushed off the LEFT edge, headline in a column down the right. The mirror of Lean right.",
|
|
2266
|
+
character: "expressive",
|
|
2267
|
+
bleedEdge: "left",
|
|
2268
|
+
shotLook: { phoneHeight: "82", hOffset: "-46", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2269
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 2, align: "right", bandCenter: 0.625 }
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
id: "swipe",
|
|
2273
|
+
label: "Swipe",
|
|
2274
|
+
blurb: "A third of the device carried into the next panel, headline full width above it. Only honest applied to every frame.",
|
|
2275
|
+
character: "expressive",
|
|
2276
|
+
bleedEdge: "right",
|
|
2277
|
+
// Standard's vertical, and that is not a free choice: a 4-line reservation over vOffset 18 puts
|
|
2278
|
+
// the device's top edge through the fourth line, and Bleed's vOffset 38 would crop the bottom
|
|
2279
|
+
// too — which makes this template claim `bottom-right` and collide with Corner bleed, the only
|
|
2280
|
+
// other entry on that edge. The horizontal push is the whole idea here; the vertical stays out
|
|
2281
|
+
// of its way. Both halves are guarded (clearance, and the declared-edge check).
|
|
2282
|
+
shotLook: { phoneHeight: "82", hOffset: "78", vOffset: "18", angle: "front", cameraPos: null, roll: "0" },
|
|
2283
|
+
caption: { sizePt: 32, maxWidth: 0.86, reserveLines: 2, align: "center", bandCenter: 0.5 }
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
id: "corner-bleed",
|
|
2287
|
+
label: "Corner bleed",
|
|
2288
|
+
blurb: "Off the bottom AND the right at once, headline in a column down the left. The most opinionated panel here.",
|
|
2289
|
+
character: "expressive",
|
|
2290
|
+
bleedEdge: "bottom-right",
|
|
2291
|
+
shotLook: { phoneHeight: "82", hOffset: "46", vOffset: "38", angle: "front", cameraPos: null, roll: "0" },
|
|
2292
|
+
caption: { sizePt: 32, maxWidth: 0.61, reserveLines: 4, align: "left", bandCenter: 0.375 }
|
|
2243
2293
|
}
|
|
2244
2294
|
];
|
|
2245
2295
|
LAYOUT_TEMPLATES = LAYOUT_TEMPLATE_CATALOG;
|
|
@@ -2248,6 +2298,10 @@ var init_layouts = __esm({
|
|
|
2248
2298
|
TEMPLATE_IDS[0],
|
|
2249
2299
|
...TEMPLATE_IDS.slice(1)
|
|
2250
2300
|
];
|
|
2301
|
+
LAYOUT_CAPTION_ABSENT_DEFAULTS = {
|
|
2302
|
+
align: "center",
|
|
2303
|
+
bandCenter: 0.5
|
|
2304
|
+
};
|
|
2251
2305
|
LAYOUT_POSE_FIELDS = ["phoneHeight", "hOffset", "vOffset"];
|
|
2252
2306
|
LAYOUT_CAMERA_FIELDS = ["angle", "cameraPos", "roll"];
|
|
2253
2307
|
LAYOUT_CAPTION_FIELDS = Array.from(
|
|
@@ -2259,8 +2313,8 @@ var init_layouts = __esm({
|
|
|
2259
2313
|
// ../mockup-engine/appstore/layoutTemplates.ts
|
|
2260
2314
|
function layoutTemplateAgentSummary(template) {
|
|
2261
2315
|
const reserveLines = template.caption.reserveLines;
|
|
2262
|
-
const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${reserveLines * 18} characters).` : "";
|
|
2263
|
-
return `${template.blurb}${capacity} Character: [${template.character}].`;
|
|
2316
|
+
const capacity = typeof reserveLines === "number" ? ` Reserves ${reserveLines} headline lines (~${Math.round(reserveLines * 18 * (template.caption.maxWidth ?? 0.86) / 0.86)} characters).` : "";
|
|
2317
|
+
return `${template.blurb}${capacity} Character: [${template.character}]. Bleeds: [${template.bleedEdge}].`;
|
|
2264
2318
|
}
|
|
2265
2319
|
function layoutTemplateById(id) {
|
|
2266
2320
|
return LAYOUT_TEMPLATES.find((t) => t.id === id);
|
|
@@ -2280,8 +2334,9 @@ function differencesFrom(template, shotLook2, captionStyle) {
|
|
|
2280
2334
|
}
|
|
2281
2335
|
if (captionStyle) {
|
|
2282
2336
|
for (const field of LAYOUT_CAPTION_FIELDS) {
|
|
2283
|
-
const
|
|
2284
|
-
const
|
|
2337
|
+
const fallback = LAYOUT_CAPTION_ABSENT_DEFAULTS[field];
|
|
2338
|
+
const actual = captionStyle[field] ?? fallback;
|
|
2339
|
+
const wanted = template.caption[field] ?? fallback;
|
|
2285
2340
|
if (actual !== wanted) differs.push({ field, actual, template: wanted });
|
|
2286
2341
|
}
|
|
2287
2342
|
}
|
|
@@ -2326,7 +2381,7 @@ function layoutTemplateToLook(template) {
|
|
|
2326
2381
|
}
|
|
2327
2382
|
return { shotLook: { ...template.shotLook }, caption };
|
|
2328
2383
|
}
|
|
2329
|
-
var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
|
|
2384
|
+
var LAYOUT_CHARACTER_ANSWERS, LAYOUT_CHARACTER_IDS, LAYOUT_BLEED_EDGE_WORDS, LAYOUT_BLEED_EDGE_IDS, LAYOUT_SELECTION_CAVEATS, valuesEqual;
|
|
2330
2385
|
var init_layoutTemplates = __esm({
|
|
2331
2386
|
"../mockup-engine/appstore/layoutTemplates.ts"() {
|
|
2332
2387
|
"use strict";
|
|
@@ -2338,9 +2393,20 @@ var init_layoutTemplates = __esm({
|
|
|
2338
2393
|
expressive: "the request explicitly asks to stand out \u2014 unique, creative, bold, striking, different, surprising"
|
|
2339
2394
|
};
|
|
2340
2395
|
LAYOUT_CHARACTER_IDS = Object.keys(LAYOUT_CHARACTER_ANSWERS);
|
|
2396
|
+
LAYOUT_BLEED_EDGE_WORDS = {
|
|
2397
|
+
none: "nothing about cropping or edges was asked for",
|
|
2398
|
+
top: "the phone should run off the TOP, or the headline should sit underneath it",
|
|
2399
|
+
bottom: "the phone should run off the BOTTOM \u2014 the ordinary store crop",
|
|
2400
|
+
left: "the phone should sit to the LEFT, or run off the left edge",
|
|
2401
|
+
right: "the phone should sit to the RIGHT, run off the right edge, or carry into the next screenshot",
|
|
2402
|
+
"bottom-right": "the phone should run off TWO edges at once, or the request asks for the boldest possible crop"
|
|
2403
|
+
};
|
|
2404
|
+
LAYOUT_BLEED_EDGE_IDS = Object.keys(LAYOUT_BLEED_EDGE_WORDS);
|
|
2341
2405
|
LAYOUT_SELECTION_CAVEATS = [
|
|
2342
2406
|
'A request that names a composition outright ("device running off the bottom edge", "headline under the phone") beats the character.',
|
|
2343
|
-
"
|
|
2407
|
+
"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
|
+
"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 is chosen for the WHOLE set or not at all. Every panel leaning the same way reads as one swipe; mirrored neighbours (lean-right beside lean-left) overlap and read as a mistake."
|
|
2344
2410
|
];
|
|
2345
2411
|
valuesEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2346
2412
|
}
|
|
@@ -6162,6 +6228,17 @@ function matchAuthoredLocale(locales, requested) {
|
|
|
6162
6228
|
});
|
|
6163
6229
|
return matches.length === 1 ? matches[0] : null;
|
|
6164
6230
|
}
|
|
6231
|
+
function localesWithoutCaptions(captionText, locales, baseLocale = DEFAULT_LOCALE) {
|
|
6232
|
+
return locales.filter((locale2) => {
|
|
6233
|
+
if (locale2 === baseLocale) return false;
|
|
6234
|
+
const inner = captionText[locale2];
|
|
6235
|
+
if (!inner) return true;
|
|
6236
|
+
for (const layers of Object.values(inner)) {
|
|
6237
|
+
if (layers && layers.some((l) => l.text.trim().length > 0)) return false;
|
|
6238
|
+
}
|
|
6239
|
+
return true;
|
|
6240
|
+
});
|
|
6241
|
+
}
|
|
6165
6242
|
function normalizeLegacyLocales(captionText, locales) {
|
|
6166
6243
|
const hasLegacy = Object.keys(captionText).some((c) => c in LEGACY_LOCALE_ALIASES) || (locales?.some((c) => c in LEGACY_LOCALE_ALIASES) ?? false);
|
|
6167
6244
|
if (!hasLegacy) return { captionText, locales };
|
|
@@ -7139,7 +7216,7 @@ var init_agentCapabilities = __esm({
|
|
|
7139
7216
|
targets: ["frame"],
|
|
7140
7217
|
requiredInputs: ["frameId"],
|
|
7141
7218
|
limits: ["The last remaining frame cannot be removed."],
|
|
7142
|
-
evaluationCases: ["structure-remove-empty-frame"]
|
|
7219
|
+
evaluationCases: ["structure-remove-empty-frame", "structure-remove-populated-frame"]
|
|
7143
7220
|
},
|
|
7144
7221
|
{
|
|
7145
7222
|
id: "frame.reorder",
|
|
@@ -7286,7 +7363,7 @@ var init_agentCapabilities = __esm({
|
|
|
7286
7363
|
domain: "caption",
|
|
7287
7364
|
status: "supported",
|
|
7288
7365
|
summary: "Restyle captions: font, size, colour, alignment, width and band placement.",
|
|
7289
|
-
studioCommands: [],
|
|
7366
|
+
studioCommands: ["patchAllCaptionStyles"],
|
|
7290
7367
|
wire: "patch.captionStyle (strip-wide) or patch.frames[i].captionStyle (one frame)",
|
|
7291
7368
|
targets: ["project", "frame"],
|
|
7292
7369
|
requiredInputs: ["the style fields to change"],
|
|
@@ -7330,7 +7407,7 @@ var init_agentCapabilities = __esm({
|
|
|
7330
7407
|
domain: "caption",
|
|
7331
7408
|
status: "supported",
|
|
7332
7409
|
summary: "Move a caption layer to another frame.",
|
|
7333
|
-
studioCommands: ["moveCaptionLayerToFrame"],
|
|
7410
|
+
studioCommands: ["moveCaptionLayer", "moveCaptionLayerToFrame"],
|
|
7334
7411
|
wire: "structuralOperations: caption.move across frames",
|
|
7335
7412
|
targets: ["caption", "frame"],
|
|
7336
7413
|
requiredInputs: ["frameId", "the caption position", "toFrameId"],
|
|
@@ -7345,19 +7422,19 @@ var init_agentCapabilities = __esm({
|
|
|
7345
7422
|
wire: null,
|
|
7346
7423
|
targets: ["project"],
|
|
7347
7424
|
requiredInputs: [],
|
|
7348
|
-
reason: "
|
|
7425
|
+
reason: "Agent refinement cannot translate captions. Open Translate in Studio to create German or other language versions while retaining the base words.",
|
|
7349
7426
|
evaluationCases: ["unsupported-translate-captions"]
|
|
7350
7427
|
},
|
|
7351
7428
|
{
|
|
7352
7429
|
id: "caption.output-override",
|
|
7353
7430
|
domain: "caption",
|
|
7354
7431
|
status: "intentionally_excluded",
|
|
7355
|
-
summary: "Give
|
|
7432
|
+
summary: "Give an output family its own caption wording.",
|
|
7356
7433
|
studioCommands: ["patchOutputCaption", "clearOutputCaption"],
|
|
7357
7434
|
wire: null,
|
|
7358
7435
|
targets: ["caption"],
|
|
7359
7436
|
requiredInputs: [],
|
|
7360
|
-
reason: "
|
|
7437
|
+
reason: "Agent refinement edits shared captions. Studio has manual output-family caption wording; both iPhone sizes share one family and cannot have separate words.",
|
|
7361
7438
|
evaluationCases: ["unsupported-per-output-caption"]
|
|
7362
7439
|
},
|
|
7363
7440
|
// ── Look and backdrop ─────────────────────────────────────────────────────────────────────────
|
|
@@ -7385,18 +7462,18 @@ var init_agentCapabilities = __esm({
|
|
|
7385
7462
|
wire: "patch.frames[i].background",
|
|
7386
7463
|
targets: ["frame"],
|
|
7387
7464
|
requiredInputs: ["the frame", "the backdrop fields"],
|
|
7388
|
-
evaluationCases: ["background-frame-one-amber", "inspection-correct-frame-two-background"]
|
|
7465
|
+
evaluationCases: ["background-frame-one-amber", "background-frame-three-flat-teal", "inspection-correct-frame-two-background"]
|
|
7389
7466
|
},
|
|
7390
7467
|
{
|
|
7391
7468
|
id: "frame.background-reset",
|
|
7392
7469
|
domain: "look",
|
|
7393
7470
|
status: "intentionally_excluded",
|
|
7394
|
-
summary: "Drop
|
|
7395
|
-
studioCommands: ["clearPanelBackground"],
|
|
7471
|
+
summary: "Drop one or all frame backdrops so they inherit the strip again.",
|
|
7472
|
+
studioCommands: ["clearPanelBackground", "clearAllPanelBackgrounds"],
|
|
7396
7473
|
wire: null,
|
|
7397
7474
|
targets: ["frame"],
|
|
7398
7475
|
requiredInputs: [],
|
|
7399
|
-
reason: "
|
|
7476
|
+
reason: "Agent refinement cannot reset backdrop inheritance. Use the frame background reset control in Studio to return it to the strip default.",
|
|
7400
7477
|
evaluationCases: ["unsupported-frame-background-reset"]
|
|
7401
7478
|
},
|
|
7402
7479
|
{
|
|
@@ -7415,13 +7492,13 @@ var init_agentCapabilities = __esm({
|
|
|
7415
7492
|
id: "frame.color",
|
|
7416
7493
|
domain: "look",
|
|
7417
7494
|
status: "intentionally_excluded",
|
|
7418
|
-
summary: "Set a frame\u2019s
|
|
7495
|
+
summary: "Set the legacy per-panel colour that sits under a frame\u2019s backdrop.",
|
|
7419
7496
|
studioCommands: ["setPanelColor"],
|
|
7420
7497
|
wire: null,
|
|
7421
7498
|
targets: ["frame"],
|
|
7422
7499
|
requiredInputs: [],
|
|
7423
|
-
reason: "No Studio surface dispatches it
|
|
7424
|
-
evaluationCases: ["
|
|
7500
|
+
reason: "No Studio surface dispatches it. A flat colour is a one-stop frame backdrop (`frame.background`), so a request for one is carried out there, never refused.",
|
|
7501
|
+
evaluationCases: ["background-frame-three-flat-teal"]
|
|
7425
7502
|
},
|
|
7426
7503
|
{
|
|
7427
7504
|
id: "look.design-preset",
|
|
@@ -7455,7 +7532,7 @@ var init_agentCapabilities = __esm({
|
|
|
7455
7532
|
wire: null,
|
|
7456
7533
|
targets: ["project"],
|
|
7457
7534
|
requiredInputs: [],
|
|
7458
|
-
reason: "
|
|
7535
|
+
reason: "Agent refinement cannot restore a saved Look. Restore the saved version through Look version history in Studio.",
|
|
7459
7536
|
evaluationCases: ["unsupported-restore-saved-look"]
|
|
7460
7537
|
},
|
|
7461
7538
|
// ── Output and locale axes ────────────────────────────────────────────────────────────────────
|
|
@@ -7484,29 +7561,42 @@ var init_agentCapabilities = __esm({
|
|
|
7484
7561
|
evaluationCases: ["project-add-german-locale", "geometry-clear-while-adding-a-language"]
|
|
7485
7562
|
},
|
|
7486
7563
|
// ── Assets ────────────────────────────────────────────────────────────────────────────────────
|
|
7564
|
+
// The whole-strip versions of the two asset capabilities (#880). No wire of their own: a whole-strip
|
|
7565
|
+
// request is carried out as the per-device operations above, one per supplied screenshot, so the
|
|
7566
|
+
// styling, captions and Output membership that hang off each device survive exactly as they do
|
|
7567
|
+
// for a single swap. Asset-gated the same way — with no screenshots on the turn they ask for them.
|
|
7487
7568
|
{
|
|
7488
7569
|
id: "strip.add-screens",
|
|
7489
7570
|
domain: "asset",
|
|
7490
|
-
status: "
|
|
7571
|
+
status: "supported",
|
|
7491
7572
|
summary: "Add new screenshots to the project, each in its own frame.",
|
|
7492
7573
|
studioCommands: ["addShots"],
|
|
7493
|
-
wire:
|
|
7574
|
+
wire: "structuralOperations: frame.add with a frameRef, then device.add into it, once per screenshot from AVAILABLE SCREENSHOTS",
|
|
7494
7575
|
targets: ["project"],
|
|
7495
7576
|
requiredInputs: ["one or more authorized screenshot references"],
|
|
7496
|
-
|
|
7497
|
-
|
|
7577
|
+
withoutAssetReason: "New frames need new pixels. Send the screenshots with the instruction and the Agent will place them.",
|
|
7578
|
+
limits: [
|
|
7579
|
+
"Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
|
|
7580
|
+
"Each new frame starts without a caption and its device on the default look, exactly as adding screenshots in Studio does."
|
|
7581
|
+
],
|
|
7582
|
+
evaluationCases: ["asset-add-screens-supplied", "asset-add-screens-needs-pixels"]
|
|
7498
7583
|
},
|
|
7499
7584
|
{
|
|
7500
7585
|
id: "strip.replace-screens",
|
|
7501
7586
|
domain: "asset",
|
|
7502
|
-
status: "
|
|
7587
|
+
status: "supported",
|
|
7503
7588
|
summary: "Replace every screenshot in the project, keeping the styling.",
|
|
7504
7589
|
studioCommands: ["loadScreens"],
|
|
7505
|
-
wire:
|
|
7590
|
+
wire: "structuralOperations: device.replace on every device in strip order, each naming its own assetId from AVAILABLE SCREENSHOTS",
|
|
7506
7591
|
targets: ["project"],
|
|
7507
7592
|
requiredInputs: ["a complete authorized screenshot set"],
|
|
7508
|
-
|
|
7509
|
-
|
|
7593
|
+
withoutAssetReason: "Replacing the whole strip needs the whole new set of pixels. This instruction carried none.",
|
|
7594
|
+
limits: [
|
|
7595
|
+
"Only screenshots listed in AVAILABLE SCREENSHOTS; the Agent never invents or generates pixels.",
|
|
7596
|
+
"Every device keeps its look, placement, captions, translated screens and Output membership \u2014 only the pixels change.",
|
|
7597
|
+
"When the supplied screenshots do not match the devices one to one, the Agent asks which goes where instead of swapping some of them."
|
|
7598
|
+
],
|
|
7599
|
+
evaluationCases: ["asset-replace-all-screens-supplied", "asset-replace-all-screens-needs-pixels", "asset-replace-all-screens-count-mismatch"]
|
|
7510
7600
|
},
|
|
7511
7601
|
{
|
|
7512
7602
|
id: "locale.load-screens",
|
|
@@ -7709,7 +7799,13 @@ function applicationOutcome(facts) {
|
|
|
7709
7799
|
}
|
|
7710
7800
|
const gap = (facts.gaps ?? []).find((entry) => entry.kind === "asset") ?? (facts.gaps ?? []).find((entry) => entry.kind === "excluded") ?? (facts.gaps ?? []).find((entry) => entry.kind === "clarification");
|
|
7711
7801
|
if (!gap) {
|
|
7712
|
-
|
|
7802
|
+
const layout = facts.unresolvedLayout?.trim();
|
|
7803
|
+
return layout ? {
|
|
7804
|
+
status: "applied_with_remaining",
|
|
7805
|
+
headline: headlineForChanges(changed),
|
|
7806
|
+
changed,
|
|
7807
|
+
remaining: boundOutcomeText(layout, AGENT_OUTCOME_BOUNDS.remaining)
|
|
7808
|
+
} : { status: "applied", headline: headlineForChanges(changed), changed };
|
|
7713
7809
|
}
|
|
7714
7810
|
return {
|
|
7715
7811
|
status: "applied_with_remaining",
|
|
@@ -8629,11 +8725,30 @@ function applyStructuralOperation(state, operation, assets = [], bindings) {
|
|
|
8629
8725
|
}
|
|
8630
8726
|
}
|
|
8631
8727
|
}
|
|
8728
|
+
function applyMergedCaptionMove(state, merge, index, toIndex) {
|
|
8729
|
+
const slot2 = merge.pending[index];
|
|
8730
|
+
if (slot2 === void 0 || panelIndex(state, merge.into) < 0) return null;
|
|
8731
|
+
const ids = captionSlotIds(state)[merge.into] ?? [];
|
|
8732
|
+
const settled = ids.length - merge.pending.length;
|
|
8733
|
+
if (settled < 0 || ids.slice(settled).some((id, offset) => id !== merge.pending[offset])) return null;
|
|
8734
|
+
const to = toIndex ?? settled;
|
|
8735
|
+
if (to < 0 || to > settled) return null;
|
|
8736
|
+
const moved = applyCaptionOperation(state, {
|
|
8737
|
+
operation: "move",
|
|
8738
|
+
frameId: merge.into,
|
|
8739
|
+
index: ids.indexOf(slot2),
|
|
8740
|
+
toFrameId: merge.into,
|
|
8741
|
+
toIndex: to
|
|
8742
|
+
});
|
|
8743
|
+
if (moved) merge.pending.splice(index, 1);
|
|
8744
|
+
return moved;
|
|
8745
|
+
}
|
|
8632
8746
|
function applyStructuralOperations(state, patch, assets = []) {
|
|
8633
8747
|
let next = state;
|
|
8634
8748
|
const assetBindings = [];
|
|
8635
8749
|
const frameRefs = /* @__PURE__ */ new Map();
|
|
8636
8750
|
const retiredRefs = /* @__PURE__ */ new Set();
|
|
8751
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8637
8752
|
for (const raw of normalizeStructuralOperations(patch)) {
|
|
8638
8753
|
const ref = raw.op === "frame.add" ? raw.frameRef : void 0;
|
|
8639
8754
|
if (ref !== void 0 && (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(ref) || frameRefs.has(ref) || state.panels.some((panel) => panel.id === ref) || state.shots.some((shot) => shot.id === ref))) return null;
|
|
@@ -8646,8 +8761,14 @@ function applyStructuralOperations(state, patch, assets = []) {
|
|
|
8646
8761
|
..."fromFrameId" in raw ? { fromFrameId: resolve14(raw.fromFrameId) } : {},
|
|
8647
8762
|
..."afterFrameId" in raw && raw.afterFrameId !== void 0 ? { afterFrameId: resolve14(raw.afterFrameId) } : {}
|
|
8648
8763
|
};
|
|
8649
|
-
const
|
|
8764
|
+
const merge = operation.op === "caption.move" && panelIndex(next, operation.frameId) < 0 ? merged.get(operation.frameId) : void 0;
|
|
8765
|
+
const applied = operation.op === "caption.move" && merge?.into === operation.toFrameId ? applyMergedCaptionMove(next, merge, operation.index, operation.toIndex) : applyStructuralOperation(next, operation, assets, assetBindings);
|
|
8650
8766
|
if (!applied) return null;
|
|
8767
|
+
if (operation.op === "frame.combine") {
|
|
8768
|
+
const carried = captionSlotIds(next)[operation.fromFrameId]?.length ?? 0;
|
|
8769
|
+
const survivor = captionSlotIds(applied)[operation.frameId] ?? [];
|
|
8770
|
+
merged.set(operation.fromFrameId, { into: operation.frameId, pending: carried > 0 ? survivor.slice(-carried) : [] });
|
|
8771
|
+
}
|
|
8651
8772
|
if (ref !== void 0) {
|
|
8652
8773
|
const created = applied.panels.find((panel) => !next.panels.some((previous) => previous.id === panel.id));
|
|
8653
8774
|
if (!created) return null;
|
|
@@ -8673,6 +8794,57 @@ var init_refinementStructure = __esm({
|
|
|
8673
8794
|
}
|
|
8674
8795
|
});
|
|
8675
8796
|
|
|
8797
|
+
// ../mockup-engine/appstore/templatePoseGuard.ts
|
|
8798
|
+
function withoutPose(entry) {
|
|
8799
|
+
if (!TEMPLATE_OWNED_SHOT_FIELDS.some((field) => entry[field] !== void 0)) return null;
|
|
8800
|
+
const kept = { ...entry };
|
|
8801
|
+
for (const field of TEMPLATE_OWNED_SHOT_FIELDS) delete kept[field];
|
|
8802
|
+
return kept;
|
|
8803
|
+
}
|
|
8804
|
+
function dropTemplatePoses(patch, templated, panelIds, panelOfDevice) {
|
|
8805
|
+
if (templated.size === 0 || !patch.shots && !patch.devices) return { patch, dropped: false };
|
|
8806
|
+
let dropped = false;
|
|
8807
|
+
let next = patch;
|
|
8808
|
+
if (patch.shots) {
|
|
8809
|
+
const shots = patch.shots.map((entry, index) => {
|
|
8810
|
+
const panelId = panelIds[index];
|
|
8811
|
+
const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry) : null;
|
|
8812
|
+
if (!kept) return entry;
|
|
8813
|
+
dropped = true;
|
|
8814
|
+
return kept;
|
|
8815
|
+
});
|
|
8816
|
+
if (dropped) next = { ...next, shots };
|
|
8817
|
+
}
|
|
8818
|
+
if (patch.devices) {
|
|
8819
|
+
let devicesDropped = false;
|
|
8820
|
+
const devices = [];
|
|
8821
|
+
for (const entry of patch.devices) {
|
|
8822
|
+
const panelId = panelOfDevice(entry.deviceId);
|
|
8823
|
+
const kept = panelId !== void 0 && templated.has(panelId) ? withoutPose(entry.patch) : null;
|
|
8824
|
+
if (!kept) {
|
|
8825
|
+
devices.push(entry);
|
|
8826
|
+
continue;
|
|
8827
|
+
}
|
|
8828
|
+
devicesDropped = true;
|
|
8829
|
+
if (Object.keys(kept).length > 0) devices.push({ ...entry, patch: kept });
|
|
8830
|
+
}
|
|
8831
|
+
if (devicesDropped) {
|
|
8832
|
+
dropped = true;
|
|
8833
|
+
const { devices: _replaced, ...rest } = next;
|
|
8834
|
+
void _replaced;
|
|
8835
|
+
next = devices.length > 0 ? { ...rest, devices } : rest;
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
return { patch: next, dropped };
|
|
8839
|
+
}
|
|
8840
|
+
var TEMPLATE_OWNED_SHOT_FIELDS;
|
|
8841
|
+
var init_templatePoseGuard = __esm({
|
|
8842
|
+
"../mockup-engine/appstore/templatePoseGuard.ts"() {
|
|
8843
|
+
"use strict";
|
|
8844
|
+
TEMPLATE_OWNED_SHOT_FIELDS = ["orbit", "roll", "angle", "phoneHeight", "hOffset", "vOffset"];
|
|
8845
|
+
}
|
|
8846
|
+
});
|
|
8847
|
+
|
|
8676
8848
|
// ../mockup-engine/appstore/refinementEffect.ts
|
|
8677
8849
|
function describeRefinementEffect(before, after) {
|
|
8678
8850
|
const changes = [];
|
|
@@ -8684,6 +8856,17 @@ function describeRefinementEffect(before, after) {
|
|
|
8684
8856
|
if (framesRemoved > 0) changes.push(`Removed ${framesRemoved} frame${framesRemoved === 1 ? "" : "s"}`);
|
|
8685
8857
|
const beforeCaptions = before.captionText[before.baseLocale] ?? {};
|
|
8686
8858
|
const afterCaptions = after.captionText[after.baseLocale] ?? {};
|
|
8859
|
+
const beforeSlots = normalizeCaptionSlotIds(before.captionSlotIds, before.captionText, before.panels.map((panel) => panel.id));
|
|
8860
|
+
const afterSlots = normalizeCaptionSlotIds(after.captionSlotIds, after.captionText, after.panels.map((panel) => panel.id));
|
|
8861
|
+
const reorderOf = (panelId) => {
|
|
8862
|
+
const was = beforeSlots[panelId] ?? [];
|
|
8863
|
+
const now = afterSlots[panelId] ?? [];
|
|
8864
|
+
if (was.length !== now.length || was.join("\0") === now.join("\0")) return null;
|
|
8865
|
+
const source = now.map((id) => was.indexOf(id));
|
|
8866
|
+
return source.every((index, position) => index >= 0 && source.indexOf(index) === position) ? source : null;
|
|
8867
|
+
};
|
|
8868
|
+
const sourceIndex = (panelId, index) => reorderOf(panelId)?.[index] ?? index;
|
|
8869
|
+
let captionFramesReordered = 0;
|
|
8687
8870
|
let rewritten = 0;
|
|
8688
8871
|
let captionSlotsAdded = 0;
|
|
8689
8872
|
let captionSlotsRemoved = 0;
|
|
@@ -8694,8 +8877,9 @@ function describeRefinementEffect(before, after) {
|
|
|
8694
8877
|
captionSlotsAdded += Math.max(0, newLayers.length - oldLayers.length);
|
|
8695
8878
|
captionSlotsRemoved += Math.max(0, oldLayers.length - newLayers.length);
|
|
8696
8879
|
if (oldLayers.length !== newLayers.length) continue;
|
|
8880
|
+
if (reorderOf(panelId)) captionFramesReordered += 1;
|
|
8697
8881
|
for (let index = 0; index < oldLayers.length; index++) {
|
|
8698
|
-
if (oldLayers[index]
|
|
8882
|
+
if (oldLayers[sourceIndex(panelId, index)]?.text !== newLayers[index].text) rewritten += 1;
|
|
8699
8883
|
}
|
|
8700
8884
|
}
|
|
8701
8885
|
const captionWordsIn = (source) => Object.entries(source).flatMap(([panelId, layers]) => layers.map((layer) => `${panelId}\0${layer.text}`));
|
|
@@ -8712,6 +8896,9 @@ function describeRefinementEffect(before, after) {
|
|
|
8712
8896
|
captionSlotsAdded -= movedCaptions;
|
|
8713
8897
|
captionSlotsRemoved -= movedCaptions;
|
|
8714
8898
|
if (rewritten > 0) changes.push(`Rewrote ${rewritten} caption${rewritten === 1 ? "" : "s"}`);
|
|
8899
|
+
if (captionFramesReordered > 0) {
|
|
8900
|
+
changes.push(`Reordered the captions in ${captionFramesReordered} frame${captionFramesReordered === 1 ? "" : "s"}`);
|
|
8901
|
+
}
|
|
8715
8902
|
if (movedCaptions > 0) {
|
|
8716
8903
|
changes.push(`Moved ${movedCaptions} caption${movedCaptions === 1 ? "" : "s"} to another frame`);
|
|
8717
8904
|
}
|
|
@@ -8744,7 +8931,10 @@ function describeRefinementEffect(before, after) {
|
|
|
8744
8931
|
const oldLayers = oldByPanel[panelId] ?? [];
|
|
8745
8932
|
const newLayers = newByPanel[panelId] ?? [];
|
|
8746
8933
|
if (oldLayers.length !== newLayers.length) return false;
|
|
8747
|
-
return
|
|
8934
|
+
return newLayers.some((layer, index) => {
|
|
8935
|
+
const prior = oldLayers[sourceIndex(panelId, index)];
|
|
8936
|
+
return !prior || styleOf(prior) !== styleOf(layer);
|
|
8937
|
+
});
|
|
8748
8938
|
});
|
|
8749
8939
|
});
|
|
8750
8940
|
const styleByText = (source) => {
|
|
@@ -8811,6 +9001,7 @@ var init_refinementEffect = __esm({
|
|
|
8811
9001
|
"../mockup-engine/appstore/refinementEffect.ts"() {
|
|
8812
9002
|
"use strict";
|
|
8813
9003
|
init_gradient();
|
|
9004
|
+
init_frameTemplates();
|
|
8814
9005
|
init_refinementValues();
|
|
8815
9006
|
}
|
|
8816
9007
|
});
|
|
@@ -8914,7 +9105,18 @@ function applyProjectRefinementWithAssets(state, patch, panelIds, assets = []) {
|
|
|
8914
9105
|
next = { ...next, shots, captionText, ...overlay.background };
|
|
8915
9106
|
}
|
|
8916
9107
|
}
|
|
8917
|
-
if (patch.layout)
|
|
9108
|
+
if (patch.layout) {
|
|
9109
|
+
next = applyLayout(next, patch.layout, sourcePanelIds);
|
|
9110
|
+
if (layoutTemplateById(patch.layout)) {
|
|
9111
|
+
const shots = next.shots;
|
|
9112
|
+
patch = dropTemplatePoses(
|
|
9113
|
+
patch,
|
|
9114
|
+
new Set(sourcePanelIds),
|
|
9115
|
+
requestedPanelIds,
|
|
9116
|
+
(deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
|
|
9117
|
+
).patch;
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
8918
9120
|
if (patch.shots?.length === positionalCount) {
|
|
8919
9121
|
const entries = new Map(requestedPanelIds.map((panelId, index) => [panelId, patch.shots[index]]));
|
|
8920
9122
|
const shared = new Set(
|
|
@@ -9051,6 +9253,7 @@ var init_refinement = __esm({
|
|
|
9051
9253
|
init_refinementStructure();
|
|
9052
9254
|
init_refinementValues();
|
|
9053
9255
|
init_gradient();
|
|
9256
|
+
init_templatePoseGuard();
|
|
9054
9257
|
init_refinementEffect();
|
|
9055
9258
|
init_refinementStructure();
|
|
9056
9259
|
}
|
|
@@ -9124,45 +9327,55 @@ var init_appStoreLocales = __esm({
|
|
|
9124
9327
|
});
|
|
9125
9328
|
|
|
9126
9329
|
// ../mockup-engine/appstore/agentTurnRepetition.ts
|
|
9127
|
-
function
|
|
9128
|
-
if (!
|
|
9330
|
+
function appliedOperationOf(operation) {
|
|
9331
|
+
if (!NON_REPEATABLE_OPS.has(operation.op)) return null;
|
|
9332
|
+
if (operation.op === "caption.remove") return { op: operation.op, target: `${operation.frameId}#${operation.index}` };
|
|
9333
|
+
if (operation.op === "device.remove") return { op: operation.op, target: operation.deviceId };
|
|
9129
9334
|
const target = "frameId" in operation ? operation.frameId : "toFrameId" in operation ? operation.toFrameId : "afterFrameId" in operation && operation.afterFrameId ? `after:${operation.afterFrameId}` : "strip";
|
|
9130
9335
|
return { op: operation.op, target };
|
|
9131
9336
|
}
|
|
9132
|
-
function
|
|
9337
|
+
function normalizeTarget(target, refs) {
|
|
9338
|
+
if (target.startsWith("after:")) {
|
|
9339
|
+
const id = target.slice(6);
|
|
9340
|
+
return `after:${refs.get(id) ?? id}`;
|
|
9341
|
+
}
|
|
9342
|
+
const ordinal = target.indexOf("#");
|
|
9343
|
+
if (ordinal >= 0) {
|
|
9344
|
+
const id = target.slice(0, ordinal);
|
|
9345
|
+
return `${refs.get(id) ?? id}${target.slice(ordinal)}`;
|
|
9346
|
+
}
|
|
9347
|
+
return refs.get(target) ?? target;
|
|
9348
|
+
}
|
|
9349
|
+
function contextualOperations(operations, resolved = {}) {
|
|
9133
9350
|
const refs = /* @__PURE__ */ new Map();
|
|
9134
9351
|
return operations.map((operation) => {
|
|
9135
|
-
const
|
|
9136
|
-
const
|
|
9137
|
-
const normalized = creation && target !== void 0 ? {
|
|
9138
|
-
...creation,
|
|
9139
|
-
target: target.startsWith("after:") ? `after:${refs.get(target.slice(6)) ?? target.slice(6)}` : refs.get(target) ?? target
|
|
9140
|
-
} : null;
|
|
9352
|
+
const applied = appliedOperationOf(operation);
|
|
9353
|
+
const normalized = applied ? { ...applied, target: normalizeTarget(applied.target, refs) } : null;
|
|
9141
9354
|
if (operation.op === "frame.add" && operation.frameRef && normalized) {
|
|
9142
9355
|
refs.set(operation.frameRef, Object.prototype.hasOwnProperty.call(resolved, operation.frameRef) ? resolved[operation.frameRef] : `created-frame:${normalized.target}`);
|
|
9143
9356
|
}
|
|
9144
9357
|
return normalized;
|
|
9145
9358
|
});
|
|
9146
9359
|
}
|
|
9147
|
-
function
|
|
9360
|
+
function appliedOperationsIn(patch, frameRefs) {
|
|
9148
9361
|
const operations = patch.structuralOperations ?? [];
|
|
9149
|
-
const symbolic =
|
|
9150
|
-
const durable = frameRefs ?
|
|
9151
|
-
return symbolic.concat(durable.filter((
|
|
9362
|
+
const symbolic = contextualOperations(operations).flatMap((applied) => applied ? [applied] : []);
|
|
9363
|
+
const durable = frameRefs ? contextualOperations(operations, frameRefs).flatMap((applied) => applied ? [applied] : []) : [];
|
|
9364
|
+
return symbolic.concat(durable.filter((applied) => !symbolic.some((entry) => entry.op === applied.op && entry.target === applied.target)));
|
|
9152
9365
|
}
|
|
9153
|
-
function
|
|
9366
|
+
function dropRepeatedOperations(patch, already) {
|
|
9154
9367
|
const operations = patch.structuralOperations;
|
|
9155
9368
|
if (!operations || operations.length === 0 || already.length === 0) return { patch, dropped: [] };
|
|
9156
|
-
const seen = new Set(already.map((
|
|
9369
|
+
const seen = new Set(already.map((applied2) => `${applied2.op} ${applied2.target}`));
|
|
9157
9370
|
const dropped = [];
|
|
9158
|
-
const
|
|
9371
|
+
const applied = contextualOperations(operations);
|
|
9159
9372
|
const unavailableRefs = /* @__PURE__ */ new Set();
|
|
9160
9373
|
const kept = operations.filter((operation, index) => {
|
|
9161
|
-
const
|
|
9374
|
+
const entry = applied[index];
|
|
9162
9375
|
const dependent = structuralOperationTargets(operation).frameIds.some((id) => unavailableRefs.has(id));
|
|
9163
|
-
if (!dependent && (!
|
|
9376
|
+
if (!dependent && (!entry || !seen.has(`${entry.op} ${entry.target}`))) return true;
|
|
9164
9377
|
if (operation.op === "frame.add" && operation.frameRef) unavailableRefs.add(operation.frameRef);
|
|
9165
|
-
if (
|
|
9378
|
+
if (entry) dropped.push(entry);
|
|
9166
9379
|
return false;
|
|
9167
9380
|
});
|
|
9168
9381
|
if (dropped.length === 0) return { patch, dropped };
|
|
@@ -9170,16 +9383,19 @@ function dropRepeatedCreations(patch, already) {
|
|
|
9170
9383
|
void _dropped;
|
|
9171
9384
|
return { patch: kept.length > 0 ? { ...rest, structuralOperations: kept } : rest, dropped };
|
|
9172
9385
|
}
|
|
9173
|
-
var
|
|
9386
|
+
var NON_REPEATABLE_OPS;
|
|
9174
9387
|
var init_agentTurnRepetition = __esm({
|
|
9175
9388
|
"../mockup-engine/appstore/agentTurnRepetition.ts"() {
|
|
9176
9389
|
"use strict";
|
|
9177
9390
|
init_refineIntent();
|
|
9178
|
-
|
|
9391
|
+
NON_REPEATABLE_OPS = /* @__PURE__ */ new Set([
|
|
9179
9392
|
"frame.add",
|
|
9180
9393
|
"frame.split",
|
|
9181
9394
|
"caption.add",
|
|
9182
|
-
"device.add"
|
|
9395
|
+
"device.add",
|
|
9396
|
+
"frame.remove",
|
|
9397
|
+
"caption.remove",
|
|
9398
|
+
"device.remove"
|
|
9183
9399
|
]);
|
|
9184
9400
|
}
|
|
9185
9401
|
});
|
|
@@ -9231,8 +9447,14 @@ async function runAgentTurn(input, ports) {
|
|
|
9231
9447
|
const layoutRepairable = canRepairLayoutForFocus(input.focus, instruction);
|
|
9232
9448
|
const loaded = await ports.loadProject();
|
|
9233
9449
|
const before = loaded.state;
|
|
9450
|
+
const sourceCaptions = before.panels.map((panel) => ({
|
|
9451
|
+
frameId: panel.id,
|
|
9452
|
+
captions: (before.captionText[before.baseLocale]?.[panel.id] ?? []).map((layer) => layer.text)
|
|
9453
|
+
}));
|
|
9234
9454
|
let working = before;
|
|
9235
9455
|
let rendered;
|
|
9456
|
+
let renderedState = null;
|
|
9457
|
+
let measuredLayout = null;
|
|
9236
9458
|
const passes = [];
|
|
9237
9459
|
const broaderChanges = /* @__PURE__ */ new Set();
|
|
9238
9460
|
let checkpointId = 0;
|
|
@@ -9244,14 +9466,16 @@ async function runAgentTurn(input, ports) {
|
|
|
9244
9466
|
let interpretation = "";
|
|
9245
9467
|
let inspection = "";
|
|
9246
9468
|
let remainingMismatch = null;
|
|
9247
|
-
const
|
|
9469
|
+
const appliedOperations = [];
|
|
9248
9470
|
const touchedCapabilities = /* @__PURE__ */ new Set();
|
|
9471
|
+
const templatedPanels = /* @__PURE__ */ new Set();
|
|
9249
9472
|
let terminalOutcome = null;
|
|
9250
9473
|
let invalid2 = false;
|
|
9251
9474
|
let cancelled = false;
|
|
9252
9475
|
await emit({ type: "turn_started" });
|
|
9253
9476
|
try {
|
|
9254
9477
|
rendered = await ports.renderComposed(working, 0);
|
|
9478
|
+
renderedState = working;
|
|
9255
9479
|
} catch {
|
|
9256
9480
|
terminalOutcome = failureOutcome(
|
|
9257
9481
|
"service_unavailable",
|
|
@@ -9279,12 +9503,13 @@ async function runAgentTurn(input, ports) {
|
|
|
9279
9503
|
actualDiff: selected?.changed ?? [],
|
|
9280
9504
|
inspection,
|
|
9281
9505
|
cycle: cycle - 1,
|
|
9282
|
-
appliedOperations: [...
|
|
9506
|
+
appliedOperations: [...appliedOperations],
|
|
9507
|
+
sourceCaptions: sourceCaptions.map((frame) => ({ ...frame, captions: [...frame.captions] }))
|
|
9283
9508
|
}
|
|
9284
9509
|
} : {}
|
|
9285
9510
|
});
|
|
9286
|
-
} catch {
|
|
9287
|
-
terminalOutcome = failureOutcome(
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
terminalOutcome = error instanceof AgentTurnAuthorizationError ? error.outcome : failureOutcome(
|
|
9288
9513
|
"service_unavailable",
|
|
9289
9514
|
"The Agent could not complete this turn. Nothing changed."
|
|
9290
9515
|
);
|
|
@@ -9322,9 +9547,19 @@ async function runAgentTurn(input, ports) {
|
|
|
9322
9547
|
remainingMismatch = authorization.remainingMismatch?.trim() || authorization.followUp?.trim() || null;
|
|
9323
9548
|
for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
|
|
9324
9549
|
if (authorization.patch && cycle > 1) {
|
|
9325
|
-
const guarded =
|
|
9550
|
+
const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
|
|
9326
9551
|
if (guarded.dropped.length > 0) authorization = { ...authorization, patch: guarded.patch };
|
|
9327
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 };
|
|
9562
|
+
}
|
|
9328
9563
|
if (authorization.patch) {
|
|
9329
9564
|
for (const id of capabilitiesForPatch(authorization.patch)) touchedCapabilities.add(id);
|
|
9330
9565
|
const application = applyProjectRefinementWithAssets(
|
|
@@ -9338,7 +9573,12 @@ async function runAgentTurn(input, ports) {
|
|
|
9338
9573
|
break;
|
|
9339
9574
|
}
|
|
9340
9575
|
working = application.state;
|
|
9341
|
-
|
|
9576
|
+
if (authorization.patch.layout && layoutTemplateById(authorization.patch.layout)) {
|
|
9577
|
+
for (const panelId of authorization.panelIds) {
|
|
9578
|
+
if (working.panels.some((panel) => panel.id === panelId)) templatedPanels.add(panelId);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
appliedOperations.push(...appliedOperationsIn(authorization.patch, application.frameRefs));
|
|
9342
9582
|
workingAssetBindings = [...workingAssetBindings, ...application.assetBindings];
|
|
9343
9583
|
checkpointId += 1;
|
|
9344
9584
|
const checkpoint = {
|
|
@@ -9365,7 +9605,24 @@ async function runAgentTurn(input, ports) {
|
|
|
9365
9605
|
});
|
|
9366
9606
|
}
|
|
9367
9607
|
const requiresRenderedInspection = cycle === 1 && authorization.patch != null;
|
|
9368
|
-
|
|
9608
|
+
let renderedThisCycle = false;
|
|
9609
|
+
let unresolvedLayout = null;
|
|
9610
|
+
if (layoutRepairable && authorization.complete === true && !requiresRenderedInspection && ports.unresolvedLayout) {
|
|
9611
|
+
let measurable = true;
|
|
9612
|
+
if (authorization.patch != null) {
|
|
9613
|
+
try {
|
|
9614
|
+
rendered = await ports.renderComposed(working, cycle);
|
|
9615
|
+
renderedState = working;
|
|
9616
|
+
renderedThisCycle = true;
|
|
9617
|
+
} catch {
|
|
9618
|
+
measurable = false;
|
|
9619
|
+
}
|
|
9620
|
+
}
|
|
9621
|
+
if (measurable) {
|
|
9622
|
+
unresolvedLayout = ports.unresolvedLayout(rendered)?.trim() || null;
|
|
9623
|
+
if (renderedState) measuredLayout = { state: renderedState, collision: unresolvedLayout };
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9369
9626
|
if (authorization.complete === true && !requiresRenderedInspection && !(unresolvedLayout && cycle < AGENT_TURN_PASS_CAP)) {
|
|
9370
9627
|
if (selected?.changed.length) best = selected;
|
|
9371
9628
|
remainingMismatch = unresolvedLayout;
|
|
@@ -9373,9 +9630,10 @@ async function runAgentTurn(input, ports) {
|
|
|
9373
9630
|
}
|
|
9374
9631
|
if (unresolvedLayout) remainingMismatch = unresolvedLayout;
|
|
9375
9632
|
else if (authorization.followUp && !authorization.patch) break;
|
|
9376
|
-
if (cycle < AGENT_TURN_PASS_CAP) {
|
|
9633
|
+
if (cycle < AGENT_TURN_PASS_CAP && !renderedThisCycle) {
|
|
9377
9634
|
try {
|
|
9378
9635
|
rendered = await ports.renderComposed(working, cycle);
|
|
9636
|
+
renderedState = working;
|
|
9379
9637
|
} catch {
|
|
9380
9638
|
terminalOutcome = failureOutcome(
|
|
9381
9639
|
"service_unavailable",
|
|
@@ -9393,7 +9651,10 @@ async function runAgentTurn(input, ports) {
|
|
|
9393
9651
|
terminalOutcome ?? applicationOutcome({
|
|
9394
9652
|
changed: selected?.changed ?? [],
|
|
9395
9653
|
invalid: invalid2,
|
|
9396
|
-
gaps: selected?.gaps ?? []
|
|
9654
|
+
gaps: selected?.gaps ?? [],
|
|
9655
|
+
// Only a measurement of the very state being committed. A collision seen on an earlier draft,
|
|
9656
|
+
// or on a strip the last pass then changed without it being rendered, says nothing about this one.
|
|
9657
|
+
unresolvedLayout: selected && measuredLayout?.state === selected.state ? measuredLayout.collision : null
|
|
9397
9658
|
}),
|
|
9398
9659
|
Array.from(touchedCapabilities)
|
|
9399
9660
|
);
|
|
@@ -9458,7 +9719,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9458
9719
|
void input.requestedCycle;
|
|
9459
9720
|
return result;
|
|
9460
9721
|
}
|
|
9461
|
-
var AGENT_TURN_PASS_CAP, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
|
|
9722
|
+
var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
|
|
9462
9723
|
var init_agentTurnCoordinator = __esm({
|
|
9463
9724
|
"../mockup-engine/appstore/agentTurnCoordinator.ts"() {
|
|
9464
9725
|
"use strict";
|
|
@@ -9470,7 +9731,17 @@ var init_agentTurnCoordinator = __esm({
|
|
|
9470
9731
|
init_appStoreLocales();
|
|
9471
9732
|
init_agentAssetCustody();
|
|
9472
9733
|
init_agentTurnRepetition();
|
|
9734
|
+
init_templatePoseGuard();
|
|
9735
|
+
init_layoutTemplates();
|
|
9473
9736
|
AGENT_TURN_PASS_CAP = 3;
|
|
9737
|
+
AgentTurnAuthorizationError = class extends Error {
|
|
9738
|
+
constructor(outcome) {
|
|
9739
|
+
super(outcome.headline);
|
|
9740
|
+
this.outcome = outcome;
|
|
9741
|
+
this.name = "AgentTurnAuthorizationError";
|
|
9742
|
+
}
|
|
9743
|
+
outcome;
|
|
9744
|
+
};
|
|
9474
9745
|
EMPTY_USAGE = {
|
|
9475
9746
|
inputTokens: 0,
|
|
9476
9747
|
cacheReadTokens: 0,
|
|
@@ -11250,6 +11521,84 @@ var init_renderCredits = __esm({
|
|
|
11250
11521
|
}
|
|
11251
11522
|
});
|
|
11252
11523
|
|
|
11524
|
+
// src/render/browserInstall.ts
|
|
11525
|
+
import { spawn as spawnProcess } from "node:child_process";
|
|
11526
|
+
import { createRequire } from "node:module";
|
|
11527
|
+
import { dirname, join as join2 } from "node:path";
|
|
11528
|
+
function isMissingBrowserError(message) {
|
|
11529
|
+
return /Executable doesn't exist at /.test(message);
|
|
11530
|
+
}
|
|
11531
|
+
function classifyInstallFailure(output2) {
|
|
11532
|
+
const lines = output2.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !/^at /.test(line));
|
|
11533
|
+
const detail = (lines[lines.length - 1] ?? "the installer exited without output").slice(0, 300);
|
|
11534
|
+
return { offline: NETWORK_FAILURE.test(output2), detail };
|
|
11535
|
+
}
|
|
11536
|
+
function rendererSetupRefusal(err) {
|
|
11537
|
+
if (!(err instanceof RendererSetupError)) return null;
|
|
11538
|
+
if (err.offline) return errorResult(RENDERER_DOWNLOAD_OFFLINE_MESSAGE, "upstream_unavailable");
|
|
11539
|
+
return errorResult(`The one-time download of the renderer (Chromium) failed: ${err.detail}`, "internal_error");
|
|
11540
|
+
}
|
|
11541
|
+
async function refuseOnRendererSetupFailure(run) {
|
|
11542
|
+
try {
|
|
11543
|
+
return await run();
|
|
11544
|
+
} catch (err) {
|
|
11545
|
+
const refusal = rendererSetupRefusal(err);
|
|
11546
|
+
if (refusal) return refusal;
|
|
11547
|
+
throw err;
|
|
11548
|
+
}
|
|
11549
|
+
}
|
|
11550
|
+
function playwrightCliPath() {
|
|
11551
|
+
const require2 = createRequire(import.meta.url);
|
|
11552
|
+
return join2(dirname(require2.resolve("playwright/package.json")), "cli.js");
|
|
11553
|
+
}
|
|
11554
|
+
function installChromium(options = {}) {
|
|
11555
|
+
const spawn2 = options.spawn ?? spawnProcess;
|
|
11556
|
+
const log = options.log ?? ((chunk) => void process.stderr.write(chunk));
|
|
11557
|
+
const execPath = options.execPath ?? process.execPath;
|
|
11558
|
+
const cliPath = options.cliPath ?? playwrightCliPath();
|
|
11559
|
+
return new Promise((resolve14, reject) => {
|
|
11560
|
+
let output2 = "";
|
|
11561
|
+
const collect = (data) => {
|
|
11562
|
+
const text3 = String(data);
|
|
11563
|
+
log(text3);
|
|
11564
|
+
output2 = (output2 + text3).slice(-OUTPUT_TAIL_BYTES);
|
|
11565
|
+
};
|
|
11566
|
+
const child = spawn2(execPath, [cliPath, "install", "chromium"], {
|
|
11567
|
+
env: options.env ?? process.env,
|
|
11568
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11569
|
+
windowsHide: true
|
|
11570
|
+
});
|
|
11571
|
+
child.stdout?.on("data", collect);
|
|
11572
|
+
child.stderr?.on("data", collect);
|
|
11573
|
+
child.on("error", (err) => reject(new RendererSetupError(classifyInstallFailure(err.message))));
|
|
11574
|
+
child.on("close", (code) => {
|
|
11575
|
+
if (code === 0) resolve14();
|
|
11576
|
+
else reject(new RendererSetupError(classifyInstallFailure(output2)));
|
|
11577
|
+
});
|
|
11578
|
+
});
|
|
11579
|
+
}
|
|
11580
|
+
var RENDERER_DOWNLOADED_NOTE, RENDERER_DOWNLOAD_OFFLINE_MESSAGE, NETWORK_FAILURE, RendererSetupError, OUTPUT_TAIL_BYTES;
|
|
11581
|
+
var init_browserInstall = __esm({
|
|
11582
|
+
"src/render/browserInstall.ts"() {
|
|
11583
|
+
"use strict";
|
|
11584
|
+
init_toolResults();
|
|
11585
|
+
RENDERER_DOWNLOADED_NOTE = "This was the first render on this machine, so ShotOps downloaded its renderer (Chromium) once before rendering; later renders skip that step.";
|
|
11586
|
+
RENDERER_DOWNLOAD_OFFLINE_MESSAGE = "The first render on this machine needs a one-time download of the renderer (Chromium), and the download server could not be reached, so connect to the internet and try again.";
|
|
11587
|
+
NETWORK_FAILURE = /\b(?:ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b|getaddrinfo|socket hang up|server closed connection|timed out after \d+ms/;
|
|
11588
|
+
RendererSetupError = class extends Error {
|
|
11589
|
+
offline;
|
|
11590
|
+
detail;
|
|
11591
|
+
constructor(failure2) {
|
|
11592
|
+
super(failure2.offline ? RENDERER_DOWNLOAD_OFFLINE_MESSAGE : `renderer download failed: ${failure2.detail}`);
|
|
11593
|
+
this.name = "RendererSetupError";
|
|
11594
|
+
this.offline = failure2.offline;
|
|
11595
|
+
this.detail = failure2.detail;
|
|
11596
|
+
}
|
|
11597
|
+
};
|
|
11598
|
+
OUTPUT_TAIL_BYTES = 16e3;
|
|
11599
|
+
}
|
|
11600
|
+
});
|
|
11601
|
+
|
|
11253
11602
|
// ../api/_lib/billingStore.ts
|
|
11254
11603
|
var SUBSCRIPTION_COLUMNS;
|
|
11255
11604
|
var init_billingStore = __esm({
|
|
@@ -11542,7 +11891,7 @@ var init_providerSpendStore = __esm({
|
|
|
11542
11891
|
evidenceSchema = z8.object({
|
|
11543
11892
|
state: z8.enum(["estimated", "partial", "unknown", "not_incurred"]),
|
|
11544
11893
|
amountNanoUsd: decimalMoney.nullable(),
|
|
11545
|
-
missing: z8.array(z8.enum(["rate", "usage", "input", "input_breakdown", "inconsistent_input", "cache_read", "cache_write", "cache_write_ttl", "output", "multiple_provider_attempts", "provider_attempts_unknown", "amount_overflow"])),
|
|
11894
|
+
missing: z8.array(z8.enum(["rate", "usage", "input", "input_breakdown", "inconsistent_input", "cache_read", "cache_write", "cache_write_ttl", "output", "multiple_provider_attempts", "provider_attempts_unknown", "gateway_bill", "amount_overflow"])),
|
|
11546
11895
|
usage: z8.object({ inputTokens: nullableCount, cacheReadTokens: nullableCount, cacheWriteTokens: nullableCount, outputTokens: nullableCount, cacheWriteTtl: z8.enum(["5m", "1h"]).nullable() }).strict().nullable(),
|
|
11547
11896
|
rate: z8.object({
|
|
11548
11897
|
version: z8.string().regex(/^[a-zA-Z0-9._-]{1,100}$/),
|
|
@@ -12058,7 +12407,7 @@ var init_package = __esm({
|
|
|
12058
12407
|
"package.json"() {
|
|
12059
12408
|
package_default = {
|
|
12060
12409
|
name: "shotops-mcp",
|
|
12061
|
-
version: "0.9.
|
|
12410
|
+
version: "0.9.7",
|
|
12062
12411
|
private: false,
|
|
12063
12412
|
type: "module",
|
|
12064
12413
|
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.",
|
|
@@ -12095,7 +12444,8 @@ var init_package = __esm({
|
|
|
12095
12444
|
prepack: "node ../scripts/third-party-notices.mjs --check",
|
|
12096
12445
|
prepublishOnly: "npm run build",
|
|
12097
12446
|
typecheck: "tsc --noEmit",
|
|
12098
|
-
"build:widget": "node build.widget.mjs"
|
|
12447
|
+
"build:widget": "node build.widget.mjs",
|
|
12448
|
+
"build:mcpb": "tsx build.mcpb.ts"
|
|
12099
12449
|
},
|
|
12100
12450
|
dependencies: {
|
|
12101
12451
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -12109,7 +12459,9 @@ var init_package = __esm({
|
|
|
12109
12459
|
zod: "^3.25.76"
|
|
12110
12460
|
},
|
|
12111
12461
|
devDependencies: {
|
|
12462
|
+
"@anthropic-ai/mcpb": "2.1.2",
|
|
12112
12463
|
"@types/express": "^5.0.6",
|
|
12464
|
+
"@vercel/blob": "2.8.0",
|
|
12113
12465
|
ai: "^7.0.48",
|
|
12114
12466
|
esbuild: "^0.28.1",
|
|
12115
12467
|
nitro: "3.0.260610-beta",
|
|
@@ -12259,6 +12611,15 @@ function projectCopyLocales(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
|
12259
12611
|
}
|
|
12260
12612
|
return out;
|
|
12261
12613
|
}
|
|
12614
|
+
function projectCopyGaps(projectFile, locales, family = BASE_SCREEN_FAMILY) {
|
|
12615
|
+
const file = projectFile;
|
|
12616
|
+
const baseLocale = typeof file?.baseLocale === "string" && file.baseLocale ? file.baseLocale : "en-US";
|
|
12617
|
+
const captionText = normalizeCaptionText(file?.captionText, file?.captionStyles, baseLocale);
|
|
12618
|
+
const outputCaptions = normalizeOutputCaptionText(file?.outputCaptions);
|
|
12619
|
+
const base = resolveOutputCaptionText(captionText, outputCaptions, family, baseLocale, baseLocale);
|
|
12620
|
+
if (!Object.values(base).some((layers) => layers.some((layer) => layer.text.trim().length > 0))) return [];
|
|
12621
|
+
return localesWithoutCaptions(captionText, [...locales], baseLocale).filter((locale2) => !Object.values(outputCaptions[family]?.[locale2] ?? {}).some((words) => words.some((word) => word.trim().length > 0))).map((locale2) => ({ code: "copy_locale_missing", subject: locale2 }));
|
|
12622
|
+
}
|
|
12262
12623
|
function directCopyLocales(style2, locales) {
|
|
12263
12624
|
const captions = style2?.captions;
|
|
12264
12625
|
const words = [];
|
|
@@ -12474,7 +12835,7 @@ var init_productionReadiness2 = __esm({
|
|
|
12474
12835
|
// src/tools/toolsRender.ts
|
|
12475
12836
|
function handleRenderStrip(deps, args, signal) {
|
|
12476
12837
|
const scope = openChargeScope(deps);
|
|
12477
|
-
return runCharged(scope, () => renderStrip(deps, args, scope, signal));
|
|
12838
|
+
return refuseOnRendererSetupFailure(() => runCharged(scope, () => renderStrip(deps, args, scope, signal)));
|
|
12478
12839
|
}
|
|
12479
12840
|
async function renderStrip(deps, args, scope, signal) {
|
|
12480
12841
|
const operationId = newOperationId("render_strip");
|
|
@@ -12592,7 +12953,7 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
12592
12953
|
}
|
|
12593
12954
|
const totalBytes = result.panels.reduce((sum, p) => sum + estimateDecodedBytes(p), 0);
|
|
12594
12955
|
const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, totalBytes, deps.forceOutput);
|
|
12595
|
-
const noteWithOutput = [note, outputNote, result.layoutNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
|
|
12956
|
+
const noteWithOutput = [note, outputNote, result.layoutNote, result.setupNote, resolutionGapNote(snapshot)].filter(Boolean).join(" ") || void 0;
|
|
12596
12957
|
const measuredReady = requireReadiness({
|
|
12597
12958
|
snapshot,
|
|
12598
12959
|
surface: "render",
|
|
@@ -13089,7 +13450,7 @@ async function handleRenderProject(deps, args, signal) {
|
|
|
13089
13450
|
const scope = openChargeScope(deps);
|
|
13090
13451
|
const attempt = {};
|
|
13091
13452
|
try {
|
|
13092
|
-
return await runCharged(scope, () => renderProject(deps, args, scope, attempt, signal));
|
|
13453
|
+
return await refuseOnRendererSetupFailure(() => runCharged(scope, () => renderProject(deps, args, scope, attempt, signal)));
|
|
13093
13454
|
} finally {
|
|
13094
13455
|
finishProductionRender(attempt.access);
|
|
13095
13456
|
}
|
|
@@ -13317,6 +13678,7 @@ async function renderProject(deps, args, scope, attempt, signal) {
|
|
|
13317
13678
|
if (outputNote) notes.push(outputNote);
|
|
13318
13679
|
if (droppedNote) notes.push(droppedNote);
|
|
13319
13680
|
if (result.layoutNote) notes.push(result.layoutNote);
|
|
13681
|
+
if (result.setupNote) notes.push(result.setupNote);
|
|
13320
13682
|
if (multi) {
|
|
13321
13683
|
notes.push(
|
|
13322
13684
|
`Rendered ${outputBlocks.length} output devices (${outputBlocks.map((o) => o.label).join(", ")}) \u2014 see \`outputs\`. The top-level panels are the active device (${active?.label}).`
|
|
@@ -13412,6 +13774,7 @@ var init_toolsRender = __esm({
|
|
|
13412
13774
|
init_studioUrl();
|
|
13413
13775
|
init_renderCredits();
|
|
13414
13776
|
init_panelDelivery();
|
|
13777
|
+
init_browserInstall();
|
|
13415
13778
|
init_toolContract();
|
|
13416
13779
|
init_schemas();
|
|
13417
13780
|
init_productionOperationStart();
|
|
@@ -13730,12 +14093,12 @@ var init_recordFromStrip = __esm({
|
|
|
13730
14093
|
// src/render/staticServer.ts
|
|
13731
14094
|
import { createServer } from "node:http";
|
|
13732
14095
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
13733
|
-
import { extname, join as
|
|
14096
|
+
import { extname, join as join4, relative, isAbsolute } from "node:path";
|
|
13734
14097
|
async function startStaticServer(rootDir) {
|
|
13735
14098
|
const server = createServer((req, res) => {
|
|
13736
14099
|
const reqPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
13737
14100
|
const rel = reqPath === "/" ? "index.html" : reqPath.replace(/^\/+/, "");
|
|
13738
|
-
const filePath =
|
|
14101
|
+
const filePath = join4(rootDir, rel);
|
|
13739
14102
|
const within = relative(rootDir, filePath);
|
|
13740
14103
|
if (within.startsWith("..") || isAbsolute(within)) {
|
|
13741
14104
|
res.statusCode = 403;
|
|
@@ -13799,6 +14162,7 @@ var init_renderer = __esm({
|
|
|
13799
14162
|
init_recordFromStrip();
|
|
13800
14163
|
init_log();
|
|
13801
14164
|
init_staticServer();
|
|
14165
|
+
init_browserInstall();
|
|
13802
14166
|
StripRenderer = class {
|
|
13803
14167
|
serveMode;
|
|
13804
14168
|
vite = null;
|
|
@@ -13814,9 +14178,14 @@ var init_renderer = __esm({
|
|
|
13814
14178
|
// The harness server's URL, cached so a Chromium re-boot (#336) re-launches the browser
|
|
13815
14179
|
// against the SAME server instead of starting a second Vite/static server per boot.
|
|
13816
14180
|
harnessUrl = null;
|
|
14181
|
+
installBrowser;
|
|
14182
|
+
// #889 — the note owed to the first SUCCESSFUL render after a browser install. Kept until one
|
|
14183
|
+
// succeeds, so a render that fails for an unrelated reason does not swallow it.
|
|
14184
|
+
pendingSetupNote;
|
|
13817
14185
|
// Default 'vite' so `new StripRenderer()` (server.ts) keeps the hosted path byte-for-byte.
|
|
13818
14186
|
constructor(options = {}) {
|
|
13819
14187
|
this.serveMode = options.serve ?? "vite";
|
|
14188
|
+
this.installBrowser = options.installBrowser;
|
|
13820
14189
|
}
|
|
13821
14190
|
boot() {
|
|
13822
14191
|
if (!this.booting) {
|
|
@@ -13829,8 +14198,7 @@ var init_renderer = __esm({
|
|
|
13829
14198
|
}
|
|
13830
14199
|
async doBoot() {
|
|
13831
14200
|
if (!this.harnessUrl) this.harnessUrl = await this.startHarnessServer();
|
|
13832
|
-
|
|
13833
|
-
this.browser = await chromium.launch({ headless: true });
|
|
14201
|
+
this.browser = await this.launchBrowser();
|
|
13834
14202
|
const page2 = await this.browser.newPage();
|
|
13835
14203
|
page2.on("console", (m) => {
|
|
13836
14204
|
if (/CONTEXT_LOST|Context Lost/i.test(m.text())) this.sawContextLost = true;
|
|
@@ -13839,6 +14207,24 @@ var init_renderer = __esm({
|
|
|
13839
14207
|
await page2.waitForFunction(() => window.__RENDER_READY === true, { timeout: 9e4 });
|
|
13840
14208
|
this.page = page2;
|
|
13841
14209
|
}
|
|
14210
|
+
// #889 — the install decision. Only a launch that failed because the browser binary is missing,
|
|
14211
|
+
// and only on a renderer that was given an installer, downloads anything; the install's own
|
|
14212
|
+
// failure (offline, most often) propagates as a RendererSetupError, which the render tools'
|
|
14213
|
+
// `refuseOnRendererSetupFailure` turns into a coded refusal. `boot()` is memoized, so concurrent
|
|
14214
|
+
// first renders share one download.
|
|
14215
|
+
async launchBrowser() {
|
|
14216
|
+
const { chromium } = await import("playwright");
|
|
14217
|
+
try {
|
|
14218
|
+
return await chromium.launch({ headless: true });
|
|
14219
|
+
} catch (err) {
|
|
14220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14221
|
+
if (!this.installBrowser || !isMissingBrowserError(message)) throw err;
|
|
14222
|
+
console.error("[shotops-mcp-local] Chromium is not installed \u2014 downloading the renderer once (playwright install chromium)\u2026");
|
|
14223
|
+
await this.installBrowser();
|
|
14224
|
+
this.pendingSetupNote = RENDERER_DOWNLOADED_NOTE;
|
|
14225
|
+
return await chromium.launch({ headless: true });
|
|
14226
|
+
}
|
|
14227
|
+
}
|
|
13842
14228
|
// The one line that forks between hosted and local. Everything downstream (browser launch,
|
|
13843
14229
|
// the __RENDER_READY handshake, window.renderStrip, boot-retry, CONTEXT_LOST) is identical
|
|
13844
14230
|
// — the page is the same harness, only its bytes come from a different server.
|
|
@@ -13915,6 +14301,11 @@ var init_renderer = __esm({
|
|
|
13915
14301
|
panels: result.count ?? void 0
|
|
13916
14302
|
});
|
|
13917
14303
|
}
|
|
14304
|
+
if (result.ok && this.pendingSetupNote) {
|
|
14305
|
+
const setupNote = this.pendingSetupNote;
|
|
14306
|
+
this.pendingSetupNote = void 0;
|
|
14307
|
+
return { ...result, setupNote };
|
|
14308
|
+
}
|
|
13918
14309
|
return result;
|
|
13919
14310
|
} catch (err) {
|
|
13920
14311
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -14028,6 +14419,9 @@ function parseControlPlaneResponse(op, value) {
|
|
|
14028
14419
|
case "read_project_screens":
|
|
14029
14420
|
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
14030
14421
|
break;
|
|
14422
|
+
case "read_attachment_screens":
|
|
14423
|
+
if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
|
|
14424
|
+
break;
|
|
14031
14425
|
case "refine_project":
|
|
14032
14426
|
if (typeof body.ok !== "boolean") throw new Error("control plane response is missing refine_project result");
|
|
14033
14427
|
if (body.ok) {
|
|
@@ -14265,6 +14659,10 @@ function connectHostedBridge(token2) {
|
|
|
14265
14659
|
...repairFindings && repairFindings.length > 0 ? { repairFindings } : {}
|
|
14266
14660
|
};
|
|
14267
14661
|
},
|
|
14662
|
+
async readAttachmentScreens(projectId, refs) {
|
|
14663
|
+
const res = await callOp(token2, "read_attachment_screens", { project: projectId, refs: [...refs] });
|
|
14664
|
+
return res.screens;
|
|
14665
|
+
},
|
|
14268
14666
|
async refineProject(input) {
|
|
14269
14667
|
return callOp(token2, "refine_project", input);
|
|
14270
14668
|
},
|
|
@@ -14529,7 +14927,9 @@ var init_visionGeneration = __esm({
|
|
|
14529
14927
|
LAYOUT_SELECTION_RULE = [
|
|
14530
14928
|
"LAYOUT SELECTION",
|
|
14531
14929
|
"- Choose the layout from the user's WORDS, not from the screenshots. A composition is a styling decision; pixels do not vote on it.",
|
|
14532
|
-
"- Every layout above ends with a [character].
|
|
14930
|
+
"- Every layout above ends with a [character] and a [bleeds] edge. Read the EDGE first: if the request says which way the device should go, that settles it.",
|
|
14931
|
+
...LAYOUT_BLEED_EDGE_IDS.map((edge) => ` - bleeds [${edge}] when ${LAYOUT_BLEED_EDGE_WORDS[edge]}.`),
|
|
14932
|
+
"- Otherwise pick the one whose character matches what was asked:",
|
|
14533
14933
|
...LAYOUT_CHARACTER_IDS.map((character) => ` - [${character}] when ${LAYOUT_CHARACTER_ANSWERS[character]}.`),
|
|
14534
14934
|
...LAYOUT_SELECTION_CAVEATS.map((line) => `- ${line}`)
|
|
14535
14935
|
].join("\n");
|
|
@@ -14538,7 +14938,7 @@ var init_visionGeneration = __esm({
|
|
|
14538
14938
|
|
|
14539
14939
|
// ../api/_lib/refineProjectContracts.ts
|
|
14540
14940
|
import { z as z10 } from "zod";
|
|
14541
|
-
var localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, refinementAttachmentsSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
14941
|
+
var MAX_STRIP_FRAMES, localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, refinementAttachmentsSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
14542
14942
|
var init_refineProjectContracts = __esm({
|
|
14543
14943
|
"../api/_lib/refineProjectContracts.ts"() {
|
|
14544
14944
|
"use strict";
|
|
@@ -14548,6 +14948,7 @@ var init_refineProjectContracts = __esm({
|
|
|
14548
14948
|
init_startIntent();
|
|
14549
14949
|
init_visionGeneration();
|
|
14550
14950
|
init_aiShotProjection();
|
|
14951
|
+
MAX_STRIP_FRAMES = 20;
|
|
14551
14952
|
localeIds = APP_STORE_LOCALES;
|
|
14552
14953
|
MAX_DURABLE_MODEL_PROSE = 32e3;
|
|
14553
14954
|
hexSchema = z10.string().trim().regex(/^#?([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
|
|
@@ -14749,7 +15150,7 @@ var init_refineProjectContracts = __esm({
|
|
|
14749
15150
|
focus: focusSchema.optional(),
|
|
14750
15151
|
baseLocale: localeCodeSchema,
|
|
14751
15152
|
current: z10.object({
|
|
14752
|
-
captions: z10.array(z10.string().max(240)).min(1).max(
|
|
15153
|
+
captions: z10.array(z10.string().max(240)).min(1).max(MAX_STRIP_FRAMES),
|
|
14753
15154
|
outputs: z10.array(z10.enum(START_OUTPUT_IDS)).min(1).max(START_OUTPUT_IDS.length),
|
|
14754
15155
|
// Bounded by the CATALOG, not by six. Studio's Add-language dialog offers every catalog entry
|
|
14755
15156
|
// with no ceiling and the reducer just appends, so a seventh language used to 400 every Agent
|
|
@@ -14758,15 +15159,16 @@ var init_refineProjectContracts = __esm({
|
|
|
14758
15159
|
locales: z10.array(localeCodeSchema).min(1).max(localeIds.length),
|
|
14759
15160
|
designPreset: z10.enum(START_DESIGN_PRESET_IDS).nullable(),
|
|
14760
15161
|
layout: z10.enum(START_LAYOUT_IDS).nullable(),
|
|
14761
|
-
frames: z10.array(currentShotSchema).min(1).max(
|
|
15162
|
+
frames: z10.array(currentShotSchema).min(1).max(MAX_STRIP_FRAMES),
|
|
14762
15163
|
background: currentStripBackgroundSchema.optional(),
|
|
14763
|
-
frameStyles: z10.array(currentFramePatchSchema).min(1).max(
|
|
14764
|
-
// Structural editing can add blank frames without adding source screenshots
|
|
14765
|
-
//
|
|
14766
|
-
//
|
|
14767
|
-
|
|
15164
|
+
frameStyles: z10.array(currentFramePatchSchema).min(1).max(MAX_STRIP_FRAMES).optional(),
|
|
15165
|
+
// Structural editing can add blank frames without adding source screenshots, so this and every
|
|
15166
|
+
// other per-frame array on `current` share `MAX_STRIP_FRAMES` rather than the vision-upload
|
|
15167
|
+
// ceiling — a real project frame belongs back in correction cycles instead of rejecting the
|
|
15168
|
+
// first successful `add`/`split` operation on a full strip (#838).
|
|
15169
|
+
structure: z10.array(currentFrameSchema).min(1).max(MAX_STRIP_FRAMES).optional()
|
|
14768
15170
|
}),
|
|
14769
|
-
screenshots: z10.array(visionScreenshotSchema).min(1).max(
|
|
15171
|
+
screenshots: z10.array(visionScreenshotSchema).min(1).max(MAX_STRIP_FRAMES),
|
|
14770
15172
|
// Private refs from Studio. Opaque asset handles are derived server-side, so a client cannot
|
|
14771
15173
|
// plant its own handle table or smuggle a ref into model-visible data.
|
|
14772
15174
|
attachments: refinementAttachmentsSchema.optional(),
|
|
@@ -14793,19 +15195,24 @@ var init_refineProjectContracts = __esm({
|
|
|
14793
15195
|
frames: z10.array(z10.number().int().min(0).max(31)).min(1).max(2),
|
|
14794
15196
|
depthPct: z10.number().int().min(0).max(100),
|
|
14795
15197
|
anchored: z10.boolean(),
|
|
14796
|
-
clearAtScale: z10.number().min(0.05).max(1).optional()
|
|
15198
|
+
clearAtScale: z10.number().min(0.05).max(1).optional(),
|
|
15199
|
+
clearAtMaxWidth: z10.number().min(0.1).max(1).optional()
|
|
14797
15200
|
})).max(24).optional(),
|
|
14798
15201
|
previous: z10.object({
|
|
14799
15202
|
actualDiff: z10.array(z10.string().trim().min(1).max(300)).max(24),
|
|
14800
15203
|
inspection: z10.string().trim().min(1).max(MAX_DURABLE_MODEL_PROSE),
|
|
14801
15204
|
cycle: z10.number().int().min(1).max(2),
|
|
14802
|
-
// The creating structural operations earlier cycles of this turn already
|
|
14803
|
-
// prompt can list them and the model stops re-
|
|
14804
|
-
// sends none, and the coordinator drops a repeat regardless.
|
|
15205
|
+
// The creating and removing structural operations earlier cycles of this turn already
|
|
15206
|
+
// performed, so the prompt can list them and the model stops re-doing what it did. Optional: an
|
|
15207
|
+
// older client sends none, and the coordinator drops a repeat regardless.
|
|
14805
15208
|
appliedOperations: z10.array(z10.object({
|
|
14806
15209
|
op: z10.string().trim().min(1).max(40),
|
|
14807
15210
|
target: z10.string().trim().min(1).max(180)
|
|
14808
|
-
})).max(32).optional()
|
|
15211
|
+
})).max(32).optional(),
|
|
15212
|
+
sourceCaptions: z10.array(z10.object({
|
|
15213
|
+
frameId: frameIdSchema,
|
|
15214
|
+
captions: z10.array(z10.string().max(240)).max(32)
|
|
15215
|
+
})).max(MAX_STRIP_FRAMES).optional()
|
|
14809
15216
|
}).optional()
|
|
14810
15217
|
}).superRefine((value, context) => {
|
|
14811
15218
|
if (value.sessionId == null !== (value.turnId == null)) {
|
|
@@ -14964,21 +15371,21 @@ var init_refineProjectContracts = __esm({
|
|
|
14964
15371
|
baseLocale: "the base language",
|
|
14965
15372
|
// The frame ceiling rides on all four fields it can surface through: Zod reports in declaration
|
|
14966
15373
|
// order, so any one of them can be the fault the user actually reads.
|
|
14967
|
-
"current.captions": `the caption list, one per frame and at most ${
|
|
15374
|
+
"current.captions": `the caption list, one per frame and at most ${MAX_STRIP_FRAMES}`,
|
|
14968
15375
|
"current.outputs": "the output size list",
|
|
14969
15376
|
"current.locales": "the language list on this project",
|
|
14970
15377
|
"current.designPreset": "the design preset",
|
|
14971
15378
|
"current.layout": "the layout template",
|
|
14972
|
-
"current.frames": `the per-frame device styling, at most ${
|
|
15379
|
+
"current.frames": `the per-frame device styling, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14973
15380
|
"current.frames.customColor": "a custom device colour",
|
|
14974
15381
|
"current.frames.clayCustom": "a custom clay colour",
|
|
14975
15382
|
"current.background": "the strip background",
|
|
14976
|
-
"current.frameStyles": `the per-frame caption styling, at most ${
|
|
15383
|
+
"current.frameStyles": `the per-frame caption styling, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14977
15384
|
"current.frameStyles.captionStyle.color": "a caption colour",
|
|
14978
15385
|
"current.frameStyles.background": "a frame's own background",
|
|
14979
|
-
"current.structure":
|
|
15386
|
+
"current.structure": `the frame structure of this project, at most ${MAX_STRIP_FRAMES} frames`,
|
|
14980
15387
|
current: "the current state of this project",
|
|
14981
|
-
screenshots: `the screenshot list, at most ${
|
|
15388
|
+
screenshots: `the screenshot list, at most ${MAX_STRIP_FRAMES} per Agent turn`,
|
|
14982
15389
|
attachments: `the screenshots offered to this turn, at most ${MAX_START_SCREENSHOTS}`,
|
|
14983
15390
|
assets: `the supplied-screenshot handles for this turn, at most ${MAX_START_SCREENSHOTS}`,
|
|
14984
15391
|
composedPreview: "the composed preview of the current strip",
|
|
@@ -15101,6 +15508,36 @@ async function compactVisionImage(data, name) {
|
|
|
15101
15508
|
}
|
|
15102
15509
|
throw new Error("A project screenshot could not be reduced for analysis.");
|
|
15103
15510
|
}
|
|
15511
|
+
async function withBoundAttachments(stored, state, variant, boundAssets, assetTable, readBytes) {
|
|
15512
|
+
const bound = state.shots.flatMap((shot) => {
|
|
15513
|
+
const assetId = boundAssets.get(shot.id);
|
|
15514
|
+
const ref = assetId ? assetTable.sources[assetId]?.ref : void 0;
|
|
15515
|
+
return ref ? [{ shot, ref }] : [];
|
|
15516
|
+
});
|
|
15517
|
+
if (bound.length === 0) return stored;
|
|
15518
|
+
const bytesByRef = await readBytes();
|
|
15519
|
+
if (!bytesByRef) return stored;
|
|
15520
|
+
const overlaid = /* @__PURE__ */ new Map();
|
|
15521
|
+
for (const { shot, ref } of bound) {
|
|
15522
|
+
const bytes = bytesByRef.get(ref);
|
|
15523
|
+
if (!bytes) continue;
|
|
15524
|
+
overlaid.set(shot.id, {
|
|
15525
|
+
shotId: shot.id,
|
|
15526
|
+
frameName: shot.frameName,
|
|
15527
|
+
variant,
|
|
15528
|
+
bytes: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"),
|
|
15529
|
+
source: "supplied"
|
|
15530
|
+
});
|
|
15531
|
+
}
|
|
15532
|
+
if (overlaid.size === 0) return stored;
|
|
15533
|
+
return {
|
|
15534
|
+
screenManifest: [
|
|
15535
|
+
...stored.screenManifest.filter((entry) => !overlaid.has(entry.shotId)),
|
|
15536
|
+
...overlaid.values()
|
|
15537
|
+
],
|
|
15538
|
+
missing: stored.missing
|
|
15539
|
+
};
|
|
15540
|
+
}
|
|
15104
15541
|
async function renderedTurn(deps, state, screens) {
|
|
15105
15542
|
const projectFile = serializeProject(state);
|
|
15106
15543
|
const variant = projectRenderVariant(projectFile, state.baseLocale);
|
|
@@ -15324,6 +15761,16 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15324
15761
|
const allowance = { remaining: null, refunded: 0, settled: false };
|
|
15325
15762
|
let storedRecord;
|
|
15326
15763
|
let screenSources = null;
|
|
15764
|
+
let boundAssets = /* @__PURE__ */ new Map();
|
|
15765
|
+
let attachmentBytes = null;
|
|
15766
|
+
const readAttachmentBytes = () => {
|
|
15767
|
+
if (!runtime.readAttachments) return Promise.resolve(null);
|
|
15768
|
+
attachmentBytes ??= runtime.readAttachments(project2.id, attachments.map((attachment) => attachment.ref)).catch((error) => {
|
|
15769
|
+
runtime.log("mcp_agent_turn_attachment_read_failed", { turnKey, ...safeFailureDiagnostic(error) });
|
|
15770
|
+
return null;
|
|
15771
|
+
});
|
|
15772
|
+
return attachmentBytes;
|
|
15773
|
+
};
|
|
15327
15774
|
const context = {
|
|
15328
15775
|
userId: deps.userId,
|
|
15329
15776
|
projectId: project2.id,
|
|
@@ -15365,10 +15812,11 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15365
15812
|
renderComposed: async (state) => {
|
|
15366
15813
|
if (!screenSources || "error" in screenSources) throw new Error("The project screenshots are unavailable.");
|
|
15367
15814
|
const variant = projectRenderVariant(serializeProject(state), state.baseLocale);
|
|
15368
|
-
const
|
|
15369
|
-
if (screenSources.expected.length > 0 &&
|
|
15815
|
+
const stored = await screenSources.resolve(variant);
|
|
15816
|
+
if (screenSources.expected.length > 0 && stored.screenManifest.length === 0 && boundAssets.size === 0) {
|
|
15370
15817
|
throw new Error("The project has no readable screenshots for Agent inspection.");
|
|
15371
15818
|
}
|
|
15819
|
+
const resolved = await withBoundAttachments(stored, state, variant, boundAssets, assetTable, readAttachmentBytes);
|
|
15372
15820
|
return renderedTurn(deps, state, resolved);
|
|
15373
15821
|
},
|
|
15374
15822
|
unresolvedLayout: (rendered) => describeUnresolvedLayout(rendered.layoutFindings),
|
|
@@ -15407,6 +15855,9 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
15407
15855
|
return write.error === "conflict" ? { ok: false, reason: "stale_version" } : { ok: false, reason: "write_failed" };
|
|
15408
15856
|
},
|
|
15409
15857
|
emit: async (event) => {
|
|
15858
|
+
if (event.type === "draft_applied") {
|
|
15859
|
+
boundAssets = new Map(event.assetBindings.map((binding) => [binding.deviceId, binding.assetId]));
|
|
15860
|
+
}
|
|
15410
15861
|
if (event.type === "turn_completed") {
|
|
15411
15862
|
const generationIds = event.result.legacy.passes.map((pass) => pass.generationId);
|
|
15412
15863
|
if (generationIds.length > 0) {
|
|
@@ -15540,6 +15991,12 @@ function localAgentTurnRuntime(bridge) {
|
|
|
15540
15991
|
}
|
|
15541
15992
|
});
|
|
15542
15993
|
},
|
|
15994
|
+
// #882 — the inspection render's pixels for devices a draft added or re-pointed. The control
|
|
15995
|
+
// plane authorizes and signs; the bytes come down here, where the render runs.
|
|
15996
|
+
readAttachments: async (projectId, refs) => {
|
|
15997
|
+
const screens = await bridge.readAttachmentScreens(projectId, refs);
|
|
15998
|
+
return new Map(await Promise.all(screens.map(async ({ ref, url }) => [ref, new Uint8Array(await downloadPng(url, "Supplied screenshot"))])));
|
|
15999
|
+
},
|
|
15543
16000
|
writeProjectRecord: async (projectId, expectedVersion, record8) => {
|
|
15544
16001
|
try {
|
|
15545
16002
|
const written = await bridge.updateProjectRecord(projectId, record8, expectedVersion);
|
|
@@ -15577,6 +16034,7 @@ var init_localRefineTurn = __esm({
|
|
|
15577
16034
|
init_agentTurnAdapter();
|
|
15578
16035
|
init_hostedBridge();
|
|
15579
16036
|
init_refineProjectReceipt();
|
|
16037
|
+
init_uploads();
|
|
15580
16038
|
}
|
|
15581
16039
|
});
|
|
15582
16040
|
|
|
@@ -15724,7 +16182,8 @@ var init_projectClaimClient = __esm({
|
|
|
15724
16182
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
15725
16183
|
function localToolDeps(token2, options = {}) {
|
|
15726
16184
|
const base = {
|
|
15727
|
-
|
|
16185
|
+
// #889 — the first render on a machine with no Playwright Chromium downloads it once, here only.
|
|
16186
|
+
renderer: new StripRenderer({ serve: "static", installBrowser: () => installChromium() }),
|
|
15728
16187
|
// Token-backed cloud writes cross the same authority in the control plane. This local handler
|
|
15729
16188
|
// stays permissive to avoid a second billing lookup; tokenless holds fail at persistence.
|
|
15730
16189
|
authorizeCapability: async (request) => ({
|
|
@@ -15914,6 +16373,7 @@ var init_localDeps = __esm({
|
|
|
15914
16373
|
"use strict";
|
|
15915
16374
|
init_env();
|
|
15916
16375
|
init_renderer();
|
|
16376
|
+
init_browserInstall();
|
|
15917
16377
|
init_hostedBridge();
|
|
15918
16378
|
init_localRefineTurn();
|
|
15919
16379
|
init_projectClaimClient();
|
|
@@ -15939,10 +16399,10 @@ __export(localCredentials_exports, {
|
|
|
15939
16399
|
});
|
|
15940
16400
|
import { chmod, mkdir, readFile as readFile5, rename, unlink, writeFile } from "node:fs/promises";
|
|
15941
16401
|
import { homedir } from "node:os";
|
|
15942
|
-
import { dirname, join as
|
|
16402
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
15943
16403
|
function credentialsPath() {
|
|
15944
|
-
const dir = process.env.SHOTOPS_CONFIG_DIR ||
|
|
15945
|
-
return
|
|
16404
|
+
const dir = process.env.SHOTOPS_CONFIG_DIR || join5(homedir(), ".shotops");
|
|
16405
|
+
return join5(dir, "credentials.json");
|
|
15946
16406
|
}
|
|
15947
16407
|
async function readFileJson() {
|
|
15948
16408
|
let raw;
|
|
@@ -15961,7 +16421,7 @@ async function readFileJson() {
|
|
|
15961
16421
|
}
|
|
15962
16422
|
async function writeFileJson(next) {
|
|
15963
16423
|
const target = credentialsPath();
|
|
15964
|
-
await mkdir(
|
|
16424
|
+
await mkdir(dirname2(target), { recursive: true, mode: 448 });
|
|
15965
16425
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
15966
16426
|
await writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
15967
16427
|
`, { mode: 384 });
|
|
@@ -16456,7 +16916,7 @@ var init_releaseCommitments = __esm({
|
|
|
16456
16916
|
import { execFile } from "node:child_process";
|
|
16457
16917
|
import { mkdtemp, open, readFile as readFile6, readdir, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
16458
16918
|
import { tmpdir } from "node:os";
|
|
16459
|
-
import { extname as extname2, join as
|
|
16919
|
+
import { extname as extname2, join as join6, resolve } from "node:path";
|
|
16460
16920
|
import { inflateSync } from "fflate";
|
|
16461
16921
|
async function readAt(path, offset, length) {
|
|
16462
16922
|
const handle = await open(path, "r");
|
|
@@ -16537,8 +16997,8 @@ function plistStringArray(xml, key) {
|
|
|
16537
16997
|
}
|
|
16538
16998
|
async function parsePlist(bytes, source, runner) {
|
|
16539
16999
|
if (bytes.subarray(0, 8).toString() === "bplist00") {
|
|
16540
|
-
const scratch = await mkdtemp(
|
|
16541
|
-
const path =
|
|
17000
|
+
const scratch = await mkdtemp(join6(tmpdir(), "shotops-plist-"));
|
|
17001
|
+
const path = join6(scratch, "Info.plist");
|
|
16542
17002
|
try {
|
|
16543
17003
|
await writeFile2(path, bytes, { mode: 384 });
|
|
16544
17004
|
const json = await runner("plutil", ["-convert", "json", "-o", "-", path]);
|
|
@@ -16596,10 +17056,10 @@ async function findInfoPlists(root, path = root, found = []) {
|
|
|
16596
17056
|
const entries = await readdir(path, { withFileTypes: true });
|
|
16597
17057
|
for (const entry of entries) {
|
|
16598
17058
|
if (entry.isSymbolicLink()) continue;
|
|
16599
|
-
const child =
|
|
17059
|
+
const child = join6(path, entry.name);
|
|
16600
17060
|
if (entry.isDirectory()) {
|
|
16601
17061
|
if (entry.name.endsWith(".app")) {
|
|
16602
|
-
const candidate =
|
|
17062
|
+
const candidate = join6(child, "Contents", "Info.plist");
|
|
16603
17063
|
if ((await stat(candidate).catch(() => null))?.isFile()) found.push(candidate);
|
|
16604
17064
|
}
|
|
16605
17065
|
await findInfoPlists(root, child, found);
|
|
@@ -16614,8 +17074,8 @@ async function inspectPkg(path, runner) {
|
|
|
16614
17074
|
if (!/status:\s*signed|signed by/i.test(signature.toString("utf8")) || /no signature|unsigned/i.test(signature.toString("utf8"))) {
|
|
16615
17075
|
throw new Error("PKG is unsigned or pkgutil could not verify its signature");
|
|
16616
17076
|
}
|
|
16617
|
-
const scratch = await mkdtemp(
|
|
16618
|
-
const expanded =
|
|
17077
|
+
const scratch = await mkdtemp(join6(tmpdir(), "shotops-pkg-"));
|
|
17078
|
+
const expanded = join6(scratch, "expanded");
|
|
16619
17079
|
try {
|
|
16620
17080
|
await runner("pkgutil", ["--expand-full", path, expanded]).catch(() => {
|
|
16621
17081
|
throw new Error("PKG is not a supported flat package (pkgutil --expand-full failed)");
|
|
@@ -16676,7 +17136,7 @@ var init_releaseBuild = __esm({
|
|
|
16676
17136
|
|
|
16677
17137
|
// src/release/desiredReleaseSchema.ts
|
|
16678
17138
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
16679
|
-
import { join as
|
|
17139
|
+
import { join as join7 } from "node:path";
|
|
16680
17140
|
function blobPath(digestValue) {
|
|
16681
17141
|
if (!SHA256.test(digestValue)) throw new Error(`not a SHA-256 digest: ${digestValue}`);
|
|
16682
17142
|
return `blobs/${digestValue.slice(0, 2)}/${digestValue}`;
|
|
@@ -17272,7 +17732,7 @@ function parseCanonicalJson(text3, path) {
|
|
|
17272
17732
|
return value;
|
|
17273
17733
|
}
|
|
17274
17734
|
async function readDesiredReleaseManifest(workspace) {
|
|
17275
|
-
const path =
|
|
17735
|
+
const path = join7(workspace, DESIRED_RELEASE_FILENAME);
|
|
17276
17736
|
let text3;
|
|
17277
17737
|
try {
|
|
17278
17738
|
text3 = await readFile7(path, "utf8");
|
|
@@ -17305,7 +17765,7 @@ function inspectDesiredReleaseValue(value) {
|
|
|
17305
17765
|
};
|
|
17306
17766
|
}
|
|
17307
17767
|
async function inspectDesiredReleaseWorkspace(workspace) {
|
|
17308
|
-
const path =
|
|
17768
|
+
const path = join7(workspace, DESIRED_RELEASE_FILENAME);
|
|
17309
17769
|
let text3;
|
|
17310
17770
|
try {
|
|
17311
17771
|
text3 = await readFile7(path, "utf8");
|
|
@@ -17430,7 +17890,7 @@ var init_desiredReleaseSchema = __esm({
|
|
|
17430
17890
|
|
|
17431
17891
|
// src/release/releaseRepository.ts
|
|
17432
17892
|
import { lstat as lstat2, readFile as readFile8, readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
17433
|
-
import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as
|
|
17893
|
+
import { basename as basename2, extname as extname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve2 } from "node:path";
|
|
17434
17894
|
async function readSmallText(path, label) {
|
|
17435
17895
|
const { size } = await regularFile(path, label);
|
|
17436
17896
|
if (size > MAX_TEXT_BYTES) throw new Error(`${label} exceeds ${MAX_TEXT_BYTES} bytes: ${path}`);
|
|
@@ -17453,11 +17913,11 @@ async function readFieldDirectory(root, path, allowed, label) {
|
|
|
17453
17913
|
const entries = await readdir2(path, { withFileTypes: true });
|
|
17454
17914
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17455
17915
|
if (!entry.isFile() || entry.isSymbolicLink() || extname3(entry.name) !== ".txt") {
|
|
17456
|
-
throw new Error(`unknown ${label} input: ${sourcePath(root,
|
|
17916
|
+
throw new Error(`unknown ${label} input: ${sourcePath(root, join8(path, entry.name))}`);
|
|
17457
17917
|
}
|
|
17458
17918
|
const key = basename2(entry.name, ".txt");
|
|
17459
|
-
if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root,
|
|
17460
|
-
fields2[key] = await textValue(root,
|
|
17919
|
+
if (!allowed.has(key)) throw new Error(`unknown ${label} field: ${sourcePath(root, join8(path, entry.name))}`);
|
|
17920
|
+
fields2[key] = await textValue(root, join8(path, entry.name), `${label} field`);
|
|
17461
17921
|
}
|
|
17462
17922
|
return fields2;
|
|
17463
17923
|
}
|
|
@@ -17521,13 +17981,13 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
|
|
|
17521
17981
|
}
|
|
17522
17982
|
for (const localeEntry of localeEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17523
17983
|
if (!localeEntry.isDirectory() || localeEntry.isSymbolicLink() || !allLocales.includes(localeEntry.name)) {
|
|
17524
|
-
throw new Error(`unknown app preview locale/input: ${sourcePath(root,
|
|
17984
|
+
throw new Error(`unknown app preview locale/input: ${sourcePath(root, join8(previewRoot, localeEntry.name))}`);
|
|
17525
17985
|
}
|
|
17526
17986
|
const locale2 = localeEntry.name;
|
|
17527
|
-
const files = await readdir2(
|
|
17987
|
+
const files = await readdir2(join8(previewRoot, locale2), { withFileTypes: true });
|
|
17528
17988
|
const positions = /* @__PURE__ */ new Map();
|
|
17529
17989
|
for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17530
|
-
const path =
|
|
17990
|
+
const path = join8(previewRoot, locale2, file.name);
|
|
17531
17991
|
if (!file.isFile() || file.isSymbolicLink() || !PREVIEW_EXTENSIONS.has(extname3(file.name).toLowerCase())) {
|
|
17532
17992
|
throw new Error(`unknown app preview input: ${sourcePath(root, path)}`);
|
|
17533
17993
|
}
|
|
@@ -17551,7 +18011,7 @@ async function readAppPreviews(root, previewRoot, allLocales, selectedLocales, a
|
|
|
17551
18011
|
return result;
|
|
17552
18012
|
}
|
|
17553
18013
|
async function readReviewAttachment(root, metadataRoot) {
|
|
17554
|
-
const pointer =
|
|
18014
|
+
const pointer = join8(metadataRoot, "app_review_attachment_file.txt");
|
|
17555
18015
|
let value;
|
|
17556
18016
|
try {
|
|
17557
18017
|
value = await readSmallText(pointer, "app review attachment pointer");
|
|
@@ -17572,7 +18032,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17572
18032
|
const root = resolve2(repository);
|
|
17573
18033
|
const info = await stat2(root).catch(() => null);
|
|
17574
18034
|
if (!info?.isDirectory()) throw new Error(`repository does not exist or is not a directory: ${repository}`);
|
|
17575
|
-
const metadataRoot =
|
|
18035
|
+
const metadataRoot = join8(root, "fastlane", "metadata");
|
|
17576
18036
|
const localized = [];
|
|
17577
18037
|
const app = {};
|
|
17578
18038
|
let review = {};
|
|
@@ -17590,7 +18050,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17590
18050
|
if (error.code !== "ENOENT") throw error;
|
|
17591
18051
|
}
|
|
17592
18052
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17593
|
-
const path =
|
|
18053
|
+
const path = join8(metadataRoot, entry.name);
|
|
17594
18054
|
if (entry.isSymbolicLink()) throw new Error(`metadata input must not be a symbolic link: ${sourcePath(root, path)}`);
|
|
17595
18055
|
if (entry.isDirectory()) {
|
|
17596
18056
|
if (entry.name === "review_information") review = await readFieldDirectory(root, path, REVIEW_METADATA, "review information");
|
|
@@ -17624,7 +18084,7 @@ async function normalizeRepositoryReleaseInputs(repository, allLocales, selected
|
|
|
17624
18084
|
compliance,
|
|
17625
18085
|
appPreviews: await readAppPreviews(
|
|
17626
18086
|
root,
|
|
17627
|
-
|
|
18087
|
+
join8(root, "fastlane", "app-previews"),
|
|
17628
18088
|
allLocales,
|
|
17629
18089
|
new Set(selectedLocales),
|
|
17630
18090
|
allOutputs,
|
|
@@ -17759,7 +18219,7 @@ var init_releaseRepository = __esm({
|
|
|
17759
18219
|
import { createReadStream as createReadStream2, createWriteStream } from "node:fs";
|
|
17760
18220
|
import { chmod as chmod2, lstat as lstat3, mkdir as mkdir2, mkdtemp as mkdtemp2, open as open2, readdir as readdir3, rename as rename2, rm as rm2, stat as stat3 } from "node:fs/promises";
|
|
17761
18221
|
import { writeFile as writeFile3 } from "node:fs/promises";
|
|
17762
|
-
import { dirname as
|
|
18222
|
+
import { dirname as dirname3, join as join9, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
17763
18223
|
import { pipeline } from "node:stream/promises";
|
|
17764
18224
|
async function secureDirectory(path) {
|
|
17765
18225
|
await mkdir2(path, { recursive: true, mode: 448 });
|
|
@@ -17816,9 +18276,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17816
18276
|
await assertConverged(target, manifestText, existing.isDirectory() && !existing.isSymbolicLink());
|
|
17817
18277
|
return { reused: true };
|
|
17818
18278
|
}
|
|
17819
|
-
const parent =
|
|
18279
|
+
const parent = dirname3(target);
|
|
17820
18280
|
await secureDirectory(parent);
|
|
17821
|
-
const temporary = await mkdtemp2(
|
|
18281
|
+
const temporary = await mkdtemp2(join9(parent, `${TEMPORARY_WORKSPACE_PREFIX}${process.pid}-`));
|
|
17822
18282
|
if (process.platform !== "win32") await chmod2(temporary, 448);
|
|
17823
18283
|
let renamed = false;
|
|
17824
18284
|
try {
|
|
@@ -17826,9 +18286,9 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17826
18286
|
for (const [path, artifacts] of expectedBlobs(manifest)) {
|
|
17827
18287
|
const content = contents.get(path);
|
|
17828
18288
|
if (!content) throw new Error(`sealed ${describe(artifacts)} has no bytes to write: ${path}`);
|
|
17829
|
-
const file =
|
|
17830
|
-
await secureDirectory(
|
|
17831
|
-
directories.add(
|
|
18289
|
+
const file = join9(temporary, ...path.split("/"));
|
|
18290
|
+
await secureDirectory(dirname3(file));
|
|
18291
|
+
directories.add(dirname3(file));
|
|
17832
18292
|
await copyBlob(content, file);
|
|
17833
18293
|
if (process.platform !== "win32") await chmod2(file, 384);
|
|
17834
18294
|
const written = await stat3(file);
|
|
@@ -17841,7 +18301,7 @@ async function sealReleaseWorkspace(destination, manifest, contents, hooks = {})
|
|
|
17841
18301
|
await fsyncPath(file, false);
|
|
17842
18302
|
await boundary("blob", path);
|
|
17843
18303
|
}
|
|
17844
|
-
const manifestPath =
|
|
18304
|
+
const manifestPath = join9(temporary, DESIRED_RELEASE_FILENAME);
|
|
17845
18305
|
await writeFile3(manifestPath, manifestText, { mode: 384 });
|
|
17846
18306
|
if (process.platform !== "win32") await chmod2(manifestPath, 384);
|
|
17847
18307
|
await fsyncPath(manifestPath, false);
|
|
@@ -17878,7 +18338,7 @@ async function assertConverged(target, manifestText, isDirectory) {
|
|
|
17878
18338
|
async function walkFiles(root, current, found) {
|
|
17879
18339
|
const entries = await readdir3(current, { withFileTypes: true });
|
|
17880
18340
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
17881
|
-
const path =
|
|
18341
|
+
const path = join9(current, entry.name);
|
|
17882
18342
|
if (entry.isSymbolicLink()) found.push(relative3(root, path).split(sep2).join("/"));
|
|
17883
18343
|
else if (entry.isDirectory()) await walkFiles(root, path, found);
|
|
17884
18344
|
else found.push(relative3(root, path).split(sep2).join("/"));
|
|
@@ -17889,7 +18349,7 @@ async function verifyReleaseWorkspace(path) {
|
|
|
17889
18349
|
const { manifest, text: text3 } = await readDesiredReleaseManifest(root);
|
|
17890
18350
|
const expected = expectedBlobs(manifest);
|
|
17891
18351
|
for (const [blob, artifacts] of expected) {
|
|
17892
|
-
const file =
|
|
18352
|
+
const file = join9(root, ...blob.split("/"));
|
|
17893
18353
|
const stats = await lstat3(file).catch(() => null);
|
|
17894
18354
|
if (!stats || stats.isSymbolicLink() || !stats.isFile()) {
|
|
17895
18355
|
throw new Error(`sealed ${describe(artifacts)} is missing or unsafe: ${blob}`);
|
|
@@ -17905,7 +18365,7 @@ async function verifyReleaseWorkspace(path) {
|
|
|
17905
18365
|
return { path: root, manifest, text: text3, unexpected };
|
|
17906
18366
|
}
|
|
17907
18367
|
function sealedBlobFile(workspace, digest4) {
|
|
17908
|
-
return
|
|
18368
|
+
return join9(resolve3(workspace), ...blobPath(digest4).split("/"));
|
|
17909
18369
|
}
|
|
17910
18370
|
async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
17911
18371
|
const base = resolve3(root);
|
|
@@ -17920,7 +18380,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
|
17920
18380
|
throw error;
|
|
17921
18381
|
}
|
|
17922
18382
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
17923
|
-
const path =
|
|
18383
|
+
const path = join9(base, entry.name);
|
|
17924
18384
|
if (!entry.name.startsWith(TEMPORARY_WORKSPACE_PREFIX)) {
|
|
17925
18385
|
result.kept += 1;
|
|
17926
18386
|
continue;
|
|
@@ -17930,7 +18390,7 @@ async function collectReleaseWorkspaceGarbage(root, options = {}) {
|
|
|
17930
18390
|
result.skipped.push({ path, reason: "not a real directory" });
|
|
17931
18391
|
continue;
|
|
17932
18392
|
}
|
|
17933
|
-
const complete = await lstat3(
|
|
18393
|
+
const complete = await lstat3(join9(path, DESIRED_RELEASE_FILENAME)).then((file) => file.isFile()).catch(() => false);
|
|
17934
18394
|
if (complete) {
|
|
17935
18395
|
result.skipped.push({ path, reason: "holds a complete desired release" });
|
|
17936
18396
|
continue;
|
|
@@ -17963,7 +18423,7 @@ var init_releaseWorkspace = __esm({
|
|
|
17963
18423
|
|
|
17964
18424
|
// src/release/releaseManifest.ts
|
|
17965
18425
|
import { homedir as homedir2 } from "node:os";
|
|
17966
|
-
import { isAbsolute as isAbsolute4, join as
|
|
18426
|
+
import { isAbsolute as isAbsolute4, join as join10, resolve as resolve4 } from "node:path";
|
|
17967
18427
|
function chosenAxis(all, explicit, label) {
|
|
17968
18428
|
const uniqueAll = Array.from(new Set(all.map((item) => item.trim()).filter(Boolean)));
|
|
17969
18429
|
if (uniqueAll.length === 0) throw new Error(`project has no authored ${label}`);
|
|
@@ -17997,7 +18457,7 @@ function resolveBundleId(explicit, project2, build) {
|
|
|
17997
18457
|
return expected;
|
|
17998
18458
|
}
|
|
17999
18459
|
function releaseWorkspaceRoot() {
|
|
18000
|
-
return
|
|
18460
|
+
return join10(process.env.SHOTOPS_CONFIG_DIR || join10(homedir2(), ".shotops"), "release-workspaces");
|
|
18001
18461
|
}
|
|
18002
18462
|
function releaseCopyWords(inputs, locale2) {
|
|
18003
18463
|
const entry = inputs.localized.find((candidate) => candidate.locale === locale2);
|
|
@@ -18019,7 +18479,7 @@ async function assembleDesiredRelease(input) {
|
|
|
18019
18479
|
const localeScope = chosenAxis(project2.locales, input.locales, "locales");
|
|
18020
18480
|
const outputScope = chosenAxis(project2.outputs.map((output2) => output2.id), input.outputs, "outputs");
|
|
18021
18481
|
const selectedOutputs2 = outputScope.selected.map((id) => project2.outputs.find((output2) => output2.id === id));
|
|
18022
|
-
const buildPath = input.build ? isAbsolute4(input.build) ? input.build :
|
|
18482
|
+
const buildPath = input.build ? isAbsolute4(input.build) ? input.build : join10(repository, input.build) : null;
|
|
18023
18483
|
const build = buildPath ? await inspectBuildArtifact(buildPath, repository) : null;
|
|
18024
18484
|
if (build && (isAbsolute4(build.source) || build.source.startsWith(".."))) {
|
|
18025
18485
|
throw new Error(`build must live inside the repository so the desired release records no absolute path: ${build.source}`);
|
|
@@ -18131,7 +18591,7 @@ async function assembleDesiredRelease(input) {
|
|
|
18131
18591
|
artifacts.push({
|
|
18132
18592
|
role: "app_preview",
|
|
18133
18593
|
custody: "sealed",
|
|
18134
|
-
path: seal(preview2.sha256, { file:
|
|
18594
|
+
path: seal(preview2.sha256, { file: join10(repository, ...preview2.source.split("/")) }),
|
|
18135
18595
|
sha256: preview2.sha256,
|
|
18136
18596
|
bytes: preview2.bytes,
|
|
18137
18597
|
mediaType: mediaTypeForPath(preview2.source),
|
|
@@ -18149,7 +18609,7 @@ async function assembleDesiredRelease(input) {
|
|
|
18149
18609
|
role: "review_attachment",
|
|
18150
18610
|
custody: "sealed",
|
|
18151
18611
|
path: seal(repositoryInputs.reviewAttachment.sha256, {
|
|
18152
|
-
file:
|
|
18612
|
+
file: join10(repository, ...repositoryInputs.reviewAttachment.source.split("/"))
|
|
18153
18613
|
}),
|
|
18154
18614
|
sha256: repositoryInputs.reviewAttachment.sha256,
|
|
18155
18615
|
bytes: repositoryInputs.reviewAttachment.bytes,
|
|
@@ -18166,11 +18626,11 @@ async function assembleDesiredRelease(input) {
|
|
|
18166
18626
|
const ccat = repositoryInputs.compliance?.export_compliance_ccat_file;
|
|
18167
18627
|
if (typeof ccat === "string" && ccat.trim()) {
|
|
18168
18628
|
if (isAbsolute4(ccat)) throw new Error("export_compliance_ccat_file must be repository-relative");
|
|
18169
|
-
const document = await referencedFile(repository,
|
|
18629
|
+
const document = await referencedFile(repository, join10(repository, ccat));
|
|
18170
18630
|
artifacts.push({
|
|
18171
18631
|
role: "compliance_file",
|
|
18172
18632
|
custody: "sealed",
|
|
18173
|
-
path: seal(document.sha256, { file:
|
|
18633
|
+
path: seal(document.sha256, { file: join10(repository, ...document.source.split("/")) }),
|
|
18174
18634
|
sha256: document.sha256,
|
|
18175
18635
|
bytes: document.bytes,
|
|
18176
18636
|
mediaType: mediaTypeForPath(document.source),
|
|
@@ -18272,7 +18732,7 @@ async function assembleDesiredRelease(input) {
|
|
|
18272
18732
|
};
|
|
18273
18733
|
const desiredId = computeDesiredId(payload);
|
|
18274
18734
|
const manifest = parseDesiredReleaseManifest({ ...payload, desiredId });
|
|
18275
|
-
const destination = resolve4(input.destination ||
|
|
18735
|
+
const destination = resolve4(input.destination || join10(releaseWorkspaceRoot(), desiredId));
|
|
18276
18736
|
const { reused } = await sealReleaseWorkspace(destination, manifest, sealed, input.seal ?? {});
|
|
18277
18737
|
if (reused) await verifyReleaseWorkspace(destination);
|
|
18278
18738
|
return { path: destination, manifest, reused };
|
|
@@ -19068,7 +19528,7 @@ var init_releaseOperations = __esm({
|
|
|
19068
19528
|
// src/release/releaseReviewArtifact.ts
|
|
19069
19529
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
19070
19530
|
import { lstat as lstat4, mkdir as mkdir3, readFile as readFile9, rename as rename3, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
|
|
19071
|
-
import { basename as basename3, dirname as
|
|
19531
|
+
import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute5, join as join11, resolve as resolve5 } from "node:path";
|
|
19072
19532
|
import { strFromU8, strToU8 as strToU82, unzipSync, zipSync as zipSync2 } from "fflate";
|
|
19073
19533
|
function isRecord5(value) {
|
|
19074
19534
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -19167,7 +19627,7 @@ function screenshotReferences(plan) {
|
|
|
19167
19627
|
}
|
|
19168
19628
|
async function readScreenshot(root, reference) {
|
|
19169
19629
|
if (isAbsolute5(reference.source)) throw new Error(`release screenshot path must be workspace-relative: ${reference.source}`);
|
|
19170
|
-
const path =
|
|
19630
|
+
const path = join11(root, ...blobPath(reference.sha256).split("/"));
|
|
19171
19631
|
const info = await lstat4(path).catch(() => null);
|
|
19172
19632
|
if (!info?.isFile() || info.isSymbolicLink() || info.size !== reference.bytes) {
|
|
19173
19633
|
throw new Error(`release screenshot is missing or changed: ${reference.source}`);
|
|
@@ -19180,8 +19640,8 @@ async function readScreenshot(root, reference) {
|
|
|
19180
19640
|
return bytes;
|
|
19181
19641
|
}
|
|
19182
19642
|
async function atomicPrivateWrite(path, bytes) {
|
|
19183
|
-
await mkdir3(
|
|
19184
|
-
const temporary =
|
|
19643
|
+
await mkdir3(dirname4(path), { recursive: true, mode: 448 });
|
|
19644
|
+
const temporary = join11(dirname4(path), `.${basename3(path)}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`);
|
|
19185
19645
|
try {
|
|
19186
19646
|
await writeFile4(temporary, bytes, { mode: 384 });
|
|
19187
19647
|
await rename3(temporary, path);
|
|
@@ -19307,16 +19767,16 @@ async function loadArtifact(path, now) {
|
|
|
19307
19767
|
async function loadPlan(path, now) {
|
|
19308
19768
|
const plan = await readCanonicalPlan(path);
|
|
19309
19769
|
const screenshots2 = /* @__PURE__ */ new Map();
|
|
19310
|
-
for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(
|
|
19770
|
+
for (const reference of screenshotReferences(plan)) screenshots2.set(reference.sha256, await readScreenshot(dirname4(path), reference));
|
|
19311
19771
|
return { plan, screenshots: screenshots2, sourcePath: path, portable: false, approval: releasePlanApproval(plan, now) };
|
|
19312
19772
|
}
|
|
19313
19773
|
async function loadReleaseReviewSource(source, now = /* @__PURE__ */ new Date()) {
|
|
19314
19774
|
let path = resolve5(source);
|
|
19315
19775
|
const info = await lstat4(path).catch(() => null);
|
|
19316
19776
|
if (info?.isDirectory()) {
|
|
19317
|
-
const artifact =
|
|
19777
|
+
const artifact = join11(path, "release-review.zip");
|
|
19318
19778
|
const directory = path;
|
|
19319
|
-
path = await lstat4(artifact).then(() => artifact).catch(() =>
|
|
19779
|
+
path = await lstat4(artifact).then(() => artifact).catch(() => join11(directory, "release-plan.json"));
|
|
19320
19780
|
if (!await lstat4(path).catch(() => null)) {
|
|
19321
19781
|
const inspected = await inspectDesiredReleaseWorkspace(directory).catch(() => null);
|
|
19322
19782
|
if (inspected) {
|
|
@@ -19352,7 +19812,7 @@ var init_releaseReviewArtifact = __esm({
|
|
|
19352
19812
|
// src/release/releaseJournal.ts
|
|
19353
19813
|
import { chmod as chmod3, mkdir as mkdir4, open as open3, readdir as readdir4, readFile as readFile10, rename as rename4, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
19354
19814
|
import { hostname } from "node:os";
|
|
19355
|
-
import { join as
|
|
19815
|
+
import { join as join12, resolve as resolve6 } from "node:path";
|
|
19356
19816
|
function assertDetail(detail) {
|
|
19357
19817
|
if (typeof detail !== "object" || detail === null || Array.isArray(detail)) {
|
|
19358
19818
|
throw new ReleaseJournalError("corrupt", "journal detail is not an object");
|
|
@@ -19533,8 +19993,8 @@ async function openReleaseAttemptJournal(options) {
|
|
|
19533
19993
|
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
19534
19994
|
if (process.platform !== "win32") await chmod3(directory, 448).catch(() => {
|
|
19535
19995
|
});
|
|
19536
|
-
const path =
|
|
19537
|
-
const releaseLock = await acquireAttemptLock(
|
|
19996
|
+
const path = join12(directory, journalFileName(options.attemptId));
|
|
19997
|
+
const releaseLock = await acquireAttemptLock(join12(directory, lockFileName(options.attemptId)), now);
|
|
19538
19998
|
let entries;
|
|
19539
19999
|
let checksum;
|
|
19540
20000
|
try {
|
|
@@ -19619,7 +20079,7 @@ async function readLiveAttemptJournals(directory) {
|
|
|
19619
20079
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
19620
20080
|
const match = entry.isFile() ? JOURNAL_FILE.exec(entry.name) : null;
|
|
19621
20081
|
if (!match) continue;
|
|
19622
|
-
const path =
|
|
20082
|
+
const path = join12(directory, entry.name);
|
|
19623
20083
|
const text3 = await readFile10(path, "utf8").catch(() => null);
|
|
19624
20084
|
if (text3 === null) continue;
|
|
19625
20085
|
const header = safeJson(text3.split("\n", 1)[0]);
|
|
@@ -19633,7 +20093,7 @@ async function readLiveAttemptJournals(directory) {
|
|
|
19633
20093
|
return found;
|
|
19634
20094
|
}
|
|
19635
20095
|
function attemptDirectoryOf(workspace) {
|
|
19636
|
-
return
|
|
20096
|
+
return join12(resolve6(workspace), ATTEMPT_DIRECTORY_NAME);
|
|
19637
20097
|
}
|
|
19638
20098
|
async function holdsLiveAttemptJournal(candidate) {
|
|
19639
20099
|
const own = await readLiveAttemptJournals(attemptDirectoryOf(candidate));
|
|
@@ -19941,7 +20401,7 @@ var init_releaseReceipt = __esm({
|
|
|
19941
20401
|
|
|
19942
20402
|
// src/release/releaseCommitBuild.ts
|
|
19943
20403
|
import { chmod as chmod4, link, copyFile, mkdtemp as mkdtemp3, rm as rm3 } from "node:fs/promises";
|
|
19944
|
-
import { basename as basename4, dirname as
|
|
20404
|
+
import { basename as basename4, dirname as dirname5, join as join13 } from "node:path";
|
|
19945
20405
|
function buildEventsOf(entries, attemptId) {
|
|
19946
20406
|
const events = [];
|
|
19947
20407
|
for (const entry of entries) {
|
|
@@ -20095,10 +20555,10 @@ function journalVouchesFor(events, approved, evidence) {
|
|
|
20095
20555
|
return namedRemoteIds.size === 0 || namedRemoteIds.size === 1 && namedRemoteIds.has(evidence.remoteBuildId);
|
|
20096
20556
|
}
|
|
20097
20557
|
async function stageApprovedBytes(approved, workspace) {
|
|
20098
|
-
const dir = await mkdtemp3(
|
|
20558
|
+
const dir = await mkdtemp3(join13(dirname5(workspace), TRANSFER_STAGING_PREFIX));
|
|
20099
20559
|
if (process.platform !== "win32") await chmod4(dir, 448);
|
|
20100
20560
|
const name = approved.fileName.toLowerCase().endsWith(`.${approved.format}`) ? approved.fileName : `${approved.fileName}.${approved.format}`;
|
|
20101
|
-
const file =
|
|
20561
|
+
const file = join13(dir, name);
|
|
20102
20562
|
try {
|
|
20103
20563
|
await link(approved.blobFile, file);
|
|
20104
20564
|
} catch {
|
|
@@ -20256,7 +20716,7 @@ async function commitReleaseBuild(input, deps) {
|
|
|
20256
20716
|
const output2 = await deps.runTransporter(
|
|
20257
20717
|
input.transporterPath,
|
|
20258
20718
|
["-m", "upload", "-assetFile", staged.file, "-apiKey", input.setup.keyId, "-apiIssuer", input.setup.issuerId],
|
|
20259
|
-
{ ...process.env, API_PRIVATE_KEYS_DIR:
|
|
20719
|
+
{ ...process.env, API_PRIVATE_KEYS_DIR: dirname5(input.setup.keyPath) }
|
|
20260
20720
|
);
|
|
20261
20721
|
answer = readTransporterAnswer(output2);
|
|
20262
20722
|
} catch {
|
|
@@ -20429,7 +20889,7 @@ __export(releaseRecovery_exports, {
|
|
|
20429
20889
|
});
|
|
20430
20890
|
import { readFile as readFile11, readdir as readdir5, rename as rename5, stat as stat4 } from "node:fs/promises";
|
|
20431
20891
|
import { hostname as hostname2 } from "node:os";
|
|
20432
|
-
import { join as
|
|
20892
|
+
import { join as join14, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
|
|
20433
20893
|
function normalizedNow(now) {
|
|
20434
20894
|
const value = now().toISOString();
|
|
20435
20895
|
if (new Date(value).toISOString() !== value) throw new ReleaseRecoveryError("recovery clock did not return a normalized timestamp");
|
|
@@ -20489,7 +20949,7 @@ function processIsAlive2(pid) {
|
|
|
20489
20949
|
}
|
|
20490
20950
|
}
|
|
20491
20951
|
async function lockState(root, attemptId) {
|
|
20492
|
-
const text3 = await readFile11(
|
|
20952
|
+
const text3 = await readFile11(join14(root, `attempt-${attemptId}.lock`), "utf8").catch((error) => {
|
|
20493
20953
|
if (error.code === "ENOENT") return null;
|
|
20494
20954
|
throw error;
|
|
20495
20955
|
});
|
|
@@ -20513,7 +20973,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20513
20973
|
const workspacePath = resolve7(input.workspace);
|
|
20514
20974
|
const info = await stat4(workspacePath).catch(() => null);
|
|
20515
20975
|
if (!info?.isDirectory()) throw new ReleaseRecoveryError("the release workspace does not exist");
|
|
20516
|
-
const planPath = resolve7(input.planPath ??
|
|
20976
|
+
const planPath = resolve7(input.planPath ?? join14(workspacePath, PLAN_FILENAME));
|
|
20517
20977
|
const plan = await readPlan(planPath);
|
|
20518
20978
|
const attemptRoot = resolve7(input.attemptRoot ?? attemptDirectoryOf(workspacePath));
|
|
20519
20979
|
const diagnostics = [];
|
|
@@ -20532,8 +20992,8 @@ async function inspectLocalEvidence(input) {
|
|
|
20532
20992
|
let finalReceipt = null;
|
|
20533
20993
|
let partialReceipt = null;
|
|
20534
20994
|
let receiptState = "none";
|
|
20535
|
-
const finalPath =
|
|
20536
|
-
const partialPath =
|
|
20995
|
+
const finalPath = join14(attemptRoot, RECEIPT_FILENAME);
|
|
20996
|
+
const partialPath = join14(attemptRoot, PARTIAL_RECEIPT_FILENAME);
|
|
20537
20997
|
const final = await readJsonEvidence(finalPath);
|
|
20538
20998
|
if (final.exists) {
|
|
20539
20999
|
const location = safeEvidenceLocation(workspacePath, finalPath);
|
|
@@ -20567,7 +21027,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20567
21027
|
for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
20568
21028
|
const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
|
|
20569
21029
|
if (!match) continue;
|
|
20570
|
-
const path =
|
|
21030
|
+
const path = join14(attemptRoot, entry.name);
|
|
20571
21031
|
const location = safeEvidenceLocation(workspacePath, path);
|
|
20572
21032
|
if (location) evidenceLocations.push(location);
|
|
20573
21033
|
const attemptId2 = match[1];
|
|
@@ -20604,7 +21064,7 @@ async function inspectLocalEvidence(input) {
|
|
|
20604
21064
|
diagnostics.push("attempt_locked");
|
|
20605
21065
|
}
|
|
20606
21066
|
}
|
|
20607
|
-
const backup =
|
|
21067
|
+
const backup = join14(attemptRoot, "screenshot-backup");
|
|
20608
21068
|
if ((await stat4(backup).catch(() => null))?.isDirectory()) {
|
|
20609
21069
|
const location = safeEvidenceLocation(workspacePath, backup);
|
|
20610
21070
|
if (location) evidenceLocations.push(location);
|
|
@@ -20640,17 +21100,17 @@ function attemptIdOf(value) {
|
|
|
20640
21100
|
}
|
|
20641
21101
|
async function inspectForeignAttempt(attemptRoot, planId) {
|
|
20642
21102
|
const foreign = (value) => value !== null && typeof value.planId === "string" && SHA2565.test(value.planId) && value.planId !== planId;
|
|
20643
|
-
const final = await readLooseRecord(
|
|
21103
|
+
const final = await readLooseRecord(join14(attemptRoot, RECEIPT_FILENAME));
|
|
20644
21104
|
if (foreign(final)) {
|
|
20645
21105
|
return { attemptId: attemptIdOf(final.attemptId), planId: final.planId, finished: final.state === "completed" };
|
|
20646
21106
|
}
|
|
20647
|
-
const partial = await readLooseRecord(
|
|
21107
|
+
const partial = await readLooseRecord(join14(attemptRoot, PARTIAL_RECEIPT_FILENAME));
|
|
20648
21108
|
if (foreign(partial)) return { attemptId: attemptIdOf(partial.attemptId), planId: partial.planId, finished: false };
|
|
20649
21109
|
const directory = await readdir5(attemptRoot, { withFileTypes: true }).catch(() => []);
|
|
20650
21110
|
for (const entry of directory.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
20651
21111
|
const match = entry.isFile() ? JOURNAL_FILE2.exec(entry.name) : null;
|
|
20652
21112
|
if (!match) continue;
|
|
20653
|
-
const text3 = await readFile11(
|
|
21113
|
+
const text3 = await readFile11(join14(attemptRoot, entry.name), "utf8").catch(() => "");
|
|
20654
21114
|
let header = null;
|
|
20655
21115
|
try {
|
|
20656
21116
|
header = JSON.parse(text3.split("\n")[0] ?? "");
|
|
@@ -22427,7 +22887,7 @@ var init_releaseCommitListing = __esm({
|
|
|
22427
22887
|
// src/release/releaseCommitScreenshots.ts
|
|
22428
22888
|
import { createHash as createHash6 } from "node:crypto";
|
|
22429
22889
|
import { chmod as chmod5, lstat as lstat5, mkdir as mkdir5, mkdtemp as mkdtemp4, open as open4, readFile as readFile12, rename as rename6, rm as rm4, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
|
|
22430
|
-
import { dirname as
|
|
22890
|
+
import { dirname as dirname6, join as join15, resolve as resolve8 } from "node:path";
|
|
22431
22891
|
function fail4(message) {
|
|
22432
22892
|
throw new Error(message);
|
|
22433
22893
|
}
|
|
@@ -22591,7 +23051,7 @@ function parseScreenshotBackup(value) {
|
|
|
22591
23051
|
async function verifyBackup(root, manifest) {
|
|
22592
23052
|
for (const set of manifest.sets) {
|
|
22593
23053
|
for (const image of set.images) {
|
|
22594
|
-
const file =
|
|
23054
|
+
const file = join15(root, ...image.path.split("/"));
|
|
22595
23055
|
const info = await lstat5(file).catch(() => null);
|
|
22596
23056
|
if (!info?.isFile() || info.isSymbolicLink()) fail4(`the recovery backup is missing an image for ${set.locale}/${set.deviceSet}`);
|
|
22597
23057
|
if (info.size !== image.bytes) fail4(`a recovery backup image for ${set.locale}/${set.deviceSet} changed size`);
|
|
@@ -22600,7 +23060,7 @@ async function verifyBackup(root, manifest) {
|
|
|
22600
23060
|
}
|
|
22601
23061
|
}
|
|
22602
23062
|
async function readExistingBackup(root) {
|
|
22603
|
-
const text3 = await readFile12(
|
|
23063
|
+
const text3 = await readFile12(join15(root, BACKUP_FILENAME), "utf8").catch(() => null);
|
|
22604
23064
|
if (text3 === null) return null;
|
|
22605
23065
|
let value;
|
|
22606
23066
|
try {
|
|
@@ -22614,23 +23074,23 @@ async function readExistingBackup(root) {
|
|
|
22614
23074
|
}
|
|
22615
23075
|
async function sealBackup(destination, manifest, blobs) {
|
|
22616
23076
|
const target = resolve8(destination);
|
|
22617
|
-
const parent =
|
|
23077
|
+
const parent = dirname6(target);
|
|
22618
23078
|
await secureDirectory2(parent);
|
|
22619
|
-
const temporary = await mkdtemp4(
|
|
23079
|
+
const temporary = await mkdtemp4(join15(parent, `.shotops-screenshot-backup-${process.pid}-`));
|
|
22620
23080
|
let renamed = false;
|
|
22621
23081
|
try {
|
|
22622
23082
|
const directories = /* @__PURE__ */ new Set();
|
|
22623
23083
|
for (const [path, bytes] of blobs) {
|
|
22624
|
-
const file =
|
|
22625
|
-
await secureDirectory2(
|
|
22626
|
-
directories.add(
|
|
23084
|
+
const file = join15(temporary, ...path.split("/"));
|
|
23085
|
+
await secureDirectory2(dirname6(file));
|
|
23086
|
+
directories.add(dirname6(file));
|
|
22627
23087
|
await writeFile6(file, bytes, { mode: 384 });
|
|
22628
23088
|
const written = await stat5(file);
|
|
22629
23089
|
if (written.size !== bytes.byteLength) fail4("a recovery backup image changed size while it was being written");
|
|
22630
23090
|
if (await sha256File(file) !== sha256(bytes)) fail4("a recovery backup image changed content while it was being written");
|
|
22631
23091
|
await fsyncPath2(file, false);
|
|
22632
23092
|
}
|
|
22633
|
-
const manifestPath =
|
|
23093
|
+
const manifestPath = join15(temporary, BACKUP_FILENAME);
|
|
22634
23094
|
await writeFile6(manifestPath, canonicalJson(manifest), { mode: 384 });
|
|
22635
23095
|
await fsyncPath2(manifestPath, false);
|
|
22636
23096
|
for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
|
|
@@ -23190,7 +23650,7 @@ __export(releasePlan_exports, {
|
|
|
23190
23650
|
import { createHash as createHash7 } from "node:crypto";
|
|
23191
23651
|
import { createReadStream as createReadStream3 } from "node:fs";
|
|
23192
23652
|
import { chmod as chmod6, lstat as lstat6, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
|
|
23193
|
-
import { basename as basename5, dirname as
|
|
23653
|
+
import { basename as basename5, dirname as dirname7, join as join16, resolve as resolve9 } from "node:path";
|
|
23194
23654
|
async function md5File(path) {
|
|
23195
23655
|
const hash = createHash7("md5");
|
|
23196
23656
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -23827,7 +24287,7 @@ function envelopeFor2(plan) {
|
|
|
23827
24287
|
};
|
|
23828
24288
|
}
|
|
23829
24289
|
async function atomicPrivateWrite2(path, value) {
|
|
23830
|
-
const temporary =
|
|
24290
|
+
const temporary = join16(dirname7(path), `.${basename5(path)}.${process.pid}.tmp`);
|
|
23831
24291
|
try {
|
|
23832
24292
|
await writeFile7(temporary, canonicalJson(value), { mode: 384 });
|
|
23833
24293
|
if (process.platform !== "win32") await chmod6(temporary, 384);
|
|
@@ -23839,9 +24299,9 @@ async function atomicPrivateWrite2(path, value) {
|
|
|
23839
24299
|
}
|
|
23840
24300
|
async function createReleasePlan(input, deps = {}) {
|
|
23841
24301
|
const workspace = resolve9(input.workspace);
|
|
23842
|
-
const output2 = resolve9(input.output ??
|
|
23843
|
-
const envelopePath =
|
|
23844
|
-
const artifactPath =
|
|
24302
|
+
const output2 = resolve9(input.output ?? join16(workspace, "release-plan.json"));
|
|
24303
|
+
const envelopePath = join16(dirname7(output2), "release-plan-envelope.json");
|
|
24304
|
+
const artifactPath = join16(dirname7(output2), "release-review.zip");
|
|
23845
24305
|
const loaded = await verifyReleaseWorkspace(workspace);
|
|
23846
24306
|
const assets = await desiredAssets(loaded.manifest, workspace);
|
|
23847
24307
|
const setup = await (deps.readConfig ?? readReleaseConfig)();
|
|
@@ -24405,13 +24865,13 @@ __export(releaseCommit_exports, {
|
|
|
24405
24865
|
validateReleaseCommitReceipt: () => validateReleaseCommitReceipt
|
|
24406
24866
|
});
|
|
24407
24867
|
import { readFile as readFile13 } from "node:fs/promises";
|
|
24408
|
-
import { join as
|
|
24868
|
+
import { join as join17, resolve as resolve10 } from "node:path";
|
|
24409
24869
|
async function commitApprovedRelease(input, deps = {}) {
|
|
24410
24870
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
24411
24871
|
const startedAt = now().toISOString();
|
|
24412
24872
|
const attemptRoot = resolve10(input.attemptRoot ?? attemptDirectoryOf(input.workspace));
|
|
24413
|
-
const receiptPath =
|
|
24414
|
-
const planId = await loosePlanIdAt(input.planPath ??
|
|
24873
|
+
const receiptPath = join17(attemptRoot, RECEIPT_FILENAME2);
|
|
24874
|
+
const planId = await loosePlanIdAt(input.planPath ?? join17(resolve10(input.workspace), PLAN_FILENAME2));
|
|
24415
24875
|
let foreign = planId ? await inspectForeignAttempt(attemptRoot, planId) : null;
|
|
24416
24876
|
if (foreign?.finished) {
|
|
24417
24877
|
const archived = await archiveForeignAttempt(attemptRoot, foreign, now);
|
|
@@ -24486,7 +24946,7 @@ async function commitApprovedRelease(input, deps = {}) {
|
|
|
24486
24946
|
durable = isReceipt(persisted, plan) ? persisted : receipt;
|
|
24487
24947
|
await authority.finalize(plan, attempt.attemptId, durable);
|
|
24488
24948
|
} else {
|
|
24489
|
-
await writePartialReceipt(
|
|
24949
|
+
await writePartialReceipt(join17(attemptRoot, PARTIAL_RECEIPT_FILENAME2), receipt);
|
|
24490
24950
|
}
|
|
24491
24951
|
await journal.record({
|
|
24492
24952
|
stage: "receipt",
|
|
@@ -24572,7 +25032,7 @@ async function runPreflight(input, deps, recovery) {
|
|
|
24572
25032
|
} catch (error) {
|
|
24573
25033
|
refuse2("workspace_corrupt", error.message);
|
|
24574
25034
|
}
|
|
24575
|
-
const plan = await readApprovedPlan(input.planPath ??
|
|
25035
|
+
const plan = await readApprovedPlan(input.planPath ?? join17(workspacePath, PLAN_FILENAME2));
|
|
24576
25036
|
if (plan.desired.desiredId !== workspace.manifest.desiredId) {
|
|
24577
25037
|
refuse2("plan_mismatch", "this plan was built from a different desired release than the workspace holds");
|
|
24578
25038
|
}
|
|
@@ -24740,7 +25200,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
|
|
|
24740
25200
|
{
|
|
24741
25201
|
plan,
|
|
24742
25202
|
workspace: preflight.workspacePath,
|
|
24743
|
-
backup:
|
|
25203
|
+
backup: join17(preflight.attemptRoot, SCREENSHOT_BACKUP_DIRNAME)
|
|
24744
25204
|
},
|
|
24745
25205
|
{
|
|
24746
25206
|
token: preflight.appleToken,
|
|
@@ -24794,7 +25254,7 @@ async function runStages(preflight, attemptId, journal, signal, deps, now) {
|
|
|
24794
25254
|
}
|
|
24795
25255
|
function readSealedPayload(workspacePath) {
|
|
24796
25256
|
return async (reference) => {
|
|
24797
|
-
const bytes = await readFile13(
|
|
25257
|
+
const bytes = await readFile13(join17(workspacePath, ...reference.source.split("/")));
|
|
24798
25258
|
if (bytes.byteLength !== reference.bytes || sha256(bytes) !== reference.sha256) {
|
|
24799
25259
|
throw new Error("a sealed payload changed since this plan was approved");
|
|
24800
25260
|
}
|
|
@@ -24913,8 +25373,8 @@ ${receipt.remainingOwnerAction}` : ""
|
|
|
24913
25373
|
}
|
|
24914
25374
|
async function readReleaseCommitStatus(workspace, attemptRoot) {
|
|
24915
25375
|
const root = resolve10(attemptRoot ?? attemptDirectoryOf(workspace));
|
|
24916
|
-
const final = await readPersistedReceipt(
|
|
24917
|
-
const partial = isReceipt(final) ? null : await readPersistedReceipt(
|
|
25376
|
+
const final = await readPersistedReceipt(join17(root, RECEIPT_FILENAME2));
|
|
25377
|
+
const partial = isReceipt(final) ? null : await readPersistedReceipt(join17(root, PARTIAL_RECEIPT_FILENAME2));
|
|
24918
25378
|
const journals = await readLiveAttemptJournals(root);
|
|
24919
25379
|
return {
|
|
24920
25380
|
attemptRoot: root,
|
|
@@ -25285,7 +25745,7 @@ __export(releasePrepare_exports, {
|
|
|
25285
25745
|
runReleasePrepareCommand: () => runReleasePrepareCommand
|
|
25286
25746
|
});
|
|
25287
25747
|
import { readFile as readFile14 } from "node:fs/promises";
|
|
25288
|
-
import { isAbsolute as isAbsolute6, join as
|
|
25748
|
+
import { isAbsolute as isAbsolute6, join as join18, resolve as resolve11 } from "node:path";
|
|
25289
25749
|
function parseGcArgs(argv2) {
|
|
25290
25750
|
const values = /* @__PURE__ */ new Map();
|
|
25291
25751
|
let dryRun = false;
|
|
@@ -25393,7 +25853,7 @@ function parsePrepareArgs(argv2) {
|
|
|
25393
25853
|
project: project2,
|
|
25394
25854
|
repository,
|
|
25395
25855
|
...values.get("--bundle-id") ? { bundleId: values.get("--bundle-id").trim() } : {},
|
|
25396
|
-
...build ? { build: isAbsolute6(build) ? build :
|
|
25856
|
+
...build ? { build: isAbsolute6(build) ? build : join18(repository, build) } : {},
|
|
25397
25857
|
...values.get("--locales") ? { locales: csv(values.get("--locales"), "--locales") } : {},
|
|
25398
25858
|
...values.get("--outputs") ? { outputs: csv(values.get("--outputs"), "--outputs") } : {},
|
|
25399
25859
|
...values.get("--output") ? { destination: resolve11(values.get("--output")) } : {},
|
|
@@ -26400,7 +26860,7 @@ import {
|
|
|
26400
26860
|
writeFile as writeFile8
|
|
26401
26861
|
} from "node:fs/promises";
|
|
26402
26862
|
import { homedir as homedir3 } from "node:os";
|
|
26403
|
-
import { delimiter, dirname as
|
|
26863
|
+
import { delimiter, dirname as dirname8, extname as extname4, join as join19, resolve as resolve13 } from "node:path";
|
|
26404
26864
|
import { stdin, stdout } from "node:process";
|
|
26405
26865
|
import { createInterface } from "node:readline/promises";
|
|
26406
26866
|
function cleanIdentifier(value, label) {
|
|
@@ -26411,7 +26871,7 @@ function cleanIdentifier(value, label) {
|
|
|
26411
26871
|
return result;
|
|
26412
26872
|
}
|
|
26413
26873
|
function expandHome(path) {
|
|
26414
|
-
return path === "~" ? homedir3() : path.startsWith("~/") ?
|
|
26874
|
+
return path === "~" ? homedir3() : path.startsWith("~/") ? join19(homedir3(), path.slice(2)) : path;
|
|
26415
26875
|
}
|
|
26416
26876
|
async function validatePrivateKeyPath(keyPath) {
|
|
26417
26877
|
const requested = resolve13(expandHome(keyPath.trim()));
|
|
@@ -26446,7 +26906,7 @@ async function findExecutable(name, env = process.env) {
|
|
|
26446
26906
|
const extensions = process.platform === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
26447
26907
|
for (const dir of (env.PATH || "").split(delimiter).filter(Boolean)) {
|
|
26448
26908
|
for (const extension of extensions) {
|
|
26449
|
-
const candidate =
|
|
26909
|
+
const candidate = join19(dir, `${name}${extension}`);
|
|
26450
26910
|
try {
|
|
26451
26911
|
const info = await stat6(candidate);
|
|
26452
26912
|
if (!info.isFile()) continue;
|
|
@@ -26579,8 +27039,8 @@ async function probeAppleAccess(input, fetchImpl = fetch, apiOrigin = APP_STORE_
|
|
|
26579
27039
|
return { teams: [{ id: input.issuerId, apps }] };
|
|
26580
27040
|
}
|
|
26581
27041
|
function releaseConfigPath() {
|
|
26582
|
-
const dir = process.env.SHOTOPS_CONFIG_DIR ||
|
|
26583
|
-
return
|
|
27042
|
+
const dir = process.env.SHOTOPS_CONFIG_DIR || join19(homedir3(), ".shotops");
|
|
27043
|
+
return join19(dir, "release.json");
|
|
26584
27044
|
}
|
|
26585
27045
|
async function readReleaseConfig(target = releaseConfigPath()) {
|
|
26586
27046
|
let parsed;
|
|
@@ -26609,7 +27069,7 @@ async function writeReleaseConfig(input, target = releaseConfigPath()) {
|
|
|
26609
27069
|
keyPath: input.keyPath
|
|
26610
27070
|
}
|
|
26611
27071
|
};
|
|
26612
|
-
const dir =
|
|
27072
|
+
const dir = dirname8(target);
|
|
26613
27073
|
await mkdir6(dir, { recursive: true, mode: 448 });
|
|
26614
27074
|
if (process.platform !== "win32") await chmod7(dir, 448);
|
|
26615
27075
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
@@ -26952,6 +27412,7 @@ wait, cancelled and cloud-only results.
|
|
|
26952
27412
|
});
|
|
26953
27413
|
|
|
26954
27414
|
// src/local.ts
|
|
27415
|
+
import { Console } from "node:console";
|
|
26955
27416
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
26956
27417
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26957
27418
|
|
|
@@ -26978,6 +27439,7 @@ init_screenshotInput();
|
|
|
26978
27439
|
init_uploads();
|
|
26979
27440
|
init_resultContract();
|
|
26980
27441
|
init_renderCredits();
|
|
27442
|
+
init_browserInstall();
|
|
26981
27443
|
init_appstore();
|
|
26982
27444
|
init_productionOperationStart();
|
|
26983
27445
|
function dedupeLocales(list) {
|
|
@@ -27090,7 +27552,13 @@ function bundleProductionSnapshot(input) {
|
|
|
27090
27552
|
const locale2 = input.locales[0] ?? "en-US";
|
|
27091
27553
|
const projectFile = input.loaded?.projectFile;
|
|
27092
27554
|
const variant = projectFile ? projectRenderVariant(projectFile, locale2, input.panelPresetId ?? void 0) : void 0;
|
|
27093
|
-
const
|
|
27555
|
+
const variants = projectFile ? Array.from(new Map(
|
|
27556
|
+
(input.outputs.length > 0 ? input.outputs.map((id) => deviceById(id)?.panelPresetId) : [input.panelPresetId ?? void 0]).flatMap((preset) => input.locales.map((targetLocale) => {
|
|
27557
|
+
const target = projectRenderVariant(projectFile, targetLocale, preset);
|
|
27558
|
+
return [variantKey(target), target];
|
|
27559
|
+
}))
|
|
27560
|
+
).values()) : [];
|
|
27561
|
+
const sources = input.screens ? variants.flatMap((target) => plannedSourceCells(input.screens.plan(target))) : input.panelEntries && input.panelEntries.length > 0 ? input.panelSourceEntries ? input.locales.flatMap(
|
|
27094
27562
|
(targetLocale) => panelSourceCells(
|
|
27095
27563
|
input.panelSourceEntries.map(
|
|
27096
27564
|
(entry) => resolveLocaleVariant(entry, targetLocale)
|
|
@@ -27133,7 +27601,10 @@ function bundleProductionSnapshot(input) {
|
|
|
27133
27601
|
panelPresetId: input.panelPresetId
|
|
27134
27602
|
},
|
|
27135
27603
|
baseLocale: projectFile ? projectBaseLocale2(projectFile) : "en-US",
|
|
27136
|
-
...input.screens ? { gaps:
|
|
27604
|
+
...input.screens ? { gaps: [
|
|
27605
|
+
...repairGaps(input.screens.repairFindings),
|
|
27606
|
+
...projectCopyGaps(projectFile, input.locales, variant?.family ?? BASE_SCREEN_FAMILY)
|
|
27607
|
+
] } : {}
|
|
27137
27608
|
});
|
|
27138
27609
|
if (!resolved.ok) return resolved;
|
|
27139
27610
|
const readiness = requireReadiness({
|
|
@@ -27163,7 +27634,7 @@ async function handleEmitBundle(deps, args, signal) {
|
|
|
27163
27634
|
const scope = openChargeScope(deps);
|
|
27164
27635
|
const attempt = {};
|
|
27165
27636
|
try {
|
|
27166
|
-
return await runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal));
|
|
27637
|
+
return await refuseOnRendererSetupFailure(() => runCharged(scope, () => emitBundle(deps, args, scope, attempt, signal)));
|
|
27167
27638
|
} finally {
|
|
27168
27639
|
finishProductionRender(attempt.access);
|
|
27169
27640
|
}
|
|
@@ -27638,7 +28109,8 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
27638
28109
|
unexpandedSavedNote(projectSizeCovered),
|
|
27639
28110
|
projectScreenUsageNote(result.shots ?? []),
|
|
27640
28111
|
projectStylingIgnoredNote(args),
|
|
27641
|
-
result.layoutNote
|
|
28112
|
+
result.layoutNote,
|
|
28113
|
+
result.setupNote
|
|
27642
28114
|
);
|
|
27643
28115
|
} else {
|
|
27644
28116
|
if (!screenshotSlots || screenshotSlots.length === 0) {
|
|
@@ -27733,7 +28205,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
|
|
|
27733
28205
|
previewPng = result.stripBase64 ? Buffer.from(result.stripBase64, "base64") : null;
|
|
27734
28206
|
projectRecord = result.projectRecord;
|
|
27735
28207
|
layout = result.layout;
|
|
27736
|
-
note = joinNotes(note, result.layoutNote);
|
|
28208
|
+
note = joinNotes(note, result.layoutNote, result.setupNote);
|
|
27737
28209
|
}
|
|
27738
28210
|
const declaredLocales = collectVariantLocales([...(screenshotSlots ?? []).flat(), ...args.panels ?? []]);
|
|
27739
28211
|
if (declaredLocales.length > 0) {
|
|
@@ -27857,6 +28329,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27857
28329
|
const projectShots = [];
|
|
27858
28330
|
let layout;
|
|
27859
28331
|
let layoutNote;
|
|
28332
|
+
let setupNote;
|
|
27860
28333
|
for (const output2 of outputs) {
|
|
27861
28334
|
const localeSlots = [];
|
|
27862
28335
|
for (const locale2 of locales) {
|
|
@@ -27881,6 +28354,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27881
28354
|
}))
|
|
27882
28355
|
});
|
|
27883
28356
|
if (!previewPng && result.stripBase64) previewPng = Buffer.from(result.stripBase64, "base64");
|
|
28357
|
+
setupNote ??= result.setupNote;
|
|
27884
28358
|
if (!layout && result.layout) {
|
|
27885
28359
|
layout = result.layout;
|
|
27886
28360
|
const scope2 = outputs.length > 1 || locales.length > 1 ? ` (measured on ${output2.label} / ${locale2})` : "";
|
|
@@ -27952,6 +28426,7 @@ async function emitMultiOutputBundle(deps, args, outputs, locales, projectName2,
|
|
|
27952
28426
|
projectScreenUsageNote(projectShots),
|
|
27953
28427
|
outputNote,
|
|
27954
28428
|
layoutNote,
|
|
28429
|
+
setupNote,
|
|
27955
28430
|
resolutionGapNote(plan.snapshot)
|
|
27956
28431
|
);
|
|
27957
28432
|
return textResult({
|
|
@@ -28013,7 +28488,9 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
28013
28488
|
const projectShots = [];
|
|
28014
28489
|
let layout;
|
|
28015
28490
|
let layoutNote;
|
|
28491
|
+
let setupNote;
|
|
28016
28492
|
const noteLayout = (result, loc) => {
|
|
28493
|
+
setupNote ??= result.setupNote;
|
|
28017
28494
|
if (layout || !result.layout) return;
|
|
28018
28495
|
layout = result.layout;
|
|
28019
28496
|
const scope2 = locales.length > 1 ? ` (measured on ${loc})` : "";
|
|
@@ -28119,7 +28596,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
|
|
|
28119
28596
|
});
|
|
28120
28597
|
if (!measuredReady.ok) return measuredReady.result;
|
|
28121
28598
|
const zip = buildMultiLocaleFastlaneBundleZip(localeSlots, { bundleId: args.bundleId, panelLabel });
|
|
28122
|
-
note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote);
|
|
28599
|
+
note = joinNotes(note, projectScreenUsageNote(projectShots), layoutNote, setupNote);
|
|
28123
28600
|
const filename2 = bundleZipFilename(projectName2);
|
|
28124
28601
|
const { mode: outputMode, note: outputNote } = resolveOutputMode(args.output, zip.byteLength, deps.forceOutput);
|
|
28125
28602
|
const storedZip = outputMode === "urls" ? await storeAsset(
|
|
@@ -28972,7 +29449,7 @@ async function handleDeleteAssets(deps, args) {
|
|
|
28972
29449
|
// src/runtime/widgets.ts
|
|
28973
29450
|
init_packagePaths();
|
|
28974
29451
|
import { readFileSync } from "node:fs";
|
|
28975
|
-
import { join as
|
|
29452
|
+
import { join as join3 } from "node:path";
|
|
28976
29453
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
28977
29454
|
var PANELS_WIDGET_URI = "ui://shotops/panels/v4.html";
|
|
28978
29455
|
var PANELS_WIDGET_URIS = [
|
|
@@ -28999,7 +29476,7 @@ function readBundle(name) {
|
|
|
28999
29476
|
if (cached !== void 0) return cached;
|
|
29000
29477
|
let bundle;
|
|
29001
29478
|
try {
|
|
29002
|
-
bundle = readFileSync(
|
|
29479
|
+
bundle = readFileSync(join3(WIDGET_DIST, name), "utf8");
|
|
29003
29480
|
} catch {
|
|
29004
29481
|
bundle = null;
|
|
29005
29482
|
}
|
|
@@ -29130,7 +29607,7 @@ function registerTools(server, deps) {
|
|
|
29130
29607
|
"render_strip",
|
|
29131
29608
|
{
|
|
29132
29609
|
title: "Render App Store screenshots",
|
|
29133
|
-
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
|
|
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 phoneHeight/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.',
|
|
29134
29611
|
inputSchema: renderStripAdvertisedShape,
|
|
29135
29612
|
outputSchema: renderStripToolOutputSchema,
|
|
29136
29613
|
// ADDITIVE, not destructive: this can mint private generated assets but cannot change a
|
|
@@ -29279,7 +29756,7 @@ function registerTools(server, deps) {
|
|
|
29279
29756
|
"describe_look",
|
|
29280
29757
|
{
|
|
29281
29758
|
title: "Design presets & style catalog",
|
|
29282
|
-
description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
|
|
29759
|
+
description: "Decide what the strip should LOOK like before rendering it. Returns five named starting-point presets (each bundling a background, device body, caption colour and typeface that agree with each other), a short two-question intake script for asking the user their vibe and their headlines, and the full styling field catalog + defaults (device/camera, background, captions, App Store panel sizes). CALL THIS BEFORE the first render of a new strip whenever the user has not said how it should look \u2014 guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. It is also the one complete copy of that material: the connect-time server instructions are truncated by most clients, so anything you need about PALETTE (presets: noir, daylight, pop, clay, studio \u2014 each bundling background + device body + caption colour + typeface so they agree) or COMPOSITION (layouts: standard, bleed, top-bleed, lean-right, lean-left, swipe, corner-bleed \u2014 how many headline lines are reserved and where the device sits under them) is here and nowhere else in full. The two are independent axes and you usually want one of each: a preset never pins placement, a layout never pins colour. SKIP this call when the user already said what they want, when the project has a saved look (read_look / useSavedLook), or when you are iterating on a strip you already rendered.",
|
|
29283
29760
|
inputSchema: describeLookShape,
|
|
29284
29761
|
outputSchema: describeLookOutputSchema,
|
|
29285
29762
|
annotations: READ_ONLY,
|
|
@@ -29705,7 +30182,11 @@ function tokenFromArgv(argv2) {
|
|
|
29705
30182
|
return idx !== -1 ? argv2[idx + 1] : void 0;
|
|
29706
30183
|
}
|
|
29707
30184
|
var LOCAL_INTAKE = 'request_screenshot_upload is hosted-only; import_screenshot accepts { "path": "/abs/shot.png" } entries here (its { "url" } / { "file" } sources need a hosted account). Either way you can pass local files straight to render_strip / emit_bundle as { "path": "..." } screenshot entries.';
|
|
30185
|
+
function silenceStdoutConsole() {
|
|
30186
|
+
globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
|
|
30187
|
+
}
|
|
29708
30188
|
async function startStdioServer(token2) {
|
|
30189
|
+
silenceStdoutConsole();
|
|
29709
30190
|
const deps = localToolDeps(token2);
|
|
29710
30191
|
const localInstructions = token2 ? `${SERVER_INSTRUCTIONS}
|
|
29711
30192
|
|