@masslessai/push-todo 4.2.0 → 4.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.
@@ -20,6 +20,38 @@ import { discoverAllProjects } from './discovery.js';
20
20
  import { createSpinner } from './utils/spinner.js';
21
21
  import { bold, green, red, dim, cyan } from './utils/colors.js';
22
22
  import { ensureDaemonRunning } from './daemon-health.js';
23
+ import { install as installLaunchAgent, getStatus as getLaunchAgentStatus } from './launchagent.js';
24
+
25
+ /**
26
+ * Install or update LaunchAgent (macOS only).
27
+ * Called at the end of auto-connect, even for early-exit paths.
28
+ */
29
+ function installLaunchAgentIfNeeded() {
30
+ if (process.platform !== 'darwin') return;
31
+
32
+ const laStatus = getLaunchAgentStatus();
33
+ if (!laStatus.installed) {
34
+ const laSpinner = createSpinner();
35
+ laSpinner.start('Installing LaunchAgent for auto-start on login...');
36
+
37
+ const laResult = installLaunchAgent();
38
+ if (laResult.success) {
39
+ laSpinner.succeed('LaunchAgent installed — daemon starts automatically on login');
40
+ } else {
41
+ laSpinner.fail(`LaunchAgent: ${laResult.message}`);
42
+ console.log(` ${dim('Daemon will still self-heal via push-todo commands.')}`);
43
+ }
44
+ } else {
45
+ // Already installed — ensure it's loaded and up to date
46
+ const laResult = installLaunchAgent();
47
+ if (laResult.alreadyInstalled) {
48
+ console.log(` ${green('✓')} LaunchAgent already configured`);
49
+ } else if (laResult.success) {
50
+ console.log(` ${green('✓')} LaunchAgent updated`);
51
+ }
52
+ }
53
+ console.log('');
54
+ }
23
55
 
24
56
  /**
25
57
  * Run the auto-connect flow.
@@ -98,6 +130,8 @@ export async function runAutoConnect(options = {}) {
98
130
  console.log(' No projects with git remotes found.');
99
131
  console.log(` ${dim('Projects must have a git remote to be registered.')}`);
100
132
  console.log('');
133
+ // Still install LaunchAgent even with no projects
134
+ installLaunchAgentIfNeeded();
101
135
  return;
102
136
  }
103
137
 
@@ -127,6 +161,8 @@ export async function runAutoConnect(options = {}) {
127
161
  console.log(` ${'='.repeat(40)}`);
128
162
  console.log(` All ${alreadyConnected} projects already connected.`);
129
163
  console.log('');
164
+ // Still install LaunchAgent even when all projects connected
165
+ installLaunchAgentIfNeeded();
130
166
  return;
131
167
  }
132
168
 
@@ -192,4 +228,7 @@ export async function runAutoConnect(options = {}) {
192
228
  console.log(` Run ${cyan("'push-todo'")} to see your tasks.`);
193
229
  }
194
230
  console.log('');
231
+
232
+ // Phase 6: Install LaunchAgent (macOS only)
233
+ installLaunchAgentIfNeeded();
195
234
  }
package/lib/cli.js CHANGED
@@ -15,6 +15,7 @@ import { runConnect } from './connect.js';
15
15
  import { startWatch } from './watch.js';
16
16
  import { showSettings, toggleSetting, setMaxBatchSize } from './config.js';
17
17
  import { ensureDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './daemon-health.js';
18
+ import { install as installLaunchAgent, uninstall as uninstallLaunchAgent, getStatus as getLaunchAgentStatus } from './launchagent.js';
18
19
  import { getScreenshotPath, screenshotExists, openScreenshot } from './utils/screenshots.js';
19
20
  import { bold, red, cyan, dim, green } from './utils/colors.js';
20
21
  import { getMachineId } from './machine-id.js';
@@ -79,6 +80,8 @@ ${bold('OPTIONS:')}
79
80
  --daemon-status Show daemon status
80
81
  --daemon-start Start daemon manually
81
82
  --daemon-stop Stop daemon
83
+ --daemon-install Install LaunchAgent (auto-start on login)
84
+ --daemon-uninstall Remove LaunchAgent
82
85
  --commands Show available user commands
83
86
  --json Output as JSON
84
87
  --version, -v Show version
@@ -155,6 +158,8 @@ const options = {
155
158
  'daemon-status': { type: 'boolean' },
156
159
  'daemon-start': { type: 'boolean' },
157
160
  'daemon-stop': { type: 'boolean' },
161
+ 'daemon-install': { type: 'boolean' },
162
+ 'daemon-uninstall': { type: 'boolean' },
158
163
  'commands': { type: 'boolean' },
159
164
  'json': { type: 'boolean' },
160
165
  'version': { type: 'boolean', short: 'v' },
@@ -254,6 +259,14 @@ export async function run(argv) {
254
259
  } else {
255
260
  console.log(`${bold('Daemon:')} Not running`);
256
261
  }
262
+ // Show LaunchAgent status
263
+ const laStatus = getLaunchAgentStatus();
264
+ if (laStatus.installed) {
265
+ console.log(`${bold('LaunchAgent:')} Installed${laStatus.loaded ? ' (loaded)' : ' (not loaded)'}`);
266
+ } else {
267
+ console.log(`${bold('LaunchAgent:')} Not installed`);
268
+ console.log(dim(' Run: push-todo --daemon-install'));
269
+ }
257
270
  return;
258
271
  }
259
272
 
@@ -281,6 +294,11 @@ export async function run(argv) {
281
294
  const success = stopDaemon();
282
295
  if (success) {
283
296
  console.log('Daemon stopped');
297
+ const laStatus = getLaunchAgentStatus();
298
+ if (laStatus.installed && laStatus.loaded) {
299
+ console.log(dim('Note: LaunchAgent will restart the daemon automatically.'));
300
+ console.log(dim('To fully stop: push-todo --daemon-uninstall'));
301
+ }
284
302
  } else {
285
303
  console.error(red('Failed to stop daemon'));
286
304
  process.exit(1);
@@ -289,6 +307,35 @@ export async function run(argv) {
289
307
  return;
290
308
  }
291
309
 
310
+ if (values['daemon-install']) {
311
+ const result = installLaunchAgent();
312
+ if (result.success) {
313
+ if (result.alreadyInstalled) {
314
+ console.log('LaunchAgent already installed');
315
+ } else {
316
+ console.log('LaunchAgent installed — daemon will start automatically on login');
317
+ }
318
+ const laStatus = getLaunchAgentStatus();
319
+ console.log(dim(`Plist: ${laStatus.plistPath}`));
320
+ } else {
321
+ console.error(red(result.message));
322
+ process.exit(1);
323
+ }
324
+ return;
325
+ }
326
+
327
+ if (values['daemon-uninstall']) {
328
+ const result = uninstallLaunchAgent();
329
+ if (result.success) {
330
+ console.log(result.message);
331
+ console.log(dim('Daemon will still self-heal via push-todo commands.'));
332
+ } else {
333
+ console.error(red(result.message));
334
+ process.exit(1);
335
+ }
336
+ return;
337
+ }
338
+
292
339
  // Handle --commands (simple user help)
293
340
  if (values.commands) {
294
341
  console.log(`
package/lib/daemon.js CHANGED
@@ -27,6 +27,7 @@ import { checkAndRunDueJobs } from './cron.js';
27
27
  import { runHeartbeatChecks } from './heartbeat.js';
28
28
  import { getAgentVersions, formatAgentVersionSummary, checkAllAgentUpdates, performAgentUpdate, checkVersionParity } from './agent-versions.js';
29
29
  import { checkAllProjectsFreshness } from './project-freshness.js';
30
+ import { getStatus as getLaunchAgentStatus, install as refreshLaunchAgent } from './launchagent.js';
30
31
 
31
32
  const __filename = fileURLToPath(import.meta.url);
32
33
  const __dirname = dirname(__filename);
@@ -2918,7 +2919,16 @@ function checkAndApplyUpdate() {
2918
2919
  if (success) {
2919
2920
  log(`Update to v${pendingUpdateVersion} successful. Restarting daemon...`);
2920
2921
 
2921
- // Spawn new daemon from updated code, then exit
2922
+ // If LaunchAgent is installed, refresh plist (paths may have changed)
2923
+ // and let launchd handle the restart instead of spawning manually
2924
+ const laStatus = getLaunchAgentStatus();
2925
+ if (laStatus.installed && laStatus.loaded) {
2926
+ refreshLaunchAgent(); // Update plist with current node/daemon paths
2927
+ log('LaunchAgent installed — letting launchd restart daemon.');
2928
+ process.exit(0);
2929
+ }
2930
+
2931
+ // No LaunchAgent — spawn new daemon manually, then exit
2922
2932
  const daemonScript = join(__dirname, 'daemon.js');
2923
2933
  const selfUpdateEnv = { ...process.env, PUSH_DAEMON: '1' };
2924
2934
  delete selfUpdateEnv.CLAUDECODE; // Strip to avoid leaking into Claude child processes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -407,11 +407,11 @@ async function main() {
407
407
  }
408
408
  console.log('');
409
409
  console.log('[push-todo] Quick start:');
410
- console.log('[push-todo] push-todo connect Set up authentication');
411
- console.log('[push-todo] push-todo List your tasks');
412
- if (claudeSuccess) console.log('[push-todo] /push-todo Use in Claude Code');
413
- if (codexSuccess) console.log('[push-todo] $push-todo Use in OpenAI Codex');
414
- if (openclawSuccess) console.log('[push-todo] /push-todo Use in OpenClaw');
410
+ console.log('[push-todo] push-todo connect --auto One-command setup (auth + projects + daemon)');
411
+ console.log('[push-todo] push-todo List your tasks');
412
+ if (claudeSuccess) console.log('[push-todo] /push-todo Use in Claude Code');
413
+ if (codexSuccess) console.log('[push-todo] $push-todo Use in OpenAI Codex');
414
+ if (openclawSuccess) console.log('[push-todo] /push-todo Use in OpenClaw');
415
415
  return;
416
416
  }
417
417
 
@@ -440,16 +440,16 @@ async function main() {
440
440
  }
441
441
  console.log('');
442
442
  console.log('[push-todo] Quick start:');
443
- console.log('[push-todo] push-todo connect Set up authentication');
444
- console.log('[push-todo] push-todo List your tasks');
443
+ console.log('[push-todo] push-todo connect --auto One-command setup (auth + projects + daemon)');
444
+ console.log('[push-todo] push-todo List your tasks');
445
445
  if (claudeSuccess) {
446
- console.log('[push-todo] /push-todo Use in Claude Code');
446
+ console.log('[push-todo] /push-todo Use in Claude Code');
447
447
  }
448
448
  if (codexSuccess) {
449
- console.log('[push-todo] $push-todo Use in OpenAI Codex');
449
+ console.log('[push-todo] $push-todo Use in OpenAI Codex');
450
450
  }
451
451
  if (openclawSuccess) {
452
- console.log('[push-todo] /push-todo Use in OpenClaw');
452
+ console.log('[push-todo] /push-todo Use in OpenClaw');
453
453
  }
454
454
  }
455
455