@flow-engineer/flow 0.0.1 → 0.2.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.
- package/LICENSE +202 -0
- package/README.md +611 -0
- package/coding-agents/skill.md +353 -0
- package/coding-agents/test-skill.md +223 -0
- package/coding-agents/tools-skill.md +89 -0
- package/dist/flow-messaging/src/client.d.ts +40 -0
- package/dist/flow-messaging/src/client.js +142 -0
- package/dist/flow-messaging/src/index.d.ts +72 -0
- package/dist/flow-messaging/src/index.js +605 -0
- package/dist/simulators/src/index.d.ts +2 -0
- package/dist/simulators/src/index.js +2 -0
- package/dist/simulators/src/page.html +1245 -0
- package/dist/simulators/src/person.html +100 -0
- package/dist/simulators/src/platforms.d.ts +29 -0
- package/dist/simulators/src/platforms.js +72 -0
- package/dist/simulators/src/server.d.ts +201 -0
- package/dist/simulators/src/server.js +759 -0
- package/dist/src/agent.d.ts +504 -0
- package/dist/src/agent.js +818 -0
- package/dist/src/channels.d.ts +178 -0
- package/dist/src/channels.js +243 -0
- package/dist/src/conversation.d.ts +361 -0
- package/dist/src/conversation.js +1487 -0
- package/dist/src/db.d.ts +18 -0
- package/dist/src/db.js +57 -0
- package/dist/src/events.d.ts +17 -0
- package/dist/src/events.js +20 -0
- package/dist/src/http.d.ts +45 -0
- package/dist/src/http.js +269 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +11 -0
- package/dist/src/model.d.ts +21 -0
- package/dist/src/model.js +96 -0
- package/dist/src/names.d.ts +22 -0
- package/dist/src/names.js +43 -0
- package/dist/src/nudges.d.ts +23 -0
- package/dist/src/nudges.js +117 -0
- package/dist/src/postgres.d.ts +20 -0
- package/dist/src/postgres.js +62 -0
- package/dist/src/schedules.d.ts +60 -0
- package/dist/src/schedules.js +249 -0
- package/dist/src/skills.d.ts +12 -0
- package/dist/src/skills.js +69 -0
- package/dist/src/store.d.ts +214 -0
- package/dist/src/store.js +448 -0
- package/dist/src/talk.d.ts +57 -0
- package/dist/src/talk.js +166 -0
- package/dist/src/time.d.ts +71 -0
- package/dist/src/time.js +170 -0
- package/dist/src/tools.d.ts +43 -0
- package/dist/src/tools.js +26 -0
- package/dist/src/traces.d.ts +31 -0
- package/dist/src/traces.js +303 -0
- package/dist/src/types.d.ts +260 -0
- package/dist/src/types.js +2 -0
- package/dist/testing/src/account.d.ts +4 -0
- package/dist/testing/src/account.js +580 -0
- package/dist/testing/src/chat.d.ts +3 -0
- package/dist/testing/src/chat.js +117 -0
- package/dist/testing/src/check-endpoint.d.ts +31 -0
- package/dist/testing/src/check-endpoint.js +158 -0
- package/dist/testing/src/chinwag.d.ts +2 -0
- package/dist/testing/src/chinwag.js +6 -0
- package/dist/testing/src/cli.d.ts +2 -0
- package/dist/testing/src/cli.js +592 -0
- package/dist/testing/src/client.d.ts +117 -0
- package/dist/testing/src/client.js +956 -0
- package/dist/testing/src/flow-login.d.ts +143 -0
- package/dist/testing/src/flow-login.js +249 -0
- package/dist/testing/src/http-agent.d.ts +47 -0
- package/dist/testing/src/http-agent.js +155 -0
- package/dist/testing/src/index.d.ts +3 -0
- package/dist/testing/src/index.js +2 -0
- package/dist/testing/src/new.d.ts +13 -0
- package/dist/testing/src/new.js +147 -0
- package/dist/testing/src/print-run.d.ts +3 -0
- package/dist/testing/src/print-run.js +22 -0
- package/dist/testing/src/protocol.d.ts +159 -0
- package/dist/testing/src/protocol.js +28 -0
- package/dist/testing/src/report.d.ts +74 -0
- package/dist/testing/src/report.js +90 -0
- package/dist/testing/src/show.d.ts +36 -0
- package/dist/testing/src/show.js +280 -0
- package/dist/testing/src/suite.d.ts +87 -0
- package/dist/testing/src/suite.js +198 -0
- package/docs/agents.md +85 -0
- package/examples/cafe/agent/AGENTS.md +10 -0
- package/examples/cafe/agent/skills/allergens/SKILL.md +7 -0
- package/examples/cafe/src/agent.ts +43 -0
- package/examples/cafe/src/channels.ts +20 -0
- package/examples/cafe/src/main.ts +30 -0
- package/examples/cafe/src/tools.ts +62 -0
- package/examples/cafe/tests/scenarios/latte-correction.yaml +30 -0
- package/examples/cafe/tests/scenarios/order-fails.yaml +26 -0
- package/examples/cafe/tests/tools/get_menu.yaml +14 -0
- package/examples/cafe/tests/tools/place_order.yaml +29 -0
- package/examples/http-agent/server.mjs +32 -0
- package/examples/http-agent/tests/scenarios/opening-hours.yaml +16 -0
- package/flow-messaging/README.md +191 -0
- package/package.json +63 -8
- package/spec/agent-endpoint.openapi.yaml +148 -0
- package/spec/nudges.openapi.yaml +167 -0
- package/spec/request.schema.json +121 -0
- package/spec/response.schema.json +284 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// `flow chat`: talk to the agent in this folder from the terminal, with no account and no channel.
|
|
2
|
+
// It builds the agent from src/agent.ts (buildAgent({ events }), as `flow test` does) with the
|
|
3
|
+
// model key in .env, and writes as the agent's first test person, the way the playground does
|
|
4
|
+
// (agent.simulate). Each line read from stdin is one message; the agent's replies are printed once
|
|
5
|
+
// its turn is over, so it works typed by a person or piped by a coding agent:
|
|
6
|
+
// printf 'hi\nA latte please\n' | npx @flow-engineer/flow chat
|
|
7
|
+
// --json prints each thing the agent sends as one JSON line. The conversation lives only as long as
|
|
8
|
+
// the command. Exit status: 0 done (stdin ended), 1 usage or the agent could not be built.
|
|
9
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
10
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
11
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
12
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return path;
|
|
16
|
+
};
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
import { parseArgs } from "node:util";
|
|
22
|
+
import { loadEnv } from "./report.js";
|
|
23
|
+
export const CHAT_LINE = ` flow chat [--channel <name>] [--person <phone>] [--json]
|
|
24
|
+
Talk to the agent in this folder from the terminal: one message per line
|
|
25
|
+
of stdin, no account needed (only the model key in .env)`;
|
|
26
|
+
const USAGE = `Usage:\n${CHAT_LINE}
|
|
27
|
+
|
|
28
|
+
Writes as the agent's first test person (createAgent({ testPeople })) on its first channel, unless
|
|
29
|
+
--person or --channel says otherwise. Exit status: 0 done, 1 usage or the agent could not be built.`;
|
|
30
|
+
/** Runs `flow chat`; resolves to the exit status. */
|
|
31
|
+
export async function chatCli(args, input = process.stdin) {
|
|
32
|
+
let values;
|
|
33
|
+
try {
|
|
34
|
+
({ values } = parseArgs({ args, strict: true, options: { channel: { type: "string" }, person: { type: "string" }, json: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false } } }));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error(`flow chat: ${error.message.split(". ")[0]}\n\n${USAGE}`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (values.help) {
|
|
41
|
+
console.log(USAGE);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
loadEnv(undefined);
|
|
45
|
+
const file = join(process.cwd(), "src", "agent.ts");
|
|
46
|
+
if (!existsSync(file)) {
|
|
47
|
+
console.error(`flow chat: no src/agent.ts here. Run it from your agent's folder (npx @flow-engineer/flow init makes one).`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const turns = [];
|
|
51
|
+
let agent;
|
|
52
|
+
try {
|
|
53
|
+
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
54
|
+
if (typeof module.buildAgent !== "function")
|
|
55
|
+
throw new Error("src/agent.ts must export buildAgent({ events }).");
|
|
56
|
+
agent = module.buildAgent({ events: [(event) => turns.forEach((listener) => listener(event))] });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(`flow chat: ${error.message}`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const person = values.person ? agent.testPeople().find((p) => p.phone.replace(/\D/g, "") === values.person.replace(/\D/g, "")) : agent.testPeople()[0];
|
|
63
|
+
const channel = values.channel ? agent.channels().find((c) => c.name === values.channel) : agent.channels()[0];
|
|
64
|
+
if (!person || !channel) {
|
|
65
|
+
console.error(!person
|
|
66
|
+
? `flow chat: ${values.person ? `${values.person} is not one of the agent's testPeople` : "the agent has no testPeople"}; declare them in createAgent({ testPeople: [{ name, phone }] }).`
|
|
67
|
+
: `flow chat: ${values.channel ? `no channel named ${values.channel}` : "the agent has no channels"}; it has ${agent.channels().map((c) => c.name).join(", ") || "none"}.`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
agent.attachPlayground(({ outbound }) => print(outbound, channel.name, values.json === true));
|
|
71
|
+
turns.push((event) => {
|
|
72
|
+
if (event.type === "model.error")
|
|
73
|
+
console.error(`flow chat: the model failed: ${String(event.error ?? event.message ?? "error")}`);
|
|
74
|
+
});
|
|
75
|
+
const prompt = process.stdin.isTTY && input === process.stdin;
|
|
76
|
+
if (!values.json)
|
|
77
|
+
console.error(`Talking to the agent on ${channel.name} as ${person.name} (${person.phone}). Ctrl-D ends.`);
|
|
78
|
+
const lines = createInterface({ input, terminal: false });
|
|
79
|
+
if (prompt)
|
|
80
|
+
process.stderr.write("you: ");
|
|
81
|
+
let conversationId;
|
|
82
|
+
for await (const line of lines) {
|
|
83
|
+
const text = line.trim();
|
|
84
|
+
if (text) {
|
|
85
|
+
const taken = await agent.simulate({ person: person.phone, channel: channel.name, message: text });
|
|
86
|
+
if (taken)
|
|
87
|
+
await agent.conversation((conversationId = taken.conversationId))?.whenIdle();
|
|
88
|
+
}
|
|
89
|
+
if (prompt)
|
|
90
|
+
process.stderr.write("you: ");
|
|
91
|
+
}
|
|
92
|
+
// A tool still running in the background answers in a turn of its own: wait for it before leaving.
|
|
93
|
+
while (conversationId && agent.status().backgroundTasks > 0) {
|
|
94
|
+
await new Promise((done) => turns.push((event) => event.type === "task.done" && done()));
|
|
95
|
+
await agent.conversation(conversationId)?.whenIdle();
|
|
96
|
+
}
|
|
97
|
+
await agent.flushTraces();
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
/** One thing the agent sent, as a person would read it (or one JSON line). */
|
|
101
|
+
function print(outbound, channel, json) {
|
|
102
|
+
if (outbound.type === "typing" || outbound.type === "status" || outbound.type === "read")
|
|
103
|
+
return;
|
|
104
|
+
if (json)
|
|
105
|
+
return void process.stdout.write(`${JSON.stringify({ channel, ...outbound })}\n`);
|
|
106
|
+
if (outbound.type === "reaction")
|
|
107
|
+
return void process.stdout.write(`agent reacted ${outbound.emoji}\n`);
|
|
108
|
+
if (outbound.type === "template")
|
|
109
|
+
return void process.stdout.write(`agent: [template ${outbound.name}]\n`);
|
|
110
|
+
const extra = [
|
|
111
|
+
outbound.image && `[photo ${outbound.image.url}]`,
|
|
112
|
+
outbound.link && `[${outbound.link.label}: ${outbound.link.url}]`,
|
|
113
|
+
outbound.buttons?.map((b) => `[${b}]`).join(" "),
|
|
114
|
+
outbound.list && `[${outbound.list.button}: ${outbound.list.rows.map((r) => r.title).join(" | ")}]`,
|
|
115
|
+
].filter(Boolean);
|
|
116
|
+
process.stdout.write(`agent: ${[outbound.text, ...extra].filter(Boolean).join(" ")}\n`);
|
|
117
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type HttpAgentRequest } from "./http-agent.ts";
|
|
2
|
+
/** One synthetic conversation: its messages go one at a time, each once the last one's answer is in. */
|
|
3
|
+
interface Case {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
messages: Array<{
|
|
7
|
+
messageId: string;
|
|
8
|
+
message: HttpAgentRequest["message"];
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare const CASES: Case[];
|
|
12
|
+
export interface CaseResult {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
pass: boolean;
|
|
16
|
+
/** What went wrong, or what came back. */
|
|
17
|
+
detail: string;
|
|
18
|
+
/** Each message's replies, as the endpoint answered them. */
|
|
19
|
+
answers: Array<{
|
|
20
|
+
messageId: string;
|
|
21
|
+
replies: unknown[];
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export declare function checkEndpoint(options: {
|
|
25
|
+
url: string;
|
|
26
|
+
token?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}): Promise<CaseResult[]>;
|
|
29
|
+
/** The command: prints one line per case (or JSON) and returns the exit status. */
|
|
30
|
+
export declare function checkEndpointCli(args: string[], env?: Record<string, string | undefined>, print?: (...data: any[]) => void, printError?: (...data: any[]) => void): Promise<number>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// `flow check-endpoint <url>`: checks an agent's HTTP endpoint against the contract in spec/
|
|
2
|
+
// (agent-endpoint.openapi.yaml, request.schema.json, response.schema.json) before any test run. It
|
|
3
|
+
// POSTs a fixed set of synthetic messages, as `flow test --agent-url` would, and checks each
|
|
4
|
+
// answer: the status and body (the same rules a run applies, testing/src/http-agent.ts), the
|
|
5
|
+
// response schema, what WhatsApp can show, and that every id a reply names is one of the person's
|
|
6
|
+
// messages. No cloud, no key and no model call on Flow Framework's side: whatever the agent does with the
|
|
7
|
+
// messages (its own model, its tools) runs on its side.
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { WHATSAPP, z } from "../../src/index.js";
|
|
13
|
+
import { setting } from "../../src/traces.js";
|
|
14
|
+
import { checkReply, postMessage, TurnError } from "./http-agent.js";
|
|
15
|
+
import { packageRoot } from "./new.js";
|
|
16
|
+
/** A 1×1 PNG. */
|
|
17
|
+
const PHOTO = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
18
|
+
export const CASES = [
|
|
19
|
+
{ id: "message", title: "a typed message", messages: [{ messageId: "m1", message: { text: "hi, what time are you open today?" } }] },
|
|
20
|
+
{
|
|
21
|
+
id: "double-text",
|
|
22
|
+
title: "two messages in a row, in one conversation",
|
|
23
|
+
messages: [
|
|
24
|
+
{ messageId: "m1", message: { text: "can i get a large latte" } },
|
|
25
|
+
{ messageId: "m2", message: { text: "oat milk actually" } },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ id: "file", title: "a photo with no caption", messages: [{ messageId: "m1", message: { text: "", attachments: [{ kind: "image", mime: "image/png", name: "photo.png", data: PHOTO }] } }] },
|
|
29
|
+
{
|
|
30
|
+
id: "message-ids",
|
|
31
|
+
title: "a tapped button quoting the agent, with an id to react to",
|
|
32
|
+
messages: [
|
|
33
|
+
{ messageId: "m1", message: { text: "do you have oat milk?" } },
|
|
34
|
+
{ messageId: "m2", message: { text: "Oat", choice: { title: "Oat" }, replyTo: { text: "Which milk would you like?", fromAgent: true } } },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{ id: "no-reply", title: "a message that may need no answer", messages: [{ messageId: "m1", message: { text: "ok 👍" } }] },
|
|
38
|
+
];
|
|
39
|
+
const schemaFile = (name) => JSON.parse(readFileSync(join(packageRoot(), "spec", name), "utf8"));
|
|
40
|
+
function schemas() {
|
|
41
|
+
const response = schemaFile("response.schema.json");
|
|
42
|
+
const shape = z.fromJSONSchema({ ...response, properties: { replies: { type: "array" } } });
|
|
43
|
+
// One schema per reply type, so a bad reply is reported against its own type.
|
|
44
|
+
const replies = new Map(Object.keys(response.$defs).map((type) => [type, z.fromJSONSchema({ $schema: response.$schema, $defs: response.$defs, $ref: `#/$defs/${type}` })]));
|
|
45
|
+
return { shape, replies };
|
|
46
|
+
}
|
|
47
|
+
const issues = (error, at) => error.issues.map((i) => `${[at, ...i.path].join(".")}: ${i.message}`).join("; ");
|
|
48
|
+
/** Why this answer breaks the contract, or null. `ids` are the person's messages so far. */
|
|
49
|
+
function problem(replies, ids, s) {
|
|
50
|
+
for (const [n, reply] of replies.entries()) {
|
|
51
|
+
const at = `replies[${n}]`;
|
|
52
|
+
const type = reply?.type;
|
|
53
|
+
const schema = typeof type === "string" ? s.replies.get(type) : undefined;
|
|
54
|
+
if (!schema)
|
|
55
|
+
return `${at}.type: must be one of ${[...s.replies.keys()].join(", ")}`;
|
|
56
|
+
const valid = schema.safeParse(reply);
|
|
57
|
+
if (!valid.success)
|
|
58
|
+
return issues(valid.error, at);
|
|
59
|
+
const shown = checkReply(reply, WHATSAPP);
|
|
60
|
+
if ("refused" in shown)
|
|
61
|
+
return `${at}: would not be sent on WhatsApp: ${shown.refused}`;
|
|
62
|
+
const r = reply;
|
|
63
|
+
for (const id of [r.replyTo, r.messageId, ...(r.messageIds ?? [])]) {
|
|
64
|
+
if (id !== undefined && !ids.includes(id))
|
|
65
|
+
return `${at} names ${JSON.stringify(id)}, which is not one of the person's messages (${ids.join(", ")})`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const described = (answers) => answers
|
|
71
|
+
.map((a) => {
|
|
72
|
+
const types = a.replies.map((r) => String(r.type));
|
|
73
|
+
return `${a.messageId}: ${types.length ? `${types.length} ${types.length === 1 ? "reply" : "replies"} (${types.join(", ")})` : "no reply"}`;
|
|
74
|
+
})
|
|
75
|
+
.join("; ");
|
|
76
|
+
export async function checkEndpoint(options) {
|
|
77
|
+
const s = schemas();
|
|
78
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
79
|
+
// Fresh conversations on every check, in the cloud's form, so an endpoint that keeps history never mixes runs.
|
|
80
|
+
const session = `check_${randomBytes(4).toString("hex")}`;
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const c of CASES) {
|
|
83
|
+
const conversationId = `whatsapp:${session}.${c.id}.whatsapp.1`;
|
|
84
|
+
const answers = [];
|
|
85
|
+
let failed = null;
|
|
86
|
+
for (const [n, { messageId, message }] of c.messages.entries()) {
|
|
87
|
+
let replies;
|
|
88
|
+
try {
|
|
89
|
+
replies = await postMessage(options, timeoutMs, { conversationId, messageId, channel: "whatsapp", message });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
failed = `${messageId}: ${error instanceof TurnError ? error.message : error.message}`;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
answers.push({ messageId, replies });
|
|
96
|
+
const wrong = problem(replies, c.messages.slice(0, n + 1).map((m) => m.messageId), s);
|
|
97
|
+
if (wrong) {
|
|
98
|
+
failed = `${messageId}: ${wrong}`;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
results.push({ id: c.id, title: c.title, pass: failed === null, detail: failed ?? described(answers), answers });
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
const USAGE = `Usage: flow check-endpoint <url> [--token-env NAME] [--agent-timeout <s>] [--json]
|
|
107
|
+
|
|
108
|
+
Checks an agent's HTTP endpoint against Flow Framework's contract (spec/agent-endpoint.openapi.yaml) with
|
|
109
|
+
a fixed set of synthetic messages. No cloud, no key, no model call on Flow Framework's side.
|
|
110
|
+
|
|
111
|
+
--token-env NAME Send the value of NAME as authorization: Bearer <value>
|
|
112
|
+
(default: FLOW_FRAMEWORK_AGENT_TOKEN, when it is set)
|
|
113
|
+
--agent-timeout <s> How long one message waits for its answer (default: 60)
|
|
114
|
+
--json Print the results as JSON
|
|
115
|
+
|
|
116
|
+
Exit status: 0 when every case passed, 1 otherwise.`;
|
|
117
|
+
/** The command: prints one line per case (or JSON) and returns the exit status. */
|
|
118
|
+
export async function checkEndpointCli(args, env = process.env, print = console.log, printError = console.error) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = parseArgs({ args, allowPositionals: true, options: { "token-env": { type: "string" }, "agent-timeout": { type: "string", default: "60" }, json: { type: "boolean", default: false }, help: { type: "boolean", short: "h", default: false } } });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
printError(`${error.message}\n\n${USAGE}`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const { values, positionals } = parsed;
|
|
128
|
+
if (values.help) {
|
|
129
|
+
print(USAGE);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
const url = positionals.length === 1 ? positionals[0] : "";
|
|
133
|
+
if (!/^https?:\/\/[^/]/.test(url) || !URL.canParse(url)) {
|
|
134
|
+
printError(`check-endpoint takes one http:// or https:// URL, such as http://localhost:3000/flow.\n\n${USAGE}`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
const timeout = Number(values["agent-timeout"]);
|
|
138
|
+
if (!(timeout > 0)) {
|
|
139
|
+
printError("--agent-timeout must be a number of seconds above 0.");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const tokenEnv = values["token-env"];
|
|
143
|
+
const token = (tokenEnv ? env[tokenEnv] : setting("AGENT_TOKEN", env)) || undefined;
|
|
144
|
+
if (tokenEnv && !token) {
|
|
145
|
+
printError(`${tokenEnv} is not set.`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
const results = await checkEndpoint({ url, token, timeoutMs: timeout * 1000 });
|
|
149
|
+
const passed = results.filter((r) => r.pass).length;
|
|
150
|
+
if (values.json)
|
|
151
|
+
print(JSON.stringify({ url, passed: passed === results.length, cases: results }, null, 2));
|
|
152
|
+
else {
|
|
153
|
+
for (const r of results)
|
|
154
|
+
print(`${r.pass ? "PASS" : "FAIL"} ${r.id} (${r.title}): ${r.detail}`);
|
|
155
|
+
print(`\n${passed} of ${results.length} cases passed.`);
|
|
156
|
+
}
|
|
157
|
+
return passed === results.length ? 0 : 1;
|
|
158
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `chinwag` command from before the rename, kept for one release: it says so on stderr (stdout
|
|
3
|
+
// stays the report's, for --json), then runs the flow command (testing/src/cli.ts) as it is.
|
|
4
|
+
console.error("chinwag is now Flow Framework (@flow-engineer/flow): run npx @flow-engineer/flow, and npm i @flow-engineer/flow.");
|
|
5
|
+
await import("./cli.js");
|
|
6
|
+
export {};
|