prodboard 0.0.0 → 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 +16 -0
- package/LICENSE +21 -0
- package/README.md +294 -0
- package/bin/prodboard.ts +4 -0
- package/config.schema.json +100 -0
- package/package.json +47 -6
- package/src/commands/comments.ts +86 -0
- package/src/commands/daemon.ts +112 -0
- package/src/commands/init.ts +83 -0
- package/src/commands/issues.ts +268 -0
- package/src/commands/schedules.ts +276 -0
- package/src/config.ts +155 -0
- package/src/confirm.ts +14 -0
- package/src/cron.ts +121 -0
- package/src/db.ts +157 -0
- package/src/format.ts +99 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +214 -0
- package/src/invocation.ts +102 -0
- package/src/logger.ts +84 -0
- package/src/mcp.ts +543 -0
- package/src/queries/comments.ts +31 -0
- package/src/queries/issues.ts +155 -0
- package/src/queries/runs.ts +159 -0
- package/src/queries/schedules.ts +115 -0
- package/src/scheduler.ts +411 -0
- package/src/templates.ts +43 -0
- package/src/types.ts +82 -0
- package/templates/CLAUDE.md +12 -0
- package/templates/config.jsonc +38 -0
- package/templates/mcp.json +8 -0
- package/templates/system-prompt-nogit.md +33 -0
- package/templates/system-prompt.md +31 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { PRODBOARD_DIR } from "../config.ts";
|
|
4
|
+
import { getDb, runMigrations } from "../db.ts";
|
|
5
|
+
|
|
6
|
+
function resolveTemplatePath(name: string): string {
|
|
7
|
+
return path.resolve(import.meta.dir, "../../templates", name);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveSchemaPath(): string {
|
|
11
|
+
return path.resolve(import.meta.dir, "../../config.schema.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function init(args: string[], dirOverride?: string): Promise<void> {
|
|
15
|
+
const prodboardDir = dirOverride ?? PRODBOARD_DIR;
|
|
16
|
+
const logsDir = path.join(prodboardDir, "logs");
|
|
17
|
+
const claudeMdFlag = args.includes("--claude-md");
|
|
18
|
+
|
|
19
|
+
// Create directories
|
|
20
|
+
if (!fs.existsSync(prodboardDir)) {
|
|
21
|
+
fs.mkdirSync(prodboardDir, { recursive: true });
|
|
22
|
+
console.log(`Created ${prodboardDir}/`);
|
|
23
|
+
}
|
|
24
|
+
if (!fs.existsSync(logsDir)) {
|
|
25
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
26
|
+
console.log(`Created ${logsDir}/`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize database
|
|
30
|
+
const dbPath = path.join(prodboardDir, "db.sqlite");
|
|
31
|
+
const db = getDb(dbPath);
|
|
32
|
+
runMigrations(db);
|
|
33
|
+
db.close();
|
|
34
|
+
try { fs.chmodSync(dbPath, 0o600); } catch {}
|
|
35
|
+
try { fs.chmodSync(logsDir, 0o700); } catch {}
|
|
36
|
+
console.log("Database initialized.");
|
|
37
|
+
|
|
38
|
+
// Config file — only write if not exists (user may have edited)
|
|
39
|
+
const configDest = path.join(prodboardDir, "config.jsonc");
|
|
40
|
+
if (!fs.existsSync(configDest)) {
|
|
41
|
+
const configSrc = resolveTemplatePath("config.jsonc");
|
|
42
|
+
fs.copyFileSync(configSrc, configDest);
|
|
43
|
+
console.log("Created config.jsonc");
|
|
44
|
+
} else {
|
|
45
|
+
console.log("config.jsonc already exists, skipping.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Schema — always overwrite (package-managed)
|
|
49
|
+
const schemaDest = path.join(prodboardDir, "config.schema.json");
|
|
50
|
+
const schemaSrc = resolveSchemaPath();
|
|
51
|
+
fs.copyFileSync(schemaSrc, schemaDest);
|
|
52
|
+
console.log("Updated config.schema.json");
|
|
53
|
+
|
|
54
|
+
// MCP config — always overwrite
|
|
55
|
+
const mcpDest = path.join(prodboardDir, "mcp.json");
|
|
56
|
+
const mcpSrc = resolveTemplatePath("mcp.json");
|
|
57
|
+
fs.copyFileSync(mcpSrc, mcpDest);
|
|
58
|
+
console.log("Updated mcp.json");
|
|
59
|
+
|
|
60
|
+
// System prompts — always overwrite
|
|
61
|
+
const spDest = path.join(prodboardDir, "system-prompt.md");
|
|
62
|
+
const spSrc = resolveTemplatePath("system-prompt.md");
|
|
63
|
+
fs.copyFileSync(spSrc, spDest);
|
|
64
|
+
|
|
65
|
+
const spNogitDest = path.join(prodboardDir, "system-prompt-nogit.md");
|
|
66
|
+
const spNogitSrc = resolveTemplatePath("system-prompt-nogit.md");
|
|
67
|
+
fs.copyFileSync(spNogitSrc, spNogitDest);
|
|
68
|
+
console.log("Updated system prompts.");
|
|
69
|
+
|
|
70
|
+
// CLAUDE.md — only if --claude-md flag and not exists
|
|
71
|
+
if (claudeMdFlag) {
|
|
72
|
+
const claudeMdDest = path.join(process.cwd(), "CLAUDE.md");
|
|
73
|
+
if (!fs.existsSync(claudeMdDest)) {
|
|
74
|
+
const claudeMdSrc = resolveTemplatePath("CLAUDE.md");
|
|
75
|
+
fs.copyFileSync(claudeMdSrc, claudeMdDest);
|
|
76
|
+
console.log("Created CLAUDE.md in current directory.");
|
|
77
|
+
} else {
|
|
78
|
+
console.log("CLAUDE.md already exists, skipping.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("\nprodboard initialized successfully!");
|
|
83
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { ensureDb } from "../db.ts";
|
|
3
|
+
import { loadConfig } from "../config.ts";
|
|
4
|
+
import {
|
|
5
|
+
createIssue, getIssueByPrefix, listIssues, updateIssue,
|
|
6
|
+
deleteIssue, validateStatus
|
|
7
|
+
} from "../queries/issues.ts";
|
|
8
|
+
import { listComments } from "../queries/comments.ts";
|
|
9
|
+
import { renderTable, formatDate, jsonOutput, bold, dim, cyan } from "../format.ts";
|
|
10
|
+
import { confirm } from "../confirm.ts";
|
|
11
|
+
|
|
12
|
+
function parseArgs(args: string[]): { flags: Record<string, string | boolean>; positional: string[] } {
|
|
13
|
+
const flags: Record<string, string | boolean> = {};
|
|
14
|
+
const positional: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
if (arg === "--") {
|
|
19
|
+
positional.push(...args.slice(i + 1));
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (arg.startsWith("--")) {
|
|
23
|
+
const key = arg.slice(2);
|
|
24
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
25
|
+
flags[key] = args[++i];
|
|
26
|
+
} else {
|
|
27
|
+
flags[key] = true;
|
|
28
|
+
}
|
|
29
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
30
|
+
const key = arg.slice(1);
|
|
31
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
32
|
+
flags[key] = args[++i];
|
|
33
|
+
} else {
|
|
34
|
+
flags[key] = true;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
positional.push(arg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { flags, positional };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getFlag(flags: Record<string, string | boolean>, ...keys: string[]): string | undefined {
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
const val = flags[k];
|
|
46
|
+
if (val !== undefined && val !== true) return val as string;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasFlag(flags: Record<string, string | boolean>, ...keys: string[]): boolean {
|
|
52
|
+
return keys.some((k) => flags[k] !== undefined);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Collect repeatable flags like --status todo --status done
|
|
56
|
+
function parseArgsMulti(args: string[]): { flags: Record<string, (string | boolean)[]>; positional: string[] } {
|
|
57
|
+
const flags: Record<string, (string | boolean)[]> = {};
|
|
58
|
+
const positional: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg === "--") {
|
|
63
|
+
positional.push(...args.slice(i + 1));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (arg.startsWith("--")) {
|
|
67
|
+
const key = arg.slice(2);
|
|
68
|
+
if (!flags[key]) flags[key] = [];
|
|
69
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
70
|
+
flags[key].push(args[++i]);
|
|
71
|
+
} else {
|
|
72
|
+
flags[key].push(true);
|
|
73
|
+
}
|
|
74
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
75
|
+
const key = arg.slice(1);
|
|
76
|
+
if (!flags[key]) flags[key] = [];
|
|
77
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
78
|
+
flags[key].push(args[++i]);
|
|
79
|
+
} else {
|
|
80
|
+
flags[key].push(true);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
positional.push(arg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { flags, positional };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function add(args: string[], dbOverride?: Database): Promise<void> {
|
|
90
|
+
const { flags, positional } = parseArgs(args);
|
|
91
|
+
const title = positional.join(" ");
|
|
92
|
+
if (!title) {
|
|
93
|
+
console.error("Usage: prodboard add <title> [-d description] [-s status]");
|
|
94
|
+
throw new Error("Invalid arguments");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const db = dbOverride ?? ensureDb();
|
|
98
|
+
const config = loadConfig();
|
|
99
|
+
|
|
100
|
+
const status = getFlag(flags, "status", "s");
|
|
101
|
+
if (status) validateStatus(status, config);
|
|
102
|
+
|
|
103
|
+
const issue = createIssue(db, {
|
|
104
|
+
title,
|
|
105
|
+
description: getFlag(flags, "description", "d"),
|
|
106
|
+
status,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
console.log(`Created issue ${issue.id}: ${issue.title} [${issue.status}]`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function ls(args: string[], dbOverride?: Database): Promise<void> {
|
|
113
|
+
const { flags } = parseArgsMulti(args);
|
|
114
|
+
const db = dbOverride ?? ensureDb();
|
|
115
|
+
|
|
116
|
+
const statusFilters = [
|
|
117
|
+
...(flags.status?.filter((v): v is string => typeof v === "string") ?? []),
|
|
118
|
+
...(flags.s?.filter((v): v is string => typeof v === "string") ?? []),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const search = (flags.search?.[0] ?? flags.q?.[0]) as string | undefined;
|
|
122
|
+
const all = flags.all !== undefined || flags.a !== undefined;
|
|
123
|
+
const sortFlag = flags.sort?.[0] as string | undefined;
|
|
124
|
+
const asc = flags.asc !== undefined;
|
|
125
|
+
const limitStr = (flags.limit?.[0] ?? flags.n?.[0]) as string | undefined;
|
|
126
|
+
const isJson = flags.json !== undefined;
|
|
127
|
+
|
|
128
|
+
const { issues, total } = listIssues(db, {
|
|
129
|
+
status: statusFilters.length > 0 ? statusFilters : undefined,
|
|
130
|
+
search: typeof search === "string" ? search : undefined,
|
|
131
|
+
includeArchived: all,
|
|
132
|
+
sort: typeof sortFlag === "string" ? sortFlag : undefined,
|
|
133
|
+
order: asc ? "ASC" : undefined,
|
|
134
|
+
limit: limitStr ? parseInt(limitStr, 10) : undefined,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (isJson) {
|
|
138
|
+
console.log(jsonOutput(issues));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (issues.length === 0) {
|
|
143
|
+
console.log("No issues found.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const table = renderTable(
|
|
148
|
+
["ID", "Title", "Status", "Updated"],
|
|
149
|
+
issues.map((i) => [i.id, i.title, i.status, formatDate(i.updated_at)]),
|
|
150
|
+
{ maxWidths: [10, 40, 15, 18] }
|
|
151
|
+
);
|
|
152
|
+
console.log(table);
|
|
153
|
+
console.log(`${total} issue${total === 1 ? "" : "s"}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function show(args: string[], dbOverride?: Database): Promise<void> {
|
|
157
|
+
const { flags, positional } = parseArgs(args);
|
|
158
|
+
const idOrPrefix = positional[0];
|
|
159
|
+
if (!idOrPrefix) {
|
|
160
|
+
console.error("Usage: prodboard show <id>");
|
|
161
|
+
throw new Error("Invalid arguments");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const db = dbOverride ?? ensureDb();
|
|
165
|
+
const isJson = hasFlag(flags, "json");
|
|
166
|
+
|
|
167
|
+
const issue = getIssueByPrefix(db, idOrPrefix);
|
|
168
|
+
const comments = listComments(db, issue.id);
|
|
169
|
+
|
|
170
|
+
if (isJson) {
|
|
171
|
+
console.log(jsonOutput({ ...issue, comments }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`Issue ${issue.id}`);
|
|
176
|
+
console.log(`Title: ${issue.title}`);
|
|
177
|
+
console.log(`Status: ${issue.status}`);
|
|
178
|
+
console.log(`Created: ${formatDate(issue.created_at)}`);
|
|
179
|
+
console.log(`Updated: ${formatDate(issue.updated_at)}`);
|
|
180
|
+
|
|
181
|
+
if (issue.description) {
|
|
182
|
+
console.log(`\nDescription:\n${issue.description}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (comments.length > 0) {
|
|
186
|
+
console.log(`\nComments (${comments.length}):`);
|
|
187
|
+
for (const c of comments) {
|
|
188
|
+
console.log(` [${c.author}] ${formatDate(c.created_at)}`);
|
|
189
|
+
console.log(` ${c.body}`);
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function edit(args: string[], dbOverride?: Database): Promise<void> {
|
|
196
|
+
const { flags, positional } = parseArgs(args);
|
|
197
|
+
const idOrPrefix = positional[0];
|
|
198
|
+
if (!idOrPrefix) {
|
|
199
|
+
console.error("Usage: prodboard edit <id> [--title/-t title] [--description/-d desc] [--status/-s status]");
|
|
200
|
+
throw new Error("Invalid arguments");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const db = dbOverride ?? ensureDb();
|
|
204
|
+
const config = loadConfig();
|
|
205
|
+
const issue = getIssueByPrefix(db, idOrPrefix);
|
|
206
|
+
|
|
207
|
+
const fields: { title?: string; description?: string; status?: string } = {};
|
|
208
|
+
const title = getFlag(flags, "title", "t");
|
|
209
|
+
const description = getFlag(flags, "description", "d");
|
|
210
|
+
const status = getFlag(flags, "status", "s");
|
|
211
|
+
|
|
212
|
+
if (title) fields.title = title;
|
|
213
|
+
if (description) fields.description = description;
|
|
214
|
+
if (status) {
|
|
215
|
+
validateStatus(status, config);
|
|
216
|
+
fields.status = status;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.keys(fields).length === 0) {
|
|
220
|
+
console.error("No fields to update. Use --title, --description, or --status.");
|
|
221
|
+
throw new Error("Invalid arguments");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const updated = updateIssue(db, issue.id, fields);
|
|
225
|
+
console.log(`Updated issue ${updated.id}: ${updated.title} [${updated.status}]`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function mv(args: string[], dbOverride?: Database): Promise<void> {
|
|
229
|
+
const { positional } = parseArgs(args);
|
|
230
|
+
const idOrPrefix = positional[0];
|
|
231
|
+
const newStatus = positional[1];
|
|
232
|
+
|
|
233
|
+
if (!idOrPrefix || !newStatus) {
|
|
234
|
+
console.error("Usage: prodboard mv <id> <status>");
|
|
235
|
+
throw new Error("Invalid arguments");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const db = dbOverride ?? ensureDb();
|
|
239
|
+
const config = loadConfig();
|
|
240
|
+
validateStatus(newStatus, config);
|
|
241
|
+
|
|
242
|
+
const issue = getIssueByPrefix(db, idOrPrefix);
|
|
243
|
+
const updated = updateIssue(db, issue.id, { status: newStatus });
|
|
244
|
+
console.log(`Moved issue ${updated.id} to ${updated.status}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function rm(args: string[], dbOverride?: Database): Promise<void> {
|
|
248
|
+
const { flags, positional } = parseArgs(args);
|
|
249
|
+
const idOrPrefix = positional[0];
|
|
250
|
+
if (!idOrPrefix) {
|
|
251
|
+
console.error("Usage: prodboard rm <id> [--force/-f]");
|
|
252
|
+
throw new Error("Invalid arguments");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const db = dbOverride ?? ensureDb();
|
|
256
|
+
const issue = getIssueByPrefix(db, idOrPrefix);
|
|
257
|
+
|
|
258
|
+
if (!hasFlag(flags, "force", "f")) {
|
|
259
|
+
const ok = await confirm(`Delete issue ${issue.id}: ${issue.title}?`);
|
|
260
|
+
if (!ok) {
|
|
261
|
+
console.log("Cancelled.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
deleteIssue(db, issue.id);
|
|
267
|
+
console.log(`Deleted issue ${issue.id}`);
|
|
268
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { ensureDb } from "../db.ts";
|
|
3
|
+
import { loadConfig } from "../config.ts";
|
|
4
|
+
import { validateCron, getNextFire } from "../cron.ts";
|
|
5
|
+
import {
|
|
6
|
+
createSchedule, getScheduleByPrefix, listSchedules,
|
|
7
|
+
updateSchedule, deleteSchedule, enableSchedule, disableSchedule,
|
|
8
|
+
} from "../queries/schedules.ts";
|
|
9
|
+
import { listRuns, getScheduleStats } from "../queries/runs.ts";
|
|
10
|
+
import { renderTable, formatDate, jsonOutput } from "../format.ts";
|
|
11
|
+
import { confirm } from "../confirm.ts";
|
|
12
|
+
import { ExecutionManager } from "../scheduler.ts";
|
|
13
|
+
import { createRun } from "../queries/runs.ts";
|
|
14
|
+
|
|
15
|
+
function safeParseInt(val: string | boolean | undefined): number | undefined {
|
|
16
|
+
if (typeof val !== "string") return undefined;
|
|
17
|
+
const n = parseInt(val, 10);
|
|
18
|
+
return isNaN(n) ? undefined : n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(args: string[]): { flags: Record<string, string | boolean>; positional: string[] } {
|
|
22
|
+
const flags: Record<string, string | boolean> = {};
|
|
23
|
+
const positional: string[] = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg.startsWith("--")) {
|
|
28
|
+
const key = arg.slice(2);
|
|
29
|
+
if (key === "no-worktree" || key === "no-context" || key === "persist-session" || key === "force" || key === "all" || key === "json" || key === "asc") {
|
|
30
|
+
flags[key] = true;
|
|
31
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
32
|
+
flags[key] = args[++i];
|
|
33
|
+
} else {
|
|
34
|
+
flags[key] = true;
|
|
35
|
+
}
|
|
36
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
37
|
+
const key = arg.slice(1);
|
|
38
|
+
if (key === "f" || key === "a") {
|
|
39
|
+
flags[key] = true;
|
|
40
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
41
|
+
flags[key] = args[++i];
|
|
42
|
+
} else {
|
|
43
|
+
flags[key] = true;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
positional.push(arg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { flags, positional };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function scheduleAdd(args: string[], dbOverride?: Database): Promise<void> {
|
|
53
|
+
const { flags } = parseArgs(args);
|
|
54
|
+
const db = dbOverride ?? ensureDb();
|
|
55
|
+
|
|
56
|
+
const name = (flags.name ?? flags.n) as string;
|
|
57
|
+
const cron = (flags.cron ?? flags.c) as string;
|
|
58
|
+
const prompt = (flags.prompt ?? flags.p) as string;
|
|
59
|
+
|
|
60
|
+
if (!name || !cron || !prompt) {
|
|
61
|
+
console.error("Usage: prodboard schedule add --name <name> --cron <expr> --prompt <prompt>");
|
|
62
|
+
throw new Error("Invalid arguments");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validation = validateCron(cron);
|
|
66
|
+
if (!validation.valid) {
|
|
67
|
+
console.error(`Invalid cron expression: ${validation.error}`);
|
|
68
|
+
throw new Error("Invalid arguments");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const schedule = createSchedule(db, {
|
|
72
|
+
name,
|
|
73
|
+
cron,
|
|
74
|
+
prompt,
|
|
75
|
+
workdir: (flags.workdir ?? flags.w) as string | undefined,
|
|
76
|
+
max_turns: flags["max-turns"] ? parseInt(flags["max-turns"] as string, 10) : undefined,
|
|
77
|
+
use_worktree: !flags["no-worktree"],
|
|
78
|
+
inject_context: !flags["no-context"],
|
|
79
|
+
persist_session: !!flags["persist-session"],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(`Created schedule ${schedule.id}: ${schedule.name} [${schedule.cron}]`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function scheduleLs(args: string[], dbOverride?: Database): Promise<void> {
|
|
86
|
+
const { flags } = parseArgs(args);
|
|
87
|
+
const db = dbOverride ?? ensureDb();
|
|
88
|
+
const isJson = !!flags.json;
|
|
89
|
+
const all = !!(flags.all || flags.a);
|
|
90
|
+
|
|
91
|
+
const schedules = listSchedules(db, { includeDisabled: all });
|
|
92
|
+
|
|
93
|
+
if (isJson) {
|
|
94
|
+
console.log(jsonOutput(schedules));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (schedules.length === 0) {
|
|
99
|
+
console.log("No schedules.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const table = renderTable(
|
|
104
|
+
["ID", "Name", "Cron", "Enabled", "Next Fire"],
|
|
105
|
+
schedules.map((s) => {
|
|
106
|
+
let nextFire = "";
|
|
107
|
+
try {
|
|
108
|
+
const next = getNextFire(s.cron, new Date());
|
|
109
|
+
nextFire = formatDate(next.toISOString());
|
|
110
|
+
} catch {}
|
|
111
|
+
return [s.id, s.name, s.cron, s.enabled ? "yes" : "no", nextFire];
|
|
112
|
+
}),
|
|
113
|
+
{ maxWidths: [10, 30, 20, 8, 18] }
|
|
114
|
+
);
|
|
115
|
+
console.log(table);
|
|
116
|
+
console.log(`${schedules.length} schedule${schedules.length === 1 ? "" : "s"}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function scheduleEdit(args: string[], dbOverride?: Database): Promise<void> {
|
|
120
|
+
const { flags, positional } = parseArgs(args);
|
|
121
|
+
const idOrPrefix = positional[0];
|
|
122
|
+
if (!idOrPrefix) {
|
|
123
|
+
console.error("Usage: prodboard schedule edit <id> [--name name] [--cron expr] [--prompt prompt]");
|
|
124
|
+
throw new Error("Invalid arguments");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const db = dbOverride ?? ensureDb();
|
|
128
|
+
const schedule = getScheduleByPrefix(db, idOrPrefix);
|
|
129
|
+
|
|
130
|
+
const fields: any = {};
|
|
131
|
+
if (flags.name || flags.n) fields.name = flags.name ?? flags.n;
|
|
132
|
+
if (flags.cron || flags.c) {
|
|
133
|
+
const newCron = (flags.cron ?? flags.c) as string;
|
|
134
|
+
const validation = validateCron(newCron);
|
|
135
|
+
if (!validation.valid) {
|
|
136
|
+
console.error(`Invalid cron expression: ${validation.error}`);
|
|
137
|
+
throw new Error("Invalid arguments");
|
|
138
|
+
}
|
|
139
|
+
fields.cron = newCron;
|
|
140
|
+
}
|
|
141
|
+
if (flags.prompt || flags.p) fields.prompt = flags.prompt ?? flags.p;
|
|
142
|
+
if (flags["max-turns"]) fields.max_turns = parseInt(flags["max-turns"] as string, 10);
|
|
143
|
+
|
|
144
|
+
const updated = updateSchedule(db, schedule.id, fields);
|
|
145
|
+
console.log(`Updated schedule ${updated.id}: ${updated.name}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function scheduleEnable(args: string[], dbOverride?: Database): Promise<void> {
|
|
149
|
+
const { positional } = parseArgs(args);
|
|
150
|
+
const idOrPrefix = positional[0];
|
|
151
|
+
if (!idOrPrefix) {
|
|
152
|
+
console.error("Usage: prodboard schedule enable <id>");
|
|
153
|
+
throw new Error("Invalid arguments");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const db = dbOverride ?? ensureDb();
|
|
157
|
+
const schedule = getScheduleByPrefix(db, idOrPrefix);
|
|
158
|
+
enableSchedule(db, schedule.id);
|
|
159
|
+
console.log(`Enabled schedule ${schedule.id}: ${schedule.name}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function scheduleDisable(args: string[], dbOverride?: Database): Promise<void> {
|
|
163
|
+
const { positional } = parseArgs(args);
|
|
164
|
+
const idOrPrefix = positional[0];
|
|
165
|
+
if (!idOrPrefix) {
|
|
166
|
+
console.error("Usage: prodboard schedule disable <id>");
|
|
167
|
+
throw new Error("Invalid arguments");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const db = dbOverride ?? ensureDb();
|
|
171
|
+
const schedule = getScheduleByPrefix(db, idOrPrefix);
|
|
172
|
+
disableSchedule(db, schedule.id);
|
|
173
|
+
console.log(`Disabled schedule ${schedule.id}: ${schedule.name}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function scheduleRm(args: string[], dbOverride?: Database): Promise<void> {
|
|
177
|
+
const { flags, positional } = parseArgs(args);
|
|
178
|
+
const idOrPrefix = positional[0];
|
|
179
|
+
if (!idOrPrefix) {
|
|
180
|
+
console.error("Usage: prodboard schedule rm <id> [--force/-f]");
|
|
181
|
+
throw new Error("Invalid arguments");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const db = dbOverride ?? ensureDb();
|
|
185
|
+
const schedule = getScheduleByPrefix(db, idOrPrefix);
|
|
186
|
+
|
|
187
|
+
if (!flags.force && !flags.f) {
|
|
188
|
+
const ok = await confirm(`Delete schedule ${schedule.id}: ${schedule.name}?`);
|
|
189
|
+
if (!ok) {
|
|
190
|
+
console.log("Cancelled.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
deleteSchedule(db, schedule.id);
|
|
196
|
+
console.log(`Deleted schedule ${schedule.id}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function scheduleLogs(args: string[], dbOverride?: Database): Promise<void> {
|
|
200
|
+
const { flags } = parseArgs(args);
|
|
201
|
+
const db = dbOverride ?? ensureDb();
|
|
202
|
+
const isJson = !!flags.json;
|
|
203
|
+
|
|
204
|
+
const runs = listRuns(db, {
|
|
205
|
+
schedule_id: (flags.schedule ?? flags.s) as string | undefined,
|
|
206
|
+
status: flags.status as string | undefined,
|
|
207
|
+
limit: safeParseInt(flags.limit ?? flags.n),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (isJson) {
|
|
211
|
+
console.log(jsonOutput(runs));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (runs.length === 0) {
|
|
216
|
+
console.log("No runs found.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const table = renderTable(
|
|
221
|
+
["ID", "Schedule", "Status", "Started", "Exit", "Tokens"],
|
|
222
|
+
runs.map((r) => [
|
|
223
|
+
r.id,
|
|
224
|
+
r.schedule_name ?? r.schedule_id,
|
|
225
|
+
r.status,
|
|
226
|
+
formatDate(r.started_at),
|
|
227
|
+
r.exit_code !== null ? String(r.exit_code) : "-",
|
|
228
|
+
r.tokens_in ? `${r.tokens_in}/${r.tokens_out}` : "-",
|
|
229
|
+
]),
|
|
230
|
+
{ maxWidths: [10, 20, 10, 18, 5, 12] }
|
|
231
|
+
);
|
|
232
|
+
console.log(table);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function scheduleRun(args: string[], dbOverride?: Database): Promise<void> {
|
|
236
|
+
const { positional } = parseArgs(args);
|
|
237
|
+
const idOrPrefix = positional[0];
|
|
238
|
+
if (!idOrPrefix) {
|
|
239
|
+
console.error("Usage: prodboard schedule run <id>");
|
|
240
|
+
throw new Error("Invalid arguments");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const db = dbOverride ?? ensureDb();
|
|
244
|
+
const config = loadConfig();
|
|
245
|
+
const schedule = getScheduleByPrefix(db, idOrPrefix);
|
|
246
|
+
|
|
247
|
+
console.log(`Running schedule ${schedule.id}: ${schedule.name}...`);
|
|
248
|
+
|
|
249
|
+
const run = createRun(db, {
|
|
250
|
+
schedule_id: schedule.id,
|
|
251
|
+
prompt_used: schedule.prompt,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const em = new ExecutionManager(db, config);
|
|
255
|
+
await em.executeRun(schedule, run);
|
|
256
|
+
|
|
257
|
+
const updatedRun = db.query("SELECT * FROM runs WHERE id = ?").get(run.id) as any;
|
|
258
|
+
console.log(`Run completed: ${updatedRun.status} (exit code: ${updatedRun.exit_code ?? "N/A"})`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function scheduleStats(args: string[], dbOverride?: Database): Promise<void> {
|
|
262
|
+
const { flags } = parseArgs(args);
|
|
263
|
+
const db = dbOverride ?? ensureDb();
|
|
264
|
+
|
|
265
|
+
const scheduleId = (flags.schedule ?? flags.s) as string | undefined;
|
|
266
|
+
const days = safeParseInt(flags.days ?? flags.d);
|
|
267
|
+
|
|
268
|
+
const stats = getScheduleStats(db, scheduleId, days);
|
|
269
|
+
|
|
270
|
+
console.log(`Total runs: ${stats.total}`);
|
|
271
|
+
console.log(`Success: ${stats.success} (${(stats.success_rate * 100).toFixed(1)}%)`);
|
|
272
|
+
console.log(`Failed: ${stats.failed}`);
|
|
273
|
+
console.log(`Avg tokens in: ${Math.round(stats.avg_tokens_in)}`);
|
|
274
|
+
console.log(`Avg tokens out: ${Math.round(stats.avg_tokens_out)}`);
|
|
275
|
+
console.log(`Total cost: $${stats.total_cost.toFixed(4)}`);
|
|
276
|
+
}
|