appduct 0.10.0 → 0.11.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.
Files changed (230) hide show
  1. package/README.md +122 -52
  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 +31 -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 +1677 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1685 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1655 -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 +1915 -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 +1893 -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 +2714 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1668 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3929 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1684 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1860 -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 +4 -4
  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 +1 -2
  52. package/dist/client/index.js +2389 -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 +3 -3
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +6 -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
@@ -0,0 +1,1075 @@
1
+ // src/artifact-inspect.ts
2
+ import { execFile } from "node:child_process";
3
+ import { readdir, readFile, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ // src/rpc/client.ts
7
+ import { join as join2 } from "node:path";
8
+ import { RPC_METHODS } from "@appduct/shared";
9
+
10
+ // src/daemon/config.ts
11
+ var DEFAULT_DAEMON_LOG_MAX_BYTES = 10 * 1024 * 1024;
12
+
13
+ // src/package-root.ts
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ var PACKAGE_NAME = "appduct";
18
+ var cached;
19
+ var getPackageRoot = () => {
20
+ if (cached !== void 0) {
21
+ return cached;
22
+ }
23
+ let dir = dirname(fileURLToPath(import.meta.url));
24
+ for (; ; ) {
25
+ const candidate = join(dir, "package.json");
26
+ if (existsSync(candidate)) {
27
+ let name;
28
+ try {
29
+ name = JSON.parse(readFileSync(candidate, "utf8")).name;
30
+ } catch {
31
+ }
32
+ if (name === PACKAGE_NAME) {
33
+ cached = dir;
34
+ return dir;
35
+ }
36
+ }
37
+ const parent = dirname(dir);
38
+ if (parent === dir) {
39
+ throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
40
+ }
41
+ dir = parent;
42
+ }
43
+ };
44
+
45
+ // src/rpc/client.ts
46
+ var defaultBinPath = join2(getPackageRoot(), "bin.js");
47
+ var DaemonRpcError = class extends Error {
48
+ constructor(code, message, data) {
49
+ super(message);
50
+ this.code = code;
51
+ this.data = data;
52
+ this.name = "DaemonRpcError";
53
+ }
54
+ };
55
+ var DaemonUnavailableError = class extends Error {
56
+ constructor(message, reason = "unreachable") {
57
+ super(message);
58
+ this.reason = reason;
59
+ this.name = "DaemonUnavailableError";
60
+ }
61
+ };
62
+ var describeLiveState = (state) => {
63
+ const parts = [];
64
+ if (state.sessions > 0) {
65
+ parts.push(`${state.sessions} connected session(s)`);
66
+ }
67
+ if (state.pendingLinks > 0) {
68
+ parts.push(`${state.pendingLinks} unclaimed link(s)`);
69
+ }
70
+ return parts.join(" and ");
71
+ };
72
+ var DaemonVersionMismatchError = class extends Error {
73
+ constructor(daemonVersion, clientVersion, live, message) {
74
+ super(
75
+ message ?? `The running Appduct daemon is version ${daemonVersion}, but this client is version ${clientVersion}. It was not restarted automatically because ${describeLiveState(live)} would be lost. Run "appduct daemon stop" once that state is expendable, or force it with "--daemon-restart".`
76
+ );
77
+ this.daemonVersion = daemonVersion;
78
+ this.clientVersion = clientVersion;
79
+ this.name = "DaemonVersionMismatchError";
80
+ this.sessionCount = live.sessions;
81
+ this.pendingLinkCount = live.pendingLinks;
82
+ }
83
+ sessionCount;
84
+ pendingLinkCount;
85
+ };
86
+ var isConnectionMissingError = (error) => {
87
+ const code = error.code;
88
+ return code === "ENOENT" || code === "ECONNREFUSED";
89
+ };
90
+ var isDaemonUnreachableError = (error) => {
91
+ return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
92
+ };
93
+
94
+ // src/errors.ts
95
+ var EXIT_CODE_BY_ERROR_TYPE = {
96
+ usage_error: 64,
97
+ validation_error: 65,
98
+ // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
99
+ // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
100
+ // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
101
+ // assertion failure (docs/tasks/08-appduct-doctor.md).
102
+ inspection_error: 66,
103
+ connection_error: 70,
104
+ session_error: 71,
105
+ tool_error: 72,
106
+ permission_error: 77,
107
+ internal_error: 1,
108
+ // `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
109
+ // assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
110
+ // and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
111
+ // conflate a normal assertion failure with an unexpected crash.
112
+ assertion_error: 3
113
+ };
114
+ var RPC_ERROR_EXIT_CLASS = {
115
+ no_session: "session_error",
116
+ ambiguous_session: "session_error",
117
+ unknown_session: "session_error",
118
+ session_not_active: "session_error",
119
+ session_suspended: "session_error",
120
+ tool_not_found: "tool_error",
121
+ tool_input_validation_error: "tool_error",
122
+ tool_output_validation_error: "tool_error",
123
+ tool_execution_error: "tool_error",
124
+ tool_serialization_error: "tool_error",
125
+ tool_timeout: "tool_error",
126
+ tool_cancelled: "tool_error",
127
+ policy_denied: "permission_error",
128
+ invalid_request: "validation_error"
129
+ };
130
+ var AppductCliError = class extends Error {
131
+ type;
132
+ details;
133
+ constructor(type, message, details) {
134
+ super(message);
135
+ this.name = "AppductCliError";
136
+ this.type = type;
137
+ this.details = details;
138
+ }
139
+ };
140
+ var usageError = (message, details) => {
141
+ return new AppductCliError("usage_error", message, details);
142
+ };
143
+ var inspectionError = (message, details) => {
144
+ return new AppductCliError("inspection_error", message, details);
145
+ };
146
+ var assertionError = (message, details) => {
147
+ return new AppductCliError("assertion_error", message, details);
148
+ };
149
+ var isAppductCliError = (value) => {
150
+ return value instanceof AppductCliError;
151
+ };
152
+ var rpcErrorClass = (error) => {
153
+ const type = error.data?.type;
154
+ return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
155
+ };
156
+ var toCliError = (error) => {
157
+ if (isAppductCliError(error)) {
158
+ return {
159
+ type: error.type,
160
+ message: error.message,
161
+ details: error.details
162
+ };
163
+ }
164
+ if (error instanceof DaemonVersionMismatchError) {
165
+ return {
166
+ type: "connection_error",
167
+ message: error.message,
168
+ details: {
169
+ daemon_version: error.daemonVersion,
170
+ client_version: error.clientVersion,
171
+ session_count: error.sessionCount,
172
+ pending_link_count: error.pendingLinkCount
173
+ }
174
+ };
175
+ }
176
+ if (error instanceof DaemonRpcError && error.data?.type) {
177
+ return {
178
+ type: error.data.type,
179
+ message: error.message,
180
+ details: error.data.details
181
+ };
182
+ }
183
+ if (isDaemonUnreachableError(error)) {
184
+ return {
185
+ type: "connection_error",
186
+ message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
187
+ };
188
+ }
189
+ if (error instanceof Error) {
190
+ return {
191
+ type: "internal_error",
192
+ message: error.message
193
+ };
194
+ }
195
+ return {
196
+ type: "internal_error",
197
+ message: "An unexpected error occurred.",
198
+ details: error
199
+ };
200
+ };
201
+ var getExitCodeForError = (error) => {
202
+ if (isAppductCliError(error)) {
203
+ return EXIT_CODE_BY_ERROR_TYPE[error.type];
204
+ }
205
+ if (error instanceof DaemonVersionMismatchError) {
206
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
207
+ }
208
+ if (error instanceof DaemonRpcError) {
209
+ const cliErrorClass = rpcErrorClass(error);
210
+ return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
211
+ }
212
+ if (isDaemonUnreachableError(error)) {
213
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
214
+ }
215
+ return EXIT_CODE_BY_ERROR_TYPE.internal_error;
216
+ };
217
+
218
+ // src/artifact-inspect.ts
219
+ var IOS_CORE_MARKER = "AppductCoreMarker";
220
+ var IOS_OBJC_CLASS_MARKER = "RCTNativeAppduct";
221
+ var IOS_INFO_PLIST_KEY_MARKERS = ["AppductCliPins", "AppductTrust", "AppductAllowPrivateLanOnly"];
222
+ var ANDROID_KEEP_RULE_MARKER_CLASSES = [
223
+ "com/callstack/appduct/AppductNativeMarker",
224
+ "com/callstackincubator/appduct/AppductNativeMarker"
225
+ ];
226
+ var ANDROID_DEX_PACKAGE_MARKERS = [
227
+ "com/callstack/appduct",
228
+ "com.callstack.appduct",
229
+ "com/callstackincubator/appduct",
230
+ "com.callstackincubator.appduct"
231
+ ];
232
+ var ANDROID_MANIFEST_KEY_MARKERS = ["com.callstack.appduct.", "com.callstackincubator.appduct."];
233
+ var bufferIncludesAscii = (haystack, needle) => {
234
+ return haystack.includes(Buffer.from(needle, "utf8"));
235
+ };
236
+ var bufferIncludesUtf16le = (haystack, needle) => {
237
+ return haystack.includes(Buffer.from(needle, "utf16le"));
238
+ };
239
+ var defaultExecBuffer = (command, args) => {
240
+ return new Promise((resolve, reject) => {
241
+ execFile(
242
+ command,
243
+ args,
244
+ { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 },
245
+ (error, stdout, stderr) => {
246
+ if (error) {
247
+ reject(Object.assign(error, { stdout, stderr }));
248
+ return;
249
+ }
250
+ resolve({ stdout, stderr });
251
+ }
252
+ );
253
+ });
254
+ };
255
+ var TOOL_INSTALL_HINT = {
256
+ unzip: "Info-ZIP's unzip (present by default on macOS and most Linux CI images)"
257
+ };
258
+ var runToolOrInspectionError = async (exec, command, args, context) => {
259
+ try {
260
+ const result = await exec(command, args);
261
+ return result.stdout;
262
+ } catch (error) {
263
+ const err = error;
264
+ if (err.code === "ENOENT") {
265
+ throw inspectionError(
266
+ `"${command}" was not found on PATH; cannot inspect ${context}. Install ${TOOL_INSTALL_HINT[command] ?? command} and retry.`
267
+ );
268
+ }
269
+ if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
270
+ throw inspectionError(
271
+ `"${command} ${args.join(" ")}" produced more output than this command will buffer while inspecting ${context}. The artifact is unusually large, not necessarily corrupt \u2014 this is a tooling limit, not a presence/absence answer.`
272
+ );
273
+ }
274
+ const stderr = err.stderr ? err.stderr.toString("utf8").trim() : "";
275
+ throw inspectionError(
276
+ `"${command} ${args.join(" ")}" failed while inspecting ${context}${stderr ? `: ${stderr}` : `: ${err.message}`}. The artifact may be corrupt or not a valid archive.`
277
+ );
278
+ }
279
+ };
280
+ var EXPECTED_ENTRY_PATTERN = {
281
+ ipa: /\.app\//iu,
282
+ apk: /(^|\/)AndroidManifest\.xml$/u,
283
+ aab: /(^|\/)AndroidManifest\.xml$/u
284
+ };
285
+ var assertExpectedZipShape = (format, entries, context) => {
286
+ if (entries.length === 0) {
287
+ throw inspectionError(`${context} contains no entries \u2014 it is not a valid archive.`);
288
+ }
289
+ if (!entries.some((entry) => EXPECTED_ENTRY_PATTERN[format].test(entry))) {
290
+ throw inspectionError(
291
+ `${context} does not look like a valid .${format}: none of its ${entries.length} entries match the expected ${format === "ipa" ? '"*.app/" bundle' : '"AndroidManifest.xml"'} shape. Refusing to report absence for an artifact that may simply be the wrong file.`
292
+ );
293
+ }
294
+ };
295
+ var listZipEntries = async (exec, archivePath, context) => {
296
+ const stdout = await runToolOrInspectionError(exec, "unzip", ["-Z1", archivePath], context);
297
+ return stdout.toString("utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
298
+ };
299
+ var readZipEntries = async (exec, archivePath, format, context) => {
300
+ const entries = await listZipEntries(exec, archivePath, context);
301
+ assertExpectedZipShape(format, entries, context);
302
+ return runToolOrInspectionError(exec, "unzip", ["-p", archivePath], context);
303
+ };
304
+ var readAppDirectory = async (appDirPath, context) => {
305
+ const chunks = [];
306
+ const walk = async (dir) => {
307
+ let entries;
308
+ try {
309
+ entries = await readdir(dir, { withFileTypes: true });
310
+ } catch (error) {
311
+ throw inspectionError(
312
+ `Could not read "${dir}" while inspecting ${context}: ${error.message}.`
313
+ );
314
+ }
315
+ for (const entry of entries) {
316
+ if (entry.isSymbolicLink()) {
317
+ continue;
318
+ }
319
+ const entryPath = path.join(dir, entry.name);
320
+ if (entry.isDirectory()) {
321
+ await walk(entryPath);
322
+ continue;
323
+ }
324
+ if (entry.isFile()) {
325
+ try {
326
+ chunks.push(await readFile(entryPath));
327
+ } catch (error) {
328
+ throw inspectionError(
329
+ `Could not read "${entryPath}" while inspecting ${context}: ${error.message}.`
330
+ );
331
+ }
332
+ }
333
+ }
334
+ };
335
+ await walk(appDirPath);
336
+ return Buffer.concat(chunks);
337
+ };
338
+ var resolveFormat = (artifactPath) => {
339
+ const extension = path.extname(artifactPath).toLowerCase();
340
+ switch (extension) {
341
+ case ".app":
342
+ return { platform: "ios", format: "app" };
343
+ case ".ipa":
344
+ return { platform: "ios", format: "ipa" };
345
+ case ".apk":
346
+ return { platform: "android", format: "apk" };
347
+ case ".aab":
348
+ return { platform: "android", format: "aab" };
349
+ default:
350
+ throw usageError(
351
+ `Unrecognized artifact extension "${extension || artifactPath}". Expected one of: .app, .ipa, .apk, .aab.`
352
+ );
353
+ }
354
+ };
355
+ var detectIosSignals = (bytes) => {
356
+ const signals = [];
357
+ if (bufferIncludesAscii(bytes, IOS_CORE_MARKER)) {
358
+ signals.push("ios-core-marker-symbol");
359
+ }
360
+ if (bufferIncludesAscii(bytes, IOS_OBJC_CLASS_MARKER)) {
361
+ signals.push("ios-objc-class-symbol");
362
+ }
363
+ if (IOS_INFO_PLIST_KEY_MARKERS.some((marker) => bufferIncludesAscii(bytes, marker))) {
364
+ signals.push("ios-info-plist-keys");
365
+ }
366
+ return signals;
367
+ };
368
+ var detectAndroidSignals = (bytes) => {
369
+ const signals = [];
370
+ if (ANDROID_KEEP_RULE_MARKER_CLASSES.some((klass) => bufferIncludesAscii(bytes, `L${klass};`))) {
371
+ signals.push("android-keep-rule-marker");
372
+ }
373
+ if (ANDROID_DEX_PACKAGE_MARKERS.some((marker) => bufferIncludesAscii(bytes, marker))) {
374
+ signals.push("android-dex-package-symbol");
375
+ }
376
+ if (ANDROID_MANIFEST_KEY_MARKERS.some(
377
+ (marker) => bufferIncludesAscii(bytes, marker) || bufferIncludesUtf16le(bytes, marker)
378
+ )) {
379
+ signals.push("android-manifest-meta-data-keys");
380
+ }
381
+ return signals;
382
+ };
383
+ var inspectArtifact = async (artifactPath, options = {}) => {
384
+ const exec = options.exec ?? defaultExecBuffer;
385
+ const { platform, format } = resolveFormat(artifactPath);
386
+ const context = `"${artifactPath}"`;
387
+ let stats;
388
+ try {
389
+ stats = await stat(artifactPath);
390
+ } catch (error) {
391
+ throw inspectionError(`Artifact not found at ${context}: ${error.message}.`);
392
+ }
393
+ let bytes;
394
+ if (format === "app") {
395
+ if (!stats.isDirectory()) {
396
+ throw inspectionError(`Expected ${context} (a ".app" bundle) to be a directory.`);
397
+ }
398
+ bytes = await readAppDirectory(artifactPath, context);
399
+ } else {
400
+ if (!stats.isFile()) {
401
+ throw inspectionError(`Expected ${context} to be a file.`);
402
+ }
403
+ bytes = await readZipEntries(exec, artifactPath, format, context);
404
+ }
405
+ const signals = platform === "ios" ? detectIosSignals(bytes) : detectAndroidSignals(bytes);
406
+ const present = platform === "android" ? signals.includes("android-keep-rule-marker") : signals.includes("ios-core-marker-symbol") || signals.includes("ios-objc-class-symbol");
407
+ return {
408
+ platform,
409
+ format,
410
+ present,
411
+ signals
412
+ };
413
+ };
414
+
415
+ // src/commands/doctor.ts
416
+ var handleDoctorCommand = async (options, context = {}) => {
417
+ if (!options.artifactPath) {
418
+ throw usageError("Usage: appduct doctor <path-to-artifact> [--assert-present | --assert-absent]");
419
+ }
420
+ if (options.assertPresent && options.assertAbsent) {
421
+ throw usageError("--assert-present and --assert-absent are mutually exclusive.");
422
+ }
423
+ const inspection = await inspectArtifact(options.artifactPath, { exec: context.exec });
424
+ const data = {
425
+ artifact: options.artifactPath,
426
+ platform: inspection.platform,
427
+ format: inspection.format,
428
+ present: inspection.present,
429
+ signals: inspection.signals
430
+ };
431
+ if (options.assertPresent && !inspection.present) {
432
+ throw assertionError(
433
+ `Expected Appduct to be present in "${options.artifactPath}", but no inclusion signal was found.`,
434
+ data
435
+ );
436
+ }
437
+ if (options.assertAbsent && inspection.present) {
438
+ throw assertionError(
439
+ `Expected Appduct to be absent from "${options.artifactPath}", but found: ${inspection.signals.join(", ")}.`,
440
+ data
441
+ );
442
+ }
443
+ if (options.assertPresent || options.assertAbsent) {
444
+ data.assertion = {
445
+ expected: options.assertPresent ? "present" : "absent",
446
+ holds: true
447
+ };
448
+ }
449
+ return { ok: true, data };
450
+ };
451
+
452
+ // src/cli/global-flags.ts
453
+ var formatJson = (value, flags) => {
454
+ return flags.pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
455
+ };
456
+
457
+ // src/output.ts
458
+ import pc from "picocolors";
459
+ import {
460
+ formatAgentWebSocketUrl,
461
+ renderToolSignature,
462
+ summarizeToolDescription
463
+ } from "@appduct/shared";
464
+
465
+ // src/qr-terminal.ts
466
+ import { toQR } from "toqr";
467
+ var HALF_UPPER = "\u2580";
468
+ var HALF_LOWER = "\u2584";
469
+ var FULL = "\u2588";
470
+ var renderQrToTerminal = (content, options) => {
471
+ const margin = options?.margin ?? 0;
472
+ const ecLevel = options?.ecLevel ?? 1;
473
+ const modules = toQR(content, ecLevel);
474
+ const side = Math.sqrt(modules.length);
475
+ if (!Number.isInteger(side)) {
476
+ throw new Error("Invalid QR matrix size");
477
+ }
478
+ const dim = side + 2 * margin;
479
+ const isDark = (row, col) => {
480
+ if (row < margin || col < margin || row >= margin + side || col >= margin + side) {
481
+ return false;
482
+ }
483
+ const mr = row - margin;
484
+ const mc = col - margin;
485
+ return modules[mr * side + mc] === 1;
486
+ };
487
+ const lines = [];
488
+ for (let row = 0; row < dim; row += 2) {
489
+ let line = "";
490
+ for (let col = 0; col < dim; col++) {
491
+ const top = isDark(row, col);
492
+ const bottom = row + 1 < dim ? isDark(row + 1, col) : false;
493
+ line += top ? bottom ? FULL : HALF_UPPER : bottom ? HALF_LOWER : " ";
494
+ }
495
+ lines.push(line);
496
+ }
497
+ return lines.join("\n");
498
+ };
499
+
500
+ // src/output.ts
501
+ var renderMetaLines = (meta) => {
502
+ if (!meta) {
503
+ return [];
504
+ }
505
+ return [
506
+ "Meta",
507
+ ` Command: ${meta.command}`,
508
+ ` Timestamp: ${meta.timestamp}`,
509
+ ...typeof meta.duration_ms === "number" ? [` Duration: ${meta.duration_ms} ms`] : []
510
+ ];
511
+ };
512
+ var formatScalar = (value, flags) => {
513
+ if (typeof value === "string") {
514
+ return value;
515
+ }
516
+ if (typeof value === "number" || typeof value === "boolean") {
517
+ return String(value);
518
+ }
519
+ if (value === null || value === void 0) {
520
+ return "null";
521
+ }
522
+ return formatJson(value, flags);
523
+ };
524
+ var renderFields = (title, fields, flags) => {
525
+ const visibleFields = fields.filter(([, value]) => value !== void 0);
526
+ if (visibleFields.length === 0) {
527
+ return [];
528
+ }
529
+ const width = Math.max(...visibleFields.map(([label]) => label.length));
530
+ return [
531
+ title,
532
+ ...visibleFields.map(([label, value]) => ` ${label.padEnd(width)} ${formatScalar(value, flags)}`)
533
+ ];
534
+ };
535
+ var formatAge = (fromIso, toIso) => {
536
+ const elapsedMs = Math.max(0, new Date(toIso).getTime() - new Date(fromIso).getTime());
537
+ const elapsedSeconds = Math.floor(elapsedMs / 1e3);
538
+ if (elapsedSeconds < 60) {
539
+ return `${elapsedSeconds}s`;
540
+ }
541
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
542
+ if (elapsedMinutes < 60) {
543
+ return `${elapsedMinutes}m`;
544
+ }
545
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
546
+ if (elapsedHours < 24) {
547
+ return `${elapsedHours}h`;
548
+ }
549
+ return `${Math.floor(elapsedHours / 24)}d`;
550
+ };
551
+ var formatDevice = (device) => {
552
+ const parts = [device.manufacturer, device.model].filter((part) => Boolean(part));
553
+ const label = parts.length > 0 ? parts.join(" ") : "unknown device";
554
+ return device.os ? `${label} (${device.os})` : label;
555
+ };
556
+ var renderLsData = (colors, data, now) => {
557
+ if (data.length === 0) {
558
+ return [colors.dim("Sessions"), " No Appduct sessions are registered."];
559
+ }
560
+ const nowIso = now.toISOString();
561
+ const headers = ["Alias", "State", "Device", "Tools", "Age"];
562
+ const rows = data.map((session) => [
563
+ session.alias,
564
+ session.state,
565
+ formatDevice(session.device),
566
+ String(session.toolCount),
567
+ formatAge(session.claimedAt ?? session.createdAt, nowIso)
568
+ ]);
569
+ const widths = headers.map(
570
+ (header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
571
+ );
572
+ return [
573
+ colors.green("Sessions"),
574
+ ` ${headers.map((header, index) => header.padEnd(widths[index])).join(" ")}`,
575
+ ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
576
+ ];
577
+ };
578
+ var isToolsListing = (data) => {
579
+ return typeof data === "object" && data !== null && Array.isArray(data.tools);
580
+ };
581
+ var isToolGroupsListing = (data) => {
582
+ return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
583
+ };
584
+ var UNGROUPED_LABEL = "(ungrouped)";
585
+ var MAX_FOOTER_GROUPS = 10;
586
+ var hasAnyGroup = (groups) => {
587
+ return (groups ?? []).some((entry) => entry.group !== null);
588
+ };
589
+ var topLevelGroups = (groups) => {
590
+ return (groups ?? []).filter((entry) => entry.group !== null && !entry.group.includes("/"));
591
+ };
592
+ var renderEmptyToolsLine = (data) => {
593
+ if (data.total > 0) {
594
+ return ` No tools at offset ${data.offset ?? 0}; ${data.total} matching tool${data.total === 1 ? "" : "s"} in total.`;
595
+ }
596
+ if (data.group !== void 0) {
597
+ const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
598
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
599
+ }
600
+ return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
601
+ };
602
+ var renderGroupHint = (data) => {
603
+ if (data.group !== void 0 || !hasAnyGroup(data.groups)) {
604
+ return void 0;
605
+ }
606
+ const top = topLevelGroups(data.groups);
607
+ const named = top.slice(0, MAX_FOOTER_GROUPS).map((entry) => `${entry.group} ${entry.total}`);
608
+ const more = top.length > MAX_FOOTER_GROUPS ? `, ... ${top.length - MAX_FOOTER_GROUPS} more; see --groups` : "";
609
+ return `--group <name> (groups: ${named.join(", ")}${more})`;
610
+ };
611
+ var renderTruncationLine = (data) => {
612
+ if (data.tools.length >= data.total) {
613
+ return [];
614
+ }
615
+ const groupHint = renderGroupHint(data);
616
+ const narrow = groupHint === void 0 ? "--filter <text> or" : `${groupHint} or --filter <text>, or`;
617
+ return [
618
+ "",
619
+ `Showing ${data.tools.length} of ${data.total} tools (offset ${data.offset ?? 0}). Narrow with ${narrow} page with --offset <n>.`
620
+ ];
621
+ };
622
+ var renderToolSummaryLines = (tool, indent) => {
623
+ const tag = tool.policy === "allow" ? "" : ` [${tool.policy}]`;
624
+ return [`${indent}${renderToolSignature(tool)}${tag}`, `${indent} ${summarizeToolDescription(tool.description)}`];
625
+ };
626
+ var renderGroupedToolLines = (colors, tools) => {
627
+ const byGroup = /* @__PURE__ */ new Map();
628
+ for (const tool of tools) {
629
+ const key = tool.group ?? null;
630
+ const bucket = byGroup.get(key);
631
+ if (bucket) {
632
+ bucket.push(tool);
633
+ } else {
634
+ byGroup.set(key, [tool]);
635
+ }
636
+ }
637
+ const tops = /* @__PURE__ */ new Map();
638
+ for (const key of byGroup.keys()) {
639
+ if (key === null) {
640
+ continue;
641
+ }
642
+ const slash = key.indexOf("/");
643
+ const top = slash === -1 ? key : key.slice(0, slash);
644
+ const subs = tops.get(top) ?? [];
645
+ if (slash !== -1) {
646
+ subs.push(key);
647
+ }
648
+ tops.set(top, subs);
649
+ }
650
+ const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
651
+ const lines = [];
652
+ for (const top of [...tops.keys()].sort(byCodePoint)) {
653
+ lines.push(` ${colors.cyan(top)}`);
654
+ lines.push(...(byGroup.get(top) ?? []).flatMap((tool) => renderToolSummaryLines(tool, " ")));
655
+ for (const sub of tops.get(top).sort(byCodePoint)) {
656
+ lines.push(` ${colors.cyan(sub)}`);
657
+ lines.push(...byGroup.get(sub).flatMap((tool) => renderToolSummaryLines(tool, " ")));
658
+ }
659
+ }
660
+ const ungrouped = byGroup.get(null);
661
+ if (ungrouped) {
662
+ lines.push(` ${colors.cyan(UNGROUPED_LABEL)}`);
663
+ lines.push(...ungrouped.flatMap((tool) => renderToolSummaryLines(tool, " ")));
664
+ }
665
+ return lines;
666
+ };
667
+ var renderToolsTitle = (colors, data) => {
668
+ return colors.green(data.group === void 0 ? "Tools" : `Tools in group ${data.group}`);
669
+ };
670
+ var renderToolSummaryTable = (colors, data) => {
671
+ if (data.tools.length === 0) {
672
+ return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
673
+ }
674
+ const grouped = data.group === void 0 && hasAnyGroup(data.groups);
675
+ return [
676
+ renderToolsTitle(colors, data),
677
+ ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
678
+ ...renderTruncationLine(data),
679
+ "",
680
+ "Run `appduct tools <name>` for a tool's full schema."
681
+ ];
682
+ };
683
+ var renderToolDetail = (colors, tool, flags) => {
684
+ return renderFields(
685
+ colors.green(`Tool: ${tool.name}`),
686
+ [
687
+ ["Signature", renderToolSignature(tool)],
688
+ ["Description", tool.description],
689
+ ["Input schema", tool.input_schema],
690
+ ["Output schema", tool.output_schema],
691
+ // `?? undefined` keeps an ungrouped tool's row hidden rather than printing a bare "null":
692
+ // `renderFields` drops undefined rows, and the machine form's `null` is for consumers, not
693
+ // for a human scanning one tool's schema.
694
+ ["Group", tool.group ?? void 0],
695
+ ["Annotations", tool.annotations],
696
+ // Only rendered for a tool that declares one; `renderFields` drops undefined rows, so a
697
+ // tool on the daemon's default deadline shows no line at all rather than a misleading
698
+ // "10000".
699
+ ["Timeout (ms)", tool.timeout_ms]
700
+ ],
701
+ flags
702
+ );
703
+ };
704
+ var renderToolsFullListing = (colors, data, flags) => {
705
+ if (data.tools.length === 0) {
706
+ return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
707
+ }
708
+ return [
709
+ ...data.tools.flatMap((tool, index) => [
710
+ ...index > 0 ? [""] : [],
711
+ ...renderToolDetail(colors, tool, flags)
712
+ ]),
713
+ ...renderTruncationLine(data)
714
+ ];
715
+ };
716
+ var renderToolGroups = (colors, data) => {
717
+ if (data.groups.length === 0) {
718
+ return [colors.green("Groups"), " No tools registered."];
719
+ }
720
+ const label = (entry) => {
721
+ if (entry.group === null) {
722
+ return UNGROUPED_LABEL;
723
+ }
724
+ return entry.group.includes("/") ? ` ${entry.group}` : entry.group;
725
+ };
726
+ const width = Math.max(...data.groups.map((entry) => label(entry).length));
727
+ const countWidth = Math.max(...data.groups.map((entry) => String(entry.total).length));
728
+ return [
729
+ colors.green("Groups"),
730
+ ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
731
+ "",
732
+ `${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
733
+ ];
734
+ };
735
+ var renderToolsData = (colors, data, flags, full) => {
736
+ if (isToolGroupsListing(data)) {
737
+ return renderToolGroups(colors, data);
738
+ }
739
+ if (!isToolsListing(data)) {
740
+ return renderToolDetail(colors, data, flags);
741
+ }
742
+ return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
743
+ };
744
+ var renderInvokeData = (colors, data, flags) => {
745
+ return [colors.green("Result"), formatScalar(data, flags)];
746
+ };
747
+ var renderLinkData = (colors, data, flags, qr) => {
748
+ const lines = [
749
+ colors.green("Link Created"),
750
+ ...renderFields(
751
+ "Link",
752
+ [
753
+ ["Session", data.sessionId],
754
+ ["Deep link", data.deepLink],
755
+ ["Endpoint", formatAgentWebSocketUrl(data.endpoint)],
756
+ ["Pin", data.pin],
757
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()],
758
+ ["Delivered", data.delivered ? `yes (${data.target})` : void 0]
759
+ ],
760
+ flags
761
+ )
762
+ ];
763
+ if (qr) {
764
+ lines.push("", "QR", renderQrToTerminal(data.deepLink));
765
+ }
766
+ return lines;
767
+ };
768
+ var renderRevokeData = (colors, _data) => {
769
+ return [colors.green("Session Revoked")];
770
+ };
771
+ var renderKeygenData = (colors, data, flags) => {
772
+ return [
773
+ colors.green("Key Ready"),
774
+ ...renderFields(
775
+ "Key",
776
+ [
777
+ ["Path", data.path],
778
+ ["Fingerprint", data.pin]
779
+ ],
780
+ flags
781
+ )
782
+ ];
783
+ };
784
+ var renderInitData = (colors, data, flags) => {
785
+ const snippet = JSON.stringify(
786
+ { mcpServers: { appduct: data.mcpServerEntry } },
787
+ null,
788
+ 2
789
+ );
790
+ return [
791
+ colors.green(data.changed ? "Project Initialized" : "Project Already Initialized"),
792
+ ...renderFields(
793
+ "Config",
794
+ [
795
+ ["Path", data.path],
796
+ ["Scheme", data.scheme],
797
+ ["Source", data.source],
798
+ // Only present for a scheme discovery read off a static project file (app.json, or one of
799
+ // the native Android/iOS probes) — names the exact file/key, not just which platform.
800
+ ["Read from", data.origin],
801
+ ["Written", data.changed ? data.created ? "created" : "updated" : "unchanged"]
802
+ ],
803
+ flags
804
+ ),
805
+ ...data.note === void 0 ? [] : ["", colors.yellow(`Note: ${data.note}`)],
806
+ "",
807
+ "MCP server entry",
808
+ ...snippet.split("\n").map((line) => ` ${line}`),
809
+ "",
810
+ "Next",
811
+ ...data.nextSteps.map((step, index) => ` ${index + 1}. ${step}`)
812
+ ];
813
+ };
814
+ var renderDaemonRunData = (colors, data, flags) => {
815
+ return [
816
+ colors.green("Daemon Running"),
817
+ ...renderFields(
818
+ "Daemon",
819
+ [
820
+ ["PID", data.daemon.pid],
821
+ ["State dir", data.daemon.state_dir],
822
+ ["Socket", data.daemon.socket_path]
823
+ ],
824
+ flags
825
+ )
826
+ ];
827
+ };
828
+ var renderDaemonStartData = (colors, data, flags) => {
829
+ return [
830
+ colors.green("Daemon Started"),
831
+ ...renderFields(
832
+ "Daemon",
833
+ [
834
+ ["PID", data.daemon.pid],
835
+ ["WSS port", data.daemon.wss_port],
836
+ ["Started at", data.daemon.started_at]
837
+ ],
838
+ flags
839
+ )
840
+ ];
841
+ };
842
+ var renderDaemonStopData = (colors, data, flags) => {
843
+ return [
844
+ colors.green("Daemon Stopped"),
845
+ ...renderFields("Daemon", [["Method", data.daemon.method]], flags)
846
+ ];
847
+ };
848
+ var formatByteSize = (bytes) => {
849
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
850
+ let value = bytes;
851
+ let unit = 0;
852
+ while (value >= 1024 && unit < units.length - 1) {
853
+ value /= 1024;
854
+ unit += 1;
855
+ }
856
+ return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
857
+ };
858
+ var renderDaemonStatusData = (colors, data, flags) => {
859
+ return [
860
+ colors.green("Daemon Status"),
861
+ ...renderFields(
862
+ "Daemon",
863
+ [
864
+ ["Version", data.daemon.version],
865
+ ["PID", data.daemon.pid],
866
+ ["Started at", data.daemon.started_at],
867
+ ["WSS port", data.daemon.wss_port],
868
+ ["Pinned keys", data.daemon.pinned_keys.length],
869
+ ["Sessions", data.daemon.session_count]
870
+ ],
871
+ flags
872
+ ),
873
+ "",
874
+ ...renderFields(
875
+ "Policy",
876
+ [
877
+ ["Default", data.policy.default],
878
+ ["Destructive", data.policy.destructive],
879
+ ["Overrides", data.policy.tools ? Object.keys(data.policy.tools).length : 0]
880
+ ],
881
+ flags
882
+ ),
883
+ "",
884
+ // The retention rows drop out entirely against a daemon that predates them (`renderFields`
885
+ // skips `undefined`), rather than printing "undefined" or an invented zero.
886
+ ...renderFields(
887
+ "Audit",
888
+ [
889
+ ["Path", data.audit.path],
890
+ ["Retention", data.audit.retention_days === void 0 ? void 0 : `${data.audit.retention_days} days`],
891
+ ["Files", data.audit.files],
892
+ ["Size", data.audit.bytes === void 0 ? void 0 : formatByteSize(data.audit.bytes)],
893
+ ["Failed writes", data.audit.failed_writes],
894
+ ["Failed prunes", data.audit.failed_prunes]
895
+ ],
896
+ flags
897
+ ),
898
+ // Version drift (issue #30) is the one thing here an operator has to act on, so it goes last
899
+ // — the line still on screen after the block scrolls — and in yellow, not a quiet field row.
900
+ ...data.warning ? ["", colors.yellow(`Warning: ${data.warning}`)] : []
901
+ ];
902
+ };
903
+ var renderDoctorData = (colors, data, flags) => {
904
+ return [
905
+ data.present ? colors.green("Appduct Present") : colors.yellow("Appduct Absent"),
906
+ ...renderFields(
907
+ "Artifact",
908
+ [
909
+ ["Path", data.artifact],
910
+ ["Platform", data.platform],
911
+ ["Format", data.format],
912
+ ["Present", data.present],
913
+ ["Signals", data.signals.length > 0 ? data.signals.join(", ") : "none"],
914
+ ["Assertion", data.assertion ? `${data.assertion.expected} (holds)` : void 0]
915
+ ],
916
+ flags
917
+ )
918
+ ];
919
+ };
920
+ var renderSuccessData = (colors, command, data, options) => {
921
+ const flags = options.flags;
922
+ switch (command) {
923
+ case "init":
924
+ return renderInitData(colors, data, flags);
925
+ case "keygen":
926
+ return renderKeygenData(colors, data, flags);
927
+ case "link":
928
+ return renderLinkData(colors, data, flags, options.qr);
929
+ case "ls":
930
+ return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
931
+ case "tools":
932
+ return renderToolsData(colors, data, flags, options.full);
933
+ case "invoke":
934
+ return renderInvokeData(colors, data, flags);
935
+ case "revoke":
936
+ return renderRevokeData(colors, data);
937
+ case "daemon run":
938
+ return renderDaemonRunData(colors, data, flags);
939
+ case "daemon start":
940
+ return renderDaemonStartData(colors, data, flags);
941
+ case "daemon stop":
942
+ return renderDaemonStopData(colors, data, flags);
943
+ case "daemon status":
944
+ return renderDaemonStatusData(colors, data, flags);
945
+ case "doctor":
946
+ return renderDoctorData(colors, data, flags);
947
+ default:
948
+ return [colors.green("Command Complete"), formatJson(data, flags)];
949
+ }
950
+ };
951
+ var renderHumanError = (colors, error, flags, meta) => {
952
+ return [
953
+ colors.red("Command Failed"),
954
+ ...renderFields(
955
+ "Error",
956
+ [
957
+ ["Type", error.type],
958
+ ["Message", error.message],
959
+ ["Details", error.details]
960
+ ],
961
+ flags
962
+ ),
963
+ ...meta ? ["", ...renderMetaLines(meta)] : []
964
+ ].join("\n");
965
+ };
966
+ var renderResult = (result, options) => {
967
+ if (options.flags.json) {
968
+ return {
969
+ stdout: `${formatJson(result, options.flags)}
970
+ `
971
+ };
972
+ }
973
+ const colors = pc.createColors(options.flags.color);
974
+ if (!result.ok) {
975
+ return {
976
+ stderr: `${renderHumanError(colors, result.error, options.flags, result.meta)}
977
+ `
978
+ };
979
+ }
980
+ const lines = [
981
+ ...renderSuccessData(colors, options.command, result.data, options),
982
+ "",
983
+ ...renderMetaLines(result.meta)
984
+ ].filter((line, index, collection) => {
985
+ if (line !== "") {
986
+ return true;
987
+ }
988
+ return index > 0 && collection[index - 1] !== "" && index < collection.length - 1;
989
+ });
990
+ return {
991
+ stdout: `${lines.join("\n")}
992
+ `
993
+ };
994
+ };
995
+
996
+ // src/cli/envelope.ts
997
+ var createCommandMeta = (command, startedAt, finishedAt) => {
998
+ return {
999
+ command,
1000
+ timestamp: finishedAt.toISOString(),
1001
+ duration_ms: finishedAt.getTime() - startedAt.getTime()
1002
+ };
1003
+ };
1004
+ var finalizeResult = (result, timing, flags) => {
1005
+ if (!flags.verbose) {
1006
+ return result;
1007
+ }
1008
+ return {
1009
+ ...result,
1010
+ meta: createCommandMeta(timing.command, timing.startedAt, timing.finishedAt)
1011
+ };
1012
+ };
1013
+
1014
+ // src/cli/runner.ts
1015
+ var writeRenderedOutput = (rendered, writers) => {
1016
+ if (rendered.stdout) {
1017
+ writers.stdout.write(rendered.stdout);
1018
+ }
1019
+ if (rendered.stderr) {
1020
+ writers.stderr.write(rendered.stderr);
1021
+ }
1022
+ };
1023
+ var executeCommand = async (command, handler, env, render = {}) => {
1024
+ const startedAt = env.clock.now();
1025
+ try {
1026
+ const result = await handler();
1027
+ const finishedAt = env.clock.now();
1028
+ const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
1029
+ const renderOptions = {
1030
+ command,
1031
+ flags: env.flags,
1032
+ now: finishedAt,
1033
+ qr: render.qr,
1034
+ full: render.full
1035
+ };
1036
+ writeRenderedOutput(renderResult(finalized, renderOptions), env);
1037
+ return 0;
1038
+ } catch (error) {
1039
+ const finishedAt = env.clock.now();
1040
+ const cliError = toCliError(error);
1041
+ const result = { ok: false, error: cliError };
1042
+ const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
1043
+ const renderOptions = {
1044
+ command,
1045
+ flags: env.flags,
1046
+ now: finishedAt,
1047
+ qr: render.qr,
1048
+ full: render.full
1049
+ };
1050
+ writeRenderedOutput(renderResult(finalized, renderOptions), env);
1051
+ return getExitCodeForError(error);
1052
+ }
1053
+ };
1054
+
1055
+ // src/cli/router.ts
1056
+ var commandName = (context) => {
1057
+ return context.path.join(" ");
1058
+ };
1059
+
1060
+ // src/cli/routes/doctor.ts
1061
+ var route = async (context) => {
1062
+ const { options } = context;
1063
+ return executeCommand(
1064
+ commandName(context),
1065
+ () => handleDoctorCommand({
1066
+ artifactPath: context.args[0],
1067
+ assertPresent: Boolean(options.assertPresent),
1068
+ assertAbsent: Boolean(options.assertAbsent)
1069
+ }),
1070
+ context.env
1071
+ );
1072
+ };
1073
+ export {
1074
+ route
1075
+ };