appduct 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +60 -287
  2. package/dist/artifact-inspect.d.ts +38 -39
  3. package/dist/bin.js +45 -3
  4. package/dist/cli/result-types.d.ts +27 -1
  5. package/dist/cli/routes/daemon/index.js +32 -1
  6. package/dist/cli/routes/daemon/run.js +963 -521
  7. package/dist/cli/routes/daemon/start.js +40 -1
  8. package/dist/cli/routes/daemon/status.js +41 -1
  9. package/dist/cli/routes/daemon/stop.js +40 -1
  10. package/dist/cli/routes/doctor.js +40 -2
  11. package/dist/cli/routes/events/index.js +32 -1
  12. package/dist/cli/routes/events/ls.js +40 -1
  13. package/dist/cli/routes/events/since.js +40 -1
  14. package/dist/cli/routes/events/tail.js +40 -1
  15. package/dist/cli/routes/init.js +207 -107
  16. package/dist/cli/routes/keygen.js +32 -1
  17. package/dist/cli/routes/mcp.js +135 -24
  18. package/dist/cli/routes/sessions/index.js +32 -1
  19. package/dist/cli/routes/sessions/link.js +141 -21
  20. package/dist/cli/routes/sessions/ls.js +40 -1
  21. package/dist/cli/routes/sessions/revoke.js +40 -1
  22. package/dist/cli/routes/tools/call.js +40 -1
  23. package/dist/cli/routes/tools/describe.js +40 -1
  24. package/dist/cli/routes/tools/index.js +32 -1
  25. package/dist/cli/routes/tools/ls.js +40 -1
  26. package/dist/client/attach-page.d.ts +39 -0
  27. package/dist/client/bootstrap.d.ts +17 -3
  28. package/dist/client/index.d.ts +2 -1
  29. package/dist/client/index.js +287 -112
  30. package/dist/commands/doctor.d.ts +3 -4
  31. package/dist/commands/init.d.ts +8 -2
  32. package/dist/commands/link.d.ts +10 -1
  33. package/dist/daemon/calls.d.ts +1 -1
  34. package/dist/daemon/config.d.ts +6 -0
  35. package/dist/daemon/daemon.d.ts +6 -1
  36. package/dist/daemon/links.d.ts +9 -5
  37. package/dist/daemon/listener.d.ts +28 -1
  38. package/dist/daemon/rpc-errors.d.ts +3 -0
  39. package/dist/daemon/sessions.d.ts +11 -7
  40. package/dist/daemon/tls.d.ts +0 -3
  41. package/dist/devtools-relay/attach-tab.d.ts +33 -0
  42. package/dist/devtools-relay/index.d.ts +8 -0
  43. package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
  44. package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
  45. package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
  46. package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
  47. package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
  48. package/dist/devtools-relay/ports.d.ts +48 -0
  49. package/dist/devtools-relay/relay.d.ts +16 -0
  50. package/dist/index.js +45 -3
  51. package/dist/link.d.ts +35 -1
  52. package/dist/mcp/connect-tool.d.ts +15 -2
  53. package/dist/native-scheme.d.ts +11 -7
  54. package/dist/scheme.d.ts +5 -1
  55. package/package.json +15 -5
@@ -1,32 +1,71 @@
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 id = message.id;
32
+ const socket = openDaemonSocket(url, {
33
+ open: () => current === socket && tellPage({ kind: "open", id }),
34
+ message: (text) => current === socket && tellPage({ kind: "message", id, text }),
35
+ close: (code, reason) => {
36
+ if (current !== socket) return;
37
+ current = void 0;
38
+ tellPage({ kind: "close", id, code, reason });
39
+ }
40
+ });
41
+ current = socket;
42
+ } else if (message.kind === "send") {
43
+ current?.send(message.text ?? "");
44
+ } else if (message.kind === "close") {
45
+ closeCurrent(message.code ?? 1e3, message.reason ?? "");
46
+ }
47
+ });
48
+ page.onContextReset(() => closeCurrent(GOING_AWAY, "page_gone"));
23
49
  };
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;
50
+
51
+ // src/devtools-relay/node-devtools-browser.ts
52
+ import WebSocket from "ws";
53
+
54
+ // src/devtools-relay/node-daemon-socket.ts
55
+ import WebSocket2 from "ws";
56
+ var openNodeDaemonSocket = (url, events) => {
57
+ const socket = new WebSocket2(url);
58
+ socket.on("open", () => events.open());
59
+ socket.on("message", (data, isBinary) => {
60
+ if (isBinary) {
61
+ socket.close(4008, "binary_frame_not_supported");
62
+ return;
63
+ }
64
+ events.message(data.toString());
65
+ });
66
+ socket.on("error", () => void 0);
67
+ socket.on("close", (code, reason) => events.close(code, reason.toString()));
68
+ return { send: (text) => socket.send(text), close: (code, reason) => socket.close(code, reason) };
30
69
  };
31
70
 
32
71
  // src/rpc/client.ts
@@ -52,6 +91,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
52
91
  "daemonLogMaxBytes",
53
92
  "eventsLogMaxBytes",
54
93
  "policy",
94
+ "webOrigins",
55
95
  "advertisedIp",
56
96
  "scheme",
57
97
  "restartDaemonOnVersionMismatch"
@@ -112,6 +152,7 @@ var defaultConfig = (paths) => {
112
152
  daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
113
153
  eventsLogMaxBytes: 10 * 1024 * 1024,
114
154
  restartDaemonOnVersionMismatch: false,
155
+ webOrigins: [],
115
156
  policy: {
116
157
  default: "allow",
117
158
  destructive: "allow"
@@ -182,6 +223,12 @@ var loadConfig = async (paths, options = {}) => {
182
223
  "restartDaemonOnVersionMismatch"
183
224
  );
184
225
  }
226
+ if (parsed.webOrigins !== void 0) {
227
+ if (!Array.isArray(parsed.webOrigins)) {
228
+ throw configError("webOrigins", "must be an array of origin strings.");
229
+ }
230
+ config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
231
+ }
185
232
  if (parsed.policy !== void 0) {
186
233
  if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
187
234
  throw configError("policy", "must be an object.");
@@ -267,6 +314,34 @@ var isSocketConnectable = (socketPath) => {
267
314
  });
268
315
  };
269
316
 
317
+ // src/daemon/state-dir.ts
318
+ import { homedir } from "node:os";
319
+ import { join } from "node:path";
320
+ import { mkdir, chmod } from "node:fs/promises";
321
+ var resolveStateDir = (override) => {
322
+ return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
323
+ };
324
+ var getStateDirPaths = (stateDir) => {
325
+ return {
326
+ root: stateDir,
327
+ socketPath: join(stateDir, "daemon.sock"),
328
+ pidFilePath: join(stateDir, "daemon.pid"),
329
+ logFilePath: join(stateDir, "daemon.log"),
330
+ eventsLogPath: join(stateDir, "events.log"),
331
+ keyPath: join(stateDir, "key.pem"),
332
+ configPath: join(stateDir, "config.json"),
333
+ auditDir: join(stateDir, "audit"),
334
+ spawnLockPath: join(stateDir, "daemon.spawn.lock")
335
+ };
336
+ };
337
+ var ensureStateDir = async (stateDir) => {
338
+ const paths = getStateDirPaths(stateDir);
339
+ await mkdir(paths.auditDir, { recursive: true });
340
+ await chmod(paths.root, 448);
341
+ await chmod(paths.auditDir, 448);
342
+ return paths;
343
+ };
344
+
270
345
  // src/daemon/log-rotation.ts
271
346
  var STATE_FILE_MODE = 384;
272
347
  var daemonLogBackupPath = (logFilePath) => `${logFilePath}.1`;
@@ -1025,6 +1100,119 @@ var openDaemonStream = async (options) => {
1025
1100
  };
1026
1101
  };
1027
1102
 
1103
+ // src/errors.ts
1104
+ var AppductCliError = class extends Error {
1105
+ type;
1106
+ details;
1107
+ constructor(type, message, details) {
1108
+ super(message);
1109
+ this.name = "AppductCliError";
1110
+ this.type = type;
1111
+ this.details = details;
1112
+ }
1113
+ };
1114
+ var usageError = (message, details) => {
1115
+ return new AppductCliError("usage_error", message, details);
1116
+ };
1117
+ var connectionError = (message, details) => {
1118
+ return new AppductCliError("connection_error", message, details);
1119
+ };
1120
+ var isAppductCliError = (value) => {
1121
+ return value instanceof AppductCliError;
1122
+ };
1123
+
1124
+ // src/client/errors.ts
1125
+ var AppductError = class extends Error {
1126
+ type;
1127
+ details;
1128
+ /** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
1129
+ code;
1130
+ constructor(type, message, details, code) {
1131
+ super(message);
1132
+ this.name = "AppductError";
1133
+ this.type = type;
1134
+ this.details = details;
1135
+ this.code = code;
1136
+ }
1137
+ };
1138
+ var toAppductError = (error) => {
1139
+ if (error instanceof AppductError) {
1140
+ return error;
1141
+ }
1142
+ const withCause = (result) => {
1143
+ result.cause = error;
1144
+ return result;
1145
+ };
1146
+ if (error instanceof DaemonRpcError) {
1147
+ const type = error.data?.type ?? "client_error";
1148
+ return withCause(new AppductError(type, error.message, error.data?.details, error.code));
1149
+ }
1150
+ if (isDaemonUnreachableError(error)) {
1151
+ return withCause(
1152
+ new AppductError(
1153
+ "connection_error",
1154
+ error instanceof Error ? error.message : "The Appduct daemon is unreachable."
1155
+ )
1156
+ );
1157
+ }
1158
+ if (isAppductCliError(error)) {
1159
+ return withCause(new AppductError("client_error", error.message, error.details));
1160
+ }
1161
+ if (error instanceof Error) {
1162
+ return withCause(new AppductError("client_error", error.message));
1163
+ }
1164
+ return new AppductError("client_error", "An unexpected error occurred.", error);
1165
+ };
1166
+
1167
+ // src/client/attach-page.ts
1168
+ var BINDING = "__appductBinding";
1169
+ var ANNOUNCE_DOCUMENT = `if (window === window.top) window.${BINDING}(JSON.stringify({ kind: "context" }));`;
1170
+ var channels = /* @__PURE__ */ new WeakMap();
1171
+ var channelOf = async (page) => {
1172
+ const existing = channels.get(page);
1173
+ if (existing) {
1174
+ existing.reset();
1175
+ await page.evaluate(`window.__APPDUCT__?.receive(${JSON.stringify(JSON.stringify({ kind: "close", code: 1001, reason: "superseded" }))})`).catch(() => void 0);
1176
+ return existing;
1177
+ }
1178
+ let bindingHandlers = [];
1179
+ let resetHandlers = [];
1180
+ const reset = () => resetHandlers.forEach((handler) => handler());
1181
+ await page.exposeBinding(BINDING, (source, json) => {
1182
+ if (source.frame !== page.mainFrame()) return;
1183
+ if (JSON.parse(json).kind === "context") reset();
1184
+ else bindingHandlers.forEach((handler) => handler(json));
1185
+ });
1186
+ await page.addInitScript(ANNOUNCE_DOCUMENT);
1187
+ page.on("close", reset);
1188
+ const channel = {
1189
+ onBindingCall: (handler) => void bindingHandlers.push(handler),
1190
+ onContextReset: (handler) => void resetHandlers.push(handler),
1191
+ evaluate: (expression) => page.evaluate(expression),
1192
+ reset: () => {
1193
+ reset();
1194
+ bindingHandlers = [];
1195
+ resetHandlers = [];
1196
+ }
1197
+ };
1198
+ channels.set(page, channel);
1199
+ return channel;
1200
+ };
1201
+ var attachPage = async (page, options) => {
1202
+ try {
1203
+ parseLink(options.link);
1204
+ const payload = linkPayload(options.link);
1205
+ relayPage(await channelOf(page), options.link, openNodeDaemonSocket);
1206
+ await page.waitForFunction("typeof window.__APPDUCT__?.connect === 'function'");
1207
+ await page.evaluate(`window.__APPDUCT__.connect(${JSON.stringify(payload)}, { transport: "devtools" })`);
1208
+ } catch (error) {
1209
+ throw toAppductError(error);
1210
+ }
1211
+ };
1212
+
1213
+ // src/client/connect.ts
1214
+ import { RPC_METHODS as RPC_METHODS4 } from "@appduct/shared";
1215
+
1028
1216
  // src/client/app-client.ts
1029
1217
  import {
1030
1218
  clampToolTimeoutMs,
@@ -1132,70 +1320,6 @@ var waitForAppEvent = async (stream, options) => {
1132
1320
  }
1133
1321
  };
1134
1322
 
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
1323
  // src/client/app-client.ts
1200
1324
  var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 3e4;
1201
1325
  var CALL_TRANSPORT_TIMEOUT_SLACK_MS = 5e3;
@@ -1965,31 +2089,42 @@ var probeIosProjectYml = async (root) => {
1965
2089
  };
1966
2090
  };
1967
2091
  var PROBES = [
1968
- probeAndroidGradle,
1969
- probeAndroidManifest,
1970
- probeIosInfoPlist,
1971
- probeIosProjectYml
2092
+ ["android", probeAndroidGradle],
2093
+ ["android", probeAndroidManifest],
2094
+ ["ios", probeIosInfoPlist],
2095
+ ["ios", probeIosProjectYml]
1972
2096
  ];
2097
+ var isFlutterProject = async (root) => {
2098
+ return (await readCappedText(join4(root, "pubspec.yaml"))).ok;
2099
+ };
1973
2100
  var discoverNativeScheme = async (root) => {
1974
2101
  const tried = [];
1975
2102
  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);
2103
+ const flutter = await isFlutterProject(root);
2104
+ const androidRoot = flutter ? join4(root, "android") : root;
2105
+ const iosRoots = flutter ? [join4(root, "ios"), join4(root, "macos")] : [root];
2106
+ for (const [platform, probe] of PROBES) {
2107
+ let roots = [root];
2108
+ if (platform === "android") {
2109
+ roots = [androidRoot];
2110
+ } else if (probe === probeIosInfoPlist) {
2111
+ roots = iosRoots;
2112
+ }
2113
+ for (const probeRoot of roots) {
2114
+ const outcome = await probe(probeRoot);
2115
+ tried.push(...outcome.tried);
2116
+ if (outcome.result !== void 0) {
2117
+ results.push(outcome.result);
2118
+ }
1981
2119
  }
1982
2120
  }
1983
- if (results.length === 0) {
1984
- return { tried };
1985
- }
1986
2121
  const distinctSchemes = new Set(results.map((entry) => entry.scheme));
1987
2122
  if (distinctSchemes.size > 1) {
1988
2123
  throw usageError(
1989
2124
  "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
2125
  );
1991
2126
  }
1992
- return { result: results[0], tried };
2127
+ return { results, tried };
1993
2128
  };
1994
2129
 
1995
2130
  // src/scheme.ts
@@ -2052,19 +2187,21 @@ var discoverStaticProjectScheme = async (cwd) => {
2052
2187
  const tried = [`${appJsonPath} ("expo.scheme")`];
2053
2188
  const appJsonScheme = await discoverExpoScheme(cwd);
2054
2189
  if (appJsonScheme !== void 0) {
2055
- return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
2190
+ return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
2056
2191
  }
2057
2192
  const native = await discoverNativeScheme(resolve(cwd));
2058
2193
  tried.push(...native.tried);
2059
- if (native.result !== void 0) {
2194
+ const [first] = native.results;
2195
+ if (first !== void 0) {
2060
2196
  return {
2061
- scheme: native.result.scheme,
2062
- source: native.result.source,
2063
- origin: native.result.origin,
2197
+ scheme: first.scheme,
2198
+ source: first.source,
2199
+ origin: first.origin,
2200
+ nativeResults: native.results,
2064
2201
  tried
2065
2202
  };
2066
2203
  }
2067
- return { tried };
2204
+ return { nativeResults: [], tried };
2068
2205
  };
2069
2206
  var globalConfigDirs = (options = {}) => {
2070
2207
  const excluded = /* @__PURE__ */ new Set([join5(resolve(options.homeDir ?? homedir2()), PROJECT_CONFIG_DIR)]);
@@ -2248,6 +2385,34 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
2248
2385
  // src/link.ts
2249
2386
  var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
2250
2387
  var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
2388
+ var assertWebPageUrl = (pageUrl) => {
2389
+ let protocol;
2390
+ try {
2391
+ protocol = new URL(pageUrl).protocol;
2392
+ } catch {
2393
+ }
2394
+ if (protocol !== "http:" && protocol !== "https:") {
2395
+ throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
2396
+ }
2397
+ };
2398
+ var composeWebLink = (pageUrl, result) => {
2399
+ const payload = result.deepLinkPayload;
2400
+ return {
2401
+ sessionId: result.sessionId,
2402
+ url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
2403
+ script: `window.__APPDUCT__.connect("${payload}")`,
2404
+ expiresAt: result.expiresAt
2405
+ };
2406
+ };
2407
+ var mintWebLink = async (options) => {
2408
+ assertWebPageUrl(options.url);
2409
+ const result = await callDaemon(
2410
+ RPC_METHODS5.linkCreate,
2411
+ { ttlSeconds: options.ttlSeconds, transport: "web" },
2412
+ { stateDir: options.stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn }
2413
+ );
2414
+ return composeWebLink(options.url, result);
2415
+ };
2251
2416
  var mintLink = async (options) => {
2252
2417
  if (options.target !== void 0 && !isOpenTarget(options.target)) {
2253
2418
  throw usageError(
@@ -2334,8 +2499,17 @@ var mintLink = async (options) => {
2334
2499
 
2335
2500
  // src/client/bootstrap.ts
2336
2501
  var DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS = 12e4;
2337
- var link = async (options = {}) => {
2502
+ async function link(options = {}) {
2338
2503
  try {
2504
+ if (options.target === "web") {
2505
+ return await mintWebLink({
2506
+ stateDir: resolveStateDir(options.stateDir),
2507
+ spawn: options.spawn,
2508
+ autoSpawn: options.autoSpawn,
2509
+ ttlSeconds: options.ttlSeconds,
2510
+ url: options.url
2511
+ });
2512
+ }
2339
2513
  return await mintLink({
2340
2514
  stateDir: resolveStateDir(options.stateDir),
2341
2515
  spawn: options.spawn,
@@ -2354,7 +2528,7 @@ var link = async (options = {}) => {
2354
2528
  } catch (error) {
2355
2529
  throw toAppductError(error);
2356
2530
  }
2357
- };
2531
+ }
2358
2532
  var waitForSession = async (sessionId, options = {}) => {
2359
2533
  const stateDir = resolveStateDir(options.stateDir);
2360
2534
  const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS;
@@ -2427,6 +2601,7 @@ var waitForSession = async (sessionId, options = {}) => {
2427
2601
  };
2428
2602
  export {
2429
2603
  AppductError,
2604
+ attachPage,
2430
2605
  connect3 as connect,
2431
2606
  link,
2432
2607
  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
@@ -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>>;
@@ -67,7 +67,7 @@ export type CallsManager = {
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
69
  rejectSession: (sessionId: string, errorType: "session_suspended" | "unknown_session", options?: {
70
- backgrounded?: boolean;
70
+ message?: (alias: string) => string;
71
71
  }) => void;
72
72
  /** Rejects and clears every pending call across every session (daemon shutdown). */
73
73
  disposeAll: () => void;
@@ -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
@@ -7,10 +7,11 @@
7
7
  * recorded to the audit log inline in the `tools.call` handler below.
8
8
  */
9
9
  import type { Clock } from "../cli/types.js";
10
+ import { type ConnectDevtoolsBrowser } from "../devtools-relay/index.js";
10
11
  import { detectAdvertisedAddress } from "./address.js";
11
12
  import { type AppductConfig, type ConfigWarnFn } from "./config.js";
12
13
  import { type EventBus } from "./event-bus.js";
13
- import { type DaemonListener } from "./listener.js";
14
+ import { type DaemonListener, type WebListener } from "./listener.js";
14
15
  import { type RpcServer } from "./rpc-server.js";
15
16
  import { type StateDirPaths } from "./state-dir.js";
16
17
  import { type TimerFns } from "./timers.js";
@@ -27,6 +28,8 @@ export type DaemonOptions = {
27
28
  * `config.advertisedIp`. Overridable so tests can simulate a network change between two
28
29
  * `link.create` calls without mocking `os.networkInterfaces()` process-wide. */
29
30
  detectAddress?: () => ReturnType<typeof detectAdvertisedAddress>;
31
+ /** Reaches a debugging-port browser for `web.attach`; defaults to raw CDP over `ws`. */
32
+ connectDevtoolsBrowser?: ConnectDevtoolsBrowser;
30
33
  };
31
34
  export type RunningDaemon = {
32
35
  paths: StateDirPaths;
@@ -34,6 +37,8 @@ export type RunningDaemon = {
34
37
  startedAt: Date;
35
38
  server: RpcServer;
36
39
  listener: DaemonListener;
40
+ /** The plain-HTTP listener web pages connect to. */
41
+ webListener: WebListener;
37
42
  /** The host certificate the listener serves, so an in-process client can trust it. */
38
43
  tls: TlsManager;
39
44
  eventBus: EventBus;
@@ -5,7 +5,7 @@
5
5
  * Pending links are cheap and re-issuable — they are never persisted and never appear in
6
6
  * `sessions.list`/`sessions.describe` (only claimed sessions do; see sessions.ts).
7
7
  */
8
- import { type AgentEndpoint } from "@appduct/shared";
8
+ import { type AgentEndpoint, type LinkTransport } from "@appduct/shared";
9
9
  import type { Clock } from "../cli/types.js";
10
10
  import type { EventBus } from "./event-bus.js";
11
11
  import type { TimerFns } from "./timers.js";
@@ -15,6 +15,8 @@ export type PendingLink = {
15
15
  token: Buffer;
16
16
  createdAt: Date;
17
17
  expiresAt: Date;
18
+ /** The listener this link can be claimed on, and only that one. */
19
+ transport: LinkTransport;
18
20
  };
19
21
  export type CreatedLink = {
20
22
  link: PendingLink;
@@ -22,8 +24,9 @@ export type CreatedLink = {
22
24
  endpoint: AgentEndpoint;
23
25
  };
24
26
  export type PendingLinkRegistryOptions = {
25
- /** Resolves the current advertised endpoint at mint time (address may be re-detected). */
26
- getEndpoint: () => AgentEndpoint;
27
+ /** Resolves the current advertised endpoint of a transport at mint time (the native address may
28
+ * be re-detected). */
29
+ getEndpoint: (transport: LinkTransport) => AgentEndpoint;
27
30
  clock: Clock;
28
31
  timers: TimerFns;
29
32
  eventBus: EventBus;
@@ -31,8 +34,9 @@ export type PendingLinkRegistryOptions = {
31
34
  export type PendingLinkRegistry = {
32
35
  /** `addressOverride` forces the endpoint address encoded into this one link's bootstrap payload
33
36
  * (the emulator/simulator fast path: `127.0.0.1`), leaving the detected endpoint used by
34
- * every other link unaffected. */
35
- create: (ttlSeconds: number, addressOverride?: string) => CreatedLink;
37
+ * every other link unaffected. It does not apply to a `web` link, whose address is always the
38
+ * loopback one the web listener binds. */
39
+ create: (ttlSeconds: number, addressOverride?: string, transport?: LinkTransport) => CreatedLink;
36
40
  /** Looks up a pending link without consuming it (used to distinguish "unknown" from "already claimed"). */
37
41
  get: (sessionId: string) => PendingLink | undefined;
38
42
  /** Removes and returns the link (successful claim consumes its single use). */