@buoy-gg/mcp 7.0.30 → 7.0.32
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 +13 -1
- package/dist/{chunk-POUWWZXB.mjs → chunk-HWYVQNMR.mjs} +883 -203
- package/dist/cli.js +915 -235
- package/dist/cli.mjs +1 -1
- package/dist/index.js +915 -235
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -613,7 +613,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
613
613
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
614
614
|
|
|
615
615
|
// src/version.ts
|
|
616
|
-
var VERSION = "7.0.
|
|
616
|
+
var VERSION = "7.0.32" ? "7.0.32" : "0.0.0-dev";
|
|
617
617
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
618
618
|
|
|
619
619
|
// src/update/checkForUpdate.ts
|
|
@@ -1350,7 +1350,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1350
1350
|
let sawActive = false;
|
|
1351
1351
|
let idleStreak = 0;
|
|
1352
1352
|
let lastCount = -1;
|
|
1353
|
-
let
|
|
1353
|
+
let resolved2 = null;
|
|
1354
1354
|
while (Date.now() - start < cap3) {
|
|
1355
1355
|
let snap2;
|
|
1356
1356
|
try {
|
|
@@ -1362,7 +1362,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1362
1362
|
const completed = snap2.live?.automationCompleted;
|
|
1363
1363
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1364
1364
|
if (completed?.phase === "done") {
|
|
1365
|
-
|
|
1365
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1366
1366
|
break;
|
|
1367
1367
|
}
|
|
1368
1368
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1372,11 +1372,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1372
1372
|
const count = runCountFor(snap2.index, newest);
|
|
1373
1373
|
if (count === lastCount) {
|
|
1374
1374
|
if (sawActive && ++idleStreak >= 3) {
|
|
1375
|
-
|
|
1375
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1376
1376
|
break;
|
|
1377
1377
|
}
|
|
1378
1378
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1379
|
-
|
|
1379
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1380
1380
|
break;
|
|
1381
1381
|
}
|
|
1382
1382
|
} else {
|
|
@@ -1388,11 +1388,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1388
1388
|
}
|
|
1389
1389
|
await delay2(POLL_INTERVAL_MS);
|
|
1390
1390
|
}
|
|
1391
|
-
if (!
|
|
1391
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1392
1392
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1393
1393
|
});
|
|
1394
1394
|
const snap = await readPerfSnapshot(sync, target);
|
|
1395
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1395
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1396
1396
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1397
1397
|
});
|
|
1398
1398
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -2359,9 +2359,9 @@ function reasonMessage(res, query) {
|
|
|
2359
2359
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
2360
2360
|
}
|
|
2361
2361
|
}
|
|
2362
|
-
function candidatesText(
|
|
2363
|
-
if (!
|
|
2364
|
-
const lines =
|
|
2362
|
+
function candidatesText(candidates2) {
|
|
2363
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
2364
|
+
const lines = candidates2.map((c) => {
|
|
2365
2365
|
const id = c.testID || c.nativeID;
|
|
2366
2366
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
2367
2367
|
});
|
|
@@ -2462,7 +2462,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2462
2462
|
|
|
2463
2463
|
// src/server.ts
|
|
2464
2464
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2465
|
-
import { z as
|
|
2465
|
+
import { z as z24 } from "zod";
|
|
2466
2466
|
|
|
2467
2467
|
// src/tools/packs/network.ts
|
|
2468
2468
|
import { z as z12 } from "zod";
|
|
@@ -2567,8 +2567,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
2567
2567
|
}
|
|
2568
2568
|
function bodyLines(e, fetched) {
|
|
2569
2569
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
2570
|
-
const label = (inline,
|
|
2571
|
-
const value = inline ??
|
|
2570
|
+
const label = (inline, resolved2, omitted) => {
|
|
2571
|
+
const value = inline ?? resolved2;
|
|
2572
2572
|
if (value !== void 0 && value !== null) return truncate2(value);
|
|
2573
2573
|
if (!omitted) return "(none)";
|
|
2574
2574
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -3111,15 +3111,679 @@ ${lines.join("\n")}`);
|
|
|
3111
3111
|
);
|
|
3112
3112
|
}
|
|
3113
3113
|
|
|
3114
|
-
// src/tools/packs/
|
|
3114
|
+
// src/tools/packs/scenarios.ts
|
|
3115
3115
|
import { z as z14 } from "zod";
|
|
3116
|
-
var
|
|
3116
|
+
var SCENARIOS_TOOL_ID = "scenarios";
|
|
3117
3117
|
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3118
3118
|
var fail14 = (message) => ({
|
|
3119
3119
|
isError: true,
|
|
3120
3120
|
content: [{ type: "text", text: message }]
|
|
3121
3121
|
});
|
|
3122
3122
|
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
3123
|
+
function varSpec(v) {
|
|
3124
|
+
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
3125
|
+
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
3126
|
+
return `${v.key}: ${v.type}${opts}${def}`;
|
|
3127
|
+
}
|
|
3128
|
+
function summarize(s) {
|
|
3129
|
+
const vars = s.vars?.length ? `
|
|
3130
|
+
variables: ${s.vars.map(varSpec).join("; ")}` : "";
|
|
3131
|
+
const steps = s.stepLines?.length ? `
|
|
3132
|
+
${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
|
|
3133
|
+
const meta = [
|
|
3134
|
+
`v${s.version}`,
|
|
3135
|
+
s.author,
|
|
3136
|
+
s.usage?.runs ? `${s.usage.runs} run(s)` : null,
|
|
3137
|
+
s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
|
|
3138
|
+
s.unsavedToRepo ? "unsaved to repo" : null
|
|
3139
|
+
].filter(Boolean).join(" \xB7 ");
|
|
3140
|
+
const outcome = s.expectedOutcome ? `
|
|
3141
|
+
expected: ${s.expectedOutcome}` : "";
|
|
3142
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
|
|
3143
|
+
}
|
|
3144
|
+
function renderSection(title, list, folders) {
|
|
3145
|
+
if (!list.length) return `**${title} (0):**
|
|
3146
|
+
_none_`;
|
|
3147
|
+
if (!folders.length) {
|
|
3148
|
+
return `**${title} (${list.length}):**
|
|
3149
|
+
${list.map(summarize).join("\n")}`;
|
|
3150
|
+
}
|
|
3151
|
+
const parts = [`**${title} (${list.length}):**`];
|
|
3152
|
+
for (const folder of folders) {
|
|
3153
|
+
const inIt = list.filter(
|
|
3154
|
+
(s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
|
|
3155
|
+
);
|
|
3156
|
+
if (!inIt.length) continue;
|
|
3157
|
+
parts.push(`_${folder} (${inIt.length})_
|
|
3158
|
+
${inIt.map(summarize).join("\n")}`);
|
|
3159
|
+
}
|
|
3160
|
+
const ungrouped = list.filter((s) => !s.folder);
|
|
3161
|
+
if (ungrouped.length) {
|
|
3162
|
+
parts.push(
|
|
3163
|
+
`_Ungrouped (${ungrouped.length})_
|
|
3164
|
+
${ungrouped.map(summarize).join("\n")}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
return parts.join("\n");
|
|
3168
|
+
}
|
|
3169
|
+
function formatActive(a) {
|
|
3170
|
+
const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
|
|
3171
|
+
return [
|
|
3172
|
+
`**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
|
|
3173
|
+
vars ? `variables: ${vars}` : null,
|
|
3174
|
+
`effects:
|
|
3175
|
+
${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
|
|
3176
|
+
].filter(Boolean).join("\n");
|
|
3177
|
+
}
|
|
3178
|
+
function registerScenariosTools(server, sync) {
|
|
3179
|
+
server.registerTool(
|
|
3180
|
+
"get_scenarios",
|
|
3181
|
+
{
|
|
3182
|
+
description: "List the app's saved Scenarios \u2014 named, parameterized app states that can be applied with one call (override a response, write storage, impersonate a user, navigate). Shows what each one WILL do in plain English with a durability label per step, plus which scenario is currently active and what it changed. Use scenario_action to run one as a deterministic fixture BEFORE driving a flow, so the flow starts from a known state instead of whatever the app happened to be in. If a scenario is active, whatever you observe on the device is partly simulated \u2014 check here first when the app's data looks wrong.",
|
|
3183
|
+
inputSchema: {
|
|
3184
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3185
|
+
}
|
|
3186
|
+
},
|
|
3187
|
+
async (args) => {
|
|
3188
|
+
try {
|
|
3189
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3190
|
+
const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
|
|
3191
|
+
const p = payload ?? {};
|
|
3192
|
+
const code = p.code ?? [];
|
|
3193
|
+
const device = p.device ?? [];
|
|
3194
|
+
const drafts = p.drafts ?? [];
|
|
3195
|
+
const folders = p.folders ?? [];
|
|
3196
|
+
const parts = [];
|
|
3197
|
+
parts.push(
|
|
3198
|
+
p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
|
|
3199
|
+
${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
|
|
3200
|
+
);
|
|
3201
|
+
if (folders.length) {
|
|
3202
|
+
parts.push(
|
|
3203
|
+
`**Folders:** ${folders.join(" \xB7 ")} \u2014 the flows this team organizes by. Scenarios below are grouped under them; file new ones into an existing folder with \`scenario_action setFolder\` rather than inventing a name.`
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3206
|
+
parts.push(renderSection("Shipped with this app", code, folders));
|
|
3207
|
+
parts.push(renderSection("Saved on this device", device, folders));
|
|
3208
|
+
if (drafts.length) {
|
|
3209
|
+
parts.push(
|
|
3210
|
+
`**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
|
|
3211
|
+
${drafts.map(summarize).join("\n")}`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
return text12(parts.join("\n\n"));
|
|
3215
|
+
} catch (e) {
|
|
3216
|
+
return fail14(errMsg14(e));
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
);
|
|
3220
|
+
server.registerTool(
|
|
3221
|
+
"scenario_action",
|
|
3222
|
+
{
|
|
3223
|
+
description: `Drive Buoy Scenarios: 'preview' dry-runs one and reports exactly what it WOULD change (plus any conflict with the active scenario) without touching the app \u2014 do this before running an unfamiliar scenario; 'run' applies it and resolves only once every step has landed, which makes it a deterministic fixture to call before run_flow; 'deactivate' reverses it and reports what it could NOT restore (storage writes have no automatic inverse); 'save' writes a new scenario onto the device \u2014 it lands as a DRAFT that a human must accept on the device before it can run, so tell the user to go accept it; 'setFolder' files a saved scenario under a flow ("Checkout", "Login") or clears its folder, which is how a QA library stays navigable \u2014 check get_scenarios for the folders already in use and reuse one instead of inventing a near-duplicate; 'delete' removes a device scenario. Prefer a network override step over a query-cache poke: the override survives refetch and restart, the poke dies on the next fetch.`,
|
|
3224
|
+
inputSchema: {
|
|
3225
|
+
action: z14.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3226
|
+
folder: z14.string().optional().describe(
|
|
3227
|
+
"Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."
|
|
3228
|
+
),
|
|
3229
|
+
id: z14.string().optional().describe(
|
|
3230
|
+
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3231
|
+
),
|
|
3232
|
+
params: z14.record(z14.union([z14.string(), z14.number(), z14.boolean()])).optional().describe(
|
|
3233
|
+
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3234
|
+
),
|
|
3235
|
+
scenario: z14.object({
|
|
3236
|
+
id: z14.string().describe("Stable kebab-case id."),
|
|
3237
|
+
name: z14.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3238
|
+
description: z14.string().optional(),
|
|
3239
|
+
expectedOutcome: z14.string().optional().describe(
|
|
3240
|
+
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3241
|
+
),
|
|
3242
|
+
author: z14.string().optional(),
|
|
3243
|
+
authoredAgainst: z14.string().optional().describe("App version this was written against."),
|
|
3244
|
+
folder: z14.string().optional().describe(
|
|
3245
|
+
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3246
|
+
),
|
|
3247
|
+
tags: z14.array(z14.string()).optional(),
|
|
3248
|
+
vars: z14.array(
|
|
3249
|
+
z14.object({
|
|
3250
|
+
key: z14.string(),
|
|
3251
|
+
label: z14.string().optional(),
|
|
3252
|
+
type: z14.enum(["string", "number", "boolean", "enum"]),
|
|
3253
|
+
options: z14.array(z14.union([z14.string(), z14.number(), z14.boolean()])).optional(),
|
|
3254
|
+
default: z14.union([z14.string(), z14.number(), z14.boolean()]).optional()
|
|
3255
|
+
})
|
|
3256
|
+
).optional().describe(
|
|
3257
|
+
"Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."
|
|
3258
|
+
),
|
|
3259
|
+
steps: z14.array(
|
|
3260
|
+
z14.object({
|
|
3261
|
+
id: z14.string().optional(),
|
|
3262
|
+
tool: z14.string().describe(
|
|
3263
|
+
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3264
|
+
),
|
|
3265
|
+
action: z14.string().describe(
|
|
3266
|
+
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3267
|
+
),
|
|
3268
|
+
params: z14.unknown().optional(),
|
|
3269
|
+
label: z14.string().optional()
|
|
3270
|
+
})
|
|
3271
|
+
).describe("Steps, in order."),
|
|
3272
|
+
undoSteps: z14.array(
|
|
3273
|
+
z14.object({
|
|
3274
|
+
id: z14.string().optional(),
|
|
3275
|
+
tool: z14.string(),
|
|
3276
|
+
action: z14.string(),
|
|
3277
|
+
params: z14.unknown().optional(),
|
|
3278
|
+
label: z14.string().optional()
|
|
3279
|
+
})
|
|
3280
|
+
).optional().describe(
|
|
3281
|
+
"How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored."
|
|
3282
|
+
)
|
|
3283
|
+
}).optional().describe("The scenario to save (action 'save')."),
|
|
3284
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3285
|
+
}
|
|
3286
|
+
},
|
|
3287
|
+
async (args) => {
|
|
3288
|
+
try {
|
|
3289
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3290
|
+
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3291
|
+
switch (args.action) {
|
|
3292
|
+
case "preview": {
|
|
3293
|
+
if (!args.id) return fail14("preview requires `id` (from get_scenarios).");
|
|
3294
|
+
const r = await call("preview", {
|
|
3295
|
+
id: args.id,
|
|
3296
|
+
params: args.params ?? {}
|
|
3297
|
+
});
|
|
3298
|
+
const lines = [
|
|
3299
|
+
`**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
|
|
3300
|
+
`variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
|
|
3301
|
+
`**Would do:**
|
|
3302
|
+
${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
3303
|
+
];
|
|
3304
|
+
if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
|
|
3305
|
+
if (r.conflict) {
|
|
3306
|
+
lines.push(
|
|
3307
|
+
`\u26A0\uFE0F **Conflict:** "${r.conflict.activeName}" is active and touches the same target(s): ${r.conflict.overlaps.join(", ")}. Running will SWAP \u2014 it deactivates first.`
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3311
|
+
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3312
|
+
lines.push(
|
|
3313
|
+
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3314
|
+
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3315
|
+
);
|
|
3316
|
+
return text12(lines.join("\n\n"));
|
|
3317
|
+
}
|
|
3318
|
+
case "run": {
|
|
3319
|
+
if (!args.id) return fail14("run requires `id` (from get_scenarios).");
|
|
3320
|
+
const r = await call("run", {
|
|
3321
|
+
id: args.id,
|
|
3322
|
+
params: args.params ?? {}
|
|
3323
|
+
});
|
|
3324
|
+
if (r.preflightErrors?.length) {
|
|
3325
|
+
return fail14(
|
|
3326
|
+
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3327
|
+
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
const steps = (r.steps ?? []).map((s) => {
|
|
3331
|
+
const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
|
|
3332
|
+
const err = s.error ? `
|
|
3333
|
+
${s.error}` : "";
|
|
3334
|
+
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3335
|
+
}).join("\n");
|
|
3336
|
+
if (!r.ok) {
|
|
3337
|
+
return fail14(
|
|
3338
|
+
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3339
|
+
${steps}
|
|
3340
|
+
|
|
3341
|
+
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
return text12(
|
|
3345
|
+
[
|
|
3346
|
+
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3347
|
+
steps,
|
|
3348
|
+
`**Now bent:**
|
|
3349
|
+
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3350
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
|
|
3351
|
+
].join("\n\n")
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
case "deactivate": {
|
|
3355
|
+
const r = await call("deactivate", {});
|
|
3356
|
+
const rep = r.report;
|
|
3357
|
+
const lines = [
|
|
3358
|
+
`Removed ${rep.removedRules} override rule(s)`,
|
|
3359
|
+
rep.stoppedImpersonation ? "Ended impersonation" : null,
|
|
3360
|
+
rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
|
|
3361
|
+
].filter(Boolean);
|
|
3362
|
+
const warn = rep.unrestoredStorageKeys.length ? `
|
|
3363
|
+
|
|
3364
|
+
\u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
|
|
3365
|
+
const errs = rep.errors.length ? `
|
|
3366
|
+
|
|
3367
|
+
Errors: ${rep.errors.join("; ")}` : "";
|
|
3368
|
+
return text12(`**Deactivated.**
|
|
3369
|
+
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3370
|
+
}
|
|
3371
|
+
case "save": {
|
|
3372
|
+
if (!args.scenario) return fail14("save requires `scenario`.");
|
|
3373
|
+
const r = await call("save", { scenario: args.scenario });
|
|
3374
|
+
if (!r.ok) {
|
|
3375
|
+
return fail14(
|
|
3376
|
+
`Save rejected:
|
|
3377
|
+
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
return text12(
|
|
3381
|
+
`Saved \`${r.id}\` **as a draft**.
|
|
3382
|
+
|
|
3383
|
+
It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
case "setFolder": {
|
|
3387
|
+
if (!args.id) return fail14("setFolder requires `id` (from get_scenarios).");
|
|
3388
|
+
const r = await call("setFolder", {
|
|
3389
|
+
id: args.id,
|
|
3390
|
+
folder: args.folder
|
|
3391
|
+
});
|
|
3392
|
+
return text12(
|
|
3393
|
+
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
case "delete": {
|
|
3397
|
+
if (!args.id) return fail14("delete requires `id`.");
|
|
3398
|
+
await call("delete", { id: args.id });
|
|
3399
|
+
return text12(`Deleted \`${args.id}\` from the device library.`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
} catch (e) {
|
|
3403
|
+
return fail14(errMsg14(e));
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/tools/packs/camera.ts
|
|
3410
|
+
import { z as z15 } from "zod";
|
|
3411
|
+
|
|
3412
|
+
// src/capture/camera.ts
|
|
3413
|
+
import { execFile as execFile2 } from "child_process";
|
|
3414
|
+
import { promisify as promisify2 } from "util";
|
|
3415
|
+
import { existsSync, readFileSync } from "fs";
|
|
3416
|
+
import os3 from "os";
|
|
3417
|
+
import path3 from "path";
|
|
3418
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3419
|
+
function candidates() {
|
|
3420
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3421
|
+
const list = [
|
|
3422
|
+
...fromEnv ? [path3.join(fromEnv, "buoycam")] : [],
|
|
3423
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3424
|
+
path3.join(os3.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3425
|
+
];
|
|
3426
|
+
let dir = __dirname;
|
|
3427
|
+
for (let i = 0; i < 8; i++) {
|
|
3428
|
+
list.push(path3.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3429
|
+
const parent = path3.dirname(dir);
|
|
3430
|
+
if (parent === dir) break;
|
|
3431
|
+
dir = parent;
|
|
3432
|
+
}
|
|
3433
|
+
return list;
|
|
3434
|
+
}
|
|
3435
|
+
var resolved = null;
|
|
3436
|
+
function cliPath() {
|
|
3437
|
+
if (resolved) return resolved;
|
|
3438
|
+
resolved = candidates().find((c) => existsSync(c)) ?? null;
|
|
3439
|
+
return resolved;
|
|
3440
|
+
}
|
|
3441
|
+
async function run(args) {
|
|
3442
|
+
const cli = cliPath();
|
|
3443
|
+
if (!cli) {
|
|
3444
|
+
return {
|
|
3445
|
+
ok: false,
|
|
3446
|
+
error: "The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
try {
|
|
3450
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3451
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
const stdout = err.stdout ?? "";
|
|
3454
|
+
try {
|
|
3455
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3456
|
+
} catch {
|
|
3457
|
+
return {
|
|
3458
|
+
ok: false,
|
|
3459
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function describeDiagnosis(checks) {
|
|
3465
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3466
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3467
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3468
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3469
|
+
}
|
|
3470
|
+
const lines = [];
|
|
3471
|
+
for (const c of [...failed, ...warned]) {
|
|
3472
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3473
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3474
|
+
}
|
|
3475
|
+
const headline = failed.length > 0 ? `${failed.length} problem${failed.length === 1 ? "" : "s"} found.` : `No hard failures, but ${warned.length} thing${warned.length === 1 ? "" : "s"} worth knowing.`;
|
|
3476
|
+
return [headline, "", ...lines].join("\n");
|
|
3477
|
+
}
|
|
3478
|
+
function describeDevices(devices, apps) {
|
|
3479
|
+
if (devices.length === 0) {
|
|
3480
|
+
return "No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._";
|
|
3481
|
+
}
|
|
3482
|
+
const lines = [
|
|
3483
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3484
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3485
|
+
];
|
|
3486
|
+
if (devices.length > 1 && apps === null) {
|
|
3487
|
+
lines.push(
|
|
3488
|
+
"",
|
|
3489
|
+
"_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._"
|
|
3490
|
+
);
|
|
3491
|
+
return lines.join("\n");
|
|
3492
|
+
}
|
|
3493
|
+
if (apps === null) {
|
|
3494
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3495
|
+
return lines.join("\n");
|
|
3496
|
+
}
|
|
3497
|
+
if (apps.length === 0) {
|
|
3498
|
+
lines.push("", "No apps installed on it.");
|
|
3499
|
+
return lines.join("\n");
|
|
3500
|
+
}
|
|
3501
|
+
lines.push(
|
|
3502
|
+
"",
|
|
3503
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3504
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3505
|
+
"",
|
|
3506
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3507
|
+
);
|
|
3508
|
+
return lines.join("\n");
|
|
3509
|
+
}
|
|
3510
|
+
function describeInputs(cameras, windows) {
|
|
3511
|
+
const lines = [];
|
|
3512
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3513
|
+
if (cameras.length) {
|
|
3514
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3515
|
+
} else {
|
|
3516
|
+
lines.push(
|
|
3517
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3518
|
+
);
|
|
3519
|
+
}
|
|
3520
|
+
lines.push("");
|
|
3521
|
+
lines.push(
|
|
3522
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3523
|
+
);
|
|
3524
|
+
if (windows.length) {
|
|
3525
|
+
lines.push(
|
|
3526
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3527
|
+
);
|
|
3528
|
+
} else {
|
|
3529
|
+
lines.push(
|
|
3530
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
return lines.join("\n");
|
|
3534
|
+
}
|
|
3535
|
+
function entitlementPaths() {
|
|
3536
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3537
|
+
if (fromEnv) return [fromEnv];
|
|
3538
|
+
return [
|
|
3539
|
+
path3.join(os3.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3540
|
+
path3.join(os3.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3541
|
+
];
|
|
3542
|
+
}
|
|
3543
|
+
function readEntitlement() {
|
|
3544
|
+
for (const p of entitlementPaths()) {
|
|
3545
|
+
try {
|
|
3546
|
+
if (!existsSync(p)) continue;
|
|
3547
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
3548
|
+
return {
|
|
3549
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3550
|
+
isPro: parsed.isPro === true
|
|
3551
|
+
};
|
|
3552
|
+
} catch {
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return { tier: "anonymous", isPro: false };
|
|
3556
|
+
}
|
|
3557
|
+
function proRequired(tool) {
|
|
3558
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3559
|
+
|
|
3560
|
+
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel without a licence. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
|
|
3561
|
+
|
|
3562
|
+
_Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
// src/tools/packs/camera.ts
|
|
3566
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3567
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3568
|
+
var fail15 = (message) => ({
|
|
3569
|
+
isError: true,
|
|
3570
|
+
content: [{ type: "text", text: message }]
|
|
3571
|
+
});
|
|
3572
|
+
function registerCameraTools(server) {
|
|
3573
|
+
server.registerTool(
|
|
3574
|
+
"camera_source",
|
|
3575
|
+
{
|
|
3576
|
+
description: "Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",
|
|
3577
|
+
inputSchema: {
|
|
3578
|
+
kind: z15.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3579
|
+
value: z15.string().optional().describe(
|
|
3580
|
+
"Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."
|
|
3581
|
+
),
|
|
3582
|
+
format: z15.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3583
|
+
}
|
|
3584
|
+
},
|
|
3585
|
+
async (args) => {
|
|
3586
|
+
const blocked = gate("camera_source");
|
|
3587
|
+
if (blocked) return blocked;
|
|
3588
|
+
const cmd = ["source", args.kind];
|
|
3589
|
+
if (args.value) cmd.push(args.value);
|
|
3590
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3591
|
+
const result = await run(cmd);
|
|
3592
|
+
if (!result.ok) {
|
|
3593
|
+
return fail15(
|
|
3594
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3595
|
+
);
|
|
3596
|
+
}
|
|
3597
|
+
const what = args.kind === "barcode" ? `a ${args.format ?? "qr"} code reading "${args.value}"` : args.kind === "screen" ? `this Mac's screen (${args.value ?? "behind the simulator window"})` : args.value ?? args.kind;
|
|
3598
|
+
return text13(
|
|
3599
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3600
|
+
|
|
3601
|
+
_Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
);
|
|
3605
|
+
server.registerTool(
|
|
3606
|
+
"camera_status",
|
|
3607
|
+
{
|
|
3608
|
+
description: "Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",
|
|
3609
|
+
inputSchema: {}
|
|
3610
|
+
},
|
|
3611
|
+
async () => {
|
|
3612
|
+
const blocked = gate("camera_status");
|
|
3613
|
+
if (blocked) return blocked;
|
|
3614
|
+
const result = await run(["status"]);
|
|
3615
|
+
if (!result.ok || !result.data) {
|
|
3616
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3617
|
+
}
|
|
3618
|
+
const { publishing, fps, codes } = result.data;
|
|
3619
|
+
if (!publishing) {
|
|
3620
|
+
return text13(
|
|
3621
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
return text13(
|
|
3625
|
+
[
|
|
3626
|
+
`Publishing at ${fps} fps.`,
|
|
3627
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3628
|
+
"",
|
|
3629
|
+
"_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"
|
|
3630
|
+
].join("\n")
|
|
3631
|
+
);
|
|
3632
|
+
}
|
|
3633
|
+
);
|
|
3634
|
+
server.registerTool(
|
|
3635
|
+
"camera_launch",
|
|
3636
|
+
{
|
|
3637
|
+
description: "Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",
|
|
3638
|
+
inputSchema: {
|
|
3639
|
+
bundleId: z15.string().describe("The app's bundle identifier."),
|
|
3640
|
+
udid: z15.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3641
|
+
}
|
|
3642
|
+
},
|
|
3643
|
+
async (args) => {
|
|
3644
|
+
const blocked = gate("camera_launch");
|
|
3645
|
+
if (blocked) return blocked;
|
|
3646
|
+
const cmd = ["launch", args.bundleId];
|
|
3647
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3648
|
+
const result = await run(cmd);
|
|
3649
|
+
if (!result.ok) {
|
|
3650
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3651
|
+
}
|
|
3652
|
+
return text13(
|
|
3653
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3656
|
+
);
|
|
3657
|
+
server.registerTool(
|
|
3658
|
+
"camera_diagnose",
|
|
3659
|
+
{
|
|
3660
|
+
description: "Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",
|
|
3661
|
+
inputSchema: {
|
|
3662
|
+
bundleId: z15.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3663
|
+
}
|
|
3664
|
+
},
|
|
3665
|
+
async (args) => {
|
|
3666
|
+
const cmd = ["diagnose"];
|
|
3667
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3668
|
+
const result = await run(cmd);
|
|
3669
|
+
if (!result.data?.checks) {
|
|
3670
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3671
|
+
}
|
|
3672
|
+
const ent = readEntitlement();
|
|
3673
|
+
const tierNote = ent.isPro ? "" : "\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";
|
|
3674
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3675
|
+
}
|
|
3676
|
+
);
|
|
3677
|
+
server.registerTool(
|
|
3678
|
+
"camera_stop",
|
|
3679
|
+
{
|
|
3680
|
+
description: "Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",
|
|
3681
|
+
inputSchema: {}
|
|
3682
|
+
},
|
|
3683
|
+
async () => {
|
|
3684
|
+
const blocked = gate("camera_stop");
|
|
3685
|
+
if (blocked) return blocked;
|
|
3686
|
+
const result = await run(["stop"]);
|
|
3687
|
+
if (!result.ok) {
|
|
3688
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3689
|
+
}
|
|
3690
|
+
return text13("The simulator camera is off.");
|
|
3691
|
+
}
|
|
3692
|
+
);
|
|
3693
|
+
server.registerTool(
|
|
3694
|
+
"camera_devices",
|
|
3695
|
+
{
|
|
3696
|
+
description: "List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",
|
|
3697
|
+
inputSchema: {
|
|
3698
|
+
udid: z15.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3699
|
+
}
|
|
3700
|
+
},
|
|
3701
|
+
async (args) => {
|
|
3702
|
+
const blocked = gate("camera_devices");
|
|
3703
|
+
if (blocked) return blocked;
|
|
3704
|
+
const devicesResult = await run(["devices"]);
|
|
3705
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3706
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3707
|
+
}
|
|
3708
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3709
|
+
let apps = null;
|
|
3710
|
+
if (devices.length === 1 || args.udid) {
|
|
3711
|
+
const appsResult = await run([
|
|
3712
|
+
"apps",
|
|
3713
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3714
|
+
]);
|
|
3715
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3716
|
+
}
|
|
3717
|
+
return text13(describeDevices(devices, apps));
|
|
3718
|
+
}
|
|
3719
|
+
);
|
|
3720
|
+
server.registerTool(
|
|
3721
|
+
"camera_inputs",
|
|
3722
|
+
{
|
|
3723
|
+
description: "List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",
|
|
3724
|
+
inputSchema: {}
|
|
3725
|
+
},
|
|
3726
|
+
async () => {
|
|
3727
|
+
const blocked = gate("camera_inputs");
|
|
3728
|
+
if (blocked) return blocked;
|
|
3729
|
+
const [cams, wins] = await Promise.all([
|
|
3730
|
+
run(["cameras"]),
|
|
3731
|
+
run(["windows"])
|
|
3732
|
+
]);
|
|
3733
|
+
if (!cams.ok && !wins.ok) {
|
|
3734
|
+
return fail15(
|
|
3735
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
return text13(
|
|
3739
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3742
|
+
);
|
|
3743
|
+
server.registerTool(
|
|
3744
|
+
"camera_zero_setup",
|
|
3745
|
+
{
|
|
3746
|
+
description: "Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",
|
|
3747
|
+
inputSchema: {
|
|
3748
|
+
action: z15.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3749
|
+
udid: z15.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3750
|
+
}
|
|
3751
|
+
},
|
|
3752
|
+
async (args) => {
|
|
3753
|
+
const blocked = gate("camera_zero_setup");
|
|
3754
|
+
if (blocked) return blocked;
|
|
3755
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3756
|
+
if (!result.ok) {
|
|
3757
|
+
return fail15(
|
|
3758
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
const d = result.data ?? {};
|
|
3762
|
+
if (args.action === "status") {
|
|
3763
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3764
|
+
return text13(
|
|
3765
|
+
d.foreign ? `Zero-setup is on, but **another tool owns it** (\`${d.path}\`). Quit that tool before turning Buoy's on.` : "Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera."
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
if (args.action === "on") {
|
|
3769
|
+
return text13(
|
|
3770
|
+
"Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._"
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
return text13("Zero-setup mode is **off**.");
|
|
3774
|
+
}
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
// src/tools/packs/interact.ts
|
|
3779
|
+
import { z as z16 } from "zod";
|
|
3780
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3781
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3782
|
+
var fail16 = (message) => ({
|
|
3783
|
+
isError: true,
|
|
3784
|
+
content: [{ type: "text", text: message }]
|
|
3785
|
+
});
|
|
3786
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3123
3787
|
function formatElement(e) {
|
|
3124
3788
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3125
3789
|
line += e.name;
|
|
@@ -3142,8 +3806,8 @@ function registerInteractTools(server, sync) {
|
|
|
3142
3806
|
{
|
|
3143
3807
|
description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. If you already know the sequence you want to drive, `run_flow` does the whole thing (taps, waits, assertions) in one call instead. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
|
|
3144
3808
|
inputSchema: {
|
|
3145
|
-
interactiveOnly:
|
|
3146
|
-
deviceId:
|
|
3809
|
+
interactiveOnly: z16.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3810
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3147
3811
|
}
|
|
3148
3812
|
},
|
|
3149
3813
|
async (args) => {
|
|
@@ -3153,16 +3817,16 @@ function registerInteractTools(server, sync) {
|
|
|
3153
3817
|
const all = res?.elements ?? [];
|
|
3154
3818
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3155
3819
|
if (elements.length === 0) {
|
|
3156
|
-
return
|
|
3820
|
+
return text14(
|
|
3157
3821
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3158
3822
|
);
|
|
3159
3823
|
}
|
|
3160
3824
|
const header = `Screen ${res.screen.width}\xD7${res.screen.height} \u2014 ${elements.length} element${elements.length === 1 ? "" : "s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).`;
|
|
3161
|
-
return
|
|
3825
|
+
return text14(`${header}
|
|
3162
3826
|
|
|
3163
3827
|
${elements.map(formatElement).join("\n")}`);
|
|
3164
3828
|
} catch (e) {
|
|
3165
|
-
return
|
|
3829
|
+
return fail16(errMsg15(e));
|
|
3166
3830
|
}
|
|
3167
3831
|
}
|
|
3168
3832
|
);
|
|
@@ -3171,20 +3835,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3171
3835
|
{
|
|
3172
3836
|
description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Driving MORE THAN ONE step (reproducing a bug, verifying a fix)? Use `run_flow` instead \u2014 it runs the whole sequence in one call. Requires Buoy Pro.",
|
|
3173
3837
|
inputSchema: {
|
|
3174
|
-
nativeTag:
|
|
3175
|
-
testID:
|
|
3176
|
-
query:
|
|
3177
|
-
value:
|
|
3178
|
-
text:
|
|
3179
|
-
longPress:
|
|
3180
|
-
scrollIntoView:
|
|
3181
|
-
deviceId:
|
|
3838
|
+
nativeTag: z16.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3839
|
+
testID: z16.string().optional().describe("Exact testID of the element."),
|
|
3840
|
+
query: z16.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3841
|
+
value: z16.union([z16.number(), z16.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
3842
|
+
text: z16.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3843
|
+
longPress: z16.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3844
|
+
scrollIntoView: z16.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3845
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3182
3846
|
}
|
|
3183
3847
|
},
|
|
3184
3848
|
async (args) => {
|
|
3185
3849
|
try {
|
|
3186
3850
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3187
|
-
return
|
|
3851
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3188
3852
|
}
|
|
3189
3853
|
const target = sync.requireProDevice(args.deviceId);
|
|
3190
3854
|
const res = await sync.callAction(
|
|
@@ -3209,26 +3873,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3209
3873
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3210
3874
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3211
3875
|
const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : m.via === "onLongPress" ? `Long-pressed "${label}"${id}` : `Tapped "${label}"${id}`;
|
|
3212
|
-
return
|
|
3876
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3213
3877
|
}
|
|
3214
3878
|
let msg = res?.reason ?? "Tap failed.";
|
|
3215
3879
|
if (res?.candidates?.length) {
|
|
3216
3880
|
msg += "\n\nTappable candidates:\n" + res.candidates.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n");
|
|
3217
3881
|
}
|
|
3218
|
-
return
|
|
3882
|
+
return fail16(msg);
|
|
3219
3883
|
} catch (e) {
|
|
3220
|
-
return
|
|
3884
|
+
return fail16(errMsg15(e));
|
|
3221
3885
|
}
|
|
3222
3886
|
}
|
|
3223
3887
|
);
|
|
3224
3888
|
}
|
|
3225
3889
|
|
|
3226
3890
|
// src/tools/packs/jsThread.ts
|
|
3227
|
-
import { z as
|
|
3891
|
+
import { z as z17 } from "zod";
|
|
3228
3892
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3229
|
-
var
|
|
3230
|
-
var
|
|
3231
|
-
var
|
|
3893
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3894
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3895
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3232
3896
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3233
3897
|
function sparkline(values) {
|
|
3234
3898
|
const max = Math.max(1, ...values);
|
|
@@ -3299,9 +3963,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3299
3963
|
{
|
|
3300
3964
|
description: 'Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. NOT the tool for React components re-rendering too often \u2014 this measures TASKS on the thread, not renders; for "the page re-renders constantly / typing lags / tapping one button re-renders everything" use measure_renders. Requires @buoy-gg/js-top on the device.',
|
|
3301
3965
|
inputSchema: {
|
|
3302
|
-
deviceId:
|
|
3303
|
-
durationMs:
|
|
3304
|
-
clearFirst:
|
|
3966
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3967
|
+
durationMs: z17.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
3968
|
+
clearFirst: z17.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3305
3969
|
}
|
|
3306
3970
|
},
|
|
3307
3971
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3314,7 +3978,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3314
3978
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3315
3979
|
target
|
|
3316
3980
|
);
|
|
3317
|
-
if (!snap) return
|
|
3981
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3318
3982
|
const md = formatTopMarkdown(snap, duration);
|
|
3319
3983
|
const slim = {
|
|
3320
3984
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3333,13 +3997,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3333
3997
|
worstFreezeMs: r.worstFreezeMs
|
|
3334
3998
|
}))
|
|
3335
3999
|
};
|
|
3336
|
-
return
|
|
4000
|
+
return text15(`${md}
|
|
3337
4001
|
|
|
3338
4002
|
\`\`\`json
|
|
3339
4003
|
${JSON.stringify(slim)}
|
|
3340
4004
|
\`\`\``);
|
|
3341
4005
|
} catch (e) {
|
|
3342
|
-
return
|
|
4006
|
+
return fail17(errMsg16(e));
|
|
3343
4007
|
}
|
|
3344
4008
|
}
|
|
3345
4009
|
);
|
|
@@ -3348,8 +4012,8 @@ ${JSON.stringify(slim)}
|
|
|
3348
4012
|
{
|
|
3349
4013
|
description: 'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',
|
|
3350
4014
|
inputSchema: {
|
|
3351
|
-
deviceId:
|
|
3352
|
-
key:
|
|
4015
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4016
|
+
key: z17.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3353
4017
|
}
|
|
3354
4018
|
},
|
|
3355
4019
|
async ({ deviceId, key }) => {
|
|
@@ -3362,18 +4026,18 @@ ${JSON.stringify(slim)}
|
|
|
3362
4026
|
target
|
|
3363
4027
|
);
|
|
3364
4028
|
if (!detail) {
|
|
3365
|
-
return
|
|
4029
|
+
return fail17(
|
|
3366
4030
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3367
4031
|
);
|
|
3368
4032
|
}
|
|
3369
4033
|
const md = formatDetailMarkdown(detail);
|
|
3370
|
-
return
|
|
4034
|
+
return text15(`${md}
|
|
3371
4035
|
|
|
3372
4036
|
\`\`\`json
|
|
3373
4037
|
${JSON.stringify(detail)}
|
|
3374
4038
|
\`\`\``);
|
|
3375
4039
|
} catch (e) {
|
|
3376
|
-
return
|
|
4040
|
+
return fail17(errMsg16(e));
|
|
3377
4041
|
}
|
|
3378
4042
|
}
|
|
3379
4043
|
);
|
|
@@ -3382,27 +4046,27 @@ ${JSON.stringify(detail)}
|
|
|
3382
4046
|
{
|
|
3383
4047
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3384
4048
|
inputSchema: {
|
|
3385
|
-
deviceId:
|
|
4049
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3386
4050
|
}
|
|
3387
4051
|
},
|
|
3388
4052
|
async ({ deviceId }) => {
|
|
3389
4053
|
try {
|
|
3390
4054
|
const target = sync.requireProDevice(deviceId);
|
|
3391
4055
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3392
|
-
return
|
|
4056
|
+
return text15("Cleared js-top stats.");
|
|
3393
4057
|
} catch (e) {
|
|
3394
|
-
return
|
|
4058
|
+
return fail17(errMsg16(e));
|
|
3395
4059
|
}
|
|
3396
4060
|
}
|
|
3397
4061
|
);
|
|
3398
4062
|
}
|
|
3399
4063
|
|
|
3400
4064
|
// src/tools/packs/images.ts
|
|
3401
|
-
import { z as
|
|
4065
|
+
import { z as z18 } from "zod";
|
|
3402
4066
|
var IMAGES_TOOL_ID = "images";
|
|
3403
|
-
var
|
|
3404
|
-
var
|
|
3405
|
-
var
|
|
4067
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4068
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4069
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3406
4070
|
function uriTail(uri) {
|
|
3407
4071
|
if (!uri) return "(no source)";
|
|
3408
4072
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -3490,9 +4154,9 @@ function registerImagesTools(server, sync) {
|
|
|
3490
4154
|
{
|
|
3491
4155
|
description: "Live image registry from the app: every RN <Image> and expo-image load with cache verdict (memory/disk/network), load ms, decoded vs displayed size with an oversize/upscale verdict and wasted-KB estimate, error details, and any active simulation overrides. Image HTTP never hits the JS network stack, so this is the only visibility into image loading. Requires @buoy-gg/images on the device.",
|
|
3492
4156
|
inputSchema: {
|
|
3493
|
-
deviceId:
|
|
3494
|
-
limit:
|
|
3495
|
-
status:
|
|
4157
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4158
|
+
limit: z18.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4159
|
+
status: z18.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3496
4160
|
}
|
|
3497
4161
|
},
|
|
3498
4162
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3505,7 +4169,7 @@ function registerImagesTools(server, sync) {
|
|
|
3505
4169
|
{ limit: capped, status },
|
|
3506
4170
|
target
|
|
3507
4171
|
);
|
|
3508
|
-
if (!list) return
|
|
4172
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3509
4173
|
const md = formatListMarkdown(list, capped);
|
|
3510
4174
|
const slim = {
|
|
3511
4175
|
stats: list.stats,
|
|
@@ -3532,13 +4196,13 @@ function registerImagesTools(server, sync) {
|
|
|
3532
4196
|
layoutShifts: r.layoutShifts
|
|
3533
4197
|
}))
|
|
3534
4198
|
};
|
|
3535
|
-
return
|
|
4199
|
+
return text16(`${md}
|
|
3536
4200
|
|
|
3537
4201
|
\`\`\`json
|
|
3538
4202
|
${JSON.stringify(slim)}
|
|
3539
4203
|
\`\`\``);
|
|
3540
4204
|
} catch (e) {
|
|
3541
|
-
return
|
|
4205
|
+
return fail18(errMsg17(e));
|
|
3542
4206
|
}
|
|
3543
4207
|
}
|
|
3544
4208
|
);
|
|
@@ -3547,9 +4211,9 @@ ${JSON.stringify(slim)}
|
|
|
3547
4211
|
{
|
|
3548
4212
|
description: "Act on one image from get_images (by id): hard_reload (bypass caches + refetch), retry, evict_disk (expo disk entry), flash (red border to locate it on screen), prove_savings (re-encode at displayed size as WebP ON DEVICE and return real byte savings + a previewable file:// uri), or simulation overrides \u2014 force_error (instant offline-safe failure), force_loading (request hangs forever), force_blank, swap_url (requires url), restore (remove override). Returns the record's state after the action so the effect is visible.",
|
|
3549
4213
|
inputSchema: {
|
|
3550
|
-
deviceId:
|
|
3551
|
-
id:
|
|
3552
|
-
action:
|
|
4214
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4215
|
+
id: z18.number().describe("Record id from get_images."),
|
|
4216
|
+
action: z18.enum([
|
|
3553
4217
|
"hard_reload",
|
|
3554
4218
|
"retry",
|
|
3555
4219
|
"evict_disk",
|
|
@@ -3561,7 +4225,7 @@ ${JSON.stringify(slim)}
|
|
|
3561
4225
|
"swap_url",
|
|
3562
4226
|
"restore"
|
|
3563
4227
|
]).describe("What to do."),
|
|
3564
|
-
url:
|
|
4228
|
+
url: z18.string().optional().describe("Replacement URL (swap_url only).")
|
|
3565
4229
|
}
|
|
3566
4230
|
},
|
|
3567
4231
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3569,8 +4233,8 @@ ${JSON.stringify(slim)}
|
|
|
3569
4233
|
const target = sync.requireProDevice(deviceId);
|
|
3570
4234
|
if (action === "prove_savings") {
|
|
3571
4235
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3572
|
-
if (!savings) return
|
|
3573
|
-
if (!savings.ok) return
|
|
4236
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4237
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3574
4238
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3575
4239
|
if (savings.originalBytes != null)
|
|
3576
4240
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3582,11 +4246,11 @@ ${JSON.stringify(slim)}
|
|
|
3582
4246
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3583
4247
|
if (savings.optimizedUri)
|
|
3584
4248
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3585
|
-
return
|
|
4249
|
+
return text16(parts.join("\n"));
|
|
3586
4250
|
}
|
|
3587
4251
|
const entry = ACTION_MAP[action];
|
|
3588
|
-
if (!entry) return
|
|
3589
|
-
if (action === "swap_url" && !url) return
|
|
4252
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4253
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3590
4254
|
const result = await sync.callAction(
|
|
3591
4255
|
IMAGES_TOOL_ID,
|
|
3592
4256
|
entry.action,
|
|
@@ -3606,9 +4270,9 @@ ${JSON.stringify(slim)}
|
|
|
3606
4270
|
);
|
|
3607
4271
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3608
4272
|
}
|
|
3609
|
-
return
|
|
4273
|
+
return text16(lines.join("\n"));
|
|
3610
4274
|
} catch (e) {
|
|
3611
|
-
return
|
|
4275
|
+
return fail18(errMsg17(e));
|
|
3612
4276
|
}
|
|
3613
4277
|
}
|
|
3614
4278
|
);
|
|
@@ -3617,12 +4281,12 @@ ${JSON.stringify(slim)}
|
|
|
3617
4281
|
{
|
|
3618
4282
|
description: "App-wide image simulation modes: network 'offline' (every NETWORK image load fails immediately, both libs \u2014 bundled assets still load; reload/remount on-screen images to see it), 'cold' (every image bypasses caches \u2014 first-launch behavior, nothing cleared), 'normal' to reset. blankImages renders every image without a source. massAction applies per-image simulations to everything mounted. Also clears the registry or expo caches when asked.",
|
|
3619
4283
|
inputSchema: {
|
|
3620
|
-
deviceId:
|
|
3621
|
-
network:
|
|
3622
|
-
blankImages:
|
|
3623
|
-
massAction:
|
|
3624
|
-
clearRegistry:
|
|
3625
|
-
clearExpoCaches:
|
|
4284
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4285
|
+
network: z18.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4286
|
+
blankImages: z18.boolean().optional().describe("Render all images without a source."),
|
|
4287
|
+
massAction: z18.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
4288
|
+
clearRegistry: z18.boolean().optional().describe("Clear the captured image log."),
|
|
4289
|
+
clearExpoCaches: z18.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3626
4290
|
}
|
|
3627
4291
|
},
|
|
3628
4292
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3649,17 +4313,17 @@ ${JSON.stringify(slim)}
|
|
|
3649
4313
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3650
4314
|
done.push(r?.message ?? "expo caches cleared");
|
|
3651
4315
|
}
|
|
3652
|
-
if (done.length === 0) return
|
|
3653
|
-
return
|
|
4316
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4317
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
3654
4318
|
} catch (e) {
|
|
3655
|
-
return
|
|
4319
|
+
return fail18(errMsg17(e));
|
|
3656
4320
|
}
|
|
3657
4321
|
}
|
|
3658
4322
|
);
|
|
3659
4323
|
}
|
|
3660
4324
|
|
|
3661
4325
|
// src/tools/packs/assets.ts
|
|
3662
|
-
import { z as
|
|
4326
|
+
import { z as z19 } from "zod";
|
|
3663
4327
|
|
|
3664
4328
|
// src/tools/packs/assetsGraphNote.ts
|
|
3665
4329
|
function graphUnavailableNote(ss) {
|
|
@@ -3674,9 +4338,9 @@ function graphUnavailableNote(ss) {
|
|
|
3674
4338
|
|
|
3675
4339
|
// src/tools/packs/assets.ts
|
|
3676
4340
|
var ASSETS_TOOL_ID = "assets";
|
|
3677
|
-
var
|
|
3678
|
-
var
|
|
3679
|
-
var
|
|
4341
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4342
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4343
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3680
4344
|
function kb2(bytes) {
|
|
3681
4345
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3682
4346
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -3764,10 +4428,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3764
4428
|
{
|
|
3765
4429
|
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
3766
4430
|
inputSchema: {
|
|
3767
|
-
deviceId:
|
|
3768
|
-
limit:
|
|
3769
|
-
kind:
|
|
3770
|
-
unusedOnly:
|
|
4431
|
+
deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4432
|
+
limit: z19.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4433
|
+
kind: z19.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4434
|
+
unusedOnly: z19.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3771
4435
|
}
|
|
3772
4436
|
},
|
|
3773
4437
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3780,7 +4444,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3780
4444
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3781
4445
|
target
|
|
3782
4446
|
);
|
|
3783
|
-
if (!list) return
|
|
4447
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3784
4448
|
if (unusedOnly) {
|
|
3785
4449
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3786
4450
|
}
|
|
@@ -3804,13 +4468,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3804
4468
|
sizeSource: r.sizeSource
|
|
3805
4469
|
}))
|
|
3806
4470
|
};
|
|
3807
|
-
return
|
|
4471
|
+
return text17(`${md}
|
|
3808
4472
|
|
|
3809
4473
|
\`\`\`json
|
|
3810
4474
|
${JSON.stringify(slim)}
|
|
3811
4475
|
\`\`\``);
|
|
3812
4476
|
} catch (e) {
|
|
3813
|
-
return
|
|
4477
|
+
return fail19(errMsg18(e));
|
|
3814
4478
|
}
|
|
3815
4479
|
}
|
|
3816
4480
|
);
|
|
@@ -3819,9 +4483,9 @@ ${JSON.stringify(slim)}
|
|
|
3819
4483
|
{
|
|
3820
4484
|
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
3821
4485
|
inputSchema: {
|
|
3822
|
-
deviceId:
|
|
3823
|
-
action:
|
|
3824
|
-
id:
|
|
4486
|
+
deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4487
|
+
action: z19.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4488
|
+
id: z19.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3825
4489
|
}
|
|
3826
4490
|
},
|
|
3827
4491
|
async ({ deviceId, action, id }) => {
|
|
@@ -3831,19 +4495,19 @@ ${JSON.stringify(slim)}
|
|
|
3831
4495
|
case "rescan": {
|
|
3832
4496
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3833
4497
|
const ss = result?.scanStatus;
|
|
3834
|
-
if (!ss) return
|
|
3835
|
-
return
|
|
4498
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4499
|
+
return text17(
|
|
3836
4500
|
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
3837
4501
|
);
|
|
3838
4502
|
}
|
|
3839
4503
|
case "measure_sizes": {
|
|
3840
4504
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3841
|
-
return
|
|
4505
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3842
4506
|
}
|
|
3843
4507
|
case "detail": {
|
|
3844
|
-
if (id == null) return
|
|
4508
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
3845
4509
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3846
|
-
if (!detail) return
|
|
4510
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3847
4511
|
const lines = [
|
|
3848
4512
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3849
4513
|
"",
|
|
@@ -3861,32 +4525,32 @@ ${JSON.stringify(slim)}
|
|
|
3861
4525
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3862
4526
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3863
4527
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3864
|
-
return
|
|
4528
|
+
return text17(lines.join("\n"));
|
|
3865
4529
|
}
|
|
3866
4530
|
case "save_baseline": {
|
|
3867
4531
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3868
|
-
return
|
|
4532
|
+
return text17(
|
|
3869
4533
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3870
4534
|
);
|
|
3871
4535
|
}
|
|
3872
4536
|
case "clear_baseline": {
|
|
3873
4537
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3874
|
-
return
|
|
4538
|
+
return text17("Baseline cleared.");
|
|
3875
4539
|
}
|
|
3876
4540
|
case "clear_records": {
|
|
3877
4541
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3878
|
-
return
|
|
4542
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
3879
4543
|
}
|
|
3880
4544
|
}
|
|
3881
4545
|
} catch (e) {
|
|
3882
|
-
return
|
|
4546
|
+
return fail19(errMsg18(e));
|
|
3883
4547
|
}
|
|
3884
4548
|
}
|
|
3885
4549
|
);
|
|
3886
4550
|
}
|
|
3887
4551
|
|
|
3888
4552
|
// src/tools/packs/reload.ts
|
|
3889
|
-
import { z as
|
|
4553
|
+
import { z as z20 } from "zod";
|
|
3890
4554
|
|
|
3891
4555
|
// src/capture/devServerReload.ts
|
|
3892
4556
|
import { WebSocket } from "ws";
|
|
@@ -3949,9 +4613,9 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
3949
4613
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3950
4614
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3951
4615
|
var PROBE_INTERVAL_MS = 400;
|
|
3952
|
-
var
|
|
3953
|
-
var
|
|
3954
|
-
var
|
|
4616
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4617
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4618
|
+
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
3955
4619
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3956
4620
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3957
4621
|
try {
|
|
@@ -3990,12 +4654,12 @@ function registerReloadTools(server, sync) {
|
|
|
3990
4654
|
{
|
|
3991
4655
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, to re-run app startup, or to RECOVER A CRASHED APP: after a fatal error the other tools stop answering, and reloading is how you get them back (it falls through to the dev server when the app is too broken to reload itself). In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost \u2014 read anything you need from get_console/get_storage BEFORE reloading. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
3992
4656
|
inputSchema: {
|
|
3993
|
-
strategy:
|
|
4657
|
+
strategy: z20.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3994
4658
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3995
4659
|
),
|
|
3996
|
-
wait:
|
|
3997
|
-
timeoutMs:
|
|
3998
|
-
deviceId:
|
|
4660
|
+
wait: z20.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4661
|
+
timeoutMs: z20.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4662
|
+
deviceId: z20.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3999
4663
|
}
|
|
4000
4664
|
},
|
|
4001
4665
|
async (args) => {
|
|
@@ -4037,32 +4701,32 @@ function registerReloadTools(server, sync) {
|
|
|
4037
4701
|
}
|
|
4038
4702
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4039
4703
|
if (args.wait === false) {
|
|
4040
|
-
return
|
|
4704
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4041
4705
|
}
|
|
4042
4706
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4043
4707
|
if (elapsed == null) {
|
|
4044
|
-
return
|
|
4708
|
+
return fail20(
|
|
4045
4709
|
`Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
|
|
4046
4710
|
);
|
|
4047
4711
|
}
|
|
4048
|
-
return
|
|
4712
|
+
return text18(
|
|
4049
4713
|
`Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
|
|
4050
4714
|
);
|
|
4051
4715
|
} catch (e) {
|
|
4052
|
-
return
|
|
4716
|
+
return fail20(errMsg19(e));
|
|
4053
4717
|
}
|
|
4054
4718
|
}
|
|
4055
4719
|
);
|
|
4056
4720
|
}
|
|
4057
4721
|
|
|
4058
4722
|
// src/tools/packs/triage.ts
|
|
4059
|
-
import { z as
|
|
4060
|
-
var
|
|
4061
|
-
var
|
|
4723
|
+
import { z as z21 } from "zod";
|
|
4724
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4725
|
+
var fail21 = (message) => ({
|
|
4062
4726
|
isError: true,
|
|
4063
4727
|
content: [{ type: "text", text: message }]
|
|
4064
4728
|
});
|
|
4065
|
-
var
|
|
4729
|
+
var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
|
|
4066
4730
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4067
4731
|
var MAX_STORAGE_KEYS = 20;
|
|
4068
4732
|
var MAX_STORAGE_VALUE = 400;
|
|
@@ -4093,13 +4757,13 @@ async function otherBackends(sync, target, opts) {
|
|
|
4093
4757
|
return notes.length ? `
|
|
4094
4758
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4095
4759
|
}
|
|
4096
|
-
async function section(title,
|
|
4760
|
+
async function section(title, run2) {
|
|
4097
4761
|
try {
|
|
4098
|
-
const body = await
|
|
4762
|
+
const body = await run2();
|
|
4099
4763
|
return body ? `${title}
|
|
4100
4764
|
${body}` : null;
|
|
4101
4765
|
} catch (e) {
|
|
4102
|
-
const msg =
|
|
4766
|
+
const msg = errMsg20(e);
|
|
4103
4767
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4104
4768
|
return `${title}
|
|
4105
4769
|
(no answer \u2014 see the note above)`;
|
|
@@ -4114,7 +4778,7 @@ function registerTriageTools(server, sync) {
|
|
|
4114
4778
|
{
|
|
4115
4779
|
description: "START HERE when debugging a running app. One call = the app's current runtime state: connected device, recent console errors (including [FATAL] crashes), recent network requests with statuses, persisted storage values, and what's on screen right now. Use it as the first step for ANY 'why is the app doing X' question \u2014 it is far cheaper than calling get_console + get_network_requests + get_storage + describe_screen separately, and it tells you which of those to drill into next. If the app has crashed it says so and shows the crash. Requires Buoy Pro.",
|
|
4116
4780
|
inputSchema: {
|
|
4117
|
-
deviceId:
|
|
4781
|
+
deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4118
4782
|
}
|
|
4119
4783
|
},
|
|
4120
4784
|
async (args) => {
|
|
@@ -4221,7 +4885,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4221
4885
|
|
|
4222
4886
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4223
4887
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4224
|
-
return
|
|
4888
|
+
return text19(
|
|
4225
4889
|
`${head}${banner}
|
|
4226
4890
|
|
|
4227
4891
|
${body}
|
|
@@ -4229,22 +4893,22 @@ ${body}
|
|
|
4229
4893
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4230
4894
|
);
|
|
4231
4895
|
} catch (e) {
|
|
4232
|
-
return
|
|
4896
|
+
return fail21(errMsg20(e));
|
|
4233
4897
|
}
|
|
4234
4898
|
}
|
|
4235
4899
|
);
|
|
4236
4900
|
}
|
|
4237
4901
|
|
|
4238
4902
|
// src/tools/packs/flow.ts
|
|
4239
|
-
import { z as
|
|
4903
|
+
import { z as z22 } from "zod";
|
|
4240
4904
|
var UI_TOOL_ID = "highlight-updates";
|
|
4241
4905
|
var APP_TOOL_ID2 = "app";
|
|
4242
|
-
var
|
|
4243
|
-
var
|
|
4906
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4907
|
+
var fail22 = (message) => ({
|
|
4244
4908
|
isError: true,
|
|
4245
4909
|
content: [{ type: "text", text: message }]
|
|
4246
4910
|
});
|
|
4247
|
-
var
|
|
4911
|
+
var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
|
|
4248
4912
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4249
4913
|
var POLL_MS = 250;
|
|
4250
4914
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4261,19 +4925,19 @@ function formatElement2(e) {
|
|
|
4261
4925
|
const display = e.text ?? e.label;
|
|
4262
4926
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4263
4927
|
}
|
|
4264
|
-
var stepSchema =
|
|
4265
|
-
tap:
|
|
4266
|
-
longPress:
|
|
4267
|
-
type:
|
|
4268
|
-
into:
|
|
4269
|
-
set:
|
|
4270
|
-
reload:
|
|
4271
|
-
waitMs:
|
|
4272
|
-
waitFor:
|
|
4273
|
-
waitGone:
|
|
4274
|
-
expect:
|
|
4275
|
-
expectGone:
|
|
4276
|
-
timeoutMs:
|
|
4928
|
+
var stepSchema = z22.object({
|
|
4929
|
+
tap: z22.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4930
|
+
longPress: z22.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4931
|
+
type: z22.string().optional().describe("Text to type (needs `into`)."),
|
|
4932
|
+
into: z22.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4933
|
+
set: z22.union([z22.number(), z22.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4934
|
+
reload: z22.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4935
|
+
waitMs: z22.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4936
|
+
waitFor: z22.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4937
|
+
waitGone: z22.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4938
|
+
expect: z22.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
4939
|
+
expectGone: z22.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
4940
|
+
timeoutMs: z22.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4277
4941
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4278
4942
|
function registerFlowTools(server, sync) {
|
|
4279
4943
|
server.registerTool(
|
|
@@ -4281,10 +4945,10 @@ function registerFlowTools(server, sync) {
|
|
|
4281
4945
|
{
|
|
4282
4946
|
description: "Run a scripted sequence of UI interactions in ONE call and report what happened: per-step results, the final screen, plus the network requests and console errors the flow produced. Use this instead of alternating describe_screen/tap_element \u2014 reproducing a bug ('open cart, checkout, submit') or verifying a fix after an edit is one call, not a dozen. Steps run in order and stop at the first failure, returning the screen at that point so you can see WHERE it broke. Each step can tap, type, set a toggle/slider, reload, wait for an element, and assert. Requires Buoy Pro.",
|
|
4283
4947
|
inputSchema: {
|
|
4284
|
-
steps:
|
|
4285
|
-
include:
|
|
4286
|
-
continueOnFailure:
|
|
4287
|
-
deviceId:
|
|
4948
|
+
steps: z22.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4949
|
+
include: z22.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final screen + network/console produced by the flow; 'screen' = final screen only; 'none' = step results only."),
|
|
4950
|
+
continueOnFailure: z22.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4951
|
+
deviceId: z22.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4288
4952
|
}
|
|
4289
4953
|
},
|
|
4290
4954
|
async (args) => {
|
|
@@ -4456,7 +5120,7 @@ function registerFlowTools(server, sync) {
|
|
|
4456
5120
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
4457
5121
|
}
|
|
4458
5122
|
} catch (e) {
|
|
4459
|
-
stepErrors.push(
|
|
5123
|
+
stepErrors.push(errMsg21(e));
|
|
4460
5124
|
}
|
|
4461
5125
|
if (stepErrors.length) {
|
|
4462
5126
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -4479,7 +5143,7 @@ function registerFlowTools(server, sync) {
|
|
|
4479
5143
|
${shown.map(formatElement2).join("\n")}`
|
|
4480
5144
|
);
|
|
4481
5145
|
} catch (e) {
|
|
4482
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
5146
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
|
|
4483
5147
|
}
|
|
4484
5148
|
}
|
|
4485
5149
|
if (include === "all") {
|
|
@@ -4508,9 +5172,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4508
5172
|
}
|
|
4509
5173
|
}
|
|
4510
5174
|
}
|
|
4511
|
-
return
|
|
5175
|
+
return text20(parts.join("\n\n"));
|
|
4512
5176
|
} catch (e) {
|
|
4513
|
-
return
|
|
5177
|
+
return fail22(errMsg21(e));
|
|
4514
5178
|
}
|
|
4515
5179
|
}
|
|
4516
5180
|
);
|
|
@@ -4531,25 +5195,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4531
5195
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4532
5196
|
|
|
4533
5197
|
// src/tools/packs/renders.ts
|
|
4534
|
-
import { z as
|
|
5198
|
+
import { z as z23 } from "zod";
|
|
4535
5199
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4536
|
-
var
|
|
4537
|
-
var
|
|
5200
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5201
|
+
var fail23 = (message) => ({
|
|
4538
5202
|
isError: true,
|
|
4539
5203
|
content: [{ type: "text", text: message }]
|
|
4540
5204
|
});
|
|
4541
|
-
var
|
|
5205
|
+
var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
|
|
4542
5206
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4543
5207
|
var DEFAULT_IDLE_MS = 3e3;
|
|
4544
5208
|
var SETTLE_MS = 400;
|
|
4545
5209
|
var DEFAULT_TOP = 12;
|
|
4546
5210
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4547
|
-
var stepSchema2 =
|
|
4548
|
-
tap:
|
|
4549
|
-
type:
|
|
4550
|
-
into:
|
|
4551
|
-
waitMs:
|
|
4552
|
-
repeat:
|
|
5211
|
+
var stepSchema2 = z23.object({
|
|
5212
|
+
tap: z23.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5213
|
+
type: z23.string().optional().describe("Text to type (needs `into`)."),
|
|
5214
|
+
into: z23.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5215
|
+
waitMs: z23.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5216
|
+
repeat: z23.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4553
5217
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4554
5218
|
function selector2(needle) {
|
|
4555
5219
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4642,14 +5306,14 @@ function registerRenderTools(server, sync) {
|
|
|
4642
5306
|
{
|
|
4643
5307
|
description: "Find out WHAT re-rendered on the current screen and WHY, in one call \u2014 no test route, no scaffolding. THE tool for 'this page is slow/laggy/janky', 'typing lags', 'the whole page re-renders when I tap one button'. Give it `steps` to measure a specific interaction (tap a button, type in a field) \u2014 measuring an idle window instead tells you nothing about what taps cost, which is how re-render fixes usually go wrong. Returns per-component render COST in ms (sorted by it \u2014 a component rendering 40x for 0.4ms is noise, one rendering 4x for 38ms is the answer), the CAUSE of each (parent cascade / own state / changed props), which parent dragged it along, and which props changed. `wasted` counts renders that produced nothing new \u2014 that's the removable part. AFTER YOU EDIT, call it again with the same steps and compareToPrevious:true to prove the fix landed and didn't move the cost somewhere else. (For choosing between several implementations of a screen, use run_benchmark_batch instead \u2014 that ranks variants; this explains one screen.) Requires Buoy Pro and @buoy-gg/highlight-updates.",
|
|
4644
5308
|
inputSchema: {
|
|
4645
|
-
steps:
|
|
5309
|
+
steps: z23.array(stepSchema2).max(20).optional().describe(
|
|
4646
5310
|
"The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."
|
|
4647
5311
|
),
|
|
4648
|
-
idleMs:
|
|
4649
|
-
label:
|
|
4650
|
-
compareToPrevious:
|
|
4651
|
-
top:
|
|
4652
|
-
deviceId:
|
|
5312
|
+
idleMs: z23.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5313
|
+
label: z23.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5314
|
+
compareToPrevious: z23.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5315
|
+
top: z23.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5316
|
+
deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4653
5317
|
}
|
|
4654
5318
|
},
|
|
4655
5319
|
async (args) => {
|
|
@@ -4658,18 +5322,18 @@ function registerRenderTools(server, sync) {
|
|
|
4658
5322
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4659
5323
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4660
5324
|
if (!uiTool) {
|
|
4661
|
-
return
|
|
5325
|
+
return fail23(
|
|
4662
5326
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4663
5327
|
);
|
|
4664
5328
|
}
|
|
4665
5329
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4666
|
-
return
|
|
5330
|
+
return fail23(
|
|
4667
5331
|
"This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route."
|
|
4668
5332
|
);
|
|
4669
5333
|
}
|
|
4670
5334
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4671
5335
|
if (began && began.ok === false) {
|
|
4672
|
-
return
|
|
5336
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4673
5337
|
}
|
|
4674
5338
|
await delay5(SETTLE_MS);
|
|
4675
5339
|
const startedAt = Date.now();
|
|
@@ -4714,7 +5378,7 @@ function registerRenderTools(server, sync) {
|
|
|
4714
5378
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
4715
5379
|
const summary = ended?.summary;
|
|
4716
5380
|
if (!summary) {
|
|
4717
|
-
return
|
|
5381
|
+
return fail23(
|
|
4718
5382
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
4719
5383
|
);
|
|
4720
5384
|
}
|
|
@@ -4739,9 +5403,9 @@ function registerRenderTools(server, sync) {
|
|
|
4739
5403
|
} else if (!args.compareToPrevious) {
|
|
4740
5404
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
4741
5405
|
}
|
|
4742
|
-
return
|
|
5406
|
+
return text21(parts.join("\n"));
|
|
4743
5407
|
} catch (e) {
|
|
4744
|
-
return
|
|
5408
|
+
return fail23(errMsg22(e));
|
|
4745
5409
|
}
|
|
4746
5410
|
}
|
|
4747
5411
|
);
|
|
@@ -4749,7 +5413,7 @@ function registerRenderTools(server, sync) {
|
|
|
4749
5413
|
|
|
4750
5414
|
// src/verify/editWatcher.ts
|
|
4751
5415
|
import fs3 from "fs";
|
|
4752
|
-
import
|
|
5416
|
+
import path4 from "path";
|
|
4753
5417
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
4754
5418
|
".ts",
|
|
4755
5419
|
".tsx",
|
|
@@ -4798,8 +5462,8 @@ var EditWatcher = class {
|
|
|
4798
5462
|
(_event, filename) => {
|
|
4799
5463
|
if (!filename) return;
|
|
4800
5464
|
const rel2 = String(filename);
|
|
4801
|
-
if (rel2.split(
|
|
4802
|
-
if (!WATCHED_EXT.has(
|
|
5465
|
+
if (rel2.split(path4.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5466
|
+
if (!WATCHED_EXT.has(path4.extname(rel2))) return;
|
|
4803
5467
|
this.noteEdit(rel2);
|
|
4804
5468
|
}
|
|
4805
5469
|
);
|
|
@@ -4899,6 +5563,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
4899
5563
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
4900
5564
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
4901
5565
|
|
|
5566
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5567
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5568
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5569
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5570
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5571
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5572
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5573
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5574
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5575
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5576
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5577
|
+
and no Buoy integration in the app.
|
|
5578
|
+
|
|
4902
5579
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
4903
5580
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
4904
5581
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -4962,6 +5639,7 @@ require the connected app to have an active Buoy Pro license. If they error with
|
|
|
4962
5639
|
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
4963
5640
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
4964
5641
|
"measure_renders",
|
|
5642
|
+
"scenario_action",
|
|
4965
5643
|
"run_flow",
|
|
4966
5644
|
"run_benchmark_batch",
|
|
4967
5645
|
"get_triage",
|
|
@@ -5012,7 +5690,9 @@ function createMcpServer(sync) {
|
|
|
5012
5690
|
registerRoutesTools(server, sync);
|
|
5013
5691
|
registerConsoleTools(server, sync);
|
|
5014
5692
|
registerTimeMachineTools(server, sync);
|
|
5693
|
+
registerScenariosTools(server, sync);
|
|
5015
5694
|
registerScreenshotTools(server, sync);
|
|
5695
|
+
registerCameraTools(server);
|
|
5016
5696
|
registerInteractTools(server, sync);
|
|
5017
5697
|
registerJsThreadTools(server, sync);
|
|
5018
5698
|
registerImagesTools(server, sync);
|
|
@@ -5021,7 +5701,7 @@ function createMcpServer(sync) {
|
|
|
5021
5701
|
return server;
|
|
5022
5702
|
}
|
|
5023
5703
|
var deviceIdField = {
|
|
5024
|
-
deviceId:
|
|
5704
|
+
deviceId: z24.string().optional().describe(
|
|
5025
5705
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5026
5706
|
)
|
|
5027
5707
|
};
|
|
@@ -5029,8 +5709,8 @@ var deviceIdField = {
|
|
|
5029
5709
|
// src/init.ts
|
|
5030
5710
|
import { spawnSync } from "child_process";
|
|
5031
5711
|
import fs4 from "fs";
|
|
5032
|
-
import
|
|
5033
|
-
import
|
|
5712
|
+
import os4 from "os";
|
|
5713
|
+
import path5 from "path";
|
|
5034
5714
|
function defaultEnv() {
|
|
5035
5715
|
return { BUOY_VERIFY: "auto" };
|
|
5036
5716
|
}
|
|
@@ -5038,19 +5718,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5038
5718
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5039
5719
|
function runInit(opts = {}) {
|
|
5040
5720
|
const cwd = opts.cwd ?? process.cwd();
|
|
5041
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5721
|
+
const skillSrcDir = opts.skillSrcDir ?? path5.resolve(__dirname, "../skill");
|
|
5042
5722
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5043
5723
|
const entry = resolveEntry(opts, log);
|
|
5044
5724
|
const done = [];
|
|
5045
|
-
upsertMcpConfig(
|
|
5046
|
-
fs4.mkdirSync(
|
|
5047
|
-
upsertMcpConfig(
|
|
5048
|
-
if (fs4.existsSync(
|
|
5049
|
-
upsertMcpConfig(
|
|
5725
|
+
upsertMcpConfig(path5.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5726
|
+
fs4.mkdirSync(path5.join(cwd, ".cursor"), { recursive: true });
|
|
5727
|
+
upsertMcpConfig(path5.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5728
|
+
if (fs4.existsSync(path5.join(cwd, ".vscode"))) {
|
|
5729
|
+
upsertMcpConfig(path5.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5050
5730
|
}
|
|
5051
5731
|
installSkills(skillSrcDir, cwd, done);
|
|
5052
|
-
appendDebuggingGuide(
|
|
5053
|
-
appendDebuggingGuide(
|
|
5732
|
+
appendDebuggingGuide(path5.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5733
|
+
appendDebuggingGuide(path5.join(cwd, "AGENTS.md"), cwd, done);
|
|
5054
5734
|
log("Buoy MCP installed:");
|
|
5055
5735
|
for (const line of done) log(" \u2022 " + line);
|
|
5056
5736
|
log("");
|
|
@@ -5092,11 +5772,11 @@ function npxEntry() {
|
|
|
5092
5772
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5093
5773
|
}
|
|
5094
5774
|
function localEntry(opts, log) {
|
|
5095
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5775
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? path5.join(os4.homedir(), ".buoy-mcp");
|
|
5096
5776
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5097
|
-
const
|
|
5098
|
-
if (
|
|
5099
|
-
return { type: "stdio", command: "node", args: [
|
|
5777
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5778
|
+
if (cliPath2) {
|
|
5779
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5100
5780
|
}
|
|
5101
5781
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5102
5782
|
return npxEntry();
|
|
@@ -5126,12 +5806,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5126
5806
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5127
5807
|
return null;
|
|
5128
5808
|
}
|
|
5129
|
-
const
|
|
5130
|
-
if (!fs4.existsSync(
|
|
5131
|
-
log(`\u26A0 install finished but ${
|
|
5809
|
+
const cliPath2 = path5.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5810
|
+
if (!fs4.existsSync(cliPath2)) {
|
|
5811
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5132
5812
|
return null;
|
|
5133
5813
|
}
|
|
5134
|
-
return
|
|
5814
|
+
return cliPath2;
|
|
5135
5815
|
}
|
|
5136
5816
|
function probeRegistry() {
|
|
5137
5817
|
const registry = effectiveRegistry();
|
|
@@ -5188,7 +5868,7 @@ function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
|
5188
5868
|
json3 = {};
|
|
5189
5869
|
}
|
|
5190
5870
|
} else {
|
|
5191
|
-
fs4.mkdirSync(
|
|
5871
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true });
|
|
5192
5872
|
}
|
|
5193
5873
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5194
5874
|
const existed = "buoy" in servers;
|
|
@@ -5279,29 +5959,29 @@ function installSkills(skillSrcDir, cwd, done) {
|
|
|
5279
5959
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5280
5960
|
return;
|
|
5281
5961
|
}
|
|
5282
|
-
const skills = fs4.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs4.existsSync(
|
|
5962
|
+
const skills = fs4.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs4.existsSync(path5.join(skillSrcDir, e.name, "SKILL.md")));
|
|
5283
5963
|
if (skills.length === 0) {
|
|
5284
5964
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5285
5965
|
return;
|
|
5286
5966
|
}
|
|
5287
5967
|
for (const skill of skills) {
|
|
5288
|
-
const from =
|
|
5289
|
-
const to =
|
|
5968
|
+
const from = path5.join(skillSrcDir, skill.name);
|
|
5969
|
+
const to = path5.join(cwd, ".claude", "skills", skill.name);
|
|
5290
5970
|
copyDir(from, to);
|
|
5291
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
5971
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(path5.join(to, "SKILL.md"), cwd)}`);
|
|
5292
5972
|
}
|
|
5293
5973
|
}
|
|
5294
5974
|
function copyDir(from, to) {
|
|
5295
5975
|
fs4.mkdirSync(to, { recursive: true });
|
|
5296
5976
|
for (const entry of fs4.readdirSync(from, { withFileTypes: true })) {
|
|
5297
|
-
const s =
|
|
5298
|
-
const d =
|
|
5977
|
+
const s = path5.join(from, entry.name);
|
|
5978
|
+
const d = path5.join(to, entry.name);
|
|
5299
5979
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5300
5980
|
else fs4.copyFileSync(s, d);
|
|
5301
5981
|
}
|
|
5302
5982
|
}
|
|
5303
5983
|
function rel(file, cwd) {
|
|
5304
|
-
const r =
|
|
5984
|
+
const r = path5.relative(cwd, file);
|
|
5305
5985
|
return r && !r.startsWith("..") ? r : file;
|
|
5306
5986
|
}
|
|
5307
5987
|
|