@masslessai/push-todo 4.5.1 → 4.5.3
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/lib/cli.js +66 -1
- package/lib/context-engine.js +4 -2
- package/lib/daemon.js +43 -10
- package/lib/journal-cron.js +236 -0
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { showSettings, toggleSetting, setMaxBatchSize } from './config.js';
|
|
|
17
17
|
import { ensureDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './daemon-health.js';
|
|
18
18
|
import { install as installLaunchAgent, uninstall as uninstallLaunchAgent, getStatus as getLaunchAgentStatus } from './launchagent.js';
|
|
19
19
|
import { getScreenshotPath, screenshotExists, openScreenshot } from './utils/screenshots.js';
|
|
20
|
-
import { bold, red, cyan, dim, green } from './utils/colors.js';
|
|
20
|
+
import { bold, red, cyan, dim, green, yellow } from './utils/colors.js';
|
|
21
21
|
import { getMachineId } from './machine-id.js';
|
|
22
22
|
import { parseReminder } from './reminder-parser.js';
|
|
23
23
|
|
|
@@ -139,6 +139,12 @@ ${bold('SCHEDULE (remote scheduled jobs):')}
|
|
|
139
139
|
push-todo schedule enable <id> Enable a schedule
|
|
140
140
|
push-todo schedule disable <id> Disable a schedule
|
|
141
141
|
|
|
142
|
+
${bold('JOURNAL (daily automated journal):')}
|
|
143
|
+
push-todo journal setup Install daily journal cron (default: 9pm)
|
|
144
|
+
--time <HH:MM> Time to run daily (e.g., "21:00", "09:30")
|
|
145
|
+
push-todo journal status Show journal cron status
|
|
146
|
+
push-todo journal remove Uninstall journal cron
|
|
147
|
+
|
|
142
148
|
${bold('SETTINGS:')}
|
|
143
149
|
push-todo setting Show all settings
|
|
144
150
|
push-todo setting auto-commit Toggle auto-commit
|
|
@@ -212,6 +218,8 @@ const options = {
|
|
|
212
218
|
'check-messages': { type: 'string' },
|
|
213
219
|
'request-input': { type: 'string' },
|
|
214
220
|
'question': { type: 'string' },
|
|
221
|
+
// Journal cron options
|
|
222
|
+
'time': { type: 'string' },
|
|
215
223
|
};
|
|
216
224
|
|
|
217
225
|
/**
|
|
@@ -970,6 +978,63 @@ ${bold('Examples:')}
|
|
|
970
978
|
return;
|
|
971
979
|
}
|
|
972
980
|
|
|
981
|
+
// Journal cron command
|
|
982
|
+
if (command === 'journal') {
|
|
983
|
+
const { install, uninstall, getStatus, parseTime, formatTime, DEFAULT_HOUR, DEFAULT_MINUTE } = await import('./journal-cron.js');
|
|
984
|
+
const subCommand = positionals[1] || 'setup';
|
|
985
|
+
|
|
986
|
+
if (subCommand === 'setup') {
|
|
987
|
+
let hour = DEFAULT_HOUR;
|
|
988
|
+
let minute = DEFAULT_MINUTE;
|
|
989
|
+
if (values.time) {
|
|
990
|
+
try {
|
|
991
|
+
({ hour, minute } = parseTime(values.time));
|
|
992
|
+
} catch (error) {
|
|
993
|
+
console.error(red(error.message));
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const result = install({ hour, minute });
|
|
998
|
+
if (result.success) {
|
|
999
|
+
console.log(green(result.message));
|
|
1000
|
+
console.log(dim(`Journal entries will be written to ~/journal/`));
|
|
1001
|
+
console.log(dim(`Logs: ~/.push/journal.log`));
|
|
1002
|
+
} else {
|
|
1003
|
+
console.error(red(result.message));
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (subCommand === 'status') {
|
|
1010
|
+
const status = getStatus();
|
|
1011
|
+
if (!status.installed) {
|
|
1012
|
+
console.log(dim('Journal cron not installed. Run: push-todo journal setup'));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const timeStr = typeof status.hour === 'number' ? formatTime(status.hour, status.minute) : 'unknown';
|
|
1016
|
+
console.log(`Journal cron: ${status.loaded ? green('active') : yellow('installed (not loaded)')}`);
|
|
1017
|
+
console.log(` Runs daily at: ${bold(timeStr)}`);
|
|
1018
|
+
console.log(` Logs: ~/.push/journal.log`);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (subCommand === 'remove') {
|
|
1023
|
+
const result = uninstall();
|
|
1024
|
+
if (result.success) {
|
|
1025
|
+
console.log(green(result.message));
|
|
1026
|
+
} else {
|
|
1027
|
+
console.error(red(result.message));
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
console.error(red(`Unknown journal subcommand: ${subCommand}`));
|
|
1034
|
+
console.log(dim('Usage: push-todo journal [setup|status|remove]'));
|
|
1035
|
+
process.exit(1);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
973
1038
|
// Connect command
|
|
974
1039
|
if (command === 'connect') {
|
|
975
1040
|
if (values.auto) {
|
package/lib/context-engine.js
CHANGED
|
@@ -111,8 +111,10 @@ export function scanProjectSkills(projectPath) {
|
|
|
111
111
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
112
112
|
|
|
113
113
|
// Parse tools field: comma-separated list of tool names/patterns
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// Skills use "allowed-tools" in frontmatter, fall back to "tools" for compatibility
|
|
115
|
+
const toolsRaw = frontmatter['allowed-tools'] || frontmatter.tools;
|
|
116
|
+
const tools = toolsRaw
|
|
117
|
+
? toolsRaw.split(',').map(t => t.trim()).filter(Boolean)
|
|
116
118
|
: [];
|
|
117
119
|
|
|
118
120
|
skills.push({
|
package/lib/daemon.js
CHANGED
|
@@ -1621,7 +1621,9 @@ function respawnWithInjectedMessage(displayNumber) {
|
|
|
1621
1621
|
'Bash(npm *)', 'Bash(npx *)', 'Bash(yarn *)',
|
|
1622
1622
|
'Bash(python *)', 'Bash(python3 *)', 'Bash(pip *)', 'Bash(pip3 *)',
|
|
1623
1623
|
'Bash(push-todo *)',
|
|
1624
|
-
'
|
|
1624
|
+
'Bash(bird *)', 'Bash(gh *)',
|
|
1625
|
+
'Bash(redbook *)',
|
|
1626
|
+
'Task', 'Agent',
|
|
1625
1627
|
'WebSearch', 'WebFetch',
|
|
1626
1628
|
'ToolSearch',
|
|
1627
1629
|
];
|
|
@@ -1943,8 +1945,15 @@ async function killIdleTasks() {
|
|
|
1943
1945
|
if (!lastOutput) continue; // No output tracking yet — not idle, just starting
|
|
1944
1946
|
|
|
1945
1947
|
const idleMs = now - lastOutput;
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
+
|
|
1949
|
+
// Agent subprocesses can run for minutes without producing stdout.
|
|
1950
|
+
// Use a longer timeout (3x) when the last known tool is Agent.
|
|
1951
|
+
const activity = taskActivityState.get(displayNumber) || {};
|
|
1952
|
+
const isAgentRunning = activity.currentTool === 'Agent';
|
|
1953
|
+
const effectiveTimeout = isAgentRunning ? IDLE_TIMEOUT_MS * 3 : IDLE_TIMEOUT_MS;
|
|
1954
|
+
|
|
1955
|
+
if (idleMs > effectiveTimeout) {
|
|
1956
|
+
log(`Task #${displayNumber} IDLE TIMEOUT: ${Math.floor(idleMs / 1000)}s since last output${isAgentRunning ? ' (Agent extended)' : ''}`);
|
|
1948
1957
|
idleTimedOut.push(displayNumber);
|
|
1949
1958
|
}
|
|
1950
1959
|
}
|
|
@@ -2274,9 +2283,11 @@ async function executeTask(task) {
|
|
|
2274
2283
|
parts.push('Links:\n' + links.map(l => ` - ${l.url}${l.title ? ` (${l.title})` : ''}`).join('\n'));
|
|
2275
2284
|
}
|
|
2276
2285
|
if (screenshots.length > 0) {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2286
|
+
const screenshotDir = join(homedir(), 'Library', 'Mobile Documents', 'iCloud~ai~massless~push', 'Documents', 'Screenshots');
|
|
2287
|
+
parts.push('Screenshots (use Read tool on these paths):\n' + screenshots.map(s => {
|
|
2288
|
+
const filename = s.imageFilename || s.image_filename;
|
|
2289
|
+
return ` - ${join(screenshotDir, filename)}${s.sourceApp ? ` (from ${s.sourceApp})` : ''}`;
|
|
2290
|
+
}).join('\n'));
|
|
2280
2291
|
}
|
|
2281
2292
|
|
|
2282
2293
|
if (parts.length > 0) {
|
|
@@ -2306,7 +2317,9 @@ async function executeTask(task) {
|
|
|
2306
2317
|
'Bash(npm *)', 'Bash(npx *)', 'Bash(yarn *)',
|
|
2307
2318
|
'Bash(python *)', 'Bash(python3 *)', 'Bash(pip *)', 'Bash(pip3 *)',
|
|
2308
2319
|
'Bash(push-todo *)',
|
|
2309
|
-
'
|
|
2320
|
+
'Bash(bird *)', 'Bash(gh *)',
|
|
2321
|
+
'Bash(redbook *)',
|
|
2322
|
+
'Task', 'Agent',
|
|
2310
2323
|
'WebSearch', 'WebFetch',
|
|
2311
2324
|
'ToolSearch',
|
|
2312
2325
|
];
|
|
@@ -2564,16 +2577,30 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
2564
2577
|
executionSummary += ` PR: ${prUrl}`;
|
|
2565
2578
|
}
|
|
2566
2579
|
|
|
2580
|
+
// Build structured recap for briefing visualization (JSONB column)
|
|
2581
|
+
const prNumber = prUrl ? parseInt((prUrl.match(/\/(\d+)$/) || [])[1] || '0', 10) || null : null;
|
|
2582
|
+
const executionRecap = {
|
|
2583
|
+
durationStr,
|
|
2584
|
+
durationSeconds: duration,
|
|
2585
|
+
diagram: visualArtifact || null,
|
|
2586
|
+
machineName,
|
|
2587
|
+
outcome: {
|
|
2588
|
+
prUrl: prUrl || null,
|
|
2589
|
+
prNumber,
|
|
2590
|
+
},
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2567
2593
|
const statusUpdated = await updateTaskStatus(displayNumber, 'session_finished', {
|
|
2568
2594
|
duration,
|
|
2569
2595
|
sessionId,
|
|
2570
|
-
summary: executionSummary
|
|
2596
|
+
summary: executionSummary,
|
|
2597
|
+
executionRecap,
|
|
2571
2598
|
}, info.taskId);
|
|
2572
2599
|
if (!statusUpdated) {
|
|
2573
2600
|
logError(`Task #${displayNumber}: Failed to update status to session_finished — will retry`);
|
|
2574
2601
|
// Retry once
|
|
2575
2602
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
2576
|
-
duration, sessionId, summary: executionSummary
|
|
2603
|
+
duration, sessionId, summary: executionSummary, executionRecap,
|
|
2577
2604
|
}, info.taskId);
|
|
2578
2605
|
}
|
|
2579
2606
|
|
|
@@ -2692,7 +2719,13 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
2692
2719
|
? `${failureSummary}\nExit code ${exitCode}. Ran for ${durationStr} on ${machineName}.`
|
|
2693
2720
|
: `Exit code ${exitCode}: ${stderr.slice(0, 200)}`;
|
|
2694
2721
|
|
|
2695
|
-
|
|
2722
|
+
const failedRecap = {
|
|
2723
|
+
durationStr,
|
|
2724
|
+
durationSeconds: duration,
|
|
2725
|
+
machineName,
|
|
2726
|
+
exitCode,
|
|
2727
|
+
};
|
|
2728
|
+
await updateTaskStatus(displayNumber, 'failed', { error: errorMsg, sessionId, executionRecap: failedRecap }, info.taskId);
|
|
2696
2729
|
|
|
2697
2730
|
if (NOTIFY_ON_FAILURE) {
|
|
2698
2731
|
sendMacNotification(
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily journal cron via LaunchAgent.
|
|
3
|
+
*
|
|
4
|
+
* Installs a user-level LaunchAgent that fires claude with /journal today
|
|
5
|
+
* at a configurable daily time. More reliable than Supabase remote schedules:
|
|
6
|
+
* - System-level: survives daemon restarts and Supabase outages
|
|
7
|
+
* - StartCalendarInterval: native macOS daily scheduling (no polling)
|
|
8
|
+
* - Writes to ~/journal/ in the user's home directory
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* push-todo journal setup Install at 9pm (default)
|
|
12
|
+
* push-todo journal setup --time 21:30 Install at 9:30pm
|
|
13
|
+
* push-todo journal status Show status
|
|
14
|
+
* push-todo journal remove Uninstall
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
18
|
+
import { execFileSync } from 'child_process';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
|
|
22
|
+
const LABEL = 'ai.massless.push.journal';
|
|
23
|
+
const LAUNCH_AGENTS_DIR = join(homedir(), 'Library', 'LaunchAgents');
|
|
24
|
+
const PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${LABEL}.plist`);
|
|
25
|
+
const PUSH_DIR = join(homedir(), '.push');
|
|
26
|
+
const LOG_FILE = join(PUSH_DIR, 'journal.log');
|
|
27
|
+
|
|
28
|
+
// Default: 9pm daily
|
|
29
|
+
const DEFAULT_HOUR = 21;
|
|
30
|
+
const DEFAULT_MINUTE = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find the claude binary (real binary, not shell function).
|
|
34
|
+
* Tries ~/.local/bin/claude first, then PATH.
|
|
35
|
+
*/
|
|
36
|
+
function findClaudeBinary() {
|
|
37
|
+
const candidates = [
|
|
38
|
+
join(homedir(), '.local', 'bin', 'claude'),
|
|
39
|
+
'/usr/local/bin/claude',
|
|
40
|
+
'/opt/homebrew/bin/claude',
|
|
41
|
+
];
|
|
42
|
+
for (const p of candidates) {
|
|
43
|
+
if (existsSync(p)) return p;
|
|
44
|
+
}
|
|
45
|
+
// Fallback: search PATH via which
|
|
46
|
+
try {
|
|
47
|
+
const found = execFileSync('which', ['claude'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
48
|
+
if (found && existsSync(found)) return found;
|
|
49
|
+
} catch {}
|
|
50
|
+
return 'claude'; // last resort
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse "HH:MM" string into { hour, minute }.
|
|
55
|
+
* Throws if format is invalid.
|
|
56
|
+
*/
|
|
57
|
+
export function parseTime(timeStr) {
|
|
58
|
+
const match = timeStr.match(/^(\d{1,2}):(\d{2})$/);
|
|
59
|
+
if (!match) throw new Error(`Invalid time format: "${timeStr}". Use HH:MM (e.g., "21:00", "09:30")`);
|
|
60
|
+
const hour = parseInt(match[1], 10);
|
|
61
|
+
const minute = parseInt(match[2], 10);
|
|
62
|
+
if (hour < 0 || hour > 23) throw new Error(`Hour must be 0-23, got ${hour}`);
|
|
63
|
+
if (minute < 0 || minute > 59) throw new Error(`Minute must be 0-59, got ${minute}`);
|
|
64
|
+
return { hour, minute };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate the plist XML for the daily journal LaunchAgent.
|
|
69
|
+
*/
|
|
70
|
+
function generatePlist(hour, minute) {
|
|
71
|
+
const claudeBin = findClaudeBinary();
|
|
72
|
+
const nodePath = join(homedir(), '.nvm', 'versions', 'node', 'v24.9.0', 'bin');
|
|
73
|
+
|
|
74
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
75
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
76
|
+
<plist version="1.0">
|
|
77
|
+
<dict>
|
|
78
|
+
<key>Label</key>
|
|
79
|
+
<string>${LABEL}</string>
|
|
80
|
+
|
|
81
|
+
<key>ProgramArguments</key>
|
|
82
|
+
<array>
|
|
83
|
+
<string>${claudeBin}</string>
|
|
84
|
+
<string>-p</string>
|
|
85
|
+
<string>Run /journal today - generate today's journal entry summarizing the day's work, experiments, and learnings. Use the journal skill to write the entry to ~/journal/ with today's date.</string>
|
|
86
|
+
<string>--dangerously-skip-permissions</string>
|
|
87
|
+
<string>--allowedTools</string>
|
|
88
|
+
<string>Read,Write,Glob,Grep</string>
|
|
89
|
+
</array>
|
|
90
|
+
|
|
91
|
+
<key>EnvironmentVariables</key>
|
|
92
|
+
<dict>
|
|
93
|
+
<key>PATH</key>
|
|
94
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${nodePath}:${join(homedir(), '.local', 'bin')}</string>
|
|
95
|
+
<key>HOME</key>
|
|
96
|
+
<string>${homedir()}</string>
|
|
97
|
+
</dict>
|
|
98
|
+
|
|
99
|
+
<key>WorkingDirectory</key>
|
|
100
|
+
<string>${homedir()}</string>
|
|
101
|
+
|
|
102
|
+
<key>StartCalendarInterval</key>
|
|
103
|
+
<dict>
|
|
104
|
+
<key>Hour</key>
|
|
105
|
+
<integer>${hour}</integer>
|
|
106
|
+
<key>Minute</key>
|
|
107
|
+
<integer>${minute}</integer>
|
|
108
|
+
</dict>
|
|
109
|
+
|
|
110
|
+
<key>StandardOutPath</key>
|
|
111
|
+
<string>${LOG_FILE}</string>
|
|
112
|
+
|
|
113
|
+
<key>StandardErrorPath</key>
|
|
114
|
+
<string>${LOG_FILE}</string>
|
|
115
|
+
</dict>
|
|
116
|
+
</plist>
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the hour/minute from an installed plist.
|
|
122
|
+
* Returns null if not installed or can't parse.
|
|
123
|
+
*/
|
|
124
|
+
function readInstalledTime() {
|
|
125
|
+
if (!existsSync(PLIST_PATH)) return null;
|
|
126
|
+
try {
|
|
127
|
+
const content = readFileSync(PLIST_PATH, 'utf8');
|
|
128
|
+
const hourMatch = content.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
|
|
129
|
+
const minMatch = content.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
|
|
130
|
+
if (hourMatch && minMatch) {
|
|
131
|
+
return { hour: parseInt(hourMatch[1], 10), minute: parseInt(minMatch[1], 10) };
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format hour/minute as "HH:MM am/pm".
|
|
139
|
+
*/
|
|
140
|
+
function formatTime(hour, minute) {
|
|
141
|
+
const suffix = hour >= 12 ? 'pm' : 'am';
|
|
142
|
+
const displayHour = hour % 12 || 12;
|
|
143
|
+
const displayMin = String(minute).padStart(2, '0');
|
|
144
|
+
return `${displayHour}:${displayMin}${suffix}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check installation status.
|
|
149
|
+
* @returns {{ installed: boolean, loaded: boolean, hour?: number, minute?: number }}
|
|
150
|
+
*/
|
|
151
|
+
export function getStatus() {
|
|
152
|
+
const installed = existsSync(PLIST_PATH);
|
|
153
|
+
let loaded = false;
|
|
154
|
+
|
|
155
|
+
if (installed) {
|
|
156
|
+
try {
|
|
157
|
+
execFileSync('launchctl', ['list', LABEL], {
|
|
158
|
+
timeout: 5000,
|
|
159
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
160
|
+
});
|
|
161
|
+
loaded = true;
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const time = readInstalledTime();
|
|
166
|
+
return { installed, loaded, ...time };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Install (or update) the daily journal LaunchAgent.
|
|
171
|
+
*
|
|
172
|
+
* @param {{ hour?: number, minute?: number }} options
|
|
173
|
+
* @returns {{ success: boolean, message: string }}
|
|
174
|
+
*/
|
|
175
|
+
export function install({ hour = DEFAULT_HOUR, minute = DEFAULT_MINUTE } = {}) {
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
178
|
+
mkdirSync(PUSH_DIR, { recursive: true });
|
|
179
|
+
|
|
180
|
+
const newContent = generatePlist(hour, minute);
|
|
181
|
+
|
|
182
|
+
// Unload existing if present (to apply time changes)
|
|
183
|
+
if (existsSync(PLIST_PATH)) {
|
|
184
|
+
try {
|
|
185
|
+
execFileSync('launchctl', ['unload', PLIST_PATH], {
|
|
186
|
+
timeout: 5000,
|
|
187
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
188
|
+
});
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
writeFileSync(PLIST_PATH, newContent);
|
|
193
|
+
|
|
194
|
+
execFileSync('launchctl', ['load', '-w', PLIST_PATH], {
|
|
195
|
+
timeout: 5000,
|
|
196
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const timeStr = formatTime(hour, minute);
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
message: `Journal cron installed — runs daily at ${timeStr}`,
|
|
203
|
+
hour,
|
|
204
|
+
minute,
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return { success: false, message: `Failed to install journal cron: ${error.message}` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Uninstall the journal LaunchAgent.
|
|
213
|
+
*
|
|
214
|
+
* @returns {{ success: boolean, message: string }}
|
|
215
|
+
*/
|
|
216
|
+
export function uninstall() {
|
|
217
|
+
try {
|
|
218
|
+
if (!existsSync(PLIST_PATH)) {
|
|
219
|
+
return { success: true, message: 'Journal cron not installed (nothing to remove)' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
execFileSync('launchctl', ['unload', PLIST_PATH], {
|
|
224
|
+
timeout: 5000,
|
|
225
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
226
|
+
});
|
|
227
|
+
} catch {}
|
|
228
|
+
|
|
229
|
+
unlinkSync(PLIST_PATH);
|
|
230
|
+
return { success: true, message: 'Journal cron removed' };
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return { success: false, message: `Failed to remove journal cron: ${error.message}` };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { PLIST_PATH, LABEL, LOG_FILE, DEFAULT_HOUR, DEFAULT_MINUTE, formatTime };
|