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.
Files changed (62) hide show
  1. package/README.md +55 -24
  2. package/dist/artifact-inspect.d.ts +38 -39
  3. package/dist/bin.js +106 -7
  4. package/dist/cli/result-types.d.ts +35 -2
  5. package/dist/cli/routes/daemon/index.js +90 -2
  6. package/dist/cli/routes/daemon/run.js +1088 -517
  7. package/dist/cli/routes/daemon/start.js +98 -2
  8. package/dist/cli/routes/daemon/status.js +99 -2
  9. package/dist/cli/routes/daemon/stop.js +98 -2
  10. package/dist/cli/routes/doctor.js +98 -3
  11. package/dist/cli/routes/events/index.d.ts +0 -3
  12. package/dist/cli/routes/events/index.js +92 -3
  13. package/dist/cli/routes/events/ls.d.ts +5 -0
  14. package/dist/cli/routes/events/ls.js +1837 -0
  15. package/dist/cli/routes/events/since.js +98 -2
  16. package/dist/cli/routes/events/tail.js +98 -2
  17. package/dist/cli/routes/init.js +265 -108
  18. package/dist/cli/routes/keygen.js +90 -2
  19. package/dist/cli/routes/mcp.js +243 -25
  20. package/dist/cli/routes/sessions/index.js +90 -2
  21. package/dist/cli/routes/sessions/link.js +199 -22
  22. package/dist/cli/routes/sessions/ls.js +98 -2
  23. package/dist/cli/routes/sessions/revoke.js +98 -2
  24. package/dist/cli/routes/tools/call.js +98 -2
  25. package/dist/cli/routes/tools/describe.js +98 -2
  26. package/dist/cli/routes/tools/index.js +90 -2
  27. package/dist/cli/routes/tools/ls.js +98 -2
  28. package/dist/client/attach-page.d.ts +39 -0
  29. package/dist/client/bootstrap.d.ts +17 -3
  30. package/dist/client/index.d.ts +2 -1
  31. package/dist/client/index.js +286 -112
  32. package/dist/commands/doctor.d.ts +3 -4
  33. package/dist/commands/events-ls.d.ts +21 -0
  34. package/dist/commands/init.d.ts +8 -2
  35. package/dist/commands/link.d.ts +10 -1
  36. package/dist/daemon/calls.d.ts +3 -1
  37. package/dist/daemon/config.d.ts +6 -0
  38. package/dist/daemon/daemon.d.ts +6 -1
  39. package/dist/daemon/event-bus.d.ts +10 -0
  40. package/dist/daemon/links.d.ts +9 -5
  41. package/dist/daemon/listener.d.ts +28 -1
  42. package/dist/daemon/registry.d.ts +20 -12
  43. package/dist/daemon/rpc-errors.d.ts +6 -0
  44. package/dist/daemon/sessions.d.ts +14 -8
  45. package/dist/daemon/tls.d.ts +0 -3
  46. package/dist/devtools-relay/attach-tab.d.ts +33 -0
  47. package/dist/devtools-relay/index.d.ts +8 -0
  48. package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
  49. package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
  50. package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
  51. package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
  52. package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
  53. package/dist/devtools-relay/ports.d.ts +48 -0
  54. package/dist/devtools-relay/relay.d.ts +16 -0
  55. package/dist/index.js +106 -7
  56. package/dist/link.d.ts +35 -1
  57. package/dist/mcp/app-tools.d.ts +19 -0
  58. package/dist/mcp/connect-tool.d.ts +15 -2
  59. package/dist/mcp/events-tool.d.ts +39 -0
  60. package/dist/native-scheme.d.ts +11 -7
  61. package/dist/scheme.d.ts +5 -1
  62. package/package.json +15 -5
@@ -1,5 +1,5 @@
1
1
  import { type ExecFn, type OpenTarget } from "../cli/open-target.js";
2
- import { type MintLinkResult } from "../link.js";
2
+ import { type MintLinkResult, type WebLinkResult } from "../link.js";
3
3
  import { type SpawnFn } from "../rpc/client.js";
4
4
  import { type AppClient, type ToolMap } from "./app-client.js";
5
5
  export type LinkOptions = {
@@ -32,9 +32,23 @@ export type LinkOptions = {
32
32
  schemeEnv?: NodeJS.ProcessEnv;
33
33
  };
34
34
  export type LinkResult = MintLinkResult;
35
+ export type WebLinkOptions = {
36
+ stateDir?: string;
37
+ spawn?: SpawnFn;
38
+ /** Auto-spawn a daemon on a missing connection. Defaults to `true`. */
39
+ autoSpawn?: boolean;
40
+ ttlSeconds?: number;
41
+ target: "web";
42
+ /** The page the link will be opened in. */
43
+ url: string;
44
+ };
45
+ export type { WebLinkResult };
35
46
  /** Mints a session link via `link.create` and composes the deep link. With `target` set, delivers
36
- * it directly to a booted emulator/simulator instead of leaving delivery to the caller. */
37
- export declare const link: (options?: LinkOptions) => Promise<LinkResult>;
47
+ * it directly to a booted emulator/simulator instead of leaving delivery to the caller. With
48
+ * `target: "web"` and the page's `url`, returns `{ url, script }` for a web page instead: `url`
49
+ * with `#appduct=<payload>` appended, and the script that connects an already-open page. */
50
+ export declare function link(options: WebLinkOptions): Promise<WebLinkResult>;
51
+ export declare function link(options?: LinkOptions): Promise<LinkResult>;
38
52
  export type WaitForSessionOptions = {
39
53
  stateDir?: string;
40
54
  spawn?: SpawnFn;
@@ -11,8 +11,9 @@
11
11
  * await app.close();
12
12
  * ```
13
13
  */
14
+ export { attachPage, type AttachablePage, type AttachPageOptions } from "./attach-page.js";
14
15
  export { connect, type ConnectOptions } from "./connect.js";
15
- export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
16
+ export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions, type WebLinkOptions, type WebLinkResult, } from "./bootstrap.js";
16
17
  export { AppductError, type AppductErrorType } from "./errors.js";
17
18
  export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, FullAppEvent, FullEventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
18
19
  export type { TruncatedAppEvent } from "@appduct/shared";
@@ -1,32 +1,70 @@
1
- // src/client/connect.ts
2
- import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
3
-
4
- // src/daemon/state-dir.ts
5
- import { homedir } from "node:os";
6
- import { join } from "node:path";
7
- import { mkdir, chmod } from "node:fs/promises";
8
- var resolveStateDir = (override) => {
9
- return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
10
- };
11
- var getStateDirPaths = (stateDir) => {
12
- return {
13
- root: stateDir,
14
- socketPath: join(stateDir, "daemon.sock"),
15
- pidFilePath: join(stateDir, "daemon.pid"),
16
- logFilePath: join(stateDir, "daemon.log"),
17
- eventsLogPath: join(stateDir, "events.log"),
18
- keyPath: join(stateDir, "key.pem"),
19
- configPath: join(stateDir, "config.json"),
20
- auditDir: join(stateDir, "audit"),
21
- spawnLockPath: join(stateDir, "daemon.spawn.lock")
1
+ // src/devtools-relay/relay.ts
2
+ import { decodeBootstrap } from "@appduct/shared";
3
+ var linkPayload = (link2) => {
4
+ const payload = new URLSearchParams(new URL(link2.url).hash.slice(1)).get("appduct");
5
+ if (payload === null) throw new Error('The link has no #appduct= payload: pass the result of link({ target: "web", url }).');
6
+ return payload;
7
+ };
8
+ var parseLink = (link2) => {
9
+ const bootstrap = decodeBootstrap(linkPayload(link2));
10
+ if (!bootstrap) throw new Error("The link's #appduct= payload is not a valid Appduct link.");
11
+ return bootstrap;
12
+ };
13
+ var GOING_AWAY = 1001;
14
+ var relayPage = (page, link2, openDaemonSocket) => {
15
+ const bootstrap = parseLink(link2);
16
+ const host = bootstrap.address.includes(":") ? `[${bootstrap.address}]` : bootstrap.address;
17
+ const url = `ws://${host}:${bootstrap.port}`;
18
+ let current;
19
+ const tellPage = (message) => {
20
+ page.evaluate(`window.__APPDUCT__.receive(${JSON.stringify(JSON.stringify(message))})`).catch(() => void 0);
21
+ };
22
+ const closeCurrent = (code, reason) => {
23
+ const socket = current;
24
+ current = void 0;
25
+ socket?.close(code, reason);
22
26
  };
27
+ page.onBindingCall((json) => {
28
+ const message = JSON.parse(json);
29
+ if (message.kind === "open") {
30
+ closeCurrent(GOING_AWAY, "superseded");
31
+ const socket = openDaemonSocket(url, {
32
+ open: () => current === socket && tellPage({ kind: "open" }),
33
+ message: (text) => current === socket && tellPage({ kind: "message", text }),
34
+ close: (code, reason) => {
35
+ if (current !== socket) return;
36
+ current = void 0;
37
+ tellPage({ kind: "close", code, reason });
38
+ }
39
+ });
40
+ current = socket;
41
+ } else if (message.kind === "send") {
42
+ current?.send(message.text ?? "");
43
+ } else if (message.kind === "close") {
44
+ closeCurrent(message.code ?? 1e3, message.reason ?? "");
45
+ }
46
+ });
47
+ page.onContextReset(() => closeCurrent(GOING_AWAY, "page_gone"));
23
48
  };
24
- var ensureStateDir = async (stateDir) => {
25
- const paths = getStateDirPaths(stateDir);
26
- await mkdir(paths.auditDir, { recursive: true });
27
- await chmod(paths.root, 448);
28
- await chmod(paths.auditDir, 448);
29
- return paths;
49
+
50
+ // src/devtools-relay/node-devtools-browser.ts
51
+ import WebSocket from "ws";
52
+
53
+ // src/devtools-relay/node-daemon-socket.ts
54
+ import WebSocket2 from "ws";
55
+ var openNodeDaemonSocket = (url, events) => {
56
+ const socket = new WebSocket2(url);
57
+ socket.on("open", () => events.open());
58
+ socket.on("message", (data, isBinary) => {
59
+ if (isBinary) {
60
+ socket.close(4008, "binary_frame_not_supported");
61
+ return;
62
+ }
63
+ events.message(data.toString());
64
+ });
65
+ socket.on("error", () => void 0);
66
+ socket.on("close", (code, reason) => events.close(code, reason.toString()));
67
+ return { send: (text) => socket.send(text), close: (code, reason) => socket.close(code, reason) };
30
68
  };
31
69
 
32
70
  // src/rpc/client.ts
@@ -52,6 +90,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
52
90
  "daemonLogMaxBytes",
53
91
  "eventsLogMaxBytes",
54
92
  "policy",
93
+ "webOrigins",
55
94
  "advertisedIp",
56
95
  "scheme",
57
96
  "restartDaemonOnVersionMismatch"
@@ -112,6 +151,7 @@ var defaultConfig = (paths) => {
112
151
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
113
152
  eventsLogMaxBytes: 10 * 1024 * 1024,
114
153
  restartDaemonOnVersionMismatch: false,
154
+ webOrigins: [],
115
155
  policy: {
116
156
  default: "allow",
117
157
  destructive: "allow"
@@ -182,6 +222,12 @@ var loadConfig = async (paths, options = {}) => {
182
222
  "restartDaemonOnVersionMismatch"
183
223
  );
184
224
  }
225
+ if (parsed.webOrigins !== void 0) {
226
+ if (!Array.isArray(parsed.webOrigins)) {
227
+ throw configError("webOrigins", "must be an array of origin strings.");
228
+ }
229
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
230
+ }
185
231
  if (parsed.policy !== void 0) {
186
232
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
187
233
  throw configError("policy", "must be an object.");
@@ -267,6 +313,34 @@ var isSocketConnectable = (socketPath) => {
267
313
  });
268
314
  };
269
315
 
316
+ // src/daemon/state-dir.ts
317
+ import { homedir } from "node:os";
318
+ import { join } from "node:path";
319
+ import { mkdir, chmod } from "node:fs/promises";
320
+ var resolveStateDir = (override) => {
321
+ return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
322
+ };
323
+ var getStateDirPaths = (stateDir) => {
324
+ return {
325
+ root: stateDir,
326
+ socketPath: join(stateDir, "daemon.sock"),
327
+ pidFilePath: join(stateDir, "daemon.pid"),
328
+ logFilePath: join(stateDir, "daemon.log"),
329
+ eventsLogPath: join(stateDir, "events.log"),
330
+ keyPath: join(stateDir, "key.pem"),
331
+ configPath: join(stateDir, "config.json"),
332
+ auditDir: join(stateDir, "audit"),
333
+ spawnLockPath: join(stateDir, "daemon.spawn.lock")
334
+ };
335
+ };
336
+ var ensureStateDir = async (stateDir) => {
337
+ const paths = getStateDirPaths(stateDir);
338
+ await mkdir(paths.auditDir, { recursive: true });
339
+ await chmod(paths.root, 448);
340
+ await chmod(paths.auditDir, 448);
341
+ return paths;
342
+ };
343
+
270
344
  // src/daemon/log-rotation.ts
271
345
  var STATE_FILE_MODE = 384;
272
346
  var daemonLogBackupPath = (logFilePath) => `${logFilePath}.1`;
@@ -1025,6 +1099,119 @@ var openDaemonStream = async (options) => {
1025
1099
  };
1026
1100
  };
1027
1101
 
1102
+ // src/errors.ts
1103
+ var AppductCliError = class extends Error {
1104
+ type;
1105
+ details;
1106
+ constructor(type, message, details) {
1107
+ super(message);
1108
+ this.name = "AppductCliError";
1109
+ this.type = type;
1110
+ this.details = details;
1111
+ }
1112
+ };
1113
+ var usageError = (message, details) => {
1114
+ return new AppductCliError("usage_error", message, details);
1115
+ };
1116
+ var connectionError = (message, details) => {
1117
+ return new AppductCliError("connection_error", message, details);
1118
+ };
1119
+ var isAppductCliError = (value) => {
1120
+ return value instanceof AppductCliError;
1121
+ };
1122
+
1123
+ // src/client/errors.ts
1124
+ var AppductError = class extends Error {
1125
+ type;
1126
+ details;
1127
+ /** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
1128
+ code;
1129
+ constructor(type, message, details, code) {
1130
+ super(message);
1131
+ this.name = "AppductError";
1132
+ this.type = type;
1133
+ this.details = details;
1134
+ this.code = code;
1135
+ }
1136
+ };
1137
+ var toAppductError = (error) => {
1138
+ if (error instanceof AppductError) {
1139
+ return error;
1140
+ }
1141
+ const withCause = (result) => {
1142
+ result.cause = error;
1143
+ return result;
1144
+ };
1145
+ if (error instanceof DaemonRpcError) {
1146
+ const type = error.data?.type ?? "client_error";
1147
+ return withCause(new AppductError(type, error.message, error.data?.details, error.code));
1148
+ }
1149
+ if (isDaemonUnreachableError(error)) {
1150
+ return withCause(
1151
+ new AppductError(
1152
+ "connection_error",
1153
+ error instanceof Error ? error.message : "The Appduct daemon is unreachable."
1154
+ )
1155
+ );
1156
+ }
1157
+ if (isAppductCliError(error)) {
1158
+ return withCause(new AppductError("client_error", error.message, error.details));
1159
+ }
1160
+ if (error instanceof Error) {
1161
+ return withCause(new AppductError("client_error", error.message));
1162
+ }
1163
+ return new AppductError("client_error", "An unexpected error occurred.", error);
1164
+ };
1165
+
1166
+ // src/client/attach-page.ts
1167
+ var BINDING = "__appductBinding";
1168
+ var ANNOUNCE_DOCUMENT = `if (window === window.top) window.${BINDING}(JSON.stringify({ kind: "context" }));`;
1169
+ var channels = /* @__PURE__ */ new WeakMap();
1170
+ var channelOf = async (page) => {
1171
+ const existing = channels.get(page);
1172
+ if (existing) {
1173
+ existing.reset();
1174
+ await page.evaluate(`window.__APPDUCT__?.receive(${JSON.stringify(JSON.stringify({ kind: "close", code: 1001, reason: "superseded" }))})`).catch(() => void 0);
1175
+ return existing;
1176
+ }
1177
+ let bindingHandlers = [];
1178
+ let resetHandlers = [];
1179
+ const reset = () => resetHandlers.forEach((handler) => handler());
1180
+ await page.exposeBinding(BINDING, (source, json) => {
1181
+ if (source.frame !== page.mainFrame()) return;
1182
+ if (JSON.parse(json).kind === "context") reset();
1183
+ else bindingHandlers.forEach((handler) => handler(json));
1184
+ });
1185
+ await page.addInitScript(ANNOUNCE_DOCUMENT);
1186
+ page.on("close", reset);
1187
+ const channel = {
1188
+ onBindingCall: (handler) => void bindingHandlers.push(handler),
1189
+ onContextReset: (handler) => void resetHandlers.push(handler),
1190
+ evaluate: (expression) => page.evaluate(expression),
1191
+ reset: () => {
1192
+ reset();
1193
+ bindingHandlers = [];
1194
+ resetHandlers = [];
1195
+ }
1196
+ };
1197
+ channels.set(page, channel);
1198
+ return channel;
1199
+ };
1200
+ var attachPage = async (page, options) => {
1201
+ try {
1202
+ parseLink(options.link);
1203
+ const payload = linkPayload(options.link);
1204
+ relayPage(await channelOf(page), options.link, openNodeDaemonSocket);
1205
+ await page.waitForFunction("typeof window.__APPDUCT__?.connect === 'function'");
1206
+ await page.evaluate(`window.__APPDUCT__.connect(${JSON.stringify(payload)}, { transport: "devtools" })`);
1207
+ } catch (error) {
1208
+ throw toAppductError(error);
1209
+ }
1210
+ };
1211
+
1212
+ // src/client/connect.ts
1213
+ import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
1214
+
1028
1215
  // src/client/app-client.ts
1029
1216
  import {
1030
1217
  clampToolTimeoutMs,
@@ -1132,70 +1319,6 @@ var waitForAppEvent = async (stream, options) => {
1132
1319
  }
1133
1320
  };
1134
1321
 
1135
- // src/errors.ts
1136
- var AppductCliError = class extends Error {
1137
- type;
1138
- details;
1139
- constructor(type, message, details) {
1140
- super(message);
1141
- this.name = "AppductCliError";
1142
- this.type = type;
1143
- this.details = details;
1144
- }
1145
- };
1146
- var usageError = (message, details) => {
1147
- return new AppductCliError("usage_error", message, details);
1148
- };
1149
- var connectionError = (message, details) => {
1150
- return new AppductCliError("connection_error", message, details);
1151
- };
1152
- var isAppductCliError = (value) => {
1153
- return value instanceof AppductCliError;
1154
- };
1155
-
1156
- // src/client/errors.ts
1157
- var AppductError = class extends Error {
1158
- type;
1159
- details;
1160
- /** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
1161
- code;
1162
- constructor(type, message, details, code) {
1163
- super(message);
1164
- this.name = "AppductError";
1165
- this.type = type;
1166
- this.details = details;
1167
- this.code = code;
1168
- }
1169
- };
1170
- var toAppductError = (error) => {
1171
- if (error instanceof AppductError) {
1172
- return error;
1173
- }
1174
- const withCause = (result) => {
1175
- result.cause = error;
1176
- return result;
1177
- };
1178
- if (error instanceof DaemonRpcError) {
1179
- const type = error.data?.type ?? "client_error";
1180
- return withCause(new AppductError(type, error.message, error.data?.details, error.code));
1181
- }
1182
- if (isDaemonUnreachableError(error)) {
1183
- return withCause(
1184
- new AppductError(
1185
- "connection_error",
1186
- error instanceof Error ? error.message : "The Appduct daemon is unreachable."
1187
- )
1188
- );
1189
- }
1190
- if (isAppductCliError(error)) {
1191
- return withCause(new AppductError("client_error", error.message, error.details));
1192
- }
1193
- if (error instanceof Error) {
1194
- return withCause(new AppductError("client_error", error.message));
1195
- }
1196
- return new AppductError("client_error", "An unexpected error occurred.", error);
1197
- };
1198
-
1199
1322
  // src/client/app-client.ts
1200
1323
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 3e4;
1201
1324
  var CALL_TRANSPORT_TIMEOUT_SLACK_MS = 5e3;
@@ -1965,31 +2088,42 @@ var probeIosProjectYml = async (root) => {
1965
2088
  };
1966
2089
  };
1967
2090
  var PROBES = [
1968
- probeAndroidGradle,
1969
- probeAndroidManifest,
1970
- probeIosInfoPlist,
1971
- probeIosProjectYml
2091
+ ["android", probeAndroidGradle],
2092
+ ["android", probeAndroidManifest],
2093
+ ["ios", probeIosInfoPlist],
2094
+ ["ios", probeIosProjectYml]
1972
2095
  ];
2096
+ var isFlutterProject = async (root) => {
2097
+ return (await readCappedText(join4(root, "pubspec.yaml"))).ok;
2098
+ };
1973
2099
  var discoverNativeScheme = async (root) => {
1974
2100
  const tried = [];
1975
2101
  const results = [];
1976
- for (const probe of PROBES) {
1977
- const outcome = await probe(root);
1978
- tried.push(...outcome.tried);
1979
- if (outcome.result !== void 0) {
1980
- results.push(outcome.result);
2102
+ const flutter = await isFlutterProject(root);
2103
+ const androidRoot = flutter ? join4(root, "android") : root;
2104
+ const iosRoots = flutter ? [join4(root, "ios"), join4(root, "macos")] : [root];
2105
+ for (const [platform, probe] of PROBES) {
2106
+ let roots = [root];
2107
+ if (platform === "android") {
2108
+ roots = [androidRoot];
2109
+ } else if (probe === probeIosInfoPlist) {
2110
+ roots = iosRoots;
2111
+ }
2112
+ for (const probeRoot of roots) {
2113
+ const outcome = await probe(probeRoot);
2114
+ tried.push(...outcome.tried);
2115
+ if (outcome.result !== void 0) {
2116
+ results.push(outcome.result);
2117
+ }
1981
2118
  }
1982
2119
  }
1983
- if (results.length === 0) {
1984
- return { tried };
1985
- }
1986
2120
  const distinctSchemes = new Set(results.map((entry) => entry.scheme));
1987
2121
  if (distinctSchemes.size > 1) {
1988
2122
  throw usageError(
1989
2123
  "Conflicting deep-link schemes found in this project: " + results.map((entry) => `${entry.origin} says ${JSON.stringify(entry.scheme)}`).join(", but ") + ". Appduct will not guess which one is right \u2014 pass --scheme <scheme> to pick one."
1990
2124
  );
1991
2125
  }
1992
- return { result: results[0], tried };
2126
+ return { results, tried };
1993
2127
  };
1994
2128
 
1995
2129
  // src/scheme.ts
@@ -2052,19 +2186,21 @@ var discoverStaticProjectScheme = async (cwd) => {
2052
2186
  const tried = [`${appJsonPath} ("expo.scheme")`];
2053
2187
  const appJsonScheme = await discoverExpoScheme(cwd);
2054
2188
  if (appJsonScheme !== void 0) {
2055
- return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
2189
+ return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
2056
2190
  }
2057
2191
  const native = await discoverNativeScheme(resolve(cwd));
2058
2192
  tried.push(...native.tried);
2059
- if (native.result !== void 0) {
2193
+ const [first] = native.results;
2194
+ if (first !== void 0) {
2060
2195
  return {
2061
- scheme: native.result.scheme,
2062
- source: native.result.source,
2063
- origin: native.result.origin,
2196
+ scheme: first.scheme,
2197
+ source: first.source,
2198
+ origin: first.origin,
2199
+ nativeResults: native.results,
2064
2200
  tried
2065
2201
  };
2066
2202
  }
2067
- return { tried };
2203
+ return { nativeResults: [], tried };
2068
2204
  };
2069
2205
  var globalConfigDirs = (options = {}) => {
2070
2206
  const excluded = /* @__PURE__ */ new Set([join5(resolve(options.homeDir ?? homedir2()), PROJECT_CONFIG_DIR)]);
@@ -2248,6 +2384,34 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
2248
2384
  // src/link.ts
2249
2385
  var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
2250
2386
  var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
2387
+ var assertWebPageUrl = (pageUrl) => {
2388
+ let protocol;
2389
+ try {
2390
+ protocol = new URL(pageUrl).protocol;
2391
+ } catch {
2392
+ }
2393
+ if (protocol !== "http:" && protocol !== "https:") {
2394
+ throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
2395
+ }
2396
+ };
2397
+ var composeWebLink = (pageUrl, result) => {
2398
+ const payload = result.deepLinkPayload;
2399
+ return {
2400
+ sessionId: result.sessionId,
2401
+ url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
2402
+ script: `window.__APPDUCT__.connect("${payload}")`,
2403
+ expiresAt: result.expiresAt
2404
+ };
2405
+ };
2406
+ var mintWebLink = async (options) => {
2407
+ assertWebPageUrl(options.url);
2408
+ const result = await callDaemon(
2409
+ RPC_METHODS5.linkCreate,
2410
+ { ttlSeconds: options.ttlSeconds, transport: "web" },
2411
+ { stateDir: options.stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn }
2412
+ );
2413
+ return composeWebLink(options.url, result);
2414
+ };
2251
2415
  var mintLink = async (options) => {
2252
2416
  if (options.target !== void 0 && !isOpenTarget(options.target)) {
2253
2417
  throw usageError(
@@ -2334,8 +2498,17 @@ var mintLink = async (options) => {
2334
2498
 
2335
2499
  // src/client/bootstrap.ts
2336
2500
  var DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS = 12e4;
2337
- var link = async (options = {}) => {
2501
+ async function link(options = {}) {
2338
2502
  try {
2503
+ if (options.target === "web") {
2504
+ return await mintWebLink({
2505
+ stateDir: resolveStateDir(options.stateDir),
2506
+ spawn: options.spawn,
2507
+ autoSpawn: options.autoSpawn,
2508
+ ttlSeconds: options.ttlSeconds,
2509
+ url: options.url
2510
+ });
2511
+ }
2339
2512
  return await mintLink({
2340
2513
  stateDir: resolveStateDir(options.stateDir),
2341
2514
  spawn: options.spawn,
@@ -2354,7 +2527,7 @@ var link = async (options = {}) => {
2354
2527
  } catch (error) {
2355
2528
  throw toAppductError(error);
2356
2529
  }
2357
- };
2530
+ }
2358
2531
  var waitForSession = async (sessionId, options = {}) => {
2359
2532
  const stateDir = resolveStateDir(options.stateDir);
2360
2533
  const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS;
@@ -2427,6 +2600,7 @@ var waitForSession = async (sessionId, options = {}) => {
2427
2600
  };
2428
2601
  export {
2429
2602
  AppductError,
2603
+ attachPage,
2430
2604
  connect3 as connect,
2431
2605
  link,
2432
2606
  waitForSession
@@ -1,8 +1,7 @@
1
1
  /**
2
- * `appduct doctor <artifact>` (docs/tasks/08-appduct-doctor.md): the release-gate replacement
3
- * for the runtime `debuggable` check removed elsewhere in opt-in hardening — see
4
- * `artifact-inspect.ts`'s doc comment for the detection strategy and why it never collapses a
5
- * missing-tool or unreadable-artifact case into "absent".
2
+ * `appduct doctor <artifact>`: the release-gate replacement for the runtime `debuggable` check
3
+ * removed elsewhere in opt-in hardening — see `artifact-inspect.ts`'s doc comment for the detection
4
+ * strategy and why it never collapses a missing-tool or unreadable-artifact case into "absent".
6
5
  *
7
6
  * This handler owns only the CLI-facing concerns: option validation (mutually exclusive
8
7
  * `--assert-present`/`--assert-absent`) and turning a failed assertion into a distinct
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `appduct events ls [selector] [--name <glob>] [--limit <n>] [--offset <n>]` (issue #124): lists the events an app declared,
3
+ * from the daemon's `events.list`. The listing carries each event's full descriptor, so `--json`
4
+ * has the payload schemas too; the human renderer prints signature lines, and the full schema when
5
+ * `--name` is an exact name (no `*`) that matched an event.
6
+ */
7
+ import type { CliResult, EventsListing } from "../cli/result-types.js";
8
+ import { type SpawnFn } from "../rpc/client.js";
9
+ export type EventsLsOptions = {
10
+ selector?: string;
11
+ /** Whole-name, case-sensitive glob, forwarded to `events.list`. */
12
+ name?: string;
13
+ /** Page size; omitted lists everything from `offset` on. */
14
+ limit?: number;
15
+ /** Zero-based start index into the name-sorted, filtered list. */
16
+ offset?: number;
17
+ };
18
+ export declare const handleEventsLsCommand: (options: EventsLsOptions, context: {
19
+ stateDir: string;
20
+ spawn?: SpawnFn;
21
+ }) => Promise<CliResult<EventsListing>>;
@@ -6,8 +6,9 @@
6
6
  * #63, the installed app's id per platform (`appId.ios`/`appId.android` — needed to deliver a
7
7
  * link with `--open ios-device`/`--open android` without an "Open with" chooser silently eating
8
8
  * it), and returns the two things that are not discoverable from the filesystem: the MCP server
9
- * entry to paste into an agent's config, and the `import "@appduct/react-native/auto"` reminder
10
- * the app needs.
9
+ * entry to paste into an agent's config, and the wiring step this project still needs — the
10
+ * `import "@appduct/react-native/auto"` reminder for a React Native app, the equivalent native
11
+ * wiring for a plain iOS or Android one (issue #153).
11
12
  *
12
13
  * Unlike `scheme`, an app id has no discovery tier: `--ios-app-id`/`--android-app-id` (or an
13
14
  * already-recorded value) is the whole story, and the two are independent flags rather than one
@@ -41,6 +42,11 @@
41
42
  * step via {@link discoverStaticProjectScheme} specifically so the two can never disagree about
42
43
  * what "discovery" means. `InitCommandData.source` names every origin this command itself can
43
44
  * produce.
45
+ *
46
+ * One read does go above the root, and it is not an exception to the rule above:
47
+ * {@link declaresReactNative} walks up a bounded number of directories for a `package.json`, which
48
+ * chooses which wiring *hint* to print and writes nothing. Nothing that lands in the file `init`
49
+ * writes is inherited from a parent directory.
44
50
  */
45
51
  import type { CliResult, InitCommandData } from "../cli/result-types.js";
46
52
  export type InitCommandOptions = {
@@ -3,7 +3,7 @@
3
3
  * CLI-specific error wording) around the shared `mintLink` core (`../link.ts`), which both this
4
4
  * command and `appduct/client`'s `link()` use so the deep-link shape can't drift between them.
5
5
  */
6
- import type { CliResult, LinkCommandData } from "../cli/result-types.js";
6
+ import type { CliResult, LinkCommandData, WebAttachCommandData, WebLinkCommandData } from "../cli/result-types.js";
7
7
  import { type ExecFn } from "../cli/open-target.js";
8
8
  import type { SpawnFn } from "../rpc/client.js";
9
9
  export type LinkCommandOptions = {
@@ -13,6 +13,12 @@ export type LinkCommandOptions = {
13
13
  device?: string;
14
14
  appId?: string;
15
15
  relaunch?: boolean;
16
+ /** The page to open, with `--open web`. */
17
+ url?: string;
18
+ /** With `--open web`: the debugging endpoint of a Chromium whose tab the daemon attaches. */
19
+ browserUrl?: string;
20
+ /** With `--browser-url`: the CDP target id of the tab to attach. */
21
+ targetId?: string;
16
22
  };
17
23
  export type LinkCommandContext = {
18
24
  stateDir: string;
@@ -27,3 +33,6 @@ export type LinkCommandContext = {
27
33
  schemeEnv?: NodeJS.ProcessEnv;
28
34
  };
29
35
  export declare const handleLinkCommand: (options: LinkCommandOptions, context: LinkCommandContext) => Promise<CliResult<LinkCommandData>>;
36
+ /** `appduct sessions link --open web <url>`: no device and no scheme, just the page URL carrying
37
+ * the link and the script that connects an already-open page. */
38
+ export declare const handleWebLinkCommand: (options: LinkCommandOptions, context: LinkCommandContext) => Promise<CliResult<WebLinkCommandData | WebAttachCommandData>>;
@@ -66,7 +66,9 @@ export type CallsManager = {
66
66
  * session has no active socket to send the frame on right now. */
67
67
  cancel: (sessionId: string, callId: string, reason: string) => boolean;
68
68
  /** Rejects every pending call for a session immediately (suspend/revoke/expiry transitions). */
69
- rejectSession: (sessionId: string, errorType: "session_suspended" | "unknown_session") => void;
69
+ rejectSession: (sessionId: string, errorType: "session_suspended" | "unknown_session", options?: {
70
+ message?: (alias: string) => string;
71
+ }) => void;
70
72
  /** Rejects and clears every pending call across every session (daemon shutdown). */
71
73
  disposeAll: () => void;
72
74
  };
@@ -47,6 +47,12 @@ export type AppductConfig = {
47
47
  * Default 10 MiB. */
48
48
  eventsLogMaxBytes: number;
49
49
  policy: AppductPolicyConfig;
50
+ /**
51
+ * Origins besides localhost, `127.0.0.1` and `[::1]` (any port) that may connect to the web
52
+ * listener, each written as a browser sends it, e.g. `"https://app.example.test"`. Compared as
53
+ * exact strings. Default: none.
54
+ */
55
+ webOrigins: string[];
50
56
  /**
51
57
  * When the CLI/MCP client finds the running daemon on a different Appduct version, restart it
52
58
  * even if that drops live sessions (issue #30, ARCHITECTURE.md §4's "Version drift"). Default