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,1860 @@
1
+ // src/cli/routes/tools.ts
2
+ import { isValidToolGroup } from "@appduct/shared";
3
+
4
+ // src/commands/tools.ts
5
+ import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
6
+
7
+ // src/rpc/client.ts
8
+ import { connect as connect2 } from "node:net";
9
+ import { spawn as spawnChildProcess } from "node:child_process";
10
+ import { open as open2, readFile as readFile3, rm as rm2, stat as stat2 } from "node:fs/promises";
11
+ import { join as join3 } from "node:path";
12
+ import { RPC_METHODS } from "@appduct/shared";
13
+
14
+ // src/daemon/log-rotation.ts
15
+ import { chmod as chmod2, rename, stat } from "node:fs/promises";
16
+
17
+ // src/daemon/config.ts
18
+ import { readFile } from "node:fs/promises";
19
+ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
20
+ "wssPort",
21
+ "keyPath",
22
+ "graceSeconds",
23
+ "linkTtlSeconds",
24
+ "keepaliveIntervalSeconds",
25
+ "eventBufferSize",
26
+ "auditRetentionDays",
27
+ "daemonLogMaxBytes",
28
+ "policy",
29
+ "advertisedIp",
30
+ "scheme",
31
+ "restartDaemonOnVersionMismatch"
32
+ ]);
33
+ var KNOWN_POLICY_KEYS = /* @__PURE__ */ new Set(["default", "destructive", "tools"]);
34
+ var POLICY_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "prompt"]);
35
+ var DEFAULT_AUDIT_RETENTION_DAYS = 30;
36
+ var DEFAULT_DAEMON_LOG_MAX_BYTES = 10 * 1024 * 1024;
37
+ var AppductConfigError = class extends Error {
38
+ constructor(key, message) {
39
+ super(message);
40
+ this.key = key;
41
+ this.name = "AppductConfigError";
42
+ }
43
+ };
44
+ var configError = (key, message) => {
45
+ return new AppductConfigError(key, `Invalid Appduct config value for "${key}": ${message}`);
46
+ };
47
+ var requireWssPort = (value, key) => {
48
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) {
49
+ throw configError(key, "must be a port number between 0 and 65535 (0 binds an OS-assigned port).");
50
+ }
51
+ return value;
52
+ };
53
+ var requirePositiveInteger = (value, key) => {
54
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
55
+ throw configError(key, "must be a positive integer.");
56
+ }
57
+ return value;
58
+ };
59
+ var requireNonEmptyString = (value, key) => {
60
+ if (typeof value !== "string" || value.length === 0) {
61
+ throw configError(key, "must be a non-empty string.");
62
+ }
63
+ return value;
64
+ };
65
+ var requireBoolean = (value, key) => {
66
+ if (typeof value !== "boolean") {
67
+ throw configError(key, "must be a boolean.");
68
+ }
69
+ return value;
70
+ };
71
+ var requirePolicyDecision = (value, key) => {
72
+ if (typeof value !== "string" || !POLICY_DECISIONS.has(value)) {
73
+ throw configError(key, 'must be "allow", "deny", or "prompt".');
74
+ }
75
+ return value;
76
+ };
77
+ var defaultConfig = (paths) => {
78
+ return {
79
+ wssPort: 8443,
80
+ keyPath: paths.keyPath,
81
+ graceSeconds: 600,
82
+ linkTtlSeconds: 300,
83
+ keepaliveIntervalSeconds: 15,
84
+ eventBufferSize: 256,
85
+ auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
86
+ daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
87
+ restartDaemonOnVersionMismatch: false,
88
+ policy: {
89
+ default: "allow",
90
+ destructive: "allow"
91
+ }
92
+ };
93
+ };
94
+ var loadConfig = async (paths, options = {}) => {
95
+ const warn = options.warn ?? (() => {
96
+ });
97
+ const config = defaultConfig(paths);
98
+ let raw;
99
+ try {
100
+ raw = JSON.parse(await readFile(paths.configPath, "utf8"));
101
+ } catch (error) {
102
+ if (error.code === "ENOENT") {
103
+ return config;
104
+ }
105
+ throw configError("config.json", `could not be read/parsed (${error.message}).`);
106
+ }
107
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
108
+ throw configError("config.json", "must be a JSON object.");
109
+ }
110
+ const parsed = raw;
111
+ for (const key of Object.keys(parsed)) {
112
+ if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
113
+ warn(`Unknown Appduct config key "${key}" is ignored.`);
114
+ }
115
+ }
116
+ if (parsed.wssPort !== void 0) {
117
+ config.wssPort = requireWssPort(parsed.wssPort, "wssPort");
118
+ }
119
+ if (parsed.keyPath !== void 0) {
120
+ config.keyPath = requireNonEmptyString(parsed.keyPath, "keyPath");
121
+ }
122
+ if (parsed.advertisedIp !== void 0) {
123
+ config.advertisedIp = requireNonEmptyString(parsed.advertisedIp, "advertisedIp");
124
+ }
125
+ if (parsed.scheme !== void 0) {
126
+ config.scheme = requireNonEmptyString(parsed.scheme, "scheme");
127
+ }
128
+ if (parsed.graceSeconds !== void 0) {
129
+ config.graceSeconds = requirePositiveInteger(parsed.graceSeconds, "graceSeconds");
130
+ }
131
+ if (parsed.linkTtlSeconds !== void 0) {
132
+ config.linkTtlSeconds = requirePositiveInteger(parsed.linkTtlSeconds, "linkTtlSeconds");
133
+ }
134
+ if (parsed.keepaliveIntervalSeconds !== void 0) {
135
+ config.keepaliveIntervalSeconds = requirePositiveInteger(
136
+ parsed.keepaliveIntervalSeconds,
137
+ "keepaliveIntervalSeconds"
138
+ );
139
+ }
140
+ if (parsed.eventBufferSize !== void 0) {
141
+ config.eventBufferSize = requirePositiveInteger(parsed.eventBufferSize, "eventBufferSize");
142
+ }
143
+ if (parsed.auditRetentionDays !== void 0) {
144
+ config.auditRetentionDays = requirePositiveInteger(parsed.auditRetentionDays, "auditRetentionDays");
145
+ }
146
+ if (parsed.daemonLogMaxBytes !== void 0) {
147
+ config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
148
+ }
149
+ if (parsed.restartDaemonOnVersionMismatch !== void 0) {
150
+ config.restartDaemonOnVersionMismatch = requireBoolean(
151
+ parsed.restartDaemonOnVersionMismatch,
152
+ "restartDaemonOnVersionMismatch"
153
+ );
154
+ }
155
+ if (parsed.policy !== void 0) {
156
+ if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
157
+ throw configError("policy", "must be an object.");
158
+ }
159
+ const policy = parsed.policy;
160
+ for (const key of Object.keys(policy)) {
161
+ if (!KNOWN_POLICY_KEYS.has(key)) {
162
+ warn(`Unknown Appduct config key "policy.${key}" is ignored.`);
163
+ }
164
+ }
165
+ if (policy.default !== void 0) {
166
+ config.policy.default = requirePolicyDecision(policy.default, "policy.default");
167
+ }
168
+ if (policy.destructive !== void 0) {
169
+ config.policy.destructive = requirePolicyDecision(policy.destructive, "policy.destructive");
170
+ }
171
+ if (policy.tools !== void 0) {
172
+ if (typeof policy.tools !== "object" || policy.tools === null || Array.isArray(policy.tools)) {
173
+ throw configError("policy.tools", "must be an object.");
174
+ }
175
+ const tools = {};
176
+ for (const [toolKey, toolValue] of Object.entries(policy.tools)) {
177
+ tools[toolKey] = requirePolicyDecision(toolValue, `policy.tools["${toolKey}"]`);
178
+ }
179
+ config.policy.tools = tools;
180
+ }
181
+ }
182
+ return config;
183
+ };
184
+
185
+ // src/daemon/pidfile.ts
186
+ import { readFileSync } from "node:fs";
187
+ import { open, readFile as readFile2, rm } from "node:fs/promises";
188
+ var readProcStatus = (pid) => {
189
+ try {
190
+ return readFileSync(`/proc/${pid}/status`, "utf8");
191
+ } catch {
192
+ return void 0;
193
+ }
194
+ };
195
+ var isZombie = (pid, readStatus) => {
196
+ const status = readStatus(pid);
197
+ if (status === void 0) {
198
+ return false;
199
+ }
200
+ return /^State:\s*Z\b/mu.test(status);
201
+ };
202
+ var isProcessAlive = (pid, readStatus = readProcStatus) => {
203
+ try {
204
+ process.kill(pid, 0);
205
+ } catch (error) {
206
+ return error.code === "EPERM";
207
+ }
208
+ return !isZombie(pid, readStatus);
209
+ };
210
+ var readPidFromFile = async (pidFilePath) => {
211
+ let contents;
212
+ try {
213
+ contents = await readFile2(pidFilePath, "utf8");
214
+ } catch (error) {
215
+ if (error.code === "ENOENT") {
216
+ return void 0;
217
+ }
218
+ throw error;
219
+ }
220
+ const pid = Number.parseInt(contents.trim(), 10);
221
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
222
+ };
223
+
224
+ // src/daemon/socket-probe.ts
225
+ import { connect } from "node:net";
226
+ var isSocketConnectable = (socketPath) => {
227
+ return new Promise((resolve) => {
228
+ const socket = connect(socketPath);
229
+ socket.once("connect", () => {
230
+ socket.destroy();
231
+ resolve(true);
232
+ });
233
+ socket.once("error", () => {
234
+ socket.destroy();
235
+ resolve(false);
236
+ });
237
+ });
238
+ };
239
+
240
+ // src/daemon/state-dir.ts
241
+ import { join } from "node:path";
242
+ import { mkdir, chmod } from "node:fs/promises";
243
+ var getStateDirPaths = (stateDir) => {
244
+ return {
245
+ root: stateDir,
246
+ socketPath: join(stateDir, "daemon.sock"),
247
+ pidFilePath: join(stateDir, "daemon.pid"),
248
+ logFilePath: join(stateDir, "daemon.log"),
249
+ keyPath: join(stateDir, "key.pem"),
250
+ configPath: join(stateDir, "config.json"),
251
+ auditDir: join(stateDir, "audit"),
252
+ spawnLockPath: join(stateDir, "daemon.spawn.lock")
253
+ };
254
+ };
255
+ var ensureStateDir = async (stateDir) => {
256
+ const paths = getStateDirPaths(stateDir);
257
+ await mkdir(paths.auditDir, { recursive: true });
258
+ await chmod(paths.root, 448);
259
+ await chmod(paths.auditDir, 448);
260
+ return paths;
261
+ };
262
+
263
+ // src/daemon/log-rotation.ts
264
+ var STATE_FILE_MODE = 384;
265
+ var daemonLogBackupPath = (logFilePath) => `${logFilePath}.1`;
266
+ var isDaemonHoldingLog = async (pidFilePath) => {
267
+ try {
268
+ const pid = await readPidFromFile(pidFilePath);
269
+ return pid !== void 0 && isProcessAlive(pid);
270
+ } catch {
271
+ return false;
272
+ }
273
+ };
274
+ var rotateDaemonLogIfNeeded = async (options) => {
275
+ const warn = options.warn ?? ((message) => {
276
+ process.stderr.write(`${message}
277
+ `);
278
+ });
279
+ try {
280
+ const stats = await stat(options.logFilePath);
281
+ if (!stats.isFile() || stats.size <= options.maxBytes) {
282
+ return { rotated: false };
283
+ }
284
+ const held = options.pidFilePath !== void 0 && await isDaemonHoldingLog(options.pidFilePath) || options.socketPath !== void 0 && await isSocketConnectable(options.socketPath);
285
+ if (held) {
286
+ warn(
287
+ `appduct: "${options.logFilePath}" is over its size cap but a daemon still holds it; leaving it in place.`
288
+ );
289
+ return { rotated: false, skipped: "daemon_running" };
290
+ }
291
+ const backupPath = daemonLogBackupPath(options.logFilePath);
292
+ await rename(options.logFilePath, backupPath);
293
+ try {
294
+ await chmod2(backupPath, STATE_FILE_MODE);
295
+ } catch (error) {
296
+ warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
297
+ }
298
+ return { rotated: true, rotatedBytes: stats.size };
299
+ } catch (error) {
300
+ if (error.code === "ENOENT") {
301
+ return { rotated: false };
302
+ }
303
+ warn(`appduct: failed to rotate "${options.logFilePath}": ${error.message}`);
304
+ return { rotated: false };
305
+ }
306
+ };
307
+ var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
308
+ `)) => {
309
+ try {
310
+ await chmod2(logFilePath, STATE_FILE_MODE);
311
+ return true;
312
+ } catch (error) {
313
+ warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
314
+ return false;
315
+ }
316
+ };
317
+ var resolveDaemonLogMaxBytes = async (stateDir) => {
318
+ try {
319
+ return (await loadConfig(getStateDirPaths(stateDir))).daemonLogMaxBytes;
320
+ } catch {
321
+ return DEFAULT_DAEMON_LOG_MAX_BYTES;
322
+ }
323
+ };
324
+
325
+ // src/package-root.ts
326
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
327
+ import { dirname, join as join2 } from "node:path";
328
+ import { fileURLToPath } from "node:url";
329
+ var PACKAGE_NAME = "appduct";
330
+ var cached;
331
+ var getPackageRoot = () => {
332
+ if (cached !== void 0) {
333
+ return cached;
334
+ }
335
+ let dir = dirname(fileURLToPath(import.meta.url));
336
+ for (; ; ) {
337
+ const candidate = join2(dir, "package.json");
338
+ if (existsSync(candidate)) {
339
+ let name;
340
+ try {
341
+ name = JSON.parse(readFileSync2(candidate, "utf8")).name;
342
+ } catch {
343
+ }
344
+ if (name === PACKAGE_NAME) {
345
+ cached = dir;
346
+ return dir;
347
+ }
348
+ }
349
+ const parent = dirname(dir);
350
+ if (parent === dir) {
351
+ throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
352
+ }
353
+ dir = parent;
354
+ }
355
+ };
356
+
357
+ // src/package-version.ts
358
+ var DAEMON_VERSION_OVERRIDE_ENV = "APPDUCT_DAEMON_VERSION_OVERRIDE";
359
+
360
+ // src/rpc/client.ts
361
+ var defaultBinPath = join3(getPackageRoot(), "bin.js");
362
+ var DaemonRpcError = class extends Error {
363
+ constructor(code, message, data) {
364
+ super(message);
365
+ this.code = code;
366
+ this.data = data;
367
+ this.name = "DaemonRpcError";
368
+ }
369
+ };
370
+ var DaemonUnavailableError = class extends Error {
371
+ constructor(message, reason = "unreachable") {
372
+ super(message);
373
+ this.reason = reason;
374
+ this.name = "DaemonUnavailableError";
375
+ }
376
+ };
377
+ var isRequestTimeoutError = (error) => {
378
+ return error instanceof DaemonUnavailableError && error.reason === "timeout";
379
+ };
380
+ var describeLiveState = (state) => {
381
+ const parts = [];
382
+ if (state.sessions > 0) {
383
+ parts.push(`${state.sessions} connected session(s)`);
384
+ }
385
+ if (state.pendingLinks > 0) {
386
+ parts.push(`${state.pendingLinks} unclaimed link(s)`);
387
+ }
388
+ return parts.join(" and ");
389
+ };
390
+ var DaemonVersionMismatchError = class extends Error {
391
+ constructor(daemonVersion, clientVersion, live, message) {
392
+ super(
393
+ 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".`
394
+ );
395
+ this.daemonVersion = daemonVersion;
396
+ this.clientVersion = clientVersion;
397
+ this.name = "DaemonVersionMismatchError";
398
+ this.sessionCount = live.sessions;
399
+ this.pendingLinkCount = live.pendingLinks;
400
+ }
401
+ sessionCount;
402
+ pendingLinkCount;
403
+ };
404
+ var DaemonVersionRestartIneffectiveError = class extends DaemonVersionMismatchError {
405
+ constructor(daemonVersion, clientVersion, live) {
406
+ super(
407
+ daemonVersion,
408
+ clientVersion,
409
+ live,
410
+ `The Appduct daemon was restarted to match this client (version ${clientVersion}), but the daemon now answering still reports version ${daemonVersion}. Either the old daemon never exited (compare "daemon.pid" with what "appduct daemon status" reports), or another process spawned an older daemon in the meantime. Run "appduct daemon stop", then retry.`
411
+ );
412
+ this.name = "DaemonVersionRestartIneffectiveError";
413
+ }
414
+ };
415
+ var parseVersion = (value) => {
416
+ const match = /^(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
417
+ if (!match) {
418
+ return void 0;
419
+ }
420
+ return {
421
+ numbers: match[1].split(".").map(Number),
422
+ // Build metadata (`+…`) is dropped, per semver: it never affects precedence, so two versions
423
+ // differing only there are the same version and neither restarts nor warns about the other.
424
+ prerelease: match[2]?.split(".")
425
+ };
426
+ };
427
+ var isNumericIdentifier = (identifier) => /^\d+$/u.test(identifier);
428
+ var comparePrerelease = (left, right) => {
429
+ const length = Math.max(left.length, right.length);
430
+ for (let index = 0; index < length; index += 1) {
431
+ const a = left[index];
432
+ const b = right[index];
433
+ if (a === void 0) {
434
+ return -1;
435
+ }
436
+ if (b === void 0) {
437
+ return 1;
438
+ }
439
+ if (a === b) {
440
+ continue;
441
+ }
442
+ const aNumeric = isNumericIdentifier(a);
443
+ const bNumeric = isNumericIdentifier(b);
444
+ if (aNumeric && bNumeric) {
445
+ return Number(a) < Number(b) ? -1 : 1;
446
+ }
447
+ if (aNumeric !== bNumeric) {
448
+ return aNumeric ? -1 : 1;
449
+ }
450
+ return a < b ? -1 : 1;
451
+ }
452
+ return 0;
453
+ };
454
+ var compareVersions = (a, b) => {
455
+ const left = parseVersion(a);
456
+ const right = parseVersion(b);
457
+ if (!left || !right) {
458
+ return void 0;
459
+ }
460
+ const length = Math.max(left.numbers.length, right.numbers.length);
461
+ for (let index = 0; index < length; index += 1) {
462
+ const difference = (left.numbers[index] ?? 0) - (right.numbers[index] ?? 0);
463
+ if (difference !== 0) {
464
+ return difference < 0 ? -1 : 1;
465
+ }
466
+ }
467
+ if (left.prerelease === void 0 && right.prerelease === void 0) {
468
+ return 0;
469
+ }
470
+ if (left.prerelease === void 0) {
471
+ return 1;
472
+ }
473
+ if (right.prerelease === void 0) {
474
+ return -1;
475
+ }
476
+ return comparePrerelease(left.prerelease, right.prerelease);
477
+ };
478
+ var isSameVersion = (a, b) => {
479
+ return a === b || compareVersions(a, b) === 0;
480
+ };
481
+ var isConnectionMissingError = (error) => {
482
+ const code = error.code;
483
+ return code === "ENOENT" || code === "ECONNREFUSED";
484
+ };
485
+ var isDaemonUnreachableError = (error) => {
486
+ return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
487
+ };
488
+ var nextRequestId = 1;
489
+ var sendRequest = (socketPath, method, params, timeoutMs) => {
490
+ return new Promise((resolve, reject) => {
491
+ const id = nextRequestId;
492
+ nextRequestId += 1;
493
+ let socket;
494
+ let settled = false;
495
+ let buffer = "";
496
+ const finish = (fn) => {
497
+ if (settled) {
498
+ return;
499
+ }
500
+ settled = true;
501
+ clearTimeout(timeout);
502
+ socket.destroy();
503
+ fn();
504
+ };
505
+ const timeout = setTimeout(() => {
506
+ finish(
507
+ () => reject(new DaemonUnavailableError(`Timed out waiting for a response to "${method}".`, "timeout"))
508
+ );
509
+ }, timeoutMs);
510
+ socket = connect2(socketPath);
511
+ socket.on("error", (error) => {
512
+ finish(() => reject(error));
513
+ });
514
+ socket.on("connect", () => {
515
+ socket.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
516
+ `);
517
+ });
518
+ socket.on("data", (chunk) => {
519
+ buffer += chunk.toString("utf8");
520
+ let newlineIndex = buffer.indexOf("\n");
521
+ while (newlineIndex !== -1) {
522
+ const line = buffer.slice(0, newlineIndex);
523
+ buffer = buffer.slice(newlineIndex + 1);
524
+ newlineIndex = buffer.indexOf("\n");
525
+ if (line.length === 0) {
526
+ continue;
527
+ }
528
+ let message;
529
+ try {
530
+ message = JSON.parse(line);
531
+ } catch {
532
+ continue;
533
+ }
534
+ if (message.id !== id) {
535
+ continue;
536
+ }
537
+ if (message.error) {
538
+ finish(
539
+ () => reject(new DaemonRpcError(message.error.code, message.error.message, message.error.data))
540
+ );
541
+ return;
542
+ }
543
+ finish(() => resolve(message.result));
544
+ return;
545
+ }
546
+ });
547
+ socket.on("close", () => {
548
+ finish(
549
+ () => reject(new DaemonUnavailableError("Connection to the Appduct daemon closed.", "closed"))
550
+ );
551
+ });
552
+ });
553
+ };
554
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
555
+ var pollForSocket = async (socketPath, timeoutMs, pollIntervalMs) => {
556
+ const deadline = Date.now() + timeoutMs;
557
+ while (Date.now() < deadline) {
558
+ if (await isSocketConnectable(socketPath)) {
559
+ return true;
560
+ }
561
+ await sleep(pollIntervalMs);
562
+ }
563
+ return isSocketConnectable(socketPath);
564
+ };
565
+ var unlinkStaleSocketIfDaemonDead = async (paths) => {
566
+ let pid;
567
+ try {
568
+ pid = Number.parseInt((await readFile3(paths.pidFilePath, "utf8")).trim(), 10);
569
+ } catch {
570
+ pid = void 0;
571
+ }
572
+ if (pid === void 0 || Number.isNaN(pid) || !isProcessAlive(pid)) {
573
+ await rm2(paths.socketPath, { force: true });
574
+ }
575
+ };
576
+ var defaultSpawn = async (args, context) => {
577
+ const paths = getStateDirPaths(context.stateDir);
578
+ await rotateDaemonLogIfNeeded({
579
+ logFilePath: context.logFilePath,
580
+ maxBytes: await resolveDaemonLogMaxBytes(context.stateDir),
581
+ pidFilePath: paths.pidFilePath,
582
+ socketPath: paths.socketPath
583
+ });
584
+ const logFd = await open2(context.logFilePath, "a", 384);
585
+ try {
586
+ await ensureDaemonLogMode(context.logFilePath);
587
+ const childEnv = { ...process.env, APPDUCT_STATE_DIR: context.stateDir };
588
+ delete childEnv[DAEMON_VERSION_OVERRIDE_ENV];
589
+ const child = spawnChildProcess(process.execPath, [defaultBinPath, ...args], {
590
+ detached: true,
591
+ stdio: ["ignore", logFd.fd, logFd.fd],
592
+ env: childEnv
593
+ });
594
+ child.unref();
595
+ } finally {
596
+ await logFd.close();
597
+ }
598
+ };
599
+ var SPAWN_LOCK_STALE_MS = 3e4;
600
+ var pollUntilLockReleased = async (lockPath, timeoutMs, pollIntervalMs) => {
601
+ const deadline = Date.now() + timeoutMs;
602
+ while (Date.now() < deadline) {
603
+ if (!await pathExists(lockPath)) {
604
+ return true;
605
+ }
606
+ await sleep(pollIntervalMs);
607
+ }
608
+ return !await pathExists(lockPath);
609
+ };
610
+ var readPidfilePid = async (paths) => {
611
+ try {
612
+ const pid = Number.parseInt((await readFile3(paths.pidFilePath, "utf8")).trim(), 10);
613
+ return Number.isNaN(pid) ? void 0 : pid;
614
+ } catch {
615
+ return void 0;
616
+ }
617
+ };
618
+ var pathExists = async (target) => {
619
+ try {
620
+ await stat2(target);
621
+ return true;
622
+ } catch {
623
+ return false;
624
+ }
625
+ };
626
+ var acquireSpawnLock = async (lockPath) => {
627
+ for (let attempt = 0; attempt < 2; attempt += 1) {
628
+ try {
629
+ const lockHandle = await open2(lockPath, "wx", 384);
630
+ await lockHandle.close();
631
+ return true;
632
+ } catch (error) {
633
+ if (error.code !== "EEXIST") {
634
+ throw error;
635
+ }
636
+ }
637
+ let heldForMs;
638
+ try {
639
+ heldForMs = Date.now() - (await stat2(lockPath)).mtimeMs;
640
+ } catch {
641
+ continue;
642
+ }
643
+ if (heldForMs < SPAWN_LOCK_STALE_MS) {
644
+ return false;
645
+ }
646
+ await rm2(lockPath, { force: true });
647
+ }
648
+ return false;
649
+ };
650
+ var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
651
+ await ensureStateDir(paths.root);
652
+ const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
653
+ let lockWaitTimedOut = false;
654
+ if (acquiredLock) {
655
+ try {
656
+ const shouldSpawn = options.beforeSpawn ? await options.beforeSpawn() : true;
657
+ if (shouldSpawn) {
658
+ await unlinkStaleSocketIfDaemonDead(paths);
659
+ await spawn(["daemon", "run"], { stateDir: paths.root, logFilePath: paths.logFilePath });
660
+ }
661
+ } finally {
662
+ await rm2(paths.spawnLockPath, { force: true });
663
+ }
664
+ } else if (options.awaitLockRelease) {
665
+ lockWaitTimedOut = !await pollUntilLockReleased(paths.spawnLockPath, waitTimeoutMs, pollIntervalMs);
666
+ }
667
+ const ready = await pollForSocket(paths.socketPath, waitTimeoutMs, pollIntervalMs);
668
+ if (!ready) {
669
+ const lockHint = await pathExists(paths.spawnLockPath) ? ` Another process holds the spawn-lock at "${paths.spawnLockPath}"; if no "appduct daemon" is starting, delete it and retry.` : "";
670
+ throw new DaemonUnavailableError(
671
+ `Timed out waiting for the Appduct daemon socket at "${paths.socketPath}".${lockHint}`
672
+ );
673
+ }
674
+ return { ownedLock: acquiredLock, lockWaitTimedOut };
675
+ };
676
+ var versionChecks = /* @__PURE__ */ new Map();
677
+ var RESTART_REQUEST_TIMEOUT_MS = 3e3;
678
+ var readStatusSpawningIfNeeded = async (context) => {
679
+ try {
680
+ return await sendRequest(
681
+ context.paths.socketPath,
682
+ RPC_METHODS.daemonStatus,
683
+ {},
684
+ context.requestTimeoutMs
685
+ );
686
+ } catch (error) {
687
+ if (!isConnectionMissingError(error)) {
688
+ throw error;
689
+ }
690
+ if (!context.autoSpawn) {
691
+ return void 0;
692
+ }
693
+ await spawnDaemonAndWait(context.paths, context.spawn, context.waitTimeoutMs, context.pollIntervalMs);
694
+ return sendRequest(
695
+ context.paths.socketPath,
696
+ RPC_METHODS.daemonStatus,
697
+ {},
698
+ context.requestTimeoutMs
699
+ );
700
+ }
701
+ };
702
+ var readStatusIfReachable = async (context) => {
703
+ try {
704
+ return await sendRequest(
705
+ context.paths.socketPath,
706
+ RPC_METHODS.daemonStatus,
707
+ {},
708
+ RESTART_REQUEST_TIMEOUT_MS
709
+ );
710
+ } catch (error) {
711
+ if (isConnectionMissingError(error)) {
712
+ return void 0;
713
+ }
714
+ if (isRequestTimeoutError(error)) {
715
+ throw new DaemonUnavailableError(
716
+ `The Appduct daemon at "${context.paths.socketPath}" did not answer "daemon.status" within ${RESTART_REQUEST_TIMEOUT_MS}ms, so it was not replaced. It is running but unresponsive \u2014 run "appduct daemon stop" and retry.`,
717
+ "timeout"
718
+ );
719
+ }
720
+ throw error;
721
+ }
722
+ };
723
+ var isDaemonGone = async (paths) => {
724
+ if (await isSocketConnectable(paths.socketPath)) {
725
+ return false;
726
+ }
727
+ let pid;
728
+ try {
729
+ pid = Number.parseInt((await readFile3(paths.pidFilePath, "utf8")).trim(), 10);
730
+ } catch {
731
+ return true;
732
+ }
733
+ return pid === void 0 || Number.isNaN(pid) || !isProcessAlive(pid);
734
+ };
735
+ var readLiveState = (status) => {
736
+ return {
737
+ sessions: Array.isArray(status.sessions) ? status.sessions.length : 0,
738
+ pendingLinks: typeof status.pendingLinks === "number" ? status.pendingLinks : 0
739
+ };
740
+ };
741
+ var isSafeToRestart = (live) => {
742
+ return live.sessions === 0 && live.pendingLinks === 0;
743
+ };
744
+ var pollUntilDaemonGone = async (paths, timeoutMs, pollIntervalMs) => {
745
+ const deadline = Date.now() + timeoutMs;
746
+ while (Date.now() < deadline) {
747
+ if (await isDaemonGone(paths)) {
748
+ return true;
749
+ }
750
+ await sleep(pollIntervalMs);
751
+ }
752
+ return isDaemonGone(paths);
753
+ };
754
+ var restartDaemon = async (context) => {
755
+ const pidBefore = await readPidfilePid(context.paths);
756
+ const outcome = await spawnDaemonAndWait(
757
+ context.paths,
758
+ context.spawn,
759
+ context.waitTimeoutMs,
760
+ context.pollIntervalMs,
761
+ {
762
+ awaitLockRelease: true,
763
+ beforeSpawn: async () => {
764
+ const status = await readStatusIfReachable(context);
765
+ if (status === void 0) {
766
+ return true;
767
+ }
768
+ if (isSameVersion(status.version, context.check.clientVersion)) {
769
+ return false;
770
+ }
771
+ const live = readLiveState(status);
772
+ if (!isSafeToRestart(live) && !context.check.forceRestart) {
773
+ throw new DaemonVersionMismatchError(status.version, context.check.clientVersion, live);
774
+ }
775
+ try {
776
+ await sendRequest(
777
+ context.paths.socketPath,
778
+ RPC_METHODS.daemonShutdown,
779
+ {},
780
+ RESTART_REQUEST_TIMEOUT_MS
781
+ );
782
+ } catch (error) {
783
+ if (isRequestTimeoutError(error)) {
784
+ throw new DaemonUnavailableError(
785
+ `The Appduct daemon at "${context.paths.socketPath}" did not acknowledge "daemon.shutdown" within ${RESTART_REQUEST_TIMEOUT_MS}ms, so it was left running. Run "appduct daemon stop" and retry.`,
786
+ "timeout"
787
+ );
788
+ }
789
+ if (!isDaemonUnreachableError(error)) {
790
+ throw error;
791
+ }
792
+ }
793
+ if (!await pollUntilDaemonGone(context.paths, context.waitTimeoutMs, context.pollIntervalMs)) {
794
+ throw new DaemonUnavailableError(
795
+ `The Appduct daemon at "${context.paths.socketPath}" did not shut down in time; run "appduct daemon stop" and retry.`
796
+ );
797
+ }
798
+ return true;
799
+ }
800
+ }
801
+ );
802
+ if (!outcome.ownedLock && outcome.lockWaitTimedOut && await readPidfilePid(context.paths) === pidBefore) {
803
+ throw new DaemonUnavailableError(
804
+ `The Appduct daemon at "${context.paths.socketPath}" did not shut down in time: another process has held the spawn-lock at "${context.paths.spawnLockPath}" for longer than this command waited. Retry, or run "appduct daemon stop" if nothing is starting a daemon.`
805
+ );
806
+ }
807
+ };
808
+ var warnOlderClient = (context, daemonVersion) => {
809
+ const message = `appduct: the running daemon is version ${daemonVersion}, newer than this client (${context.check.clientVersion}). Continuing without restarting it \u2014 a newer daemon serves an older client, and replacing it would downgrade whatever started it. Run "appduct daemon stop" if you want this client's daemon instead.
810
+ `;
811
+ const warn = context.check.onWarning ?? ((text) => void process.stderr.write(text));
812
+ warn(message);
813
+ };
814
+ var runVersionCheck = async (context) => {
815
+ const status = await readStatusSpawningIfNeeded(context);
816
+ if (status === void 0) {
817
+ return;
818
+ }
819
+ if (isSameVersion(status.version, context.check.clientVersion)) {
820
+ return;
821
+ }
822
+ if ((compareVersions(context.check.clientVersion, status.version) ?? -1) <= 0) {
823
+ warnOlderClient(context, status.version);
824
+ return;
825
+ }
826
+ const live = readLiveState(status);
827
+ if (!isSafeToRestart(live) && !context.check.forceRestart) {
828
+ throw new DaemonVersionMismatchError(status.version, context.check.clientVersion, live);
829
+ }
830
+ await restartDaemon(context);
831
+ const replacement = await readStatusIfReachable(context);
832
+ if (replacement === void 0) {
833
+ throw new DaemonUnavailableError(
834
+ `The Appduct daemon was restarted but nothing is listening at "${context.paths.socketPath}"; check "${context.paths.logFilePath}".`
835
+ );
836
+ }
837
+ if (isSameVersion(replacement.version, context.check.clientVersion)) {
838
+ return;
839
+ }
840
+ if ((compareVersions(context.check.clientVersion, replacement.version) ?? -1) <= 0) {
841
+ warnOlderClient(context, replacement.version);
842
+ return;
843
+ }
844
+ throw new DaemonVersionRestartIneffectiveError(
845
+ replacement.version,
846
+ context.check.clientVersion,
847
+ readLiveState(replacement)
848
+ );
849
+ };
850
+ var ensureDaemonVersion = async (paths, options, spawn) => {
851
+ const check = options.checkVersion;
852
+ if (!check) {
853
+ return;
854
+ }
855
+ const cached2 = versionChecks.get(paths.socketPath);
856
+ if (cached2) {
857
+ return cached2;
858
+ }
859
+ const pending = runVersionCheck({
860
+ paths,
861
+ spawn,
862
+ autoSpawn: options.autoSpawn ?? true,
863
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
864
+ waitTimeoutMs: options.spawnWaitTimeoutMs ?? 5e3,
865
+ pollIntervalMs: options.spawnPollIntervalMs ?? 100,
866
+ check
867
+ }).catch((error) => {
868
+ if (!(error instanceof DaemonVersionMismatchError)) {
869
+ versionChecks.delete(paths.socketPath);
870
+ }
871
+ throw error;
872
+ });
873
+ versionChecks.set(paths.socketPath, pending);
874
+ return pending;
875
+ };
876
+ var ensureDaemonVersionMatches = async (options) => {
877
+ const paths = getStateDirPaths(options.stateDir);
878
+ await ensureDaemonVersion(
879
+ paths,
880
+ { ...options, autoSpawn: options.autoSpawn ?? false },
881
+ options.spawn ?? defaultSpawn
882
+ );
883
+ };
884
+ var callDaemon = async (method, params, options) => {
885
+ const paths = getStateDirPaths(options.stateDir);
886
+ const autoSpawn = options.autoSpawn ?? true;
887
+ const spawn = options.spawn ?? defaultSpawn;
888
+ const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
889
+ await ensureDaemonVersion(paths, options, spawn);
890
+ try {
891
+ return await sendRequest(paths.socketPath, method, params, requestTimeoutMs);
892
+ } catch (error) {
893
+ if (!autoSpawn || !isConnectionMissingError(error)) {
894
+ throw error;
895
+ }
896
+ await spawnDaemonAndWait(
897
+ paths,
898
+ spawn,
899
+ options.spawnWaitTimeoutMs ?? 5e3,
900
+ options.spawnPollIntervalMs ?? 100
901
+ );
902
+ return sendRequest(paths.socketPath, method, params, requestTimeoutMs);
903
+ }
904
+ };
905
+
906
+ // src/errors.ts
907
+ var EXIT_CODE_BY_ERROR_TYPE = {
908
+ usage_error: 64,
909
+ validation_error: 65,
910
+ // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
911
+ // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
912
+ // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
913
+ // assertion failure (docs/tasks/08-appduct-doctor.md).
914
+ inspection_error: 66,
915
+ connection_error: 70,
916
+ session_error: 71,
917
+ tool_error: 72,
918
+ permission_error: 77,
919
+ internal_error: 1,
920
+ // `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
921
+ // assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
922
+ // and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
923
+ // conflate a normal assertion failure with an unexpected crash.
924
+ assertion_error: 3
925
+ };
926
+ var RPC_ERROR_EXIT_CLASS = {
927
+ no_session: "session_error",
928
+ ambiguous_session: "session_error",
929
+ unknown_session: "session_error",
930
+ session_not_active: "session_error",
931
+ session_suspended: "session_error",
932
+ tool_not_found: "tool_error",
933
+ tool_input_validation_error: "tool_error",
934
+ tool_output_validation_error: "tool_error",
935
+ tool_execution_error: "tool_error",
936
+ tool_serialization_error: "tool_error",
937
+ tool_timeout: "tool_error",
938
+ tool_cancelled: "tool_error",
939
+ policy_denied: "permission_error",
940
+ invalid_request: "validation_error"
941
+ };
942
+ var AppductCliError = class extends Error {
943
+ type;
944
+ details;
945
+ constructor(type, message, details) {
946
+ super(message);
947
+ this.name = "AppductCliError";
948
+ this.type = type;
949
+ this.details = details;
950
+ }
951
+ };
952
+ var usageError = (message, details) => {
953
+ return new AppductCliError("usage_error", message, details);
954
+ };
955
+ var connectionError = (message, details) => {
956
+ return new AppductCliError("connection_error", message, details);
957
+ };
958
+ var isAppductCliError = (value) => {
959
+ return value instanceof AppductCliError;
960
+ };
961
+ var rpcErrorClass = (error) => {
962
+ const type = error.data?.type;
963
+ return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
964
+ };
965
+ var toCliError = (error) => {
966
+ if (isAppductCliError(error)) {
967
+ return {
968
+ type: error.type,
969
+ message: error.message,
970
+ details: error.details
971
+ };
972
+ }
973
+ if (error instanceof DaemonVersionMismatchError) {
974
+ return {
975
+ type: "connection_error",
976
+ message: error.message,
977
+ details: {
978
+ daemon_version: error.daemonVersion,
979
+ client_version: error.clientVersion,
980
+ session_count: error.sessionCount,
981
+ pending_link_count: error.pendingLinkCount
982
+ }
983
+ };
984
+ }
985
+ if (error instanceof DaemonRpcError && error.data?.type) {
986
+ return {
987
+ type: error.data.type,
988
+ message: error.message,
989
+ details: error.data.details
990
+ };
991
+ }
992
+ if (isDaemonUnreachableError(error)) {
993
+ return {
994
+ type: "connection_error",
995
+ message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
996
+ };
997
+ }
998
+ if (error instanceof Error) {
999
+ return {
1000
+ type: "internal_error",
1001
+ message: error.message
1002
+ };
1003
+ }
1004
+ return {
1005
+ type: "internal_error",
1006
+ message: "An unexpected error occurred.",
1007
+ details: error
1008
+ };
1009
+ };
1010
+ var getExitCodeForError = (error) => {
1011
+ if (isAppductCliError(error)) {
1012
+ return EXIT_CODE_BY_ERROR_TYPE[error.type];
1013
+ }
1014
+ if (error instanceof DaemonVersionMismatchError) {
1015
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1016
+ }
1017
+ if (error instanceof DaemonRpcError) {
1018
+ const cliErrorClass = rpcErrorClass(error);
1019
+ return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
1020
+ }
1021
+ if (isDaemonUnreachableError(error)) {
1022
+ return EXIT_CODE_BY_ERROR_TYPE.connection_error;
1023
+ }
1024
+ return EXIT_CODE_BY_ERROR_TYPE.internal_error;
1025
+ };
1026
+
1027
+ // src/commands/tools.ts
1028
+ var listTools = (selector, context, params = {}) => {
1029
+ return callDaemon(
1030
+ RPC_METHODS2.toolsList,
1031
+ { selector, ...params },
1032
+ { stateDir: context.stateDir, spawn: context.spawn }
1033
+ );
1034
+ };
1035
+ var findTool = (tools, name) => {
1036
+ return tools.find((tool) => tool.name === name);
1037
+ };
1038
+ var hasPagingOptions = (options) => {
1039
+ return options.group !== void 0 || options.filter !== void 0 || options.limit !== void 0 || options.offset !== void 0;
1040
+ };
1041
+ var hasListingOnlyOptions = (options) => {
1042
+ return hasPagingOptions(options) || options.groups === true;
1043
+ };
1044
+ var toListParams = (options) => {
1045
+ return { group: options.group, filter: options.filter, limit: options.limit, offset: options.offset };
1046
+ };
1047
+ var toListing = (result, params) => {
1048
+ return {
1049
+ ...result,
1050
+ ...params.group !== void 0 ? { group: params.group } : {},
1051
+ ...params.filter !== void 0 ? { filter: params.filter } : {},
1052
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
1053
+ ...params.offset !== void 0 ? { offset: params.offset } : {}
1054
+ };
1055
+ };
1056
+ var listingOnlyError = () => {
1057
+ return usageError(
1058
+ '"--group", "--groups", "--filter", "--limit", and "--offset" only apply to a tools listing, not a single tool lookup.'
1059
+ );
1060
+ };
1061
+ var listGroups = async (selector, context) => {
1062
+ const result = await listTools(selector, context, { limit: 1 });
1063
+ return { groups: requireGroupSupport(result), total: result.total };
1064
+ };
1065
+ var requireGroupSupport = (result) => {
1066
+ if (!Array.isArray(result.groups)) {
1067
+ throw connectionError(
1068
+ 'The running Appduct daemon does not support tool groups ("--group"/"--groups"). Restart it with a newer version: `appduct daemon stop`, or pass `--daemon-restart`.'
1069
+ );
1070
+ }
1071
+ return result.groups;
1072
+ };
1073
+ var listOrGroups = async (selector, options, context) => {
1074
+ if (options.groups === true) {
1075
+ try {
1076
+ return await listGroups(selector, context);
1077
+ } catch (error) {
1078
+ if (selector !== void 0 && error instanceof DaemonRpcError && error.data?.type === "unknown_session") {
1079
+ throw usageError(
1080
+ `No session matches "${selector}". "--groups" takes no value; to list one group's tools, use "--group ${selector}".`
1081
+ );
1082
+ }
1083
+ throw error;
1084
+ }
1085
+ }
1086
+ const params = toListParams(options);
1087
+ const result = await listTools(selector, context, params);
1088
+ if (params.group !== void 0) {
1089
+ requireGroupSupport(result);
1090
+ }
1091
+ return toListing(result, params);
1092
+ };
1093
+ var handleToolsCommand = async (options, context) => {
1094
+ if (options.groups === true && hasPagingOptions(options)) {
1095
+ throw usageError(
1096
+ '"--groups" lists every group in the session and cannot be combined with "--group", "--filter", "--limit", or "--offset".'
1097
+ );
1098
+ }
1099
+ if (options.name !== void 0 && hasListingOnlyOptions(options)) {
1100
+ throw listingOnlyError();
1101
+ }
1102
+ if (options.selector !== void 0 && options.name !== void 0) {
1103
+ const result = await listTools(options.selector, context);
1104
+ const tool = findTool(result.tools, options.name);
1105
+ if (!tool) {
1106
+ throw usageError(
1107
+ `Tool "${options.name}" is not registered on session "${options.selector}".`,
1108
+ { available: result.tools.map((entry) => entry.name) }
1109
+ );
1110
+ }
1111
+ return { ok: true, data: tool };
1112
+ }
1113
+ if (options.selector !== void 0) {
1114
+ let implicitTools;
1115
+ try {
1116
+ implicitTools = await listTools(void 0, context);
1117
+ } catch (error) {
1118
+ if (!(error instanceof DaemonRpcError)) {
1119
+ throw error;
1120
+ }
1121
+ }
1122
+ if (implicitTools) {
1123
+ const tool = findTool(implicitTools.tools, options.selector);
1124
+ if (tool) {
1125
+ if (hasListingOnlyOptions(options)) {
1126
+ throw listingOnlyError();
1127
+ }
1128
+ return { ok: true, data: tool };
1129
+ }
1130
+ }
1131
+ return { ok: true, data: await listOrGroups(options.selector, options, context) };
1132
+ }
1133
+ return { ok: true, data: await listOrGroups(void 0, options, context) };
1134
+ };
1135
+
1136
+ // src/cli/command-options.ts
1137
+ var parsePositiveIntegerOption = (value, flagName) => {
1138
+ if (value === void 0) {
1139
+ return void 0;
1140
+ }
1141
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1142
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
1143
+ throw usageError(`"${flagName}" must be a positive integer.`);
1144
+ }
1145
+ return parsed;
1146
+ };
1147
+ var parseNonNegativeIntegerOption = (value, flagName) => {
1148
+ if (value === void 0) {
1149
+ return void 0;
1150
+ }
1151
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1152
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
1153
+ throw usageError(`"${flagName}" must be a non-negative integer.`);
1154
+ }
1155
+ return parsed;
1156
+ };
1157
+ var readTextOption = (argv, value, flagName) => {
1158
+ if (value === void 0) {
1159
+ return void 0;
1160
+ }
1161
+ if (typeof value === "boolean") {
1162
+ throw usageError(`"${flagName}" requires a value.`);
1163
+ }
1164
+ let raw;
1165
+ for (let index = 0; index < argv.length; index += 1) {
1166
+ const token = argv[index];
1167
+ if (token === "--") {
1168
+ break;
1169
+ }
1170
+ if (token === flagName && index + 1 < argv.length) {
1171
+ raw = argv[index + 1];
1172
+ index += 1;
1173
+ } else if (token.startsWith(`${flagName}=`)) {
1174
+ raw = token.slice(flagName.length + 1);
1175
+ }
1176
+ }
1177
+ return raw ?? String(Array.isArray(value) ? value.at(-1) : value);
1178
+ };
1179
+ var splitOptionalSelectorAndTarget = (args, commandUsage) => {
1180
+ if (args.length > 2) {
1181
+ throw usageError(`Usage: ${commandUsage} (too many arguments).`);
1182
+ }
1183
+ if (args.length === 2) {
1184
+ return { selector: args[0], target: args[1] };
1185
+ }
1186
+ if (args.length === 1) {
1187
+ return { selectorOrTarget: args[0] };
1188
+ }
1189
+ return {};
1190
+ };
1191
+
1192
+ // src/cli/global-flags.ts
1193
+ var formatJson = (value, flags) => {
1194
+ return flags.pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
1195
+ };
1196
+
1197
+ // src/output.ts
1198
+ import pc from "picocolors";
1199
+ import {
1200
+ formatAgentWebSocketUrl,
1201
+ renderToolSignature,
1202
+ summarizeToolDescription
1203
+ } from "@appduct/shared";
1204
+
1205
+ // src/qr-terminal.ts
1206
+ import { toQR } from "toqr";
1207
+ var HALF_UPPER = "\u2580";
1208
+ var HALF_LOWER = "\u2584";
1209
+ var FULL = "\u2588";
1210
+ var renderQrToTerminal = (content, options) => {
1211
+ const margin = options?.margin ?? 0;
1212
+ const ecLevel = options?.ecLevel ?? 1;
1213
+ const modules = toQR(content, ecLevel);
1214
+ const side = Math.sqrt(modules.length);
1215
+ if (!Number.isInteger(side)) {
1216
+ throw new Error("Invalid QR matrix size");
1217
+ }
1218
+ const dim = side + 2 * margin;
1219
+ const isDark = (row, col) => {
1220
+ if (row < margin || col < margin || row >= margin + side || col >= margin + side) {
1221
+ return false;
1222
+ }
1223
+ const mr = row - margin;
1224
+ const mc = col - margin;
1225
+ return modules[mr * side + mc] === 1;
1226
+ };
1227
+ const lines = [];
1228
+ for (let row = 0; row < dim; row += 2) {
1229
+ let line = "";
1230
+ for (let col = 0; col < dim; col++) {
1231
+ const top = isDark(row, col);
1232
+ const bottom = row + 1 < dim ? isDark(row + 1, col) : false;
1233
+ line += top ? bottom ? FULL : HALF_UPPER : bottom ? HALF_LOWER : " ";
1234
+ }
1235
+ lines.push(line);
1236
+ }
1237
+ return lines.join("\n");
1238
+ };
1239
+
1240
+ // src/output.ts
1241
+ var renderMetaLines = (meta) => {
1242
+ if (!meta) {
1243
+ return [];
1244
+ }
1245
+ return [
1246
+ "Meta",
1247
+ ` Command: ${meta.command}`,
1248
+ ` Timestamp: ${meta.timestamp}`,
1249
+ ...typeof meta.duration_ms === "number" ? [` Duration: ${meta.duration_ms} ms`] : []
1250
+ ];
1251
+ };
1252
+ var formatScalar = (value, flags) => {
1253
+ if (typeof value === "string") {
1254
+ return value;
1255
+ }
1256
+ if (typeof value === "number" || typeof value === "boolean") {
1257
+ return String(value);
1258
+ }
1259
+ if (value === null || value === void 0) {
1260
+ return "null";
1261
+ }
1262
+ return formatJson(value, flags);
1263
+ };
1264
+ var renderFields = (title, fields, flags) => {
1265
+ const visibleFields = fields.filter(([, value]) => value !== void 0);
1266
+ if (visibleFields.length === 0) {
1267
+ return [];
1268
+ }
1269
+ const width = Math.max(...visibleFields.map(([label]) => label.length));
1270
+ return [
1271
+ title,
1272
+ ...visibleFields.map(([label, value]) => ` ${label.padEnd(width)} ${formatScalar(value, flags)}`)
1273
+ ];
1274
+ };
1275
+ var formatAge = (fromIso, toIso) => {
1276
+ const elapsedMs = Math.max(0, new Date(toIso).getTime() - new Date(fromIso).getTime());
1277
+ const elapsedSeconds = Math.floor(elapsedMs / 1e3);
1278
+ if (elapsedSeconds < 60) {
1279
+ return `${elapsedSeconds}s`;
1280
+ }
1281
+ const elapsedMinutes = Math.floor(elapsedSeconds / 60);
1282
+ if (elapsedMinutes < 60) {
1283
+ return `${elapsedMinutes}m`;
1284
+ }
1285
+ const elapsedHours = Math.floor(elapsedMinutes / 60);
1286
+ if (elapsedHours < 24) {
1287
+ return `${elapsedHours}h`;
1288
+ }
1289
+ return `${Math.floor(elapsedHours / 24)}d`;
1290
+ };
1291
+ var formatDevice = (device) => {
1292
+ const parts = [device.manufacturer, device.model].filter((part) => Boolean(part));
1293
+ const label = parts.length > 0 ? parts.join(" ") : "unknown device";
1294
+ return device.os ? `${label} (${device.os})` : label;
1295
+ };
1296
+ var renderLsData = (colors, data, now) => {
1297
+ if (data.length === 0) {
1298
+ return [colors.dim("Sessions"), " No Appduct sessions are registered."];
1299
+ }
1300
+ const nowIso = now.toISOString();
1301
+ const headers = ["Alias", "State", "Device", "Tools", "Age"];
1302
+ const rows = data.map((session) => [
1303
+ session.alias,
1304
+ session.state,
1305
+ formatDevice(session.device),
1306
+ String(session.toolCount),
1307
+ formatAge(session.claimedAt ?? session.createdAt, nowIso)
1308
+ ]);
1309
+ const widths = headers.map(
1310
+ (header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
1311
+ );
1312
+ return [
1313
+ colors.green("Sessions"),
1314
+ ` ${headers.map((header, index) => header.padEnd(widths[index])).join(" ")}`,
1315
+ ...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
1316
+ ];
1317
+ };
1318
+ var isToolsListing = (data) => {
1319
+ return typeof data === "object" && data !== null && Array.isArray(data.tools);
1320
+ };
1321
+ var isToolGroupsListing = (data) => {
1322
+ return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
1323
+ };
1324
+ var UNGROUPED_LABEL = "(ungrouped)";
1325
+ var MAX_FOOTER_GROUPS = 10;
1326
+ var hasAnyGroup = (groups) => {
1327
+ return (groups ?? []).some((entry) => entry.group !== null);
1328
+ };
1329
+ var topLevelGroups = (groups) => {
1330
+ return (groups ?? []).filter((entry) => entry.group !== null && !entry.group.includes("/"));
1331
+ };
1332
+ var renderEmptyToolsLine = (data) => {
1333
+ if (data.total > 0) {
1334
+ return ` No tools at offset ${data.offset ?? 0}; ${data.total} matching tool${data.total === 1 ? "" : "s"} in total.`;
1335
+ }
1336
+ if (data.group !== void 0) {
1337
+ const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
1338
+ return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
1339
+ }
1340
+ return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
1341
+ };
1342
+ var renderGroupHint = (data) => {
1343
+ if (data.group !== void 0 || !hasAnyGroup(data.groups)) {
1344
+ return void 0;
1345
+ }
1346
+ const top = topLevelGroups(data.groups);
1347
+ const named = top.slice(0, MAX_FOOTER_GROUPS).map((entry) => `${entry.group} ${entry.total}`);
1348
+ const more = top.length > MAX_FOOTER_GROUPS ? `, ... ${top.length - MAX_FOOTER_GROUPS} more; see --groups` : "";
1349
+ return `--group <name> (groups: ${named.join(", ")}${more})`;
1350
+ };
1351
+ var renderTruncationLine = (data) => {
1352
+ if (data.tools.length >= data.total) {
1353
+ return [];
1354
+ }
1355
+ const groupHint = renderGroupHint(data);
1356
+ const narrow = groupHint === void 0 ? "--filter <text> or" : `${groupHint} or --filter <text>, or`;
1357
+ return [
1358
+ "",
1359
+ `Showing ${data.tools.length} of ${data.total} tools (offset ${data.offset ?? 0}). Narrow with ${narrow} page with --offset <n>.`
1360
+ ];
1361
+ };
1362
+ var renderToolSummaryLines = (tool, indent) => {
1363
+ const tag = tool.policy === "allow" ? "" : ` [${tool.policy}]`;
1364
+ return [`${indent}${renderToolSignature(tool)}${tag}`, `${indent} ${summarizeToolDescription(tool.description)}`];
1365
+ };
1366
+ var renderGroupedToolLines = (colors, tools) => {
1367
+ const byGroup = /* @__PURE__ */ new Map();
1368
+ for (const tool of tools) {
1369
+ const key = tool.group ?? null;
1370
+ const bucket = byGroup.get(key);
1371
+ if (bucket) {
1372
+ bucket.push(tool);
1373
+ } else {
1374
+ byGroup.set(key, [tool]);
1375
+ }
1376
+ }
1377
+ const tops = /* @__PURE__ */ new Map();
1378
+ for (const key of byGroup.keys()) {
1379
+ if (key === null) {
1380
+ continue;
1381
+ }
1382
+ const slash = key.indexOf("/");
1383
+ const top = slash === -1 ? key : key.slice(0, slash);
1384
+ const subs = tops.get(top) ?? [];
1385
+ if (slash !== -1) {
1386
+ subs.push(key);
1387
+ }
1388
+ tops.set(top, subs);
1389
+ }
1390
+ const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
1391
+ const lines = [];
1392
+ for (const top of [...tops.keys()].sort(byCodePoint)) {
1393
+ lines.push(` ${colors.cyan(top)}`);
1394
+ lines.push(...(byGroup.get(top) ?? []).flatMap((tool) => renderToolSummaryLines(tool, " ")));
1395
+ for (const sub of tops.get(top).sort(byCodePoint)) {
1396
+ lines.push(` ${colors.cyan(sub)}`);
1397
+ lines.push(...byGroup.get(sub).flatMap((tool) => renderToolSummaryLines(tool, " ")));
1398
+ }
1399
+ }
1400
+ const ungrouped = byGroup.get(null);
1401
+ if (ungrouped) {
1402
+ lines.push(` ${colors.cyan(UNGROUPED_LABEL)}`);
1403
+ lines.push(...ungrouped.flatMap((tool) => renderToolSummaryLines(tool, " ")));
1404
+ }
1405
+ return lines;
1406
+ };
1407
+ var renderToolsTitle = (colors, data) => {
1408
+ return colors.green(data.group === void 0 ? "Tools" : `Tools in group ${data.group}`);
1409
+ };
1410
+ var renderToolSummaryTable = (colors, data) => {
1411
+ if (data.tools.length === 0) {
1412
+ return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
1413
+ }
1414
+ const grouped = data.group === void 0 && hasAnyGroup(data.groups);
1415
+ return [
1416
+ renderToolsTitle(colors, data),
1417
+ ...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
1418
+ ...renderTruncationLine(data),
1419
+ "",
1420
+ "Run `appduct tools <name>` for a tool's full schema."
1421
+ ];
1422
+ };
1423
+ var renderToolDetail = (colors, tool, flags) => {
1424
+ return renderFields(
1425
+ colors.green(`Tool: ${tool.name}`),
1426
+ [
1427
+ ["Signature", renderToolSignature(tool)],
1428
+ ["Description", tool.description],
1429
+ ["Input schema", tool.input_schema],
1430
+ ["Output schema", tool.output_schema],
1431
+ // `?? undefined` keeps an ungrouped tool's row hidden rather than printing a bare "null":
1432
+ // `renderFields` drops undefined rows, and the machine form's `null` is for consumers, not
1433
+ // for a human scanning one tool's schema.
1434
+ ["Group", tool.group ?? void 0],
1435
+ ["Annotations", tool.annotations],
1436
+ // Only rendered for a tool that declares one; `renderFields` drops undefined rows, so a
1437
+ // tool on the daemon's default deadline shows no line at all rather than a misleading
1438
+ // "10000".
1439
+ ["Timeout (ms)", tool.timeout_ms]
1440
+ ],
1441
+ flags
1442
+ );
1443
+ };
1444
+ var renderToolsFullListing = (colors, data, flags) => {
1445
+ if (data.tools.length === 0) {
1446
+ return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
1447
+ }
1448
+ return [
1449
+ ...data.tools.flatMap((tool, index) => [
1450
+ ...index > 0 ? [""] : [],
1451
+ ...renderToolDetail(colors, tool, flags)
1452
+ ]),
1453
+ ...renderTruncationLine(data)
1454
+ ];
1455
+ };
1456
+ var renderToolGroups = (colors, data) => {
1457
+ if (data.groups.length === 0) {
1458
+ return [colors.green("Groups"), " No tools registered."];
1459
+ }
1460
+ const label = (entry) => {
1461
+ if (entry.group === null) {
1462
+ return UNGROUPED_LABEL;
1463
+ }
1464
+ return entry.group.includes("/") ? ` ${entry.group}` : entry.group;
1465
+ };
1466
+ const width = Math.max(...data.groups.map((entry) => label(entry).length));
1467
+ const countWidth = Math.max(...data.groups.map((entry) => String(entry.total).length));
1468
+ return [
1469
+ colors.green("Groups"),
1470
+ ...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
1471
+ "",
1472
+ `${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.")
1473
+ ];
1474
+ };
1475
+ var renderToolsData = (colors, data, flags, full) => {
1476
+ if (isToolGroupsListing(data)) {
1477
+ return renderToolGroups(colors, data);
1478
+ }
1479
+ if (!isToolsListing(data)) {
1480
+ return renderToolDetail(colors, data, flags);
1481
+ }
1482
+ return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
1483
+ };
1484
+ var renderInvokeData = (colors, data, flags) => {
1485
+ return [colors.green("Result"), formatScalar(data, flags)];
1486
+ };
1487
+ var renderLinkData = (colors, data, flags, qr) => {
1488
+ const lines = [
1489
+ colors.green("Link Created"),
1490
+ ...renderFields(
1491
+ "Link",
1492
+ [
1493
+ ["Session", data.sessionId],
1494
+ ["Deep link", data.deepLink],
1495
+ ["Endpoint", formatAgentWebSocketUrl(data.endpoint)],
1496
+ ["Pin", data.pin],
1497
+ ["Expires", new Date(data.expiresAt * 1e3).toISOString()],
1498
+ ["Delivered", data.delivered ? `yes (${data.target})` : void 0]
1499
+ ],
1500
+ flags
1501
+ )
1502
+ ];
1503
+ if (qr) {
1504
+ lines.push("", "QR", renderQrToTerminal(data.deepLink));
1505
+ }
1506
+ return lines;
1507
+ };
1508
+ var renderRevokeData = (colors, _data) => {
1509
+ return [colors.green("Session Revoked")];
1510
+ };
1511
+ var renderKeygenData = (colors, data, flags) => {
1512
+ return [
1513
+ colors.green("Key Ready"),
1514
+ ...renderFields(
1515
+ "Key",
1516
+ [
1517
+ ["Path", data.path],
1518
+ ["Fingerprint", data.pin]
1519
+ ],
1520
+ flags
1521
+ )
1522
+ ];
1523
+ };
1524
+ var renderInitData = (colors, data, flags) => {
1525
+ const snippet = JSON.stringify(
1526
+ { mcpServers: { appduct: data.mcpServerEntry } },
1527
+ null,
1528
+ 2
1529
+ );
1530
+ return [
1531
+ colors.green(data.changed ? "Project Initialized" : "Project Already Initialized"),
1532
+ ...renderFields(
1533
+ "Config",
1534
+ [
1535
+ ["Path", data.path],
1536
+ ["Scheme", data.scheme],
1537
+ ["Source", data.source],
1538
+ // Only present for a scheme discovery read off a static project file (app.json, or one of
1539
+ // the native Android/iOS probes) — names the exact file/key, not just which platform.
1540
+ ["Read from", data.origin],
1541
+ ["Written", data.changed ? data.created ? "created" : "updated" : "unchanged"]
1542
+ ],
1543
+ flags
1544
+ ),
1545
+ ...data.note === void 0 ? [] : ["", colors.yellow(`Note: ${data.note}`)],
1546
+ "",
1547
+ "MCP server entry",
1548
+ ...snippet.split("\n").map((line) => ` ${line}`),
1549
+ "",
1550
+ "Next",
1551
+ ...data.nextSteps.map((step, index) => ` ${index + 1}. ${step}`)
1552
+ ];
1553
+ };
1554
+ var renderDaemonRunData = (colors, data, flags) => {
1555
+ return [
1556
+ colors.green("Daemon Running"),
1557
+ ...renderFields(
1558
+ "Daemon",
1559
+ [
1560
+ ["PID", data.daemon.pid],
1561
+ ["State dir", data.daemon.state_dir],
1562
+ ["Socket", data.daemon.socket_path]
1563
+ ],
1564
+ flags
1565
+ )
1566
+ ];
1567
+ };
1568
+ var renderDaemonStartData = (colors, data, flags) => {
1569
+ return [
1570
+ colors.green("Daemon Started"),
1571
+ ...renderFields(
1572
+ "Daemon",
1573
+ [
1574
+ ["PID", data.daemon.pid],
1575
+ ["WSS port", data.daemon.wss_port],
1576
+ ["Started at", data.daemon.started_at]
1577
+ ],
1578
+ flags
1579
+ )
1580
+ ];
1581
+ };
1582
+ var renderDaemonStopData = (colors, data, flags) => {
1583
+ return [
1584
+ colors.green("Daemon Stopped"),
1585
+ ...renderFields("Daemon", [["Method", data.daemon.method]], flags)
1586
+ ];
1587
+ };
1588
+ var formatByteSize = (bytes) => {
1589
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
1590
+ let value = bytes;
1591
+ let unit = 0;
1592
+ while (value >= 1024 && unit < units.length - 1) {
1593
+ value /= 1024;
1594
+ unit += 1;
1595
+ }
1596
+ return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
1597
+ };
1598
+ var renderDaemonStatusData = (colors, data, flags) => {
1599
+ return [
1600
+ colors.green("Daemon Status"),
1601
+ ...renderFields(
1602
+ "Daemon",
1603
+ [
1604
+ ["Version", data.daemon.version],
1605
+ ["PID", data.daemon.pid],
1606
+ ["Started at", data.daemon.started_at],
1607
+ ["WSS port", data.daemon.wss_port],
1608
+ ["Pinned keys", data.daemon.pinned_keys.length],
1609
+ ["Sessions", data.daemon.session_count]
1610
+ ],
1611
+ flags
1612
+ ),
1613
+ "",
1614
+ ...renderFields(
1615
+ "Policy",
1616
+ [
1617
+ ["Default", data.policy.default],
1618
+ ["Destructive", data.policy.destructive],
1619
+ ["Overrides", data.policy.tools ? Object.keys(data.policy.tools).length : 0]
1620
+ ],
1621
+ flags
1622
+ ),
1623
+ "",
1624
+ // The retention rows drop out entirely against a daemon that predates them (`renderFields`
1625
+ // skips `undefined`), rather than printing "undefined" or an invented zero.
1626
+ ...renderFields(
1627
+ "Audit",
1628
+ [
1629
+ ["Path", data.audit.path],
1630
+ ["Retention", data.audit.retention_days === void 0 ? void 0 : `${data.audit.retention_days} days`],
1631
+ ["Files", data.audit.files],
1632
+ ["Size", data.audit.bytes === void 0 ? void 0 : formatByteSize(data.audit.bytes)],
1633
+ ["Failed writes", data.audit.failed_writes],
1634
+ ["Failed prunes", data.audit.failed_prunes]
1635
+ ],
1636
+ flags
1637
+ ),
1638
+ // Version drift (issue #30) is the one thing here an operator has to act on, so it goes last
1639
+ // — the line still on screen after the block scrolls — and in yellow, not a quiet field row.
1640
+ ...data.warning ? ["", colors.yellow(`Warning: ${data.warning}`)] : []
1641
+ ];
1642
+ };
1643
+ var renderDoctorData = (colors, data, flags) => {
1644
+ return [
1645
+ data.present ? colors.green("Appduct Present") : colors.yellow("Appduct Absent"),
1646
+ ...renderFields(
1647
+ "Artifact",
1648
+ [
1649
+ ["Path", data.artifact],
1650
+ ["Platform", data.platform],
1651
+ ["Format", data.format],
1652
+ ["Present", data.present],
1653
+ ["Signals", data.signals.length > 0 ? data.signals.join(", ") : "none"],
1654
+ ["Assertion", data.assertion ? `${data.assertion.expected} (holds)` : void 0]
1655
+ ],
1656
+ flags
1657
+ )
1658
+ ];
1659
+ };
1660
+ var renderSuccessData = (colors, command, data, options) => {
1661
+ const flags = options.flags;
1662
+ switch (command) {
1663
+ case "init":
1664
+ return renderInitData(colors, data, flags);
1665
+ case "keygen":
1666
+ return renderKeygenData(colors, data, flags);
1667
+ case "link":
1668
+ return renderLinkData(colors, data, flags, options.qr);
1669
+ case "ls":
1670
+ return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
1671
+ case "tools":
1672
+ return renderToolsData(colors, data, flags, options.full);
1673
+ case "invoke":
1674
+ return renderInvokeData(colors, data, flags);
1675
+ case "revoke":
1676
+ return renderRevokeData(colors, data);
1677
+ case "daemon run":
1678
+ return renderDaemonRunData(colors, data, flags);
1679
+ case "daemon start":
1680
+ return renderDaemonStartData(colors, data, flags);
1681
+ case "daemon stop":
1682
+ return renderDaemonStopData(colors, data, flags);
1683
+ case "daemon status":
1684
+ return renderDaemonStatusData(colors, data, flags);
1685
+ case "doctor":
1686
+ return renderDoctorData(colors, data, flags);
1687
+ default:
1688
+ return [colors.green("Command Complete"), formatJson(data, flags)];
1689
+ }
1690
+ };
1691
+ var renderHumanError = (colors, error, flags, meta) => {
1692
+ return [
1693
+ colors.red("Command Failed"),
1694
+ ...renderFields(
1695
+ "Error",
1696
+ [
1697
+ ["Type", error.type],
1698
+ ["Message", error.message],
1699
+ ["Details", error.details]
1700
+ ],
1701
+ flags
1702
+ ),
1703
+ ...meta ? ["", ...renderMetaLines(meta)] : []
1704
+ ].join("\n");
1705
+ };
1706
+ var renderResult = (result, options) => {
1707
+ if (options.flags.json) {
1708
+ return {
1709
+ stdout: `${formatJson(result, options.flags)}
1710
+ `
1711
+ };
1712
+ }
1713
+ const colors = pc.createColors(options.flags.color);
1714
+ if (!result.ok) {
1715
+ return {
1716
+ stderr: `${renderHumanError(colors, result.error, options.flags, result.meta)}
1717
+ `
1718
+ };
1719
+ }
1720
+ const lines = [
1721
+ ...renderSuccessData(colors, options.command, result.data, options),
1722
+ "",
1723
+ ...renderMetaLines(result.meta)
1724
+ ].filter((line, index, collection) => {
1725
+ if (line !== "") {
1726
+ return true;
1727
+ }
1728
+ return index > 0 && collection[index - 1] !== "" && index < collection.length - 1;
1729
+ });
1730
+ return {
1731
+ stdout: `${lines.join("\n")}
1732
+ `
1733
+ };
1734
+ };
1735
+
1736
+ // src/cli/envelope.ts
1737
+ var createCommandMeta = (command, startedAt, finishedAt) => {
1738
+ return {
1739
+ command,
1740
+ timestamp: finishedAt.toISOString(),
1741
+ duration_ms: finishedAt.getTime() - startedAt.getTime()
1742
+ };
1743
+ };
1744
+ var finalizeResult = (result, timing, flags) => {
1745
+ if (!flags.verbose) {
1746
+ return result;
1747
+ }
1748
+ return {
1749
+ ...result,
1750
+ meta: createCommandMeta(timing.command, timing.startedAt, timing.finishedAt)
1751
+ };
1752
+ };
1753
+
1754
+ // src/cli/runner.ts
1755
+ var writeRenderedOutput = (rendered, writers) => {
1756
+ if (rendered.stdout) {
1757
+ writers.stdout.write(rendered.stdout);
1758
+ }
1759
+ if (rendered.stderr) {
1760
+ writers.stderr.write(rendered.stderr);
1761
+ }
1762
+ };
1763
+ var executeCommand = async (command, handler, env, render = {}) => {
1764
+ const startedAt = env.clock.now();
1765
+ try {
1766
+ const result = await handler();
1767
+ const finishedAt = env.clock.now();
1768
+ const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
1769
+ const renderOptions = {
1770
+ command,
1771
+ flags: env.flags,
1772
+ now: finishedAt,
1773
+ qr: render.qr,
1774
+ full: render.full
1775
+ };
1776
+ writeRenderedOutput(renderResult(finalized, renderOptions), env);
1777
+ return 0;
1778
+ } catch (error) {
1779
+ const finishedAt = env.clock.now();
1780
+ const cliError = toCliError(error);
1781
+ const result = { ok: false, error: cliError };
1782
+ const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
1783
+ const renderOptions = {
1784
+ command,
1785
+ flags: env.flags,
1786
+ now: finishedAt,
1787
+ qr: render.qr,
1788
+ full: render.full
1789
+ };
1790
+ writeRenderedOutput(renderResult(finalized, renderOptions), env);
1791
+ return getExitCodeForError(error);
1792
+ }
1793
+ };
1794
+
1795
+ // src/cli/router.ts
1796
+ var commandName = (context) => {
1797
+ return context.path.join(" ");
1798
+ };
1799
+
1800
+ // src/cli/version-guard.ts
1801
+ var versionCheckOptions = async (context, onWarning = context.versionCheck.warn) => {
1802
+ return {
1803
+ clientVersion: context.versionCheck.clientVersion,
1804
+ forceRestart: await context.versionCheck.forceRestart(),
1805
+ onWarning
1806
+ };
1807
+ };
1808
+ var guarded = (context) => {
1809
+ return (handler) => {
1810
+ return async () => {
1811
+ await ensureDaemonVersionMatches({
1812
+ stateDir: context.stateDir,
1813
+ autoSpawn: false,
1814
+ checkVersion: await versionCheckOptions(context)
1815
+ });
1816
+ return handler();
1817
+ };
1818
+ };
1819
+ };
1820
+
1821
+ // src/cli/routes/tools.ts
1822
+ var route = async (context) => {
1823
+ const { options, stateDir } = context;
1824
+ return executeCommand(
1825
+ commandName(context),
1826
+ // Argument parsing runs inside the handler so a usage error renders through the runner (exit
1827
+ // 64, `--json` envelope) instead of escaping the route as an uncaught rejection, and before
1828
+ // the version check so a typo never waits on (or restarts) the daemon.
1829
+ () => {
1830
+ const { selector, target, selectorOrTarget } = splitOptionalSelectorAndTarget(
1831
+ context.args,
1832
+ "tools [selector] [name]"
1833
+ );
1834
+ const limit = parsePositiveIntegerOption(options.limit, "--limit");
1835
+ const offset = parseNonNegativeIntegerOption(options.offset, "--offset");
1836
+ const filter = readTextOption(context.argv, options.filter, "--filter");
1837
+ const group = readTextOption(context.argv, options.group, "--group");
1838
+ if (group !== void 0 && !isValidToolGroup(group)) {
1839
+ throw usageError(
1840
+ `"--group" must be a group name like "checkout" or "checkout/payment" (one or two "/"-separated segments of [a-zA-Z0-9_-], at most 64 characters each); got ${JSON.stringify(group)}.`
1841
+ );
1842
+ }
1843
+ const groups = options.groups === void 0 ? void 0 : Boolean(options.groups);
1844
+ if (groups && options.full) {
1845
+ throw usageError('"--groups" lists groups only and cannot be combined with "--full".');
1846
+ }
1847
+ return guarded(context)(
1848
+ () => handleToolsCommand(
1849
+ { selector: selector ?? selectorOrTarget, name: target, group, groups, filter, limit, offset },
1850
+ { stateDir }
1851
+ )
1852
+ )();
1853
+ },
1854
+ context.env,
1855
+ { full: Boolean(options.full) }
1856
+ );
1857
+ };
1858
+ export {
1859
+ route
1860
+ };