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,96 @@
|
|
|
1
|
+
import { PermissionFlagsBits, PermissionsBitField } from "discord.js";
|
|
2
|
+
import type { Http, HttpResponse } from "./http.ts";
|
|
3
|
+
|
|
4
|
+
const API = "https://discord.com/api/v10";
|
|
5
|
+
|
|
6
|
+
/** What the bot needs in the entry channel, and why; the doctor names any it lacks. */
|
|
7
|
+
export const REQUIRED_PERMISSIONS = [
|
|
8
|
+
["ViewChannel", "see the channel"],
|
|
9
|
+
["SendMessages", "answer in it"],
|
|
10
|
+
["SendMessagesInThreads", "answer in threads"],
|
|
11
|
+
["ReadMessageHistory", "read what was said before"],
|
|
12
|
+
["EmbedLinks", "post cards"],
|
|
13
|
+
["AttachFiles", "post pictures and files"],
|
|
14
|
+
["PinMessages", "pin the dashboard"],
|
|
15
|
+
["CreatePublicThreads", "open threads for background reports"],
|
|
16
|
+
["ManageChannels", "create the agents' channels"],
|
|
17
|
+
["ManageWebhooks", "let each agent speak under its own name"],
|
|
18
|
+
] as const satisfies readonly (readonly [
|
|
19
|
+
keyof typeof PermissionFlagsBits,
|
|
20
|
+
string,
|
|
21
|
+
])[];
|
|
22
|
+
|
|
23
|
+
/** The permission integer an invitation asks for. */
|
|
24
|
+
export const INVITE_PERMISSIONS = REQUIRED_PERMISSIONS.reduce(
|
|
25
|
+
(bits, [name]) => bits | PermissionFlagsBits[name],
|
|
26
|
+
0n,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function inviteUrl(botId: string): string {
|
|
30
|
+
return `https://discord.com/oauth2/authorize?client_id=${botId}&scope=bot+applications.commands&permissions=${INVITE_PERMISSIONS}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Role {
|
|
34
|
+
id: string;
|
|
35
|
+
permissions: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Overwrite {
|
|
39
|
+
id: string;
|
|
40
|
+
type: number;
|
|
41
|
+
allow: string;
|
|
42
|
+
deny: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What Discord grants `botId` in a channel: the guild's roles, then the channel's overwrites, as Discord documents. */
|
|
46
|
+
export function channelPermissions(input: {
|
|
47
|
+
botId: string;
|
|
48
|
+
guildId: string;
|
|
49
|
+
ownerId: string;
|
|
50
|
+
roles: readonly Role[];
|
|
51
|
+
memberRoles: readonly string[];
|
|
52
|
+
overwrites: readonly Overwrite[];
|
|
53
|
+
}): PermissionsBitField {
|
|
54
|
+
if (input.botId === input.ownerId)
|
|
55
|
+
return new PermissionsBitField(PermissionsBitField.All);
|
|
56
|
+
let bits = 0n;
|
|
57
|
+
for (const role of input.roles)
|
|
58
|
+
if (role.id === input.guildId || input.memberRoles.includes(role.id))
|
|
59
|
+
bits |= BigInt(role.permissions);
|
|
60
|
+
if (bits & PermissionFlagsBits.Administrator)
|
|
61
|
+
return new PermissionsBitField(PermissionsBitField.All);
|
|
62
|
+
const apply = (allow: bigint, deny: bigint) => {
|
|
63
|
+
bits = (bits & ~deny) | allow;
|
|
64
|
+
};
|
|
65
|
+
const pick = (match: (overwrite: Overwrite) => boolean) => {
|
|
66
|
+
let allow = 0n;
|
|
67
|
+
let deny = 0n;
|
|
68
|
+
for (const overwrite of input.overwrites.filter(match)) {
|
|
69
|
+
allow |= BigInt(overwrite.allow);
|
|
70
|
+
deny |= BigInt(overwrite.deny);
|
|
71
|
+
}
|
|
72
|
+
apply(allow, deny);
|
|
73
|
+
};
|
|
74
|
+
pick((overwrite) => overwrite.id === input.guildId);
|
|
75
|
+
pick(
|
|
76
|
+
(overwrite) =>
|
|
77
|
+
overwrite.type === 0 && input.memberRoles.includes(overwrite.id),
|
|
78
|
+
);
|
|
79
|
+
pick((overwrite) => overwrite.type === 1 && overwrite.id === input.botId);
|
|
80
|
+
return new PermissionsBitField(bits);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The Discord REST calls the doctor makes with the bot's own token, over an injected client. */
|
|
84
|
+
export class DiscordApi {
|
|
85
|
+
readonly #http: Http;
|
|
86
|
+
readonly #headers: Record<string, string>;
|
|
87
|
+
|
|
88
|
+
constructor(http: Http, token: string) {
|
|
89
|
+
this.#http = http;
|
|
90
|
+
this.#headers = { Authorization: `Bot ${token}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get(path: string): Promise<HttpResponse> {
|
|
94
|
+
return this.#http.get(`${API}${path}`, this.#headers);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DefinedRoundtable } from "../core/define-roundtable.ts";
|
|
3
|
+
import type { DatabasePort } from "./checks/database.ts";
|
|
4
|
+
import { INVITE_PERMISSIONS } from "./discord-api.ts";
|
|
5
|
+
import { buildChecks, type DoctorInputs, doctor } from "./doctor.ts";
|
|
6
|
+
import { type Check, formatOutcomes, runChecks } from "./report.ts";
|
|
7
|
+
import { start } from "./start.ts";
|
|
8
|
+
import {
|
|
9
|
+
fakeHttp,
|
|
10
|
+
fakePorts,
|
|
11
|
+
healthyDiscord,
|
|
12
|
+
tempDir,
|
|
13
|
+
validConfig,
|
|
14
|
+
} from "./testing/fixtures.ts";
|
|
15
|
+
|
|
16
|
+
const dirs: ReturnType<typeof tempDir>[] = [];
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const dir of dirs.splice(0)) dir.done();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const workingDatabase: DatabasePort = { check: async () => {} };
|
|
22
|
+
const bun = { version: "1.3.10", required: ">=1.3.0" };
|
|
23
|
+
|
|
24
|
+
function inputs(overrides: Partial<DoctorInputs> = {}): DoctorInputs {
|
|
25
|
+
const dir = tempDir();
|
|
26
|
+
dirs.push(dir);
|
|
27
|
+
return {
|
|
28
|
+
cwd: dir.path,
|
|
29
|
+
env: {},
|
|
30
|
+
bun,
|
|
31
|
+
ports: fakePorts(),
|
|
32
|
+
database: workingDatabase,
|
|
33
|
+
http: fakeHttp({
|
|
34
|
+
...healthyDiscord(INVITE_PERMISSIONS),
|
|
35
|
+
"/": { status: 200, body: undefined },
|
|
36
|
+
}),
|
|
37
|
+
reachable: false,
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("doctor", () => {
|
|
43
|
+
test("lists the checks in the order the spec gives", () => {
|
|
44
|
+
expect(buildChecks(inputs()).map((check) => check.name)).toEqual([
|
|
45
|
+
"Bun",
|
|
46
|
+
"environment",
|
|
47
|
+
"configuration",
|
|
48
|
+
"plugins",
|
|
49
|
+
"PostgreSQL",
|
|
50
|
+
"Discord token",
|
|
51
|
+
"Discord guild",
|
|
52
|
+
"Discord intents",
|
|
53
|
+
"Discord channel",
|
|
54
|
+
"model login",
|
|
55
|
+
"public URL",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("passes on a healthy setup", async () => {
|
|
60
|
+
const report = await doctor(inputs());
|
|
61
|
+
expect(report.ok).toBe(true);
|
|
62
|
+
expect(
|
|
63
|
+
report.outcomes.every(({ result }) => result.status !== "fail"),
|
|
64
|
+
).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("fails when any check fails, and prints each check as passed or failed with its fix", async () => {
|
|
68
|
+
const report = await doctor(
|
|
69
|
+
inputs({
|
|
70
|
+
ports: fakePorts(validConfig, { login: async () => undefined }),
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
expect(report.ok).toBe(false);
|
|
74
|
+
const lines = formatOutcomes(report.outcomes);
|
|
75
|
+
expect(lines.filter((line) => line.startsWith("✓"))).not.toHaveLength(0);
|
|
76
|
+
const failed = lines.findIndex((line) => line.startsWith("✗ model login"));
|
|
77
|
+
expect(failed).toBeGreaterThan(-1);
|
|
78
|
+
expect(lines[failed + 1]).toStartWith(" ");
|
|
79
|
+
expect(lines[failed + 1]).toContain("API key");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a missing credential fails the checks that need it and skips what depends on it, changing nothing", async () => {
|
|
83
|
+
const incomplete = {
|
|
84
|
+
...validConfig,
|
|
85
|
+
discord: { ...validConfig.discord, token: "" },
|
|
86
|
+
};
|
|
87
|
+
const http = fakeHttp({});
|
|
88
|
+
const report = await doctor(inputs({ ports: fakePorts(incomplete), http }));
|
|
89
|
+
const byName = Object.fromEntries(
|
|
90
|
+
report.outcomes.map(({ name, result }) => [name, result.status]),
|
|
91
|
+
);
|
|
92
|
+
expect(byName.configuration).toBe("fail");
|
|
93
|
+
expect(byName["Discord token"]).toBe("skipped");
|
|
94
|
+
expect(byName["Discord guild"]).toBe("skipped");
|
|
95
|
+
expect(byName["Discord channel"]).toBe("skipped");
|
|
96
|
+
expect(http.requests).toEqual([]);
|
|
97
|
+
expect(report.ok).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a check that throws is reported as failed without hiding the ones after it", async () => {
|
|
101
|
+
const checks: Check[] = [
|
|
102
|
+
{
|
|
103
|
+
name: "boom",
|
|
104
|
+
offline: true,
|
|
105
|
+
run: async () => {
|
|
106
|
+
throw new Error("kaput");
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{ name: "fine", offline: true, run: async () => ({ status: "ok" }) },
|
|
110
|
+
];
|
|
111
|
+
const outcomes = await runChecks(checks);
|
|
112
|
+
expect(outcomes.map(({ result }) => result.status)).toEqual(["fail", "ok"]);
|
|
113
|
+
expect(formatOutcomes(outcomes).join("\n")).toContain("kaput");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("asks the public address to answer only with reachable", async () => {
|
|
117
|
+
const quiet = inputs();
|
|
118
|
+
await doctor(quiet);
|
|
119
|
+
expect((quiet.http as ReturnType<typeof fakeHttp>).requests).not.toContain(
|
|
120
|
+
"/",
|
|
121
|
+
);
|
|
122
|
+
const asking = inputs({ reachable: true });
|
|
123
|
+
await doctor(asking);
|
|
124
|
+
expect((asking.http as ReturnType<typeof fakeHttp>).requests).toContain(
|
|
125
|
+
"/",
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("start", () => {
|
|
131
|
+
const launched = () => {
|
|
132
|
+
const seen: DefinedRoundtable[] = [];
|
|
133
|
+
return {
|
|
134
|
+
seen,
|
|
135
|
+
launch: async (defined: DefinedRoundtable) => void seen.push(defined),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
test("runs only the checks that need no network, then starts the bot", async () => {
|
|
140
|
+
const http = fakeHttp({});
|
|
141
|
+
const { seen, launch } = launched();
|
|
142
|
+
const report = await start({ ...inputs({ http }), launch });
|
|
143
|
+
expect(report.started).toBe(true);
|
|
144
|
+
expect(seen).toHaveLength(1);
|
|
145
|
+
expect(http.requests).toEqual([]);
|
|
146
|
+
expect(report.outcomes.map(({ name }) => name)).toEqual([
|
|
147
|
+
"Bun",
|
|
148
|
+
"environment",
|
|
149
|
+
"configuration",
|
|
150
|
+
"plugins",
|
|
151
|
+
"model login",
|
|
152
|
+
"public URL",
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a failing check stops it before anything launches, with the message doctor prints", async () => {
|
|
157
|
+
const failing = inputs({
|
|
158
|
+
ports: fakePorts(validConfig, { login: async () => undefined }),
|
|
159
|
+
});
|
|
160
|
+
const { seen, launch } = launched();
|
|
161
|
+
const started = await start({ ...failing, launch });
|
|
162
|
+
expect(started.started).toBe(false);
|
|
163
|
+
expect(seen).toEqual([]);
|
|
164
|
+
const fromDoctor = (await doctor(failing)).outcomes.find(
|
|
165
|
+
({ name }) => name === "model login",
|
|
166
|
+
);
|
|
167
|
+
const fromStart = started.outcomes.find(
|
|
168
|
+
({ name }) => name === "model login",
|
|
169
|
+
);
|
|
170
|
+
expect(fromStart).toEqual(fromDoctor as NonNullable<typeof fromDoctor>);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("does not touch the network when the configuration is wrong", async () => {
|
|
174
|
+
const http = fakeHttp({});
|
|
175
|
+
const { seen, launch } = launched();
|
|
176
|
+
const report = await start({
|
|
177
|
+
...inputs({ ports: fakePorts({}), http }),
|
|
178
|
+
launch,
|
|
179
|
+
});
|
|
180
|
+
expect(report.started).toBe(false);
|
|
181
|
+
expect(seen).toEqual([]);
|
|
182
|
+
expect(http.requests).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type BunFacts, checkBun } from "./checks/bun.ts";
|
|
2
|
+
import { checkConfiguration, checkPlugins } from "./checks/configuration.ts";
|
|
3
|
+
import { checkDatabase, type DatabasePort } from "./checks/database.ts";
|
|
4
|
+
import {
|
|
5
|
+
checkChannel,
|
|
6
|
+
checkGuild,
|
|
7
|
+
checkIntents,
|
|
8
|
+
checkToken,
|
|
9
|
+
Discord,
|
|
10
|
+
} from "./checks/discord.ts";
|
|
11
|
+
import { checkEnvironment } from "./checks/environment.ts";
|
|
12
|
+
import { checkModelLogin } from "./checks/model.ts";
|
|
13
|
+
import { checkPublicUrl } from "./checks/public-url.ts";
|
|
14
|
+
import type { Http } from "./http.ts";
|
|
15
|
+
import { type Ports, Project } from "./project.ts";
|
|
16
|
+
import { type Check, failed, type Outcome, runChecks } from "./report.ts";
|
|
17
|
+
|
|
18
|
+
export interface DoctorInputs {
|
|
19
|
+
cwd: string;
|
|
20
|
+
env: Record<string, string | undefined>;
|
|
21
|
+
bun: BunFacts;
|
|
22
|
+
ports: Ports;
|
|
23
|
+
database: DatabasePort;
|
|
24
|
+
http: Http;
|
|
25
|
+
/** Also ask the public address to answer, which is only true while the bot runs. */
|
|
26
|
+
reachable: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DoctorReport {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
outcomes: Outcome[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every check, in the order they are shown; `offline` ones need no network and are the ones `start` runs. */
|
|
35
|
+
export function buildChecks(
|
|
36
|
+
inputs: DoctorInputs,
|
|
37
|
+
project = new Project(inputs.cwd, inputs.ports),
|
|
38
|
+
): Check[] {
|
|
39
|
+
const { cwd, env, bun, ports, database, http, reachable } = inputs;
|
|
40
|
+
const discord = new Discord(project, http);
|
|
41
|
+
return [
|
|
42
|
+
{ name: "Bun", offline: true, run: async () => checkBun(bun) },
|
|
43
|
+
{
|
|
44
|
+
name: "environment",
|
|
45
|
+
offline: true,
|
|
46
|
+
run: async () => checkEnvironment(cwd, env),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "configuration",
|
|
50
|
+
offline: true,
|
|
51
|
+
run: () => checkConfiguration(project),
|
|
52
|
+
},
|
|
53
|
+
{ name: "plugins", offline: true, run: () => checkPlugins(project) },
|
|
54
|
+
{
|
|
55
|
+
name: "PostgreSQL",
|
|
56
|
+
offline: false,
|
|
57
|
+
run: () => checkDatabase(project, database),
|
|
58
|
+
},
|
|
59
|
+
{ name: "Discord token", offline: false, run: () => checkToken(discord) },
|
|
60
|
+
{
|
|
61
|
+
name: "Discord guild",
|
|
62
|
+
offline: false,
|
|
63
|
+
run: () => checkGuild(project, discord),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "Discord intents",
|
|
67
|
+
offline: false,
|
|
68
|
+
run: () => checkIntents(discord),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "Discord channel",
|
|
72
|
+
offline: false,
|
|
73
|
+
run: () => checkChannel(project, discord),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "model login",
|
|
77
|
+
offline: true,
|
|
78
|
+
run: () => checkModelLogin(project, ports),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "public URL",
|
|
82
|
+
offline: true,
|
|
83
|
+
run: () => checkPublicUrl(project, http, reachable),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Runs every check and reports each; it changes nothing it looked at. */
|
|
89
|
+
export async function doctor(inputs: DoctorInputs): Promise<DoctorReport> {
|
|
90
|
+
const outcomes = await runChecks(buildChecks(inputs));
|
|
91
|
+
return { ok: failed(outcomes).length === 0, outcomes };
|
|
92
|
+
}
|
package/src/cli/http.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** One HTTP answer: its status and its JSON body, undefined when the body is not JSON. */
|
|
2
|
+
export interface HttpResponse {
|
|
3
|
+
status: number;
|
|
4
|
+
body: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The one network call the checks make, so tests never touch the network. */
|
|
8
|
+
export interface Http {
|
|
9
|
+
get(url: string, headers?: Record<string, string>): Promise<HttpResponse>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
export const fetchHttp: Http = {
|
|
15
|
+
async get(url, headers) {
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
headers,
|
|
18
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
19
|
+
});
|
|
20
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
21
|
+
return { status: response.status, body };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { BUN_INSTALL_PAGE } from "./checks/bun.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 temp = () => {
|
|
13
|
+
const dir = tempDir();
|
|
14
|
+
dirs.push(dir);
|
|
15
|
+
return dir;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const bun = { version: "1.3.10", required: ">=1.3.0" };
|
|
19
|
+
|
|
20
|
+
const everything = (dir: string): string[] =>
|
|
21
|
+
readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isFile())
|
|
23
|
+
.map((entry) => join(entry.parentPath, entry.name).slice(dir.length + 1))
|
|
24
|
+
.sort();
|
|
25
|
+
|
|
26
|
+
test("init writes the skeleton and the hello plugin, pins the version, and prints three next steps", () => {
|
|
27
|
+
const dir = temp();
|
|
28
|
+
const report = init({ cwd: dir.path, dir: "bot", bun, version: "1.2.3" });
|
|
29
|
+
expect(report.ok).toBe(true);
|
|
30
|
+
expect(everything(join(dir.path, "bot"))).toEqual(
|
|
31
|
+
[
|
|
32
|
+
".env.example",
|
|
33
|
+
".gitignore",
|
|
34
|
+
"README.md",
|
|
35
|
+
"agents.ts",
|
|
36
|
+
"biome.json",
|
|
37
|
+
"docker-compose.yml",
|
|
38
|
+
"package.json",
|
|
39
|
+
"persona/shared.md",
|
|
40
|
+
"plugins/hello.test.ts",
|
|
41
|
+
"plugins/hello.ts",
|
|
42
|
+
"roundtable.config.ts",
|
|
43
|
+
"tsconfig.json",
|
|
44
|
+
].sort(),
|
|
45
|
+
);
|
|
46
|
+
expect([...report.created].sort()).toEqual(everything(join(dir.path, "bot")));
|
|
47
|
+
const manifest = JSON.parse(
|
|
48
|
+
readFileSync(join(dir.path, "bot/package.json"), "utf8"),
|
|
49
|
+
) as { name: string; dependencies: Record<string, string> };
|
|
50
|
+
expect(manifest.name).toBe("bot");
|
|
51
|
+
expect(manifest.dependencies["pi-roundtable"]).toBe("1.2.3");
|
|
52
|
+
expect(report.nextSteps).toHaveLength(3);
|
|
53
|
+
expect(report.nextSteps[0]).toStartWith("cd bot && bun install");
|
|
54
|
+
expect(report.nextSteps[1]).toContain("roundtable doctor");
|
|
55
|
+
expect(report.nextSteps[2]).toContain("roundtable start");
|
|
56
|
+
const hello = readFileSync(join(dir.path, "bot/plugins/hello.ts"), "utf8");
|
|
57
|
+
expect(hello).toContain('name: "hello"');
|
|
58
|
+
expect(hello).toContain("hello_greet");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("init in the current directory needs no cd, and no file keeps a placeholder", () => {
|
|
62
|
+
const dir = temp();
|
|
63
|
+
const report = init({ cwd: dir.path, bun, version: "1.2.3" });
|
|
64
|
+
expect(report.ok).toBe(true);
|
|
65
|
+
expect(report.nextSteps[0]).toStartWith("bun install");
|
|
66
|
+
for (const path of everything(dir.path))
|
|
67
|
+
expect(readFileSync(join(dir.path, path), "utf8")).not.toMatch(
|
|
68
|
+
/__[A-Z]+__/,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("init refuses before writing anything when Bun is missing, naming the install page", () => {
|
|
73
|
+
const dir = temp();
|
|
74
|
+
const report = init({
|
|
75
|
+
cwd: dir.path,
|
|
76
|
+
bun: { version: undefined, required: ">=1.3.0" },
|
|
77
|
+
version: "1.2.3",
|
|
78
|
+
});
|
|
79
|
+
expect(report.ok).toBe(false);
|
|
80
|
+
expect(report.problems.join("\n")).toContain(BUN_INSTALL_PAGE);
|
|
81
|
+
expect(everything(dir.path)).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("init refuses when Bun is older than engines.bun, naming both versions and the page", () => {
|
|
85
|
+
const dir = temp();
|
|
86
|
+
const report = init({
|
|
87
|
+
cwd: dir.path,
|
|
88
|
+
bun: { version: "1.2.9", required: ">=1.3.0" },
|
|
89
|
+
version: "1.2.3",
|
|
90
|
+
});
|
|
91
|
+
expect(report.ok).toBe(false);
|
|
92
|
+
const text = report.problems.join("\n");
|
|
93
|
+
expect(text).toContain("1.2.9");
|
|
94
|
+
expect(text).toContain(">=1.3.0");
|
|
95
|
+
expect(text).toContain(BUN_INSTALL_PAGE);
|
|
96
|
+
expect(everything(dir.path)).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("init refuses, listing every file that exists, and writes nothing", () => {
|
|
100
|
+
const dir = temp();
|
|
101
|
+
dir.write("roundtable.config.ts", "// mine");
|
|
102
|
+
dir.write("plugins/hello.ts", "// mine");
|
|
103
|
+
const before = everything(dir.path);
|
|
104
|
+
const report = init({ cwd: dir.path, bun, version: "1.2.3" });
|
|
105
|
+
expect(report.ok).toBe(false);
|
|
106
|
+
const text = report.problems.join("\n");
|
|
107
|
+
expect(text).toContain("roundtable.config.ts");
|
|
108
|
+
expect(text).toContain("plugins/hello.ts");
|
|
109
|
+
expect(text).not.toContain("package.json");
|
|
110
|
+
expect(everything(dir.path)).toEqual(before);
|
|
111
|
+
expect(readFileSync(join(dir.path, "roundtable.config.ts"), "utf8")).toBe(
|
|
112
|
+
"// mine",
|
|
113
|
+
);
|
|
114
|
+
expect(existsSync(join(dir.path, "package.json"))).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("the generated configuration reads credentials from the environment and holds none", () => {
|
|
118
|
+
const dir = temp();
|
|
119
|
+
init({ cwd: dir.path, bun, version: "1.2.3" });
|
|
120
|
+
const config = readFileSync(join(dir.path, "roundtable.config.ts"), "utf8");
|
|
121
|
+
expect(config).toContain("process.env");
|
|
122
|
+
for (const key of ["DISCORD_TOKEN", "DATABASE_URL", "OWNER_ID", "MODEL"])
|
|
123
|
+
expect(config).toContain(`env("${key}")`);
|
|
124
|
+
expect(config).not.toMatch(/\d{15,}/);
|
|
125
|
+
expect(config).not.toMatch(/token:\s*"/);
|
|
126
|
+
expect(readFileSync(join(dir.path, ".gitignore"), "utf8")).toContain(".env");
|
|
127
|
+
});
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { type BunFacts, checkBun } from "./checks/bun.ts";
|
|
4
|
+
import { type Rendered, renderProject, writeRendered } from "./templates.ts";
|
|
5
|
+
|
|
6
|
+
export interface InitInputs {
|
|
7
|
+
cwd: string;
|
|
8
|
+
/** The directory to create the project in, relative to `cwd`; default `cwd` itself. */
|
|
9
|
+
dir?: string;
|
|
10
|
+
bun: BunFacts;
|
|
11
|
+
/** The exact version of pi-roundtable the project pins. */
|
|
12
|
+
version: string;
|
|
13
|
+
/** Where the templates are; default the package's own. */
|
|
14
|
+
templates?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InitReport {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
/** Why nothing was written, when `ok` is false. */
|
|
20
|
+
problems: string[];
|
|
21
|
+
/** Paths written, relative to the project directory. */
|
|
22
|
+
created: string[];
|
|
23
|
+
/** The project directory. */
|
|
24
|
+
root: string;
|
|
25
|
+
nextSteps: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A package name npm accepts, from a directory's name. */
|
|
29
|
+
function projectName(root: string): string {
|
|
30
|
+
const name = basename(root)
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "");
|
|
34
|
+
return name === "" ? "roundtable-bot" : name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nextSteps(cwd: string, root: string): string[] {
|
|
38
|
+
const where = relative(cwd, root);
|
|
39
|
+
return [
|
|
40
|
+
`${where === "" ? "" : `cd ${where} && `}bun install, then copy .env.example to .env and fill in the credentials it lists`,
|
|
41
|
+
"bunx roundtable doctor: it lists what is still missing and how to fix it",
|
|
42
|
+
"bunx roundtable start",
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a project. Every refusal is decided before the first write: Bun missing or too old,
|
|
48
|
+
* or any file it would create already there.
|
|
49
|
+
*/
|
|
50
|
+
export function init(inputs: InitInputs): InitReport {
|
|
51
|
+
const root = resolve(inputs.cwd, inputs.dir ?? ".");
|
|
52
|
+
const refused = (problems: string[]): InitReport => ({
|
|
53
|
+
ok: false,
|
|
54
|
+
problems,
|
|
55
|
+
created: [],
|
|
56
|
+
root,
|
|
57
|
+
nextSteps: [],
|
|
58
|
+
});
|
|
59
|
+
const bun = checkBun(inputs.bun);
|
|
60
|
+
if (bun.status === "fail") return refused([bun.problem, bun.fix]);
|
|
61
|
+
const files: Rendered[] = renderProject(
|
|
62
|
+
{ project: projectName(root), version: inputs.version },
|
|
63
|
+
inputs.templates,
|
|
64
|
+
);
|
|
65
|
+
const existing = files
|
|
66
|
+
.map((file) => file.path)
|
|
67
|
+
.filter((path) => existsSync(join(root, path)));
|
|
68
|
+
if (existing.length > 0)
|
|
69
|
+
return refused([
|
|
70
|
+
`${existing.length === 1 ? "A file" : "Files"} init would create already exist in ${root}:`,
|
|
71
|
+
...existing.map((path) => ` ${path}`),
|
|
72
|
+
"Remove them or run init in an empty directory; nothing was written.",
|
|
73
|
+
]);
|
|
74
|
+
writeRendered(root, files);
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
problems: [],
|
|
78
|
+
created: files.map((file) => file.path),
|
|
79
|
+
root,
|
|
80
|
+
nextSteps: nextSteps(inputs.cwd, root),
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { processEnvironment, runCli } from "./cli.ts";
|
|
3
|
+
|
|
4
|
+
// pi-lens-ignore: no-unknown-parameters
|
|
5
|
+
runCli(process.argv.slice(2), processEnvironment()).then(
|
|
6
|
+
(code) => {
|
|
7
|
+
process.exitCode = code;
|
|
8
|
+
},
|
|
9
|
+
(error: unknown) => {
|
|
10
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
},
|
|
13
|
+
);
|