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
|
@@ -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
|
-
|
|
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;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/client/index.js
CHANGED
|
@@ -1,32 +1,70 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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 {
|
|
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
|
-
|
|
2193
|
+
const [first] = native.results;
|
|
2194
|
+
if (first !== void 0) {
|
|
2060
2195
|
return {
|
|
2061
|
-
scheme:
|
|
2062
|
-
source:
|
|
2063
|
-
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
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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>>;
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
*
|
|
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 = {
|
package/dist/commands/link.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/daemon/calls.d.ts
CHANGED
|
@@ -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"
|
|
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
|
};
|
package/dist/daemon/config.d.ts
CHANGED
|
@@ -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
|