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.
- package/README.md +60 -287
- package/dist/artifact-inspect.d.ts +38 -39
- package/dist/bin.js +45 -3
- package/dist/cli/result-types.d.ts +27 -1
- package/dist/cli/routes/daemon/index.js +32 -1
- package/dist/cli/routes/daemon/run.js +963 -521
- package/dist/cli/routes/daemon/start.js +40 -1
- package/dist/cli/routes/daemon/status.js +41 -1
- package/dist/cli/routes/daemon/stop.js +40 -1
- package/dist/cli/routes/doctor.js +40 -2
- package/dist/cli/routes/events/index.js +32 -1
- package/dist/cli/routes/events/ls.js +40 -1
- package/dist/cli/routes/events/since.js +40 -1
- package/dist/cli/routes/events/tail.js +40 -1
- package/dist/cli/routes/init.js +207 -107
- package/dist/cli/routes/keygen.js +32 -1
- package/dist/cli/routes/mcp.js +135 -24
- package/dist/cli/routes/sessions/index.js +32 -1
- package/dist/cli/routes/sessions/link.js +141 -21
- package/dist/cli/routes/sessions/ls.js +40 -1
- package/dist/cli/routes/sessions/revoke.js +40 -1
- package/dist/cli/routes/tools/call.js +40 -1
- package/dist/cli/routes/tools/describe.js +40 -1
- package/dist/cli/routes/tools/index.js +32 -1
- package/dist/cli/routes/tools/ls.js +40 -1
- 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 +287 -112
- package/dist/commands/doctor.d.ts +3 -4
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/link.d.ts +10 -1
- package/dist/daemon/calls.d.ts +1 -1
- package/dist/daemon/config.d.ts +6 -0
- package/dist/daemon/daemon.d.ts +6 -1
- package/dist/daemon/links.d.ts +9 -5
- package/dist/daemon/listener.d.ts +28 -1
- package/dist/daemon/rpc-errors.d.ts +3 -0
- package/dist/daemon/sessions.d.ts +11 -7
- 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 +45 -3
- package/dist/link.d.ts +35 -1
- package/dist/mcp/connect-tool.d.ts +15 -2
- package/dist/native-scheme.d.ts +11 -7
- package/dist/scheme.d.ts +5 -1
- package/package.json +15 -5
package/dist/client/index.js
CHANGED
|
@@ -1,32 +1,71 @@
|
|
|
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 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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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 {
|
|
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
|
-
|
|
2194
|
+
const [first] = native.results;
|
|
2195
|
+
if (first !== void 0) {
|
|
2060
2196
|
return {
|
|
2061
|
-
scheme:
|
|
2062
|
-
source:
|
|
2063
|
-
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
|
-
|
|
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
|
|
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
|
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
|
@@ -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
|
-
|
|
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;
|
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
|
package/dist/daemon/daemon.d.ts
CHANGED
|
@@ -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;
|
package/dist/daemon/links.d.ts
CHANGED
|
@@ -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
|
|
26
|
-
|
|
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
|
-
|
|
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). */
|