nightytidy 0.3.0 → 0.3.2
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/package.json +1 -1
- package/src/agent/index.js +43 -0
- package/src/agent/service.js +36 -1
package/package.json
CHANGED
package/src/agent/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/agent/index.js
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { info, warn, debug } from '../logger.js';
|
|
6
7
|
import { getConfigDir, readConfig, writeConfig, ensureConfigDir } from './config.js';
|
|
@@ -1075,6 +1076,44 @@ export async function startAgent() {
|
|
|
1075
1076
|
currentProjectId = null;
|
|
1076
1077
|
}
|
|
1077
1078
|
|
|
1079
|
+
// ── Idle heartbeat: keeps Firestore agentStatus/current fresh ──────────
|
|
1080
|
+
// Sends agent_heartbeat to webhookIngest every 60s regardless of run state.
|
|
1081
|
+
// This powers the "Agent online" badge in the web app.
|
|
1082
|
+
let idleHeartbeatInterval = null;
|
|
1083
|
+
const IDLE_HEARTBEAT_MS = 60_000;
|
|
1084
|
+
|
|
1085
|
+
function startIdleHeartbeat() {
|
|
1086
|
+
// Send one immediately so the web app sees "online" within seconds
|
|
1087
|
+
sendIdleHeartbeat();
|
|
1088
|
+
idleHeartbeatInterval = setInterval(sendIdleHeartbeat, IDLE_HEARTBEAT_MS);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function sendIdleHeartbeat() {
|
|
1092
|
+
if (!firebaseAuth.isAuthenticated()) return;
|
|
1093
|
+
const projects = projectManager.listProjects().map(p => ({
|
|
1094
|
+
id: p.id, name: p.name, path: p.path,
|
|
1095
|
+
}));
|
|
1096
|
+
const isRunning = !!activeBridge;
|
|
1097
|
+
webhookDispatcher.dispatch('agent_heartbeat', {
|
|
1098
|
+
machineName: os.hostname(),
|
|
1099
|
+
version: AGENT_VERSION,
|
|
1100
|
+
state: isRunning ? 'running' : 'idle',
|
|
1101
|
+
agentStartedAt: Date.now() - (process.uptime() * 1000),
|
|
1102
|
+
projects,
|
|
1103
|
+
}, [{
|
|
1104
|
+
url: FIREBASE_WEBHOOK_URL,
|
|
1105
|
+
label: 'nightytidy.com',
|
|
1106
|
+
headers: firebaseAuth.getAuthHeader(),
|
|
1107
|
+
}]);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function stopIdleHeartbeat() {
|
|
1111
|
+
if (idleHeartbeatInterval) {
|
|
1112
|
+
clearInterval(idleHeartbeatInterval);
|
|
1113
|
+
idleHeartbeatInterval = null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1078
1117
|
// Preserve interrupted run state on shutdown
|
|
1079
1118
|
function saveInterruptedState() {
|
|
1080
1119
|
const current = runQueue.getCurrent();
|
|
@@ -1110,6 +1149,7 @@ export async function startAgent() {
|
|
|
1110
1149
|
const shutdown = async () => {
|
|
1111
1150
|
info('Agent shutting down...');
|
|
1112
1151
|
releaseKeepAwake();
|
|
1152
|
+
stopIdleHeartbeat();
|
|
1113
1153
|
saveInterruptedState();
|
|
1114
1154
|
poller.stop();
|
|
1115
1155
|
scheduler.stopAll();
|
|
@@ -1197,6 +1237,9 @@ export async function startAgent() {
|
|
|
1197
1237
|
}
|
|
1198
1238
|
}
|
|
1199
1239
|
|
|
1240
|
+
// Start idle heartbeat — keeps Firestore agentStatus/current fresh
|
|
1241
|
+
startIdleHeartbeat();
|
|
1242
|
+
|
|
1200
1243
|
// Print startup info
|
|
1201
1244
|
console.log(`\nNightyTidy Agent v${AGENT_VERSION}`);
|
|
1202
1245
|
console.log(`WebSocket: ws://127.0.0.1:${actualPort}`);
|
package/src/agent/service.js
CHANGED
|
@@ -87,7 +87,30 @@ export function unregisterService() {
|
|
|
87
87
|
|
|
88
88
|
// --- Platform implementations ---
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Windows: Use the Startup folder (no admin needed).
|
|
92
|
+
* Writes a .vbs wrapper script that launches the agent hidden (no console window).
|
|
93
|
+
* Falls back to schtasks if Startup folder isn't writable.
|
|
94
|
+
*/
|
|
95
|
+
const STARTUP_DIR = process.platform === 'win32'
|
|
96
|
+
? path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
97
|
+
: '';
|
|
98
|
+
const STARTUP_VBS = path.join(STARTUP_DIR, 'NightyTidy Agent.vbs');
|
|
99
|
+
|
|
90
100
|
function _registerWindows(cmd) {
|
|
101
|
+
// Primary: Startup folder (no admin required)
|
|
102
|
+
try {
|
|
103
|
+
// VBS wrapper runs the agent without showing a console window
|
|
104
|
+
const vbs = `' NightyTidy Agent — auto-start on login\r\nSet ws = CreateObject("WScript.Shell")\r\nws.Run ${JSON.stringify(cmd)}, 0, False\r\n`;
|
|
105
|
+
fs.mkdirSync(STARTUP_DIR, { recursive: true });
|
|
106
|
+
fs.writeFileSync(STARTUP_VBS, vbs, 'utf-8');
|
|
107
|
+
debug('Registered via Windows Startup folder');
|
|
108
|
+
return;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
debug(`Startup folder failed: ${err.message}, trying schtasks`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fallback: Task Scheduler (needs admin on some systems)
|
|
91
114
|
execSync(
|
|
92
115
|
`schtasks /create /tn "${SERVICE_NAME}" /tr "${cmd}" /sc onlogon /rl LIMITED /f`,
|
|
93
116
|
{ stdio: 'pipe' },
|
|
@@ -95,7 +118,19 @@ function _registerWindows(cmd) {
|
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
function _unregisterWindows() {
|
|
98
|
-
|
|
121
|
+
// Remove Startup folder entry
|
|
122
|
+
try {
|
|
123
|
+
if (fs.existsSync(STARTUP_VBS)) {
|
|
124
|
+
fs.unlinkSync(STARTUP_VBS);
|
|
125
|
+
debug('Removed Startup folder entry');
|
|
126
|
+
}
|
|
127
|
+
} catch { /* ignore */ }
|
|
128
|
+
|
|
129
|
+
// Also try removing schtasks entry (may not exist)
|
|
130
|
+
try {
|
|
131
|
+
execSync(`schtasks /delete /tn "${SERVICE_NAME}" /f`, { stdio: 'pipe' });
|
|
132
|
+
debug('Removed Task Scheduler entry');
|
|
133
|
+
} catch { /* ignore — may not have been registered via schtasks */ }
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
function _registerMacOS(cmd) {
|