appduct 0.9.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/LICENSE +21 -0
- package/README.md +274 -0
- package/bin.js +2 -0
- package/dist/artifact-inspect.d.ts +110 -0
- package/dist/artifact-inspect.d.ts.map +1 -0
- package/dist/artifact-inspect.js +315 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/cli/command-options.d.ts +44 -0
- package/dist/cli/command-options.d.ts.map +1 -0
- package/dist/cli/command-options.js +95 -0
- package/dist/cli/create-cli.d.ts +2 -0
- package/dist/cli/create-cli.d.ts.map +1 -0
- package/dist/cli/create-cli.js +56 -0
- package/dist/cli/dispatch.d.ts +3 -0
- package/dist/cli/dispatch.d.ts.map +1 -0
- package/dist/cli/dispatch.js +275 -0
- package/dist/cli/open-target.d.ts +112 -0
- package/dist/cli/open-target.d.ts.map +1 -0
- package/dist/cli/open-target.js +438 -0
- package/dist/cli/result-types.d.ts +194 -0
- package/dist/cli/result-types.d.ts.map +1 -0
- package/dist/cli/result-types.js +6 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +111 -0
- package/dist/cli/types.d.ts +20 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +10 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/client/app-client.d.ts +126 -0
- package/dist/client/app-client.d.ts.map +1 -0
- package/dist/client/app-client.js +223 -0
- package/dist/client/bootstrap.d.ts +48 -0
- package/dist/client/bootstrap.d.ts.map +1 -0
- package/dist/client/bootstrap.js +108 -0
- package/dist/client/connect.d.ts +17 -0
- package/dist/client/connect.d.ts.map +1 -0
- package/dist/client/connect.js +38 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +40 -0
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +16 -0
- package/dist/commands/daemon.d.ts +22 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +128 -0
- package/dist/commands/doctor.d.ts +23 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/events.d.ts +37 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +76 -0
- package/dist/commands/init.d.ts +51 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +231 -0
- package/dist/commands/invoke.d.ts +22 -0
- package/dist/commands/invoke.d.ts.map +1 -0
- package/dist/commands/invoke.js +54 -0
- package/dist/commands/keygen.d.ts +23 -0
- package/dist/commands/keygen.d.ts.map +1 -0
- package/dist/commands/keygen.js +46 -0
- package/dist/commands/link.d.ts +30 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +43 -0
- package/dist/commands/ls.d.ts +9 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +7 -0
- package/dist/commands/mcp.d.ts +49 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +98 -0
- package/dist/commands/revoke.d.ts +12 -0
- package/dist/commands/revoke.d.ts.map +1 -0
- package/dist/commands/revoke.js +7 -0
- package/dist/commands/tools.d.ts +23 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +52 -0
- package/dist/daemon/address.d.ts +30 -0
- package/dist/daemon/address.d.ts.map +1 -0
- package/dist/daemon/address.js +71 -0
- package/dist/daemon/audit.d.ts +107 -0
- package/dist/daemon/audit.d.ts.map +1 -0
- package/dist/daemon/audit.js +237 -0
- package/dist/daemon/calls.d.ts +84 -0
- package/dist/daemon/calls.d.ts.map +1 -0
- package/dist/daemon/calls.js +158 -0
- package/dist/daemon/config.d.ts +79 -0
- package/dist/daemon/config.d.ts.map +1 -0
- package/dist/daemon/config.js +170 -0
- package/dist/daemon/daemon.d.ts +43 -0
- package/dist/daemon/daemon.d.ts.map +1 -0
- package/dist/daemon/daemon.js +560 -0
- package/dist/daemon/event-bus.d.ts +49 -0
- package/dist/daemon/event-bus.d.ts.map +1 -0
- package/dist/daemon/event-bus.js +111 -0
- package/dist/daemon/links.d.ts +54 -0
- package/dist/daemon/links.d.ts.map +1 -0
- package/dist/daemon/links.js +118 -0
- package/dist/daemon/listener.d.ts +34 -0
- package/dist/daemon/listener.d.ts.map +1 -0
- package/dist/daemon/listener.js +132 -0
- package/dist/daemon/log-rotation.d.ts +82 -0
- package/dist/daemon/log-rotation.d.ts.map +1 -0
- package/dist/daemon/log-rotation.js +136 -0
- package/dist/daemon/pidfile.d.ts +38 -0
- package/dist/daemon/pidfile.d.ts.map +1 -0
- package/dist/daemon/pidfile.js +90 -0
- package/dist/daemon/policy.d.ts +22 -0
- package/dist/daemon/policy.d.ts.map +1 -0
- package/dist/daemon/policy.js +24 -0
- package/dist/daemon/registry.d.ts +19 -0
- package/dist/daemon/registry.d.ts.map +1 -0
- package/dist/daemon/registry.js +31 -0
- package/dist/daemon/rpc-server.d.ts +54 -0
- package/dist/daemon/rpc-server.d.ts.map +1 -0
- package/dist/daemon/rpc-server.js +203 -0
- package/dist/daemon/sessions.d.ts +86 -0
- package/dist/daemon/sessions.d.ts.map +1 -0
- package/dist/daemon/sessions.js +429 -0
- package/dist/daemon/socket-probe.d.ts +16 -0
- package/dist/daemon/socket-probe.d.ts.map +1 -0
- package/dist/daemon/socket-probe.js +28 -0
- package/dist/daemon/state-dir.d.ts +31 -0
- package/dist/daemon/state-dir.d.ts.map +1 -0
- package/dist/daemon/state-dir.js +43 -0
- package/dist/daemon/timers.d.ts +16 -0
- package/dist/daemon/timers.d.ts.map +1 -0
- package/dist/daemon/timers.js +12 -0
- package/dist/daemon/tls.d.ts +58 -0
- package/dist/daemon/tls.d.ts.map +1 -0
- package/dist/daemon/tls.js +79 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +162 -0
- package/dist/host-certificate.d.ts +8 -0
- package/dist/host-certificate.d.ts.map +1 -0
- package/dist/host-certificate.js +111 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/key-material.d.ts +14 -0
- package/dist/key-material.d.ts.map +1 -0
- package/dist/key-material.js +42 -0
- package/dist/link.d.ts +76 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +117 -0
- package/dist/mcp/connect-tool.d.ts +115 -0
- package/dist/mcp/connect-tool.d.ts.map +1 -0
- package/dist/mcp/connect-tool.js +396 -0
- package/dist/mcp/daemon-tools.d.ts +13 -0
- package/dist/mcp/daemon-tools.d.ts.map +1 -0
- package/dist/mcp/daemon-tools.js +29 -0
- package/dist/mcp/events-tool.d.ts +111 -0
- package/dist/mcp/events-tool.d.ts.map +1 -0
- package/dist/mcp/events-tool.js +322 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +538 -0
- package/dist/mcp/tool-mapping.d.ts +58 -0
- package/dist/mcp/tool-mapping.d.ts.map +1 -0
- package/dist/mcp/tool-mapping.js +143 -0
- package/dist/mcp/tool-namespace.d.ts +40 -0
- package/dist/mcp/tool-namespace.d.ts.map +1 -0
- package/dist/mcp/tool-namespace.js +65 -0
- package/dist/native-scheme.d.ts +59 -0
- package/dist/native-scheme.d.ts.map +1 -0
- package/dist/native-scheme.js +465 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +342 -0
- package/dist/package-version.d.ts +30 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +47 -0
- package/dist/qr-terminal.d.ts +13 -0
- package/dist/qr-terminal.d.ts.map +1 -0
- package/dist/qr-terminal.js +45 -0
- package/dist/rpc/client.d.ts +137 -0
- package/dist/rpc/client.d.ts.map +1 -0
- package/dist/rpc/client.js +819 -0
- package/dist/scheme.d.ts +181 -0
- package/dist/scheme.d.ts.map +1 -0
- package/dist/scheme.js +321 -0
- package/dist/spki-pin.d.ts +3 -0
- package/dist/spki-pin.d.ts.map +1 -0
- package/dist/spki-pin.js +14 -0
- package/package.json +65 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `link()`/`waitForSession()` (issue #8): the bootstrap half of the client, so a test's
|
|
3
|
+
* `globalSetup` can pair a simulator/emulator without shelling out to `appduct link --open`.
|
|
4
|
+
* `link()` delegates to the same `mintLink` core `commands/link.ts` uses (`../link.js`) so the
|
|
5
|
+
* deep-link shape can't drift between the CLI and this package; `waitForSession()` mirrors
|
|
6
|
+
* `mcp/connect-tool.ts`'s `handleWaitForSessionTool`.
|
|
7
|
+
*/
|
|
8
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
9
|
+
import {} from "../cli/open-target.js";
|
|
10
|
+
import { resolveStateDir } from "../daemon/state-dir.js";
|
|
11
|
+
import { mintLink } from "../link.js";
|
|
12
|
+
import { openDaemonStream, DaemonRpcError } from "../rpc/client.js";
|
|
13
|
+
import { makeAppClient } from "./app-client.js";
|
|
14
|
+
import { AppductError, toAppductError } from "./errors.js";
|
|
15
|
+
const DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** Mints a session link via `link.create` and composes the deep link. With `target` set, delivers
|
|
17
|
+
* it directly to a booted emulator/simulator instead of leaving delivery to the caller. */
|
|
18
|
+
export const link = async (options = {}) => {
|
|
19
|
+
try {
|
|
20
|
+
return await mintLink({
|
|
21
|
+
stateDir: resolveStateDir(options.stateDir),
|
|
22
|
+
spawn: options.spawn,
|
|
23
|
+
autoSpawn: options.autoSpawn,
|
|
24
|
+
ttlSeconds: options.ttlSeconds,
|
|
25
|
+
target: options.target,
|
|
26
|
+
device: options.device,
|
|
27
|
+
bundleId: options.bundleId,
|
|
28
|
+
relaunch: options.relaunch,
|
|
29
|
+
scheme: options.scheme,
|
|
30
|
+
cwd: options.cwd,
|
|
31
|
+
exec: options.exec,
|
|
32
|
+
env: options.env,
|
|
33
|
+
schemeEnv: options.schemeEnv,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw toAppductError(error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** Waits for `sessionId` (from {@link link}) to be claimed by a device, then returns a connected
|
|
41
|
+
* {@link AppClient} for it — resolves immediately if it's already claimed. */
|
|
42
|
+
export const waitForSession = async (sessionId, options = {}) => {
|
|
43
|
+
const stateDir = resolveStateDir(options.stateDir);
|
|
44
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_WAIT_FOR_SESSION_TIMEOUT_MS;
|
|
45
|
+
let stream;
|
|
46
|
+
try {
|
|
47
|
+
stream = await openDaemonStream({ stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw toAppductError(error);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// The session may already be claimed by the time this runs — resolve immediately rather than
|
|
54
|
+
// waiting for an event that already happened.
|
|
55
|
+
try {
|
|
56
|
+
await stream.call(RPC_METHODS.sessionsDescribe, { selector: sessionId });
|
|
57
|
+
return makeAppClient(stream, sessionId);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof DaemonRpcError) || error.data?.type !== "unknown_session") {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await new Promise((resolve, reject) => {
|
|
65
|
+
let settled = false;
|
|
66
|
+
const settle = (fn) => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
unsubscribeNotification();
|
|
73
|
+
unsubscribeClose();
|
|
74
|
+
fn();
|
|
75
|
+
};
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
settle(() => reject(new AppductError("timeout", `Timed out after ${timeoutMs}ms waiting for session "${sessionId}" to be claimed.`)));
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
const unsubscribeClose = stream.onClose(() => {
|
|
80
|
+
settle(() => reject(new AppductError("connection_error", `The connection to the Appduct daemon closed while waiting for session "${sessionId}" to be claimed.`)));
|
|
81
|
+
});
|
|
82
|
+
// Registered *before* `events.subscribe` is sent (not after awaiting it) — a
|
|
83
|
+
// `session_claimed` notification that arrives in the same TCP chunk as the subscribe
|
|
84
|
+
// response is dispatched synchronously within that chunk's processing, before an `await`
|
|
85
|
+
// continuation below would get a chance to run; registering the listener first means it's
|
|
86
|
+
// always in place in time to see it.
|
|
87
|
+
const unsubscribeNotification = stream.onNotification((payload) => {
|
|
88
|
+
if (settled) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const event = payload;
|
|
92
|
+
if (event.kind === "session_claimed" && event.sessionId === sessionId) {
|
|
93
|
+
settle(resolve);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
stream
|
|
97
|
+
.call(RPC_METHODS.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] })
|
|
98
|
+
.catch((error) => {
|
|
99
|
+
settle(() => reject(toAppductError(error)));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return makeAppClient(stream, sessionId);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
stream.close();
|
|
106
|
+
throw toAppductError(error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SpawnFn } from "../rpc/client.js";
|
|
2
|
+
import { type AppClient, type ToolMap } from "./app-client.js";
|
|
3
|
+
export type ConnectOptions = {
|
|
4
|
+
/** Session id or alias; omitted selects the sole active/suspended session (rejects with
|
|
5
|
+
* `"no_session"`/`"ambiguous_session"` otherwise — the same rule the CLI/MCP surfaces use). */
|
|
6
|
+
selector?: string;
|
|
7
|
+
/** Defaults to `APPDUCT_STATE_DIR`, else `~/.appduct` — the same resolution the CLI uses. */
|
|
8
|
+
stateDir?: string;
|
|
9
|
+
/** Injectable daemon-spawn seam (tests only); real callers never need this. */
|
|
10
|
+
spawn?: SpawnFn;
|
|
11
|
+
/** Auto-spawn a daemon on a missing connection. Defaults to `true`. */
|
|
12
|
+
autoSpawn?: boolean;
|
|
13
|
+
requestTimeoutMs?: number;
|
|
14
|
+
};
|
|
15
|
+
/** Opens a persistent connection to the daemon and resolves `options.selector` to one session. */
|
|
16
|
+
export declare const connect: <TTools = ToolMap>(options?: ConnectOptions) => Promise<AppClient<TTools>>;
|
|
17
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/client/connect.ts"],"names":[],"mappings":"AAUA,OAAO,EAAuC,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG9E,MAAM,MAAM,cAAc,GAAG;IAC3B;mGAC+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,OAAO,GAAU,MAAM,GAAG,OAAO,EAC5C,UAAS,cAAmB,KAC3B,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CA0B3B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `connect()` (issue #8): the entry point for a test runner that wants to drive an already-claimed
|
|
3
|
+
* session without shelling out to the CLI. Auto-spawns the daemon (same seam `rpc/client.ts` gives
|
|
4
|
+
* the CLI/MCP server) and resolves `selector` up front via `sessions.describe` so a bad/ambiguous
|
|
5
|
+
* selector fails fast with a typed {@link AppductError} rather than surfacing on the first
|
|
6
|
+
* `call()`.
|
|
7
|
+
*/
|
|
8
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
9
|
+
import { resolveStateDir } from "../daemon/state-dir.js";
|
|
10
|
+
import { openDaemonStream } from "../rpc/client.js";
|
|
11
|
+
import { makeAppClient } from "./app-client.js";
|
|
12
|
+
import { toAppductError } from "./errors.js";
|
|
13
|
+
/** Opens a persistent connection to the daemon and resolves `options.selector` to one session. */
|
|
14
|
+
export const connect = async (options = {}) => {
|
|
15
|
+
const stateDir = resolveStateDir(options.stateDir);
|
|
16
|
+
let stream;
|
|
17
|
+
try {
|
|
18
|
+
stream = await openDaemonStream({
|
|
19
|
+
stateDir,
|
|
20
|
+
spawn: options.spawn,
|
|
21
|
+
autoSpawn: options.autoSpawn,
|
|
22
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw toAppductError(error);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const detail = await stream.call(RPC_METHODS.sessionsDescribe, {
|
|
30
|
+
selector: options.selector,
|
|
31
|
+
});
|
|
32
|
+
return makeAppClient(stream, detail.sessionId);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
stream.close();
|
|
36
|
+
throw toAppductError(error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-facing error type (issue #8): the wire `error.data.type` (ARCHITECTURE.md §5) surfaced as
|
|
3
|
+
* a typed exception instead of a `DaemonRpcError`/stderr string a test would have to pattern-match.
|
|
4
|
+
* `type` preserves the wire value verbatim — the same non-rewrapping rule `errors.ts`'s
|
|
5
|
+
* `toCliError` already enforces for the CLI — so `expect(...).rejects.toMatchObject({ type:
|
|
6
|
+
* "policy_denied" })` works without this package's internal error classes leaking into test code.
|
|
7
|
+
*/
|
|
8
|
+
import type { ErrorType } from "@appduct/shared";
|
|
9
|
+
/** The wire `ErrorType` union, plus three client-only buckets for conditions the daemon never
|
|
10
|
+
* produced: `"connection_error"` (daemon unreachable/unspawnable, or the connection dropped mid-
|
|
11
|
+
* wait), `"timeout"` (a client-side wait — `waitForEvent`, `waitForSession` — gave up; distinct
|
|
12
|
+
* from the wire `"tool_timeout"` so a test can tell "the app's tool timed out" apart from "my wait
|
|
13
|
+
* gave up"), and `"client_error"` (anything else, e.g. a failed emulator/simulator delivery from
|
|
14
|
+
* {@link link}, or a malformed JSON-RPC error with no `data.type`). */
|
|
15
|
+
export type AppductErrorType = ErrorType | "connection_error" | "timeout" | "client_error";
|
|
16
|
+
export declare class AppductError extends Error {
|
|
17
|
+
readonly type: AppductErrorType;
|
|
18
|
+
readonly details?: unknown;
|
|
19
|
+
/** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
|
|
20
|
+
readonly code?: number;
|
|
21
|
+
constructor(type: AppductErrorType, message: string, details?: unknown, code?: number);
|
|
22
|
+
}
|
|
23
|
+
/** Converts any error thrown by the RPC layer into a {@link AppductError}, preserving the wire
|
|
24
|
+
* `data.type` verbatim when present. Idempotent on an already-converted error. */
|
|
25
|
+
export declare const toAppductError: (error: unknown) => AppductError;
|
|
26
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD;;;;;uEAKuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,GAAG,cAAc,CAAC;AAE3F,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEX,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM;CAOtF;AAED;kFACkF;AAClF,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,YAiC/C,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isAppductCliError } from "../errors.js";
|
|
2
|
+
import { DaemonRpcError, isDaemonUnreachableError } from "../rpc/client.js";
|
|
3
|
+
export class AppductError extends Error {
|
|
4
|
+
type;
|
|
5
|
+
details;
|
|
6
|
+
/** The JSON-RPC error code, when this wraps a `DaemonRpcError`. */
|
|
7
|
+
code;
|
|
8
|
+
constructor(type, message, details, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AppductError";
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.details = details;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Converts any error thrown by the RPC layer into a {@link AppductError}, preserving the wire
|
|
17
|
+
* `data.type` verbatim when present. Idempotent on an already-converted error. */
|
|
18
|
+
export const toAppductError = (error) => {
|
|
19
|
+
if (error instanceof AppductError) {
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
const withCause = (result) => {
|
|
23
|
+
result.cause = error;
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
if (error instanceof DaemonRpcError) {
|
|
27
|
+
const type = error.data?.type ?? "client_error";
|
|
28
|
+
return withCause(new AppductError(type, error.message, error.data?.details, error.code));
|
|
29
|
+
}
|
|
30
|
+
if (isDaemonUnreachableError(error)) {
|
|
31
|
+
return withCause(new AppductError("connection_error", error instanceof Error ? error.message : "The Appduct daemon is unreachable."));
|
|
32
|
+
}
|
|
33
|
+
if (isAppductCliError(error)) {
|
|
34
|
+
return withCause(new AppductError("client_error", error.message, error.details));
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
return withCause(new AppductError("client_error", error.message));
|
|
38
|
+
}
|
|
39
|
+
return new AppductError("client_error", "An unexpected error occurred.", error);
|
|
40
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct/client` (issue #8): a first-class programmatic client for test runners — a thin typed
|
|
3
|
+
* wrapper over the same daemon RPC the CLI and MCP server use, so a Jest/Vitest/Detox spec can
|
|
4
|
+
* `connect()`/`call()` instead of spawning `appduct invoke ... --json` and parsing stdout.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { connect } from "appduct/client";
|
|
8
|
+
*
|
|
9
|
+
* const app = await connect();
|
|
10
|
+
* const { total } = await app.call("sum", { a: 2, b: 3 });
|
|
11
|
+
* await app.close();
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export { connect, type ConnectOptions } from "./connect.js";
|
|
15
|
+
export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
|
|
16
|
+
export { AppductError, type AppductErrorType } from "./errors.js";
|
|
17
|
+
export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
|
|
18
|
+
export type { AgentEndpoint, ErrorType, ToolDescriptor } from "@appduct/shared";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACrH,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,SAAS,EACT,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,OAAO,EACP,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct/client` (issue #8): a first-class programmatic client for test runners — a thin typed
|
|
3
|
+
* wrapper over the same daemon RPC the CLI and MCP server use, so a Jest/Vitest/Detox spec can
|
|
4
|
+
* `connect()`/`call()` instead of spawning `appduct invoke ... --json` and parsing stdout.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { connect } from "appduct/client";
|
|
8
|
+
*
|
|
9
|
+
* const app = await connect();
|
|
10
|
+
* const { total } = await app.call("sum", { a: 2, b: 3 });
|
|
11
|
+
* await app.close();
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export { connect } from "./connect.js";
|
|
15
|
+
export { link, waitForSession } from "./bootstrap.js";
|
|
16
|
+
export { AppductError } from "./errors.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CliResult, DaemonRunCommandData, DaemonStartCommandData, DaemonStatusCommandData, DaemonStopCommandData } from "../cli/result-types.js";
|
|
2
|
+
import type { Clock } from "../cli/types.js";
|
|
3
|
+
import { type SpawnFn } from "../rpc/client.js";
|
|
4
|
+
export type DaemonCommandContext = {
|
|
5
|
+
stateDir: string;
|
|
6
|
+
clock?: Clock;
|
|
7
|
+
spawn?: SpawnFn;
|
|
8
|
+
warn?: (message: string) => void;
|
|
9
|
+
/** Test seam: the version `daemon status` compares the daemon against; defaults to this
|
|
10
|
+
* package's own version. */
|
|
11
|
+
clientVersion?: string;
|
|
12
|
+
};
|
|
13
|
+
export type DaemonRunHostedResult = {
|
|
14
|
+
result: CliResult<DaemonRunCommandData>;
|
|
15
|
+
completion: Promise<void>;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare const handleDaemonRunCommand: (context: DaemonCommandContext) => Promise<DaemonRunHostedResult>;
|
|
19
|
+
export declare const handleDaemonStartCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStartCommandData>>;
|
|
20
|
+
export declare const handleDaemonStopCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStopCommandData>>;
|
|
21
|
+
export declare const handleDaemonStatusCommand: (context: DaemonCommandContext) => Promise<CliResult<DaemonStatusCommandData>>;
|
|
22
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/commands/daemon.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAM7C,OAAO,EAAuD,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAErG,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;gCAC4B;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,SAAS,CAAC,oBAAoB,CAAC,CAAC;IACxC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,sBAAsB,GACjC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,qBAAqB,CAiC/B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAiB3C,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAyB1C,CAAC;AAQF,eAAO,MAAM,yBAAyB,GACpC,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,uBAAuB,CAAC,CA2D5C,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
2
|
+
import { startDaemon } from "../daemon/daemon.js";
|
|
3
|
+
import { DaemonAlreadyRunningError, readPidFromFile } from "../daemon/pidfile.js";
|
|
4
|
+
import { getStateDirPaths } from "../daemon/state-dir.js";
|
|
5
|
+
import { connectionError } from "../errors.js";
|
|
6
|
+
import { getPackageVersion } from "../package-version.js";
|
|
7
|
+
import { callDaemon, isDaemonUnreachableError, isSameVersion } from "../rpc/client.js";
|
|
8
|
+
export const handleDaemonRunCommand = async (context) => {
|
|
9
|
+
let daemon;
|
|
10
|
+
try {
|
|
11
|
+
daemon = await startDaemon({
|
|
12
|
+
stateDir: context.stateDir,
|
|
13
|
+
clock: context.clock,
|
|
14
|
+
warn: context.warn,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof DaemonAlreadyRunningError) {
|
|
19
|
+
throw connectionError(error.message, { pid: error.pid });
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
result: {
|
|
25
|
+
ok: true,
|
|
26
|
+
data: {
|
|
27
|
+
daemon: {
|
|
28
|
+
pid: process.pid,
|
|
29
|
+
state_dir: daemon.paths.root,
|
|
30
|
+
socket_path: daemon.paths.socketPath,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
completion: daemon.exited,
|
|
35
|
+
stop: () => {
|
|
36
|
+
void daemon.shutdown();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export const handleDaemonStartCommand = async (context) => {
|
|
41
|
+
const status = await callDaemon(RPC_METHODS.daemonStatus, {}, { stateDir: context.stateDir, autoSpawn: true, spawn: context.spawn });
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
data: {
|
|
45
|
+
daemon: {
|
|
46
|
+
pid: status.pid,
|
|
47
|
+
wss_port: status.wssPort,
|
|
48
|
+
started_at: status.startedAt,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export const handleDaemonStopCommand = async (context) => {
|
|
54
|
+
try {
|
|
55
|
+
await callDaemon(RPC_METHODS.daemonShutdown, {}, { stateDir: context.stateDir, autoSpawn: false });
|
|
56
|
+
return { ok: true, data: { daemon: { ok: true, method: "rpc" } } };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!isDaemonUnreachableError(error)) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const paths = getStateDirPaths(context.stateDir);
|
|
63
|
+
const pid = await readPidFromFile(paths.pidFilePath);
|
|
64
|
+
if (pid === undefined) {
|
|
65
|
+
throw connectionError("No Appduct daemon is running.");
|
|
66
|
+
}
|
|
67
|
+
process.kill(pid, "SIGTERM");
|
|
68
|
+
return { ok: true, data: { daemon: { ok: true, method: "sigterm" } } };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** Drops the entries whose value is `undefined`, so spreading the result adds only keys that
|
|
72
|
+
* carry an actual answer. */
|
|
73
|
+
const definedOnly = (fields) => {
|
|
74
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
75
|
+
};
|
|
76
|
+
export const handleDaemonStatusCommand = async (context) => {
|
|
77
|
+
const status = await callDaemon(RPC_METHODS.daemonStatus, {}, { stateDir: context.stateDir, autoSpawn: true, spawn: context.spawn });
|
|
78
|
+
// The wire type declares the retention fields required, as it must for any daemon built from
|
|
79
|
+
// this tree — but the daemon on the other end of the socket may predate them, and a running
|
|
80
|
+
// daemon outlives the CLI upgrade that would replace it. Reading them through a partial view
|
|
81
|
+
// keeps that possibility in the types instead of in a comment, and leaves each one `undefined`
|
|
82
|
+
// rather than `0` when it was never reported.
|
|
83
|
+
const retention = status.audit;
|
|
84
|
+
const clientVersion = context.clientVersion ?? getPackageVersion();
|
|
85
|
+
// Guarded the same way the version check guards it (`rpc/client.ts`): a `daemon.status` without
|
|
86
|
+
// `sessions` would otherwise throw from the middle of the command that exists to diagnose a
|
|
87
|
+
// daemon — the one command that must keep working against a daemon it does not fully understand.
|
|
88
|
+
const sessionCount = Array.isArray(status.sessions) ? status.sessions.length : 0;
|
|
89
|
+
const pendingLinkCount = typeof status.pendingLinks === "number" ? status.pendingLinks : 0;
|
|
90
|
+
// Names the same cost the restart path names (`rpc/client.ts`), so the diagnosis an operator
|
|
91
|
+
// reads here and the error they hit on the next command tell one consistent story: an unclaimed
|
|
92
|
+
// link is destroyed by a restart just as surely as a session is.
|
|
93
|
+
const atStake = `${sessionCount} session(s) and ${pendingLinkCount} unclaimed link(s)`;
|
|
94
|
+
const warning = isSameVersion(status.version, clientVersion)
|
|
95
|
+
? undefined
|
|
96
|
+
: `Version drift: this daemon is ${status.version} but the CLI is ${clientVersion}. ` +
|
|
97
|
+
`Newer RPC methods will fail against it — run "appduct daemon stop" (this drops ${atStake}) ` +
|
|
98
|
+
`and the next command will start a matching daemon, or pass "--daemon-restart" to any command to replace it in place.`;
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
data: {
|
|
102
|
+
warning,
|
|
103
|
+
daemon: {
|
|
104
|
+
version: status.version,
|
|
105
|
+
pid: status.pid,
|
|
106
|
+
started_at: status.startedAt,
|
|
107
|
+
wss_port: status.wssPort,
|
|
108
|
+
pinned_keys: status.pinnedKeys,
|
|
109
|
+
session_count: sessionCount,
|
|
110
|
+
},
|
|
111
|
+
policy: status.policy,
|
|
112
|
+
audit: {
|
|
113
|
+
path: status.audit.path,
|
|
114
|
+
failed_writes: status.audit.failedWrites,
|
|
115
|
+
// Spread so an unreported field is a *missing key*, not a key holding `undefined`. The two
|
|
116
|
+
// serialize identically, but only the first is honestly shaped: a caller can ask whether
|
|
117
|
+
// the daemon answered, and `--json` consumers see the same absence in-process as on the
|
|
118
|
+
// wire.
|
|
119
|
+
...definedOnly({
|
|
120
|
+
failed_prunes: retention.failedPrunes,
|
|
121
|
+
retention_days: retention.retentionDays,
|
|
122
|
+
files: retention.files,
|
|
123
|
+
bytes: retention.bytes,
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct doctor <artifact>` (docs/tasks/08-appduct-doctor.md): the release-gate replacement
|
|
3
|
+
* for the runtime `debuggable` check removed elsewhere in opt-in hardening — see
|
|
4
|
+
* `artifact-inspect.ts`'s doc comment for the detection strategy and why it never collapses a
|
|
5
|
+
* missing-tool or unreadable-artifact case into "absent".
|
|
6
|
+
*
|
|
7
|
+
* This handler owns only the CLI-facing concerns: option validation (mutually exclusive
|
|
8
|
+
* `--assert-present`/`--assert-absent`) and turning a failed assertion into a distinct
|
|
9
|
+
* {@link assertionError} exit, separate from `inspectionError`'s "couldn't tell" exit. All artifact
|
|
10
|
+
* inspection itself lives in `artifact-inspect.ts`, unit-testable without going through the CLI.
|
|
11
|
+
*/
|
|
12
|
+
import type { CliResult, DoctorCommandData } from "../cli/result-types.js";
|
|
13
|
+
import { type ExecBufferFn } from "../artifact-inspect.js";
|
|
14
|
+
export type DoctorCommandOptions = {
|
|
15
|
+
artifactPath?: string;
|
|
16
|
+
assertPresent?: boolean;
|
|
17
|
+
assertAbsent?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type DoctorCommandContext = {
|
|
20
|
+
exec?: ExecBufferFn;
|
|
21
|
+
};
|
|
22
|
+
export declare const handleDoctorCommand: (options: DoctorCommandOptions, context?: DoctorCommandContext) => Promise<CliResult<DoctorCommandData>>;
|
|
23
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG5E,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,UAAS,oBAAyB,KACjC,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAyCtC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct doctor <artifact>` (docs/tasks/08-appduct-doctor.md): the release-gate replacement
|
|
3
|
+
* for the runtime `debuggable` check removed elsewhere in opt-in hardening — see
|
|
4
|
+
* `artifact-inspect.ts`'s doc comment for the detection strategy and why it never collapses a
|
|
5
|
+
* missing-tool or unreadable-artifact case into "absent".
|
|
6
|
+
*
|
|
7
|
+
* This handler owns only the CLI-facing concerns: option validation (mutually exclusive
|
|
8
|
+
* `--assert-present`/`--assert-absent`) and turning a failed assertion into a distinct
|
|
9
|
+
* {@link assertionError} exit, separate from `inspectionError`'s "couldn't tell" exit. All artifact
|
|
10
|
+
* inspection itself lives in `artifact-inspect.ts`, unit-testable without going through the CLI.
|
|
11
|
+
*/
|
|
12
|
+
import { inspectArtifact } from "../artifact-inspect.js";
|
|
13
|
+
import { assertionError, usageError } from "../errors.js";
|
|
14
|
+
export const handleDoctorCommand = async (options, context = {}) => {
|
|
15
|
+
if (!options.artifactPath) {
|
|
16
|
+
throw usageError("Usage: appduct doctor <path-to-artifact> [--assert-present | --assert-absent]");
|
|
17
|
+
}
|
|
18
|
+
if (options.assertPresent && options.assertAbsent) {
|
|
19
|
+
throw usageError("--assert-present and --assert-absent are mutually exclusive.");
|
|
20
|
+
}
|
|
21
|
+
const inspection = await inspectArtifact(options.artifactPath, { exec: context.exec });
|
|
22
|
+
const data = {
|
|
23
|
+
artifact: options.artifactPath,
|
|
24
|
+
platform: inspection.platform,
|
|
25
|
+
format: inspection.format,
|
|
26
|
+
present: inspection.present,
|
|
27
|
+
signals: inspection.signals,
|
|
28
|
+
};
|
|
29
|
+
if (options.assertPresent && !inspection.present) {
|
|
30
|
+
throw assertionError(`Expected Appduct to be present in "${options.artifactPath}", but no inclusion signal was found.`, data);
|
|
31
|
+
}
|
|
32
|
+
if (options.assertAbsent && inspection.present) {
|
|
33
|
+
throw assertionError(`Expected Appduct to be absent from "${options.artifactPath}", but found: ${inspection.signals.join(", ")}.`, data);
|
|
34
|
+
}
|
|
35
|
+
if (options.assertPresent || options.assertAbsent) {
|
|
36
|
+
data.assertion = {
|
|
37
|
+
expected: options.assertPresent ? "present" : "absent",
|
|
38
|
+
holds: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, data };
|
|
42
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` over a persistent
|
|
3
|
+
* connection and streams notifications until interrupted (Ctrl-C) or the connection drops. This is
|
|
4
|
+
* the one command whose output isn't a single rendered `CliResult` — each event is written as it
|
|
5
|
+
* arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
|
|
6
|
+
* with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
|
|
7
|
+
*
|
|
8
|
+
* `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
|
|
9
|
+
* of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
|
|
10
|
+
* is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
|
|
11
|
+
* "hosted command" here just completes immediately instead of running until interrupted.
|
|
12
|
+
*/
|
|
13
|
+
import { type EventNotification } from "@appduct/shared";
|
|
14
|
+
import type { CliResult } from "../cli/result-types.js";
|
|
15
|
+
import { type SpawnFn } from "../rpc/client.js";
|
|
16
|
+
export type EventsCommandOptions = {
|
|
17
|
+
selector?: string;
|
|
18
|
+
/** Exclusive lower bound on `EventNotification.seq`; switches this command to a one-shot
|
|
19
|
+
* `events.since` pull instead of a live `events.subscribe` stream. */
|
|
20
|
+
since?: number;
|
|
21
|
+
};
|
|
22
|
+
export type EventsCommandContext = {
|
|
23
|
+
stateDir: string;
|
|
24
|
+
spawn?: SpawnFn;
|
|
25
|
+
onEvent: (event: EventNotification) => void;
|
|
26
|
+
/** `--since` mode only: called once with the pull's resulting cursor, so a scripted caller
|
|
27
|
+
* doesn't have to reconstruct it by maxing `seq` over the printed lines (impossible when the
|
|
28
|
+
* response is empty — the whole point of a cursor is knowing where to resume from either way). */
|
|
29
|
+
onCursor?: (cursor: number) => void;
|
|
30
|
+
};
|
|
31
|
+
export type EventsHostedResult = {
|
|
32
|
+
result: CliResult<never>;
|
|
33
|
+
completion: Promise<void>;
|
|
34
|
+
stop: () => void;
|
|
35
|
+
};
|
|
36
|
+
export declare const handleEventsCommand: (options: EventsCommandOptions, context: EventsCommandContext) => Promise<EventsHostedResult>;
|
|
37
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/commands/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAe,KAAK,iBAAiB,EAAsD,MAAM,iBAAiB,CAAC;AAE1H,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC5C;;sGAEkG;IAClG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AA4BF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,SAAS,oBAAoB,KAC5B,OAAO,CAAC,kBAAkB,CAkD5B,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` over a persistent
|
|
3
|
+
* connection and streams notifications until interrupted (Ctrl-C) or the connection drops. This is
|
|
4
|
+
* the one command whose output isn't a single rendered `CliResult` — each event is written as it
|
|
5
|
+
* arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
|
|
6
|
+
* with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
|
|
7
|
+
*
|
|
8
|
+
* `--since <cursor>` (issue #6) switches to a one-shot mode: a single `events.since` pull instead
|
|
9
|
+
* of a live subscription. It falls out of the same `EventsHostedResult` shape (each retained event
|
|
10
|
+
* is written through `onEvent` exactly like a live one) so the CLI plumbing needs no forking — the
|
|
11
|
+
* "hosted command" here just completes immediately instead of running until interrupted.
|
|
12
|
+
*/
|
|
13
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
14
|
+
import { openDaemonStream } from "../rpc/client.js";
|
|
15
|
+
const NEVER_RESULT = { ok: true, data: undefined };
|
|
16
|
+
const handleEventsSinceCommand = async (options, context) => {
|
|
17
|
+
const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
|
|
18
|
+
try {
|
|
19
|
+
const since = await stream.call(RPC_METHODS.eventsSince, {
|
|
20
|
+
selector: options.selector,
|
|
21
|
+
since: options.since,
|
|
22
|
+
});
|
|
23
|
+
for (const event of since.events) {
|
|
24
|
+
context.onEvent(event);
|
|
25
|
+
}
|
|
26
|
+
context.onCursor?.(since.cursor);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
stream.close();
|
|
30
|
+
}
|
|
31
|
+
return { result: NEVER_RESULT, completion: Promise.resolve(), stop: () => { } };
|
|
32
|
+
};
|
|
33
|
+
export const handleEventsCommand = async (options, context) => {
|
|
34
|
+
if (options.since !== undefined) {
|
|
35
|
+
return handleEventsSinceCommand(options, context);
|
|
36
|
+
}
|
|
37
|
+
const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
|
|
38
|
+
try {
|
|
39
|
+
await stream.call(RPC_METHODS.eventsSubscribe, {
|
|
40
|
+
sessionSelector: options.selector,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
stream.close();
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
let resolveCompletion;
|
|
48
|
+
const completion = new Promise((resolve) => {
|
|
49
|
+
resolveCompletion = resolve;
|
|
50
|
+
});
|
|
51
|
+
const unsubscribeNotifications = stream.onNotification((payload) => {
|
|
52
|
+
context.onEvent(payload);
|
|
53
|
+
});
|
|
54
|
+
let unsubscribeClose = () => { };
|
|
55
|
+
let stopped = false;
|
|
56
|
+
const stop = () => {
|
|
57
|
+
if (stopped) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
stopped = true;
|
|
61
|
+
unsubscribeNotifications();
|
|
62
|
+
unsubscribeClose();
|
|
63
|
+
stream.close();
|
|
64
|
+
resolveCompletion();
|
|
65
|
+
};
|
|
66
|
+
// The daemon going away mid-stream (shutdown, crash) must end the command gracefully rather than
|
|
67
|
+
// hang forever waiting for a Ctrl-C that will never resolve anything.
|
|
68
|
+
unsubscribeClose = stream.onClose(stop);
|
|
69
|
+
return {
|
|
70
|
+
// Never rendered: the reporter passed to `executeHostedCommand` suppresses the bootstrap
|
|
71
|
+
// render for this command (see the doc comment above).
|
|
72
|
+
result: NEVER_RESULT,
|
|
73
|
+
completion,
|
|
74
|
+
stop,
|
|
75
|
+
};
|
|
76
|
+
};
|