@masslessai/push-todo 3.7.6 → 3.7.7
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/config.js +42 -1
- package/lib/daemon.js +102 -0
- package/lib/self-update.js +115 -0
- package/package.json +1 -1
package/lib/config.js
CHANGED
|
@@ -192,6 +192,27 @@ export function setAutoCompleteEnabled(enabled) {
|
|
|
192
192
|
return setConfigValue('AUTO_COMPLETE', enabled ? 'true' : 'false');
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Check if auto-update is enabled for daemon self-updates.
|
|
197
|
+
* Default: true (daemon checks npm hourly and updates when idle)
|
|
198
|
+
*
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
export function getAutoUpdateEnabled() {
|
|
202
|
+
const value = getConfigValue('AUTO_UPDATE', 'true');
|
|
203
|
+
return value.toLowerCase() === 'true' || value === '1' || value.toLowerCase() === 'yes';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Set auto-update setting.
|
|
208
|
+
*
|
|
209
|
+
* @param {boolean} enabled
|
|
210
|
+
* @returns {boolean} True if successful
|
|
211
|
+
*/
|
|
212
|
+
export function setAutoUpdateEnabled(enabled) {
|
|
213
|
+
return setConfigValue('AUTO_UPDATE', enabled ? 'true' : 'false');
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/**
|
|
196
217
|
* Get the maximum batch size for queuing tasks.
|
|
197
218
|
* Default: 5
|
|
@@ -270,6 +291,7 @@ export function showSettings() {
|
|
|
270
291
|
const autoCommit = getAutoCommitEnabled();
|
|
271
292
|
const autoMerge = getAutoMergeEnabled();
|
|
272
293
|
const autoComplete = getAutoCompleteEnabled();
|
|
294
|
+
const autoUpdate = getAutoUpdateEnabled();
|
|
273
295
|
const batchSize = getMaxBatchSize();
|
|
274
296
|
|
|
275
297
|
console.log(` auto-commit: ${autoCommit ? 'ON' : 'OFF'}`);
|
|
@@ -281,6 +303,9 @@ export function showSettings() {
|
|
|
281
303
|
console.log(` auto-complete: ${autoComplete ? 'ON' : 'OFF'}`);
|
|
282
304
|
console.log(' Mark task completed after successful merge');
|
|
283
305
|
console.log();
|
|
306
|
+
console.log(` auto-update: ${autoUpdate ? 'ON' : 'OFF'}`);
|
|
307
|
+
console.log(' Daemon auto-updates from npm when idle');
|
|
308
|
+
console.log();
|
|
284
309
|
console.log(` batch-size: ${batchSize}`);
|
|
285
310
|
console.log(' Max tasks for batch queue (1-20)');
|
|
286
311
|
console.log();
|
|
@@ -346,6 +371,22 @@ export function toggleSetting(settingName) {
|
|
|
346
371
|
return false;
|
|
347
372
|
}
|
|
348
373
|
|
|
374
|
+
if (normalized === 'auto-update') {
|
|
375
|
+
const current = getAutoUpdateEnabled();
|
|
376
|
+
const newValue = !current;
|
|
377
|
+
if (setAutoUpdateEnabled(newValue)) {
|
|
378
|
+
console.log(`Auto-update is now ${newValue ? 'ON' : 'OFF'}`);
|
|
379
|
+
if (newValue) {
|
|
380
|
+
console.log('Daemon will auto-update from npm when idle (hourly check).');
|
|
381
|
+
} else {
|
|
382
|
+
console.log('Daemon will NOT auto-update. Manual updates required.');
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
console.error('Failed to update setting');
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
349
390
|
if (normalized === 'batch-size') {
|
|
350
391
|
const batchSize = getMaxBatchSize();
|
|
351
392
|
console.log(`Current batch size: ${batchSize}`);
|
|
@@ -354,7 +395,7 @@ export function toggleSetting(settingName) {
|
|
|
354
395
|
}
|
|
355
396
|
|
|
356
397
|
console.error(`Unknown setting: ${settingName}`);
|
|
357
|
-
console.error('Available settings: auto-commit, auto-merge, auto-complete, batch-size');
|
|
398
|
+
console.error('Available settings: auto-commit, auto-merge, auto-complete, auto-update, batch-size');
|
|
358
399
|
return false;
|
|
359
400
|
}
|
|
360
401
|
|
package/lib/daemon.js
CHANGED
|
@@ -19,6 +19,8 @@ import { homedir, hostname, platform } from 'os';
|
|
|
19
19
|
import { join, dirname } from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
|
|
22
|
+
import { checkForUpdate, performUpdate } from './self-update.js';
|
|
23
|
+
|
|
22
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
25
|
const __dirname = dirname(__filename);
|
|
24
26
|
|
|
@@ -230,6 +232,48 @@ function getAutoCompleteEnabled() {
|
|
|
230
232
|
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
function getAutoUpdateEnabled() {
|
|
236
|
+
const v = getConfigValueFromFile('AUTO_UPDATE', 'true');
|
|
237
|
+
return v.toLowerCase() === 'true' || v === '1' || v.toLowerCase() === 'yes';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ==================== Capabilities Detection ====================
|
|
241
|
+
|
|
242
|
+
let cachedCapabilities = null;
|
|
243
|
+
let lastCapabilityCheck = 0;
|
|
244
|
+
const CAPABILITY_CHECK_INTERVAL = 3600000; // 1 hour
|
|
245
|
+
|
|
246
|
+
function detectCapabilities() {
|
|
247
|
+
const caps = {
|
|
248
|
+
auto_merge: getAutoMergeEnabled(),
|
|
249
|
+
auto_complete: getAutoCompleteEnabled(),
|
|
250
|
+
auto_update: getAutoUpdateEnabled(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
execFileSync('gh', ['--version'], { timeout: 5000, stdio: 'pipe' });
|
|
255
|
+
try {
|
|
256
|
+
execFileSync('gh', ['auth', 'status'], { timeout: 5000, stdio: 'pipe' });
|
|
257
|
+
caps.gh_cli = 'authenticated';
|
|
258
|
+
} catch {
|
|
259
|
+
caps.gh_cli = 'installed_not_authenticated';
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
caps.gh_cli = 'not_installed';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return caps;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getCapabilities() {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
if (!cachedCapabilities || now - lastCapabilityCheck > CAPABILITY_CHECK_INTERVAL) {
|
|
271
|
+
cachedCapabilities = detectCapabilities();
|
|
272
|
+
lastCapabilityCheck = now;
|
|
273
|
+
}
|
|
274
|
+
return cachedCapabilities;
|
|
275
|
+
}
|
|
276
|
+
|
|
233
277
|
// ==================== E2EE Decryption ====================
|
|
234
278
|
|
|
235
279
|
let decryptTodoField = null;
|
|
@@ -335,6 +379,8 @@ async function fetchQueuedTasks() {
|
|
|
335
379
|
heartbeatHeaders['X-Machine-Id'] = machineId;
|
|
336
380
|
heartbeatHeaders['X-Machine-Name'] = machineName || 'Unknown Mac';
|
|
337
381
|
heartbeatHeaders['X-Git-Remotes'] = gitRemotes.join(',');
|
|
382
|
+
heartbeatHeaders['X-Daemon-Version'] = getVersion();
|
|
383
|
+
heartbeatHeaders['X-Capabilities'] = JSON.stringify(getCapabilities());
|
|
338
384
|
}
|
|
339
385
|
|
|
340
386
|
const response = await apiRequest(`synced-todos?${params}`, {
|
|
@@ -1317,6 +1363,54 @@ async function checkTimeouts() {
|
|
|
1317
1363
|
}
|
|
1318
1364
|
}
|
|
1319
1365
|
|
|
1366
|
+
// ==================== Self-Update ====================
|
|
1367
|
+
|
|
1368
|
+
let pendingUpdateVersion = null;
|
|
1369
|
+
|
|
1370
|
+
function checkAndApplyUpdate() {
|
|
1371
|
+
const currentVersion = getVersion();
|
|
1372
|
+
|
|
1373
|
+
// Check for update (throttled internally to once per hour)
|
|
1374
|
+
if (!pendingUpdateVersion) {
|
|
1375
|
+
const result = checkForUpdate(currentVersion);
|
|
1376
|
+
if (result.available) {
|
|
1377
|
+
log(`Update available: v${currentVersion} -> v${result.version}`);
|
|
1378
|
+
pendingUpdateVersion = result.version;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Only apply when no tasks are running
|
|
1383
|
+
if (pendingUpdateVersion && runningTasks.size === 0) {
|
|
1384
|
+
log(`Applying update to v${pendingUpdateVersion}...`);
|
|
1385
|
+
sendMacNotification(
|
|
1386
|
+
'Push Daemon Updating',
|
|
1387
|
+
`v${currentVersion} → v${pendingUpdateVersion}`,
|
|
1388
|
+
'Glass'
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
const success = performUpdate(pendingUpdateVersion);
|
|
1392
|
+
if (success) {
|
|
1393
|
+
log(`Update to v${pendingUpdateVersion} successful. Restarting daemon...`);
|
|
1394
|
+
|
|
1395
|
+
// Spawn new daemon from updated code, then exit
|
|
1396
|
+
const daemonScript = join(__dirname, 'daemon.js');
|
|
1397
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
1398
|
+
detached: true,
|
|
1399
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1400
|
+
env: { ...process.env, PUSH_DAEMON: '1' }
|
|
1401
|
+
});
|
|
1402
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
1403
|
+
child.unref();
|
|
1404
|
+
|
|
1405
|
+
log(`New daemon spawned (PID: ${child.pid}). Old daemon exiting.`);
|
|
1406
|
+
process.exit(0);
|
|
1407
|
+
} else {
|
|
1408
|
+
logError(`Update to v${pendingUpdateVersion} failed, will retry next hour`);
|
|
1409
|
+
pendingUpdateVersion = null;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1320
1414
|
// ==================== Main Loop ====================
|
|
1321
1415
|
|
|
1322
1416
|
async function pollAndExecute() {
|
|
@@ -1364,6 +1458,9 @@ async function mainLoop() {
|
|
|
1364
1458
|
log(`Polling interval: ${POLL_INTERVAL / 1000}s`);
|
|
1365
1459
|
log(`Max concurrent tasks: ${MAX_CONCURRENT_TASKS}`);
|
|
1366
1460
|
log(`E2EE: ${e2eeAvailable ? 'Available' : 'Not available'}`);
|
|
1461
|
+
log(`Auto-update: ${getAutoUpdateEnabled() ? 'Enabled' : 'Disabled'}`);
|
|
1462
|
+
const caps = getCapabilities();
|
|
1463
|
+
log(`Capabilities: gh=${caps.gh_cli}, auto-merge=${caps.auto_merge}, auto-complete=${caps.auto_complete}`);
|
|
1367
1464
|
log(`Log file: ${LOG_FILE}`);
|
|
1368
1465
|
|
|
1369
1466
|
// Show registered projects
|
|
@@ -1399,6 +1496,11 @@ async function mainLoop() {
|
|
|
1399
1496
|
try {
|
|
1400
1497
|
await checkTimeouts();
|
|
1401
1498
|
await pollAndExecute();
|
|
1499
|
+
|
|
1500
|
+
// Self-update check (throttled to once per hour, only applies when idle)
|
|
1501
|
+
if (getAutoUpdateEnabled()) {
|
|
1502
|
+
checkAndApplyUpdate();
|
|
1503
|
+
}
|
|
1402
1504
|
} catch (error) {
|
|
1403
1505
|
logError(`Poll error: ${error.message}`);
|
|
1404
1506
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update module for Push daemon.
|
|
3
|
+
*
|
|
4
|
+
* Checks npm registry for newer versions and auto-updates.
|
|
5
|
+
* Safety: Only updates to versions published >1 hour ago.
|
|
6
|
+
* Throttle: Checks at most once per hour.
|
|
7
|
+
* Config: PUSH_AUTO_UPDATE (default true)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const PUSH_DIR = join(homedir(), '.push');
|
|
16
|
+
const LAST_UPDATE_CHECK_FILE = join(PUSH_DIR, 'last_update_check');
|
|
17
|
+
const UPDATE_CHECK_INTERVAL = 3600000; // 1 hour
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compare semver strings.
|
|
21
|
+
* @returns -1 if a < b, 0 if equal, 1 if a > b
|
|
22
|
+
*/
|
|
23
|
+
export function compareSemver(a, b) {
|
|
24
|
+
const pa = a.split('.').map(Number);
|
|
25
|
+
const pb = b.split('.').map(Number);
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
28
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch latest version info from npm registry.
|
|
35
|
+
* @returns {{ version: string, publishedAt: string | null }} or null on failure
|
|
36
|
+
*/
|
|
37
|
+
function fetchLatestVersionInfo() {
|
|
38
|
+
try {
|
|
39
|
+
const result = execFileSync('npm', ['view', '@masslessai/push-todo', '--json'], {
|
|
40
|
+
timeout: 15000,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
43
|
+
});
|
|
44
|
+
const data = JSON.parse(result);
|
|
45
|
+
const latest = data['dist-tags']?.latest || data.version;
|
|
46
|
+
return {
|
|
47
|
+
version: latest,
|
|
48
|
+
publishedAt: data.time?.[latest] || null
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if an update is available and safe to install.
|
|
57
|
+
* Throttled to once per hour. Enforces 1-hour age gate on new versions.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} currentVersion
|
|
60
|
+
* @returns {{ available: boolean, version?: string, reason?: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function checkForUpdate(currentVersion) {
|
|
63
|
+
// Throttle: check at most once per hour
|
|
64
|
+
if (existsSync(LAST_UPDATE_CHECK_FILE)) {
|
|
65
|
+
try {
|
|
66
|
+
const lastCheck = parseInt(readFileSync(LAST_UPDATE_CHECK_FILE, 'utf8').trim(), 10);
|
|
67
|
+
if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL) {
|
|
68
|
+
return { available: false, reason: 'throttled' };
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Record check time
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(PUSH_DIR, { recursive: true });
|
|
76
|
+
writeFileSync(LAST_UPDATE_CHECK_FILE, String(Date.now()));
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
const info = fetchLatestVersionInfo();
|
|
80
|
+
if (!info) {
|
|
81
|
+
return { available: false, reason: 'registry_unreachable' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Already up to date
|
|
85
|
+
if (compareSemver(currentVersion, info.version) >= 0) {
|
|
86
|
+
return { available: false, reason: 'up_to_date' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Safety: only update to versions published >1 hour ago
|
|
90
|
+
if (info.publishedAt) {
|
|
91
|
+
const publishedAge = Date.now() - new Date(info.publishedAt).getTime();
|
|
92
|
+
if (publishedAge < UPDATE_CHECK_INTERVAL) {
|
|
93
|
+
return { available: false, reason: 'too_recent', version: info.version };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { available: true, version: info.version };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Install a specific version globally.
|
|
102
|
+
* @param {string} targetVersion
|
|
103
|
+
* @returns {boolean} true if update succeeded
|
|
104
|
+
*/
|
|
105
|
+
export function performUpdate(targetVersion) {
|
|
106
|
+
try {
|
|
107
|
+
execFileSync('npm', ['install', '-g', `@masslessai/push-todo@${targetVersion}`], {
|
|
108
|
+
timeout: 120000,
|
|
109
|
+
stdio: 'pipe'
|
|
110
|
+
});
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|