appduct 0.10.0 → 0.11.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.
Files changed (230) hide show
  1. package/README.md +121 -51
  2. package/dist/artifact-inspect.d.ts +0 -1
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +1250 -2
  5. package/dist/cli/command-options.d.ts +8 -1
  6. package/dist/cli/create-cli.d.ts +0 -1
  7. package/dist/cli/dispatch.d.ts +10 -1
  8. package/dist/cli/envelope.d.ts +19 -0
  9. package/dist/cli/global-flags.d.ts +30 -0
  10. package/dist/cli/open-target.d.ts +25 -8
  11. package/dist/cli/result-types.d.ts +30 -5
  12. package/dist/cli/router.d.ts +78 -0
  13. package/dist/cli/routes/daemon/index.d.ts +9 -0
  14. package/dist/cli/routes/daemon/index.js +848 -0
  15. package/dist/cli/routes/daemon/run.d.ts +3 -0
  16. package/dist/cli/routes/daemon/run.js +3246 -0
  17. package/dist/cli/routes/daemon/start.d.ts +3 -0
  18. package/dist/cli/routes/daemon/start.js +1668 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1676 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1646 -0
  23. package/dist/cli/routes/doctor.d.ts +3 -0
  24. package/dist/cli/routes/doctor.js +1075 -0
  25. package/dist/cli/routes/events.d.ts +3 -0
  26. package/dist/cli/routes/events.js +1906 -0
  27. package/dist/cli/routes/init.d.ts +3 -0
  28. package/dist/cli/routes/init.js +1486 -0
  29. package/dist/cli/routes/invoke.d.ts +3 -0
  30. package/dist/cli/routes/invoke.js +1884 -0
  31. package/dist/cli/routes/keygen.d.ts +3 -0
  32. package/dist/cli/routes/keygen.js +928 -0
  33. package/dist/cli/routes/link.d.ts +3 -0
  34. package/dist/cli/routes/link.js +2705 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1659 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3917 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1675 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1851 -0
  43. package/dist/cli/runner.d.ts +3 -11
  44. package/dist/cli/types.d.ts +9 -6
  45. package/dist/cli/version-guard.d.ts +20 -0
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/client/app-client.d.ts +0 -1
  48. package/dist/client/bootstrap.d.ts +4 -4
  49. package/dist/client/connect.d.ts +0 -1
  50. package/dist/client/errors.d.ts +0 -1
  51. package/dist/client/index.d.ts +0 -1
  52. package/dist/client/index.js +2379 -16
  53. package/dist/commands/daemon/context.d.ts +14 -0
  54. package/dist/commands/daemon/run.d.ts +11 -0
  55. package/dist/commands/daemon/start.d.ts +5 -0
  56. package/dist/commands/daemon/status.d.ts +5 -0
  57. package/dist/commands/daemon/stop.d.ts +5 -0
  58. package/dist/commands/doctor.d.ts +0 -1
  59. package/dist/commands/events.d.ts +0 -1
  60. package/dist/commands/init.d.ts +17 -4
  61. package/dist/commands/invoke.d.ts +0 -1
  62. package/dist/commands/keygen.d.ts +0 -1
  63. package/dist/commands/link.d.ts +1 -2
  64. package/dist/commands/ls.d.ts +0 -1
  65. package/dist/commands/mcp.d.ts +2 -4
  66. package/dist/commands/revoke.d.ts +0 -1
  67. package/dist/commands/tools.d.ts +19 -3
  68. package/dist/daemon/address.d.ts +0 -1
  69. package/dist/daemon/audit.d.ts +6 -11
  70. package/dist/daemon/calls.d.ts +0 -1
  71. package/dist/daemon/config.d.ts +13 -16
  72. package/dist/daemon/daemon.d.ts +0 -1
  73. package/dist/daemon/event-bus.d.ts +0 -1
  74. package/dist/daemon/links.d.ts +0 -1
  75. package/dist/daemon/listener.d.ts +0 -1
  76. package/dist/daemon/log-rotation.d.ts +0 -1
  77. package/dist/daemon/pidfile.d.ts +14 -8
  78. package/dist/daemon/policy.d.ts +0 -1
  79. package/dist/daemon/registry.d.ts +0 -1
  80. package/dist/daemon/rpc-errors.d.ts +16 -0
  81. package/dist/daemon/rpc-server.d.ts +0 -11
  82. package/dist/daemon/sessions.d.ts +0 -1
  83. package/dist/daemon/socket-probe.d.ts +0 -1
  84. package/dist/daemon/state-dir.d.ts +0 -1
  85. package/dist/daemon/timers.d.ts +0 -1
  86. package/dist/daemon/tls.d.ts +0 -1
  87. package/dist/errors.d.ts +0 -1
  88. package/dist/host-certificate.d.ts +0 -1
  89. package/dist/index.d.ts +0 -1
  90. package/dist/index.js +1307 -3
  91. package/dist/key-material.d.ts +0 -1
  92. package/dist/link.d.ts +8 -7
  93. package/dist/mcp/app-tools.d.ts +153 -0
  94. package/dist/mcp/connect-tool.d.ts +14 -8
  95. package/dist/mcp/daemon-tools.d.ts +2 -11
  96. package/dist/mcp/events-tool.d.ts +0 -1
  97. package/dist/mcp/server.d.ts +33 -14
  98. package/dist/native-scheme.d.ts +0 -1
  99. package/dist/output.d.ts +8 -11
  100. package/dist/package-root.d.ts +11 -0
  101. package/dist/package-version.d.ts +0 -1
  102. package/dist/qr-terminal.d.ts +0 -1
  103. package/dist/rpc/client.d.ts +0 -1
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +4 -3
  107. package/dist/artifact-inspect.d.ts.map +0 -1
  108. package/dist/artifact-inspect.js +0 -334
  109. package/dist/bin.d.ts.map +0 -1
  110. package/dist/cli/command-options.d.ts.map +0 -1
  111. package/dist/cli/command-options.js +0 -95
  112. package/dist/cli/create-cli.d.ts.map +0 -1
  113. package/dist/cli/create-cli.js +0 -56
  114. package/dist/cli/dispatch.d.ts.map +0 -1
  115. package/dist/cli/dispatch.js +0 -275
  116. package/dist/cli/open-target.d.ts.map +0 -1
  117. package/dist/cli/open-target.js +0 -438
  118. package/dist/cli/result-types.d.ts.map +0 -1
  119. package/dist/cli/result-types.js +0 -6
  120. package/dist/cli/runner.d.ts.map +0 -1
  121. package/dist/cli/runner.js +0 -111
  122. package/dist/cli/types.d.ts.map +0 -1
  123. package/dist/cli/types.js +0 -10
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -2
  126. package/dist/client/app-client.d.ts.map +0 -1
  127. package/dist/client/app-client.js +0 -223
  128. package/dist/client/bootstrap.d.ts.map +0 -1
  129. package/dist/client/bootstrap.js +0 -108
  130. package/dist/client/connect.d.ts.map +0 -1
  131. package/dist/client/connect.js +0 -38
  132. package/dist/client/errors.d.ts.map +0 -1
  133. package/dist/client/errors.js +0 -40
  134. package/dist/client/index.d.ts.map +0 -1
  135. package/dist/commands/daemon.d.ts +0 -22
  136. package/dist/commands/daemon.d.ts.map +0 -1
  137. package/dist/commands/daemon.js +0 -128
  138. package/dist/commands/doctor.d.ts.map +0 -1
  139. package/dist/commands/doctor.js +0 -42
  140. package/dist/commands/events.d.ts.map +0 -1
  141. package/dist/commands/events.js +0 -76
  142. package/dist/commands/init.d.ts.map +0 -1
  143. package/dist/commands/init.js +0 -231
  144. package/dist/commands/invoke.d.ts.map +0 -1
  145. package/dist/commands/invoke.js +0 -54
  146. package/dist/commands/keygen.d.ts.map +0 -1
  147. package/dist/commands/keygen.js +0 -46
  148. package/dist/commands/link.d.ts.map +0 -1
  149. package/dist/commands/link.js +0 -43
  150. package/dist/commands/ls.d.ts.map +0 -1
  151. package/dist/commands/ls.js +0 -7
  152. package/dist/commands/mcp.d.ts.map +0 -1
  153. package/dist/commands/mcp.js +0 -98
  154. package/dist/commands/revoke.d.ts.map +0 -1
  155. package/dist/commands/revoke.js +0 -7
  156. package/dist/commands/tools.d.ts.map +0 -1
  157. package/dist/commands/tools.js +0 -52
  158. package/dist/daemon/address.d.ts.map +0 -1
  159. package/dist/daemon/address.js +0 -71
  160. package/dist/daemon/audit.d.ts.map +0 -1
  161. package/dist/daemon/audit.js +0 -237
  162. package/dist/daemon/calls.d.ts.map +0 -1
  163. package/dist/daemon/calls.js +0 -158
  164. package/dist/daemon/config.d.ts.map +0 -1
  165. package/dist/daemon/config.js +0 -170
  166. package/dist/daemon/daemon.d.ts.map +0 -1
  167. package/dist/daemon/daemon.js +0 -560
  168. package/dist/daemon/event-bus.d.ts.map +0 -1
  169. package/dist/daemon/event-bus.js +0 -111
  170. package/dist/daemon/links.d.ts.map +0 -1
  171. package/dist/daemon/links.js +0 -118
  172. package/dist/daemon/listener.d.ts.map +0 -1
  173. package/dist/daemon/listener.js +0 -132
  174. package/dist/daemon/log-rotation.d.ts.map +0 -1
  175. package/dist/daemon/log-rotation.js +0 -136
  176. package/dist/daemon/pidfile.d.ts.map +0 -1
  177. package/dist/daemon/pidfile.js +0 -90
  178. package/dist/daemon/policy.d.ts.map +0 -1
  179. package/dist/daemon/policy.js +0 -24
  180. package/dist/daemon/registry.d.ts.map +0 -1
  181. package/dist/daemon/registry.js +0 -31
  182. package/dist/daemon/rpc-server.d.ts.map +0 -1
  183. package/dist/daemon/rpc-server.js +0 -203
  184. package/dist/daemon/sessions.d.ts.map +0 -1
  185. package/dist/daemon/sessions.js +0 -429
  186. package/dist/daemon/socket-probe.d.ts.map +0 -1
  187. package/dist/daemon/socket-probe.js +0 -28
  188. package/dist/daemon/state-dir.d.ts.map +0 -1
  189. package/dist/daemon/state-dir.js +0 -43
  190. package/dist/daemon/timers.d.ts.map +0 -1
  191. package/dist/daemon/timers.js +0 -12
  192. package/dist/daemon/tls.d.ts.map +0 -1
  193. package/dist/daemon/tls.js +0 -79
  194. package/dist/errors.d.ts.map +0 -1
  195. package/dist/errors.js +0 -162
  196. package/dist/host-certificate.d.ts.map +0 -1
  197. package/dist/host-certificate.js +0 -111
  198. package/dist/index.d.ts.map +0 -1
  199. package/dist/key-material.d.ts.map +0 -1
  200. package/dist/key-material.js +0 -42
  201. package/dist/link.d.ts.map +0 -1
  202. package/dist/link.js +0 -117
  203. package/dist/mcp/connect-tool.d.ts.map +0 -1
  204. package/dist/mcp/connect-tool.js +0 -396
  205. package/dist/mcp/daemon-tools.d.ts.map +0 -1
  206. package/dist/mcp/daemon-tools.js +0 -29
  207. package/dist/mcp/events-tool.d.ts.map +0 -1
  208. package/dist/mcp/events-tool.js +0 -322
  209. package/dist/mcp/server.d.ts.map +0 -1
  210. package/dist/mcp/server.js +0 -538
  211. package/dist/mcp/tool-mapping.d.ts +0 -58
  212. package/dist/mcp/tool-mapping.d.ts.map +0 -1
  213. package/dist/mcp/tool-mapping.js +0 -143
  214. package/dist/mcp/tool-namespace.d.ts +0 -40
  215. package/dist/mcp/tool-namespace.d.ts.map +0 -1
  216. package/dist/mcp/tool-namespace.js +0 -65
  217. package/dist/native-scheme.d.ts.map +0 -1
  218. package/dist/native-scheme.js +0 -465
  219. package/dist/output.d.ts.map +0 -1
  220. package/dist/output.js +0 -342
  221. package/dist/package-version.d.ts.map +0 -1
  222. package/dist/package-version.js +0 -47
  223. package/dist/qr-terminal.d.ts.map +0 -1
  224. package/dist/qr-terminal.js +0 -45
  225. package/dist/rpc/client.d.ts.map +0 -1
  226. package/dist/rpc/client.js +0 -819
  227. package/dist/scheme.d.ts.map +0 -1
  228. package/dist/scheme.js +0 -321
  229. package/dist/spki-pin.d.ts.map +0 -1
  230. package/dist/spki-pin.js +0 -14
@@ -1,76 +0,0 @@
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
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAYzE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAyFF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,kBAAkB,EAC3B,UAAS,kBAAuB,KAC/B,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,CAsKpC,CAAC"}
@@ -1,231 +0,0 @@
1
- /**
2
- * `appduct init` (issue #29): the one command that takes an app root from "package installed"
3
- * to "an agent can connect".
4
- *
5
- * It writes a *project-level* `.appduct/config.json` holding the deep-link scheme, and returns
6
- * the two things that are not discoverable from the filesystem: the MCP server entry to paste into
7
- * an agent's config, and the `import "@appduct/react-native/auto"` reminder the app needs.
8
- *
9
- * What it deliberately does **not** do:
10
- *
11
- * - It never generates or touches key material. The daemon auto-generates a host key at startup
12
- * (`daemon/tls.ts`), so making `init` a key-generating step would re-introduce exactly the
13
- * "run keygen, paste a pin" ceremony this command exists to remove. `appduct keygen` remains
14
- * for explicit rotation/provisioning.
15
- * - It never writes daemon-side settings (`wssPort`, `keyPath`, `policy`, ...), and the project
16
- * file is never *read* for them either (`scheme.ts`). A project config that could redirect
17
- * `keyPath` would mean a file checked into a repo could move another developer's private key.
18
- * - It never starts, stops or contacts the daemon.
19
- *
20
- * Idempotency (issue #29's acceptance criterion): re-running with the same scheme is a no-op that
21
- * still prints the snippet, a *different* scheme is refused unless `--force`, and `--force` merges
22
- * into the existing JSON rather than truncating it, so a key a future version adds is not silently
23
- * dropped by an old binary.
24
- *
25
- * **`init` is not the resolver.** It considers exactly two *kinds* of source — `--scheme`, and
26
- * whatever {@link discoverStaticProjectScheme} finds on disk (`<cwd>/app.json`, then the native
27
- * Android/iOS probes from `native-scheme.ts`) — plus whatever this very file already records. It
28
- * deliberately does *not* consult `APPDUCT_SCHEME` or walk up for a parent
29
- * `.appduct/config.json`, because its job is to decide what to *write here*, and inheriting
30
- * either would bake an ambient value into a committed file: a shell variable that happened to be
31
- * exported, or a parent project's scheme silently copied into a sub-package. `scheme.ts`'s full
32
- * precedence chain is what *reads* the result. Discovery is shared with that chain's own final
33
- * step via {@link discoverStaticProjectScheme} specifically so the two can never disagree about
34
- * what "discovery" means. `InitCommandData.source` names every origin this command itself can
35
- * produce.
36
- */
37
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
38
- import { dirname, join, resolve } from "node:path";
39
- import { usageError } from "../errors.js";
40
- import { discoverStaticProjectScheme, globalConfigDirs, isValidScheme, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILENAME, PROJECT_CONFIG_RELATIVE_PATH, SCHEME_ENV_VAR, } from "../scheme.js";
41
- const readExistingConfig = async (path) => {
42
- let text;
43
- try {
44
- text = await readFile(path, "utf8");
45
- }
46
- catch (error) {
47
- const code = error.code;
48
- if (code === "ENOENT") {
49
- return undefined;
50
- }
51
- // `.appduct` is a regular file (ENOTDIR), or `config.json` is a directory (EISDIR). Both
52
- // are things a person did to their own project, so they get a usage error naming the path —
53
- // not a raw errno from `readFile` rendered as an internal failure.
54
- if (code === "ENOTDIR") {
55
- throw usageError(`Cannot write ${path}: a file already exists where the "${PROJECT_CONFIG_DIR}" directory ` +
56
- "needs to go. Remove or rename it, then re-run `appduct init`.");
57
- }
58
- if (code === "EISDIR") {
59
- throw usageError(`Cannot write ${path}: it is a directory, not a file. Remove it, then re-run \`appduct init\`.`);
60
- }
61
- throw error;
62
- }
63
- let raw;
64
- try {
65
- raw = JSON.parse(text);
66
- }
67
- catch (error) {
68
- throw usageError(`Could not parse the existing ${path}: ${error.message}. Fix or delete it, then re-run \`appduct init\`.`);
69
- }
70
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
71
- throw usageError(`The existing ${path} is not a JSON object. Fix or delete it, then re-run \`appduct init\`.`);
72
- }
73
- return raw;
74
- };
75
- /**
76
- * The `scheme` already recorded in the project config.
77
- *
78
- * A hand-edited `"scheme": "myapp://"` must not be adopted and echoed back into `mcpServerEntry`
79
- * as though it were usable — every consumer of that entry would compose an unopenable link, and
80
- * `appduct link` would reject the very value `init` just blessed.
81
- *
82
- * But it is only fatal when the run would go on to *keep* that value: `replaceable` says a
83
- * `--scheme` or `--force` is about to overwrite it anyway, and refusing then would make the
84
- * error's own suggested remedy reproduce the error.
85
- */
86
- const readExistingScheme = (existing, path, replaceable) => {
87
- const scheme = existing?.scheme;
88
- if (scheme === undefined) {
89
- return undefined;
90
- }
91
- if (typeof scheme !== "string" || !isValidScheme(scheme)) {
92
- if (replaceable) {
93
- return undefined;
94
- }
95
- throw usageError(`${path} records an invalid deep-link scheme ${JSON.stringify(scheme)}: a scheme must start ` +
96
- 'with a letter and contain only letters, digits, "+", "-" or "." (for example "myapp") — ' +
97
- 'do not include "://". Fix it, or re-run with `--scheme <scheme> --force` to replace it.');
98
- }
99
- return scheme;
100
- };
101
- export const handleInitCommand = async (options, context = {}) => {
102
- const root = resolve(context.cwd ?? process.cwd());
103
- const projectDir = join(root, PROJECT_CONFIG_DIR);
104
- const configPath = join(projectDir, PROJECT_CONFIG_FILENAME);
105
- // Run from `$HOME` (or from the parent of a `APPDUCT_STATE_DIR`), `<cwd>/.appduct` *is*
106
- // the daemon's state directory. Writing there would drop a config beside `key.pem` and the
107
- // audit log and then tell the user it is safe to commit. The same exclusion the walk-up uses
108
- // decides this, so "which directory is global state" has exactly one answer in the codebase.
109
- if (globalConfigDirs({ stateDirRoot: context.stateDir, homeDir: context.homeDir }).has(projectDir)) {
110
- throw usageError(`Refusing to write ${configPath}: that is Appduct's own state directory (it holds the ` +
111
- "daemon's private key and audit log), not a project config. Run `appduct init` from " +
112
- "your app's root directory instead.");
113
- }
114
- if (options.scheme !== undefined && !isValidScheme(options.scheme)) {
115
- throw usageError(`Invalid deep-link scheme ${JSON.stringify(options.scheme)} from --scheme: a scheme must ` +
116
- 'start with a letter and contain only letters, digits, "+", "-" or "." (for example ' +
117
- '"myapp") — do not include "://".');
118
- }
119
- const existing = await readExistingConfig(configPath);
120
- // A `--scheme` or a `--force` is about to overwrite whatever is recorded, so a garbage value
121
- // there is not worth failing over — and failing would make this error's own remedy unusable.
122
- const existingScheme = readExistingScheme(existing, configPath, options.force === true || options.scheme !== undefined);
123
- // Same discovery `resolveScheme`'s own last step runs (app.json, then the native Android/iOS
124
- // probes) — never APPDUCT_SCHEME, never a walk-up, per this file's doc comment. `discovered`
125
- // throws on its own for a malformed value or two native probes disagreeing, exactly as it would
126
- // for `resolveScheme`.
127
- const discovered = await discoverStaticProjectScheme(root);
128
- const discoveredScheme = discovered.scheme;
129
- const discoveredSourceLabel = discovered.source === undefined
130
- ? undefined
131
- : discovered.source === "app-json"
132
- ? "app.json"
133
- : discovered.source;
134
- /*
135
- * Which scheme wins, and when that is an error, is the whole idempotency contract:
136
- *
137
- * - `--scheme` always wins, but replacing a *different* recorded scheme needs `--force`, since
138
- * that is a person asking for one thing while the file already says another.
139
- * - A plain re-run keeps whatever is already recorded, even when discovery has since changed.
140
- * Erroring there would mean `appduct init` — documented as safe to re-run — starts failing
141
- * because somebody renamed a scheme in `app.json` or a native project file. The divergence is
142
- * reported as a `note` instead: visible, but not fatal.
143
- * - `--force` on its own is the escape hatch that re-adopts discovery, replacing the recorded
144
- * scheme with whatever it currently finds.
145
- * - With nothing recorded, discovery decides.
146
- */
147
- const [scheme, source, origin] = (() => {
148
- if (options.scheme !== undefined) {
149
- return [options.scheme, "--scheme", undefined];
150
- }
151
- if (options.force && discoveredScheme !== undefined) {
152
- return [discoveredScheme, discoveredSourceLabel ?? "app.json", discovered.origin];
153
- }
154
- return existingScheme === undefined
155
- ? [discoveredScheme, discoveredSourceLabel ?? "app.json", discovered.origin]
156
- : [existingScheme, "already-recorded", undefined];
157
- })();
158
- if (scheme === undefined) {
159
- throw usageError(`No deep-link scheme found for ${root}, and none was given. Looked in, in order:\n${discovered.tried
160
- .map((location, index) => ` ${index + 1}. ${location}`)
161
- .join("\n")}\nRun \`appduct init --scheme <scheme>\` with the scheme your app ` +
162
- 'registers for deep links (e.g. "myapp"), or declare it in one of the locations above.');
163
- }
164
- if (options.scheme !== undefined &&
165
- existingScheme !== undefined &&
166
- existingScheme !== options.scheme &&
167
- !options.force) {
168
- throw usageError(`${configPath} already records the scheme "${existingScheme}", but --scheme asked for ` +
169
- `"${options.scheme}". Re-run with --force to replace it (every other key in the file is ` +
170
- "preserved), or drop --scheme to keep what is recorded.");
171
- }
172
- // Only when the recorded scheme is the one being *kept by default* and discovery disagrees. An
173
- // explicit `--scheme` (even one that matches what is recorded) is the user stating the answer,
174
- // so there is nothing to bring to their attention; `--force` has already resolved it.
175
- const note = options.scheme === undefined &&
176
- scheme === existingScheme &&
177
- discoveredScheme !== undefined &&
178
- discoveredScheme !== scheme
179
- ? `${discovered.origin} declares "${discoveredScheme}", but ${configPath} records ` +
180
- `"${existingScheme}", which is what Appduct uses. Run \`appduct init --force\` to ` +
181
- "adopt that value instead."
182
- : undefined;
183
- const alreadyCorrect = existingScheme === scheme;
184
- if (!alreadyCorrect) {
185
- // Merge rather than replace: `--force` changes the scheme, it does not reset the file.
186
- const next = { ...(existing ?? {}), scheme };
187
- await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
188
- await writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, {
189
- encoding: "utf8",
190
- mode: 0o600,
191
- });
192
- }
193
- // `mkdir`'s and `writeFile`'s `mode` only apply when they *create*, and both are subject to the
194
- // process umask, so tighten explicitly — exactly as `ensureStateDir` does (ARCHITECTURE.md §3).
195
- // Unconditional, not just on write: an idempotent re-run is the natural way to repair a
196
- // directory that an earlier version, a umask, or a `git checkout` left world-readable. Safe in
197
- // both branches — `alreadyCorrect` implies the file already exists.
198
- await chmod(dirname(configPath), 0o700);
199
- await chmod(configPath, 0o600);
200
- // `origin` (set only in the two discovery branches above — see the tuple returned there) names
201
- // the exact file/key `scheme` was read from, so the human-readable hint can say more than just
202
- // "source: android-manifest".
203
- return {
204
- ok: true,
205
- data: {
206
- path: configPath,
207
- scheme,
208
- source,
209
- ...(origin === undefined ? {} : { origin }),
210
- created: existing === undefined,
211
- changed: !alreadyCorrect,
212
- ...(note === undefined ? {} : { note }),
213
- mcpServerEntry: {
214
- command: "appduct",
215
- args: ["mcp", "--scheme", scheme],
216
- },
217
- nextSteps: [
218
- 'Add `import "@appduct/react-native/auto";` to your app entry (index.js / App.tsx) — it ' +
219
- "is what starts the in-app agent endpoint.",
220
- `Add the Appduct MCP server entry to your agent's MCP config. "--scheme ${scheme}" keeps ` +
221
- `that entry self-contained; ${SCHEME_ENV_VAR} and this ${PROJECT_CONFIG_RELATIVE_PATH} ` +
222
- "work too.",
223
- "With the app running, pair a device: `appduct link --open ios-sim` (or `--open android`).",
224
- ...(origin === undefined ? [] : [`Scheme "${scheme}" was read from ${origin}.`]),
225
- `This file is safe to commit — it holds only "scheme". Do not point --state-dir at this ` +
226
- "directory: the state dir holds the daemon's private key and audit log, which must " +
227
- "never be committed.",
228
- ],
229
- },
230
- };
231
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/commands/invoke.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3E,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,SAAS,oBAAoB;AAC7B;oFACoF;AACpF,SAAS,WAAW,KACnB,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAmDtC,CAAC"}
@@ -1,54 +0,0 @@
1
- /**
2
- * `appduct invoke` (ARCHITECTURE.md §10): `tools.call` round trip. Failures propagate as-is
3
- * (`DaemonRpcError`, preserving `error.data.type` verbatim) — `errors.ts`'s `toCliError` is what
4
- * renders `error.type` as the app's wire error type end-to-end, not this module's job.
5
- */
6
- import { RPC_METHODS } from "@appduct/shared";
7
- import { deriveCallTransportTimeoutMs, MAX_CALL_TIMEOUT_MS } from "../daemon/calls.js";
8
- import { DaemonRpcError, openDaemonStream } from "../rpc/client.js";
9
- export const handleInvokeCommand = async (options, context,
10
- /** Aborted on SIGINT (issue #9): closes the RPC connection that issued `tools.call`, which the
11
- * daemon treats as a mid-flight cancel and forwards to the app as `tool_cancel`. */
12
- signal) => {
13
- const stream = await openDaemonStream({ stateDir: context.stateDir, spawn: context.spawn });
14
- try {
15
- const callPromise = stream.call(RPC_METHODS.toolsCall, {
16
- selector: options.selector,
17
- name: options.tool,
18
- args: options.args,
19
- timeoutMs: options.timeoutMs,
20
- },
21
- // Without this the socket watchdog defaults to 10 s and `--timeout 60000` still dies at 10 s
22
- // with a generic transport error instead of the daemon's own `tool_timeout` (issue #25).
23
- // With no `--timeout` the daemon falls back to the *tool's* declared deadline, which this
24
- // command does not know without an extra `tools.list` round trip, so the watchdog is sized
25
- // for the largest deadline the daemon could enforce. It is only ever a backstop for a daemon
26
- // that answers nothing: one that dies or drops the socket rejects pending calls at once.
27
- deriveCallTransportTimeoutMs(options.timeoutMs ?? MAX_CALL_TIMEOUT_MS));
28
- if (!signal) {
29
- const result = await callPromise;
30
- return { ok: true, data: result.result };
31
- }
32
- const result = await new Promise((resolve, reject) => {
33
- // Attached unconditionally, before the aborted check below — otherwise an already-aborted
34
- // signal cancels without ever wiring a handler onto `callPromise`, and the rejection that
35
- // follows from closing the stream below becomes an unhandled rejection.
36
- callPromise.then(resolve, reject).finally(() => signal.removeEventListener("abort", cancel));
37
- function cancel() {
38
- reject(new DaemonRpcError(-32000, `Tool "${options.tool}" was cancelled.`, { type: "tool_cancelled" }));
39
- // Dropping the connection is what actually cancels the call daemon-side (calls.ts's
40
- // per-connection onClose hook) — this promise settling doesn't wait for that to finish.
41
- stream.close();
42
- }
43
- if (signal.aborted) {
44
- cancel();
45
- return;
46
- }
47
- signal.addEventListener("abort", cancel, { once: true });
48
- });
49
- return { ok: true, data: result.result };
50
- }
51
- finally {
52
- stream.close();
53
- }
54
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"keygen.d.ts","sourceRoot":"","sources":["../../src/commands/keygen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAO3E,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAiCtC,CAAC"}
@@ -1,46 +0,0 @@
1
- /**
2
- * `appduct keygen` (ARCHITECTURE.md §10): fully non-interactive — v1's TTY-only prompt flow made
3
- * it unusable in CI/agent contexts, and neither ARCHITECTURE.md nor the v2 command table describe
4
- * any interactive mode, so it is not carried forward. Default output is `<state-dir>/key.pem`,
5
- * the same path the daemon's TLS listener loads by default (ARCHITECTURE.md §3).
6
- *
7
- * Since opt-in hardening, this is no longer the *only* way to get a key: the daemon now
8
- * auto-generates one at this same default path if it's missing at startup (dev-mode zero-config
9
- * bootstrap — `daemon/tls.ts`). `keygen` remains for explicit key management (rotation via
10
- * `--force`, a non-default `--out`, or CI/scripted provisioning ahead of a release build).
11
- * Key generation/file-hygiene itself is shared with the daemon's auto-generate path via
12
- * `key-material.ts` so both produce byte-identical material.
13
- */
14
- import { stat } from "node:fs/promises";
15
- import { getStateDirPaths } from "../daemon/state-dir.js";
16
- import { usageError } from "../errors.js";
17
- import { generatePrivateKeyPem, writePrivateKeyAtomically } from "../key-material.js";
18
- import { getSpkiPinFromPrivateKeyPem } from "../spki-pin.js";
19
- export const handleKeygenCommand = async (options, context) => {
20
- const outputPath = options.out ?? getStateDirPaths(context.stateDir).keyPath;
21
- if (!options.force) {
22
- let exists = true;
23
- try {
24
- await stat(outputPath);
25
- }
26
- catch (error) {
27
- if (error.code !== "ENOENT") {
28
- throw error;
29
- }
30
- exists = false;
31
- }
32
- if (exists) {
33
- throw usageError(`Refusing to overwrite an existing key at "${outputPath}" without --force.`);
34
- }
35
- }
36
- const keyPem = generatePrivateKeyPem();
37
- const pin = getSpkiPinFromPrivateKeyPem(keyPem);
38
- await writePrivateKeyAtomically(outputPath, keyPem);
39
- return {
40
- ok: true,
41
- data: {
42
- path: outputPath,
43
- pin,
44
- },
45
- };
46
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/commands/link.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAA8B,KAAK,MAAM,EAAmB,MAAM,uBAAuB,CAAC;AAGjG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;+FAC2F;IAC3F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,kBAAkB,EAC3B,SAAS,kBAAkB,KAC1B,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,CA6CpC,CAAC"}
@@ -1,43 +0,0 @@
1
- /**
2
- * `appduct link` (ARCHITECTURE.md §10, §8): CLI-flag validation (`--open`/`--device`'s
3
- * CLI-specific error wording) around the shared `mintLink` core (`../link.ts`), which both this
4
- * command and `appduct/client`'s `link()` use so the deep-link shape can't drift between them.
5
- */
6
- import { isOpenTarget, OPEN_TARGETS } from "../cli/open-target.js";
7
- import { mintLink } from "../link.js";
8
- import { usageError } from "../errors.js";
9
- export const handleLinkCommand = async (options, context) => {
10
- let openTarget;
11
- if (options.open !== undefined) {
12
- if (!isOpenTarget(options.open)) {
13
- throw usageError(`"--open" must be one of ${OPEN_TARGETS.map((target) => `"${target}"`).join(", ")} (got "${options.open}").`);
14
- }
15
- openTarget = options.open;
16
- }
17
- if (options.device !== undefined && openTarget === undefined) {
18
- throw usageError('"--device" only applies with "--open".');
19
- }
20
- // A bundle id is only ever consumed by the `devicectl` launch; accepting it silently elsewhere
21
- // would let `--open ios-sim --bundle-id ...` look like it did something it did not.
22
- if (options.bundleId !== undefined && openTarget !== "ios-device") {
23
- throw usageError('"--bundle-id" only applies with "--open ios-device".');
24
- }
25
- if (options.relaunch !== undefined && openTarget !== "ios-device") {
26
- throw usageError('"--relaunch" only applies with "--open ios-device".');
27
- }
28
- const result = await mintLink({
29
- stateDir: context.stateDir,
30
- spawn: context.spawn,
31
- ttlSeconds: options.ttlSeconds,
32
- scheme: options.scheme,
33
- cwd: context.cwd,
34
- target: openTarget,
35
- device: options.device,
36
- bundleId: options.bundleId,
37
- relaunch: options.relaunch,
38
- exec: context.exec,
39
- env: context.env,
40
- schemeEnv: context.schemeEnv,
41
- });
42
- return { ok: true, data: result };
43
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../src/commands/ls.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAIvE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,SAAS,gBAAgB,KAAG,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,CAQjG,CAAC"}
@@ -1,7 +0,0 @@
1
- /** `appduct ls` (ARCHITECTURE.md §10): `sessions.list` passthrough. */
2
- import { RPC_METHODS } from "@appduct/shared";
3
- import { callDaemon } from "../rpc/client.js";
4
- export const handleLsCommand = async (context) => {
5
- const sessions = await callDaemon(RPC_METHODS.sessionsList, {}, { stateDir: context.stateDir, spawn: context.spawn });
6
- return { ok: true, data: sessions };
7
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAItD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGrE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uFAAuF;IACvF,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB;8BAC0B;IAC1B,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CAC5C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,wFAAwF;IACxF,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AA2CF,eAAO,MAAM,gBAAgB,GAAU,SAAS,iBAAiB,KAAG,OAAO,CAAC,eAAe,CAqD1F,CAAC"}
@@ -1,98 +0,0 @@
1
- /**
2
- * `appduct mcp` (ARCHITECTURE.md §9, §10): starts a stdio MCP server that proxies the daemon RPC
3
- * (auto-spawn applies, same as every other command). This is a hosted command like `daemon run`/
4
- * `events` — it runs until the transport closes (client disconnected) or the process is
5
- * interrupted — and its stdout is reserved entirely for MCP protocol frames, so unlike every other
6
- * command it must never let `executeHostedCommand`'s default bootstrap render reach stdout (the
7
- * dispatcher wires an `"interactive"` reporter for exactly this reason, same as `events`).
8
- *
9
- * The deep-link scheme (`--scheme`, `APPDUCT_SCHEME`, project/state config, `app.json` — see
10
- * `scheme.ts`) is resolved once here at startup, and *not* finding one is not an error: see
11
- * {@link resolveSchemeForServer}.
12
- */
13
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
- import { loadConfig } from "../daemon/config.js";
15
- import { getStateDirPaths } from "../daemon/state-dir.js";
16
- import { createMcpServer } from "../mcp/server.js";
17
- import { resolveScheme } from "../scheme.js";
18
- /**
19
- * Resolves the deep-link scheme for the server, *never* throwing.
20
- *
21
- * Unlike `appduct link`, an unresolved (or unresolvable) scheme must not stop `appduct mcp`
22
- * from starting: the server is still fully useful for proxying an app's tools to a session that was
23
- * paired some other way (a QR scan, `appduct link` in another terminal), and an MCP client that
24
- * cannot start its server gets a much worse failure than one whose `appduct_connect` call
25
- * returns a clear `invalid_request`. So a scheme problem here is downgraded to an extra entry in
26
- * the `tried` list, which `appduct_connect` renders if and when it is actually called.
27
- *
28
- * It is *also* reported on stderr at startup, because an explicitly wrong `--scheme myapp://` or
29
- * `APPDUCT_SCHEME` is a typo the operator wants to hear about immediately, not only if and when
30
- * some agent happens to call `appduct_connect`. Stderr, never stdout: stdout carries MCP
31
- * protocol frames exclusively, and MCP clients surface a server's stderr in their logs.
32
- */
33
- const resolveSchemeForServer = async (context, configScheme, paths) => {
34
- try {
35
- return await resolveScheme({
36
- flagScheme: context.scheme,
37
- env: context.schemeEnv,
38
- cwd: context.cwd,
39
- configScheme,
40
- stateConfigPath: paths.configPath,
41
- stateDirRoot: paths.root,
42
- });
43
- }
44
- catch (error) {
45
- const message = error.message;
46
- (context.stderr ?? process.stderr).write(`appduct mcp: could not resolve a deep-link scheme: ${message}\n` +
47
- "The server is starting anyway; appduct_connect will fail until this is fixed.\n");
48
- return { tried: [`(scheme resolution failed: ${message})`] };
49
- }
50
- };
51
- export const handleMcpCommand = async (context) => {
52
- const paths = getStateDirPaths(context.stateDir);
53
- const config = await loadConfig(paths);
54
- const { scheme, tried } = await resolveSchemeForServer(context, config.scheme, paths);
55
- const handle = await createMcpServer({
56
- stateDir: context.stateDir,
57
- spawn: context.spawn,
58
- checkVersion: context.checkVersion,
59
- scheme,
60
- schemeTried: tried,
61
- iosBundleId: context.iosBundleId ?? config.iosBundleId,
62
- exec: context.exec,
63
- env: context.env,
64
- });
65
- let resolveCompletion;
66
- const completion = new Promise((resolve) => {
67
- resolveCompletion = resolve;
68
- });
69
- let stopped = false;
70
- const stop = () => {
71
- if (stopped) {
72
- return;
73
- }
74
- stopped = true;
75
- void handle.close().finally(resolveCompletion);
76
- };
77
- // The client disconnecting (stdin closed) ends the transport, which must end the command
78
- // gracefully rather than hang forever waiting for a Ctrl-C that will never come.
79
- handle.server.onclose = stop;
80
- handle.server.onerror = (error) => {
81
- console.error("appduct mcp: transport error:", error);
82
- };
83
- try {
84
- await handle.connect(new StdioServerTransport(context.stdin, context.stdout));
85
- }
86
- catch (error) {
87
- await handle.close();
88
- throw error;
89
- }
90
- return {
91
- // Never rendered: the dispatcher suppresses the bootstrap render for this command (see the
92
- // doc comment above) so stdout carries only MCP protocol frames.
93
- result: { ok: true, data: undefined },
94
- completion,
95
- stop,
96
- handle,
97
- };
98
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"revoke.d.ts","sourceRoot":"","sources":["../../src/commands/revoke.ts"],"names":[],"mappings":"AAAA,yFAAyF;AAIzF,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,EAC7B,SAAS,oBAAoB,KAC5B,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAQtC,CAAC"}
@@ -1,7 +0,0 @@
1
- /** `appduct revoke` (ARCHITECTURE.md §10): revokes a session, optionally by selector. */
2
- import { RPC_METHODS } from "@appduct/shared";
3
- import { callDaemon } from "../rpc/client.js";
4
- export const handleRevokeCommand = async (options, context) => {
5
- await callDaemon(RPC_METHODS.sessionsRevoke, { selector: options.selector }, { stateDir: context.stateDir, spawn: context.spawn });
6
- return { ok: true, data: { ok: true } };
7
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/commands/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,EAA8B,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE5E,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAiBF,eAAO,MAAM,kBAAkB,GAC7B,SAAS,mBAAmB,EAC5B,SAAS,mBAAmB,KAC3B,OAAO,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAyCrC,CAAC"}