@lovenyberg/ove 0.1.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/src/setup.ts CHANGED
@@ -18,27 +18,40 @@ export function validateConfig(opts?: { configPath?: string; envPath?: string })
18
18
 
19
19
  // Load env values from file if it exists
20
20
  const env = loadEnvFile(envPath);
21
- const cliMode = process.env.CLI_MODE === "true" || env.CLI_MODE === "true";
21
+ const get = (key: string) => process.env[key] || env[key] || "";
22
+ const cliMode = get("CLI_MODE") === "true";
23
+
24
+ // Detect which transports are configured
25
+ const hasSlack = !!(get("SLACK_BOT_TOKEN") && get("SLACK_BOT_TOKEN") !== "xoxb-...");
26
+ const hasTelegram = !!get("TELEGRAM_BOT_TOKEN");
27
+ const hasDiscord = !!get("DISCORD_BOT_TOKEN");
28
+ const hasWhatsApp = get("WHATSAPP_ENABLED") === "true";
29
+ const hasHttp = !!get("HTTP_API_PORT") || !!get("HTTP_API_KEY");
30
+ const hasGitHub = !!get("GITHUB_POLL_REPOS");
31
+ const hasAnyTransport = cliMode || hasSlack || hasTelegram || hasDiscord || hasWhatsApp || hasHttp || hasGitHub;
22
32
 
23
33
  // Only warn about missing .env if env vars aren't already set
24
- // (in Docker, env_file loads vars into process.env without the file being at ./.env)
25
- const hasEnvVars = cliMode || (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN);
26
- if (!existsSync(envPath) && !hasEnvVars) {
34
+ if (!existsSync(envPath) && !hasAnyTransport) {
27
35
  issues.push(".env not found");
28
36
  }
29
37
 
30
- if (!cliMode) {
31
- const botToken = process.env.SLACK_BOT_TOKEN || env.SLACK_BOT_TOKEN || "";
32
- if (!botToken || botToken === "xoxb-..." || botToken.startsWith("xoxb-...")) {
38
+ // Validate Slack tokens if Slack is partially configured
39
+ const slackBot = get("SLACK_BOT_TOKEN");
40
+ const slackApp = get("SLACK_APP_TOKEN");
41
+ if (slackBot || slackApp) {
42
+ if (!slackBot || slackBot === "xoxb-...") {
33
43
  issues.push("SLACK_BOT_TOKEN is a placeholder");
34
44
  }
35
-
36
- const appToken = process.env.SLACK_APP_TOKEN || env.SLACK_APP_TOKEN || "";
37
- if (!appToken || appToken === "xapp-..." || appToken.startsWith("xapp-...")) {
45
+ if (!slackApp || slackApp === "xapp-...") {
38
46
  issues.push("SLACK_APP_TOKEN is a placeholder");
39
47
  }
40
48
  }
41
49
 
50
+ // Require at least one transport
51
+ if (!hasAnyTransport) {
52
+ issues.push("No transport configured");
53
+ }
54
+
42
55
  // Check config.json content if it exists
43
56
  if (existsSync(configPath)) {
44
57
  try {
@@ -95,6 +108,25 @@ export async function choose(rl: ReturnType<typeof createInterface>, question: s
95
108
  return choice - 1;
96
109
  }
97
110
 
111
+ export async function chooseMulti(rl: ReturnType<typeof createInterface>, question: string, options: string[]): Promise<number[]> {
112
+ process.stdout.write(`\n ${question} (comma-separated, e.g. 1,3,5)\n`);
113
+ for (let i = 0; i < options.length; i++) {
114
+ process.stdout.write(` ${i + 1}. ${options[i]}\n`);
115
+ }
116
+ const answer = await rl.question(" > ");
117
+ const indices: number[] = [];
118
+ for (const part of answer.split(",")) {
119
+ const n = parseInt(part.trim(), 10);
120
+ if (!isNaN(n) && n >= 1 && n <= options.length) {
121
+ indices.push(n - 1);
122
+ }
123
+ }
124
+ return indices.length > 0 ? indices : [0];
125
+ }
126
+
127
+ const TRANSPORTS = ["Slack", "Telegram", "Discord", "WhatsApp", "HTTP API", "GitHub", "CLI"] as const;
128
+ type Transport = (typeof TRANSPORTS)[number];
129
+
98
130
  export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
99
131
  const rl = createInterface({ input: process.stdin, output: process.stdout });
100
132
 
@@ -119,41 +151,74 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
119
151
  process.stdout.write("\n Nåväl. Let's get this sorted.\n");
120
152
  }
121
153
 
122
- // Determine adapter mode
123
- let useSlack = false;
124
- let useCli = false;
125
-
126
- const needsSlackTokens = !fixing || fixing.some(i =>
127
- i.includes("SLACK_BOT_TOKEN") || i.includes("SLACK_APP_TOKEN")
154
+ const needsTokens = !fixing || fixing.some(i =>
155
+ i.includes("SLACK_BOT_TOKEN") || i.includes("SLACK_APP_TOKEN") || i.includes("No transport")
128
156
  );
129
157
  const needsRepos = !fixing || fixing.some(i => i.includes("No repos"));
130
158
  const needsUsers = !fixing || fixing.some(i => i.includes("No users"));
131
159
  const needsConfigFile = !fixing || fixing.some(i => i.includes("config.json not found"));
132
160
  const needsEnvFile = !fixing || fixing.some(i => i.includes(".env not found"));
133
161
 
162
+ // Select transports
163
+ let selected: Transport[] = [];
164
+
134
165
  if (!fixing) {
135
- const mode = await choose(rl, "How will you talk to me?", ["Slack", "CLI only", "Both"]);
136
- useSlack = mode === 0 || mode === 2;
137
- useCli = mode === 1 || mode === 2;
166
+ const indices = await chooseMulti(rl, "Which transports?", [...TRANSPORTS]);
167
+ selected = indices.map(i => TRANSPORTS[i]);
138
168
  } else {
139
- // When fixing, infer mode from what's needed
140
- useSlack = needsSlackTokens;
141
- useCli = !useSlack;
169
+ // When fixing, infer from what's needed
170
+ if (needsTokens && fixing.some(i => i.includes("SLACK"))) {
171
+ selected.push("Slack");
172
+ }
173
+ if (selected.length === 0) selected.push("CLI");
142
174
  }
143
175
 
176
+ const has = (t: Transport) => selected.includes(t);
177
+
144
178
  // Collect env values
145
179
  const envValues: Record<string, string> = { ...existingEnv };
146
180
 
147
- if (useSlack && needsSlackTokens) {
181
+ if (has("Slack") && needsTokens) {
148
182
  process.stdout.write("\n");
149
183
  const botToken = await ask(rl, "Slack Bot Token (xoxb-...)");
150
184
  if (botToken) envValues.SLACK_BOT_TOKEN = botToken;
151
-
152
185
  const appToken = await ask(rl, "Slack App Token (xapp-...)");
153
186
  if (appToken) envValues.SLACK_APP_TOKEN = appToken;
154
187
  }
155
188
 
156
- if (!fixing && useCli && !useSlack) {
189
+ if (has("Telegram") && needsTokens) {
190
+ process.stdout.write("\n");
191
+ const token = await ask(rl, "Telegram Bot Token (from BotFather)");
192
+ if (token) envValues.TELEGRAM_BOT_TOKEN = token;
193
+ }
194
+
195
+ if (has("Discord") && needsTokens) {
196
+ process.stdout.write("\n");
197
+ const token = await ask(rl, "Discord Bot Token");
198
+ if (token) envValues.DISCORD_BOT_TOKEN = token;
199
+ }
200
+
201
+ if (has("WhatsApp")) {
202
+ envValues.WHATSAPP_ENABLED = "true";
203
+ }
204
+
205
+ if (has("HTTP API") && needsTokens) {
206
+ process.stdout.write("\n");
207
+ const port = (await ask(rl, "HTTP API port [3000]")) || "3000";
208
+ envValues.HTTP_API_PORT = port;
209
+ const key = await ask(rl, "API key (leave empty to generate)");
210
+ envValues.HTTP_API_KEY = key || crypto.randomUUID();
211
+ }
212
+
213
+ if (has("GitHub") && needsTokens) {
214
+ process.stdout.write("\n");
215
+ const repos = await ask(rl, "GitHub repos to poll (comma-separated owner/repo)");
216
+ if (repos) envValues.GITHUB_POLL_REPOS = repos;
217
+ const botName = (await ask(rl, "GitHub bot name [ove]")) || "ove";
218
+ envValues.GITHUB_BOT_NAME = botName;
219
+ }
220
+
221
+ if (has("CLI")) {
157
222
  envValues.CLI_MODE = "true";
158
223
  }
159
224
 
@@ -184,42 +249,103 @@ export async function runSetup(opts?: { fixOnly?: string[] }): Promise<void> {
184
249
  const repoNames = Object.keys(repos);
185
250
 
186
251
  if (needsUsers || needsConfigFile) {
187
- if (useSlack) {
252
+ // Ask for user name once
253
+ let userName = "";
254
+ const chatTransports = selected.filter(t => t !== "HTTP API" && t !== "GitHub" && t !== "CLI");
255
+ if (chatTransports.length > 0 || has("GitHub")) {
188
256
  process.stdout.write("\n");
189
- const userId = await ask(rl, "Your Slack user ID (U...)");
190
- const name = await ask(rl, "Your name");
191
- if (userId && name) {
192
- users[`slack:${userId}`] = { name, repos: repoNames };
193
- }
257
+ userName = await ask(rl, "Your name");
194
258
  }
195
259
 
196
- if (useCli || !useSlack) {
197
- if (!fixing) {
198
- users["cli:local"] = { name: "local", repos: repoNames };
199
- } else {
200
- const name = await ask(rl, "Your name");
201
- users["cli:local"] = { name: name || "local", repos: repoNames };
202
- }
260
+ if (has("Slack")) {
261
+ const userId = await ask(rl, "Your Slack user ID (U...)");
262
+ if (userId) users[`slack:${userId}`] = { name: userName || "user", repos: repoNames };
263
+ }
264
+ if (has("Telegram")) {
265
+ const userId = await ask(rl, "Your Telegram user ID");
266
+ if (userId) users[`telegram:${userId}`] = { name: userName || "user", repos: repoNames };
267
+ }
268
+ if (has("Discord")) {
269
+ const userId = await ask(rl, "Your Discord user ID");
270
+ if (userId) users[`discord:${userId}`] = { name: userName || "user", repos: repoNames };
271
+ }
272
+ if (has("WhatsApp")) {
273
+ const phone = await ask(rl, "Your phone number (with country code)");
274
+ if (phone) users[`whatsapp:${phone}`] = { name: userName || "user", repos: repoNames };
275
+ }
276
+ if (has("HTTP API")) {
277
+ users["http:anon"] = { name: "http", repos: repoNames };
278
+ }
279
+ if (has("GitHub")) {
280
+ const ghUser = await ask(rl, "Your GitHub username");
281
+ if (ghUser) users[`github:${ghUser}`] = { name: userName || ghUser, repos: repoNames };
282
+ }
283
+ if (has("CLI")) {
284
+ users["cli:local"] = { name: userName || "local", repos: repoNames };
203
285
  }
204
286
  }
205
287
 
206
288
  // Write .env
207
- if (needsEnvFile || needsSlackTokens || !fixing) {
208
- const envLines: string[] = [
209
- "# Slack (Socket Mode)",
210
- `SLACK_BOT_TOKEN=${envValues.SLACK_BOT_TOKEN || "xoxb-..."}`,
211
- `SLACK_APP_TOKEN=${envValues.SLACK_APP_TOKEN || "xapp-..."}`,
212
- "",
213
- "# WhatsApp",
214
- `WHATSAPP_ENABLED=${envValues.WHATSAPP_ENABLED || "false"}`,
215
- "",
216
- "# CLI mode",
217
- ...(envValues.CLI_MODE ? [`CLI_MODE=${envValues.CLI_MODE}`] : ["# CLI_MODE=true"]),
218
- "",
219
- "# Repos directory",
220
- `REPOS_DIR=${envValues.REPOS_DIR || "./repos"}`,
221
- "",
222
- ];
289
+ if (needsEnvFile || needsTokens || !fixing) {
290
+ const envLines: string[] = [];
291
+
292
+ // Slack
293
+ if (has("Slack")) {
294
+ envLines.push("# Slack (Socket Mode)");
295
+ envLines.push(`SLACK_BOT_TOKEN=${envValues.SLACK_BOT_TOKEN || "xoxb-..."}`);
296
+ envLines.push(`SLACK_APP_TOKEN=${envValues.SLACK_APP_TOKEN || "xapp-..."}`);
297
+ envLines.push("");
298
+ }
299
+
300
+ // Telegram
301
+ if (has("Telegram")) {
302
+ envLines.push("# Telegram");
303
+ envLines.push(`TELEGRAM_BOT_TOKEN=${envValues.TELEGRAM_BOT_TOKEN || ""}`);
304
+ envLines.push("");
305
+ }
306
+
307
+ // Discord
308
+ if (has("Discord")) {
309
+ envLines.push("# Discord");
310
+ envLines.push(`DISCORD_BOT_TOKEN=${envValues.DISCORD_BOT_TOKEN || ""}`);
311
+ envLines.push("");
312
+ }
313
+
314
+ // WhatsApp
315
+ if (has("WhatsApp")) {
316
+ envLines.push("# WhatsApp");
317
+ envLines.push(`WHATSAPP_ENABLED=true`);
318
+ envLines.push("");
319
+ }
320
+
321
+ // HTTP API
322
+ if (has("HTTP API")) {
323
+ envLines.push("# HTTP API + Web UI");
324
+ envLines.push(`HTTP_API_PORT=${envValues.HTTP_API_PORT || "3000"}`);
325
+ envLines.push(`HTTP_API_KEY=${envValues.HTTP_API_KEY || ""}`);
326
+ envLines.push("");
327
+ }
328
+
329
+ // GitHub
330
+ if (has("GitHub")) {
331
+ envLines.push("# GitHub");
332
+ envLines.push(`GITHUB_POLL_REPOS=${envValues.GITHUB_POLL_REPOS || ""}`);
333
+ envLines.push(`GITHUB_BOT_NAME=${envValues.GITHUB_BOT_NAME || "ove"}`);
334
+ envLines.push("");
335
+ }
336
+
337
+ // CLI
338
+ if (has("CLI")) {
339
+ envLines.push("# CLI mode");
340
+ envLines.push("CLI_MODE=true");
341
+ envLines.push("");
342
+ }
343
+
344
+ // Always include repos dir
345
+ envLines.push("# Repos directory");
346
+ envLines.push(`REPOS_DIR=${envValues.REPOS_DIR || "./repos"}`);
347
+ envLines.push("");
348
+
223
349
  writeFileSync(envPath, envLines.join("\n") + "\n");
224
350
  process.stdout.write("\n Wrote .env\n");
225
351
  }
package/docs/CNAME DELETED
@@ -1 +0,0 @@
1
- ove.jacksoncage.se