@openclaw/crabline 0.1.9 → 0.1.11

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 (189) hide show
  1. package/README.md +47 -22
  2. package/assets/crabline-banner.svg +20 -0
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +25 -1
  5. package/dist/src/cli/program.js +579 -82
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +59 -6
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +116 -92
  10. package/dist/src/config/schema.js +179 -73
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.js +9 -2
  13. package/dist/src/core/errors.js.map +1 -1
  14. package/dist/src/core/exit-codes.js.map +1 -1
  15. package/dist/src/core/matcher.js +7 -6
  16. package/dist/src/core/matcher.js.map +1 -1
  17. package/dist/src/core/message-template.js.map +1 -1
  18. package/dist/src/core/nonces.d.ts +1 -0
  19. package/dist/src/core/nonces.js +4 -2
  20. package/dist/src/core/nonces.js.map +1 -1
  21. package/dist/src/core/reporters.d.ts +1 -0
  22. package/dist/src/core/reporters.js +42 -4
  23. package/dist/src/core/reporters.js.map +1 -1
  24. package/dist/src/core/run.d.ts +3 -0
  25. package/dist/src/core/run.js +415 -107
  26. package/dist/src/core/run.js.map +1 -1
  27. package/dist/src/core/safe-regex.d.ts +3 -0
  28. package/dist/src/core/safe-regex.js +21 -0
  29. package/dist/src/core/safe-regex.js.map +1 -0
  30. package/dist/src/index.d.ts +2 -2
  31. package/dist/src/index.js +1 -1
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/openclaw/artifact-generation.d.ts +43 -0
  34. package/dist/src/openclaw/artifact-generation.js +363 -0
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -0
  36. package/dist/src/openclaw/bridges/matrix.js +97 -12
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +32 -10
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/signal.js +55 -18
  41. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  42. package/dist/src/openclaw/bridges/slack.js +85 -7
  43. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  44. package/dist/src/openclaw/bridges/telegram.js +124 -30
  45. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  46. package/dist/src/openclaw/bridges/whatsapp.js +78 -17
  47. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  48. package/dist/src/openclaw/bridges/zalo.js +14 -5
  49. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  50. package/dist/src/openclaw/outbound-contract.d.ts +5 -0
  51. package/dist/src/openclaw/outbound-contract.js +40 -0
  52. package/dist/src/openclaw/outbound-contract.js.map +1 -0
  53. package/dist/src/openclaw/private-file.d.ts +28 -0
  54. package/dist/src/openclaw/private-file.js +465 -0
  55. package/dist/src/openclaw/private-file.js.map +1 -0
  56. package/dist/src/openclaw/shared.d.ts +21 -2
  57. package/dist/src/openclaw/shared.js +115 -18
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +63 -0
  60. package/dist/src/openclaw/smoke-lock.js +1004 -0
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -0
  62. package/dist/src/openclaw.d.ts +16 -8
  63. package/dist/src/openclaw.js +334 -52
  64. package/dist/src/openclaw.js.map +1 -1
  65. package/dist/src/providers/builtin/discord.d.ts +10 -1
  66. package/dist/src/providers/builtin/discord.js +144 -18
  67. package/dist/src/providers/builtin/discord.js.map +1 -1
  68. package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
  69. package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
  70. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
  71. package/dist/src/providers/builtin/feishu.d.ts +19 -2
  72. package/dist/src/providers/builtin/feishu.js +307 -25
  73. package/dist/src/providers/builtin/feishu.js.map +1 -1
  74. package/dist/src/providers/builtin/googlechat.d.ts +20 -1
  75. package/dist/src/providers/builtin/googlechat.js +178 -19
  76. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  77. package/dist/src/providers/builtin/imessage.d.ts +4 -0
  78. package/dist/src/providers/builtin/imessage.js +26 -11
  79. package/dist/src/providers/builtin/imessage.js.map +1 -1
  80. package/dist/src/providers/builtin/loopback.js +92 -30
  81. package/dist/src/providers/builtin/loopback.js.map +1 -1
  82. package/dist/src/providers/builtin/matrix.d.ts +11 -1
  83. package/dist/src/providers/builtin/matrix.js +53 -29
  84. package/dist/src/providers/builtin/matrix.js.map +1 -1
  85. package/dist/src/providers/builtin/mattermost.d.ts +10 -0
  86. package/dist/src/providers/builtin/mattermost.js +38 -15
  87. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  88. package/dist/src/providers/builtin/msteams.d.ts +17 -1
  89. package/dist/src/providers/builtin/msteams.js +113 -14
  90. package/dist/src/providers/builtin/msteams.js.map +1 -1
  91. package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
  92. package/dist/src/providers/builtin/native-local-mock.js +12 -35
  93. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  94. package/dist/src/providers/builtin/script.d.ts +11 -15
  95. package/dist/src/providers/builtin/script.js +566 -104
  96. package/dist/src/providers/builtin/script.js.map +1 -1
  97. package/dist/src/providers/builtin/slack.d.ts +6 -0
  98. package/dist/src/providers/builtin/slack.js +99 -40
  99. package/dist/src/providers/builtin/slack.js.map +1 -1
  100. package/dist/src/providers/builtin/telegram.d.ts +6 -0
  101. package/dist/src/providers/builtin/telegram.js +64 -43
  102. package/dist/src/providers/builtin/telegram.js.map +1 -1
  103. package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
  104. package/dist/src/providers/builtin/whatsapp.js +567 -30
  105. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  106. package/dist/src/providers/builtin/zalo.js +30 -19
  107. package/dist/src/providers/builtin/zalo.js.map +1 -1
  108. package/dist/src/providers/catalog.d.ts +3 -3
  109. package/dist/src/providers/catalog.js +3 -3
  110. package/dist/src/providers/catalog.js.map +1 -1
  111. package/dist/src/providers/local-mock.d.ts +11 -2
  112. package/dist/src/providers/local-mock.js +322 -96
  113. package/dist/src/providers/local-mock.js.map +1 -1
  114. package/dist/src/providers/native-ids.d.ts +1 -0
  115. package/dist/src/providers/native-ids.js +6 -1
  116. package/dist/src/providers/native-ids.js.map +1 -1
  117. package/dist/src/providers/recorder.d.ts +30 -2
  118. package/dist/src/providers/recorder.js +549 -70
  119. package/dist/src/providers/recorder.js.map +1 -1
  120. package/dist/src/providers/registry.d.ts +25 -1
  121. package/dist/src/providers/registry.js +280 -40
  122. package/dist/src/providers/registry.js.map +1 -1
  123. package/dist/src/providers/signed-jwt.d.ts +29 -0
  124. package/dist/src/providers/signed-jwt.js +202 -0
  125. package/dist/src/providers/signed-jwt.js.map +1 -0
  126. package/dist/src/providers/slack-ids.js.map +1 -1
  127. package/dist/src/providers/target-normalizers.d.ts +26 -0
  128. package/dist/src/providers/target-normalizers.js +276 -0
  129. package/dist/src/providers/target-normalizers.js.map +1 -0
  130. package/dist/src/providers/types.d.ts +5 -0
  131. package/dist/src/providers/types.js.map +1 -1
  132. package/dist/src/providers/webhook-server.d.ts +2 -0
  133. package/dist/src/providers/webhook-server.js +72 -25
  134. package/dist/src/providers/webhook-server.js.map +1 -1
  135. package/dist/src/servers/http.d.ts +23 -3
  136. package/dist/src/servers/http.js +213 -20
  137. package/dist/src/servers/http.js.map +1 -1
  138. package/dist/src/servers/index.js +20 -21
  139. package/dist/src/servers/index.js.map +1 -1
  140. package/dist/src/servers/matrix.js +465 -51
  141. package/dist/src/servers/matrix.js.map +1 -1
  142. package/dist/src/servers/mattermost.d.ts +5 -0
  143. package/dist/src/servers/mattermost.js +306 -58
  144. package/dist/src/servers/mattermost.js.map +1 -1
  145. package/dist/src/servers/pending-events.d.ts +2 -0
  146. package/dist/src/servers/pending-events.js +11 -0
  147. package/dist/src/servers/pending-events.js.map +1 -0
  148. package/dist/src/servers/recorder.d.ts +5 -0
  149. package/dist/src/servers/recorder.js +262 -3
  150. package/dist/src/servers/recorder.js.map +1 -1
  151. package/dist/src/servers/signal.d.ts +2 -0
  152. package/dist/src/servers/signal.js +444 -56
  153. package/dist/src/servers/signal.js.map +1 -1
  154. package/dist/src/servers/slack.d.ts +5 -0
  155. package/dist/src/servers/slack.js +515 -49
  156. package/dist/src/servers/slack.js.map +1 -1
  157. package/dist/src/servers/telegram.d.ts +111 -0
  158. package/dist/src/servers/telegram.js +830 -158
  159. package/dist/src/servers/telegram.js.map +1 -1
  160. package/dist/src/servers/webhook-target.d.ts +40 -0
  161. package/dist/src/servers/webhook-target.js +349 -0
  162. package/dist/src/servers/webhook-target.js.map +1 -0
  163. package/dist/src/servers/websocket.d.ts +2 -0
  164. package/dist/src/servers/websocket.js +29 -0
  165. package/dist/src/servers/websocket.js.map +1 -0
  166. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
  167. package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
  168. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  169. package/dist/src/servers/whatsapp-jid.d.ts +5 -0
  170. package/dist/src/servers/whatsapp-jid.js +32 -0
  171. package/dist/src/servers/whatsapp-jid.js.map +1 -0
  172. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
  173. package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
  174. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  175. package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
  176. package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
  177. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  178. package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
  179. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  181. package/dist/src/servers/whatsapp.d.ts +12 -7
  182. package/dist/src/servers/whatsapp.js +269 -107
  183. package/dist/src/servers/whatsapp.js.map +1 -1
  184. package/dist/src/servers/zalo.d.ts +14 -0
  185. package/dist/src/servers/zalo.js +521 -88
  186. package/dist/src/servers/zalo.js.map +1 -1
  187. package/docs/channel-setup.md +197 -19
  188. package/fixtures/examples/crabline.example.yaml +10 -0
  189. package/package.json +20 -15
@@ -1,12 +1,17 @@
1
- import { Command } from "commander";
1
+ import { Command, CommanderError } from "commander";
2
+ import { randomUUID } from "node:crypto";
3
+ import { constants as fsConstants } from "node:fs";
2
4
  import fs from "node:fs/promises";
3
5
  import nodePath from "node:path";
6
+ import { lock } from "proper-lockfile";
4
7
  import { loadManifest } from "../config/load.js";
5
8
  import { createRegistry } from "../providers/registry.js";
6
- import { formatJson, formatRunResultText } from "../core/reporters.js";
7
- import { computeExitCode, runFixtureCommand, runSuite } from "../core/run.js";
9
+ import { formatJson, formatRunResultText, sanitizeTerminalText } from "../core/reporters.js";
10
+ import { assertScriptStdinPayloadSize, computeExitCode, runFixtureCommand, runSuite, } from "../core/run.js";
8
11
  import { CrablineError, ensureErrorMessage } from "../core/errors.js";
9
12
  import { isCrablineServerChannel, startCrablineServer, } from "../servers/index.js";
13
+ const READY_FILE_LEASE_STALE_MS = 60_000;
14
+ const READY_FILE_LEASE_UPDATE_MS = 1_000;
10
15
  const SERVE_PARAM_FACTORIES = {
11
16
  mattermost: (shared, commandOptions) => ({
12
17
  ...shared,
@@ -56,17 +61,85 @@ const SERVE_PARAM_FACTORIES = {
56
61
  channel: "zalo",
57
62
  }),
58
63
  };
59
- function print(value) {
60
- process.stdout.write(`${value}\n`);
64
+ function isClosedPipeError(error) {
65
+ const code = error.code;
66
+ return code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
67
+ }
68
+ async function writeOutput(stream, value) {
69
+ try {
70
+ await new Promise((resolve, reject) => {
71
+ let drained = false;
72
+ let written = false;
73
+ const cleanup = () => {
74
+ stream.off("drain", onDrain);
75
+ stream.off("error", onError);
76
+ };
77
+ const finish = () => {
78
+ if (drained && written) {
79
+ cleanup();
80
+ resolve();
81
+ }
82
+ };
83
+ const onDrain = () => {
84
+ drained = true;
85
+ finish();
86
+ };
87
+ const onError = (error) => {
88
+ cleanup();
89
+ reject(error);
90
+ };
91
+ stream.once("error", onError);
92
+ let hasCapacity;
93
+ try {
94
+ hasCapacity = stream.write(value, (error) => {
95
+ if (error) {
96
+ stream.once("error", () => undefined);
97
+ onError(error);
98
+ return;
99
+ }
100
+ written = true;
101
+ finish();
102
+ });
103
+ }
104
+ catch (error) {
105
+ cleanup();
106
+ reject(error);
107
+ return;
108
+ }
109
+ if (hasCapacity) {
110
+ drained = true;
111
+ finish();
112
+ }
113
+ else {
114
+ stream.once("drain", onDrain);
115
+ }
116
+ });
117
+ return true;
118
+ }
119
+ catch (error) {
120
+ if (isClosedPipeError(error)) {
121
+ return false;
122
+ }
123
+ throw error;
124
+ }
125
+ }
126
+ async function print(value) {
127
+ return await writeOutput(process.stdout, `${value}\n`);
61
128
  }
62
129
  async function withManifest(options, action) {
63
130
  return action(await loadManifest(options.config));
64
131
  }
65
132
  export function createProgram(setExitCode = (code) => {
66
133
  process.exitCode = code;
67
- }) {
134
+ }, dependencies = {}) {
68
135
  const program = new Command();
136
+ const acquireReadyLease = dependencies.acquireReadyFileLease ?? acquireReadyFileLease;
137
+ const registryFactory = dependencies.createRegistry ?? createRegistry;
138
+ const publish = dependencies.publishReadyFile ?? publishReadyFileUnlocked;
139
+ const removeReady = dependencies.removeReadyFile ?? removeReadyFileIfOwned;
140
+ const startServer = dependencies.startServer ?? startCrablineServer;
69
141
  program
142
+ .exitOverride()
70
143
  .name("crabline")
71
144
  .description("Deterministic CLI harness for messaging provider E2E tests")
72
145
  .option("-c, --config <path>", "Config file path")
@@ -78,7 +151,7 @@ export function createProgram(setExitCode = (code) => {
78
151
  .action(async () => {
79
152
  const options = program.opts();
80
153
  const { manifest, path } = await loadManifest(options.config);
81
- const registry = createRegistry(manifest, path);
154
+ const registry = registryFactory(manifest, path);
82
155
  const payload = {
83
156
  configured: Object.entries(manifest.providers).map(([id, config]) => ({
84
157
  adapter: config.adapter,
@@ -89,7 +162,7 @@ export function createProgram(setExitCode = (code) => {
89
162
  })),
90
163
  support: registry.catalog,
91
164
  };
92
- print(options.json ? formatJson(payload) : renderProvidersText(payload));
165
+ await print(options.json ? formatJson(payload) : renderProvidersText(payload));
93
166
  });
94
167
  program
95
168
  .command("fixtures")
@@ -97,24 +170,23 @@ export function createProgram(setExitCode = (code) => {
97
170
  .action(async () => {
98
171
  const options = program.opts();
99
172
  const { manifest } = await loadManifest(options.config);
100
- print(options.json
173
+ await print(options.json
101
174
  ? formatJson(manifest.fixtures)
102
175
  : manifest.fixtures
103
- .map((fixture) => `${fixture.id} ${fixture.mode} provider=${fixture.provider} target=${fixture.target.id}`)
176
+ .map((fixture) => `${sanitizeTerminalText(fixture.id, true)} ${sanitizeTerminalText(fixture.mode, true)} provider=${sanitizeTerminalText(fixture.provider, true)} target=${sanitizeTerminalText(fixture.target.id, true)}`)
104
177
  .join("\n"));
105
178
  });
106
179
  program
107
- .command("probe <fixtureOrProvider>")
108
- .description("Probe provider readiness using a fixture or provider id")
109
- .action(async (fixtureOrProvider) => {
180
+ .command("probe <fixtureId>")
181
+ .description("Probe provider readiness using a fixture")
182
+ .action(async (fixtureId) => {
110
183
  const options = program.opts();
111
184
  const { manifest, path } = await loadManifest(options.config);
112
- const fixture = manifest.fixtures.find((entry) => entry.id === fixtureOrProvider) ??
113
- manifest.fixtures.find((entry) => entry.provider === fixtureOrProvider);
185
+ const fixture = manifest.fixtures.find((entry) => entry.id === fixtureId);
114
186
  if (!fixture) {
115
- throw new CrablineError(`No fixture found for "${fixtureOrProvider}"`, { kind: "config" });
187
+ throw new CrablineError(`Unknown fixture: ${fixtureId}`, { kind: "config" });
116
188
  }
117
- const registry = createRegistry(manifest, path);
189
+ const registry = registryFactory(manifest, path);
118
190
  const result = await runFixtureCommand({
119
191
  fixtureId: fixture.id,
120
192
  manifest,
@@ -122,7 +194,7 @@ export function createProgram(setExitCode = (code) => {
122
194
  modeOverride: "probe",
123
195
  registry,
124
196
  });
125
- print(options.json ? formatJson(result) : formatRunResultText(result));
197
+ await print(options.json ? formatJson(result) : formatRunResultText(result));
126
198
  setExitCode(computeExitCode(result));
127
199
  });
128
200
  for (const mode of ["send", "roundtrip", "agent"]) {
@@ -132,7 +204,7 @@ export function createProgram(setExitCode = (code) => {
132
204
  .action(async (fixtureId) => {
133
205
  const options = program.opts();
134
206
  const { manifest, path } = await loadManifest(options.config);
135
- const registry = createRegistry(manifest, path);
207
+ const registry = registryFactory(manifest, path);
136
208
  const result = await runFixtureCommand({
137
209
  fixtureId,
138
210
  manifest,
@@ -140,7 +212,7 @@ export function createProgram(setExitCode = (code) => {
140
212
  modeOverride: mode,
141
213
  registry,
142
214
  });
143
- print(options.json ? formatJson(result) : formatRunResultText(result));
215
+ await print(options.json ? formatJson(result) : formatRunResultText(result));
144
216
  setExitCode(computeExitCode(result));
145
217
  });
146
218
  }
@@ -150,14 +222,14 @@ export function createProgram(setExitCode = (code) => {
150
222
  .action(async (fixtureIds) => {
151
223
  const options = program.opts();
152
224
  const { manifest, path } = await loadManifest(options.config);
153
- const registry = createRegistry(manifest, path);
225
+ const registry = registryFactory(manifest, path);
154
226
  const result = await runSuite({
155
227
  fixtureIds,
156
228
  manifest,
157
229
  manifestPath: path,
158
230
  registry,
159
231
  });
160
- print(options.json ? formatJson(result) : formatRunResultText(result));
232
+ await print(options.json ? formatJson(result) : formatRunResultText(result));
161
233
  setExitCode(computeExitCode(result));
162
234
  });
163
235
  program
@@ -171,28 +243,83 @@ export function createProgram(setExitCode = (code) => {
171
243
  if (!fixture) {
172
244
  throw new CrablineError(`Unknown fixture: ${fixtureId}`, { kind: "config" });
173
245
  }
174
- const provider = createRegistry(manifest, path).resolve(fixture.provider, fixture.id);
246
+ const provider = registryFactory(manifest, path).resolve(fixture.provider, fixture.id);
175
247
  if (!provider.watch) {
176
248
  throw new CrablineError(`Provider "${fixture.provider}" does not implement watch.`, {
177
249
  kind: "config",
178
250
  });
179
251
  }
252
+ const controller = new AbortController();
253
+ let iterator;
254
+ const stopWatch = onceAsync(async () => {
255
+ controller.abort();
256
+ await iterator?.return?.();
257
+ });
258
+ const shutdown = installShutdownHandler(stopWatch);
259
+ const lifecycleErrors = [];
180
260
  try {
181
- for await (const message of provider.watch({
261
+ const watchContext = {
182
262
  config: manifest.providers[fixture.provider],
183
263
  fixture,
184
264
  manifestPath: path,
185
265
  providerId: fixture.provider,
266
+ signal: controller.signal,
186
267
  userName: manifest.userName,
187
- })) {
188
- print(options.json
268
+ };
269
+ if (watchContext.config.adapter === "script") {
270
+ assertScriptStdinPayloadSize({
271
+ fixture,
272
+ provider: {
273
+ config: watchContext.config,
274
+ id: fixture.provider,
275
+ manifestPath: path,
276
+ },
277
+ watch: {
278
+ target: fixture.target,
279
+ },
280
+ });
281
+ }
282
+ const watch = provider.watch(watchContext);
283
+ iterator = watch[Symbol.asyncIterator]();
284
+ while (true) {
285
+ const result = await iterator.next();
286
+ if (result.done) {
287
+ break;
288
+ }
289
+ const message = result.value;
290
+ const written = await print(options.json
189
291
  ? formatJson(message)
190
- : `${message.sentAt} ${message.author} ${message.text}`);
292
+ : `${sanitizeTerminalText(message.sentAt, true)} ${sanitizeTerminalText(message.author, true)} ${sanitizeTerminalText(message.text, true)}`);
293
+ if (!written) {
294
+ break;
295
+ }
191
296
  }
192
297
  }
193
- finally {
298
+ catch (error) {
299
+ lifecycleErrors.push(error);
300
+ }
301
+ try {
302
+ await stopWatch();
303
+ }
304
+ catch (error) {
305
+ if (!lifecycleErrors.includes(error)) {
306
+ lifecycleErrors.push(error);
307
+ }
308
+ }
309
+ try {
194
310
  await provider.cleanup?.();
195
311
  }
312
+ catch (error) {
313
+ if (!lifecycleErrors.includes(error)) {
314
+ lifecycleErrors.push(error);
315
+ }
316
+ }
317
+ finally {
318
+ shutdown.dispose();
319
+ }
320
+ if (lifecycleErrors.length > 0) {
321
+ throw combineLifecycleErrors(lifecycleErrors, "Crabline watch lifecycle failed.");
322
+ }
196
323
  });
197
324
  });
198
325
  program
@@ -200,15 +327,16 @@ export function createProgram(setExitCode = (code) => {
200
327
  .description("Start a local provider server that OpenClaw live adapters can target")
201
328
  .option("--host <host>", "Bind host", "127.0.0.1")
202
329
  .option("--port <port>", "Bind port", "0")
203
- .option("--admin-token <token>", "Admin token for inbound test messages")
330
+ .option("--admin-token <token>", "Admin token for inbound test messages (or CRABLINE_ADMIN_TOKEN)")
204
331
  .option("--account <number>", "Signal account number")
205
- .option("--access-token <token>", "Matrix or WhatsApp access token")
206
- .option("--bot-token <token>", "Mattermost, Slack, Telegram, or Zalo bot token")
332
+ .option("--access-token <token>", "Matrix or WhatsApp access token (or CRABLINE_ACCESS_TOKEN)")
333
+ .option("--bot-token <token>", "Mattermost, Slack, Telegram, or Zalo bot token (or CRABLINE_BOT_TOKEN)")
207
334
  .option("--bot-username <username>", "Mattermost, Telegram, or Zalo bot username", "crabline_bot")
208
335
  .option("--recorder <path>", "JSONL recorder path")
209
336
  .option("--ready-file <path>", "Write the server runtime manifest to this path")
210
337
  .option("--self-jid <jid>", "WhatsApp self JID")
211
- .option("--signing-secret <secret>", "Slack signing secret")
338
+ .option("--show-secrets", "Print provider credentials in text output", false)
339
+ .option("--signing-secret <secret>", "Slack signing secret (or CRABLINE_SIGNING_SECRET)")
212
340
  .option("--once", "Start, print the runtime manifest, and stop immediately", false)
213
341
  .action(async (provider, commandOptions) => {
214
342
  const options = program.opts();
@@ -217,6 +345,15 @@ export function createProgram(setExitCode = (code) => {
217
345
  kind: "config",
218
346
  });
219
347
  }
348
+ commandOptions.accessToken ??= process.env.CRABLINE_ACCESS_TOKEN;
349
+ commandOptions.adminToken ??= process.env.CRABLINE_ADMIN_TOKEN;
350
+ commandOptions.botToken ??= process.env.CRABLINE_BOT_TOKEN;
351
+ commandOptions.signingSecret ??= process.env.CRABLINE_SIGNING_SECRET;
352
+ if (!/^[0-9]+$/u.test(commandOptions.port)) {
353
+ throw new CrablineError(`Invalid local server port: ${commandOptions.port}`, {
354
+ kind: "config",
355
+ });
356
+ }
220
357
  const port = Number(commandOptions.port);
221
358
  if (!Number.isInteger(port) || port < 0 || port > 65_535) {
222
359
  throw new CrablineError(`Invalid local server port: ${commandOptions.port}`, {
@@ -229,22 +366,116 @@ export function createProgram(setExitCode = (code) => {
229
366
  kind: "config",
230
367
  });
231
368
  }
232
- const server = await startCrablineServer(factory({
233
- host: commandOptions.host,
234
- port,
235
- recorderPath: commandOptions.recorder,
236
- }, commandOptions));
237
- const payload = formatJson(server.manifest);
238
- if (commandOptions.readyFile) {
239
- await fs.mkdir(nodePath.dirname(commandOptions.readyFile), { recursive: true });
240
- await fs.writeFile(commandOptions.readyFile, `${payload}\n`, "utf8");
241
- }
242
- print(options.json ? payload : renderServeText(server.manifest));
243
- if (commandOptions.once) {
244
- await server.close();
245
- return;
369
+ let server;
370
+ let startupSettled = false;
371
+ let settleStartup;
372
+ const startup = new Promise((resolve) => {
373
+ settleStartup = resolve;
374
+ });
375
+ const finishStartup = () => {
376
+ if (!startupSettled) {
377
+ startupSettled = true;
378
+ settleStartup?.();
379
+ }
380
+ };
381
+ let publicationSettled = false;
382
+ let settlePublication;
383
+ const publication = new Promise((resolve) => {
384
+ settlePublication = resolve;
385
+ });
386
+ const finishPublication = () => {
387
+ if (!publicationSettled) {
388
+ publicationSettled = true;
389
+ settlePublication?.();
390
+ }
391
+ };
392
+ const close = onceAsync(async () => {
393
+ await startup;
394
+ await publication;
395
+ await server?.close();
396
+ });
397
+ const shutdown = installShutdownHandler(close);
398
+ let releaseReadyLease;
399
+ let publishedReady;
400
+ let actionFailed = false;
401
+ let actionError;
402
+ try {
403
+ if (commandOptions.readyFile) {
404
+ releaseReadyLease = await acquireReadyLease(commandOptions.readyFile);
405
+ }
406
+ if (shutdown.requested()) {
407
+ finishStartup();
408
+ finishPublication();
409
+ await shutdown.wait();
410
+ }
411
+ else {
412
+ try {
413
+ server = await startServer(factory({
414
+ host: commandOptions.host,
415
+ port,
416
+ recorderPath: commandOptions.recorder,
417
+ }, commandOptions));
418
+ }
419
+ finally {
420
+ finishStartup();
421
+ }
422
+ if (shutdown.requested()) {
423
+ finishPublication();
424
+ await shutdown.wait();
425
+ }
426
+ else {
427
+ try {
428
+ const payload = formatJson(server.manifest);
429
+ if (commandOptions.readyFile) {
430
+ const readyContents = `${payload}\n`;
431
+ publishedReady = {
432
+ contents: readyContents,
433
+ identity: await publish(commandOptions.readyFile, readyContents),
434
+ };
435
+ }
436
+ await print(options.json
437
+ ? payload
438
+ : renderServeText(server.manifest, commandOptions.showSecrets === true));
439
+ }
440
+ finally {
441
+ finishPublication();
442
+ }
443
+ if (shutdown.requested() || !commandOptions.once) {
444
+ await shutdown.wait();
445
+ }
446
+ }
447
+ }
448
+ }
449
+ catch (error) {
450
+ actionFailed = true;
451
+ actionError = error;
452
+ }
453
+ finishStartup();
454
+ finishPublication();
455
+ const cleanupErrors = [];
456
+ const readyToRemove = publishedReady;
457
+ for (const cleanup of [
458
+ close,
459
+ ...(commandOptions.readyFile && readyToRemove
460
+ ? [
461
+ () => removeReady(commandOptions.readyFile, readyToRemove.contents, readyToRemove.identity),
462
+ ]
463
+ : []),
464
+ ...(releaseReadyLease ? [releaseReadyLease] : []),
465
+ ]) {
466
+ try {
467
+ await cleanup();
468
+ }
469
+ catch (error) {
470
+ if (!actionFailed || error !== actionError) {
471
+ cleanupErrors.push(error);
472
+ }
473
+ }
474
+ }
475
+ shutdown.dispose();
476
+ if (actionFailed || cleanupErrors.length > 0) {
477
+ throw combineLifecycleErrors(actionFailed ? [actionError, ...cleanupErrors] : cleanupErrors, "Crabline server lifecycle failed.");
246
478
  }
247
- await waitForShutdown(server.close);
248
479
  });
249
480
  program
250
481
  .command("doctor")
@@ -255,28 +486,242 @@ export function createProgram(setExitCode = (code) => {
255
486
  const findings = diagnose(manifest);
256
487
  const ok = findings.length === 0;
257
488
  const payload = { findings, ok };
258
- print(options.json ? formatJson(payload) : ok ? "doctor ok" : findings.join("\n"));
489
+ await print(options.json ? formatJson(payload) : ok ? "doctor ok" : findings.join("\n"));
259
490
  setExitCode(ok ? 0 : 10);
260
491
  });
261
492
  return program;
262
493
  }
263
- function waitForShutdown(close) {
264
- return new Promise((resolve, reject) => {
265
- const shutdown = () => {
266
- close().then(resolve, reject);
267
- };
268
- process.once("SIGINT", shutdown);
269
- process.once("SIGTERM", shutdown);
494
+ function onceAsync(action) {
495
+ let result;
496
+ return () => (result ??= Promise.resolve().then(action));
497
+ }
498
+ async function readReadyFileIdentity(filePath) {
499
+ const stats = await fs.stat(filePath, { bigint: true });
500
+ return {
501
+ birthtimeNs: stats.birthtimeNs,
502
+ ctimeNs: stats.ctimeNs,
503
+ dev: stats.dev,
504
+ ino: stats.ino,
505
+ size: stats.size,
506
+ };
507
+ }
508
+ function sameReadyFileIdentity(left, right) {
509
+ return (left.birthtimeNs === right.birthtimeNs &&
510
+ left.ctimeNs === right.ctimeNs &&
511
+ left.dev === right.dev &&
512
+ left.ino === right.ino &&
513
+ left.size === right.size);
514
+ }
515
+ function sameReadyFileObject(left, right) {
516
+ return (left.birthtimeNs === right.birthtimeNs &&
517
+ left.dev === right.dev &&
518
+ left.ino === right.ino &&
519
+ left.size === right.size);
520
+ }
521
+ async function withReadyFileLock(filePath, action) {
522
+ const release = await acquireReadyFileLease(filePath);
523
+ let actionFailed = false;
524
+ let actionError;
525
+ let result;
526
+ try {
527
+ result = await action();
528
+ }
529
+ catch (error) {
530
+ actionFailed = true;
531
+ actionError = error;
532
+ }
533
+ try {
534
+ await release();
535
+ }
536
+ catch (releaseError) {
537
+ if (actionFailed) {
538
+ throw combineLifecycleErrors([actionError, releaseError], `Ready-file action and lock release both failed for "${filePath}".`);
539
+ }
540
+ throw releaseError;
541
+ }
542
+ if (actionFailed) {
543
+ throw actionError;
544
+ }
545
+ return result;
546
+ }
547
+ async function acquireReadyFileLease(filePath) {
548
+ await fs.mkdir(nodePath.dirname(filePath), { recursive: true });
549
+ return await lock(filePath, {
550
+ realpath: false,
551
+ retries: {
552
+ factor: 1,
553
+ maxTimeout: 10,
554
+ minTimeout: 10,
555
+ retries: 100,
556
+ },
557
+ stale: READY_FILE_LEASE_STALE_MS,
558
+ update: READY_FILE_LEASE_UPDATE_MS,
270
559
  });
271
560
  }
272
- function renderServeText(manifest) {
561
+ export async function publishReadyFile(filePath, contents) {
562
+ let publishedIdentity;
563
+ try {
564
+ return await withReadyFileLock(filePath, async () => {
565
+ publishedIdentity = await publishReadyFileUnlocked(filePath, contents);
566
+ return publishedIdentity;
567
+ });
568
+ }
569
+ catch (error) {
570
+ if (publishedIdentity) {
571
+ try {
572
+ await removeReadyFileIfOwned(filePath, contents, publishedIdentity);
573
+ }
574
+ catch (cleanupError) {
575
+ const aggregateError = new AggregateError([error, cleanupError], `Ready-file publication and compensation both failed for "${filePath}".`);
576
+ aggregateError.cause = error;
577
+ throw aggregateError;
578
+ }
579
+ }
580
+ throw error;
581
+ }
582
+ }
583
+ async function publishReadyFileUnlocked(filePath, contents) {
584
+ const suffix = `${process.pid}.${randomUUID()}.tmp`;
585
+ const temporaryPath = nodePath.join(nodePath.dirname(filePath), `.${nodePath.basename(filePath)}.${suffix}`);
586
+ const backupPath = `${temporaryPath}.backup`;
587
+ let manifestPublished = false;
588
+ let destinationBackedUp = false;
589
+ try {
590
+ await fs.writeFile(temporaryPath, contents, {
591
+ encoding: "utf8",
592
+ flag: "wx",
593
+ mode: 0o600,
594
+ });
595
+ destinationBackedUp = await backupReadyFile(filePath, backupPath);
596
+ await fs.rename(temporaryPath, filePath);
597
+ manifestPublished = true;
598
+ const identity = await readReadyFileIdentity(filePath);
599
+ if (destinationBackedUp) {
600
+ await fs.rm(backupPath);
601
+ destinationBackedUp = false;
602
+ }
603
+ return identity;
604
+ }
605
+ catch (error) {
606
+ const recoveryErrors = [];
607
+ if (manifestPublished && !destinationBackedUp) {
608
+ await fs.rm(filePath, { force: true }).catch((cleanupError) => {
609
+ recoveryErrors.push(cleanupError);
610
+ });
611
+ }
612
+ if (destinationBackedUp) {
613
+ await fs.rename(backupPath, filePath).catch((restoreError) => {
614
+ recoveryErrors.push(restoreError);
615
+ });
616
+ }
617
+ if (recoveryErrors.length > 0) {
618
+ const aggregateError = new AggregateError([error, ...recoveryErrors], `Ready-file replacement and recovery both failed for "${filePath}".`);
619
+ aggregateError.cause = error;
620
+ throw aggregateError;
621
+ }
622
+ throw error;
623
+ }
624
+ finally {
625
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
626
+ }
627
+ }
628
+ async function backupReadyFile(filePath, backupPath) {
629
+ try {
630
+ await fs.copyFile(filePath, backupPath, fsConstants.COPYFILE_EXCL);
631
+ return true;
632
+ }
633
+ catch (error) {
634
+ if (error.code === "ENOENT") {
635
+ return false;
636
+ }
637
+ throw error;
638
+ }
639
+ }
640
+ async function removeReadyFileIfOwned(filePath, expectedContents, expectedIdentity) {
641
+ const tombstonePath = nodePath.join(nodePath.dirname(filePath), `.${nodePath.basename(filePath)}.${process.pid}.${randomUUID()}.remove`);
642
+ try {
643
+ if ((await fs.readFile(filePath, "utf8")) !== expectedContents ||
644
+ !sameReadyFileIdentity(await readReadyFileIdentity(filePath), expectedIdentity)) {
645
+ return;
646
+ }
647
+ await fs.rename(filePath, tombstonePath);
648
+ if ((await fs.readFile(tombstonePath, "utf8")) === expectedContents &&
649
+ sameReadyFileObject(await readReadyFileIdentity(tombstonePath), expectedIdentity)) {
650
+ await fs.rm(tombstonePath);
651
+ return;
652
+ }
653
+ try {
654
+ await fs.link(tombstonePath, filePath);
655
+ await fs.rm(tombstonePath);
656
+ }
657
+ catch (restoreError) {
658
+ throw new Error(`Ready-file path changed while removing "${filePath}"; the moved file remains at "${tombstonePath}".`, { cause: restoreError });
659
+ }
660
+ }
661
+ catch (error) {
662
+ if (error.code !== "ENOENT") {
663
+ throw error;
664
+ }
665
+ }
666
+ }
667
+ export async function removeReadyFile(filePath, expectedContents, expectedIdentity) {
668
+ await withReadyFileLock(filePath, async () => {
669
+ await removeReadyFileIfOwned(filePath, expectedContents, expectedIdentity);
670
+ });
671
+ }
672
+ export function waitForShutdown(close, signalTarget = process) {
673
+ return installShutdownHandler(close, signalTarget).wait();
674
+ }
675
+ function installShutdownHandler(close, signalTarget = process) {
676
+ let shutdownPromise;
677
+ let resolveWait;
678
+ let rejectWait;
679
+ const wait = new Promise((resolve, reject) => {
680
+ resolveWait = resolve;
681
+ rejectWait = reject;
682
+ });
683
+ void wait.catch(() => undefined);
684
+ const removeListeners = () => {
685
+ signalTarget.removeListener("SIGINT", shutdown);
686
+ signalTarget.removeListener("SIGTERM", shutdown);
687
+ };
688
+ const shutdown = () => {
689
+ if (shutdownPromise) {
690
+ return;
691
+ }
692
+ shutdownPromise = Promise.resolve().then(close);
693
+ void shutdownPromise.then(() => {
694
+ removeListeners();
695
+ resolveWait?.();
696
+ }, (error) => {
697
+ removeListeners();
698
+ rejectWait?.(error);
699
+ });
700
+ };
701
+ signalTarget.on("SIGINT", shutdown);
702
+ signalTarget.on("SIGTERM", shutdown);
703
+ return {
704
+ dispose: removeListeners,
705
+ requested: () => shutdownPromise !== undefined,
706
+ wait: () => wait,
707
+ };
708
+ }
709
+ function combineLifecycleErrors(errors, message) {
710
+ if (errors.length === 1) {
711
+ return errors[0];
712
+ }
713
+ const aggregateError = new AggregateError(errors, message);
714
+ aggregateError.cause = errors[0];
715
+ return aggregateError;
716
+ }
717
+ function renderServeText(manifest, showSecrets) {
273
718
  const lines = [
274
719
  `${manifest.provider} local server ready`,
275
720
  ` apiRoot: ${manifest.provider === "matrix" ? manifest.endpoints.clientApiRoot : manifest.endpoints.apiRoot}`,
276
721
  ` inbound: ${manifest.endpoints.adminInboundUrl}`,
277
722
  ` recorder: ${manifest.recorderPath}`,
278
723
  ];
279
- const providerFields = renderServeProviderFields(manifest);
724
+ const providerFields = renderServeProviderFields(manifest, showSecrets);
280
725
  if (!providerFields) {
281
726
  throw new CrablineError(`Unsupported server manifest provider: ${String(manifest.provider)}`, {
282
727
  kind: "config",
@@ -285,49 +730,57 @@ function renderServeText(manifest) {
285
730
  lines.splice(2, 0, ...providerFields);
286
731
  return lines.join("\n");
287
732
  }
288
- function renderServeProviderFields(manifest) {
733
+ function renderServeProviderFields(manifest, showSecrets) {
734
+ const secret = (value) => (showSecrets ? value : "<redacted>");
289
735
  if (manifest.provider === "mattermost") {
290
736
  return [
291
- ` adminToken: ${manifest.adminToken}`,
292
- ` botToken: ${manifest.botToken}`,
737
+ ` adminToken: ${secret(manifest.adminToken)}`,
738
+ ` botToken: ${secret(manifest.botToken)}`,
293
739
  ` websocket: ${manifest.endpoints.websocketUrl}`,
294
740
  ];
295
741
  }
296
742
  if (manifest.provider === "matrix") {
297
743
  return [
298
- ` adminToken: ${manifest.adminToken}`,
299
- ` accessToken: ${manifest.accessToken}`,
744
+ ` adminToken: ${secret(manifest.adminToken)}`,
745
+ ` accessToken: ${secret(manifest.accessToken)}`,
300
746
  ` botUserId: ${manifest.botUserId}`,
301
747
  ` sync: ${manifest.endpoints.syncUrl}`,
302
748
  ];
303
749
  }
304
750
  if (manifest.provider === "signal") {
305
- return [` adminToken: ${manifest.adminToken}`, ` account: ${manifest.account}`];
751
+ return [` adminToken: ${secret(manifest.adminToken)}`, ` account: ${manifest.account}`];
306
752
  }
307
753
  if (manifest.provider === "slack") {
308
754
  return [
309
- ` adminToken: ${manifest.adminToken}`,
310
- ` botToken: ${manifest.botToken}`,
311
- ` signingSecret: ${manifest.signingSecret}`,
755
+ ` adminToken: ${secret(manifest.adminToken)}`,
756
+ ` botToken: ${secret(manifest.botToken)}`,
757
+ ` signingSecret: ${secret(manifest.signingSecret)}`,
312
758
  ];
313
759
  }
314
760
  if (manifest.provider === "telegram") {
315
- return [` adminToken: ${manifest.adminToken}`, ` botToken: ${manifest.botToken}`];
761
+ return [
762
+ ` adminToken: ${secret(manifest.adminToken)}`,
763
+ ` botToken: ${secret(manifest.botToken)}`,
764
+ ];
316
765
  }
317
766
  if (manifest.provider === "whatsapp") {
767
+ const baileysWebSocketUrl = showSecrets
768
+ ? manifest.endpoints.baileysWebSocketUrl
769
+ : `${manifest.endpoints.baileysWebSocketUrl.split("?")[0]}?access_token=<redacted>`;
318
770
  return [
319
- ` adminToken: ${manifest.adminToken}`,
320
- ` accessToken: ${manifest.accessToken}`,
321
- ` baileysWebSocket: ${manifest.endpoints.baileysWebSocketUrl}`,
771
+ ` adminToken: ${secret(manifest.adminToken)}`,
772
+ ` accessToken: ${secret(manifest.accessToken)}`,
773
+ ` baileysWebSocket: ${baileysWebSocketUrl}`,
322
774
  ` messages: ${manifest.endpoints.messagesUrl}`,
323
- ` presence: ${manifest.endpoints.presenceUrl}`,
775
+ ` phoneNumber: ${manifest.endpoints.phoneNumberUrl}`,
776
+ ` status: ${manifest.endpoints.statusUrl}`,
324
777
  ` selfJid: ${manifest.selfJid}`,
325
778
  ];
326
779
  }
327
780
  if (manifest.provider === "zalo") {
328
781
  return [
329
- ` adminToken: ${manifest.adminToken}`,
330
- ` botToken: ${manifest.botToken}`,
782
+ ` adminToken: ${secret(manifest.adminToken)}`,
783
+ ` botToken: ${secret(manifest.botToken)}`,
331
784
  ` botId: ${manifest.botId}`,
332
785
  ];
333
786
  }
@@ -372,15 +825,20 @@ function diagnose(manifest) {
372
825
  findings.push(`provider ${providerId} missing env ${envName}`);
373
826
  }
374
827
  }
375
- if (provider.adapter === "script") {
376
- if (!provider.script?.commands.send) {
828
+ if (provider.adapter === "script" && provider.status === "active") {
829
+ const commands = provider.script?.commands;
830
+ if (provider.capabilities.includes("probe") && !commands?.probe) {
831
+ findings.push(`provider ${providerId} missing script.commands.probe`);
832
+ }
833
+ if (provider.capabilities.some((capability) => ["agent", "roundtrip", "send"].includes(capability)) &&
834
+ !commands?.send) {
377
835
  findings.push(`provider ${providerId} missing script.commands.send`);
378
836
  }
379
- if (!provider.script?.commands.waitForInbound) {
837
+ if (provider.capabilities.some((capability) => ["agent", "roundtrip"].includes(capability)) &&
838
+ !commands?.waitForInbound) {
380
839
  findings.push(`provider ${providerId} missing script.commands.waitForInbound`);
381
840
  }
382
841
  }
383
- void providerId;
384
842
  }
385
843
  return findings;
386
844
  }
@@ -389,17 +847,56 @@ export async function runCli(argv) {
389
847
  const program = createProgram((code) => {
390
848
  exitCode = code;
391
849
  });
850
+ const parserErrors = [];
851
+ const parserOutput = [];
852
+ const bufferParserErrors = (command) => {
853
+ command.configureOutput({
854
+ writeErr: (message) => parserErrors.push(message),
855
+ writeOut: (message) => parserOutput.push(message),
856
+ });
857
+ for (const child of command.commands) {
858
+ bufferParserErrors(child);
859
+ }
860
+ };
861
+ bufferParserErrors(program);
392
862
  try {
393
863
  await program.parseAsync(argv);
864
+ await writeOutput(process.stdout, parserOutput.join(""));
394
865
  return exitCode;
395
866
  }
396
867
  catch (error) {
397
- const message = ensureErrorMessage(error);
398
- process.stderr.write(`${message}\n`);
399
- if (error instanceof CrablineError) {
400
- return error.exitCode;
868
+ const errorExitCode = error instanceof CommanderError
869
+ ? error.code === "commander.help" && error.exitCode !== 0
870
+ ? 1
871
+ : error.exitCode
872
+ : error instanceof CrablineError
873
+ ? error.exitCode
874
+ : 1;
875
+ if (error instanceof CommanderError && errorExitCode === 0) {
876
+ await writeOutput(process.stdout, parserOutput.join(""));
877
+ return 0;
878
+ }
879
+ const json = program.opts().json === true;
880
+ if (json) {
881
+ await writeOutput(process.stderr, `${formatJson({
882
+ error: {
883
+ ...(error instanceof CommanderError ? { code: error.code } : {}),
884
+ exitCode: errorExitCode,
885
+ ...(error instanceof CrablineError && error.kind ? { kind: error.kind } : {}),
886
+ message: error instanceof CommanderError && error.code === "commander.help"
887
+ ? "No command specified."
888
+ : ensureErrorMessage(error),
889
+ },
890
+ ok: false,
891
+ })}\n`);
892
+ }
893
+ else if (error instanceof CommanderError) {
894
+ await writeOutput(process.stderr, parserErrors.join(""));
895
+ }
896
+ else {
897
+ await writeOutput(process.stderr, `${ensureErrorMessage(error)}\n`);
401
898
  }
402
- return 1;
899
+ return errorExitCode;
403
900
  }
404
901
  }
405
902
  //# sourceMappingURL=program.js.map