appduct 0.13.0 → 0.15.0
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 +55 -24
- package/dist/artifact-inspect.d.ts +38 -39
- package/dist/bin.js +106 -7
- package/dist/cli/result-types.d.ts +35 -2
- package/dist/cli/routes/daemon/index.js +90 -2
- package/dist/cli/routes/daemon/run.js +1088 -517
- package/dist/cli/routes/daemon/start.js +98 -2
- package/dist/cli/routes/daemon/status.js +99 -2
- package/dist/cli/routes/daemon/stop.js +98 -2
- package/dist/cli/routes/doctor.js +98 -3
- package/dist/cli/routes/events/index.d.ts +0 -3
- package/dist/cli/routes/events/index.js +92 -3
- package/dist/cli/routes/events/ls.d.ts +5 -0
- package/dist/cli/routes/events/ls.js +1837 -0
- package/dist/cli/routes/events/since.js +98 -2
- package/dist/cli/routes/events/tail.js +98 -2
- package/dist/cli/routes/init.js +265 -108
- package/dist/cli/routes/keygen.js +90 -2
- package/dist/cli/routes/mcp.js +243 -25
- package/dist/cli/routes/sessions/index.js +90 -2
- package/dist/cli/routes/sessions/link.js +199 -22
- package/dist/cli/routes/sessions/ls.js +98 -2
- package/dist/cli/routes/sessions/revoke.js +98 -2
- package/dist/cli/routes/tools/call.js +98 -2
- package/dist/cli/routes/tools/describe.js +98 -2
- package/dist/cli/routes/tools/index.js +90 -2
- package/dist/cli/routes/tools/ls.js +98 -2
- package/dist/client/attach-page.d.ts +39 -0
- package/dist/client/bootstrap.d.ts +17 -3
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +286 -112
- package/dist/commands/doctor.d.ts +3 -4
- package/dist/commands/events-ls.d.ts +21 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/link.d.ts +10 -1
- package/dist/daemon/calls.d.ts +3 -1
- package/dist/daemon/config.d.ts +6 -0
- package/dist/daemon/daemon.d.ts +6 -1
- package/dist/daemon/event-bus.d.ts +10 -0
- package/dist/daemon/links.d.ts +9 -5
- package/dist/daemon/listener.d.ts +28 -1
- package/dist/daemon/registry.d.ts +20 -12
- package/dist/daemon/rpc-errors.d.ts +6 -0
- package/dist/daemon/sessions.d.ts +14 -8
- package/dist/daemon/tls.d.ts +0 -3
- package/dist/devtools-relay/attach-tab.d.ts +33 -0
- package/dist/devtools-relay/index.d.ts +8 -0
- package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
- package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
- package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
- package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
- package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
- package/dist/devtools-relay/ports.d.ts +48 -0
- package/dist/devtools-relay/relay.d.ts +16 -0
- package/dist/index.js +106 -7
- package/dist/link.d.ts +35 -1
- package/dist/mcp/app-tools.d.ts +19 -0
- package/dist/mcp/connect-tool.d.ts +15 -2
- package/dist/mcp/events-tool.d.ts +39 -0
- package/dist/native-scheme.d.ts +11 -7
- package/dist/scheme.d.ts +5 -1
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[![Appduct][appduct-banner]][repo]
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Call functions in your running iOS, Android, React Native, Flutter or web app from a terminal, a test or an agent
|
|
4
4
|
|
|
5
5
|
[![MIT license][license-badge]][license] [![npm downloads][npm-downloads-badge]][npm-downloads] [![PRs Welcome][prs-welcome-badge]][prs-welcome]
|
|
6
6
|
|
|
@@ -8,19 +8,19 @@ The `appduct` package is the operator/agent side of Appduct: a CLI and an MCP se
|
|
|
8
8
|
|
|
9
9
|
## Why use this package
|
|
10
10
|
|
|
11
|
-
- **One daemon, many devices.** It
|
|
11
|
+
- **One daemon, many devices.** It starts on first use and serves every connected iOS, Android and React Native app on one port. A session survives backgrounding, network drops and React Native reloads: it pauses while the app is away and resumes when it reconnects.
|
|
12
12
|
- **Same surface for humans and agents.** The CLI (`sessions`, `tools`, `events`, ...) and `appduct mcp` use the same RPC methods, so an agent sees the tools a human operator does.
|
|
13
|
-
- **
|
|
13
|
+
- **Local to your user account.** The CLI and MCP server reach the daemon through a local socket only your user account can open, and the daemon's key and logs are readable only by you (see [Security][security]).
|
|
14
14
|
- **Fits production-minded apps.** The app only exposes what you register; trust boundaries are pins and TLS, and production deployments can gate tools with policy and audit every call.
|
|
15
15
|
|
|
16
16
|
## Getting started
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install -g appduct
|
|
20
|
-
appduct sessions link --
|
|
20
|
+
appduct sessions link --qr
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`sessions link` needs your app's deep-link scheme
|
|
23
|
+
`sessions link` needs your app's deep-link scheme. Run from your app's directory, it usually finds the scheme on its own, in `app.json` or the iOS and Android project files (see [The deep-link scheme](#the-deep-link-scheme)). `--scheme` overrides whatever it finds.
|
|
24
24
|
|
|
25
25
|
Scan the QR (or open the deep link) in an Appduct-enabled app, then:
|
|
26
26
|
|
|
@@ -40,14 +40,15 @@ Every command is `appduct <noun> <verb> [selector] [args]`, the same shape as `a
|
|
|
40
40
|
| `appduct init [--scheme <s>] [--ios-app-id <id>] [--android-app-id <id>] [--force]` | set up an app directory: write `.appduct/config.json`, print the MCP snippet |
|
|
41
41
|
| `appduct keygen [--out <path>] [--force]` | generate a daemon private key, print its app pin |
|
|
42
42
|
| `appduct sessions ls` | list sessions: alias, state, device, tool count |
|
|
43
|
-
| `appduct sessions link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--app-id <id>] [--scheme <s>]` | mint a pending session and print its deep link |
|
|
43
|
+
| `appduct sessions link [--ttl <s>] [--qr] [--open android\|ios-sim\|ios-device] [--device <id>] [--app-id <id>] [--relaunch] [--scheme <s>]` | mint a pending session and print its deep link |
|
|
44
44
|
| `appduct sessions revoke [selector]` | revoke a session |
|
|
45
45
|
| `appduct tools ls [selector] [--full] [--group <name>] [--filter <text>] [--limit <n>] [--offset <n>]` | list a session's tools (one call signature + description per line) |
|
|
46
46
|
| `appduct tools ls [selector] --groups` | list a session's tool groups with their tool counts |
|
|
47
47
|
| `appduct tools describe [selector] <name>` | show one tool's full schema |
|
|
48
48
|
| `appduct tools call [selector] <name> --input '<json>' [--timeout <ms>]` | call a tool |
|
|
49
|
-
| `appduct events
|
|
50
|
-
| `appduct events
|
|
49
|
+
| `appduct events ls [selector] [--name <glob>] [--limit <n>] [--offset <n>]` | list the events the app declares, one signature line each; an exact `--name` prints the full payload schema; `--limit`/`--offset` page a long list, ending with a `Showing n of total events (offset o).` line when events were left out; `--json` carries the full descriptors, with `total` counting matches before paging |
|
|
50
|
+
| `appduct events tail [selector] [--follow] [--name <glob>] [--payload-max-bytes <n>]` | stream the events the app posts with `postEvent`; `--name` keeps only matching names, `--payload-max-bytes` cuts each payload down to a preview; `--json` emits NDJSON |
|
|
51
|
+
| `appduct events since [selector] <cursor> [--name <glob>] [--payload-max-bytes <n>]` | one-shot pull the events retained since `<cursor>`, with the same filters as `tail`; `--json` emits NDJSON |
|
|
51
52
|
| `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
|
|
52
53
|
| `appduct mcp [--scheme <s>]` | start a stdio MCP server proxying connected apps' tools to MCP clients |
|
|
53
54
|
| `appduct doctor <artifact> [--assert-present\|--assert-absent]` | release-gate step: report or assert whether a built `.app`/`.ipa`/`.apk`/`.aab` contains Appduct |
|
|
@@ -76,7 +77,7 @@ Use `--filter <text>` to narrow the listing to tools whose name or description c
|
|
|
76
77
|
|
|
77
78
|
### Tool groups
|
|
78
79
|
|
|
79
|
-
When the app puts its tools in groups ([
|
|
80
|
+
When the app puts its tools in groups ([Write tools](https://callstackincubator.github.io/appduct/guides/writing-tools/#group-tools-in-a-large-app)), `appduct tools ls` lists them under group headings, with subgroups indented under their parent and ungrouped tools last:
|
|
80
81
|
|
|
81
82
|
```
|
|
82
83
|
Tools
|
|
@@ -125,13 +126,15 @@ $ appduct tools ls --group checkout
|
|
|
125
126
|
3. **iOS**: any `Info.plist` up to two levels below the app root (excluding `Pods`, `build`, `node_modules`, `DerivedData`) for the first `CFBundleURLSchemes` entry, then xcodegen's `project.yml` for the same key under `info.properties.CFBundleURLTypes`
|
|
126
127
|
6. otherwise an error naming every location above
|
|
127
128
|
|
|
129
|
+
In a Flutter project root (a directory with a `pubspec.yaml`), the Android probes read `android/app/` instead of `app/`, and the iOS and macOS ones find `ios/Runner/Info.plist` and `macos/Runner/Info.plist`. `appduct init` there prints the Flutter setup steps instead of the React Native reminder.
|
|
130
|
+
|
|
128
131
|
In an Expo app root, none of this needs configuring; a plain Xcode or Gradle app root doesn't either, once its `Info.plist`/`AndroidManifest.xml`/`build.gradle` already declares deep links. These probes never execute `app.config.js`/`app.config.ts`, `xcodebuild`, `plutil`, or a Gradle evaluation — each is a plain, defensively-parsed read of a static project file. If your project uses dynamic config, or the probes can't make sense of your setup, use `--scheme`, `APPDUCT_SCHEME`, or `appduct init --scheme <s>` instead.
|
|
129
132
|
|
|
130
133
|
**Disagreement is never guessed away.** If more than one native probe resolves to a different scheme (say, an `android/` and an `ios/` tree in the same repo declaring different values), resolution fails with a usage error naming every conflicting source instead of picking one.
|
|
131
134
|
|
|
132
|
-
`appduct init`, run in an app root, writes `.appduct/config.json` (mode `0600`, in a `0700` directory) with the discovered scheme and prints the MCP server entry to paste, plus the `import "@appduct/react-native/auto"` reminder. It never generates keys or touches daemon state.
|
|
135
|
+
`appduct init`, run in an app root, writes `.appduct/config.json` (mode `0600`, in a `0700` directory) with the discovered scheme and prints the MCP server entry to paste, plus the wiring your app still needs: the `import "@appduct/react-native/auto"` reminder for a React Native or Expo app, or the step a plain iOS or Android app needs (`Appduct.shared.handle(url)`, or the `appductScheme` placeholder). A `react-native` or `expo` dependency in your `package.json` is what says this is a React Native app; running `init` from the app's `ios/` or `android/` directory finds that manifest where the native toolchain puts you. It never generates keys or touches daemon state.
|
|
133
136
|
|
|
134
|
-
`init` only looks at `--scheme` and the static-file discovery in step 5 above (`app.json`, then the native probes), plus whatever the file already records — it deliberately ignores `APPDUCT_SCHEME` and does not walk up to a parent `.appduct/config.json`, since baking either into a committed file would be wrong (a shell variable that happened to be exported, or a parent project's scheme silently copied into a sub-package). `--json`'s `source` field reports which input won: `"--scheme"`, `"app.json"`, one of the four native-probe names (`"android-gradle"`, `"android-manifest"`, `"ios-info-plist"`, `"ios-project-yml"`), or `"already-recorded"`.
|
|
137
|
+
`init` only looks at `--scheme` and the static-file discovery in step 5 above (`app.json`, then the native probes), plus whatever the file already records — it deliberately ignores `APPDUCT_SCHEME` and does not walk up to a parent `.appduct/config.json`, since baking either into a committed file would be wrong (a shell variable that happened to be exported, or a parent project's scheme silently copied into a sub-package). `--json`'s `source` field reports which input won: `"--scheme"`, `"app.json"`, one of the four native-probe names (`"android-gradle"`, `"android-manifest"`, `"ios-info-plist"`, `"ios-project-yml"`), or `"already-recorded"`. `platforms` lists every native platform discovery saw (`["android", "ios"]` for a root that spans both), on every run including `"already-recorded"` and `--scheme`, and is left out when it saw none; `init` prints the wiring step for each platform in it.
|
|
135
138
|
|
|
136
139
|
It's idempotent and safe to re-run:
|
|
137
140
|
|
|
@@ -222,9 +225,9 @@ Your MCP client asks permission for `appduct_call_tool` as a single tool, so cho
|
|
|
222
225
|
|
|
223
226
|
Set this before you connect: the daemon reads `config.json` only when it starts, so changing it later takes `appduct daemon stop`. That disconnects every device, which then has to link again, and leaves a running `appduct mcp` without a daemon, so restart the MCP server in your client as well.
|
|
224
227
|
|
|
225
|
-
|
|
228
|
+
Five more built-in tools cover what the app's own tools can't. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there's nothing to deliver to. Delivering to `android` (chosen or detected) needs `appId`, resolved the same way as `--app-id` (see [Delivering the link to a device](#delivering-the-link-to-a-device)); passing it with `target: "ios-sim"` or `"none"` is an error. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `appId` and the [prerequisites above](#--open-ios-device-experimental) — it's experimental and never auto-detected, so an agent has to ask for it by name.
|
|
226
229
|
|
|
227
|
-
The other
|
|
230
|
+
The other three give an agent a pull surface over `postEvent()`-pushed app events. `appduct_list_events` lists the events the app declared (`appduct events ls`) as signatures with descriptions, plus the payload schema when `name` is an exact name, so an agent learns the names before it waits on one. `appduct_events` drains everything retained since a cursor, and `appduct_wait_for_event` blocks for the next event whose name matches `name`, a whole-name glob (`*` waits for any name) — checking what's already retained before waiting live — rejecting with `tool_timeout` if none arrives in time.
|
|
228
231
|
|
|
229
232
|
## Test runners: `appduct/client`
|
|
230
233
|
|
|
@@ -276,11 +279,11 @@ const next = await app.waitForEvent("checkout_done", { since: cursor });
|
|
|
276
279
|
const capped = await app.waitForEvent("checkout_done", { payloadMaxBytes: 1024 }); // check `truncated` before reading `payload`
|
|
277
280
|
```
|
|
278
281
|
|
|
279
|
-
For
|
|
282
|
+
For anything the client doesn't cover, the package's main entry exports `runCli(argv, { stdout, stderr })`, which runs any `appduct` command in your Node or Bun process and resolves to its exit code.
|
|
280
283
|
|
|
281
284
|
## Keys and pins
|
|
282
285
|
|
|
283
|
-
You can skip this entirely while your app has no `cliPins` configured — the zero-config default, in any build type. The daemon auto-generates its own `key.pem` the first time it starts if one isn't already there (mode `0600`) and prints its `sha256/...` fingerprint on that first run; `appduct sessions link` carries that fingerprint on the deep link for the app to pick up. See [
|
|
286
|
+
You can skip this entirely while your app has no `cliPins` configured — the zero-config default, in any build type. The daemon auto-generates its own `key.pem` the first time it starts if one isn't already there (mode `0600`) and prints its `sha256/...` fingerprint on that first run; `appduct sessions link` carries that fingerprint on the deep link for the app to pick up. See [Security][security]'s "Trust modes" for what that does and doesn't protect.
|
|
284
287
|
|
|
285
288
|
For a build that should trust only a key you embedded ahead of time, generate one explicitly:
|
|
286
289
|
|
|
@@ -290,33 +293,61 @@ appduct keygen
|
|
|
290
293
|
|
|
291
294
|
This writes an unencrypted PEM private key (PKCS#8) to `<state-dir>/key.pem` by default (override with `--out`; add `--force` to overwrite) and prints the exact `sha256/...` SPKI fingerprint your app should place into `cliPins`. It runs non-interactively, so it's safe to call from CI or a setup script.
|
|
292
295
|
|
|
293
|
-
|
|
296
|
+
The deep link `appduct sessions link` prints carries the daemon's current fingerprint in its `pin` parameter. A build with embedded `cliPins` ignores it: embedded pins always win, in every build type. A build with no embedded pins trusts it for that one session.
|
|
294
297
|
|
|
295
298
|
## Daemon lifecycle
|
|
296
299
|
|
|
297
|
-
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It
|
|
300
|
+
The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run `appduct daemon start` yourself. It keeps its key, config, logs and audit log in the state directory (`~/.appduct` by default), runs one instance per state directory, and keeps running whatever happens to a device, so an app reload or crash never takes the daemon down.
|
|
298
301
|
|
|
299
302
|
Use `appduct daemon status` to see what's running (version, pid, `wssPort`, pinned keys, live sessions, effective policy, and the audit log's retention window, file count, size, and failure counters) and `appduct daemon stop` to shut it down explicitly.
|
|
300
303
|
|
|
301
304
|
Because the daemon outlives the CLI that started it, upgrading Appduct would otherwise leave the old daemon serving your commands. Every CLI/MCP process compares its version with the daemon's on its first command and replaces a stale daemon when nothing would be lost; when there is something at stake — connected sessions, or an unexpired link nobody has scanned yet — the command stops instead, naming both versions, until you run `appduct daemon stop` or pass `--daemon-restart`. A daemon newer than your CLI is left alone with a warning, so a project-local install never downgrades a global one's daemon.
|
|
302
305
|
|
|
303
|
-
No log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned, and `events.log` is rotated to `events.log.1` as soon as it passes `eventsLogMaxBytes` (default 10 MiB). `events.log` records the daemon's own events (links, sessions, tool registries and tool calls), one JSON line each, for debugging Appduct itself; your app's events are never written to it. All three are `config.json` keys
|
|
306
|
+
No log grows without bound: `audit/<YYYY-MM-DD>.jsonl` files older than `auditRetentionDays` (default 30) are pruned at daemon start and once a day after, a `daemon.log` over `daemonLogMaxBytes` (default 10 MiB) is rotated to `daemon.log.1` when a daemon is next spawned, and `events.log` is rotated to `events.log.1` as soon as it passes `eventsLogMaxBytes` (default 10 MiB). `events.log` records the daemon's own events (links, sessions, tool registries and tool calls), one JSON line each, for debugging Appduct itself; your app's events are never written to it. All three are `config.json` keys; see below.
|
|
307
|
+
|
|
308
|
+
### Daemon settings: `config.json`
|
|
309
|
+
|
|
310
|
+
`<state-dir>/config.json` (`~/.appduct/config.json` by default) is optional, and so is every key in it. The daemon reads it when it starts, so after editing it run `appduct daemon stop`; the next command starts a fresh daemon with the new values. Stopping the daemon disconnects every device.
|
|
311
|
+
|
|
312
|
+
| Key | Default | What it sets |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| `wssPort` | `8443` | Port devices connect to. `0` picks a free port. |
|
|
315
|
+
| `graceSeconds` | `600` | How long a disconnected session can still resume. |
|
|
316
|
+
| `linkTtlSeconds` | `300` | How long a new link stays valid. Override per link with `--ttl`. |
|
|
317
|
+
| `eventBufferSize` | `256` | App events kept per session for `events since` and `waitForEvent`. |
|
|
318
|
+
| `advertisedIp` | detected | Address put in links for physical devices. |
|
|
319
|
+
| `scheme` | none | Fallback deep-link scheme (step 4 of [The deep-link scheme](#the-deep-link-scheme)). |
|
|
320
|
+
| `restartDaemonOnVersionMismatch` | `false` | Always replace an outdated daemon, even with devices connected. |
|
|
321
|
+
| `policy` | `{ "default": "allow", "destructive": "allow" }` | Which tools callers may run without approval; see [Security][security]. |
|
|
322
|
+
| `auditRetentionDays` | `30` | Days of audit log to keep. |
|
|
323
|
+
| `daemonLogMaxBytes`, `eventsLogMaxBytes` | 10 MiB | Size at which `daemon.log` and `events.log` are rotated. |
|
|
324
|
+
|
|
325
|
+
`keyPath` and `keepaliveIntervalSeconds` exist too; you rarely need them. An unknown key prints a warning and is ignored.
|
|
304
326
|
|
|
305
327
|
## Release gate: `appduct doctor`
|
|
306
328
|
|
|
307
|
-
Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [
|
|
329
|
+
Appduct's inclusion in a build is controlled entirely by autolinking exclusion (see [Build variants][build-variants]) — there's no runtime `debuggable` check to catch a pipeline that forgot to exclude the package. `doctor` checks the built artifact itself, not the config you think produced it:
|
|
308
330
|
|
|
309
331
|
```bash
|
|
310
332
|
appduct doctor ./build/MyApp.ipa --assert-absent
|
|
311
333
|
appduct doctor ./build/app-release.apk --assert-absent
|
|
312
334
|
```
|
|
313
335
|
|
|
314
|
-
|
|
336
|
+
It exits with:
|
|
337
|
+
|
|
338
|
+
| Code | Meaning |
|
|
339
|
+
| --- | --- |
|
|
340
|
+
| `0` | The artifact was inspected, and any `--assert-present`/`--assert-absent` held |
|
|
341
|
+
| `3` | The artifact was inspected and the assertion failed |
|
|
342
|
+
| `66` | The artifact couldn't be inspected: it's missing, unreadable or corrupt, or a tool `doctor` needs (such as `unzip`) isn't installed |
|
|
343
|
+
|
|
344
|
+
A pipeline should fail on anything other than `0`. `66` never means "absent", so a broken runner image can't pass the gate by accident.
|
|
315
345
|
|
|
316
346
|
## Related packages
|
|
317
347
|
|
|
318
|
-
- **[
|
|
319
|
-
- **[
|
|
348
|
+
- **[Appduct for iOS](https://github.com/callstackincubator/appduct/blob/main/packages/native/ios/README.md)** — Swift SDK for native iOS apps.
|
|
349
|
+
- **[Appduct for Android](https://github.com/callstackincubator/appduct/blob/main/packages/native/android/README.md)** — Kotlin SDK for native Android apps.
|
|
350
|
+
- **[@appduct/react-native](https://github.com/callstackincubator/appduct/blob/main/packages/react-native/README.md)** — React Native SDK and Expo config plugin.
|
|
320
351
|
|
|
321
352
|
## Documentation
|
|
322
353
|
|
|
@@ -337,8 +368,8 @@ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_camp
|
|
|
337
368
|
[callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=appduct&utm_term=readme-with-love
|
|
338
369
|
[architecture]: https://github.com/callstackincubator/appduct/blob/main/docs/ARCHITECTURE.md
|
|
339
370
|
[protocol]: https://github.com/callstackincubator/appduct/blob/main/docs/PROTOCOL.md
|
|
340
|
-
[security]: https://github.
|
|
341
|
-
[build-variants]: https://github.
|
|
371
|
+
[security]: https://callstackincubator.github.io/appduct/guides/security/
|
|
372
|
+
[build-variants]: https://callstackincubator.github.io/appduct/guides/build-variants/
|
|
342
373
|
[license-badge]: https://img.shields.io/npm/l/appduct?style=for-the-badge
|
|
343
374
|
[license]: https://github.com/callstackincubator/appduct/blob/main/LICENSE
|
|
344
375
|
[npm-downloads-badge]: https://img.shields.io/npm/dm/appduct?style=for-the-badge
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `appduct doctor
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `appduct doctor`: the artifact-level replacement for the runtime `debuggable`/`#if DEBUG` gate
|
|
3
|
+
* removed elsewhere in opt-in hardening. Given a built `.app`/`.ipa`/`.apk`/`.aab`, decide whether
|
|
4
|
+
* Appduct's native code actually shipped inside it — not whether the config that's supposed to have
|
|
5
|
+
* produced it looks right.
|
|
6
6
|
*
|
|
7
7
|
* The one property everything here is built around: **never report "absent" because a tool was
|
|
8
8
|
* missing or the artifact couldn't be read.** That failure mode is strictly worse than not having
|
|
@@ -14,31 +14,30 @@
|
|
|
14
14
|
* Detection strategy, and why no single signal is authoritative on either platform:
|
|
15
15
|
*
|
|
16
16
|
* - iOS: `present` is decided by real-code-only symbols alone, exactly like Android below —
|
|
17
|
-
* `AppductCoreMarker` (`packages/native/ios/Sources/AppductCore/Real/AppductCoreMarker.swift
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the Android signals below aren't all treated as equally authoritative either.
|
|
17
|
+
* `AppductCoreMarker` (`packages/native/ios/Sources/AppductCore/Real/AppductCoreMarker.swift`),
|
|
18
|
+
* an `@objc` class compiled only into the real implementation, never into the SwiftPM package's
|
|
19
|
+
* `Stub/` branch, so its presence cannot be confused with a stub build. `RCTNativeAppduct`
|
|
20
|
+
* (`RCT_EXPORT_MODULE`, see `packages/react-native/ios/RCTNativeAppduct.mm`) is kept as a second
|
|
21
|
+
* real-code signal for artifacts built before the marker existed. Both are Objective-C runtime
|
|
22
|
+
* metadata — they live in `__objc_classname`/`__objc_data`, and `strip`/release optimization
|
|
23
|
+
* leaves them alone because removing them would break `+[NSObject class]`-based dispatch. (A
|
|
24
|
+
* build that dead-strips the whole translation unit for lack of `-ObjC`/`-force_load` could still
|
|
25
|
+
* drop them — these signals assume the module actually links into the binary, which is the thing
|
|
26
|
+
* being checked in the first place.) The plugin-authored `Info.plist` keys
|
|
27
|
+
* (`AppductCliPins`/`AppductTrust`/`AppductAllowPrivateLanOnly`) are reported as a corroborating
|
|
28
|
+
* signal only and can no longer flip `present` to `true` on their own — an app author can write
|
|
29
|
+
* these keys by hand (or a future stub could ship them) without the real implementation being
|
|
30
|
+
* present, exactly the reason the Android signals below aren't all treated as equally
|
|
31
|
+
* authoritative either.
|
|
33
32
|
* - Android: the primary signal is `AppductNativeMarker`
|
|
34
33
|
* (`packages/native/android/core/src/main/java/com/callstack/appduct/AppductNativeMarker.kt`,
|
|
35
34
|
* vendored into `@appduct/react-native` at `android/core/src/main/java/...`), a marker class with no
|
|
36
35
|
* other purpose. Its fully-qualified name is kept unminified and unremoved by a `-keep` rule in
|
|
37
36
|
* `consumer-rules.pro` (same vendoring path), shipped to every consuming app via `consumerProguardFiles`
|
|
38
37
|
* (see `../build.gradle`) — R8 applies it regardless of whether the app used the Expo config plugin or
|
|
39
|
-
* bare-RN autolinking, and regardless of whether the app authored any keep rules of its own
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* bare-RN autolinking, and regardless of whether the app authored any keep rules of its own.
|
|
39
|
+
* This is the only Android signal guaranteed to survive minification in every supported consumer
|
|
40
|
+
* setup.
|
|
42
41
|
*
|
|
43
42
|
* Two more signals are kept as fallbacks for artifacts built before this marker existed: the
|
|
44
43
|
* `com.callstack.appduct` package string in the dex string pool (and its pre-rename spelling,
|
|
@@ -56,30 +55,34 @@
|
|
|
56
55
|
* a real inclusion to "absent" just because one check missed — but the Info.plist keys signal cannot
|
|
57
56
|
* flip it to `true` by itself, matching Android's marker-only rule below (a stub that ships the same
|
|
58
57
|
* plist keys without the real implementation is exactly the failure mode this guards against, even
|
|
59
|
-
* though no such stub exists yet
|
|
58
|
+
* though no such stub exists yet).
|
|
60
59
|
*
|
|
61
60
|
* On Android, `present` is decided by the keep-rule marker alone, not an OR across all three
|
|
62
61
|
* signals. The other two are reported in `signals` for corroboration/debugging but cannot flip
|
|
63
62
|
* `present` to `true` on their own: `android/build.gradle`'s `APPDUCT_ENABLED`-gated vendored
|
|
64
|
-
* source-directory swap (`core` vs `core-noop
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* the *same* fully-qualified name the real one uses (required so the shared, non-variant-aware
|
|
63
|
+
* source-directory swap (`core` vs `core-noop`; previously a `src/debug`/`src/release-stub` swap)
|
|
64
|
+
* compiles a genuine no-op `AppductPackage` into a default release build at the *same*
|
|
65
|
+
* fully-qualified name the real one uses (required so the shared, non-variant-aware
|
|
68
66
|
* `PackageList.java` still resolves), and the config plugin writes the same manifest meta-data
|
|
69
67
|
* regardless of variant (Expo mods edit the single merged manifest, not a per-variant one). Both
|
|
70
68
|
* fallback signals therefore fire on that harmless stub exactly as they would on the real module, and
|
|
71
69
|
* can no longer prove inclusion by themselves.
|
|
72
70
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
71
|
+
* Flutter: the Dart core writes `appduct-dart-core/` into the AOT snapshot only when
|
|
72
|
+
* `appductEnabled` is true; that string in `libapp.so` / `App.framework/App` is a third
|
|
73
|
+
* authoritative signal on both platforms. The native shim is in every Flutter build and proves
|
|
74
|
+
* nothing, so doctor reports on the Dart core alone.
|
|
75
|
+
*
|
|
76
|
+
* `appduct doctor` is strictly better than the runtime check it replaces: it runs in CI, before
|
|
77
|
+
* distribution, against the thing actually shipped. The keep-rule marker closes the
|
|
78
|
+
* previously-documented gap where a bare-RN app with no config plugin and no keep rule could evade
|
|
79
|
+
* both older Android signals under R8 minification — that gap applied to builds produced before
|
|
80
|
+
* this library shipped the marker/keep rule; an app that pins an older `@appduct/react-native`
|
|
81
|
+
* version still lacks it, and doctor cannot detect inclusion on such a build at all.
|
|
79
82
|
*/
|
|
80
83
|
export type ArtifactPlatform = "ios" | "android";
|
|
81
84
|
export type ArtifactFormat = "app" | "ipa" | "apk" | "aab";
|
|
82
|
-
export type DetectionSignal = "ios-core-marker-symbol" | "ios-objc-class-symbol" | "ios-info-plist-keys" | "android-keep-rule-marker" | "android-dex-package-symbol" | "android-manifest-meta-data-keys";
|
|
85
|
+
export type DetectionSignal = "ios-core-marker-symbol" | "ios-objc-class-symbol" | "ios-info-plist-keys" | "android-keep-rule-marker" | "android-dex-package-symbol" | "android-manifest-meta-data-keys" | "flutter-dart-core-marker";
|
|
83
86
|
export type ArtifactInspection = {
|
|
84
87
|
platform: ArtifactPlatform;
|
|
85
88
|
format: ArtifactFormat;
|
|
@@ -97,10 +100,6 @@ export type ExecBufferResult = {
|
|
|
97
100
|
* corrupt-archive failure without needing either condition to be true on the machine running the
|
|
98
101
|
* tests. */
|
|
99
102
|
export type ExecBufferFn = (command: string, args: string[]) => Promise<ExecBufferResult>;
|
|
100
|
-
/** Real subprocess execution via `execFile`, buffered as raw bytes. `maxBuffer` is generous (an
|
|
101
|
-
* artifact's full decompressed contents are read into memory at once — see {@link readZipEntries})
|
|
102
|
-
* since real `.ipa`/`.apk`/`.aab` files can run into the hundreds of MB. */
|
|
103
|
-
export declare const defaultExecBuffer: ExecBufferFn;
|
|
104
103
|
export type InspectArtifactOptions = {
|
|
105
104
|
exec?: ExecBufferFn;
|
|
106
105
|
};
|
package/dist/bin.js
CHANGED
|
@@ -128,8 +128,11 @@ var createCli = () => {
|
|
|
128
128
|
`link: deep-link URI scheme (also: APPDUCT_SCHEME; default: .appduct/config.json, then app.json's "expo.scheme", then the Android/iOS project files in <cwd>).`
|
|
129
129
|
).option(
|
|
130
130
|
"--open <target>",
|
|
131
|
-
"link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
|
|
132
|
-
).option(
|
|
131
|
+
"link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental), or with `web <url>` return the page URL and a connect script for a web page."
|
|
132
|
+
).option(
|
|
133
|
+
"--browser-url <url>",
|
|
134
|
+
"link: with --open web, attach the tab of a Chrome launched with --remote-debugging-port (such as http://127.0.0.1:9222) and claim a session on it."
|
|
135
|
+
).option("--target-id <id>", "link: with --browser-url, the CDP target id of the tab to attach when several tabs match <url>.").option("--device <id>", "link: adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
|
|
133
136
|
"--app-id <id>",
|
|
134
137
|
`link: installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
|
|
135
138
|
).option(
|
|
@@ -140,10 +143,10 @@ var createCli = () => {
|
|
|
140
143
|
"--timeout <ms>",
|
|
141
144
|
"call: call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
142
145
|
);
|
|
143
|
-
cli.command("events [...args]", "
|
|
146
|
+
cli.command("events [...args]", "List the events an app declares, stream them, or replay them since a cursor: ls, tail, or since.").usage("events <ls|tail|since> [selector] [args]").option("--follow", "tail: accepted for script readability; the default behavior already follows.").option(
|
|
144
147
|
"--name <glob>",
|
|
145
|
-
|
|
146
|
-
).option(
|
|
148
|
+
`Only events whose name matches this whole-name, case-sensitive glob ("*" matches any run of characters). ls: an exact name prints that event's full payload schema.`
|
|
149
|
+
).option("--limit <n>", "ls: show at most n events.").option("--offset <n>", "ls: skip the first n events of the name-sorted list.").option(
|
|
147
150
|
"--payload-max-bytes <n>",
|
|
148
151
|
"Cap each event's payload at this many UTF-8 bytes of its JSON; over the cap, the event carries payloadPreview, payloadBytes and truncated: true instead of payload."
|
|
149
152
|
);
|
|
@@ -171,6 +174,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
171
174
|
"daemonLogMaxBytes",
|
|
172
175
|
"eventsLogMaxBytes",
|
|
173
176
|
"policy",
|
|
177
|
+
"webOrigins",
|
|
174
178
|
"advertisedIp",
|
|
175
179
|
"scheme",
|
|
176
180
|
"restartDaemonOnVersionMismatch"
|
|
@@ -231,6 +235,7 @@ var defaultConfig = (paths) => {
|
|
|
231
235
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
232
236
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
233
237
|
restartDaemonOnVersionMismatch: false,
|
|
238
|
+
webOrigins: [],
|
|
234
239
|
policy: {
|
|
235
240
|
default: "allow",
|
|
236
241
|
destructive: "allow"
|
|
@@ -301,6 +306,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
301
306
|
"restartDaemonOnVersionMismatch"
|
|
302
307
|
);
|
|
303
308
|
}
|
|
309
|
+
if (parsed.webOrigins !== void 0) {
|
|
310
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
311
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
312
|
+
}
|
|
313
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
314
|
+
}
|
|
304
315
|
if (parsed.policy !== void 0) {
|
|
305
316
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
306
317
|
throw configError("policy", "must be an object.");
|
|
@@ -409,7 +420,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
409
420
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
410
421
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
411
422
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
412
|
-
// assertion failure
|
|
423
|
+
// assertion failure.
|
|
413
424
|
inspection_error: 66,
|
|
414
425
|
connection_error: 70,
|
|
415
426
|
session_error: 71,
|
|
@@ -524,6 +535,7 @@ var getExitCodeForError = (error) => {
|
|
|
524
535
|
import pc from "picocolors";
|
|
525
536
|
import {
|
|
526
537
|
formatAgentWebSocketUrl,
|
|
538
|
+
renderEventSignature,
|
|
527
539
|
renderToolSignature,
|
|
528
540
|
summarizeToolDescription
|
|
529
541
|
} from "@appduct/shared";
|
|
@@ -619,6 +631,13 @@ var formatDevice = (device) => {
|
|
|
619
631
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
620
632
|
return device.os ? `${label} (${device.os})` : label;
|
|
621
633
|
};
|
|
634
|
+
var SUSPEND_REASON_LABELS = {
|
|
635
|
+
app_backgrounded: "app in background",
|
|
636
|
+
connection_lost: "connection lost"
|
|
637
|
+
};
|
|
638
|
+
var formatSessionState = (session) => {
|
|
639
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
640
|
+
};
|
|
622
641
|
var renderLsData = (colors, data, now) => {
|
|
623
642
|
if (data.length === 0) {
|
|
624
643
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -627,7 +646,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
627
646
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
628
647
|
const rows = data.map((session) => [
|
|
629
648
|
session.alias,
|
|
630
|
-
session
|
|
649
|
+
formatSessionState(session),
|
|
631
650
|
formatDevice(session.device),
|
|
632
651
|
String(session.toolCount),
|
|
633
652
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -801,10 +820,87 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
801
820
|
}
|
|
802
821
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
803
822
|
};
|
|
823
|
+
var renderEventsTruncationLine = (data) => {
|
|
824
|
+
const offset = data.offset ?? 0;
|
|
825
|
+
if (data.events.length >= data.total) {
|
|
826
|
+
return [];
|
|
827
|
+
}
|
|
828
|
+
return [
|
|
829
|
+
"",
|
|
830
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
831
|
+
];
|
|
832
|
+
};
|
|
833
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
834
|
+
const [only] = data.events;
|
|
835
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
836
|
+
return renderFields(
|
|
837
|
+
colors.green(`Event: ${only.name}`),
|
|
838
|
+
[
|
|
839
|
+
["Signature", renderEventSignature(only)],
|
|
840
|
+
["Description", only.description],
|
|
841
|
+
["Payload schema", only.payload_schema]
|
|
842
|
+
],
|
|
843
|
+
flags
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
847
|
+
return [
|
|
848
|
+
colors.green("Events"),
|
|
849
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
850
|
+
];
|
|
851
|
+
}
|
|
852
|
+
if (data.events.length === 0) {
|
|
853
|
+
return [
|
|
854
|
+
colors.green("Events"),
|
|
855
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
856
|
+
];
|
|
857
|
+
}
|
|
858
|
+
const lines = data.events.flatMap((event) => [
|
|
859
|
+
` ${renderEventSignature(event)}`,
|
|
860
|
+
` ${summarizeToolDescription(event.description)}`
|
|
861
|
+
]);
|
|
862
|
+
return [
|
|
863
|
+
colors.green("Events"),
|
|
864
|
+
...lines,
|
|
865
|
+
...renderEventsTruncationLine(data),
|
|
866
|
+
"",
|
|
867
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
868
|
+
];
|
|
869
|
+
};
|
|
804
870
|
var renderInvokeData = (colors, data, flags) => {
|
|
805
871
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
806
872
|
};
|
|
807
873
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
874
|
+
if ("attached" in data) {
|
|
875
|
+
return [
|
|
876
|
+
colors.green("Page Attached"),
|
|
877
|
+
...renderFields(
|
|
878
|
+
"Page",
|
|
879
|
+
[
|
|
880
|
+
["Session", data.sessionId],
|
|
881
|
+
["Tab", data.targetId],
|
|
882
|
+
["URL", data.url],
|
|
883
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
884
|
+
],
|
|
885
|
+
flags
|
|
886
|
+
)
|
|
887
|
+
];
|
|
888
|
+
}
|
|
889
|
+
if ("script" in data) {
|
|
890
|
+
return [
|
|
891
|
+
colors.green("Link Created"),
|
|
892
|
+
...renderFields(
|
|
893
|
+
"Link",
|
|
894
|
+
[
|
|
895
|
+
["Session", data.sessionId],
|
|
896
|
+
["URL", data.url],
|
|
897
|
+
["Script", data.script],
|
|
898
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
899
|
+
],
|
|
900
|
+
flags
|
|
901
|
+
)
|
|
902
|
+
];
|
|
903
|
+
}
|
|
808
904
|
const lines = [
|
|
809
905
|
colors.green("Link Created"),
|
|
810
906
|
...renderFields(
|
|
@@ -925,6 +1021,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
925
1021
|
["PID", data.daemon.pid],
|
|
926
1022
|
["Started at", data.daemon.started_at],
|
|
927
1023
|
["WSS port", data.daemon.wss_port],
|
|
1024
|
+
["Web port", data.daemon.web_port],
|
|
928
1025
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
929
1026
|
["Sessions", data.daemon.session_count]
|
|
930
1027
|
],
|
|
@@ -992,6 +1089,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
992
1089
|
return renderToolsListData(colors, data, flags, options.full);
|
|
993
1090
|
case "tools describe":
|
|
994
1091
|
return renderToolDetail(colors, data, flags);
|
|
1092
|
+
case "events ls":
|
|
1093
|
+
return renderEventsListData(colors, data, flags);
|
|
995
1094
|
case "tools call":
|
|
996
1095
|
return renderInvokeData(colors, data, flags);
|
|
997
1096
|
case "sessions revoke":
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the thin-client shapes for the v2 command table.
|
|
5
5
|
*/
|
|
6
6
|
import type { OpenTarget } from "./open-target.js";
|
|
7
|
-
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
|
|
7
|
+
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, EventsListResult, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
|
|
8
8
|
/**
|
|
9
9
|
* CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
|
|
10
10
|
* `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
|
|
@@ -66,6 +66,11 @@ export type InitCommandData = {
|
|
|
66
66
|
* tier rather than `"--scheme"`/`"already-recorded"` — `init`'s human-readable hint surfaces
|
|
67
67
|
* this so a native-probe result says which file to go edit, not just which platform. */
|
|
68
68
|
origin?: string;
|
|
69
|
+
/** The native platforms discovery saw in this project (`"android"` for a Gradle file or Android
|
|
70
|
+
* manifest, `"ios"` for an `Info.plist` or `project.yml`), alphabetical. Unlike `source`, which
|
|
71
|
+
* names the one input that won, this lists every platform, and it is reported on every run,
|
|
72
|
+
* including `"already-recorded"` and `"--scheme"`. Omitted when none was seen. */
|
|
73
|
+
platforms?: ("android" | "ios")[];
|
|
69
74
|
/** The project config did not exist before this run. */
|
|
70
75
|
created: boolean;
|
|
71
76
|
/** This run wrote to the file. `false` on an idempotent re-run. */
|
|
@@ -111,6 +116,26 @@ export type LinkCommandData = {
|
|
|
111
116
|
/** The `--open` target the link was delivered to, alongside `delivered`. */
|
|
112
117
|
target?: OpenTarget;
|
|
113
118
|
};
|
|
119
|
+
/** `appduct sessions link --open web <url>`: the page URL carrying the link, and the script that
|
|
120
|
+
* connects an already-open page instead. */
|
|
121
|
+
export type WebLinkCommandData = {
|
|
122
|
+
sessionId: string;
|
|
123
|
+
url: string;
|
|
124
|
+
script: string;
|
|
125
|
+
/** Unix seconds. */
|
|
126
|
+
expiresAt: number;
|
|
127
|
+
};
|
|
128
|
+
/** `appduct sessions link --open web <url> --browser-url <u>`: the tab the daemon attached and the
|
|
129
|
+
* session it will claim. */
|
|
130
|
+
export type WebAttachCommandData = {
|
|
131
|
+
attached: true;
|
|
132
|
+
sessionId: string;
|
|
133
|
+
url: string;
|
|
134
|
+
/** The CDP target id of the attached tab. */
|
|
135
|
+
targetId: string;
|
|
136
|
+
/** Unix seconds. */
|
|
137
|
+
expiresAt: number;
|
|
138
|
+
};
|
|
114
139
|
/** `appduct sessions ls`: `sessions.list` passthrough, verbatim (ARCHITECTURE.md §10: "--json
|
|
115
140
|
* passthrough"). */
|
|
116
141
|
export type LsCommandData = SessionSummary[];
|
|
@@ -135,6 +160,13 @@ export type ToolGroupsListing = {
|
|
|
135
160
|
* listing entry, picked out of `tools.list` — not a registration, so its `group` is `null` for an
|
|
136
161
|
* ungrouped tool. */
|
|
137
162
|
export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolsListEntry;
|
|
163
|
+
/** `appduct events ls`: the daemon's `events.list` result, plus the `--name`/`--limit`/`--offset`
|
|
164
|
+
* inputs that produced it (only the ones actually given). */
|
|
165
|
+
export type EventsListing = EventsListResult & {
|
|
166
|
+
name?: string;
|
|
167
|
+
limit?: number;
|
|
168
|
+
offset?: number;
|
|
169
|
+
};
|
|
138
170
|
/** `appduct tools call`: the tool's raw result payload, printed as-is. */
|
|
139
171
|
export type InvokeCommandData = unknown;
|
|
140
172
|
export type RevokeCommandData = {
|
|
@@ -164,7 +196,7 @@ export type DaemonStopCommandData = {
|
|
|
164
196
|
method: "rpc" | "sigterm";
|
|
165
197
|
};
|
|
166
198
|
};
|
|
167
|
-
/** `appduct doctor <artifact>`: artifact-level Appduct inclusion report
|
|
199
|
+
/** `appduct doctor <artifact>`: artifact-level Appduct inclusion report. */
|
|
168
200
|
export type DoctorCommandData = {
|
|
169
201
|
artifact: string;
|
|
170
202
|
platform: "ios" | "android";
|
|
@@ -187,6 +219,7 @@ export type DaemonStatusCommandData = {
|
|
|
187
219
|
pid: number;
|
|
188
220
|
started_at: string;
|
|
189
221
|
wss_port: number;
|
|
222
|
+
web_port: number;
|
|
190
223
|
pinned_keys: string[];
|
|
191
224
|
session_count: number;
|
|
192
225
|
};
|