@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/CLAUDE.md +4 -3
- package/README.md +38 -6
- package/bin/ove.ts +21 -2
- package/config.example.json +3 -0
- package/docs/examples.md +65 -3
- package/docs/favicon.ico +0 -0
- package/docs/index.html +25 -8
- package/docs/plans/2026-02-21-codex-runner-design.md +51 -0
- package/docs/plans/2026-02-21-codex-runner-plan.md +475 -0
- package/package.json +1 -1
- package/src/config.test.ts +52 -2
- package/src/config.ts +39 -1
- package/src/index.ts +76 -12
- package/src/router.test.ts +25 -0
- package/src/router.ts +11 -0
- package/src/runner.ts +1 -0
- package/src/runners/codex.test.ts +85 -0
- package/src/runners/codex.ts +137 -0
- package/src/setup.test.ts +87 -20
- package/src/setup.ts +180 -54
- package/docs/CNAME +0 -1
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
|
|
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
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
136
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 ||
|
|
208
|
-
const envLines: string[] = [
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
""
|
|
213
|
-
|
|
214
|
-
`
|
|
215
|
-
""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|