pi-roundtable 0.1.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/docs/plugins.md +924 -0
- package/examples/channels.test.ts +13 -0
- package/examples/channels.ts +27 -0
- package/examples/dashboard.test.ts +11 -0
- package/examples/dashboard.ts +9 -0
- package/examples/events.test.ts +26 -0
- package/examples/events.ts +25 -0
- package/examples/guide.test.ts +52 -0
- package/examples/holds.test.ts +16 -0
- package/examples/holds.ts +30 -0
- package/examples/http.test.ts +12 -0
- package/examples/http.ts +17 -0
- package/examples/interactions.test.ts +25 -0
- package/examples/interactions.ts +28 -0
- package/examples/migrations.test.ts +31 -0
- package/examples/migrations.ts +42 -0
- package/examples/packages.test.ts +9 -0
- package/examples/packages.ts +9 -0
- package/examples/preflight.test.ts +11 -0
- package/examples/preflight.ts +19 -0
- package/examples/prompt.test.ts +25 -0
- package/examples/prompt.ts +20 -0
- package/examples/providers.test.ts +10 -0
- package/examples/providers.ts +17 -0
- package/examples/seeds.test.ts +12 -0
- package/examples/seeds.ts +18 -0
- package/examples/selection.test.ts +13 -0
- package/examples/selection.ts +14 -0
- package/examples/services.test.ts +20 -0
- package/examples/services.ts +30 -0
- package/examples/session-tools.test.ts +24 -0
- package/examples/session-tools.ts +34 -0
- package/examples/tools.test.ts +18 -0
- package/examples/tools.ts +28 -0
- package/package.json +55 -0
- package/src/cli/add-plugin.test.ts +107 -0
- package/src/cli/add-plugin.ts +75 -0
- package/src/cli/checks/basic.test.ts +276 -0
- package/src/cli/checks/bun.ts +26 -0
- package/src/cli/checks/configuration.ts +28 -0
- package/src/cli/checks/database.test.ts +117 -0
- package/src/cli/checks/database.ts +100 -0
- package/src/cli/checks/discord.test.ts +200 -0
- package/src/cli/checks/discord.ts +218 -0
- package/src/cli/checks/environment.ts +27 -0
- package/src/cli/checks/model.ts +28 -0
- package/src/cli/checks/public-url.ts +40 -0
- package/src/cli/cli.test.ts +122 -0
- package/src/cli/cli.ts +166 -0
- package/src/cli/config-edit.test.ts +130 -0
- package/src/cli/config-edit.ts +199 -0
- package/src/cli/discord-api.ts +96 -0
- package/src/cli/doctor.test.ts +184 -0
- package/src/cli/doctor.ts +92 -0
- package/src/cli/http.ts +23 -0
- package/src/cli/init.test.ts +127 -0
- package/src/cli/init.ts +82 -0
- package/src/cli/main.ts +13 -0
- package/src/cli/project.ts +123 -0
- package/src/cli/report.ts +68 -0
- package/src/cli/roundtable.mjs +22 -0
- package/src/cli/runtime.ts +53 -0
- package/src/cli/size.test.ts +9 -0
- package/src/cli/start.ts +33 -0
- package/src/cli/templates.test.ts +129 -0
- package/src/cli/templates.ts +119 -0
- package/src/cli/testing/fixtures.ts +106 -0
- package/src/core/agents/agent-claim.test.ts +142 -0
- package/src/core/agents/agent-claim.ts +170 -0
- package/src/core/agents/agent-dashboard.test.ts +120 -0
- package/src/core/agents/agent-dashboard.ts +185 -0
- package/src/core/agents/agent-guild.test.ts +124 -0
- package/src/core/agents/agent-messages.ts +189 -0
- package/src/core/agents/agent-ports.ts +103 -0
- package/src/core/agents/agent-prompt.ts +163 -0
- package/src/core/agents/agent-rules.ts +106 -0
- package/src/core/agents/agent-schema.ts +115 -0
- package/src/core/agents/agent-settings.ts +49 -0
- package/src/core/agents/agent-store.test.ts +205 -0
- package/src/core/agents/agent-store.ts +384 -0
- package/src/core/agents/agent-team.ts +445 -0
- package/src/core/agents/agent-tools.ts +386 -0
- package/src/core/agents/avatar-studio.test.ts +88 -0
- package/src/core/agents/avatar-studio.ts +147 -0
- package/src/core/agents/group-messages.ts +99 -0
- package/src/core/agents/group-round.test.ts +143 -0
- package/src/core/agents/group-round.ts +117 -0
- package/src/core/agents/group-turns.ts +132 -0
- package/src/core/agents/owner-identity.test.ts +172 -0
- package/src/core/agents/team-editing.ts +205 -0
- package/src/core/agents/team-keys.ts +40 -0
- package/src/core/agents/team-layout.ts +135 -0
- package/src/core/agents/team-lifecycle.ts +188 -0
- package/src/core/agents/team-options.ts +52 -0
- package/src/core/agents/team-status.ts +80 -0
- package/src/core/agents/team-text.ts +60 -0
- package/src/core/agents/team-turn-types.ts +107 -0
- package/src/core/agents/team-turns.test.ts +278 -0
- package/src/core/agents/team-turns.ts +323 -0
- package/src/core/assets/neutral.png +0 -0
- package/src/core/assets/prompts/shared-guest.md +11 -0
- package/src/core/assets/prompts/shared.md +11 -0
- package/src/core/assets/skills/writing-skills/SKILL.md +21 -0
- package/src/core/attachments/attachment-dir.ts +15 -0
- package/src/core/attachments/attachment-fetcher.ts +83 -0
- package/src/core/attachments/attachments.test.ts +85 -0
- package/src/core/attachments/image-prep.ts +50 -0
- package/src/core/attachments/prompt-block.ts +37 -0
- package/src/core/attachments/turn-attachments.ts +79 -0
- package/src/core/boundary.test.ts +45 -0
- package/src/core/builtin/agent-server.ts +275 -0
- package/src/core/builtin/discord.ts +95 -0
- package/src/core/builtin/modules.test.ts +127 -0
- package/src/core/builtin/modules.ts +176 -0
- package/src/core/builtin/seeds.ts +10 -0
- package/src/core/builtin/stores.ts +42 -0
- package/src/core/config/config.test.ts +135 -0
- package/src/core/config/config.ts +285 -0
- package/src/core/config/schema.ts +181 -0
- package/src/core/contract/channels.ts +152 -0
- package/src/core/contract/discord.ts +33 -0
- package/src/core/contract/providers.ts +67 -0
- package/src/core/db/guild-scope.ts +53 -0
- package/src/core/db/migrations.test.ts +237 -0
- package/src/core/db/migrations.ts +37 -0
- package/src/core/define-roundtable.test.ts +134 -0
- package/src/core/define-roundtable.ts +195 -0
- package/src/core/define.test.ts +144 -0
- package/src/core/define.ts +154 -0
- package/src/core/discord/agent-commands.test.ts +58 -0
- package/src/core/discord/agent-commands.ts +385 -0
- package/src/core/discord/agent-discord.test.ts +70 -0
- package/src/core/discord/agent-discord.ts +367 -0
- package/src/core/discord/channel-executor.ts +404 -0
- package/src/core/discord/channel-operations.ts +329 -0
- package/src/core/discord/discord-surface.ts +431 -0
- package/src/core/discord/dispatch-thread-host.test.ts +114 -0
- package/src/core/discord/dispatch-thread-host.ts +72 -0
- package/src/core/discord/dispatch-threads.test.ts +94 -0
- package/src/core/discord/dispatch-threads.ts +180 -0
- package/src/core/discord/interaction-module.ts +1 -0
- package/src/core/discord/owner-cards.test.ts +435 -0
- package/src/core/discord/owner-cards.ts +463 -0
- package/src/core/discord/owner-command.ts +126 -0
- package/src/core/discord/owner-discord-access.ts +183 -0
- package/src/core/discord/owner-discord-threads.ts +160 -0
- package/src/core/discord/owner-discord.test.ts +325 -0
- package/src/core/discord/owner-discord.ts +282 -0
- package/src/core/discord/owner-panel.ts +103 -0
- package/src/core/discord/schedule-commands.ts +149 -0
- package/src/core/discord/stop-button.ts +20 -0
- package/src/core/domain/attachment.ts +39 -0
- package/src/core/domain/conversation.ts +42 -0
- package/src/core/domain/errors.ts +31 -0
- package/src/core/domain/expression.ts +21 -0
- package/src/core/domain/owner-prompts.ts +45 -0
- package/src/core/domain/ports.ts +91 -0
- package/src/core/domain/profile.ts +32 -0
- package/src/core/drain.test.ts +46 -0
- package/src/core/drain.ts +33 -0
- package/src/core/errors.ts +29 -0
- package/src/core/events.test.ts +81 -0
- package/src/core/events.ts +55 -0
- package/src/core/holds.test.ts +54 -0
- package/src/core/holds.ts +46 -0
- package/src/core/host.test.ts +536 -0
- package/src/core/host.ts +297 -0
- package/src/core/http/listeners.test.ts +164 -0
- package/src/core/http/listeners.ts +151 -0
- package/src/core/i18n/agent-panel.ts +96 -0
- package/src/core/i18n/cards.ts +64 -0
- package/src/core/i18n/channels.ts +28 -0
- package/src/core/i18n/conversation.ts +49 -0
- package/src/core/i18n/dashboard.ts +72 -0
- package/src/core/i18n/discord.ts +23 -0
- package/src/core/i18n/en.ts +27 -0
- package/src/core/i18n/i18n.test.ts +139 -0
- package/src/core/i18n/index.ts +58 -0
- package/src/core/i18n/owner.ts +35 -0
- package/src/core/i18n/schedules.ts +92 -0
- package/src/core/i18n/time-zones.ts +27 -0
- package/src/core/i18n/types.ts +7 -0
- package/src/core/i18n/zh-tw.ts +26 -0
- package/src/core/identity.test.ts +18 -0
- package/src/core/identity.ts +31 -0
- package/src/core/judging/confirmation-judge.ts +68 -0
- package/src/core/judging/effort-judge.test.ts +112 -0
- package/src/core/judging/effort-judge.ts +158 -0
- package/src/core/judging/model-judge.test.ts +126 -0
- package/src/core/judging/model-judge.ts +193 -0
- package/src/core/log.ts +45 -0
- package/src/core/models.ts +52 -0
- package/src/core/modules/background/background-turns.ts +78 -0
- package/src/core/modules/delegation/delegate.ts +54 -0
- package/src/core/modules/delegation/delegator.test.ts +170 -0
- package/src/core/modules/delegation/delegator.ts +191 -0
- package/src/core/modules/delegation/sol-worker.ts +78 -0
- package/src/core/modules/discord-admin/discord-admin.ts +288 -0
- package/src/core/modules/host-shell/shell-policy.ts +386 -0
- package/src/core/modules/memory/owner-memory-store.test.ts +244 -0
- package/src/core/modules/memory/owner-memory-store.ts +228 -0
- package/src/core/modules/memory/owner-memory.ts +153 -0
- package/src/core/modules/notify/notify.ts +26 -0
- package/src/core/modules/schedules/recurrence.ts +206 -0
- package/src/core/modules/schedules/schedule-store.ts +213 -0
- package/src/core/modules/schedules/schedule-tools.ts +196 -0
- package/src/core/modules/schedules/schedule.test.ts +358 -0
- package/src/core/modules/schedules/scheduler.ts +121 -0
- package/src/core/modules/schedules/schedules.ts +87 -0
- package/src/core/modules/skills/repo-name.ts +11 -0
- package/src/core/modules/skills/skill-kind.test.ts +55 -0
- package/src/core/modules/skills/skill-link.ts +69 -0
- package/src/core/modules/skills/skill-listing.ts +128 -0
- package/src/core/modules/skills/skill-registry.test.ts +376 -0
- package/src/core/modules/skills/skill-registry.ts +323 -0
- package/src/core/modules/skills/skill-rules.ts +120 -0
- package/src/core/modules/skills/skill-store.ts +285 -0
- package/src/core/modules/skills/skill-tools.ts +207 -0
- package/src/core/ops/error-reporter.test.ts +283 -0
- package/src/core/ops/error-reporter.ts +271 -0
- package/src/core/plugin.ts +200 -0
- package/src/core/presentation/card-cadence.ts +41 -0
- package/src/core/presentation/headline.ts +34 -0
- package/src/core/presentation/presentation.test.ts +166 -0
- package/src/core/presentation/quiet-links.ts +36 -0
- package/src/core/presentation/reply-splitter.ts +110 -0
- package/src/core/presentation/thinking-line.ts +24 -0
- package/src/core/public-entry.test.ts +59 -0
- package/src/core/registry/contributions.test.ts +215 -0
- package/src/core/registry/contributions.ts +249 -0
- package/src/core/registry/interactions.test.ts +87 -0
- package/src/core/registry/interactions.ts +55 -0
- package/src/core/registry/providers.test.ts +103 -0
- package/src/core/registry/providers.ts +55 -0
- package/src/core/routing/channel-queue.test.ts +31 -0
- package/src/core/routing/channel-queue.ts +44 -0
- package/src/core/routing/channel-router.test.ts +326 -0
- package/src/core/routing/channel-router.ts +157 -0
- package/src/core/routing/conversation-kind.ts +14 -0
- package/src/core/routing/forward-join.ts +65 -0
- package/src/core/routing/message-text.ts +16 -0
- package/src/core/routing/settle-turn.test.ts +21 -0
- package/src/core/routing/settle-turn.ts +26 -0
- package/src/core/runtime/compaction-tiers.test.ts +227 -0
- package/src/core/runtime/compaction-tiers.ts +203 -0
- package/src/core/runtime/conversation-sessions.ts +162 -0
- package/src/core/runtime/extensions/agent-prompt.ts +16 -0
- package/src/core/runtime/extensions/ask-user.test.ts +114 -0
- package/src/core/runtime/extensions/ask-user.ts +98 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +247 -0
- package/src/core/runtime/extensions/self-compact-guard.test.ts +50 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +23 -0
- package/src/core/runtime/mcp.ts +23 -0
- package/src/core/runtime/pending-confirmation-store.test.ts +52 -0
- package/src/core/runtime/pending-confirmation-store.ts +75 -0
- package/src/core/runtime/pi-agent-runtime.ts +477 -0
- package/src/core/runtime/prompt-slot.ts +81 -0
- package/src/core/runtime/runtime-types.ts +150 -0
- package/src/core/runtime/session-archive.test.ts +20 -0
- package/src/core/runtime/session-archive.ts +17 -0
- package/src/core/runtime/session-factory.ts +291 -0
- package/src/core/runtime/steerable-run.test.ts +264 -0
- package/src/core/runtime/steerable-run.ts +109 -0
- package/src/core/runtime/text-tools.test.ts +110 -0
- package/src/core/runtime/text-tools.ts +66 -0
- package/src/core/runtime/turn-answer.test.ts +72 -0
- package/src/core/runtime/turn-answer.ts +79 -0
- package/src/core/runtime/worker-task.test.ts +82 -0
- package/src/core/runtime/worker-task.ts +65 -0
- package/src/core/services.test.ts +28 -0
- package/src/core/services.ts +107 -0
- package/src/core/sessions.test.ts +85 -0
- package/src/core/sessions.ts +149 -0
- package/src/core/shared/attachment-reader.ts +91 -0
- package/src/core/shared/delegate-tool.ts +15 -0
- package/src/core/shared/mcp-adapter.ts +58 -0
- package/src/core/shared/package-dir.ts +9 -0
- package/src/core/shared/profile-tools.ts +16 -0
- package/src/core/shared/read-attachment-tool.ts +43 -0
- package/src/core/shared/schedule-tools.ts +118 -0
- package/src/core/shared/session-messages.ts +50 -0
- package/src/core/shared/tool-result.ts +9 -0
- package/src/core/shared/unix-server.ts +18 -0
- package/src/core/size.test.ts +9 -0
- package/src/core/speakers.test.ts +78 -0
- package/src/core/speakers.ts +143 -0
- package/src/core/testing/database.ts +52 -0
- package/src/core/testing/file-size.ts +34 -0
- package/src/core/testing/locale.ts +8 -0
- package/src/core/testing/modules.ts +97 -0
- package/src/core/testing/owner.ts +15 -0
- package/src/core/testing/thread-host.ts +60 -0
- package/src/core/time.test.ts +166 -0
- package/src/core/time.ts +67 -0
- package/src/core/tool-tiers.test.ts +117 -0
- package/src/core/tool-tiers.ts +127 -0
- package/src/entries.test.ts +112 -0
- package/src/index.ts +28 -0
- package/src/testing.test.ts +156 -0
- package/src/testing.ts +182 -0
- package/templates/.env.example +22 -0
- package/templates/README.md +11 -0
- package/templates/_gitignore +3 -0
- package/templates/agents.ts +12 -0
- package/templates/biome.json.tmpl +7 -0
- package/templates/docker-compose.yml +16 -0
- package/templates/package.json.tmpl +24 -0
- package/templates/persona/shared.md +2 -0
- package/templates/plugin/plugin.test.ts.tmpl +13 -0
- package/templates/plugin/plugin.ts +18 -0
- package/templates/roundtable.config.ts +24 -0
- package/templates/tsconfig.json.tmpl +16 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { definePlugin } from "pi-roundtable";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A session tool is a Pi extension added to every conversation session: the raw form of `tools`,
|
|
6
|
+
* for what defineTool cannot express. The factory returns null for a session it does not apply
|
|
7
|
+
* to, and a new `revision` rebuilds open sessions on their next turn.
|
|
8
|
+
*/
|
|
9
|
+
export const clock = definePlugin({
|
|
10
|
+
name: "clock",
|
|
11
|
+
setup: () => ({
|
|
12
|
+
sessionTools: [
|
|
13
|
+
{
|
|
14
|
+
name: "clock",
|
|
15
|
+
phase: "tools",
|
|
16
|
+
snapshot: () => ({
|
|
17
|
+
revision: 0,
|
|
18
|
+
factory: () => (pi) => {
|
|
19
|
+
pi.registerTool({
|
|
20
|
+
name: "clock_now",
|
|
21
|
+
label: "clock_now",
|
|
22
|
+
description: "Tell the current UTC time.",
|
|
23
|
+
parameters: Type.Object({}),
|
|
24
|
+
execute: async () => ({
|
|
25
|
+
content: [{ type: "text", text: new Date().toISOString() }],
|
|
26
|
+
details: undefined,
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { Speaker } from "pi-roundtable";
|
|
3
|
+
import { testPlugin } from "pi-roundtable/testing";
|
|
4
|
+
import { notes } from "./tools.ts";
|
|
5
|
+
|
|
6
|
+
test("note_add saves a note for the speaker and refuses an empty one", async () => {
|
|
7
|
+
const harness = await testPlugin(notes);
|
|
8
|
+
const ada: Speaker = { id: "1", name: "Ada", tier: "member" };
|
|
9
|
+
expect(harness.tools).toEqual(["note_add"]);
|
|
10
|
+
expect(harness.tiers.minTier("note_add")).toBe("member");
|
|
11
|
+
expect(
|
|
12
|
+
await harness.runTool("note_add", { text: "milk" }, { speaker: ada }),
|
|
13
|
+
).toBe("Saved note 1 for Ada.");
|
|
14
|
+
expect(await harness.runTool("note_add", { text: " " })).toBe(
|
|
15
|
+
"The note is empty. Ask what to save.",
|
|
16
|
+
);
|
|
17
|
+
await harness.stop();
|
|
18
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { definePlugin, defineTool, ToolRefusal } from "pi-roundtable";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
/** Tools are what agents can call; each names the lowest tier of speaker whose turns may call it. */
|
|
5
|
+
export const notes = definePlugin({
|
|
6
|
+
name: "notes",
|
|
7
|
+
setup: () => {
|
|
8
|
+
const saved: string[] = [];
|
|
9
|
+
return {
|
|
10
|
+
tools: [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: "note_add",
|
|
13
|
+
description:
|
|
14
|
+
"Save a short note. Call it when asked to remember something.",
|
|
15
|
+
parameters: Type.Object({ text: Type.String() }),
|
|
16
|
+
minTier: "member",
|
|
17
|
+
run: ({ text }, turn) => {
|
|
18
|
+
// A refusal is read by the model, which can correct the call.
|
|
19
|
+
if (text.trim() === "")
|
|
20
|
+
throw new ToolRefusal("The note is empty. Ask what to save.");
|
|
21
|
+
saved.push(text);
|
|
22
|
+
return `Saved note ${saved.length} for ${turn.speaker?.name ?? "nobody"}.`;
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-roundtable",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A plugin-driven Pi agent server for Discord",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"bun": ">=1.3.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./testing": "./src/testing.ts"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"roundtable": "./src/cli/roundtable.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"templates",
|
|
20
|
+
"docs",
|
|
21
|
+
"examples",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/wayne930242/pi-roundtable.git"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"lint": "biome check .",
|
|
37
|
+
"test": "bun test"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@babel/parser": "7.29.9",
|
|
41
|
+
"@earendil-works/pi-ai": "0.87.1",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
43
|
+
"canvas": "3.2.3",
|
|
44
|
+
"discord.js": "14.27.0",
|
|
45
|
+
"pi-mcp-adapter": "2.37.0",
|
|
46
|
+
"pino": "10.3.1",
|
|
47
|
+
"typebox": "1.3.34",
|
|
48
|
+
"unpdf": "1.8.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"typescript": "7.0.2",
|
|
52
|
+
"@biomejs/biome": "2.5.14",
|
|
53
|
+
"@types/bun": "1.4.2"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { addPlugin } from "./add-plugin.ts";
|
|
5
|
+
import { init } from "./init.ts";
|
|
6
|
+
import { tempDir } from "./testing/fixtures.ts";
|
|
7
|
+
|
|
8
|
+
const dirs: ReturnType<typeof tempDir>[] = [];
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of dirs.splice(0)) dir.done();
|
|
11
|
+
});
|
|
12
|
+
const project = () => {
|
|
13
|
+
const dir = tempDir();
|
|
14
|
+
dirs.push(dir);
|
|
15
|
+
init({
|
|
16
|
+
cwd: dir.path,
|
|
17
|
+
bun: { version: "1.3.10", required: ">=1.3.0" },
|
|
18
|
+
version: "1.2.3",
|
|
19
|
+
});
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
const read = (dir: string, file: string) =>
|
|
23
|
+
readFileSync(join(dir, file), "utf8");
|
|
24
|
+
|
|
25
|
+
test("add plugin renders the plugin and its test from the hello template and lists it", () => {
|
|
26
|
+
const dir = project();
|
|
27
|
+
const report = addPlugin({ cwd: dir.path, name: "my-notes" });
|
|
28
|
+
expect(report.ok).toBe(true);
|
|
29
|
+
expect(report.changed).toEqual([
|
|
30
|
+
"plugins/my-notes.ts",
|
|
31
|
+
"plugins/my-notes.test.ts",
|
|
32
|
+
"roundtable.config.ts",
|
|
33
|
+
]);
|
|
34
|
+
const plugin = read(dir.path, "plugins/my-notes.ts");
|
|
35
|
+
expect(plugin).toContain("export const myNotes = definePlugin(");
|
|
36
|
+
expect(plugin).toContain('name: "my-notes"');
|
|
37
|
+
expect(plugin).toContain("my_notes_greet");
|
|
38
|
+
expect(read(dir.path, "plugins/my-notes.test.ts")).toContain(
|
|
39
|
+
'import { myNotes } from "./my-notes.ts"',
|
|
40
|
+
);
|
|
41
|
+
const config = read(dir.path, "roundtable.config.ts");
|
|
42
|
+
expect(config).toContain('import { myNotes } from "./plugins/my-notes.ts";');
|
|
43
|
+
expect(config).toContain("plugins: [hello, myNotes]");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a second plugin joins the first", () => {
|
|
47
|
+
const dir = project();
|
|
48
|
+
addPlugin({ cwd: dir.path, name: "alpha" });
|
|
49
|
+
addPlugin({ cwd: dir.path, name: "beta" });
|
|
50
|
+
expect(read(dir.path, "roundtable.config.ts")).toContain(
|
|
51
|
+
"plugins: [hello, alpha, beta]",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("add plugin refuses a name that is not lowercase kebab-case, changing nothing", () => {
|
|
56
|
+
const dir = project();
|
|
57
|
+
const before = read(dir.path, "roundtable.config.ts");
|
|
58
|
+
for (const name of [
|
|
59
|
+
"MyNotes",
|
|
60
|
+
"my_notes",
|
|
61
|
+
"-a",
|
|
62
|
+
"a--b",
|
|
63
|
+
"1a",
|
|
64
|
+
"",
|
|
65
|
+
"a b",
|
|
66
|
+
"../x",
|
|
67
|
+
]) {
|
|
68
|
+
const report = addPlugin({ cwd: dir.path, name });
|
|
69
|
+
expect(report.ok).toBe(false);
|
|
70
|
+
expect(report.problems.join(" ")).toContain("is not a plugin name");
|
|
71
|
+
}
|
|
72
|
+
expect(read(dir.path, "roundtable.config.ts")).toBe(before);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("add plugin refuses an existing file and leaves the config alone", () => {
|
|
76
|
+
const dir = project();
|
|
77
|
+
const before = read(dir.path, "roundtable.config.ts");
|
|
78
|
+
const report = addPlugin({ cwd: dir.path, name: "hello" });
|
|
79
|
+
expect(report.ok).toBe(false);
|
|
80
|
+
expect(report.problems.join(" ")).toContain("plugins/hello.ts");
|
|
81
|
+
expect(report.problems.join(" ")).toContain("already");
|
|
82
|
+
expect(read(dir.path, "roundtable.config.ts")).toBe(before);
|
|
83
|
+
dir.write("plugins/mine.test.ts", "// mine");
|
|
84
|
+
expect(addPlugin({ cwd: dir.path, name: "mine" }).ok).toBe(false);
|
|
85
|
+
expect(existsSync(join(dir.path, "plugins/mine.ts"))).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("add plugin refuses a config whose plugin list it cannot find, writing no plugin file", () => {
|
|
89
|
+
const dir = project();
|
|
90
|
+
dir.write("roundtable.config.ts", "export default buildConfig();\n");
|
|
91
|
+
const report = addPlugin({ cwd: dir.path, name: "extra" });
|
|
92
|
+
expect(report.ok).toBe(false);
|
|
93
|
+
expect(report.problems.join(" ")).toContain("cannot find the plugin list");
|
|
94
|
+
expect(existsSync(join(dir.path, "plugins/extra.ts"))).toBe(false);
|
|
95
|
+
expect(existsSync(join(dir.path, "plugins/extra.test.ts"))).toBe(false);
|
|
96
|
+
expect(read(dir.path, "roundtable.config.ts")).toBe(
|
|
97
|
+
"export default buildConfig();\n",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("add plugin outside a project says where it looked", () => {
|
|
102
|
+
const dir = tempDir();
|
|
103
|
+
dirs.push(dir);
|
|
104
|
+
const report = addPlugin({ cwd: dir.path, name: "extra" });
|
|
105
|
+
expect(report.ok).toBe(false);
|
|
106
|
+
expect(report.problems.join(" ")).toContain("roundtable.config.ts is not in");
|
|
107
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { addPluginToConfig, ConfigEditError } from "./config-edit.ts";
|
|
4
|
+
import {
|
|
5
|
+
isPluginName,
|
|
6
|
+
pluginNames,
|
|
7
|
+
renderPlugin,
|
|
8
|
+
writeRendered,
|
|
9
|
+
} from "./templates.ts";
|
|
10
|
+
|
|
11
|
+
const CONFIG = "roundtable.config.ts";
|
|
12
|
+
|
|
13
|
+
export interface AddPluginInputs {
|
|
14
|
+
cwd: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** Where the templates are; default the package's own. */
|
|
17
|
+
templates?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AddPluginReport {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
problems: string[];
|
|
23
|
+
/** Paths written or changed, relative to `cwd`. */
|
|
24
|
+
changed: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const refused = (...problems: string[]): AddPluginReport => ({
|
|
28
|
+
ok: false,
|
|
29
|
+
problems,
|
|
30
|
+
changed: [],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates `plugins/<name>.ts` and its test from the `hello` template and lists the plugin in
|
|
35
|
+
* `roundtable.config.ts`. Everything is checked and rendered before the first write, so a refusal
|
|
36
|
+
* leaves the project untouched.
|
|
37
|
+
*/
|
|
38
|
+
export function addPlugin(inputs: AddPluginInputs): AddPluginReport {
|
|
39
|
+
const { cwd, name } = inputs;
|
|
40
|
+
if (!isPluginName(name))
|
|
41
|
+
return refused(
|
|
42
|
+
`${JSON.stringify(name)} is not a plugin name. Use lowercase words joined by dashes, such as my-notes.`,
|
|
43
|
+
);
|
|
44
|
+
const configPath = join(cwd, CONFIG);
|
|
45
|
+
if (!existsSync(configPath))
|
|
46
|
+
return refused(
|
|
47
|
+
`${CONFIG} is not in ${cwd}. Run this in the project directory, or create the project with \`roundtable init\`.`,
|
|
48
|
+
);
|
|
49
|
+
const files = renderPlugin(pluginNames(name), inputs.templates);
|
|
50
|
+
const existing = files
|
|
51
|
+
.map((file) => file.path)
|
|
52
|
+
.filter((path) => existsSync(join(cwd, path)));
|
|
53
|
+
if (existing.length > 0)
|
|
54
|
+
return refused(
|
|
55
|
+
`${existing.join(" and ")} already ${existing.length === 1 ? "exists" : "exist"}. Choose another name or remove the file.`,
|
|
56
|
+
);
|
|
57
|
+
let edited: string;
|
|
58
|
+
try {
|
|
59
|
+
edited = addPluginToConfig(
|
|
60
|
+
readFileSync(configPath, "utf8"),
|
|
61
|
+
pluginNames(name),
|
|
62
|
+
CONFIG,
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof ConfigEditError) return refused(error.message);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
writeRendered(cwd, files);
|
|
69
|
+
writeFileSync(configPath, edited);
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
problems: [],
|
|
73
|
+
changed: [...files.map((file) => file.path), CONFIG],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Project } from "../project.ts";
|
|
3
|
+
import type { Result } from "../report.ts";
|
|
4
|
+
import {
|
|
5
|
+
fakeHttp,
|
|
6
|
+
fakePorts,
|
|
7
|
+
tempDir,
|
|
8
|
+
validConfig,
|
|
9
|
+
} from "../testing/fixtures.ts";
|
|
10
|
+
import { checkBun } from "./bun.ts";
|
|
11
|
+
import { checkConfiguration, checkPlugins } from "./configuration.ts";
|
|
12
|
+
import { checkEnvironment } from "./environment.ts";
|
|
13
|
+
import { checkModelLogin } from "./model.ts";
|
|
14
|
+
import { checkPublicUrl } from "./public-url.ts";
|
|
15
|
+
|
|
16
|
+
const dirs: ReturnType<typeof tempDir>[] = [];
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const dir of dirs.splice(0)) dir.done();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** The failure's problem and fix text, or an assertion failure when the check passed or skipped. */
|
|
22
|
+
function failure(result: Result): string {
|
|
23
|
+
if (result.status !== "fail")
|
|
24
|
+
throw new Error(`expected a failure, got ${JSON.stringify(result)}`);
|
|
25
|
+
return `${result.problem}\n${result.fix}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("Bun", () => {
|
|
29
|
+
test("passes on a version in range", () => {
|
|
30
|
+
expect(checkBun({ version: "1.3.10", required: ">=1.3.0" }).status).toBe(
|
|
31
|
+
"ok",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
test("fails when missing, with the install page as the fix", () => {
|
|
35
|
+
const text = failure(checkBun({ version: undefined, required: ">=1.3.0" }));
|
|
36
|
+
expect(text).toContain("not installed");
|
|
37
|
+
expect(text).toContain("https://bun.sh/docs/installation");
|
|
38
|
+
});
|
|
39
|
+
test("fails when too old, saying to upgrade", () => {
|
|
40
|
+
const text = failure(checkBun({ version: "1.2.0", required: ">=1.3.0" }));
|
|
41
|
+
expect(text).toContain("1.2.0");
|
|
42
|
+
expect(text).toContain("bun upgrade");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("environment", () => {
|
|
47
|
+
const example =
|
|
48
|
+
"# comment\nDISCORD_TOKEN=\nDATABASE_URL=postgres://x\n# OPTIONAL=\nOWNER_ID=\n";
|
|
49
|
+
test("passes when every listed variable has a value", () => {
|
|
50
|
+
const dir = tempDir();
|
|
51
|
+
dirs.push(dir);
|
|
52
|
+
dir.write(".env.example", example);
|
|
53
|
+
expect(
|
|
54
|
+
checkEnvironment(dir.path, {
|
|
55
|
+
DISCORD_TOKEN: "t",
|
|
56
|
+
DATABASE_URL: "d",
|
|
57
|
+
OWNER_ID: "1",
|
|
58
|
+
}).status,
|
|
59
|
+
).toBe("ok");
|
|
60
|
+
});
|
|
61
|
+
test("names every variable without a value, but not commented ones, and says to copy the example", () => {
|
|
62
|
+
const dir = tempDir();
|
|
63
|
+
dirs.push(dir);
|
|
64
|
+
dir.write(".env.example", example);
|
|
65
|
+
const text = failure(
|
|
66
|
+
checkEnvironment(dir.path, { DATABASE_URL: "d", OWNER_ID: " " }),
|
|
67
|
+
);
|
|
68
|
+
expect(text).toContain("DISCORD_TOKEN, OWNER_ID");
|
|
69
|
+
expect(text).not.toContain("OPTIONAL");
|
|
70
|
+
expect(text).not.toContain("DATABASE_URL");
|
|
71
|
+
expect(text).toContain("Copy .env.example to .env");
|
|
72
|
+
});
|
|
73
|
+
test("says to fill .env when it exists", () => {
|
|
74
|
+
const dir = tempDir();
|
|
75
|
+
dirs.push(dir);
|
|
76
|
+
dir.write(".env.example", example);
|
|
77
|
+
dir.write(".env", "");
|
|
78
|
+
expect(failure(checkEnvironment(dir.path, {}))).toContain(
|
|
79
|
+
"Fill them in .env",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
test("skips a project without an example", () => {
|
|
83
|
+
const dir = tempDir();
|
|
84
|
+
dirs.push(dir);
|
|
85
|
+
expect(checkEnvironment(dir.path, {}).status).toBe("skipped");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("configuration", () => {
|
|
90
|
+
const project = (config: unknown) => new Project("/x", fakePorts(config));
|
|
91
|
+
test("passes a configuration the schema accepts", async () => {
|
|
92
|
+
expect((await checkConfiguration(project(validConfig))).status).toBe("ok");
|
|
93
|
+
});
|
|
94
|
+
test("names the failing key, and says where to fix it", async () => {
|
|
95
|
+
const text = failure(
|
|
96
|
+
await checkConfiguration(
|
|
97
|
+
project({
|
|
98
|
+
...validConfig,
|
|
99
|
+
discord: { ...validConfig.discord, token: "" },
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
expect(text).toContain("config discord.token");
|
|
104
|
+
expect(text).toContain("roundtable.config.ts");
|
|
105
|
+
});
|
|
106
|
+
test("names the closest known key for one it does not know", async () => {
|
|
107
|
+
const text = failure(
|
|
108
|
+
await checkConfiguration(project({ ...validConfig, modle: "x" })),
|
|
109
|
+
);
|
|
110
|
+
expect(text).toContain('Did you mean "model"');
|
|
111
|
+
});
|
|
112
|
+
test("fails with the loader's message when the file cannot be loaded", async () => {
|
|
113
|
+
const failing = new Project(
|
|
114
|
+
"/x",
|
|
115
|
+
fakePorts(undefined, {
|
|
116
|
+
loadConfig: async () => {
|
|
117
|
+
throw new Error("roundtable.config.ts is not in /x");
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(failure(await checkConfiguration(failing))).toContain(
|
|
122
|
+
"is not in /x",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
test("fails with the assembly's own message", async () => {
|
|
126
|
+
const failing = new Project(
|
|
127
|
+
"/x",
|
|
128
|
+
fakePorts(validConfig, {
|
|
129
|
+
define: async () => {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"config prompts.shared: cannot read ./persona/shared.md",
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
expect(failure(await checkConfiguration(failing))).toContain(
|
|
137
|
+
"prompts.shared",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("plugins", () => {
|
|
143
|
+
const plugin = (name: string) => ({ name, setup: () => ({}) });
|
|
144
|
+
const withPlugins = (names: string[]) =>
|
|
145
|
+
new Project(
|
|
146
|
+
"/x",
|
|
147
|
+
fakePorts(
|
|
148
|
+
{ ...validConfig, plugins: [plugin("hello")] },
|
|
149
|
+
{
|
|
150
|
+
define: async () => ({
|
|
151
|
+
options: { logger: {} as never },
|
|
152
|
+
plugins: names.map(plugin),
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
test("passes when every name is its own and lists the operator's", async () => {
|
|
158
|
+
const result = await checkPlugins(withPlugins(["database", "hello"]));
|
|
159
|
+
expect(result).toEqual({ status: "ok", detail: "yours: hello" });
|
|
160
|
+
});
|
|
161
|
+
test("fails on a repeated name and says to rename", async () => {
|
|
162
|
+
const text = failure(
|
|
163
|
+
await checkPlugins(withPlugins(["database", "hello", "hello"])),
|
|
164
|
+
);
|
|
165
|
+
expect(text).toContain("two plugins are named hello");
|
|
166
|
+
expect(text).toContain("Rename");
|
|
167
|
+
});
|
|
168
|
+
test("is skipped while the configuration is invalid", async () => {
|
|
169
|
+
const result = await checkPlugins(new Project("/x", fakePorts({})));
|
|
170
|
+
expect(result.status).toBe("skipped");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("model login", () => {
|
|
175
|
+
const login = (source: string | undefined, seen: string[] = []) =>
|
|
176
|
+
fakePorts(validConfig, {
|
|
177
|
+
login: async (dir, provider) => {
|
|
178
|
+
seen.push(`${dir} ${provider}`);
|
|
179
|
+
return source;
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
test("passes when the provider has a login and says where it comes from", async () => {
|
|
183
|
+
const seen: string[] = [];
|
|
184
|
+
const result = await checkModelLogin(
|
|
185
|
+
new Project("/x", login("ANTHROPIC_API_KEY", seen)),
|
|
186
|
+
login("ANTHROPIC_API_KEY", seen),
|
|
187
|
+
);
|
|
188
|
+
expect(result).toEqual({
|
|
189
|
+
status: "ok",
|
|
190
|
+
detail: "anthropic login from ANTHROPIC_API_KEY",
|
|
191
|
+
});
|
|
192
|
+
expect(seen).toEqual(["/data/pi anthropic"]);
|
|
193
|
+
});
|
|
194
|
+
test("fails when there is none, naming the provider, the model, and the ways to log in", async () => {
|
|
195
|
+
const ports = login(undefined);
|
|
196
|
+
const text = failure(
|
|
197
|
+
await checkModelLogin(new Project("/x", ports), ports),
|
|
198
|
+
);
|
|
199
|
+
expect(text).toContain("no login for anthropic");
|
|
200
|
+
expect(text).toContain("anthropic/claude-sonnet-5-5");
|
|
201
|
+
expect(text).toContain("ANTHROPIC_API_KEY");
|
|
202
|
+
expect(text).toContain("/data/pi/auth.json");
|
|
203
|
+
});
|
|
204
|
+
test("fails on a model that is not <provider>/<id>", async () => {
|
|
205
|
+
const ports = fakePorts({ ...validConfig, model: "sonnet" });
|
|
206
|
+
expect(
|
|
207
|
+
failure(await checkModelLogin(new Project("/x", ports), ports)),
|
|
208
|
+
).toContain("<provider>/<id>");
|
|
209
|
+
});
|
|
210
|
+
test("uses the configured agent directory", async () => {
|
|
211
|
+
const seen: string[] = [];
|
|
212
|
+
const ports = fakePorts(
|
|
213
|
+
{ ...validConfig, agentDir: "/agents" },
|
|
214
|
+
{
|
|
215
|
+
login: async (dir) => {
|
|
216
|
+
seen.push(dir);
|
|
217
|
+
return "x";
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
await checkModelLogin(new Project("/x", ports), ports);
|
|
222
|
+
expect(seen).toEqual(["/agents"]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("public URL", () => {
|
|
227
|
+
const project = (url: unknown) =>
|
|
228
|
+
new Project("/x", fakePorts({ ...validConfig, http: { publicUrl: url } }));
|
|
229
|
+
test("passes a well-formed address without asking the network", async () => {
|
|
230
|
+
const http = fakeHttp({});
|
|
231
|
+
const result = await checkPublicUrl(
|
|
232
|
+
project("https://bot.example.test"),
|
|
233
|
+
http,
|
|
234
|
+
false,
|
|
235
|
+
);
|
|
236
|
+
expect(result.status).toBe("ok");
|
|
237
|
+
expect(http.requests).toEqual([]);
|
|
238
|
+
});
|
|
239
|
+
test("fails on something that is not a URL, showing an example", async () => {
|
|
240
|
+
const text = failure(
|
|
241
|
+
await checkPublicUrl(project("bot.example.test"), fakeHttp({}), false),
|
|
242
|
+
);
|
|
243
|
+
expect(text).toContain("not a URL");
|
|
244
|
+
expect(text).toContain("https://bot.example.com");
|
|
245
|
+
});
|
|
246
|
+
test("fails on a scheme Discord cannot fetch from", async () => {
|
|
247
|
+
expect(
|
|
248
|
+
failure(
|
|
249
|
+
await checkPublicUrl(
|
|
250
|
+
project("ftp://bot.example.test"),
|
|
251
|
+
fakeHttp({}),
|
|
252
|
+
false,
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
).toContain("http(s)");
|
|
256
|
+
});
|
|
257
|
+
test("with reachable, passes on any answer and fails when nothing answers", async () => {
|
|
258
|
+
const answering = fakeHttp({ "/": { status: 404, body: undefined } });
|
|
259
|
+
expect(
|
|
260
|
+
await checkPublicUrl(
|
|
261
|
+
project("https://bot.example.test"),
|
|
262
|
+
answering,
|
|
263
|
+
true,
|
|
264
|
+
),
|
|
265
|
+
).toEqual({
|
|
266
|
+
status: "ok",
|
|
267
|
+
detail: "https://bot.example.test answered 404",
|
|
268
|
+
});
|
|
269
|
+
const down = fakeHttp({ "/": new Error("connection refused") });
|
|
270
|
+
const text = failure(
|
|
271
|
+
await checkPublicUrl(project("https://bot.example.test"), down, true),
|
|
272
|
+
);
|
|
273
|
+
expect(text).toContain("connection refused");
|
|
274
|
+
expect(text).toContain("roundtable start");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fail, ok, type Result } from "../report.ts";
|
|
2
|
+
|
|
3
|
+
export const BUN_INSTALL_PAGE = "https://bun.sh/docs/installation";
|
|
4
|
+
|
|
5
|
+
/** What the command line knows about the Bun it runs on and the one the package needs. */
|
|
6
|
+
export interface BunFacts {
|
|
7
|
+
/** The running Bun's version; undefined when there is none. */
|
|
8
|
+
version: string | undefined;
|
|
9
|
+
/** The package's `engines.bun` range. */
|
|
10
|
+
required: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Bun's version against the package's range; both `init` and `doctor` refuse on the same words. */
|
|
14
|
+
export function checkBun({ version, required }: BunFacts): Result {
|
|
15
|
+
if (version === undefined)
|
|
16
|
+
return fail(
|
|
17
|
+
"Bun is not installed.",
|
|
18
|
+
`pi-roundtable runs on Bun ${required}. Install it from ${BUN_INSTALL_PAGE} and run the command again.`,
|
|
19
|
+
);
|
|
20
|
+
if (!Bun.semver.satisfies(version, required))
|
|
21
|
+
return fail(
|
|
22
|
+
`Bun ${version} is older than the ${required} this package needs.`,
|
|
23
|
+
`Upgrade with \`bun upgrade\` (see ${BUN_INSTALL_PAGE}) and run the command again.`,
|
|
24
|
+
);
|
|
25
|
+
return ok(`Bun ${version}`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CONFIG_FILE, type Project } from "../project.ts";
|
|
2
|
+
import { fail, ok, type Result } from "../report.ts";
|
|
3
|
+
|
|
4
|
+
/** The configuration against its schema: the failing key is named by the schema's own message. */
|
|
5
|
+
export async function checkConfiguration(project: Project): Promise<Result> {
|
|
6
|
+
const assembled = await project.assembled();
|
|
7
|
+
return assembled.ok ? ok(`${CONFIG_FILE} is valid`) : assembled.failure;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Every plugin has its own name, the operator's and the built-ins' together. */
|
|
11
|
+
export async function checkPlugins(project: Project): Promise<Result> {
|
|
12
|
+
const assembled = await project.assembled();
|
|
13
|
+
if (!assembled.ok)
|
|
14
|
+
return { status: "skipped", reason: "the configuration is not valid yet" };
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (const { name } of assembled.value.defined.plugins) {
|
|
17
|
+
if (seen.has(name))
|
|
18
|
+
return fail(
|
|
19
|
+
`two plugins are named ${name}.`,
|
|
20
|
+
"Rename yours: a plugin's name is in its definePlugin call, and the built-in plugins already use theirs.",
|
|
21
|
+
);
|
|
22
|
+
seen.add(name);
|
|
23
|
+
}
|
|
24
|
+
const own = assembled.value.config.plugins.map((plugin) => plugin.name);
|
|
25
|
+
return ok(
|
|
26
|
+
own.length === 0 ? "no plugins of your own" : `yours: ${own.join(", ")}`,
|
|
27
|
+
);
|
|
28
|
+
}
|