prodboard 0.2.0 → 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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/commands/daemon.ts +57 -1
- package/src/commands/install.ts +21 -2
- package/src/config.ts +88 -3
- package/src/index.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# prodboard
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#9](https://github.com/G4brym/prodboard/pull/9) [`d418fc4`](https://github.com/G4brym/prodboard/commit/d418fc41b52e26789dd3b5be7f2dcdf9429ef287) Thanks [@G4brym](https://github.com/G4brym)! - Add `daemon restart` command with config validation and webui dependency checks. The `install` command now also validates config before proceeding. Invalid config values produce clear warnings with fix tips.
|
|
8
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
package/src/commands/daemon.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
3
4
|
import { ensureDb } from "../db.ts";
|
|
4
|
-
import { loadConfig, PRODBOARD_DIR } from "../config.ts";
|
|
5
|
+
import { loadConfig, loadConfigRaw, validateConfig, checkWebuiDependencies, PRODBOARD_DIR } from "../config.ts";
|
|
5
6
|
import { listSchedules } from "../queries/schedules.ts";
|
|
6
7
|
import { getNextFire } from "../cron.ts";
|
|
7
8
|
import { formatDate } from "../format.ts";
|
|
8
9
|
import { Daemon } from "../scheduler.ts";
|
|
10
|
+
import { systemctlAvailable, runSystemctl } from "./install.ts";
|
|
9
11
|
|
|
10
12
|
function parseArgs(args: string[]): { flags: Record<string, string | boolean>; positional: string[] } {
|
|
11
13
|
const flags: Record<string, string | boolean> = {};
|
|
@@ -110,3 +112,57 @@ export async function daemonStatus(args: string[]): Promise<void> {
|
|
|
110
112
|
try { fs.unlinkSync(pidFile); } catch {}
|
|
111
113
|
}
|
|
112
114
|
}
|
|
115
|
+
|
|
116
|
+
export async function daemonRestart(_args: string[]): Promise<void> {
|
|
117
|
+
// Validate config
|
|
118
|
+
let config;
|
|
119
|
+
try {
|
|
120
|
+
const { config: cfg, rawParsed } = loadConfigRaw();
|
|
121
|
+
config = cfg;
|
|
122
|
+
const { errors, warnings } = validateConfig(rawParsed);
|
|
123
|
+
for (const e of errors) {
|
|
124
|
+
console.error(`✗ Config: ${e}`);
|
|
125
|
+
}
|
|
126
|
+
if (errors.length > 0) {
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
for (const w of warnings) {
|
|
130
|
+
console.warn(`⚠ Config: ${w}`);
|
|
131
|
+
}
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
console.error(`Config error: ${err.message}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check webui dependencies
|
|
138
|
+
if (config.webui.enabled) {
|
|
139
|
+
const depWarnings = await checkWebuiDependencies();
|
|
140
|
+
for (const w of depWarnings) {
|
|
141
|
+
console.warn(`⚠ ${w}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check systemd availability
|
|
146
|
+
if (!(await systemctlAvailable())) {
|
|
147
|
+
console.error("systemd is not available. daemon restart requires systemd.");
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check service file exists
|
|
152
|
+
const servicePath = path.join(os.homedir(), ".config", "systemd", "user", "prodboard.service");
|
|
153
|
+
if (!fs.existsSync(servicePath)) {
|
|
154
|
+
console.error("prodboard is not installed as a systemd service. Run: prodboard install");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Restart and show status
|
|
159
|
+
const restart = await runSystemctl("restart", "prodboard");
|
|
160
|
+
if (restart.exitCode !== 0) {
|
|
161
|
+
console.error("Failed to restart prodboard:", restart.stderr);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log("prodboard daemon restarted.");
|
|
166
|
+
const { stdout } = await runSystemctl("status", "prodboard");
|
|
167
|
+
console.log(stdout);
|
|
168
|
+
}
|
package/src/commands/install.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
|
+
import { loadConfigRaw, validateConfig, checkWebuiDependencies } from "../config.ts";
|
|
4
5
|
|
|
5
6
|
const SERVICE_NAME = "prodboard";
|
|
6
7
|
const SERVICE_DIR = path.join(os.homedir(), ".config", "systemd", "user");
|
|
@@ -16,7 +17,7 @@ function parseArgs(args: string[]): { flags: Record<string, boolean> } {
|
|
|
16
17
|
return { flags };
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
async function systemctlAvailable(): Promise<boolean> {
|
|
20
|
+
export async function systemctlAvailable(): Promise<boolean> {
|
|
20
21
|
try {
|
|
21
22
|
const proc = Bun.spawn(["systemctl", "--version"], {
|
|
22
23
|
stdout: "ignore",
|
|
@@ -29,7 +30,7 @@ async function systemctlAvailable(): Promise<boolean> {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
async function runSystemctl(...args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
33
|
+
export async function runSystemctl(...args: string[]): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
33
34
|
const proc = Bun.spawn(["systemctl", "--user", ...args], {
|
|
34
35
|
stdout: "pipe",
|
|
35
36
|
stderr: "pipe",
|
|
@@ -62,6 +63,24 @@ WantedBy=default.target
|
|
|
62
63
|
export async function install(args: string[]): Promise<void> {
|
|
63
64
|
const { flags } = parseArgs(args);
|
|
64
65
|
|
|
66
|
+
// Validate config before proceeding
|
|
67
|
+
try {
|
|
68
|
+
const { config, rawParsed } = loadConfigRaw();
|
|
69
|
+
const { warnings } = validateConfig(rawParsed);
|
|
70
|
+
for (const w of warnings) {
|
|
71
|
+
console.warn(`⚠ Config: ${w}`);
|
|
72
|
+
}
|
|
73
|
+
if (config.webui.enabled) {
|
|
74
|
+
const depWarnings = await checkWebuiDependencies();
|
|
75
|
+
for (const w of depWarnings) {
|
|
76
|
+
console.warn(`⚠ ${w}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
console.error(`Config error: ${err.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
if (!(await systemctlAvailable())) {
|
|
66
85
|
console.error("systemd is not available on this system.");
|
|
67
86
|
console.error("The install command requires systemd (Linux).");
|
package/src/config.ts
CHANGED
|
@@ -140,13 +140,13 @@ export function deepMerge(defaults: any, user: any): any {
|
|
|
140
140
|
return result;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export function
|
|
143
|
+
export function loadConfigRaw(configDir?: string): { config: Config; rawParsed: any } {
|
|
144
144
|
const dir = configDir ?? PRODBOARD_DIR;
|
|
145
145
|
const configPath = path.join(dir, "config.jsonc");
|
|
146
146
|
const defaults = getDefaults();
|
|
147
147
|
|
|
148
148
|
if (!fs.existsSync(configPath)) {
|
|
149
|
-
return defaults;
|
|
149
|
+
return { config: defaults, rawParsed: {} };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
let text: string;
|
|
@@ -165,5 +165,90 @@ export function loadConfig(configDir?: string): Config {
|
|
|
165
165
|
throw new Error(`Invalid JSON in config file ${configPath}: ${err.message}`);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
return deepMerge(defaults, parsed);
|
|
168
|
+
return { config: deepMerge(defaults, parsed), rawParsed: parsed };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function loadConfig(configDir?: string): Config {
|
|
172
|
+
return loadConfigRaw(configDir).config;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateConfig(rawParsed: any): { errors: string[]; warnings: string[] } {
|
|
176
|
+
const errors: string[] = [];
|
|
177
|
+
const warnings: string[] = [];
|
|
178
|
+
|
|
179
|
+
if (typeof rawParsed !== "object" || rawParsed === null) {
|
|
180
|
+
errors.push("Config must be a JSON object.");
|
|
181
|
+
return { errors, warnings };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const knownTopLevel = ["general", "daemon", "webui"];
|
|
185
|
+
for (const key of Object.keys(rawParsed)) {
|
|
186
|
+
if (!knownTopLevel.includes(key)) {
|
|
187
|
+
warnings.push(`Unknown top-level key "${key}". Known keys: ${knownTopLevel.join(", ")}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const g = rawParsed.general;
|
|
192
|
+
if (g !== undefined) {
|
|
193
|
+
if (g.statuses !== undefined && (!Array.isArray(g.statuses) || !g.statuses.every((s: any) => typeof s === "string"))) {
|
|
194
|
+
warnings.push("general.statuses must be an array of strings.");
|
|
195
|
+
}
|
|
196
|
+
if (g.defaultStatus !== undefined && typeof g.defaultStatus !== "string") {
|
|
197
|
+
warnings.push(`general.defaultStatus must be a string, got ${typeof g.defaultStatus}.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const d = rawParsed.daemon;
|
|
202
|
+
if (d !== undefined) {
|
|
203
|
+
if (d.agent !== undefined && d.agent !== "claude" && d.agent !== "opencode") {
|
|
204
|
+
warnings.push(`daemon.agent must be "claude" or "opencode", got "${d.agent}".`);
|
|
205
|
+
}
|
|
206
|
+
if (d.useWorktrees !== undefined && !["auto", "always", "never"].includes(d.useWorktrees)) {
|
|
207
|
+
warnings.push(`daemon.useWorktrees must be "auto", "always", or "never", got "${d.useWorktrees}".`);
|
|
208
|
+
}
|
|
209
|
+
if (d.useTmux !== undefined && typeof d.useTmux !== "boolean") {
|
|
210
|
+
warnings.push(`daemon.useTmux must be a boolean, got ${typeof d.useTmux}.`);
|
|
211
|
+
}
|
|
212
|
+
for (const numField of ["maxConcurrentRuns", "maxTurns", "hardMaxTurns", "runTimeoutSeconds", "runRetentionDays"]) {
|
|
213
|
+
if (d[numField] !== undefined && typeof d[numField] !== "number") {
|
|
214
|
+
warnings.push(`daemon.${numField} must be a number, got ${typeof d[numField]}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const w = rawParsed.webui;
|
|
220
|
+
if (w !== undefined) {
|
|
221
|
+
if (w.enabled !== undefined && typeof w.enabled !== "boolean") {
|
|
222
|
+
warnings.push(`webui.enabled must be a boolean, got ${typeof w.enabled}.`);
|
|
223
|
+
}
|
|
224
|
+
if (w.port !== undefined && (typeof w.port !== "number" || w.port < 1 || w.port > 65535)) {
|
|
225
|
+
warnings.push(`webui.port must be a number between 1 and 65535, got ${JSON.stringify(w.port)}.`);
|
|
226
|
+
}
|
|
227
|
+
if (w.hostname !== undefined && typeof w.hostname !== "string") {
|
|
228
|
+
warnings.push(`webui.hostname must be a string, got ${typeof w.hostname}.`);
|
|
229
|
+
}
|
|
230
|
+
if (w.password !== undefined && w.password !== null && typeof w.password !== "string") {
|
|
231
|
+
warnings.push(`webui.password must be a string or null, got ${typeof w.password}.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { errors, warnings };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function checkWebuiDependencies(): Promise<string[]> {
|
|
239
|
+
const warnings: string[] = [];
|
|
240
|
+
try {
|
|
241
|
+
await import("hono");
|
|
242
|
+
} catch {
|
|
243
|
+
warnings.push("webui is enabled but 'hono' is not installed. Run: bun install");
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
await import("hono/jsx/jsx-runtime");
|
|
247
|
+
} catch {
|
|
248
|
+
warnings.push(
|
|
249
|
+
"webui is enabled but the Hono JSX runtime could not be loaded. " +
|
|
250
|
+
"If prodboard is installed globally, you may need to install hono in the global package directory."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return warnings;
|
|
169
254
|
}
|
package/src/index.ts
CHANGED
|
@@ -137,6 +137,8 @@ export async function main(): Promise<void> {
|
|
|
137
137
|
const daemonMod = await import("./commands/daemon.ts");
|
|
138
138
|
if (sub === "status") {
|
|
139
139
|
await daemonMod.daemonStatus(args.slice(2));
|
|
140
|
+
} else if (sub === "restart") {
|
|
141
|
+
await daemonMod.daemonRestart(args.slice(2));
|
|
140
142
|
} else {
|
|
141
143
|
await daemonMod.daemonStart(args.slice(1));
|
|
142
144
|
}
|
|
@@ -201,6 +203,8 @@ Commands:
|
|
|
201
203
|
comments <id> List comments for an issue
|
|
202
204
|
schedule <sub> Manage scheduled tasks
|
|
203
205
|
daemon Start the scheduler daemon
|
|
206
|
+
daemon restart Restart the daemon (systemd)
|
|
207
|
+
daemon status Show daemon status
|
|
204
208
|
install Install systemd service
|
|
205
209
|
uninstall Remove systemd service
|
|
206
210
|
config Show configuration
|