appduct 0.14.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 +52 -22
- 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 +962 -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 +286 -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/cli/routes/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
14
14
|
"daemonLogMaxBytes",
|
|
15
15
|
"eventsLogMaxBytes",
|
|
16
16
|
"policy",
|
|
17
|
+
"webOrigins",
|
|
17
18
|
"advertisedIp",
|
|
18
19
|
"scheme",
|
|
19
20
|
"restartDaemonOnVersionMismatch"
|
|
@@ -74,6 +75,7 @@ var defaultConfig = (paths) => {
|
|
|
74
75
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
75
76
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
76
77
|
restartDaemonOnVersionMismatch: false,
|
|
78
|
+
webOrigins: [],
|
|
77
79
|
policy: {
|
|
78
80
|
default: "allow",
|
|
79
81
|
destructive: "allow"
|
|
@@ -144,6 +146,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
144
146
|
"restartDaemonOnVersionMismatch"
|
|
145
147
|
);
|
|
146
148
|
}
|
|
149
|
+
if (parsed.webOrigins !== void 0) {
|
|
150
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
151
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
152
|
+
}
|
|
153
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
154
|
+
}
|
|
147
155
|
if (parsed.policy !== void 0) {
|
|
148
156
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
149
157
|
throw configError("policy", "must be an object.");
|
|
@@ -1069,7 +1077,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
1069
1077
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
1070
1078
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
1071
1079
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
1072
|
-
// assertion failure
|
|
1080
|
+
// assertion failure.
|
|
1073
1081
|
inspection_error: 66,
|
|
1074
1082
|
connection_error: 70,
|
|
1075
1083
|
session_error: 71,
|
|
@@ -1829,23 +1837,34 @@ var probeIosProjectYml = async (root) => {
|
|
|
1829
1837
|
};
|
|
1830
1838
|
};
|
|
1831
1839
|
var PROBES = [
|
|
1832
|
-
probeAndroidGradle,
|
|
1833
|
-
probeAndroidManifest,
|
|
1834
|
-
probeIosInfoPlist,
|
|
1835
|
-
probeIosProjectYml
|
|
1840
|
+
["android", probeAndroidGradle],
|
|
1841
|
+
["android", probeAndroidManifest],
|
|
1842
|
+
["ios", probeIosInfoPlist],
|
|
1843
|
+
["ios", probeIosProjectYml]
|
|
1836
1844
|
];
|
|
1845
|
+
var isFlutterProject = async (root) => {
|
|
1846
|
+
return (await readCappedText(join5(root, "pubspec.yaml"))).ok;
|
|
1847
|
+
};
|
|
1837
1848
|
var discoverNativeScheme = async (root) => {
|
|
1838
1849
|
const tried = [];
|
|
1839
1850
|
const results = [];
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1851
|
+
const flutter = await isFlutterProject(root);
|
|
1852
|
+
const androidRoot = flutter ? join5(root, "android") : root;
|
|
1853
|
+
const iosRoots = flutter ? [join5(root, "ios"), join5(root, "macos")] : [root];
|
|
1854
|
+
for (const [platform, probe] of PROBES) {
|
|
1855
|
+
let roots = [root];
|
|
1856
|
+
if (platform === "android") {
|
|
1857
|
+
roots = [androidRoot];
|
|
1858
|
+
} else if (probe === probeIosInfoPlist) {
|
|
1859
|
+
roots = iosRoots;
|
|
1860
|
+
}
|
|
1861
|
+
for (const probeRoot of roots) {
|
|
1862
|
+
const outcome = await probe(probeRoot);
|
|
1863
|
+
tried.push(...outcome.tried);
|
|
1864
|
+
if (outcome.result !== void 0) {
|
|
1865
|
+
results.push(outcome.result);
|
|
1866
|
+
}
|
|
1845
1867
|
}
|
|
1846
|
-
}
|
|
1847
|
-
if (results.length === 0) {
|
|
1848
|
-
return { tried };
|
|
1849
1868
|
}
|
|
1850
1869
|
const distinctSchemes = new Set(results.map((entry) => entry.scheme));
|
|
1851
1870
|
if (distinctSchemes.size > 1) {
|
|
@@ -1853,7 +1872,7 @@ var discoverNativeScheme = async (root) => {
|
|
|
1853
1872
|
"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."
|
|
1854
1873
|
);
|
|
1855
1874
|
}
|
|
1856
|
-
return {
|
|
1875
|
+
return { results, tried };
|
|
1857
1876
|
};
|
|
1858
1877
|
|
|
1859
1878
|
// src/scheme.ts
|
|
@@ -1916,19 +1935,21 @@ var discoverStaticProjectScheme = async (cwd) => {
|
|
|
1916
1935
|
const tried = [`${appJsonPath} ("expo.scheme")`];
|
|
1917
1936
|
const appJsonScheme = await discoverExpoScheme(cwd);
|
|
1918
1937
|
if (appJsonScheme !== void 0) {
|
|
1919
|
-
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
|
|
1938
|
+
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
|
|
1920
1939
|
}
|
|
1921
1940
|
const native = await discoverNativeScheme(resolve(cwd));
|
|
1922
1941
|
tried.push(...native.tried);
|
|
1923
|
-
|
|
1942
|
+
const [first] = native.results;
|
|
1943
|
+
if (first !== void 0) {
|
|
1924
1944
|
return {
|
|
1925
|
-
scheme:
|
|
1926
|
-
source:
|
|
1927
|
-
origin:
|
|
1945
|
+
scheme: first.scheme,
|
|
1946
|
+
source: first.source,
|
|
1947
|
+
origin: first.origin,
|
|
1948
|
+
nativeResults: native.results,
|
|
1928
1949
|
tried
|
|
1929
1950
|
};
|
|
1930
1951
|
}
|
|
1931
|
-
return { tried };
|
|
1952
|
+
return { nativeResults: [], tried };
|
|
1932
1953
|
};
|
|
1933
1954
|
var globalConfigDirs = (options = {}) => {
|
|
1934
1955
|
const excluded = /* @__PURE__ */ new Set([join6(resolve(options.homeDir ?? homedir()), PROJECT_CONFIG_DIR)]);
|
|
@@ -2104,6 +2125,25 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
|
|
|
2104
2125
|
|
|
2105
2126
|
// src/link.ts
|
|
2106
2127
|
var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
|
|
2128
|
+
var assertWebPageUrl = (pageUrl) => {
|
|
2129
|
+
let protocol;
|
|
2130
|
+
try {
|
|
2131
|
+
protocol = new URL(pageUrl).protocol;
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
2135
|
+
throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
var composeWebLink = (pageUrl, result) => {
|
|
2139
|
+
const payload = result.deepLinkPayload;
|
|
2140
|
+
return {
|
|
2141
|
+
sessionId: result.sessionId,
|
|
2142
|
+
url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
|
|
2143
|
+
script: `window.__APPDUCT__.connect("${payload}")`,
|
|
2144
|
+
expiresAt: result.expiresAt
|
|
2145
|
+
};
|
|
2146
|
+
};
|
|
2107
2147
|
|
|
2108
2148
|
// src/qr-terminal.ts
|
|
2109
2149
|
import { toQR } from "toqr";
|
|
@@ -2146,14 +2186,18 @@ var WAIT_FOR_SESSION_TOOL_NAME = "appduct_wait_for_session";
|
|
|
2146
2186
|
var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
|
|
2147
2187
|
var DEFAULT_WAIT_TIMEOUT_MS = 12e4;
|
|
2148
2188
|
var NO_TARGET = "none";
|
|
2189
|
+
var WEB_TARGET = "web";
|
|
2149
2190
|
var QR_INSTRUCTIONS = 'This link was NOT delivered to a device \u2014 a human has to open it. Before calling appduct_wait_for_session: show the user the "qr" field verbatim inside a fenced code block (it is pre-rendered terminal art \u2014 do not redraw, summarize, or describe it), show "deepLink" underneath it for a device that cannot scan, and ask the user to scan it with the target device. Only then call appduct_wait_for_session; it blocks until the device connects and gives no output while it waits, so calling it before you have asked will look like a hang.';
|
|
2150
2191
|
var CONNECT_TOOL_DESCRIPTOR = {
|
|
2151
2192
|
name: CONNECT_TOOL_NAME,
|
|
2152
|
-
description:
|
|
2193
|
+
description: `Mint an Appduct session link and get it onto a device. With no "target", auto-detects a booted iOS simulator or attached Android device and delivers the link to it, returning { sessionId, delivered: true, autoDetected: true } \u2014 this is the normal agent path and needs no human. Pass target "android" or "ios-sim" (optionally with "device") to choose explicitly, or target "none" to force the human flow. Delivering to "android" (explicit or auto-detected) needs "appId" (the Android package name) \u2014 without it, more than one installed app declaring the same scheme would show an ambiguous "Open with" chooser on the device instead of failing loudly. Target "ios-device" is an experimental, never-auto-detected path that delivers to a paired physical iPhone/iPad via xcrun devicectl; it needs "appId" (the iOS bundle id), iOS 17+, Xcode 15+, a connected and trusted device with Developer Mode on, a dev-signed build installed, and the phone on the same network as this machine. An "appId" not supplied here falls back to "appId.<platform>" in the nearest .appduct/config.json (see "appduct init"); it is rejected outright with target "ios-sim" or "none", which need no app id. Pass "relaunch": true with ios-device if the app is already running and the delivery does not take. Target "web" is for a web page: pass "url" (the page to open) and get back { url, script } \u2014 open "url" in a browser, or run "script" in a page that is already open, then call appduct_wait_for_session. It needs no scheme and no device. To have the daemon attach a page that is already open in a Chrome launched with --remote-debugging-port, also pass "browserUrl" (its debugging endpoint on this machine: 127.0.0.1, [::1] or localhost, such as http://127.0.0.1:9222): the tab whose address starts with "url" is attached and the call returns { sessionId, url, targetId, attached: true }, with no further step; "url" is the tab's own address. When no tab or several match, the error lists the open tabs with their target ids; pass one as "targetId" to pick it. A popup, or a page in a new tab, is not attached. Only when no device is detected (or target is "none") does this return a QR code for a human to scan, along with an "instructions" field saying what to do with it. Follow up with appduct_wait_for_session to know when the device has connected.`,
|
|
2153
2194
|
inputSchema: {
|
|
2154
2195
|
type: "object",
|
|
2155
2196
|
properties: {
|
|
2156
|
-
target: { type: "string", enum: ["android", "ios-sim", "ios-device", NO_TARGET] },
|
|
2197
|
+
target: { type: "string", enum: ["android", "ios-sim", "ios-device", NO_TARGET, WEB_TARGET] },
|
|
2198
|
+
url: { type: "string" },
|
|
2199
|
+
browserUrl: { type: "string" },
|
|
2200
|
+
targetId: { type: "string" },
|
|
2157
2201
|
device: { type: "string" },
|
|
2158
2202
|
appId: { type: "string" },
|
|
2159
2203
|
relaunch: { type: "boolean" },
|
|
@@ -2192,10 +2236,10 @@ var asOptionalConnectTarget = (value) => {
|
|
|
2192
2236
|
if (value === void 0) {
|
|
2193
2237
|
return void 0;
|
|
2194
2238
|
}
|
|
2195
|
-
if (typeof value !== "string" || !(isOpenTarget(value) || value === NO_TARGET)) {
|
|
2239
|
+
if (typeof value !== "string" || !(isOpenTarget(value) || value === NO_TARGET || value === WEB_TARGET)) {
|
|
2196
2240
|
throw new McpBuiltinToolError(
|
|
2197
2241
|
"invalid_request",
|
|
2198
|
-
'"target" must be "android", "ios-sim", "ios-device", or "none".'
|
|
2242
|
+
'"target" must be "android", "ios-sim", "ios-device", "web", or "none".'
|
|
2199
2243
|
);
|
|
2200
2244
|
}
|
|
2201
2245
|
return value;
|
|
@@ -2273,6 +2317,42 @@ var handleConnectTool = async (rawArgs, deps) => {
|
|
|
2273
2317
|
const appId = asOptionalNonEmptyString(args.appId, "appId");
|
|
2274
2318
|
const relaunch = asOptionalBoolean(args.relaunch, "relaunch");
|
|
2275
2319
|
const ttlSeconds = asOptionalPositiveNumber(args.ttlSeconds, "ttlSeconds");
|
|
2320
|
+
const url = asOptionalNonEmptyString(args.url, "url");
|
|
2321
|
+
const browserUrl = asOptionalNonEmptyString(args.browserUrl, "browserUrl");
|
|
2322
|
+
const targetId = asOptionalNonEmptyString(args.targetId, "targetId");
|
|
2323
|
+
if ((browserUrl !== void 0 || targetId !== void 0) && requestedTarget !== WEB_TARGET) {
|
|
2324
|
+
throw new McpBuiltinToolError("invalid_request", '"browserUrl" and "targetId" only apply with target "web".');
|
|
2325
|
+
}
|
|
2326
|
+
if (targetId !== void 0 && browserUrl === void 0) {
|
|
2327
|
+
throw new McpBuiltinToolError("invalid_request", '"targetId" needs "browserUrl": it names a tab of that browser.');
|
|
2328
|
+
}
|
|
2329
|
+
if (requestedTarget === WEB_TARGET) {
|
|
2330
|
+
if (url === void 0) {
|
|
2331
|
+
throw new McpBuiltinToolError("invalid_request", '"url" (the page to open) is required with target "web".');
|
|
2332
|
+
}
|
|
2333
|
+
if (device !== void 0 || appId !== void 0 || relaunch !== void 0) {
|
|
2334
|
+
throw new McpBuiltinToolError(
|
|
2335
|
+
"invalid_request",
|
|
2336
|
+
'"device", "appId" and "relaunch" do not apply with target "web".'
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
assertWebPageUrl(url);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
throw new McpBuiltinToolError("invalid_request", error.message);
|
|
2343
|
+
}
|
|
2344
|
+
if (browserUrl !== void 0) {
|
|
2345
|
+
const attached = await deps.call(RPC_METHODS3.webAttach, { url, browserUrl, targetId, ttlSeconds });
|
|
2346
|
+
return { ...attached, attached: true };
|
|
2347
|
+
}
|
|
2348
|
+
return composeWebLink(
|
|
2349
|
+
url,
|
|
2350
|
+
await deps.call(RPC_METHODS3.linkCreate, { ttlSeconds, transport: "web" })
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
if (url !== void 0) {
|
|
2354
|
+
throw new McpBuiltinToolError("invalid_request", '"url" only applies with target "web".');
|
|
2355
|
+
}
|
|
2276
2356
|
if (device !== void 0 && (requestedTarget === void 0 || requestedTarget === NO_TARGET)) {
|
|
2277
2357
|
throw new McpBuiltinToolError(
|
|
2278
2358
|
"invalid_request",
|
|
@@ -3678,6 +3758,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
3678
3758
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3679
3759
|
};
|
|
3680
3760
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
3761
|
+
if ("attached" in data) {
|
|
3762
|
+
return [
|
|
3763
|
+
colors.green("Page Attached"),
|
|
3764
|
+
...renderFields(
|
|
3765
|
+
"Page",
|
|
3766
|
+
[
|
|
3767
|
+
["Session", data.sessionId],
|
|
3768
|
+
["Tab", data.targetId],
|
|
3769
|
+
["URL", data.url],
|
|
3770
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3771
|
+
],
|
|
3772
|
+
flags
|
|
3773
|
+
)
|
|
3774
|
+
];
|
|
3775
|
+
}
|
|
3776
|
+
if ("script" in data) {
|
|
3777
|
+
return [
|
|
3778
|
+
colors.green("Link Created"),
|
|
3779
|
+
...renderFields(
|
|
3780
|
+
"Link",
|
|
3781
|
+
[
|
|
3782
|
+
["Session", data.sessionId],
|
|
3783
|
+
["URL", data.url],
|
|
3784
|
+
["Script", data.script],
|
|
3785
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3786
|
+
],
|
|
3787
|
+
flags
|
|
3788
|
+
)
|
|
3789
|
+
];
|
|
3790
|
+
}
|
|
3681
3791
|
const lines = [
|
|
3682
3792
|
colors.green("Link Created"),
|
|
3683
3793
|
...renderFields(
|
|
@@ -3798,6 +3908,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
3798
3908
|
["PID", data.daemon.pid],
|
|
3799
3909
|
["Started at", data.daemon.started_at],
|
|
3800
3910
|
["WSS port", data.daemon.wss_port],
|
|
3911
|
+
["Web port", data.daemon.web_port],
|
|
3801
3912
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
3802
3913
|
["Sessions", data.daemon.session_count]
|
|
3803
3914
|
],
|
|
@@ -93,7 +93,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
93
93
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
94
94
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
95
95
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
96
|
-
// assertion failure
|
|
96
|
+
// assertion failure.
|
|
97
97
|
inspection_error: 66,
|
|
98
98
|
connection_error: 70,
|
|
99
99
|
session_error: 71,
|
|
@@ -549,6 +549,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
549
549
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
550
550
|
};
|
|
551
551
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
552
|
+
if ("attached" in data) {
|
|
553
|
+
return [
|
|
554
|
+
colors.green("Page Attached"),
|
|
555
|
+
...renderFields(
|
|
556
|
+
"Page",
|
|
557
|
+
[
|
|
558
|
+
["Session", data.sessionId],
|
|
559
|
+
["Tab", data.targetId],
|
|
560
|
+
["URL", data.url],
|
|
561
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
562
|
+
],
|
|
563
|
+
flags
|
|
564
|
+
)
|
|
565
|
+
];
|
|
566
|
+
}
|
|
567
|
+
if ("script" in data) {
|
|
568
|
+
return [
|
|
569
|
+
colors.green("Link Created"),
|
|
570
|
+
...renderFields(
|
|
571
|
+
"Link",
|
|
572
|
+
[
|
|
573
|
+
["Session", data.sessionId],
|
|
574
|
+
["URL", data.url],
|
|
575
|
+
["Script", data.script],
|
|
576
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
577
|
+
],
|
|
578
|
+
flags
|
|
579
|
+
)
|
|
580
|
+
];
|
|
581
|
+
}
|
|
552
582
|
const lines = [
|
|
553
583
|
colors.green("Link Created"),
|
|
554
584
|
...renderFields(
|
|
@@ -669,6 +699,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
669
699
|
["PID", data.daemon.pid],
|
|
670
700
|
["Started at", data.daemon.started_at],
|
|
671
701
|
["WSS port", data.daemon.wss_port],
|
|
702
|
+
["Web port", data.daemon.web_port],
|
|
672
703
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
673
704
|
["Sessions", data.daemon.session_count]
|
|
674
705
|
],
|
|
@@ -27,6 +27,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
27
27
|
"daemonLogMaxBytes",
|
|
28
28
|
"eventsLogMaxBytes",
|
|
29
29
|
"policy",
|
|
30
|
+
"webOrigins",
|
|
30
31
|
"advertisedIp",
|
|
31
32
|
"scheme",
|
|
32
33
|
"restartDaemonOnVersionMismatch"
|
|
@@ -87,6 +88,7 @@ var defaultConfig = (paths) => {
|
|
|
87
88
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
88
89
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
89
90
|
restartDaemonOnVersionMismatch: false,
|
|
91
|
+
webOrigins: [],
|
|
90
92
|
policy: {
|
|
91
93
|
default: "allow",
|
|
92
94
|
destructive: "allow"
|
|
@@ -157,6 +159,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
157
159
|
"restartDaemonOnVersionMismatch"
|
|
158
160
|
);
|
|
159
161
|
}
|
|
162
|
+
if (parsed.webOrigins !== void 0) {
|
|
163
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
164
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
165
|
+
}
|
|
166
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
167
|
+
}
|
|
160
168
|
if (parsed.policy !== void 0) {
|
|
161
169
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
162
170
|
throw configError("policy", "must be an object.");
|
|
@@ -916,7 +924,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
916
924
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
917
925
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
918
926
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
919
|
-
// assertion failure
|
|
927
|
+
// assertion failure.
|
|
920
928
|
inspection_error: 66,
|
|
921
929
|
connection_error: 70,
|
|
922
930
|
session_error: 71,
|
|
@@ -1643,31 +1651,42 @@ var probeIosProjectYml = async (root) => {
|
|
|
1643
1651
|
};
|
|
1644
1652
|
};
|
|
1645
1653
|
var PROBES = [
|
|
1646
|
-
probeAndroidGradle,
|
|
1647
|
-
probeAndroidManifest,
|
|
1648
|
-
probeIosInfoPlist,
|
|
1649
|
-
probeIosProjectYml
|
|
1654
|
+
["android", probeAndroidGradle],
|
|
1655
|
+
["android", probeAndroidManifest],
|
|
1656
|
+
["ios", probeIosInfoPlist],
|
|
1657
|
+
["ios", probeIosProjectYml]
|
|
1650
1658
|
];
|
|
1659
|
+
var isFlutterProject = async (root) => {
|
|
1660
|
+
return (await readCappedText(join4(root, "pubspec.yaml"))).ok;
|
|
1661
|
+
};
|
|
1651
1662
|
var discoverNativeScheme = async (root) => {
|
|
1652
1663
|
const tried = [];
|
|
1653
1664
|
const results = [];
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1665
|
+
const flutter = await isFlutterProject(root);
|
|
1666
|
+
const androidRoot = flutter ? join4(root, "android") : root;
|
|
1667
|
+
const iosRoots = flutter ? [join4(root, "ios"), join4(root, "macos")] : [root];
|
|
1668
|
+
for (const [platform, probe] of PROBES) {
|
|
1669
|
+
let roots = [root];
|
|
1670
|
+
if (platform === "android") {
|
|
1671
|
+
roots = [androidRoot];
|
|
1672
|
+
} else if (probe === probeIosInfoPlist) {
|
|
1673
|
+
roots = iosRoots;
|
|
1674
|
+
}
|
|
1675
|
+
for (const probeRoot of roots) {
|
|
1676
|
+
const outcome = await probe(probeRoot);
|
|
1677
|
+
tried.push(...outcome.tried);
|
|
1678
|
+
if (outcome.result !== void 0) {
|
|
1679
|
+
results.push(outcome.result);
|
|
1680
|
+
}
|
|
1659
1681
|
}
|
|
1660
1682
|
}
|
|
1661
|
-
if (results.length === 0) {
|
|
1662
|
-
return { tried };
|
|
1663
|
-
}
|
|
1664
1683
|
const distinctSchemes = new Set(results.map((entry) => entry.scheme));
|
|
1665
1684
|
if (distinctSchemes.size > 1) {
|
|
1666
1685
|
throw usageError(
|
|
1667
1686
|
"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."
|
|
1668
1687
|
);
|
|
1669
1688
|
}
|
|
1670
|
-
return {
|
|
1689
|
+
return { results, tried };
|
|
1671
1690
|
};
|
|
1672
1691
|
|
|
1673
1692
|
// src/scheme.ts
|
|
@@ -1730,19 +1749,21 @@ var discoverStaticProjectScheme = async (cwd) => {
|
|
|
1730
1749
|
const tried = [`${appJsonPath} ("expo.scheme")`];
|
|
1731
1750
|
const appJsonScheme = await discoverExpoScheme(cwd);
|
|
1732
1751
|
if (appJsonScheme !== void 0) {
|
|
1733
|
-
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
|
|
1752
|
+
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
|
|
1734
1753
|
}
|
|
1735
1754
|
const native = await discoverNativeScheme(resolve(cwd));
|
|
1736
1755
|
tried.push(...native.tried);
|
|
1737
|
-
|
|
1756
|
+
const [first] = native.results;
|
|
1757
|
+
if (first !== void 0) {
|
|
1738
1758
|
return {
|
|
1739
|
-
scheme:
|
|
1740
|
-
source:
|
|
1741
|
-
origin:
|
|
1759
|
+
scheme: first.scheme,
|
|
1760
|
+
source: first.source,
|
|
1761
|
+
origin: first.origin,
|
|
1762
|
+
nativeResults: native.results,
|
|
1742
1763
|
tried
|
|
1743
1764
|
};
|
|
1744
1765
|
}
|
|
1745
|
-
return { tried };
|
|
1766
|
+
return { nativeResults: [], tried };
|
|
1746
1767
|
};
|
|
1747
1768
|
var globalConfigDirs = (options = {}) => {
|
|
1748
1769
|
const excluded = /* @__PURE__ */ new Set([join5(resolve(options.homeDir ?? homedir()), PROJECT_CONFIG_DIR)]);
|
|
@@ -1926,6 +1947,39 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
|
|
|
1926
1947
|
// src/link.ts
|
|
1927
1948
|
var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
|
|
1928
1949
|
var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
|
|
1950
|
+
var assertWebPageUrl = (pageUrl) => {
|
|
1951
|
+
let protocol;
|
|
1952
|
+
try {
|
|
1953
|
+
protocol = new URL(pageUrl).protocol;
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
1957
|
+
throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
var composeWebLink = (pageUrl, result) => {
|
|
1961
|
+
const payload = result.deepLinkPayload;
|
|
1962
|
+
return {
|
|
1963
|
+
sessionId: result.sessionId,
|
|
1964
|
+
url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
|
|
1965
|
+
script: `window.__APPDUCT__.connect("${payload}")`,
|
|
1966
|
+
expiresAt: result.expiresAt
|
|
1967
|
+
};
|
|
1968
|
+
};
|
|
1969
|
+
var mintWebLink = async (options) => {
|
|
1970
|
+
assertWebPageUrl(options.url);
|
|
1971
|
+
const result = await callDaemon(
|
|
1972
|
+
RPC_METHODS2.linkCreate,
|
|
1973
|
+
{ ttlSeconds: options.ttlSeconds, transport: "web" },
|
|
1974
|
+
{ stateDir: options.stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn }
|
|
1975
|
+
);
|
|
1976
|
+
return composeWebLink(options.url, result);
|
|
1977
|
+
};
|
|
1978
|
+
var attachWebTab = async ({ stateDir, spawn, autoSpawn, ...params }) => {
|
|
1979
|
+
assertWebPageUrl(params.url);
|
|
1980
|
+
const result = await callDaemon(RPC_METHODS2.webAttach, params, { stateDir, spawn, autoSpawn, requestTimeoutMs: 3e4 });
|
|
1981
|
+
return { ...result, attached: true };
|
|
1982
|
+
};
|
|
1929
1983
|
var mintLink = async (options) => {
|
|
1930
1984
|
if (options.target !== void 0 && !isOpenTarget(options.target)) {
|
|
1931
1985
|
throw usageError(
|
|
@@ -2013,6 +2067,9 @@ var mintLink = async (options) => {
|
|
|
2013
2067
|
// src/commands/link.ts
|
|
2014
2068
|
var handleLinkCommand = async (options, context) => {
|
|
2015
2069
|
let openTarget;
|
|
2070
|
+
if (options.browserUrl !== void 0 || options.targetId !== void 0) {
|
|
2071
|
+
throw usageError('"--browser-url" and "--target-id" only apply with "--open web".');
|
|
2072
|
+
}
|
|
2016
2073
|
if (options.open !== void 0) {
|
|
2017
2074
|
if (!isOpenTarget(options.open)) {
|
|
2018
2075
|
throw usageError(
|
|
@@ -2046,6 +2103,35 @@ var handleLinkCommand = async (options, context) => {
|
|
|
2046
2103
|
});
|
|
2047
2104
|
return { ok: true, data: result };
|
|
2048
2105
|
};
|
|
2106
|
+
var handleWebLinkCommand = async (options, context) => {
|
|
2107
|
+
if (options.url === void 0) {
|
|
2108
|
+
throw usageError('"--open web" needs the page URL: appduct sessions link --open web <url>.');
|
|
2109
|
+
}
|
|
2110
|
+
if (options.device !== void 0 || options.appId !== void 0 || options.relaunch !== void 0) {
|
|
2111
|
+
throw usageError('"--device", "--app-id" and "--relaunch" do not apply with "--open web".');
|
|
2112
|
+
}
|
|
2113
|
+
if (options.targetId !== void 0 && options.browserUrl === void 0) {
|
|
2114
|
+
throw usageError('"--target-id" needs "--browser-url": it names a tab of that browser.');
|
|
2115
|
+
}
|
|
2116
|
+
if (options.browserUrl !== void 0) {
|
|
2117
|
+
const attached = await attachWebTab({
|
|
2118
|
+
stateDir: context.stateDir,
|
|
2119
|
+
spawn: context.spawn,
|
|
2120
|
+
ttlSeconds: options.ttlSeconds,
|
|
2121
|
+
url: options.url,
|
|
2122
|
+
browserUrl: options.browserUrl,
|
|
2123
|
+
targetId: options.targetId
|
|
2124
|
+
});
|
|
2125
|
+
return { ok: true, data: attached };
|
|
2126
|
+
}
|
|
2127
|
+
const result = await mintWebLink({
|
|
2128
|
+
stateDir: context.stateDir,
|
|
2129
|
+
spawn: context.spawn,
|
|
2130
|
+
ttlSeconds: options.ttlSeconds,
|
|
2131
|
+
url: options.url
|
|
2132
|
+
});
|
|
2133
|
+
return { ok: true, data: result };
|
|
2134
|
+
};
|
|
2049
2135
|
|
|
2050
2136
|
// src/cli/command-options.ts
|
|
2051
2137
|
var parsePositiveIntegerOption = (value, flagName) => {
|
|
@@ -2404,6 +2490,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
2404
2490
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
2405
2491
|
};
|
|
2406
2492
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
2493
|
+
if ("attached" in data) {
|
|
2494
|
+
return [
|
|
2495
|
+
colors.green("Page Attached"),
|
|
2496
|
+
...renderFields(
|
|
2497
|
+
"Page",
|
|
2498
|
+
[
|
|
2499
|
+
["Session", data.sessionId],
|
|
2500
|
+
["Tab", data.targetId],
|
|
2501
|
+
["URL", data.url],
|
|
2502
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
2503
|
+
],
|
|
2504
|
+
flags
|
|
2505
|
+
)
|
|
2506
|
+
];
|
|
2507
|
+
}
|
|
2508
|
+
if ("script" in data) {
|
|
2509
|
+
return [
|
|
2510
|
+
colors.green("Link Created"),
|
|
2511
|
+
...renderFields(
|
|
2512
|
+
"Link",
|
|
2513
|
+
[
|
|
2514
|
+
["Session", data.sessionId],
|
|
2515
|
+
["URL", data.url],
|
|
2516
|
+
["Script", data.script],
|
|
2517
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
2518
|
+
],
|
|
2519
|
+
flags
|
|
2520
|
+
)
|
|
2521
|
+
];
|
|
2522
|
+
}
|
|
2407
2523
|
const lines = [
|
|
2408
2524
|
colors.green("Link Created"),
|
|
2409
2525
|
...renderFields(
|
|
@@ -2524,6 +2640,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
2524
2640
|
["PID", data.daemon.pid],
|
|
2525
2641
|
["Started at", data.daemon.started_at],
|
|
2526
2642
|
["WSS port", data.daemon.wss_port],
|
|
2643
|
+
["Web port", data.daemon.web_port],
|
|
2527
2644
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
2528
2645
|
["Sessions", data.daemon.session_count]
|
|
2529
2646
|
],
|
|
@@ -2747,10 +2864,13 @@ var route = async (context) => {
|
|
|
2747
2864
|
return executeCommand(
|
|
2748
2865
|
commandName(context),
|
|
2749
2866
|
guarded(context)(
|
|
2750
|
-
() => handleLinkCommand(
|
|
2867
|
+
() => (options.open === "web" ? handleWebLinkCommand : handleLinkCommand)(
|
|
2751
2868
|
{
|
|
2752
2869
|
ttlSeconds: parsePositiveIntegerOption(options.ttl, "--ttl"),
|
|
2753
2870
|
scheme: typeof options.scheme === "string" ? options.scheme : void 0,
|
|
2871
|
+
url: context.args[0],
|
|
2872
|
+
browserUrl: typeof options.browserUrl === "string" ? options.browserUrl : void 0,
|
|
2873
|
+
targetId: typeof options.targetId === "string" ? options.targetId : void 0,
|
|
2754
2874
|
open: typeof options.open === "string" ? options.open : void 0,
|
|
2755
2875
|
device: typeof options.device === "string" ? options.device : void 0,
|
|
2756
2876
|
// cac camelCases `--app-id`; the dashed spelling is kept as a fallback so a
|
|
@@ -24,6 +24,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
24
24
|
"daemonLogMaxBytes",
|
|
25
25
|
"eventsLogMaxBytes",
|
|
26
26
|
"policy",
|
|
27
|
+
"webOrigins",
|
|
27
28
|
"advertisedIp",
|
|
28
29
|
"scheme",
|
|
29
30
|
"restartDaemonOnVersionMismatch"
|
|
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
|
|
|
84
85
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
85
86
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
86
87
|
restartDaemonOnVersionMismatch: false,
|
|
88
|
+
webOrigins: [],
|
|
87
89
|
policy: {
|
|
88
90
|
default: "allow",
|
|
89
91
|
destructive: "allow"
|
|
@@ -154,6 +156,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
154
156
|
"restartDaemonOnVersionMismatch"
|
|
155
157
|
);
|
|
156
158
|
}
|
|
159
|
+
if (parsed.webOrigins !== void 0) {
|
|
160
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
161
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
162
|
+
}
|
|
163
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
164
|
+
}
|
|
157
165
|
if (parsed.policy !== void 0) {
|
|
158
166
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
159
167
|
throw configError("policy", "must be an object.");
|
|
@@ -923,7 +931,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
923
931
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
924
932
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
925
933
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
926
|
-
// assertion failure
|
|
934
|
+
// assertion failure.
|
|
927
935
|
inspection_error: 66,
|
|
928
936
|
connection_error: 70,
|
|
929
937
|
session_error: 71,
|
|
@@ -1376,6 +1384,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
1376
1384
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
1377
1385
|
};
|
|
1378
1386
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
1387
|
+
if ("attached" in data) {
|
|
1388
|
+
return [
|
|
1389
|
+
colors.green("Page Attached"),
|
|
1390
|
+
...renderFields(
|
|
1391
|
+
"Page",
|
|
1392
|
+
[
|
|
1393
|
+
["Session", data.sessionId],
|
|
1394
|
+
["Tab", data.targetId],
|
|
1395
|
+
["URL", data.url],
|
|
1396
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
1397
|
+
],
|
|
1398
|
+
flags
|
|
1399
|
+
)
|
|
1400
|
+
];
|
|
1401
|
+
}
|
|
1402
|
+
if ("script" in data) {
|
|
1403
|
+
return [
|
|
1404
|
+
colors.green("Link Created"),
|
|
1405
|
+
...renderFields(
|
|
1406
|
+
"Link",
|
|
1407
|
+
[
|
|
1408
|
+
["Session", data.sessionId],
|
|
1409
|
+
["URL", data.url],
|
|
1410
|
+
["Script", data.script],
|
|
1411
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
1412
|
+
],
|
|
1413
|
+
flags
|
|
1414
|
+
)
|
|
1415
|
+
];
|
|
1416
|
+
}
|
|
1379
1417
|
const lines = [
|
|
1380
1418
|
colors.green("Link Created"),
|
|
1381
1419
|
...renderFields(
|
|
@@ -1496,6 +1534,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
1496
1534
|
["PID", data.daemon.pid],
|
|
1497
1535
|
["Started at", data.daemon.started_at],
|
|
1498
1536
|
["WSS port", data.daemon.wss_port],
|
|
1537
|
+
["Web port", data.daemon.web_port],
|
|
1499
1538
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
1500
1539
|
["Sessions", data.daemon.session_count]
|
|
1501
1540
|
],
|