@aarwitz/tapp 0.17.0-rc.9 → 0.17.1
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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +33 -0
- package/AGENTS.md +44 -15
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +65 -7
- package/README.md +141 -81
- package/bin/tapp.js +253 -64
- package/docs/BROWSER-PRODUCT.md +1 -1
- package/docs/application-model.md +12 -3
- package/docs/scenarios.md +1 -1
- package/mcp-server/src/android-driver.js +13 -2
- package/mcp-server/src/android-explorer.js +3 -1
- package/mcp-server/src/android-flow.js +18 -1
- package/mcp-server/src/application-model.js +83 -13
- package/mcp-server/src/ci-report.js +2 -2
- package/mcp-server/src/ci-setup.js +4 -4
- package/mcp-server/src/environment-preflight.js +43 -0
- package/mcp-server/src/focused-navigation.js +271 -0
- package/mcp-server/src/html-report.js +3 -2
- package/mcp-server/src/index.js +565 -134
- package/mcp-server/src/pr-selection.js +2 -2
- package/mcp-server/src/product-operations.js +111 -6
- package/mcp-server/src/report.js +17 -4
- package/mcp-server/src/ui-map.js +2 -2
- package/mcp-server/src/web-explorer.js +123 -10
- package/mcp-server/src/web-flow.js +17 -1
- package/package.json +6 -4
- package/scripts/ci-gate.sh +42 -0
- package/scripts/flow_lib.py +1 -1
- package/scripts/quick-capture.sh +44 -13
- package/scripts/run-flow.sh +12 -1
- package/skills/tapp/SKILL.md +95 -0
- package/skills/tapp/agents/openai.yaml +4 -0
- package/skills/tapp/references/commands.md +105 -0
package/bin/tapp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Tapp CLI — agent-driven app testing on real app surfaces.
|
|
3
3
|
//
|
|
4
4
|
// Zero-config verbs (the same engine the MCP tools use, exported by mcp-server/src/index.js):
|
|
5
5
|
// tapp explore <bundleId|appId|url> Autonomous exploration → findings + evidence (observation)
|
|
@@ -28,8 +28,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
28
28
|
const packageRoot = path.resolve(__dirname, "..");
|
|
29
29
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
30
30
|
|
|
31
|
-
// Redirect all writable output away from the (possibly read-only) package dir.
|
|
32
|
-
//
|
|
31
|
+
// Redirect all writable output away from the (possibly read-only) package dir. TAPP_HOME and
|
|
32
|
+
// ~/.tapp are the only current runtime locations; retired environment/path aliases are ignored.
|
|
33
33
|
const tappHome = (process.env.TAPP_HOME || path.join(os.homedir(), ".tapp")).trim();
|
|
34
34
|
process.env.TAPP_HOME = tappHome;
|
|
35
35
|
// TAPP_HOME is created lazily (just before the switch) so `--help`, `help`, and `version` never
|
|
@@ -53,6 +53,19 @@ function bad(label, detail = "") {
|
|
|
53
53
|
console.log(` ❌ ${label}${detail ? ` — ${detail}` : ""}`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
let lastProgressLine = "";
|
|
57
|
+
function writeProgress(line) {
|
|
58
|
+
const text = String(line || "").trimEnd();
|
|
59
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${text} `);
|
|
60
|
+
else if (text && text !== lastProgressLine) console.error(text);
|
|
61
|
+
lastProgressLine = text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function finishProgress() {
|
|
65
|
+
if (process.stderr.isTTY && lastProgressLine) process.stderr.write("\n");
|
|
66
|
+
lastProgressLine = "";
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
function bootedSims() {
|
|
57
70
|
const r = run("xcrun", ["simctl", "list", "devices", "booted", "-j"]);
|
|
58
71
|
if (r.code !== 0) return [];
|
|
@@ -155,7 +168,7 @@ const engineImport = () => import(path.join(packageRoot, "mcp-server", "src", "i
|
|
|
155
168
|
|
|
156
169
|
function requireMacFor(what) {
|
|
157
170
|
if (process.platform === "darwin") return;
|
|
158
|
-
console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp explore https://localhost:3000`);
|
|
171
|
+
console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: npx -y @aarwitz/tapp@latest explore https://localhost:3000`);
|
|
159
172
|
process.exit(1);
|
|
160
173
|
}
|
|
161
174
|
|
|
@@ -206,6 +219,26 @@ function printEngineError(r) {
|
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
async function promptForInitTarget(details) {
|
|
223
|
+
const choices = Array.isArray(details?.choices) ? details.choices : [];
|
|
224
|
+
if (!choices.length || !process.stdin.isTTY || !process.stderr.isTTY || process.env.CI) return null;
|
|
225
|
+
const { createInterface } = await import("node:readline/promises");
|
|
226
|
+
const terminal = createInterface({ input: process.stdin, output: process.stderr });
|
|
227
|
+
console.error("\nTapp found multiple application targets. Which one should it explore?");
|
|
228
|
+
choices.forEach((choice, index) => console.error(` ${index + 1}) ${choice.platform} · ${choice.name} (${choice.sourcePath})`));
|
|
229
|
+
try {
|
|
230
|
+
while (true) {
|
|
231
|
+
const answer = String(await terminal.question(`Select 1-${choices.length} (or q to cancel): `)).trim();
|
|
232
|
+
if (/^(?:q|quit|cancel)$/i.test(answer)) return null;
|
|
233
|
+
const selected = Number(answer);
|
|
234
|
+
if (Number.isInteger(selected) && selected >= 1 && selected <= choices.length) return choices[selected - 1];
|
|
235
|
+
console.error(`Enter a number from 1 to ${choices.length}, or q to cancel.`);
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
terminal.close();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
209
242
|
// Turn whatever the user gave us (nothing / repo dir / .app / bundle id) into an installed
|
|
210
243
|
// bundle id, narrating build/install progress on stderr.
|
|
211
244
|
async function resolveTargetOrExit(engine, input) {
|
|
@@ -220,14 +253,15 @@ async function resolveTargetOrExit(engine, input) {
|
|
|
220
253
|
|
|
221
254
|
function safeCommandUsage(verb) {
|
|
222
255
|
const usage = {
|
|
223
|
-
explore: "tapp explore [target] [--platform ios|android|web] [--actions N] [--timeout SEC] [--email VALUE] [--password VALUE] [--baseline FILE] [--json FILE]\n iOS launch configuration: [--launch-arg VALUE ...] [--launch-env '{\"KEY\":\"VALUE\"}']\n Android: [--app-id ID] [--apk FILE] [--serial ID] [--keep-data]",
|
|
224
|
-
|
|
256
|
+
explore: "tapp explore [target] [--platform ios|android|web] [--actions N] [--timeout SEC] [--email VALUE] [--password VALUE] [--baseline FILE] [--json FILE]\n Web: [--watch] opens Tapp's controlled browser and shows its actions\n iOS launch configuration: [--launch-arg VALUE ...] [--launch-env '{\"KEY\":\"VALUE\"}']\n Android: [--app-id ID] [--apk FILE] [--serial ID] [--keep-data]",
|
|
257
|
+
focus: "tapp focus \"SCREEN OR CONTROL\" [target] [--platform ios|android|web] [--project-dir REPO] [--map FILE] [--out FILE]",
|
|
258
|
+
init: "tapp init [repo] [--explore] [--refresh] [--platform PLATFORM] [--target NAME] [--url URL] [--watch] [--dry-run]",
|
|
225
259
|
open: "tapp open [target] [--platform ios|android|web] [--out FILE] [--tap TEXT] [--wait-for TEXT]",
|
|
226
260
|
tree: "tapp tree [target] [--platform ios|android|web] [--json] [--tap TEXT] [--wait-for TEXT]",
|
|
227
261
|
shot: "tapp shot [--out FILE]",
|
|
228
262
|
apps: "tapp apps",
|
|
229
263
|
build: "tapp build [repo] [--scheme NAME] [--configuration NAME]",
|
|
230
|
-
flow: "tapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
|
|
264
|
+
flow: "tapp flow example\ntapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
|
|
231
265
|
task: "tapp task validate FILE [--platform PLATFORM] [--map FILE]\ntapp task compile FILE --platform PLATFORM [--inputs JSON] [--out FILE]\ntapp task run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID] [--inputs JSON]",
|
|
232
266
|
contract: "tapp contract validate FILE [--platform PLATFORM] [--map FILE]\ntapp contract compile FILE --platform PLATFORM [--out FILE]\ntapp contract run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID]",
|
|
233
267
|
scenario: "tapp scenario validate FILE [--project-dir DIR]\ntapp scenario run FILE --platform web --url URL [--project-dir DIR]",
|
|
@@ -255,13 +289,22 @@ if (["--help", "-h"].includes(command)) {
|
|
|
255
289
|
command = "help";
|
|
256
290
|
rest = [];
|
|
257
291
|
}
|
|
292
|
+
const knownCommands = new Set([
|
|
293
|
+
"help", "version", "--version", "-v", "mcp", "init", "focus", "explore", "qa", "open",
|
|
294
|
+
"tree", "shot", "screenshot", "apps", "build", "flow", "task", "contract", "scenario", "map",
|
|
295
|
+
"pr", "plan", "baseline", "ci", "actor", "app", "studio", "report", "doctor", "install",
|
|
296
|
+
]);
|
|
297
|
+
if (!knownCommands.has(command)) {
|
|
298
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
299
|
+
console.error("Run `npx -y @aarwitz/tapp@latest --help` for the command reference.");
|
|
300
|
+
process.exit(2);
|
|
301
|
+
}
|
|
258
302
|
const safeHelpRequested = (rest.includes("--help") || rest.includes("-h"))
|
|
259
303
|
&& !["help", "version", "--version", "-v"].includes(command)
|
|
260
304
|
&& (command !== "ci" || rest[0] === "install");
|
|
261
305
|
if (safeHelpRequested) {
|
|
262
|
-
console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens.
|
|
263
|
-
|
|
264
|
-
rest = [];
|
|
306
|
+
console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens. Run \`tapp --help\` for the full command reference.`);
|
|
307
|
+
process.exit(0);
|
|
265
308
|
}
|
|
266
309
|
|
|
267
310
|
// Create TAPP_HOME only for commands that actually use it — never for help/version/--help.
|
|
@@ -302,7 +345,7 @@ switch (command) {
|
|
|
302
345
|
}
|
|
303
346
|
const platform = typeof flags.platform === "string" ? flags.platform.toLowerCase()
|
|
304
347
|
: typeof flags.url === "string" ? "web"
|
|
305
|
-
: typeof flags["app-id"] === "string" || typeof flags.apk === "string" ? "android" : "
|
|
348
|
+
: typeof flags["app-id"] === "string" || typeof flags.apk === "string" ? "android" : "";
|
|
306
349
|
if (explore && platform === "ios") requireMacFor("iOS init exploration");
|
|
307
350
|
const actions = flags.actions === undefined ? 40 : Number(flags.actions);
|
|
308
351
|
const timeout = flags.timeout === undefined ? 600 : Number(flags.timeout);
|
|
@@ -312,51 +355,85 @@ switch (command) {
|
|
|
312
355
|
}
|
|
313
356
|
const engine = explore ? await engineImport() : null;
|
|
314
357
|
const { initializeProductProject } = await import(path.join(packageRoot, "mcp-server", "src", "product-operations.js"));
|
|
358
|
+
const initOptions = {
|
|
359
|
+
projectDir,
|
|
360
|
+
mode: flags["dry-run"] === true ? "inspect" : explore ? "explore" : flags.refresh === true ? "refresh" : "write",
|
|
361
|
+
ownedUrl: typeof flags.url === "string" ? flags.url : "",
|
|
362
|
+
platform: typeof flags.platform === "string" ? flags.platform.toLowerCase() : explore ? platform : "",
|
|
363
|
+
target: typeof flags.target === "string" ? flags.target : "",
|
|
364
|
+
bundleId: typeof flags["bundle-id"] === "string" ? flags["bundle-id"] : "",
|
|
365
|
+
appId: typeof flags["app-id"] === "string" ? flags["app-id"] : "",
|
|
366
|
+
apkPath: typeof flags.apk === "string" ? path.resolve(flags.apk) : undefined,
|
|
367
|
+
serial: typeof flags.serial === "string" ? flags.serial : undefined,
|
|
368
|
+
maxActions: actions,
|
|
369
|
+
timeout,
|
|
370
|
+
testEmail: typeof flags.email === "string" ? flags.email : undefined,
|
|
371
|
+
testPassword: typeof flags.password === "string" ? flags.password : undefined,
|
|
372
|
+
watch: flags.watch === true,
|
|
373
|
+
runExploration: engine?.runInitExploration,
|
|
374
|
+
onProgress: (progress) => {
|
|
375
|
+
const activePlatform = progress.platform || platform;
|
|
376
|
+
writeProgress(`🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${activePlatform === "web" ? "pages reached" : activePlatform === "ios" ? "structural states observed" : "screens reached"}`);
|
|
377
|
+
},
|
|
378
|
+
onStatus: (status) => console.error(`⏳ ${status}`),
|
|
379
|
+
outDir,
|
|
380
|
+
maxContracts,
|
|
381
|
+
};
|
|
315
382
|
let result;
|
|
383
|
+
let failure = null;
|
|
316
384
|
try {
|
|
317
|
-
result = await initializeProductProject(
|
|
318
|
-
projectDir,
|
|
319
|
-
mode: flags["dry-run"] === true ? "inspect" : explore ? "explore" : flags.refresh === true ? "refresh" : "write",
|
|
320
|
-
ownedUrl: typeof flags.url === "string" ? flags.url : "",
|
|
321
|
-
platform: typeof flags.platform === "string" ? flags.platform.toLowerCase() : explore ? platform : "",
|
|
322
|
-
target: typeof flags.target === "string" ? flags.target : projectDir,
|
|
323
|
-
bundleId: typeof flags["bundle-id"] === "string" ? flags["bundle-id"] : "",
|
|
324
|
-
appId: typeof flags["app-id"] === "string" ? flags["app-id"] : "",
|
|
325
|
-
apkPath: typeof flags.apk === "string" ? path.resolve(flags.apk) : undefined,
|
|
326
|
-
serial: typeof flags.serial === "string" ? flags.serial : undefined,
|
|
327
|
-
maxActions: actions,
|
|
328
|
-
timeout,
|
|
329
|
-
testEmail: typeof flags.email === "string" ? flags.email : undefined,
|
|
330
|
-
testPassword: typeof flags.password === "string" ? flags.password : undefined,
|
|
331
|
-
runExploration: engine?.runInitExploration,
|
|
332
|
-
onProgress: (progress) => process.stderr.write(`\r🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached"} `),
|
|
333
|
-
onStatus: (status) => console.error(`⏳ ${status}`),
|
|
334
|
-
outDir,
|
|
335
|
-
maxContracts,
|
|
336
|
-
});
|
|
385
|
+
result = await initializeProductProject(initOptions);
|
|
337
386
|
} catch (error) {
|
|
338
|
-
|
|
339
|
-
|
|
387
|
+
failure = error;
|
|
388
|
+
const choice = explore && error.details?.reason === "target-selection-required"
|
|
389
|
+
? await promptForInitTarget(error.details)
|
|
390
|
+
: null;
|
|
391
|
+
if (choice) {
|
|
392
|
+
if (choice.platform === "ios") requireMacFor("iOS init exploration");
|
|
393
|
+
console.error(`🎯 Exploring ${choice.platform}:${choice.name} (${choice.sourcePath})`);
|
|
394
|
+
try {
|
|
395
|
+
result = await initializeProductProject({ ...initOptions, platform: choice.platform, target: choice.selector });
|
|
396
|
+
failure = null;
|
|
397
|
+
} catch (retryError) {
|
|
398
|
+
failure = retryError;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (failure) {
|
|
403
|
+
if (explore) finishProgress();
|
|
404
|
+
printEngineError({ error: `Could not initialize repository: ${failure.message || String(failure)}`, details: failure.details || {} });
|
|
340
405
|
process.exit(2);
|
|
341
406
|
}
|
|
342
|
-
if (explore)
|
|
407
|
+
if (explore) finishProgress();
|
|
343
408
|
const built = { model: result.model, plan: result.plan };
|
|
344
409
|
const written = result.written;
|
|
345
410
|
const exploration = result.exploration;
|
|
346
411
|
if (typeof flags["json-out"] === "string") {
|
|
347
412
|
const out = path.resolve(flags["json-out"]);
|
|
348
413
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
349
|
-
fs.writeFileSync(out, JSON.stringify({
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
fs.writeFileSync(out, JSON.stringify({
|
|
415
|
+
model: built.model,
|
|
416
|
+
plan: written?.plan || built.plan,
|
|
417
|
+
...(exploration ? { exploration } : {}),
|
|
418
|
+
...(result.selectedTarget ? { selectedTarget: result.selectedTarget } : {}),
|
|
419
|
+
requirementScope: result.requirementScope,
|
|
420
|
+
}, null, 2) + "\n");
|
|
421
|
+
}
|
|
422
|
+
const activeRequirements = result.requirementScope?.active || built.model.requirements;
|
|
423
|
+
const deferredRequirements = result.requirementScope?.deferred || [];
|
|
424
|
+
const blocking = activeRequirements.filter((item) => item.severity === "blocking");
|
|
425
|
+
const deferredBlocking = deferredRequirements.filter((item) => item.severity === "blocking");
|
|
352
426
|
const pending = (written?.plan || built.plan).items.filter((item) => item.decision === "pending");
|
|
353
427
|
console.log(`🧭 Tapp init — ${built.model.application.name}`);
|
|
354
428
|
console.log(` targets: ${built.model.targets.length ? built.model.targets.map((target) => `${target.platform}:${target.name}`).join(", ") : "none"}`);
|
|
355
429
|
console.log(` UI Map: ${built.model.uiMap.status} · ${built.model.uiMap.nodeCount} states · ${built.model.uiMap.edgeCount} transitions`);
|
|
356
430
|
if (exploration) console.log(` Exploration: ${(exploration.findings || []).length} finding(s)${exploration.inconclusive ? " (inconclusive)" : ""} · ${exploration.uiMap.nodeCount} states · evidence: ${exploration.reportHtml || exploration.capture?.path || "capture recorded"}`);
|
|
357
431
|
if (exploration?.managedRuntime) console.log(` Managed web runtime: built/started ${exploration.target} for exploration and stopped it afterward · log: ${exploration.runtime.logPath}`);
|
|
358
|
-
|
|
359
|
-
|
|
432
|
+
const selectedLabel = result.selectedTarget ? ` for ${result.selectedTarget.platform}:${result.selectedTarget.name}` : "";
|
|
433
|
+
const deferredLabel = deferredBlocking.length ? ` · ${deferredBlocking.length} setup gap(s) on unselected target(s)` : "";
|
|
434
|
+
console.log(` release plan: ${(written?.plan || built.plan).items.length} item(s) · ${pending.length} pending review · ${blocking.length} blocking requirement(s)${selectedLabel}${deferredLabel}`);
|
|
435
|
+
for (const requirement of activeRequirements) console.log(` ${requirement.severity === "blocking" ? "❌" : "⚠️"} ${requirement.message} Next: ${requirement.remediation}`);
|
|
436
|
+
for (const requirement of deferredRequirements) console.log(` ℹ️ Unselected ${requirement.targetPlatform}:${requirement.targetName} setup gap: ${requirement.message} Next: ${requirement.remediation}`);
|
|
360
437
|
if (written) console.log(` model: ${written.modelPath}\n plan: ${written.planPath}`);
|
|
361
438
|
else console.log(" dry run: repository files were not changed");
|
|
362
439
|
break;
|
|
@@ -411,7 +488,10 @@ switch (command) {
|
|
|
411
488
|
testPassword: typeof flags.password === "string" ? flags.password : undefined,
|
|
412
489
|
startWebTarget: engine.startManagedWebTarget,
|
|
413
490
|
stopWebTarget: engine.stopManagedWebTarget,
|
|
414
|
-
|
|
491
|
+
// Contract execution is emitted in full on stdout below. Keep build/runtime/replay
|
|
492
|
+
// status live on stderr, but do not echo the execution transcript there as well — an
|
|
493
|
+
// interactive terminal merges the streams and would otherwise show every result twice.
|
|
494
|
+
onProgress: (entry) => { if (entry.text && entry.phase !== "execute") console.error(`⏳ ${entry.text}`); },
|
|
415
495
|
});
|
|
416
496
|
} catch (error) {
|
|
417
497
|
console.error(`❌ Could not validate contract drafts: ${error.message || String(error)}`);
|
|
@@ -460,6 +540,63 @@ switch (command) {
|
|
|
460
540
|
// ---- Zero-config verbs: the same engine the MCP tools use (exported by index.js),
|
|
461
541
|
// invokable by any agent or human with no server setup at all.
|
|
462
542
|
|
|
543
|
+
case "focus": {
|
|
544
|
+
const { flags, positionals } = parseVerbArgs(rest);
|
|
545
|
+
const query = positionals[0] || "";
|
|
546
|
+
if (!query) {
|
|
547
|
+
console.error('usage: tapp focus "SCREEN OR CONTROL" [target] [--platform ios|android|web] [--project-dir REPO] [--map FILE] [--out FILE]');
|
|
548
|
+
process.exit(2);
|
|
549
|
+
}
|
|
550
|
+
let projectDir;
|
|
551
|
+
try { projectDir = fs.realpathSync(path.resolve(typeof flags["project-dir"] === "string" ? flags["project-dir"] : process.cwd())); }
|
|
552
|
+
catch { console.error("❌ --project-dir must be an existing repository directory"); process.exit(2); }
|
|
553
|
+
const target = positionals[1] || (typeof flags.url === "string" ? flags.url : "");
|
|
554
|
+
const platform = requestedPlatform(flags, target);
|
|
555
|
+
if (!["ios", "android", "web"].includes(platform)) { console.error("❌ --platform must be ios|android|web"); process.exit(2); }
|
|
556
|
+
const engine = await engineImport();
|
|
557
|
+
let started = null;
|
|
558
|
+
try {
|
|
559
|
+
if (platform === "ios") {
|
|
560
|
+
requireMacFor("iOS focused navigation");
|
|
561
|
+
const sim = await engine.ensureBootedSim({ autoBoot:true });
|
|
562
|
+
if (sim.error) throw new Error(sim.error);
|
|
563
|
+
const bundleId = await resolveTargetOrExit(engine, target || projectDir);
|
|
564
|
+
const launch = iosLaunchOptions(flags, rest);
|
|
565
|
+
started = await engine.startIosInteractiveSession(bundleId, engine.explorationEnvFromArgs({ testEmail:flags.email, testPassword:flags.password, ...launch }));
|
|
566
|
+
} else if (platform === "android") {
|
|
567
|
+
const android = androidTarget(flags, target);
|
|
568
|
+
started = await engine.startAndroidInteractiveSession(android.appId, { serial:android.serial, apkPath:android.apkPath, clearData:flags["keep-data"] !== true });
|
|
569
|
+
} else {
|
|
570
|
+
if (!/^https?:\/\//i.test(target)) { console.error("❌ Web focus needs an http(s) target URL"); process.exit(2); }
|
|
571
|
+
started = await engine.startWebInteractiveSession(target);
|
|
572
|
+
}
|
|
573
|
+
if (started.error) throw new Error(started.error);
|
|
574
|
+
const focused = await engine.focusInteractiveSession({ projectDir, query, platform, mapPath:typeof flags.map === "string" ? flags.map : "" });
|
|
575
|
+
const { focusedTargetSummary } = await import(path.join(packageRoot, "mcp-server", "src", "focused-navigation.js"));
|
|
576
|
+
console.log(focusedTargetSummary(focused));
|
|
577
|
+
if (focused.execution?.status === "reached") {
|
|
578
|
+
console.log(`\n⚡ Reached in ${(focused.execution.steps || []).length} route action(s).\n`);
|
|
579
|
+
console.log(engine.formatScreen(focused.screenTitle, focused.elements));
|
|
580
|
+
const frame = await engine.captureInteractiveSessionFrame(Number(flags.width) || 900);
|
|
581
|
+
if (!frame.error) {
|
|
582
|
+
const out = saveShot(frame, typeof flags.out === "string" ? path.resolve(flags.out) : null, `focus-${Date.now()}.${frame.mimeType === "image/png" ? "png" : "jpg"}`);
|
|
583
|
+
console.log(`\n📸 Screenshot: ${out}`);
|
|
584
|
+
}
|
|
585
|
+
} else if (focused.execution?.status === "failed") {
|
|
586
|
+
console.error(`\n❌ Observed route stopped: ${focused.execution.reason}`);
|
|
587
|
+
process.exitCode = 1;
|
|
588
|
+
} else {
|
|
589
|
+
console.error("\nℹ️ Tapp located the source but did not drive an unobserved route. Ground the UI Map with `npx -y @aarwitz/tapp@latest init . --explore`.");
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error(`❌ ${error.message || String(error)}`);
|
|
593
|
+
process.exitCode = 1;
|
|
594
|
+
} finally {
|
|
595
|
+
if (started) await engine.endInteractiveSession();
|
|
596
|
+
}
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
|
|
463
600
|
case "explore":
|
|
464
601
|
case "qa": {
|
|
465
602
|
// `explore` is the canonical verb (ADR-0005: exploration observes; the gate judges). `qa` is a
|
|
@@ -490,7 +627,7 @@ switch (command) {
|
|
|
490
627
|
const modelPlatform = typeof flags.platform === "string" ? flags.platform.toLowerCase() : "";
|
|
491
628
|
if (modelPlatform === "ios") requireMacFor("iOS testing");
|
|
492
629
|
const onProgress = (p) =>
|
|
493
|
-
|
|
630
|
+
writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} states observed`);
|
|
494
631
|
const r = await engine.runExploreTarget({
|
|
495
632
|
projectDir: process.cwd(),
|
|
496
633
|
platform: modelPlatform,
|
|
@@ -501,11 +638,12 @@ switch (command) {
|
|
|
501
638
|
testPassword: flags.password,
|
|
502
639
|
...launchOptions,
|
|
503
640
|
baselineFindings,
|
|
641
|
+
watch: flags.watch === true,
|
|
504
642
|
surface: "cli",
|
|
505
643
|
onProgress,
|
|
506
644
|
onStatus: (t) => console.error(`ℹ️ ${t}`),
|
|
507
645
|
});
|
|
508
|
-
|
|
646
|
+
finishProgress();
|
|
509
647
|
if (r.error) { printEngineError(r); process.exit(1); }
|
|
510
648
|
console.log(r.text);
|
|
511
649
|
if (flags.json && typeof flags.json === "string") {
|
|
@@ -521,19 +659,23 @@ switch (command) {
|
|
|
521
659
|
process.exit(2);
|
|
522
660
|
}
|
|
523
661
|
if (platform === "ios") requireMacFor("iOS testing");
|
|
662
|
+
if (flags.watch === true && platform !== "web") {
|
|
663
|
+
console.error("❌ --watch is currently available for web exploration only");
|
|
664
|
+
process.exit(2);
|
|
665
|
+
}
|
|
524
666
|
if (platform !== "ios" && Object.keys(launchOptions).length) {
|
|
525
667
|
console.error("❌ --launch-arg and --launch-env apply only to iOS targets");
|
|
526
668
|
process.exit(2);
|
|
527
669
|
}
|
|
528
670
|
if (platform === "web" && !/^https?:\/\//i.test(target)) {
|
|
529
|
-
console.error("❌ Web
|
|
671
|
+
console.error("❌ Web exploration needs an http(s) URL");
|
|
530
672
|
process.exit(2);
|
|
531
673
|
}
|
|
532
674
|
const bundleId = platform === "ios" ? await resolveTargetOrExit(engine, target) : null;
|
|
533
675
|
const android = platform === "android" ? androidTarget(flags, target) : null;
|
|
534
676
|
const progressMetric = platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached";
|
|
535
677
|
const onProgress = (p) =>
|
|
536
|
-
|
|
678
|
+
writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${progressMetric}`);
|
|
537
679
|
const r = platform === "web"
|
|
538
680
|
? await engine.runQaWeb({
|
|
539
681
|
url: target,
|
|
@@ -542,6 +684,7 @@ switch (command) {
|
|
|
542
684
|
testEmail: flags.email,
|
|
543
685
|
testPassword: flags.password,
|
|
544
686
|
baselineFindings,
|
|
687
|
+
watch: flags.watch === true,
|
|
545
688
|
surface: "cli",
|
|
546
689
|
onProgress,
|
|
547
690
|
})
|
|
@@ -565,7 +708,7 @@ switch (command) {
|
|
|
565
708
|
surface: "cli",
|
|
566
709
|
onProgress,
|
|
567
710
|
});
|
|
568
|
-
|
|
711
|
+
finishProgress();
|
|
569
712
|
if (r.error) {
|
|
570
713
|
printEngineError(r);
|
|
571
714
|
process.exit(1);
|
|
@@ -776,8 +919,22 @@ switch (command) {
|
|
|
776
919
|
printEngineError(inst);
|
|
777
920
|
process.exit(1);
|
|
778
921
|
}
|
|
922
|
+
let modelRefresh = null;
|
|
923
|
+
try {
|
|
924
|
+
const { persistIosBuildValidation } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
|
|
925
|
+
modelRefresh = await persistIosBuildValidation({
|
|
926
|
+
projectDir: dir,
|
|
927
|
+
bundleId: inst.bundleId,
|
|
928
|
+
container: built.container,
|
|
929
|
+
scheme: built.scheme,
|
|
930
|
+
configuration: built.configuration,
|
|
931
|
+
});
|
|
932
|
+
} catch (error) {
|
|
933
|
+
console.error(`⚠️ Build succeeded, but Tapp could not refresh the existing application model: ${error.message || String(error)}`);
|
|
934
|
+
}
|
|
779
935
|
console.log(`🔨 Built ${path.basename(built.appPath)} (scheme ${built.scheme}) — installed as ${inst.bundleId}`);
|
|
780
|
-
console.log(
|
|
936
|
+
if (modelRefresh) console.log(` application model refreshed: ${modelRefresh.modelPath}`);
|
|
937
|
+
console.log(`\nNext: npx -y @aarwitz/tapp@latest explore ${inst.bundleId}`);
|
|
781
938
|
break;
|
|
782
939
|
}
|
|
783
940
|
|
|
@@ -860,8 +1017,12 @@ switch (command) {
|
|
|
860
1017
|
const { flags, positionals } = parseVerbArgs(rest);
|
|
861
1018
|
const verb = positionals[0] || "run";
|
|
862
1019
|
const flowPath = positionals[1] || (verb === "run" || verb === "validate" ? "" : verb);
|
|
1020
|
+
if (verb === "example") {
|
|
1021
|
+
console.log(`# Tapp Flow — deterministic, keyless replay\nname: sign-in-smoke\nplatform: web\nurl: https://example.test/login\nsteps:\n - login:\n email: $TEST_EMAIL\n password: $TEST_PASSWORD\n - wait_for: Dashboard\n - assert_screen: Dashboard\n`);
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
863
1024
|
if (!["run", "validate"].includes(verb) || !flowPath) {
|
|
864
|
-
console.error("usage: tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
|
|
1025
|
+
console.error("usage: tapp flow example\n tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
|
|
865
1026
|
process.exit(2);
|
|
866
1027
|
}
|
|
867
1028
|
const absolute = path.resolve(flowPath);
|
|
@@ -910,7 +1071,9 @@ switch (command) {
|
|
|
910
1071
|
invocation = ["bash", [path.join(packageRoot, "scripts", "run-flow.sh"), absolute, typeof flags["bundle-id"] === "string" ? flags["bundle-id"] : flow.app || ""]];
|
|
911
1072
|
}
|
|
912
1073
|
const result = spawnSync(invocation[0], invocation[1], { stdio: "inherit", env });
|
|
913
|
-
|
|
1074
|
+
const evidenceWritten = fs.existsSync(evidenceDir) && fs.readdirSync(evidenceDir).length > 0;
|
|
1075
|
+
if (evidenceWritten) console.log(`\nEvidence: ${evidenceDir}`);
|
|
1076
|
+
else console.error("\n⚠️ Evidence unavailable — the platform runner did not write any artifacts for this Flow run.");
|
|
914
1077
|
process.exit(result.status ?? 1);
|
|
915
1078
|
}
|
|
916
1079
|
|
|
@@ -991,7 +1154,7 @@ switch (command) {
|
|
|
991
1154
|
try {
|
|
992
1155
|
const adopted = adoptPrCoverageProposal({ projectDir, prPlanPath, item: flags.item, releasePlanPath: typeof flags["release-plan"] === "string" ? flags["release-plan"] : undefined });
|
|
993
1156
|
console.log(`📥 ${adopted.mode === "reconciled-existing" ? "Reconciled PR evidence into" : "Adopted"} ${adopted.item.name}${adopted.mode === "reconciled-existing" ? ` while preserving decision '${adopted.item.decision}'` : " as a pending release-plan item"}; no Task or contract was generated or trusted`);
|
|
994
|
-
console.log(` plan: ${adopted.path}\n next: tapp plan review ${adopted.path} --approve ${adopted.item.id}`);
|
|
1157
|
+
console.log(` plan: ${adopted.path}\n next: npx -y @aarwitz/tapp@latest plan review ${adopted.path} --approve ${adopted.item.id}`);
|
|
995
1158
|
} catch (error) { console.error(`❌ Could not adopt PR coverage proposal: ${error.message || String(error)}`); process.exit(2); }
|
|
996
1159
|
break;
|
|
997
1160
|
}
|
|
@@ -1151,6 +1314,13 @@ switch (command) {
|
|
|
1151
1314
|
const python = run("python3", ["--version"]);
|
|
1152
1315
|
python.code === 0 ? ok("python3", `${python.stdout} (used by Flows)`) : bad("python3", "not found — Flow replay needs python3 + pyyaml (everything else works)");
|
|
1153
1316
|
|
|
1317
|
+
const { storagePreflight } = await import(path.join(packageRoot, "mcp-server", "src", "environment-preflight.js"));
|
|
1318
|
+
const storage = storagePreflight(tappHome);
|
|
1319
|
+
if (storage.level === "blocked") { bad("Disk space", storage.message); healthy = false; }
|
|
1320
|
+
else if (storage.level === "warning") console.log(` ⚠️ Disk space — ${storage.message}`);
|
|
1321
|
+
else if (storage.level === "ok") ok("Disk space", storage.message);
|
|
1322
|
+
else console.log(` ⬜ Disk space — ${storage.message || "could not be checked"}`);
|
|
1323
|
+
|
|
1154
1324
|
console.log("\n Platforms:");
|
|
1155
1325
|
if (process.platform === "darwin") {
|
|
1156
1326
|
const xcode = run("xcode-select", ["-p"]);
|
|
@@ -1160,7 +1330,7 @@ switch (command) {
|
|
|
1160
1330
|
const booted = bootedSims();
|
|
1161
1331
|
ok("iOS", `${ver || "Xcode"}; ${booted.length ? `${booted[0].name} booted` : "no simulator booted yet"}`);
|
|
1162
1332
|
const xctestrun = harnessXctestrun();
|
|
1163
|
-
xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: tapp install)");
|
|
1333
|
+
xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: npx -y @aarwitz/tapp@latest install)");
|
|
1164
1334
|
} else {
|
|
1165
1335
|
console.log(" ⬜ iOS — unavailable (install Xcode + simulator runtime)");
|
|
1166
1336
|
}
|
|
@@ -1179,16 +1349,22 @@ switch (command) {
|
|
|
1179
1349
|
}
|
|
1180
1350
|
|
|
1181
1351
|
try {
|
|
1182
|
-
await import("playwright");
|
|
1183
|
-
|
|
1352
|
+
const { chromium } = await import("playwright");
|
|
1353
|
+
let executable = "";
|
|
1354
|
+
try { executable = chromium.executablePath(); } catch { /* report the missing browser below */ }
|
|
1355
|
+
if (executable && fs.existsSync(executable)) {
|
|
1356
|
+
ok("Web", `Playwright + Chromium (${executable})`);
|
|
1357
|
+
} else {
|
|
1358
|
+
console.log(" ⬜ Web — Playwright installed; Chromium browser missing (run: npx playwright install chromium)");
|
|
1359
|
+
}
|
|
1184
1360
|
} catch {
|
|
1185
1361
|
console.log(" ⬜ Web — install Playwright in the app workspace: npm install -D playwright && npx playwright install chromium");
|
|
1186
1362
|
}
|
|
1187
1363
|
|
|
1188
1364
|
console.log(`\n Home: ${tappHome}`);
|
|
1189
1365
|
console.log(healthy
|
|
1190
|
-
? "\nReady. Start with:\n npx -y @aarwitz/tapp open [target]\n npx -y @aarwitz/tapp explore [target]"
|
|
1191
|
-
: "\nFix the ❌ items above, then re-run: tapp doctor");
|
|
1366
|
+
? "\nReady. Start with:\n npx -y @aarwitz/tapp@latest open [target]\n npx -y @aarwitz/tapp@latest explore [target]"
|
|
1367
|
+
: "\nFix the ❌ items above, then re-run: npx -y @aarwitz/tapp@latest doctor");
|
|
1192
1368
|
process.exit(healthy ? 0 : 1);
|
|
1193
1369
|
}
|
|
1194
1370
|
|
|
@@ -1263,7 +1439,13 @@ switch (command) {
|
|
|
1263
1439
|
console.log(`✅ Actor '${name}' configured — ${result.actor.session} session · ${result.actor.provisioning} provisioning`);
|
|
1264
1440
|
console.log(` ${result.path}`);
|
|
1265
1441
|
console.log(` bindings: ${Object.entries(result.actor.credentials).map(([key, binding]) => `${key}=$${binding.env}`).join(", ") || "none"}`);
|
|
1266
|
-
|
|
1442
|
+
const { refreshExistingInitArtifacts } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
|
|
1443
|
+
let refreshed = null;
|
|
1444
|
+
let refreshWarning = "";
|
|
1445
|
+
try { refreshed = await refreshExistingInitArtifacts({ projectDir }); }
|
|
1446
|
+
catch (error) { refreshWarning = error.message || String(error); }
|
|
1447
|
+
console.log(` No credential values were accepted or written.${refreshed ? ` Application model refreshed: ${refreshed.modelPath}` : " Run npx -y @aarwitz/tapp@latest init when you are ready to create the application model."}`);
|
|
1448
|
+
if (refreshWarning) console.error(`⚠️ Actor was saved, but Tapp could not refresh the existing application model: ${refreshWarning}`);
|
|
1267
1449
|
} catch (error) { console.error(`❌ Actor not configured: ${error.message || String(error)}`); process.exit(2); }
|
|
1268
1450
|
break;
|
|
1269
1451
|
}
|
|
@@ -1278,7 +1460,7 @@ switch (command) {
|
|
|
1278
1460
|
const projectDir = fs.realpathSync(path.resolve(positionals[1] || (typeof flags["project-dir"] === "string" ? flags["project-dir"] : process.cwd())));
|
|
1279
1461
|
const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
|
|
1280
1462
|
if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
|
|
1281
|
-
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore, review/generate/validate/promote the plan, then create the baseline.`);
|
|
1463
|
+
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore, review/generate/validate/promote the plan, then create the baseline.`);
|
|
1282
1464
|
process.exit(2);
|
|
1283
1465
|
}
|
|
1284
1466
|
let model;
|
|
@@ -1385,7 +1567,7 @@ switch (command) {
|
|
|
1385
1567
|
catch { console.error(`❌ Repository directory not found: ${positionals[0] || flags["project-dir"] || process.cwd()}`); process.exit(2); }
|
|
1386
1568
|
const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
|
|
1387
1569
|
if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
|
|
1388
|
-
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore first.`);
|
|
1570
|
+
console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore first.`);
|
|
1389
1571
|
process.exit(2);
|
|
1390
1572
|
}
|
|
1391
1573
|
const actionRef = typeof flags["action-ref"] === "string" ? flags["action-ref"] : `aarwitz/tapp@v${pkg.version}`;
|
|
@@ -1502,17 +1684,18 @@ switch (command) {
|
|
|
1502
1684
|
}
|
|
1503
1685
|
|
|
1504
1686
|
default: {
|
|
1505
|
-
console.log(`tapp v${pkg.version} —
|
|
1687
|
+
console.log(`tapp v${pkg.version} — agent-driven app testing for iOS, Android, and web.
|
|
1506
1688
|
|
|
1507
|
-
Core —
|
|
1689
|
+
Core — inspect, explore, gate (no Tapp account or server required):
|
|
1508
1690
|
tapp explore [target] Autonomous exploration → findings + evidence (an observation, NOT a
|
|
1509
|
-
release decision — run 'tapp ci' to gate a merge)
|
|
1510
|
-
(--platform ios|android|web · --
|
|
1691
|
+
release decision — run 'npx -y @aarwitz/tapp@latest ci' to gate a merge)
|
|
1692
|
+
(web: --watch · all: --platform ios|android|web · --actions N)
|
|
1693
|
+
tapp focus "goal" [target] Source-locate a named screen/control and take the shortest observed route
|
|
1511
1694
|
tapp contract run FILE Replay a business-level release contract — the guarantees that must hold
|
|
1512
1695
|
tapp ci ... Merge-blocking release gate — explore + suites + baseline → pass/fail/inconclusive
|
|
1513
1696
|
(see: tapp ci --help)
|
|
1514
1697
|
|
|
1515
|
-
Primitives — an agent's eyes and hands
|
|
1698
|
+
Primitives — an agent's eyes and hands:
|
|
1516
1699
|
tapp open [target] Launch the app → screen summary + screenshot saved to a file
|
|
1517
1700
|
(web: --tap TEXT · --wait-for TEXT · --out FILE)
|
|
1518
1701
|
tapp tree [target] Accessibility tree of the current screen (--json for every element)
|
|
@@ -1528,6 +1711,7 @@ Repository & release:
|
|
|
1528
1711
|
tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
|
|
1529
1712
|
|
|
1530
1713
|
Advanced — deterministic suites, lifecycle & compilers:
|
|
1714
|
+
tapp flow example Print a complete starter Flow YAML (no target or MCP required)
|
|
1531
1715
|
tapp flow run FILE Replay a committed deterministic Flow (no AI/API key)
|
|
1532
1716
|
tapp flow validate FILE Validate a Flow without launching a target
|
|
1533
1717
|
tapp task validate FILE Validate a reusable deterministic Task (+ optional UI Map grounding)
|
|
@@ -1567,10 +1751,15 @@ Setup:
|
|
|
1567
1751
|
tapp doctor Check Xcode / simulators / toolchain
|
|
1568
1752
|
tapp mcp Start the MCP server on stdio (adds inline screenshots + interactive sessions)
|
|
1569
1753
|
|
|
1754
|
+
Agent Skill (recommended — so a short “Use Tapp to test this app” prompt is enough):
|
|
1755
|
+
Any supported agent: npx -y skills add aarwitz/tapp --skill tapp
|
|
1756
|
+
Claude skill + MCP: claude plugin marketplace add aarwitz/tapp
|
|
1757
|
+
claude plugin install tapp@tapp
|
|
1758
|
+
|
|
1570
1759
|
MCP hookup (optional — for inline screenshots and the tap/type/inspect session loop):
|
|
1571
|
-
Claude Code: claude mcp add tapp -- npx -y @aarwitz/tapp mcp
|
|
1760
|
+
Claude Code: claude mcp add tapp -- npx -y @aarwitz/tapp@latest mcp
|
|
1572
1761
|
Cursor/VS Code (mcp.json):
|
|
1573
|
-
{ "servers": { "tapp": { "type": "stdio", "command": "npx", "args": ["-y", "@aarwitz/tapp", "mcp"] } } }
|
|
1762
|
+
{ "servers": { "tapp": { "type": "stdio", "command": "npx", "args": ["-y", "@aarwitz/tapp@latest", "mcp"] } } }
|
|
1574
1763
|
|
|
1575
1764
|
Then ask your agent things like:
|
|
1576
1765
|
"Explore com.mycompany.app and show me what breaks"
|
package/docs/BROWSER-PRODUCT.md
CHANGED
|
@@ -16,6 +16,15 @@ resolution actually builds and installs the detected Xcode container, the model
|
|
|
16
16
|
scheme as runtime-observed validation and removes the corresponding confirmation blocker. Merely
|
|
17
17
|
supplying a bundle id or prebuilt `.app` does not prove repository build configuration.
|
|
18
18
|
|
|
19
|
+
A repository with more than one detected application target is never resolved by detection order
|
|
20
|
+
during explicit initialization, even when a prior default exists. Bare `tapp init . --explore`
|
|
21
|
+
prompts in a human TTY; non-interactive CLI/MCP callers receive
|
|
22
|
+
exact `--platform`/`--target` commands before any build or write, and MCP also carries them as
|
|
23
|
+
structured `target-selection-required` choices. The selected run records that target as the default
|
|
24
|
+
for later bare `tapp explore`, but the application model retains the repository's other detected
|
|
25
|
+
targets and their unmet coverage. The selected init run reports other-target setup gaps as deferred
|
|
26
|
+
information rather than presenting them as failures of the target that was actually explored.
|
|
27
|
+
|
|
19
28
|
## First inspection
|
|
20
29
|
|
|
21
30
|
```bash
|
|
@@ -49,8 +58,8 @@ tapp init . --refresh --explore --platform web --url http://127.0.0.1:3000
|
|
|
49
58
|
|
|
50
59
|
MCP clients use `tapp_init` with `operation: inspect|write|refresh|explore`. `inspect` is the safe
|
|
51
60
|
default. `explore` writes real evidence, so the CLI rejects `--explore --dry-run`; the CLI also
|
|
52
|
-
|
|
53
|
-
runtime and are never written into the model, map, or plan.
|
|
61
|
+
refreshes existing model/plan artifacts through the same decision-preserving semantics. Credentials
|
|
62
|
+
are passed only to the runtime and are never written into the model, map, or plan.
|
|
54
63
|
|
|
55
64
|
Successful repository-driven iOS build validation is portable and durable. The application model
|
|
56
65
|
stores the repository-relative container, scheme, configuration, bundle id, and a
|
|
@@ -227,7 +236,7 @@ tapp baseline create . --platform web
|
|
|
227
236
|
tapp baseline create . --platform web --from /path/to/tapp-report.json
|
|
228
237
|
|
|
229
238
|
# Generate one target-aware job per model target plus a machine-readable manifest.
|
|
230
|
-
tapp ci install . --action-ref aarwitz/tapp@v0.
|
|
239
|
+
tapp ci install . --action-ref aarwitz/tapp@v0.17.0
|
|
231
240
|
```
|
|
232
241
|
|
|
233
242
|
Baseline creation rejects non-gate JSON, missing or mismatched target identity, platform mismatch,
|
package/docs/scenarios.md
CHANGED