@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.
- package/README.md +47 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +43 -0
- package/dist/src/openclaw/artifact-generation.js +363 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +28 -0
- package/dist/src/openclaw/private-file.js +465 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +21 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +1004 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +334 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +549 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.js +465 -51
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +306 -58
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +5 -0
- package/dist/src/servers/recorder.js +262 -3
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +2 -0
- package/dist/src/servers/signal.js +444 -56
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +5 -0
- package/dist/src/servers/slack.js +515 -49
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +111 -0
- package/dist/src/servers/telegram.js +830 -158
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +12 -7
- package/dist/src/servers/whatsapp.js +269 -107
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +14 -0
- package/dist/src/servers/zalo.js +521 -88
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
package/dist/src/cli/program.js
CHANGED
|
@@ -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
|
|
60
|
-
|
|
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 =
|
|
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 <
|
|
108
|
-
.description("Probe provider readiness using a fixture
|
|
109
|
-
.action(async (
|
|
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 ===
|
|
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(`
|
|
187
|
+
throw new CrablineError(`Unknown fixture: ${fixtureId}`, { kind: "config" });
|
|
116
188
|
}
|
|
117
|
-
const registry =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("--
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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 [
|
|
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: ${
|
|
771
|
+
` adminToken: ${secret(manifest.adminToken)}`,
|
|
772
|
+
` accessToken: ${secret(manifest.accessToken)}`,
|
|
773
|
+
` baileysWebSocket: ${baileysWebSocketUrl}`,
|
|
322
774
|
` messages: ${manifest.endpoints.messagesUrl}`,
|
|
323
|
-
`
|
|
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
|
-
|
|
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 (
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
899
|
+
return errorExitCode;
|
|
403
900
|
}
|
|
404
901
|
}
|
|
405
902
|
//# sourceMappingURL=program.js.map
|