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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The pinned-TLS WebSocket listener (ARCHITECTURE.md §7).
|
|
2
|
+
* The pinned-TLS WebSocket listener and the plain-HTTP web listener (ARCHITECTURE.md §7). Both put
|
|
3
|
+
* the same thin frame-level gate in front of
|
|
3
4
|
* `SessionManager`: JSON/binary/size hygiene, the pre-claim timeout, and post-claim
|
|
4
5
|
* type/session-id routing. All state-machine decisions (claim/resume/registry/close-code choice)
|
|
5
6
|
* live in sessions.ts — this module never inspects session state itself.
|
|
@@ -14,7 +15,13 @@ import { type SessionManager } from "./sessions.js";
|
|
|
14
15
|
import type { TlsManager, TlsMaterial } from "./tls.js";
|
|
15
16
|
import { type TimerFns } from "./timers.js";
|
|
16
17
|
export type ListenerOptions = {
|
|
18
|
+
/** `0` asks the OS for a free port. */
|
|
17
19
|
port: number;
|
|
20
|
+
/** Also bind `127.0.0.1` on the same port (issue #249). macOS lets another program bind
|
|
21
|
+
* `127.0.0.1:<port>` under a wildcard listener and then routes loopback clients to it; holding
|
|
22
|
+
* that address ourselves stops it. Linux refuses that bind for everyone, us included, because
|
|
23
|
+
* the wildcard listener already covers loopback there. */
|
|
24
|
+
bindLoopback: boolean;
|
|
18
25
|
tls: TlsManager;
|
|
19
26
|
sessionManager: SessionManager;
|
|
20
27
|
preClaimTimeoutMs?: number;
|
|
@@ -31,3 +38,23 @@ export type DaemonListener = {
|
|
|
31
38
|
close: () => Promise<void>;
|
|
32
39
|
};
|
|
33
40
|
export declare const startListener: (options: ListenerOptions) => Promise<DaemonListener>;
|
|
41
|
+
export type WebListenerOptions = {
|
|
42
|
+
sessionManager: SessionManager;
|
|
43
|
+
/** Origins allowed in addition to localhost, `127.0.0.1` and `[::1]` on any port. Compared
|
|
44
|
+
* as exact strings against the `Origin` header (`config.json`'s `webOrigins`). */
|
|
45
|
+
origins: readonly string[];
|
|
46
|
+
preClaimTimeoutMs?: number;
|
|
47
|
+
timers?: TimerFns;
|
|
48
|
+
};
|
|
49
|
+
export type WebListener = {
|
|
50
|
+
port: () => number | undefined;
|
|
51
|
+
close: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The plain-HTTP listener web pages connect to, bound to `127.0.0.1` on an OS-assigned port. A
|
|
55
|
+
* browser cannot pin the daemon's self-signed certificate, so it cannot use the TLS listener.
|
|
56
|
+
* Instead every upgrade is checked against its `Origin` header and refused with 403 unless that
|
|
57
|
+
* is a loopback origin or listed in `origins`. A request with no `Origin` is let through: only a
|
|
58
|
+
* local non-browser process sends none, and it could connect to the daemon anyway.
|
|
59
|
+
*/
|
|
60
|
+
export declare const startWebListener: (options: WebListenerOptions) => Promise<WebListener>;
|
|
@@ -17,3 +17,6 @@ export declare class RpcApplicationError extends Error {
|
|
|
17
17
|
/** The `session_suspended` message for an app that closed its socket because it went to the
|
|
18
18
|
* background: says why and how to recover. */
|
|
19
19
|
export declare const appBackgroundedMessage: (alias: string) => string;
|
|
20
|
+
/** The `session_suspended` message for a call that was pending when the app reconnected on a new
|
|
21
|
+
* socket: the app dropped the call with the old socket, so its result is lost. */
|
|
22
|
+
export declare const appReconnectedMessage: (alias: string) => string;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* §6: "terminal states free the alias" — there is no persisted history, matching §3/§13's "no
|
|
11
11
|
* per-session files, all state in-daemon-memory").
|
|
12
12
|
*/
|
|
13
|
-
import { type EventMessage, type SessionClaimMessage, type SessionResumeMessage, type SessionSummary, type SessionSuspendReason, type ToolCallMessage, type ToolCallProgressMessage, type ToolCancelMessage, type ToolErrorMessage, type ToolResultMessage } from "@appduct/shared";
|
|
13
|
+
import { type EventMessage, type SessionClaimMessage, type SessionResumeMessage, type LinkTransport, type SessionSummary, type SessionSuspendReason, type ToolCallMessage, type ToolCallProgressMessage, type ToolCancelMessage, type ToolErrorMessage, type ToolResultMessage } from "@appduct/shared";
|
|
14
14
|
import type { WebSocket } from "ws";
|
|
15
15
|
import type { Clock } from "../cli/types.js";
|
|
16
16
|
import type { EventBus } from "./event-bus.js";
|
|
@@ -26,7 +26,7 @@ export type SessionManagerOptions = {
|
|
|
26
26
|
graceSeconds: number;
|
|
27
27
|
keepaliveIntervalSeconds: number;
|
|
28
28
|
linkTtlSeconds: number;
|
|
29
|
-
getEndpoint: () => CreatedLink["endpoint"];
|
|
29
|
+
getEndpoint: (transport: LinkTransport) => CreatedLink["endpoint"];
|
|
30
30
|
eventBus: EventBus;
|
|
31
31
|
clock?: Clock;
|
|
32
32
|
timers?: TimerFns;
|
|
@@ -48,11 +48,15 @@ export type SessionManager = {
|
|
|
48
48
|
* 07's emulator/simulator fast path); see `links.ts`'s `PendingLinkRegistry.create`. Omits
|
|
49
49
|
* `LinkCreateResult.pin` — the session manager has no `TlsManager` handle, so the RPC handler
|
|
50
50
|
* (`daemon.ts`) attaches the current SPKI pin itself before returning to the caller. */
|
|
51
|
-
createLink: (ttlSeconds?: number, addressOverride?: string) => Omit<LinkCreateResult, "pin">;
|
|
52
|
-
/** First message on a fresh socket
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
createLink: (ttlSeconds?: number, addressOverride?: string, transport?: LinkTransport) => Omit<LinkCreateResult, "pin">;
|
|
52
|
+
/** First message on a fresh socket accepted on `transport`'s listener. Returns the claimed
|
|
53
|
+
* sessionId, or `null` after closing the socket. A link minted for the other transport is
|
|
54
|
+
* refused with `wrong_transport` and stays claimable on its own listener. */
|
|
55
|
+
handleClaim: (socket: WebSocket, message: SessionClaimMessage, transport: LinkTransport) => string | null;
|
|
56
|
+
/** First message on a resume socket accepted on `transport`'s listener. Returns the resumed
|
|
57
|
+
* sessionId, or `null` after closing the socket. A session claimed on the other listener is
|
|
58
|
+
* refused with `wrong_transport` before its token is checked, so its token is not rotated. */
|
|
59
|
+
handleResume: (socket: WebSocket, message: SessionResumeMessage, transport: LinkTransport) => string | null;
|
|
56
60
|
/** Dispatches an already-type-checked post-claim message; closes the socket on invalid content. */
|
|
57
61
|
handlePostClaimMessage: (sessionId: string, socket: WebSocket, message: Record<string, unknown>) => void;
|
|
58
62
|
list: () => SessionSummary[];
|
package/dist/daemon/tls.d.ts
CHANGED
|
@@ -23,9 +23,6 @@ export declare class HostKeyError extends Error {
|
|
|
23
23
|
* Deliberately the same shape as `ConfigWarnFn` (`config.ts`) so callers can pass the same
|
|
24
24
|
* function through without an adapter. */
|
|
25
25
|
export type TlsWarnFn = (message: string) => void;
|
|
26
|
-
/** Refuses a key file that is group/world-readable. Missing files are handled by the caller
|
|
27
|
-
* (`loadOrGenerateHostKeyPem`), which auto-generates instead of throwing. */
|
|
28
|
-
export declare const loadHostKeyPem: (keyPath: string) => Promise<string>;
|
|
29
26
|
/**
|
|
30
27
|
* Loads the host key at `keyPath`, generating one in its place if none exists yet (dev-mode
|
|
31
28
|
* zero-config bootstrap). Only the ENOENT case auto-generates: any other `stat`/`readFile` failure
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type RelayLink } from "./relay.js";
|
|
2
|
+
import type { DevtoolsBrowser, DevtoolsPage, OpenDaemonSocket } from "./ports.js";
|
|
3
|
+
export type AttachBrowserTabOptions = {
|
|
4
|
+
browser: DevtoolsBrowser;
|
|
5
|
+
/**
|
|
6
|
+
* The page relayed for each target id. Attaching a tab closes the page already relayed for it, so a
|
|
7
|
+
* tab has one relay however its browser was reached; the new page is recorded here as soon as it is open.
|
|
8
|
+
*/
|
|
9
|
+
relayedTabs: Map<string, DevtoolsPage>;
|
|
10
|
+
/** The tab's address, or the start of it. */
|
|
11
|
+
url: string;
|
|
12
|
+
/** Picks the tab directly, for when several tabs match `url`. */
|
|
13
|
+
targetId?: string;
|
|
14
|
+
/** Mints the web link for the page. Called once a tab is picked, so a failed pick strands no link. */
|
|
15
|
+
mintLink: () => Promise<RelayLink & {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
}>;
|
|
19
|
+
openDaemonSocket: OpenDaemonSocket;
|
|
20
|
+
};
|
|
21
|
+
/** `url` is the tab's own address, without the link's payload. */
|
|
22
|
+
export type AttachedTab = {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
url: string;
|
|
25
|
+
targetId: string;
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Picks a tab of a debugging-port browser, relays its page to the daemon and connects it over the
|
|
30
|
+
* binding, so the session outlives this call. The page must already load `@appduct/web`. Reloading
|
|
31
|
+
* resumes the session; a popup or a navigation that creates a new target is not relayed.
|
|
32
|
+
*/
|
|
33
|
+
export declare const attachBrowserTab: (options: AttachBrowserTabOptions) => Promise<AttachedTab>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createMemoryDaemonSockets, type MemoryDaemonSocket, type MemoryDaemonSockets } from "./memory-daemon-sockets.js";
|
|
2
|
+
export { attachBrowserTab, type AttachBrowserTabOptions, type AttachedTab } from "./attach-tab.js";
|
|
3
|
+
export { createMemoryDevtoolsBrowser, type MemoryDevtoolsBrowser } from "./memory-devtools-browser.js";
|
|
4
|
+
export { createMemoryPageChannel, type MemoryPageChannel } from "./memory-page-channel.js";
|
|
5
|
+
export { connectNodeDevtoolsBrowser } from "./node-devtools-browser.js";
|
|
6
|
+
export { openNodeDaemonSocket } from "./node-daemon-socket.js";
|
|
7
|
+
export type { ConnectDevtoolsBrowser, DaemonSocket, DaemonSocketEvents, DevtoolsBrowser, DevtoolsPage, DevtoolsTarget, OpenDaemonSocket, PageChannel } from "./ports.js";
|
|
8
|
+
export { linkPayload, parseLink, relayPage, type RelayLink } from "./relay.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OpenDaemonSocket } from "./ports.js";
|
|
2
|
+
/** One daemon socket the relay opened, driven by the test. */
|
|
3
|
+
export type MemoryDaemonSocket = {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
/** Text frames the relay sent, in order. */
|
|
6
|
+
readonly sent: string[];
|
|
7
|
+
/** How the relay closed the socket, once it did. */
|
|
8
|
+
readonly closedByRelay: {
|
|
9
|
+
code: number;
|
|
10
|
+
reason: string;
|
|
11
|
+
} | undefined;
|
|
12
|
+
open(): void;
|
|
13
|
+
receive(text: string): void;
|
|
14
|
+
closeFromDaemon(code: number, reason: string): void;
|
|
15
|
+
};
|
|
16
|
+
export type MemoryDaemonSockets = {
|
|
17
|
+
open: OpenDaemonSocket;
|
|
18
|
+
readonly sockets: MemoryDaemonSocket[];
|
|
19
|
+
};
|
|
20
|
+
export declare const createMemoryDaemonSockets: () => MemoryDaemonSockets;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DevtoolsBrowser, DevtoolsTarget } from "./ports.js";
|
|
2
|
+
import { type MemoryPageChannel } from "./memory-page-channel.js";
|
|
3
|
+
export type MemoryDevtoolsBrowser = DevtoolsBrowser & {
|
|
4
|
+
/** The tab's page, as the relay sees it: the one the next `openPage` returns, then the latest it returned. */
|
|
5
|
+
page(targetId: string): MemoryPageChannel;
|
|
6
|
+
/** Target ids passed to `openPage`, in order, that have not been closed since. */
|
|
7
|
+
attachedTargets(): string[];
|
|
8
|
+
};
|
|
9
|
+
/** A browser with the given tabs, each backed by a memory page channel. */
|
|
10
|
+
export declare const createMemoryDevtoolsBrowser: (tabs: DevtoolsTarget[]) => MemoryDevtoolsBrowser;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PageChannel } from "./ports.js";
|
|
2
|
+
export type MemoryPageChannel = PageChannel & {
|
|
3
|
+
/** The page calls its binding with an object, sent as JSON. */
|
|
4
|
+
call(message: Record<string, unknown>): void;
|
|
5
|
+
/** The document is destroyed. */
|
|
6
|
+
resetContext(): void;
|
|
7
|
+
/** The connection is dropped: the context resets once, then the page stops calling the relay. */
|
|
8
|
+
close(): void;
|
|
9
|
+
/** What the relay delivered to `__APPDUCT__.receive`, parsed. */
|
|
10
|
+
received(): Record<string, unknown>[];
|
|
11
|
+
/** Every other expression evaluated in the page, in order. */
|
|
12
|
+
evaluated(): string[];
|
|
13
|
+
/** Makes `evaluate` reject for expressions matching `pattern`. */
|
|
14
|
+
failEvaluate(pattern: RegExp, error: Error): void;
|
|
15
|
+
};
|
|
16
|
+
export declare const createMemoryPageChannel: () => MemoryPageChannel;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** What the relay needs from outside the process. Each port has a memory fake beside it. */
|
|
2
|
+
/** The page the session runs in, as the relay sees it. A Playwright `Page` is adapted to this by `attachPage`. */
|
|
3
|
+
export type PageChannel = {
|
|
4
|
+
/** Calls the page makes to its exposed binding, as the JSON string it passed. */
|
|
5
|
+
onBindingCall(handler: (json: string) => void): void;
|
|
6
|
+
/** Runs an expression in the page's current document. Rejects when there is none. */
|
|
7
|
+
evaluate(expression: string): Promise<unknown>;
|
|
8
|
+
/** A document was destroyed or replaced: a reload, a navigation to another document, or the page closing. */
|
|
9
|
+
onContextReset(handler: () => void): void;
|
|
10
|
+
};
|
|
11
|
+
export type DaemonSocketEvents = {
|
|
12
|
+
open(): void;
|
|
13
|
+
message(text: string): void;
|
|
14
|
+
close(code: number, reason: string): void;
|
|
15
|
+
};
|
|
16
|
+
export type DaemonSocket = {
|
|
17
|
+
send(text: string): void;
|
|
18
|
+
close(code: number, reason: string): void;
|
|
19
|
+
};
|
|
20
|
+
/** Opens a WebSocket to the daemon's web listener, with no `Origin`. */
|
|
21
|
+
export type OpenDaemonSocket = (url: string, events: DaemonSocketEvents) => DaemonSocket;
|
|
22
|
+
/** A tab the browser lists. */
|
|
23
|
+
export type DevtoolsTarget = {
|
|
24
|
+
/** The CDP target id, stable for the life of the tab. */
|
|
25
|
+
id: string;
|
|
26
|
+
url: string;
|
|
27
|
+
title: string;
|
|
28
|
+
};
|
|
29
|
+
/** A tab opened for relaying: the page channel plus the way to let go of it. */
|
|
30
|
+
export type DevtoolsPage = PageChannel & {
|
|
31
|
+
/** Drops the tab's binding connection. The page keeps running; it just has no relay. */
|
|
32
|
+
close(): void;
|
|
33
|
+
};
|
|
34
|
+
/** A Chromium launched with `--remote-debugging-port`, as the daemon sees it. */
|
|
35
|
+
export type DevtoolsBrowser = {
|
|
36
|
+
/** The open tabs, in the order the browser lists them. Rejects when the browser cannot be reached. */
|
|
37
|
+
targets(): Promise<DevtoolsTarget[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Opens the tab for relaying: `window.__appductBinding(json)` exists in its current document and in
|
|
40
|
+
* every later one, and `onContextReset` fires when its document is destroyed or the tab closes.
|
|
41
|
+
* Opening a tab that is already open drops the earlier connection.
|
|
42
|
+
*/
|
|
43
|
+
openPage(targetId: string): Promise<DevtoolsPage>;
|
|
44
|
+
/** Drops every connection `openPage` made. */
|
|
45
|
+
close(): void;
|
|
46
|
+
};
|
|
47
|
+
/** Reaches the browser whose debugging endpoint is `browserUrl`, such as `http://127.0.0.1:9222`. */
|
|
48
|
+
export type ConnectDevtoolsBrowser = (browserUrl: string) => DevtoolsBrowser;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BootstrapPayload } from "@appduct/shared";
|
|
2
|
+
import type { OpenDaemonSocket, PageChannel } from "./ports.js";
|
|
3
|
+
/** A link from `link({ target: "web", url })`: the page URL with `#appduct=<payload>`. */
|
|
4
|
+
export type RelayLink = {
|
|
5
|
+
url: string;
|
|
6
|
+
};
|
|
7
|
+
/** The bootstrap payload in the link's fragment. */
|
|
8
|
+
export declare const linkPayload: (link: RelayLink) => string;
|
|
9
|
+
/** The bootstrap a link carries; throws when the link has no payload or the payload is not an Appduct link. */
|
|
10
|
+
export declare const parseLink: (link: RelayLink) => BootstrapPayload;
|
|
11
|
+
/**
|
|
12
|
+
* Carries one page's session frames between the page's binding and the daemon's web listener. The
|
|
13
|
+
* page asks for a daemon socket when its transport opens, and the relay closes it when the page's
|
|
14
|
+
* document is destroyed, so the daemon suspends the session and the reloaded page resumes it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const relayPage: (page: PageChannel, link: RelayLink, openDaemonSocket: OpenDaemonSocket) => void;
|
package/dist/index.js
CHANGED
|
@@ -126,8 +126,11 @@ var createCli = () => {
|
|
|
126
126
|
`link: deep-link URI scheme (also: APPDUCT_SCHEME; default: .appduct/config.json, then app.json's "expo.scheme", then the Android/iOS project files in <cwd>).`
|
|
127
127
|
).option(
|
|
128
128
|
"--open <target>",
|
|
129
|
-
"link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
|
|
130
|
-
).option(
|
|
129
|
+
"link: deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental), or with `web <url>` return the page URL and a connect script for a web page."
|
|
130
|
+
).option(
|
|
131
|
+
"--browser-url <url>",
|
|
132
|
+
"link: with --open web, attach the tab of a Chrome launched with --remote-debugging-port (such as http://127.0.0.1:9222) and claim a session on it."
|
|
133
|
+
).option("--target-id <id>", "link: with --browser-url, the CDP target id of the tab to attach when several tabs match <url>.").option("--device <id>", "link: adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
|
|
131
134
|
"--app-id <id>",
|
|
132
135
|
`link: installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
|
|
133
136
|
).option(
|
|
@@ -169,6 +172,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
169
172
|
"daemonLogMaxBytes",
|
|
170
173
|
"eventsLogMaxBytes",
|
|
171
174
|
"policy",
|
|
175
|
+
"webOrigins",
|
|
172
176
|
"advertisedIp",
|
|
173
177
|
"scheme",
|
|
174
178
|
"restartDaemonOnVersionMismatch"
|
|
@@ -229,6 +233,7 @@ var defaultConfig = (paths) => {
|
|
|
229
233
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
230
234
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
231
235
|
restartDaemonOnVersionMismatch: false,
|
|
236
|
+
webOrigins: [],
|
|
232
237
|
policy: {
|
|
233
238
|
default: "allow",
|
|
234
239
|
destructive: "allow"
|
|
@@ -299,6 +304,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
299
304
|
"restartDaemonOnVersionMismatch"
|
|
300
305
|
);
|
|
301
306
|
}
|
|
307
|
+
if (parsed.webOrigins !== void 0) {
|
|
308
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
309
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
310
|
+
}
|
|
311
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
312
|
+
}
|
|
302
313
|
if (parsed.policy !== void 0) {
|
|
303
314
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
304
315
|
throw configError("policy", "must be an object.");
|
|
@@ -407,7 +418,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
407
418
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
408
419
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
409
420
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
410
|
-
// assertion failure
|
|
421
|
+
// assertion failure.
|
|
411
422
|
inspection_error: 66,
|
|
412
423
|
connection_error: 70,
|
|
413
424
|
session_error: 71,
|
|
@@ -882,6 +893,36 @@ var renderInvokeData = (colors, data, flags) => {
|
|
|
882
893
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
883
894
|
};
|
|
884
895
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
896
|
+
if ("attached" in data) {
|
|
897
|
+
return [
|
|
898
|
+
colors.green("Page Attached"),
|
|
899
|
+
...renderFields(
|
|
900
|
+
"Page",
|
|
901
|
+
[
|
|
902
|
+
["Session", data.sessionId],
|
|
903
|
+
["Tab", data.targetId],
|
|
904
|
+
["URL", data.url],
|
|
905
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
906
|
+
],
|
|
907
|
+
flags
|
|
908
|
+
)
|
|
909
|
+
];
|
|
910
|
+
}
|
|
911
|
+
if ("script" in data) {
|
|
912
|
+
return [
|
|
913
|
+
colors.green("Link Created"),
|
|
914
|
+
...renderFields(
|
|
915
|
+
"Link",
|
|
916
|
+
[
|
|
917
|
+
["Session", data.sessionId],
|
|
918
|
+
["URL", data.url],
|
|
919
|
+
["Script", data.script],
|
|
920
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
921
|
+
],
|
|
922
|
+
flags
|
|
923
|
+
)
|
|
924
|
+
];
|
|
925
|
+
}
|
|
885
926
|
const lines = [
|
|
886
927
|
colors.green("Link Created"),
|
|
887
928
|
...renderFields(
|
|
@@ -1002,6 +1043,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
1002
1043
|
["PID", data.daemon.pid],
|
|
1003
1044
|
["Started at", data.daemon.started_at],
|
|
1004
1045
|
["WSS port", data.daemon.wss_port],
|
|
1046
|
+
["Web port", data.daemon.web_port],
|
|
1005
1047
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
1006
1048
|
["Sessions", data.daemon.session_count]
|
|
1007
1049
|
],
|
package/dist/link.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* (`sha256/<44-char-base64>`, may contain `+`/`/`/`=`), so it is percent-encoded here; native/JS
|
|
18
18
|
* parsers use `URLSearchParams`, which decodes it back.
|
|
19
19
|
*/
|
|
20
|
-
import { type AgentEndpoint, type LinkCreateResult } from "@appduct/shared";
|
|
20
|
+
import { type AgentEndpoint, type LinkCreateResult, type WebAttachParams, type WebAttachResult } from "@appduct/shared";
|
|
21
21
|
import { type ExecFn, type OpenTarget } from "./cli/open-target.js";
|
|
22
22
|
import { type SpawnFn } from "./rpc/client.js";
|
|
23
23
|
/**
|
|
@@ -30,6 +30,40 @@ import { type SpawnFn } from "./rpc/client.js";
|
|
|
30
30
|
* *after* the bootstrap blob, so anything reading `appduct=` must stop at the `&`.
|
|
31
31
|
*/
|
|
32
32
|
export declare const composeDeepLink: (scheme: string, result: LinkCreateResult) => string;
|
|
33
|
+
/** What a web target hands back instead of a deep link: the page URL to open, and the script to
|
|
34
|
+
* run in an already-open page. Both carry the same bootstrap payload. */
|
|
35
|
+
export type WebLinkResult = {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
/** The page URL with `#appduct=<payload>` appended (replacing any fragment it had). */
|
|
38
|
+
url: string;
|
|
39
|
+
script: string;
|
|
40
|
+
/** Unix seconds. */
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
};
|
|
43
|
+
/** Throws a usage error unless `pageUrl` is an http(s) URL. Called before a link is minted, so a
|
|
44
|
+
* bad URL does not strand a pending link. */
|
|
45
|
+
export declare const assertWebPageUrl: (pageUrl: string) => void;
|
|
46
|
+
/** Composes the web target's result from a `link.create` result minted with `transport: "web"`. */
|
|
47
|
+
export declare const composeWebLink: (pageUrl: string, result: LinkCreateResult) => WebLinkResult;
|
|
48
|
+
export type MintWebLinkOptions = {
|
|
49
|
+
stateDir: string;
|
|
50
|
+
spawn?: SpawnFn;
|
|
51
|
+
autoSpawn?: boolean;
|
|
52
|
+
ttlSeconds?: number;
|
|
53
|
+
/** The page the link will be opened in. */
|
|
54
|
+
url: string;
|
|
55
|
+
};
|
|
56
|
+
/** Mints a link only the web listener accepts. Needs no scheme: a web page is reached by URL. */
|
|
57
|
+
export declare const mintWebLink: (options: MintWebLinkOptions) => Promise<WebLinkResult>;
|
|
58
|
+
export type AttachWebTabOptions = WebAttachParams & {
|
|
59
|
+
stateDir: string;
|
|
60
|
+
spawn?: SpawnFn;
|
|
61
|
+
autoSpawn?: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** Asks the daemon to attach a tab of the debugging-port browser at `browserUrl` and claim a session on it. */
|
|
64
|
+
export declare const attachWebTab: ({ stateDir, spawn, autoSpawn, ...params }: AttachWebTabOptions) => Promise<WebAttachResult & {
|
|
65
|
+
attached: true;
|
|
66
|
+
}>;
|
|
33
67
|
export type MintLinkOptions = {
|
|
34
68
|
stateDir: string;
|
|
35
69
|
spawn?: SpawnFn;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* caller asked for it with `target: "none"`), where it carries explicit instructions telling the
|
|
12
12
|
* agent to show the code and ask.
|
|
13
13
|
*/
|
|
14
|
+
import { type WebAttachResult } from "@appduct/shared";
|
|
14
15
|
import { type ExecFn, type OpenTarget } from "../cli/open-target.js";
|
|
16
|
+
import { type WebLinkResult } from "../link.js";
|
|
15
17
|
import { type SpawnFn } from "../rpc/client.js";
|
|
16
18
|
import type { DaemonCall } from "./daemon-tools.js";
|
|
17
19
|
export declare const CONNECT_TOOL_NAME = "appduct_connect";
|
|
@@ -24,7 +26,16 @@ export declare const CONNECT_TOOL_DESCRIPTOR: {
|
|
|
24
26
|
readonly properties: {
|
|
25
27
|
readonly target: {
|
|
26
28
|
readonly type: "string";
|
|
27
|
-
readonly enum: readonly ["android", "ios-sim", "ios-device", "none"];
|
|
29
|
+
readonly enum: readonly ["android", "ios-sim", "ios-device", "none", "web"];
|
|
30
|
+
};
|
|
31
|
+
readonly url: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
};
|
|
34
|
+
readonly browserUrl: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
};
|
|
37
|
+
readonly targetId: {
|
|
38
|
+
readonly type: "string";
|
|
28
39
|
};
|
|
29
40
|
readonly device: {
|
|
30
41
|
readonly type: "string";
|
|
@@ -108,7 +119,9 @@ export type ConnectToolResult = {
|
|
|
108
119
|
/** Present only on the QR path: what the agent must do before waiting. See {@link QR_INSTRUCTIONS}. */
|
|
109
120
|
instructions?: string;
|
|
110
121
|
};
|
|
111
|
-
export declare const handleConnectTool: (rawArgs: unknown, deps: ConnectToolDeps) => Promise<ConnectToolResult
|
|
122
|
+
export declare const handleConnectTool: (rawArgs: unknown, deps: ConnectToolDeps) => Promise<ConnectToolResult | WebLinkResult | (WebAttachResult & {
|
|
123
|
+
attached: true;
|
|
124
|
+
})>;
|
|
112
125
|
export type WaitForSessionToolDeps = {
|
|
113
126
|
stateDir: string;
|
|
114
127
|
spawn?: SpawnFn;
|
package/dist/native-scheme.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps
|
|
3
|
-
* (docs/tasks/20-cli-native-scheme-discovery.md, phase 3 of issue #48).
|
|
2
|
+
* Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps.
|
|
4
3
|
*
|
|
5
4
|
* `scheme.ts`'s `resolveScheme` treats this module as one more thing to try after `<cwd>/app.json`
|
|
6
5
|
* — it never walks up (an app root is where these commands run, same rule as `app.json`), and,
|
|
@@ -8,6 +7,8 @@
|
|
|
8
7
|
* `xcodebuild`, no Gradle evaluation. Every probe is a plain read of a well-known project file,
|
|
9
8
|
* parsed defensively:
|
|
10
9
|
*
|
|
10
|
+
* - Flutter (a root with a `pubspec.yaml`): the Android probes below read `android/app/…` instead
|
|
11
|
+
* of `app/…`; the iOS walk already reaches `ios/Runner` and `macos/Runner`.
|
|
11
12
|
* - Android: `app/build.gradle.kts` / `app/build.gradle` for a `manifestPlaceholders["appductScheme"]`
|
|
12
13
|
* (or `.appductScheme =`) assignment, then `app/src/main/AndroidManifest.xml` for the first
|
|
13
14
|
* `<data android:scheme>` inside an intent filter that also declares
|
|
@@ -35,19 +36,22 @@ export type NativeSchemeProbeResult = {
|
|
|
35
36
|
origin: string;
|
|
36
37
|
};
|
|
37
38
|
export type NativeSchemeDiscovery = {
|
|
38
|
-
/**
|
|
39
|
-
|
|
39
|
+
/** Every probe that resolved a scheme, in probe order (all agree on the scheme, or discovery
|
|
40
|
+
* throws). Empty when none did. */
|
|
41
|
+
results: NativeSchemeProbeResult[];
|
|
40
42
|
/** Every probe's human-readable description, in order, for {@link describeMissingScheme}-style
|
|
41
43
|
* reporting — always as many entries as there are probes, whether or not any of them hit. */
|
|
42
44
|
tried: string[];
|
|
43
45
|
};
|
|
46
|
+
/** Whether `root` is a Flutter project root: it has a `pubspec.yaml`. */
|
|
47
|
+
export declare const isFlutterProject: (root: string) => Promise<boolean>;
|
|
44
48
|
/**
|
|
45
49
|
* Runs every static-file probe against `root` (never walking up — same rule as `app.json`) and
|
|
46
50
|
* decides the result:
|
|
47
51
|
*
|
|
48
|
-
* - No probe resolves a scheme: `
|
|
49
|
-
* - Every probe that resolves one agrees:
|
|
50
|
-
*
|
|
52
|
+
* - No probe resolves a scheme: `results` is empty.
|
|
53
|
+
* - Every probe that resolves one agrees: `results` holds each of them in the order of
|
|
54
|
+
* {@link PROBES}, so a caller can see every platform the project spans, not one winner.
|
|
51
55
|
* - Two probes resolve *different* schemes: throws a usage error naming both sources and their
|
|
52
56
|
* values, since guessing which one is right would silently target the wrong app on one
|
|
53
57
|
* platform.
|
package/dist/scheme.d.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* redirects the state directory — `--state-dir` / `APPDUCT_STATE_DIR` remain the only way to do
|
|
40
40
|
* that — so a project file can never move the daemon's key, socket or audit log.
|
|
41
41
|
*/
|
|
42
|
-
import { type NativeSchemeSource } from "./native-scheme.js";
|
|
42
|
+
import { type NativeSchemeProbeResult, type NativeSchemeSource } from "./native-scheme.js";
|
|
43
43
|
/** The directory a project-level config lives in, relative to an app root. */
|
|
44
44
|
export declare const PROJECT_CONFIG_DIR = ".appduct";
|
|
45
45
|
/** The project-level config filename, inside {@link PROJECT_CONFIG_DIR}. */
|
|
@@ -85,6 +85,10 @@ export type StaticProjectSchemeDiscovery = {
|
|
|
85
85
|
* it) — distinct from `tried` below, which lists every location whether or not it hit. Only set
|
|
86
86
|
* alongside `scheme`. */
|
|
87
87
|
origin?: string;
|
|
88
|
+
/** Every native-project probe that resolved a scheme, in probe order — `scheme`, `source` and
|
|
89
|
+
* `origin` are the first of them. Empty when `app.json` answered (the native probes do not run)
|
|
90
|
+
* or nothing did; the platforms a project spans are read from here. */
|
|
91
|
+
nativeResults: NativeSchemeProbeResult[];
|
|
88
92
|
/** Every location this step consulted, in order: `app.json` first (unchanged from pre-#48), then
|
|
89
93
|
* every `native-scheme.ts` probe — always present, hit or miss, one entry each. */
|
|
90
94
|
tried: string[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appduct",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.15.1",
|
|
4
|
+
"description": "Call functions inside your running iOS, Android or React Native app from a terminal, test or AI agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"appduct",
|
|
27
|
+
"ios",
|
|
28
|
+
"android",
|
|
27
29
|
"react-native",
|
|
28
30
|
"agent"
|
|
29
31
|
],
|
|
@@ -45,14 +47,22 @@
|
|
|
45
47
|
"reflect-metadata": "^0.2.2",
|
|
46
48
|
"toqr": "^0.1.1",
|
|
47
49
|
"ws": "^8.18.3",
|
|
48
|
-
"@appduct/shared": "0.
|
|
50
|
+
"@appduct/shared": "0.15.1"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@types/node": "^22.10.0",
|
|
54
|
+
"@types/react": "~19.1.17",
|
|
52
55
|
"@types/ws": "^8.18.1",
|
|
53
56
|
"esbuild": "^0.25.12",
|
|
54
57
|
"eslint": "^10",
|
|
55
|
-
"
|
|
58
|
+
"playwright-core": "1.56.1",
|
|
59
|
+
"react": "19.1.0",
|
|
60
|
+
"react-dom": "19.1.0",
|
|
61
|
+
"typescript": "~5.9.2",
|
|
62
|
+
"vite": "^8.1.4",
|
|
63
|
+
"webpack": "^5.111.1",
|
|
64
|
+
"@appduct/web": "0.15.1",
|
|
65
|
+
"@appduct/react-native": "0.15.1"
|
|
56
66
|
},
|
|
57
67
|
"files": [
|
|
58
68
|
"bin.js",
|
|
@@ -62,7 +72,7 @@
|
|
|
62
72
|
"build": "tsc -p tsconfig.build.json && node scripts/bundle.mjs",
|
|
63
73
|
"clean": "rm -rf dist",
|
|
64
74
|
"lint": "eslint src",
|
|
65
|
-
"test": "vitest run
|
|
75
|
+
"test": "vitest run src/__tests__",
|
|
66
76
|
"typecheck": "tsc -p tsconfig.json"
|
|
67
77
|
}
|
|
68
78
|
}
|