prodboard 0.0.0 → 0.1.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/db.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import { PRODBOARD_DIR } from "./config.ts";
5
+
6
+ export function getDbPath(): string {
7
+ return path.join(PRODBOARD_DIR, "db.sqlite");
8
+ }
9
+
10
+ export function getDb(dbPath?: string): Database {
11
+ const p = dbPath ?? getDbPath();
12
+ if (p !== ":memory:") {
13
+ const dir = path.dirname(p);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ }
18
+ const db = new Database(p);
19
+ if (p !== ":memory:") {
20
+ db.exec("PRAGMA journal_mode=WAL");
21
+ }
22
+ db.exec("PRAGMA foreign_keys=ON");
23
+ return db;
24
+ }
25
+
26
+ export function ensureDb(): Database {
27
+ return getDb(getDbPath());
28
+ }
29
+
30
+ interface Migration {
31
+ version: number;
32
+ sql: string;
33
+ }
34
+
35
+ const MIGRATIONS: Migration[] = [
36
+ {
37
+ version: 1,
38
+ sql: `
39
+ CREATE TABLE IF NOT EXISTS issues (
40
+ id TEXT PRIMARY KEY,
41
+ title TEXT NOT NULL,
42
+ description TEXT NOT NULL DEFAULT '',
43
+ status TEXT NOT NULL DEFAULT 'todo',
44
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
45
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS comments (
49
+ id TEXT PRIMARY KEY,
50
+ issue_id TEXT NOT NULL,
51
+ body TEXT NOT NULL,
52
+ author TEXT NOT NULL DEFAULT 'user',
53
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
54
+ FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS schedules (
58
+ id TEXT PRIMARY KEY,
59
+ name TEXT NOT NULL,
60
+ cron TEXT NOT NULL,
61
+ prompt TEXT NOT NULL,
62
+ workdir TEXT NOT NULL DEFAULT '.',
63
+ enabled INTEGER NOT NULL DEFAULT 1,
64
+ max_turns INTEGER,
65
+ allowed_tools TEXT,
66
+ use_worktree INTEGER NOT NULL DEFAULT 1,
67
+ inject_context INTEGER NOT NULL DEFAULT 1,
68
+ persist_session INTEGER NOT NULL DEFAULT 0,
69
+ agents_json TEXT,
70
+ source TEXT NOT NULL DEFAULT 'cli',
71
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
72
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS runs (
76
+ id TEXT PRIMARY KEY,
77
+ schedule_id TEXT NOT NULL,
78
+ status TEXT NOT NULL DEFAULT 'running',
79
+ prompt_used TEXT NOT NULL,
80
+ pid INTEGER,
81
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
82
+ finished_at TEXT,
83
+ exit_code INTEGER,
84
+ stdout_tail TEXT,
85
+ stderr_tail TEXT,
86
+ session_id TEXT,
87
+ worktree_path TEXT,
88
+ tokens_in INTEGER,
89
+ tokens_out INTEGER,
90
+ cost_usd REAL,
91
+ tools_used TEXT,
92
+ issues_touched TEXT,
93
+ FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE CASCADE
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_issues_status ON issues(status);
97
+ CREATE INDEX IF NOT EXISTS idx_issues_updated ON issues(updated_at);
98
+ CREATE INDEX IF NOT EXISTS idx_comments_issue ON comments(issue_id);
99
+ CREATE INDEX IF NOT EXISTS idx_runs_schedule ON runs(schedule_id);
100
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
101
+ CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at);
102
+ `,
103
+ },
104
+ ];
105
+
106
+ export { MIGRATIONS };
107
+
108
+ function splitStatements(sql: string): string[] {
109
+ const results: string[] = [];
110
+ let current = "";
111
+ let depth = 0;
112
+
113
+ for (let i = 0; i < sql.length; i++) {
114
+ const ch = sql[i];
115
+ if (ch === "(") depth++;
116
+ else if (ch === ")") depth--;
117
+ else if (ch === ";" && depth === 0) {
118
+ const trimmed = current.trim();
119
+ if (trimmed.length > 0) results.push(trimmed);
120
+ current = "";
121
+ continue;
122
+ }
123
+ current += ch;
124
+ }
125
+ const trimmed = current.trim();
126
+ if (trimmed.length > 0) results.push(trimmed);
127
+ return results;
128
+ }
129
+
130
+ function execStatements(db: Database, sql: string): void {
131
+ for (const stmt of splitStatements(sql)) {
132
+ db.exec(stmt);
133
+ }
134
+ }
135
+
136
+ export function runMigrations(db: Database): void {
137
+ db.exec(
138
+ "CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')))"
139
+ );
140
+
141
+ const appliedRows = db.query("SELECT version FROM _migrations ORDER BY version").all() as { version: number }[];
142
+ const applied = new Set(appliedRows.map((r) => r.version));
143
+
144
+ for (const migration of MIGRATIONS) {
145
+ if (applied.has(migration.version)) continue;
146
+
147
+ db.exec("BEGIN TRANSACTION");
148
+ try {
149
+ execStatements(db, migration.sql);
150
+ db.exec(`INSERT INTO _migrations (version) VALUES (${migration.version})`);
151
+ db.exec("COMMIT");
152
+ } catch (err: any) {
153
+ try { db.exec("ROLLBACK"); } catch {}
154
+ throw new Error(`Migration v${migration.version} failed: ${err.message}`);
155
+ }
156
+ }
157
+ }
package/src/format.ts ADDED
@@ -0,0 +1,99 @@
1
+ const NO_COLOR = !!process.env.NO_COLOR;
2
+
3
+ export function color(text: string, code: number): string {
4
+ if (NO_COLOR) return text;
5
+ return `\x1b[${code}m${text}\x1b[0m`;
6
+ }
7
+
8
+ export function dim(text: string): string {
9
+ return color(text, 2);
10
+ }
11
+ export function bold(text: string): string {
12
+ return color(text, 1);
13
+ }
14
+ export function green(text: string): string {
15
+ return color(text, 32);
16
+ }
17
+ export function red(text: string): string {
18
+ return color(text, 31);
19
+ }
20
+ export function yellow(text: string): string {
21
+ return color(text, 33);
22
+ }
23
+ export function cyan(text: string): string {
24
+ return color(text, 36);
25
+ }
26
+
27
+ export function renderTable(
28
+ headers: string[],
29
+ rows: string[][],
30
+ options?: { maxWidths?: number[] }
31
+ ): string {
32
+ const maxWidths = options?.maxWidths;
33
+
34
+ // Calculate column widths
35
+ const colWidths = headers.map((h, i) => {
36
+ let max = h.length;
37
+ for (const row of rows) {
38
+ if (row[i] && row[i].length > max) max = row[i].length;
39
+ }
40
+ if (maxWidths && maxWidths[i] && max > maxWidths[i]) {
41
+ max = maxWidths[i];
42
+ }
43
+ return max;
44
+ });
45
+
46
+ function truncate(text: string, width: number): string {
47
+ if (text.length <= width) return text;
48
+ return text.slice(0, width - 1) + "…";
49
+ }
50
+
51
+ function pad(text: string, width: number): string {
52
+ const t = truncate(text, width);
53
+ return t + " ".repeat(Math.max(0, width - t.length));
54
+ }
55
+
56
+ const top =
57
+ "┌" + colWidths.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
58
+ const mid =
59
+ "├" + colWidths.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
60
+ const bot =
61
+ "└" + colWidths.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
62
+
63
+ const headerRow =
64
+ "│" +
65
+ headers.map((h, i) => " " + pad(h, colWidths[i]) + " ").join("│") +
66
+ "│";
67
+
68
+ const dataRows = rows.map(
69
+ (row) =>
70
+ "│" +
71
+ row
72
+ .map((cell, i) => " " + pad(cell ?? "", colWidths[i]) + " ")
73
+ .join("│") +
74
+ "│"
75
+ );
76
+
77
+ const lines = [top, headerRow];
78
+ if (rows.length > 0) {
79
+ lines.push(mid);
80
+ lines.push(...dataRows);
81
+ }
82
+ lines.push(bot);
83
+ return lines.join("\n");
84
+ }
85
+
86
+ export function formatDate(iso: string): string {
87
+ if (!iso) return "";
88
+ const d = new Date(iso + (iso.includes("Z") || iso.includes("+") ? "" : "Z"));
89
+ const year = d.getUTCFullYear();
90
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
91
+ const day = String(d.getUTCDate()).padStart(2, "0");
92
+ const hour = String(d.getUTCHours()).padStart(2, "0");
93
+ const min = String(d.getUTCMinutes()).padStart(2, "0");
94
+ return `${year}-${month}-${day} ${hour}:${min}`;
95
+ }
96
+
97
+ export function jsonOutput(data: unknown): string {
98
+ return JSON.stringify(data);
99
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { randomBytes } from "crypto";
2
+
3
+ export function generateId(): string {
4
+ return randomBytes(8).toString("hex");
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { existsSync } from "fs";
2
+ import { PRODBOARD_DIR } from "./config.ts";
3
+
4
+ export class NotInitializedError extends Error {
5
+ constructor() {
6
+ super(`prodboard is not initialized. Run 'prodboard init' first.`);
7
+ this.name = "NotInitializedError";
8
+ }
9
+ }
10
+
11
+ export class DatabaseError extends Error {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = "DatabaseError";
15
+ }
16
+ }
17
+
18
+ function ensureInitialized(): void {
19
+ if (!existsSync(PRODBOARD_DIR)) {
20
+ throw new NotInitializedError();
21
+ }
22
+ }
23
+
24
+ export async function main(): Promise<void> {
25
+ const args = Bun.argv.slice(2);
26
+ const command = args[0];
27
+
28
+ if (command === "--version" || command === "version") {
29
+ const pkg = await import("../package.json");
30
+ console.log(pkg.version);
31
+ return;
32
+ }
33
+
34
+ if (command === "--help" || command === "help" || !command) {
35
+ printHelp();
36
+ return;
37
+ }
38
+
39
+ try {
40
+ switch (command) {
41
+ case "init": {
42
+ const { init } = await import("./commands/init.ts");
43
+ await init(args.slice(1));
44
+ break;
45
+ }
46
+ case "add": {
47
+ ensureInitialized();
48
+ const { add } = await import("./commands/issues.ts");
49
+ await add(args.slice(1));
50
+ break;
51
+ }
52
+ case "ls": {
53
+ ensureInitialized();
54
+ const { ls } = await import("./commands/issues.ts");
55
+ await ls(args.slice(1));
56
+ break;
57
+ }
58
+ case "show": {
59
+ ensureInitialized();
60
+ const { show } = await import("./commands/issues.ts");
61
+ await show(args.slice(1));
62
+ break;
63
+ }
64
+ case "edit": {
65
+ ensureInitialized();
66
+ const { edit } = await import("./commands/issues.ts");
67
+ await edit(args.slice(1));
68
+ break;
69
+ }
70
+ case "mv": {
71
+ ensureInitialized();
72
+ const { mv } = await import("./commands/issues.ts");
73
+ await mv(args.slice(1));
74
+ break;
75
+ }
76
+ case "rm": {
77
+ ensureInitialized();
78
+ const { rm } = await import("./commands/issues.ts");
79
+ await rm(args.slice(1));
80
+ break;
81
+ }
82
+ case "comment": {
83
+ ensureInitialized();
84
+ const { comment } = await import("./commands/comments.ts");
85
+ await comment(args.slice(1));
86
+ break;
87
+ }
88
+ case "comments": {
89
+ ensureInitialized();
90
+ const { comments } = await import("./commands/comments.ts");
91
+ await comments(args.slice(1));
92
+ break;
93
+ }
94
+ case "schedule": {
95
+ ensureInitialized();
96
+ const sub = args[1];
97
+ const subArgs = args.slice(2);
98
+ const schedMod = await import("./commands/schedules.ts");
99
+ switch (sub) {
100
+ case "add":
101
+ await schedMod.scheduleAdd(subArgs);
102
+ break;
103
+ case "ls":
104
+ await schedMod.scheduleLs(subArgs);
105
+ break;
106
+ case "edit":
107
+ await schedMod.scheduleEdit(subArgs);
108
+ break;
109
+ case "enable":
110
+ await schedMod.scheduleEnable(subArgs);
111
+ break;
112
+ case "disable":
113
+ await schedMod.scheduleDisable(subArgs);
114
+ break;
115
+ case "rm":
116
+ await schedMod.scheduleRm(subArgs);
117
+ break;
118
+ case "logs":
119
+ await schedMod.scheduleLogs(subArgs);
120
+ break;
121
+ case "run":
122
+ await schedMod.scheduleRun(subArgs);
123
+ break;
124
+ case "stats":
125
+ await schedMod.scheduleStats(subArgs);
126
+ break;
127
+ default:
128
+ console.error(`Unknown schedule subcommand: ${sub}`);
129
+ console.error("Available: add, ls, edit, enable, disable, rm, logs, run, stats");
130
+ process.exit(1);
131
+ }
132
+ break;
133
+ }
134
+ case "daemon": {
135
+ ensureInitialized();
136
+ const sub = args[1];
137
+ const daemonMod = await import("./commands/daemon.ts");
138
+ if (sub === "status") {
139
+ await daemonMod.daemonStatus(args.slice(2));
140
+ } else {
141
+ await daemonMod.daemonStart(args.slice(1));
142
+ }
143
+ break;
144
+ }
145
+ case "install": {
146
+ ensureInitialized();
147
+ const { install } = await import("./commands/install.ts");
148
+ await install(args.slice(1));
149
+ break;
150
+ }
151
+ case "uninstall": {
152
+ const { uninstall } = await import("./commands/install.ts");
153
+ await uninstall(args.slice(1));
154
+ break;
155
+ }
156
+ case "config": {
157
+ ensureInitialized();
158
+ const { loadConfig } = await import("./config.ts");
159
+ const config = loadConfig();
160
+ console.log(JSON.stringify(config, null, 2));
161
+ break;
162
+ }
163
+ case "mcp": {
164
+ ensureInitialized();
165
+ const { startMcpServer } = await import("./mcp.ts");
166
+ await startMcpServer();
167
+ break;
168
+ }
169
+ default:
170
+ console.error(`Unknown command: ${command}`);
171
+ printHelp();
172
+ process.exit(1);
173
+ }
174
+ } catch (err: any) {
175
+ if (err instanceof NotInitializedError) {
176
+ console.error(err.message);
177
+ process.exit(3);
178
+ }
179
+ if (err instanceof DatabaseError) {
180
+ console.error(`Database error: ${err.message}`);
181
+ process.exit(2);
182
+ }
183
+ console.error(err instanceof Error ? err.message : String(err));
184
+ process.exit(1);
185
+ }
186
+ }
187
+
188
+ function printHelp(): void {
189
+ console.log(`prodboard — CLI-first issue tracker for AI coding agents
190
+
191
+ Usage: prodboard <command> [options]
192
+
193
+ Commands:
194
+ init Initialize prodboard (~/.prodboard/)
195
+ add <title> Create a new issue
196
+ ls List issues
197
+ show <id> Show issue details
198
+ edit <id> Edit an issue
199
+ mv <id> <status> Change issue status
200
+ rm <id> Delete an issue
201
+ comment <id> Add a comment to an issue
202
+ comments <id> List comments for an issue
203
+ schedule <sub> Manage scheduled tasks
204
+ daemon Start the scheduler daemon
205
+ install Install systemd service
206
+ uninstall Remove systemd service
207
+ config Show configuration
208
+ mcp Start MCP server (stdio)
209
+ version Show version
210
+ help Show this help
211
+
212
+ Schedule subcommands:
213
+ schedule add Create a schedule
214
+ schedule ls List schedules
215
+ schedule edit Edit a schedule
216
+ schedule enable Enable a schedule
217
+ schedule disable Disable a schedule
218
+ schedule rm Delete a schedule
219
+ schedule logs Show run history
220
+ schedule run Run a schedule immediately
221
+ schedule stats Show schedule statistics
222
+
223
+ Options:
224
+ --json Output in JSON format
225
+ --help Show help for a command
226
+ --version Show version`);
227
+ }
@@ -0,0 +1,102 @@
1
+ import * as path from "path";
2
+ import type { Config, Schedule, Run, EnvironmentInfo } from "./types.ts";
3
+ import { PRODBOARD_DIR } from "./config.ts";
4
+ import { getLastSessionId } from "./queries/runs.ts";
5
+ import { Database } from "bun:sqlite";
6
+
7
+ export function detectEnvironment(workdir: string, config: Config): EnvironmentInfo {
8
+ let hasGit = false;
9
+ try {
10
+ const result = Bun.spawnSync(["git", "rev-parse", "--git-dir"], {
11
+ cwd: workdir,
12
+ stdout: "pipe",
13
+ stderr: "pipe",
14
+ });
15
+ hasGit = result.exitCode === 0;
16
+ } catch {}
17
+
18
+ let hasClaude = false;
19
+ try {
20
+ const result = Bun.spawnSync(["claude", "--version"], {
21
+ stdout: "pipe",
22
+ stderr: "pipe",
23
+ });
24
+ hasClaude = result.exitCode === 0;
25
+ } catch {}
26
+
27
+ const worktreeSupported = hasGit && config.daemon.useWorktrees !== "never";
28
+
29
+ return { hasGit, hasClaude, worktreeSupported };
30
+ }
31
+
32
+ export function buildInvocation(
33
+ schedule: Schedule,
34
+ run: Run,
35
+ config: Config,
36
+ env: EnvironmentInfo,
37
+ resolvedPrompt: string,
38
+ db?: Database
39
+ ): string[] {
40
+ const args: string[] = ["claude"];
41
+
42
+ // Prompt
43
+ args.push("-p", resolvedPrompt);
44
+
45
+ // Permissions
46
+ args.push("--dangerously-skip-permissions");
47
+
48
+ // Output format
49
+ args.push("--output-format", "stream-json");
50
+
51
+ // MCP config
52
+ const mcpConfigPath = path.join(PRODBOARD_DIR, "mcp.json");
53
+ args.push("--mcp-config", mcpConfigPath);
54
+
55
+ // System prompt
56
+ const systemPromptFile = env.hasGit
57
+ ? path.join(PRODBOARD_DIR, "system-prompt.md")
58
+ : path.join(PRODBOARD_DIR, "system-prompt-nogit.md");
59
+ args.push("--append-system-prompt-file", systemPromptFile);
60
+
61
+ // Max turns: min of schedule override, config default, and hard max
62
+ const scheduleTurns = schedule.max_turns ?? config.daemon.maxTurns;
63
+ const maxTurns = Math.min(scheduleTurns, config.daemon.hardMaxTurns);
64
+ args.push("--max-turns", String(maxTurns));
65
+
66
+ // Allowed tools
67
+ let tools: string[];
68
+ if (schedule.allowed_tools) {
69
+ try {
70
+ tools = JSON.parse(schedule.allowed_tools);
71
+ } catch {
72
+ tools = env.hasGit ? config.daemon.defaultAllowedTools : config.daemon.nonGitDefaultAllowedTools;
73
+ }
74
+ } else if (!env.hasGit) {
75
+ tools = config.daemon.nonGitDefaultAllowedTools;
76
+ } else {
77
+ tools = config.daemon.defaultAllowedTools;
78
+ }
79
+ for (const tool of tools) {
80
+ args.push("--allowedTools", tool);
81
+ }
82
+
83
+ // Worktree
84
+ if (env.worktreeSupported && schedule.use_worktree !== 0) {
85
+ args.push("--worktree");
86
+ }
87
+
88
+ // Session resume
89
+ if (schedule.persist_session && db) {
90
+ const lastSessionId = getLastSessionId(db, schedule.id);
91
+ if (lastSessionId) {
92
+ args.push("--resume", lastSessionId);
93
+ }
94
+ }
95
+
96
+ // Agents JSON
97
+ if (schedule.agents_json) {
98
+ args.push("--agents", schedule.agents_json);
99
+ }
100
+
101
+ return args;
102
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,84 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ export type LogLevel = "debug" | "info" | "warn" | "error";
5
+
6
+ const LEVEL_ORDER: Record<LogLevel, number> = {
7
+ debug: 0,
8
+ info: 1,
9
+ warn: 2,
10
+ error: 3,
11
+ };
12
+
13
+ export class Logger {
14
+ private logDir: string;
15
+ private level: LogLevel;
16
+ private maxSizeBytes: number;
17
+ private maxFiles: number;
18
+ private logFile: string;
19
+
20
+ constructor(options: { logDir: string; level: LogLevel; maxSizeMb: number; maxFiles: number }) {
21
+ this.logDir = options.logDir;
22
+ this.level = options.level;
23
+ this.maxSizeBytes = options.maxSizeMb * 1024 * 1024;
24
+ this.maxFiles = options.maxFiles;
25
+ this.logFile = path.join(this.logDir, "daemon.log");
26
+
27
+ if (!fs.existsSync(this.logDir)) {
28
+ fs.mkdirSync(this.logDir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ debug(msg: string, data?: Record<string, unknown>): void {
33
+ this.log("debug", msg, data);
34
+ }
35
+
36
+ info(msg: string, data?: Record<string, unknown>): void {
37
+ this.log("info", msg, data);
38
+ }
39
+
40
+ warn(msg: string, data?: Record<string, unknown>): void {
41
+ this.log("warn", msg, data);
42
+ }
43
+
44
+ error(msg: string, data?: Record<string, unknown>): void {
45
+ this.log("error", msg, data);
46
+ }
47
+
48
+ private log(level: LogLevel, msg: string, data?: Record<string, unknown>): void {
49
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
50
+
51
+ const timestamp = new Date().toISOString();
52
+ const dataStr = data ? " " + JSON.stringify(data) : "";
53
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${msg}${dataStr}\n`;
54
+
55
+ this.rotate();
56
+ fs.appendFileSync(this.logFile, line);
57
+
58
+ // Also output to stderr
59
+ process.stderr.write(line);
60
+ }
61
+
62
+ private rotate(): void {
63
+ try {
64
+ if (!fs.existsSync(this.logFile)) return;
65
+ const stat = fs.statSync(this.logFile);
66
+ if (stat.size < this.maxSizeBytes) return;
67
+
68
+ // Shift existing rotated files
69
+ for (let i = this.maxFiles; i >= 1; i--) {
70
+ const src = path.join(this.logDir, `daemon.${i}.log`);
71
+ if (fs.existsSync(src)) {
72
+ if (i >= this.maxFiles) {
73
+ fs.unlinkSync(src);
74
+ } else {
75
+ fs.renameSync(src, path.join(this.logDir, `daemon.${i + 1}.log`));
76
+ }
77
+ }
78
+ }
79
+
80
+ // Move current to .1
81
+ fs.renameSync(this.logFile, path.join(this.logDir, "daemon.1.log"));
82
+ } catch {}
83
+ }
84
+ }