Skip to content

Latest commit

 

History

History
327 lines (261 loc) · 17 KB

File metadata and controls

327 lines (261 loc) · 17 KB

10-Minute Quick Path

This is the shortest way to prove to yourself — or to an agent with zero prior context — that Patchbay works, using only the app and CLI already checked into this repository. No app integration work, no writing Dart. If you already have a Patchbay-enabled app of your own, skip this and use the README Quick Start instead; this page exists to get a first result in minutes, not to teach integration.

Six steps: install, identity, catalog, snapshot, one safe write through ui perform, and a capture. Every command below is copy-pasteable and was checked against this checkout's own patchbay help output. Each step says what you should see and which error.code to look for if it does not work that way.

Prerequisites: a working Dart and Flutter toolchain on PATH (Dart >=3.12.0, Flutter >=3.44.0) and a Flutter target you can normally flutter run against — simulator, emulator, physical device, or desktop all work, since Patchbay only needs the Dart VM Service URI that flutter run prints, not a specific platform. A clean checkout of this repository. The 10-minute budget assumes that toolchain already works, flutter run's first build is not unusually slow, and the example's mobile platform directory already exists or you take the non-interactive path in step 1 below — generating that directory from scratch (see step 1) is a one-time cost on top of the budget.

If any step below fails to connect at all, run patchbay doctor first — it checks session, connection, catalog and app lifecycle in one pass and reports which one broke; see Doctor for how to read it. The full syntax for every command mentioned here is in patchbay help <command>; the full error code catalog is in Exit codes.

1. Install

From the repository root, this checkout is not a tagged release, so build the CLI from source once and fetch dependencies:

$ dart pub get
$ (cd packages/patchbay_flutter/example && flutter pub get)
$ dart run packages/patchbay_cli/tool/build_cli.dart
$ export PATH="$PWD/packages/patchbay_cli/build:$PATH"

You'll see: dart/flutter pub get resolving dependencies for the two packages, then Built .../build/patchbay (N.N MiB) from the build script. patchbay --help should now print the command group list from any directory.

The bundled example ships with no platform directory. This is a four-package repository, not a maintained Flutter app project — example/.gitignore excludes ios/, android/, and the other platform folders on purpose, so a simulator, emulator, or physical device target does not exist yet in a fresh checkout. Generate the one you need, once, from inside packages/patchbay_flutter/example:

$ (cd packages/patchbay_flutter/example && flutter create --platforms=ios .)   # or --platforms=android

flutter create runs its own pub get outside the checked-in lockfile, which can bump a transitive dependency in pubspec.lock and add a template test/widget_test.dart that does not match this example's own main.dart. Both are side effects of the generator, not changes you meant to make — undo them before committing anything from this checkout:

$ git checkout -- packages/patchbay_flutter/example/pubspec.lock
$ rm -f packages/patchbay_flutter/example/test/widget_test.dart

(The non-interactive path below performs the same generation for you and warns you on stderr if it touches pubspec.lock, so you can skip doing this by hand if you use it.)

Now run the bundled example app and copy the VM Service URI it prints:

$ cd packages/patchbay_flutter/example
$ flutter run

You'll see: Flutter's normal build/launch output, ending with a line like A Dart VM Service on <device> is available at: http://127.0.0.1:<port>/<token>=/ and a running app showing a counter and a "Debug note" text field. Copy that URI — it changes on every flutter run, and it carries authentication material, so keep it out of scripts, shell history files, and anything you commit.

If this doesn't work: an error like No application found for TargetPlatform.ios. Is your project missing an ios/Runner/Info.plist? (or the Android equivalent) means the platform directory above is still missing — go back and generate it. Anything else at this step is an ordinary Flutter-level failure (no device found, build error), not Patchbay's — resolve it with flutter doctor / flutter run troubleshooting before continuing.

Non-interactive path (scripts and agents)

An agent or a script cannot drive the interactive flutter run above: the process stays in the foreground and never returns, and copying a URI out of its stdout by hand does not scale. Source the repository's own session helper instead — it also generates the platform directory for you, so it replaces both this step and the one above:

$ source tool/example_session.sh
$ example_session_start <device-id>

Keep the launch terminal alive until step 6 is complete. Use a persistent terminal session and leave its shell open after example_session_start returns. Some agent hosts reclaim child processes when a one-shot shell command finishes; the helper reporting ready does not make the app independent of that shell. Run the remaining steps in that terminal, or keep it alive while using separate command shells as described below.

<device-id> is whatever flutter devices lists for your target (an adb serial for Android, a xcrun simctl list devices UDID for an iOS Simulator); omit it to use the first online adb device. On success it prints [session] 会话已就绪:<id> and exports PATCHBAY_SESSION_ID (and PATCHBAY_SESSION_DIR) — the URI itself is never printed and never touches shell history. Run the commands in steps 2 through 6 through its example_session_cli wrapper instead of a bare patchbay --ws-uri call, for example example_session_cli --json catalog; it forwards to the same --session mechanism Automatic session discovery describes. Call example_session_stop when you're done to end the app process and clean up the session record.

The helper's own status lines are written in Chinese and all start with [session]; they are progress and warnings from the helper, not Patchbay responses, and none of them carries an error.code.

The helper locates this checkout on its own whether you source it from bash or zsh. If it stops with [session] 无法定位仓库根 instead, set PATCHBAY_REPO_ROOT to the root directory of this checkout and source it again — an explicit PATCHBAY_REPO_ROOT always wins over auto-detection.

If your runner starts a fresh shell per command (most agent hosts do), the exported PATCHBAY_SESSION_ID does not survive to the next command. Keep the id from the [session] 会话已就绪:<id> line, then in each later shell source tool/example_session.sh and export PATCHBAY_SESSION_ID=<id> before calling example_session_cli; PATCHBAY_SESSION_DIR re-derives itself from the checkout, so you only have to carry the id.

This restores the variables, not the launcher process. The CLI judges a session by its endpoint, not the recorded PID, and keeps the records it refuses. patchbay launch also records the App's device-side VM Service endpoint (when it can read it off the DDS process) and tries it before the host-side DDS, so a terminal killed (SIGKILL, hang-up) while the App kept running leaves the same session id usable. A graceful stop (Ctrl-C, SIGTERM) stops the App too, and on an iOS device the endpoint survives only through a flutter implementation detail. If identity returns sessionStaleProcess, restart the helper, use the new id, and repeat the read steps before writing.

1b. The smallest read-only integration

If you are wiring Patchbay into your own app rather than running the bundled example, this is the whole read-only surface. It is not a sketch — the block below is generated from packages/patchbay_flutter/example/lib/minimal_readonly_root.dart, which the example compiles and tests on every run, so it cannot drift from an API that has moved on.

/// Everything a read-only integration has to declare. Call it once from `main()`.
///
/// Three things, none of them optional:
///
/// 1. **Unreachable in release.** `kReleaseMode` is a compile-time constant, so AOT
///    tree-shakes this branch together with the host and bridge it builds. Do not turn
///    it into a runtime flag — that leaves the symbols in the artifact.
/// 2. **You write the base gate yourself.** It is not optional: even read-only commands
///    pass through it. Checking a precondition before returning `allow()` — "is the
///    controller attached yet?" — is exactly what it is for.
/// 3. **Writes are closed by default, and closed in a way you can read.** The consumer
///    gate below refuses every gate id and says in `notice` who can open it. That
///    sentence is what shows up in the CLI's rejection details, so write it for the
///    person who will read it there.
void installReadOnlyPatchbay({
  required PatchbayUiRegistry registry,
  required String applicationId,
  required Future<Map<String, Object?>> Function() snapshot,
}) {
  if (kReleaseMode) return;

  final PatchbayFlutterBridge bridge = PatchbayFlutterBridge(
    gates: PatchbayGateEvaluator(
      baseGate: () async => const PatchbayGateDecision.allow(),
      consumerGate: (String id) async => PatchbayGateDecision.reject(
        code: 'writesNotAuthorized',
        notice:
            'This build only answers read-only commands. To open "$id", '
            'declare it in the app composition root.',
      ),
    ),
    registry: registry,
    rootController: PatchbayRootController.instance,
  );

  PatchbayFlutterServiceHost(
    applicationId: applicationId,
    bridge: bridge,
    // No `domainCatalog`: a read-only integration registers no domain commands,
    // so `catalog` answers with the framework's own entries and nothing else.
    snapshot: snapshot,
  ).register();
}

What you get with this and nothing else: identity, catalog, snapshot, doctor and the diagnostic trees all answer. Every write refuses with consumerGateRejected, and the notice you wrote above is what the operator reads in the rejection's details — patchbay doctor shows the same thing. That is the intended state for a first integration: the read-only path works before you have authorized a single write.

When you do want a write path, declare the gate in the composition root and open it there. There is deliberately no permissiveDebug() or readOnly() preset to call: a preset would be copied into production together with the default it changed, and the default that matters here is the one that refuses.

2. Say hello: identity

$ export WS_URI='<the VM Service URI you just copied>'
$ patchbay --ws-uri "$WS_URI" identity

(exporting it once avoids retyping the URI in every command below; nothing about that variable is Patchbay-specific.)

You'll see: one human-readable line naming the app and the instance it handed you — for this example dev.patchbay.example instance=<id>. That is the whole point of this step: exit code 0 and no error means the minimal read-only path is already working. Add --json (as every step below does) when you want the full handshake instead — applicationId, appInstanceId, isolateId, schemaVersion, serverVersion and the features this host declares.

If it fails: exit 3 means no valid connection — the most common cause at this step is a stale URI (each flutter run mints a new one); re-copy it. Exit 4 means a schema/identity mismatch. Either way, patchbay doctor will name the broken layer.

3. Discover: catalog

$ patchbay --ws-uri "$WS_URI" --json catalog

You'll see: the commands and UI targets this running app actually registers — this is the only source of truth for what is callable; each entry under uiTargets carries an id and a generation you will need in step 5. The example registers example.note (a text field) as a target. Accessibility nodes such as the counter button (example.counter.increment) are not registered targets and do not appear here — step 5 reads them from ui semantics tree. Nothing else in this guide, the README, or the Skill is a substitute for reading this output yourself. If the plain output is too long to read, add --view summary to keep each command's name, summary, side effect and gates, or --view brief to keep the decision facts and drop the bulky field (it is always named in localView.omitted, never silently missing).

If it fails: same connection-class codes as step 2. A patchbay describe <command> on any name you see here shows its live parameters and gates without invoking it.

4. Read: snapshot

$ patchbay --ws-uri "$WS_URI" --json snapshot

You'll see: the app's typed state tree, with every value tagged by its fact source (app-recorded / command echo / device-reported / UI-observed) — not a plain value dump. Narrow it with --path <dot.path> (for example --path counter) once you know the field you want; use snapshot wait <dot.path> --until exists|absent|equals [<json>] to have the app wait for a condition instead of polling yourself.

If it fails: the same connection-class codes apply; a bad --path fails closed rather than guessing at a partial match.

5. Make a safe write: ui perform

ui perform is the one canonical entry point for every UI write in this version; it always takes an explicit selector (target:, semantics:, or node:) and the generation you observed before writing — a write against a stale generation is rejected, not silently retried. The two identity domains keep separate counters: a target: generation comes from catalog's uiTargets (step 3, patchbay --json catalog); a semantics: or node: generation comes from ui semantics tree, never from catalog.

Write into the registered text target:

$ patchbay --ws-uri "$WS_URI" ui perform enter-text target:example.note <generation> "Hello from Patchbay"

You'll see: the app's "Debug note" field now shows that text, and the CLI exits 0. Substitute <generation> with the value you read for id: "example.note" in step 3.

Or dispatch a real accessibility tap on the counter button. First read the semantics tree and find the node whose identifier is example.counter.increment; its generation is the value to pass:

$ patchbay --ws-uri "$WS_URI" --json --view brief ui semantics tree
$ patchbay --ws-uri "$WS_URI" ui perform tap semantics:example.counter.increment <generation> --via semantics

You'll see: the on-screen counter increments by one. (--view brief keeps the node list readable; the full tree is one --view full away.)

If it fails: uiTargetNotFound / uiSemanticsIdentifierNotFound means that selector is not currently registered or mounted — re-read catalog. uiGenerationStale / uiSemanticsGenerationStale means the target remounted since you read its generation — the reply carries currentGeneration; read it again and resend, the CLI never retries this for you. uiSemanticsIdentifierAmbiguous means more than one mounted node currently matches. A gate rejection (baseGateRejected, consumerGateRejected, or a consumer-declared code such as unknownConsumerGate) means the app itself declined the write; this bundled example ships with its one write gate open, so you should not see this here — it is what to expect against an app that has not opened that capability.

6. Capture

$ patchbay --ws-uri "$WS_URI" --output note.png capture root

You'll see: exit 0 and a PNG written to note.png — the Flutter root repaint boundary at the moment of the call, including the text and counter value from step 5.

If it fails: if note.png already exists you get a usageError (exit 64) telling you to add --force or pick a new path; connection-class failures are the same codes as the earlier steps.

Where to go next

You have now run the whole read → write → capture path this repository ships end to end. From here:

The usage guide is the complete reference and is currently Chinese-only; this page is the English entry point into it, not a replacement for it.