nstantpage-agent 0.5.21 → 0.5.22
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/dist/agentSync.d.ts +85 -0
- package/dist/agentSync.js +590 -0
- package/dist/cli.js +1 -1
- package/dist/commands/service.js +219 -5
- package/dist/commands/start.js +43 -2
- package/dist/index.d.ts +1 -1
- package/dist/localServer.d.ts +9 -0
- package/dist/localServer.js +112 -5
- package/dist/statusServer.d.ts +60 -0
- package/dist/statusServer.js +117 -0
- package/dist/tunnel.d.ts +20 -0
- package/dist/tunnel.js +85 -9
- package/package.json +1 -1
package/dist/commands/service.js
CHANGED
|
@@ -24,6 +24,7 @@ import { getConfig } from '../config.js';
|
|
|
24
24
|
import { startCommand } from './start.js';
|
|
25
25
|
const PLIST_LABEL = 'com.nstantpage.agent';
|
|
26
26
|
const SYSTEMD_SERVICE = 'nstantpage-agent';
|
|
27
|
+
const WIN_TASK_NAME = 'NstantpageAgent';
|
|
27
28
|
/**
|
|
28
29
|
* `nstantpage service start` — run agent in standby mode (foreground).
|
|
29
30
|
* This keeps your machine online and visible from the web UI.
|
|
@@ -76,6 +77,14 @@ export async function serviceStopCommand() {
|
|
|
76
77
|
}
|
|
77
78
|
catch { }
|
|
78
79
|
}
|
|
80
|
+
else if (platform === 'win32') {
|
|
81
|
+
try {
|
|
82
|
+
execSync(`schtasks /end /tn "${WIN_TASK_NAME}" 2>nul`, { encoding: 'utf-8' });
|
|
83
|
+
console.log(chalk.green(' ✓ Background task stopped'));
|
|
84
|
+
stopped = true;
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
}
|
|
79
88
|
// Also try PID-based stop from global config
|
|
80
89
|
const conf = getConfig();
|
|
81
90
|
const pid = conf.get('agentPid');
|
|
@@ -103,16 +112,35 @@ export async function serviceInstallCommand(options = {}) {
|
|
|
103
112
|
}
|
|
104
113
|
const gateway = options.gateway || 'wss://webprev.live';
|
|
105
114
|
const platform = os.platform();
|
|
115
|
+
// On desktop platforms, prefer the Electron tray app (agent + tray icon in one)
|
|
116
|
+
const electronAppPath = findElectronApp();
|
|
117
|
+
if (electronAppPath && (platform === 'darwin' || platform === 'win32')) {
|
|
118
|
+
console.log(chalk.blue(`\n Found nstantpage desktop app: ${electronAppPath}`));
|
|
119
|
+
console.log(chalk.blue(' Installing with system tray icon...\n'));
|
|
120
|
+
if (platform === 'darwin') {
|
|
121
|
+
await installElectronLaunchd(electronAppPath);
|
|
122
|
+
}
|
|
123
|
+
else if (platform === 'win32') {
|
|
124
|
+
await installElectronWindowsTask(electronAppPath);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Fallback: headless service (no tray icon)
|
|
129
|
+
if (electronAppPath === null) {
|
|
130
|
+
console.log(chalk.gray(' Tip: Install the nstantpage desktop app for a system tray icon.'));
|
|
131
|
+
}
|
|
106
132
|
if (platform === 'darwin') {
|
|
107
133
|
await installLaunchd(gateway, token);
|
|
108
134
|
}
|
|
109
135
|
else if (platform === 'linux') {
|
|
110
136
|
await installSystemd(gateway, token);
|
|
111
137
|
}
|
|
138
|
+
else if (platform === 'win32') {
|
|
139
|
+
await installWindowsTask(gateway, token);
|
|
140
|
+
}
|
|
112
141
|
else {
|
|
113
142
|
console.log(chalk.yellow('⚠ Background service not yet supported on this platform.'));
|
|
114
143
|
console.log(chalk.gray(' Use "nstantpage start --project-id X" to run manually.'));
|
|
115
|
-
console.log(chalk.gray(' Windows support coming soon.'));
|
|
116
144
|
process.exit(1);
|
|
117
145
|
}
|
|
118
146
|
}
|
|
@@ -124,6 +152,9 @@ export async function serviceUninstallCommand() {
|
|
|
124
152
|
else if (platform === 'linux') {
|
|
125
153
|
await uninstallSystemd();
|
|
126
154
|
}
|
|
155
|
+
else if (platform === 'win32') {
|
|
156
|
+
await uninstallWindowsTask();
|
|
157
|
+
}
|
|
127
158
|
else {
|
|
128
159
|
console.log(chalk.yellow(' ⚠ Service uninstall not supported on this platform'));
|
|
129
160
|
}
|
|
@@ -158,6 +189,22 @@ export async function serviceStatusCommand() {
|
|
|
158
189
|
console.log(chalk.yellow(' ⚠ Service is not running or not installed'));
|
|
159
190
|
}
|
|
160
191
|
}
|
|
192
|
+
else if (platform === 'win32') {
|
|
193
|
+
try {
|
|
194
|
+
const result = execSync(`schtasks /query /tn "${WIN_TASK_NAME}" /fo CSV /nh 2>nul`, { encoding: 'utf-8' }).trim();
|
|
195
|
+
if (result) {
|
|
196
|
+
const parts = result.split('","');
|
|
197
|
+
const status = parts[2]?.replace(/"/g, '') || 'Unknown';
|
|
198
|
+
console.log(chalk.green(` ✓ Task is installed (status: ${status})`));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log(chalk.yellow(' ⚠ Task is not installed'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
console.log(chalk.yellow(' ⚠ Task is not installed'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
161
208
|
else {
|
|
162
209
|
console.log(chalk.gray(' Service status not available on this platform'));
|
|
163
210
|
}
|
|
@@ -178,6 +225,114 @@ export async function serviceStatusCommand() {
|
|
|
178
225
|
}
|
|
179
226
|
}
|
|
180
227
|
// ─── Helper Functions ─────────────────────────────────
|
|
228
|
+
// ─── Electron App Detection ──────────────────────────────
|
|
229
|
+
function findElectronApp() {
|
|
230
|
+
const platform = os.platform();
|
|
231
|
+
if (platform === 'darwin') {
|
|
232
|
+
// Check common macOS install paths
|
|
233
|
+
const candidates = [
|
|
234
|
+
'/Applications/nstantpage.app',
|
|
235
|
+
path.join(os.homedir(), 'Applications', 'nstantpage.app'),
|
|
236
|
+
// Dev build location (from electron-builder --dir)
|
|
237
|
+
path.join(__dirname, '..', '..', 'tray', 'dist', 'mac-arm64', 'nstantpage.app'),
|
|
238
|
+
path.join(__dirname, '..', '..', 'tray', 'dist', 'mac', 'nstantpage.app'),
|
|
239
|
+
];
|
|
240
|
+
for (const p of candidates) {
|
|
241
|
+
if (fs.existsSync(p))
|
|
242
|
+
return p;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (platform === 'win32') {
|
|
246
|
+
const candidates = [
|
|
247
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'nstantpage', 'nstantpage.exe'),
|
|
248
|
+
path.join('C:\\Program Files', 'nstantpage', 'nstantpage.exe'),
|
|
249
|
+
];
|
|
250
|
+
for (const p of candidates) {
|
|
251
|
+
if (fs.existsSync(p))
|
|
252
|
+
return p;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
async function installElectronLaunchd(appPath) {
|
|
258
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
259
|
+
const plistPath = path.join(plistDir, `${PLIST_LABEL}.plist`);
|
|
260
|
+
const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
|
|
261
|
+
const errPath = path.join(os.homedir(), '.nstantpage', 'agent.err.log');
|
|
262
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
263
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
264
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
265
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
266
|
+
<plist version="1.0">
|
|
267
|
+
<dict>
|
|
268
|
+
<key>Label</key>
|
|
269
|
+
<string>${PLIST_LABEL}</string>
|
|
270
|
+
<key>ProgramArguments</key>
|
|
271
|
+
<array>
|
|
272
|
+
<string>/usr/bin/open</string>
|
|
273
|
+
<string>-a</string>
|
|
274
|
+
<string>${appPath}</string>
|
|
275
|
+
</array>
|
|
276
|
+
<key>RunAtLoad</key>
|
|
277
|
+
<true/>
|
|
278
|
+
<key>KeepAlive</key>
|
|
279
|
+
<dict>
|
|
280
|
+
<key>Crashed</key>
|
|
281
|
+
<true/>
|
|
282
|
+
</dict>
|
|
283
|
+
<key>ProcessType</key>
|
|
284
|
+
<string>Interactive</string>
|
|
285
|
+
<key>StandardOutPath</key>
|
|
286
|
+
<string>${logPath}</string>
|
|
287
|
+
<key>StandardErrorPath</key>
|
|
288
|
+
<string>${errPath}</string>
|
|
289
|
+
<key>EnvironmentVariables</key>
|
|
290
|
+
<dict>
|
|
291
|
+
<key>NSAppSleepDisabled</key>
|
|
292
|
+
<string>YES</string>
|
|
293
|
+
</dict>
|
|
294
|
+
<key>ThrottleInterval</key>
|
|
295
|
+
<integer>10</integer>
|
|
296
|
+
</dict>
|
|
297
|
+
</plist>`;
|
|
298
|
+
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
299
|
+
try {
|
|
300
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
|
|
301
|
+
}
|
|
302
|
+
catch { }
|
|
303
|
+
execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8' });
|
|
304
|
+
console.log(chalk.green('\n ✓ nstantpage installed as desktop app with tray icon\n'));
|
|
305
|
+
console.log(chalk.gray(` App: ${appPath}`));
|
|
306
|
+
console.log(chalk.gray(` Plist: ${plistPath}`));
|
|
307
|
+
console.log(chalk.gray(` Log: ${logPath}`));
|
|
308
|
+
console.log(chalk.gray(` Status: nstantpage service status`));
|
|
309
|
+
console.log(chalk.gray(` Remove: nstantpage service uninstall\n`));
|
|
310
|
+
console.log(chalk.blue(' The app will start on login with a tray icon showing connection status.'));
|
|
311
|
+
console.log(chalk.blue(' Right-click the tray icon for options. Open nstantpage.com to connect projects.\n'));
|
|
312
|
+
}
|
|
313
|
+
async function installElectronWindowsTask(appPath) {
|
|
314
|
+
const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
|
|
315
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
316
|
+
try {
|
|
317
|
+
execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f 2>nul`, { encoding: 'utf-8' });
|
|
318
|
+
}
|
|
319
|
+
catch { }
|
|
320
|
+
try {
|
|
321
|
+
execSync(`schtasks /create /tn "${WIN_TASK_NAME}" /tr "\\"${appPath}\\"" /sc onlogon /rl highest /delay 0000:10 /f`, { encoding: 'utf-8' });
|
|
322
|
+
// Start immediately
|
|
323
|
+
execSync(`start "" "${appPath}"`, { encoding: 'utf-8' });
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
console.log(chalk.yellow(` ⚠ Task Scheduler error: ${err.message}`));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
console.log(chalk.green('\n ✓ nstantpage installed as desktop app with tray icon\n'));
|
|
330
|
+
console.log(chalk.gray(` App: ${appPath}`));
|
|
331
|
+
console.log(chalk.gray(` Task: ${WIN_TASK_NAME}`));
|
|
332
|
+
console.log(chalk.gray(` Status: nstantpage service status`));
|
|
333
|
+
console.log(chalk.gray(` Remove: nstantpage service uninstall\n`));
|
|
334
|
+
console.log(chalk.blue(' The app will start on login with a tray icon.\n'));
|
|
335
|
+
}
|
|
181
336
|
function getAgentBinPath() {
|
|
182
337
|
try {
|
|
183
338
|
const resolved = execSync('which nstantpage 2>/dev/null || which nstantpage-agent 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
@@ -233,10 +388,9 @@ async function installLaunchd(gateway, token) {
|
|
|
233
388
|
<key>RunAtLoad</key>
|
|
234
389
|
<true/>
|
|
235
390
|
<key>KeepAlive</key>
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
</dict>
|
|
391
|
+
<true/>
|
|
392
|
+
<key>ProcessType</key>
|
|
393
|
+
<string>Interactive</string>
|
|
240
394
|
<key>StandardOutPath</key>
|
|
241
395
|
<string>${logPath}</string>
|
|
242
396
|
<key>StandardErrorPath</key>
|
|
@@ -245,9 +399,13 @@ async function installLaunchd(gateway, token) {
|
|
|
245
399
|
<dict>
|
|
246
400
|
<key>PATH</key>
|
|
247
401
|
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
402
|
+
<key>NSAppSleepDisabled</key>
|
|
403
|
+
<string>YES</string>
|
|
248
404
|
</dict>
|
|
249
405
|
<key>ThrottleInterval</key>
|
|
250
406
|
<integer>10</integer>
|
|
407
|
+
<key>Nice</key>
|
|
408
|
+
<integer>-5</integer>
|
|
251
409
|
</dict>
|
|
252
410
|
</plist>`;
|
|
253
411
|
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
@@ -337,4 +495,60 @@ async function uninstallSystemd() {
|
|
|
337
495
|
console.log(chalk.green(' ✓ Agent service uninstalled (systemd)'));
|
|
338
496
|
console.log(chalk.gray(' The agent will no longer start automatically.'));
|
|
339
497
|
}
|
|
498
|
+
// ─── Windows (Task Scheduler) ────────────────────────────
|
|
499
|
+
async function installWindowsTask(gateway, token) {
|
|
500
|
+
const binPath = getAgentBinPath();
|
|
501
|
+
const nodePath = getNodePath();
|
|
502
|
+
const logPath = path.join(os.homedir(), '.nstantpage', 'agent.log');
|
|
503
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
504
|
+
// Create a wrapper batch script that redirects output to log
|
|
505
|
+
const batchDir = path.join(os.homedir(), '.nstantpage');
|
|
506
|
+
const batchPath = path.join(batchDir, 'agent-service.cmd');
|
|
507
|
+
const batchContent = `@echo off\r\n"${nodePath}" "${binPath}" start --gateway ${gateway} --token ${token} >> "${logPath}" 2>&1\r\n`;
|
|
508
|
+
fs.writeFileSync(batchPath, batchContent, 'utf-8');
|
|
509
|
+
// Remove existing task if any
|
|
510
|
+
try {
|
|
511
|
+
execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f 2>nul`, { encoding: 'utf-8' });
|
|
512
|
+
}
|
|
513
|
+
catch { }
|
|
514
|
+
// Create a scheduled task that runs at logon and restarts on failure
|
|
515
|
+
// /sc onlogon — runs when any user logs on
|
|
516
|
+
// /rl highest — run with highest privileges available to user
|
|
517
|
+
// /delay 0000:10 — 10 second delay after trigger
|
|
518
|
+
try {
|
|
519
|
+
execSync(`schtasks /create /tn "${WIN_TASK_NAME}" /tr "\\"${batchPath}\\"" /sc onlogon /rl highest /delay 0000:10 /f`, { encoding: 'utf-8' });
|
|
520
|
+
// Start it immediately
|
|
521
|
+
execSync(`schtasks /run /tn "${WIN_TASK_NAME}"`, { encoding: 'utf-8' });
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
console.log(chalk.yellow(` ⚠ Task Scheduler error: ${err.message}`));
|
|
525
|
+
console.log(chalk.gray(' You may need to run this command as Administrator.'));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
console.log(chalk.green('\n ✓ Agent installed as scheduled task (Windows Task Scheduler)\n'));
|
|
529
|
+
console.log(chalk.gray(` Task: ${WIN_TASK_NAME}`));
|
|
530
|
+
console.log(chalk.gray(` Script: ${batchPath}`));
|
|
531
|
+
console.log(chalk.gray(` Log: ${logPath}`));
|
|
532
|
+
console.log(chalk.gray(` Status: nstantpage service status`));
|
|
533
|
+
console.log(chalk.gray(` Remove: nstantpage service uninstall\n`));
|
|
534
|
+
console.log(chalk.blue(' The agent will start on login and stay connected to your account.'));
|
|
535
|
+
console.log(chalk.blue(' Open any project on nstantpage.com and click "Connect" to use it.\n'));
|
|
536
|
+
}
|
|
537
|
+
async function uninstallWindowsTask() {
|
|
538
|
+
try {
|
|
539
|
+
execSync(`schtasks /delete /tn "${WIN_TASK_NAME}" /f 2>nul`, { encoding: 'utf-8' });
|
|
540
|
+
console.log(chalk.green(' ✓ Agent task removed (Windows Task Scheduler)'));
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
console.log(chalk.yellow(' ⚠ Task is not installed or could not be removed'));
|
|
544
|
+
}
|
|
545
|
+
// Clean up batch script
|
|
546
|
+
const batchPath = path.join(os.homedir(), '.nstantpage', 'agent-service.cmd');
|
|
547
|
+
try {
|
|
548
|
+
if (fs.existsSync(batchPath))
|
|
549
|
+
fs.unlinkSync(batchPath);
|
|
550
|
+
}
|
|
551
|
+
catch { }
|
|
552
|
+
console.log(chalk.gray(' The agent will no longer start automatically.'));
|
|
553
|
+
}
|
|
340
554
|
//# sourceMappingURL=service.js.map
|
package/dist/commands/start.js
CHANGED
|
@@ -25,7 +25,8 @@ import { TunnelClient } from '../tunnel.js';
|
|
|
25
25
|
import { LocalServer } from '../localServer.js';
|
|
26
26
|
import { PackageInstaller } from '../packageInstaller.js';
|
|
27
27
|
import { probeLocalPostgres, ensureLocalProjectDb, closeAdminPool, writeDatabaseUrlToEnv } from '../projectDb.js';
|
|
28
|
-
|
|
28
|
+
import { StatusServer } from '../statusServer.js';
|
|
29
|
+
const VERSION = '0.5.22';
|
|
29
30
|
/**
|
|
30
31
|
* Resolve the backend API base URL.
|
|
31
32
|
* - If --backend is passed, use it
|
|
@@ -296,15 +297,24 @@ export async function startCommand(directory, options) {
|
|
|
296
297
|
serverEnv['DATABASE_URL'] = databaseUrl;
|
|
297
298
|
}
|
|
298
299
|
// 3. Start local server (API + dev server)
|
|
300
|
+
// Note: tunnel is created after localServer but before localServer.start(),
|
|
301
|
+
// so the onSyncDirty callback captures it via closure (late-binding).
|
|
302
|
+
let tunnel;
|
|
299
303
|
const localServer = new LocalServer({
|
|
300
304
|
projectDir,
|
|
301
305
|
projectId,
|
|
302
306
|
apiPort,
|
|
303
307
|
devPort,
|
|
304
308
|
env: serverEnv,
|
|
309
|
+
backendUrl,
|
|
310
|
+
onSyncDirty: (pid) => {
|
|
311
|
+
if (tunnel) {
|
|
312
|
+
tunnel.sendSyncDirty(pid);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
305
315
|
});
|
|
306
316
|
// 4. Create tunnel client
|
|
307
|
-
|
|
317
|
+
tunnel = new TunnelClient({
|
|
308
318
|
gatewayUrl: options.gateway,
|
|
309
319
|
token,
|
|
310
320
|
projectId,
|
|
@@ -570,9 +580,39 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
|
|
|
570
580
|
}
|
|
571
581
|
catch { }
|
|
572
582
|
}, 60_000);
|
|
583
|
+
// Start status server (for tray app IPC)
|
|
584
|
+
const startedAt = new Date().toISOString();
|
|
585
|
+
let shutdownFn = null;
|
|
586
|
+
const statusServer = new StatusServer(() => ({
|
|
587
|
+
tunnelStatus: standbyTunnel.status,
|
|
588
|
+
version: VERSION,
|
|
589
|
+
hostname: os.hostname(),
|
|
590
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
591
|
+
uptime: Date.now() - Date.parse(startedAt),
|
|
592
|
+
startedAt,
|
|
593
|
+
activeProjects: Array.from(activeProjects.entries()).map(([pid, proj]) => ({
|
|
594
|
+
projectId: pid,
|
|
595
|
+
devPort: allocatePortsForProject(pid).devPort,
|
|
596
|
+
apiPort: allocatePortsForProject(pid).apiPort,
|
|
597
|
+
tunnelStatus: proj.tunnel.status,
|
|
598
|
+
})),
|
|
599
|
+
requestsForwarded: standbyTunnel.stats.requestsForwarded +
|
|
600
|
+
Array.from(activeProjects.values()).reduce((sum, p) => sum + p.tunnel.stats.requestsForwarded, 0),
|
|
601
|
+
deviceId,
|
|
602
|
+
gatewayUrl: options.gateway,
|
|
603
|
+
}), async () => { if (shutdownFn)
|
|
604
|
+
await shutdownFn(); });
|
|
605
|
+
try {
|
|
606
|
+
await statusServer.start();
|
|
607
|
+
console.log(chalk.green(` ✓ Status server on port 18999`));
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
console.log(chalk.gray(` ⚠ Status server: ${err.message} (non-fatal)`));
|
|
611
|
+
}
|
|
573
612
|
// Shutdown handler
|
|
574
613
|
const shutdown = async () => {
|
|
575
614
|
console.log(chalk.yellow('\n Shutting down...'));
|
|
615
|
+
await statusServer.stop();
|
|
576
616
|
standbyTunnel.disconnect();
|
|
577
617
|
for (const [, proj] of activeProjects) {
|
|
578
618
|
proj.tunnel.disconnect();
|
|
@@ -589,6 +629,7 @@ async function startStandbyMode(token, options, backendUrl, deviceId) {
|
|
|
589
629
|
catch { }
|
|
590
630
|
process.exit(0);
|
|
591
631
|
};
|
|
632
|
+
shutdownFn = shutdown;
|
|
592
633
|
process.on('SIGTERM', shutdown);
|
|
593
634
|
process.on('SIGINT', shutdown);
|
|
594
635
|
console.log(chalk.blue.bold(` ┌──────────────────────────────────────────────┐`));
|
package/dist/index.d.ts
CHANGED
package/dist/localServer.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface LocalServerOptions {
|
|
|
58
58
|
apiPort: number;
|
|
59
59
|
devPort: number;
|
|
60
60
|
env?: Record<string, string>;
|
|
61
|
+
/** Backend API URL (e.g., http://localhost:5001 or https://nstantpage.com) */
|
|
62
|
+
backendUrl?: string;
|
|
63
|
+
/** Called when file watcher detects changes — should send sync-dirty via tunnel */
|
|
64
|
+
onSyncDirty?: (projectId: string) => void;
|
|
61
65
|
}
|
|
62
66
|
export declare class LocalServer {
|
|
63
67
|
private server;
|
|
@@ -67,6 +71,7 @@ export declare class LocalServer {
|
|
|
67
71
|
private checker;
|
|
68
72
|
private errorStore;
|
|
69
73
|
private packageInstaller;
|
|
74
|
+
private agentSync;
|
|
70
75
|
private lastHeartbeat;
|
|
71
76
|
constructor(options: LocalServerOptions);
|
|
72
77
|
/**
|
|
@@ -114,6 +119,10 @@ export declare class LocalServer {
|
|
|
114
119
|
private handleStats;
|
|
115
120
|
private handleUsage;
|
|
116
121
|
private handleBuild;
|
|
122
|
+
private handleSyncStatus;
|
|
123
|
+
private handleSyncDiff;
|
|
124
|
+
private handlePushToBackend;
|
|
125
|
+
private handleDiskFile;
|
|
117
126
|
private handleHealth;
|
|
118
127
|
/**
|
|
119
128
|
* Get a pg Pool connected to this project's local database.
|
package/dist/localServer.js
CHANGED
|
@@ -24,6 +24,7 @@ import { Checker } from './checker.js';
|
|
|
24
24
|
import { ErrorStore, structuredErrorToString } from './errorStore.js';
|
|
25
25
|
import { PackageInstaller } from './packageInstaller.js';
|
|
26
26
|
import { probeLocalPostgres, ensureLocalProjectDb, isLocalPgAvailable, writeDatabaseUrlToEnv } from './projectDb.js';
|
|
27
|
+
import { AgentSync } from './agentSync.js';
|
|
27
28
|
// ─── Try to load node-pty for real PTY support ─────────────────
|
|
28
29
|
let ptyModule = null;
|
|
29
30
|
try {
|
|
@@ -101,6 +102,7 @@ export class LocalServer {
|
|
|
101
102
|
checker;
|
|
102
103
|
errorStore;
|
|
103
104
|
packageInstaller;
|
|
105
|
+
agentSync = null;
|
|
104
106
|
lastHeartbeat = Date.now();
|
|
105
107
|
constructor(options) {
|
|
106
108
|
this.options = options;
|
|
@@ -113,6 +115,15 @@ export class LocalServer {
|
|
|
113
115
|
this.checker = new Checker({ projectDir: options.projectDir, projectId: options.projectId });
|
|
114
116
|
this.errorStore = new ErrorStore();
|
|
115
117
|
this.packageInstaller = new PackageInstaller({ projectDir: options.projectDir });
|
|
118
|
+
// Initialize sync manager if backendUrl is available
|
|
119
|
+
if (options.backendUrl) {
|
|
120
|
+
this.agentSync = new AgentSync({
|
|
121
|
+
projectDir: options.projectDir,
|
|
122
|
+
projectId: options.projectId,
|
|
123
|
+
backendUrl: options.backendUrl,
|
|
124
|
+
onSyncDirty: options.onSyncDirty,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
116
127
|
}
|
|
117
128
|
/**
|
|
118
129
|
* Ensure the project database is provisioned and DATABASE_URL is set in the DevServer env.
|
|
@@ -187,8 +198,16 @@ export class LocalServer {
|
|
|
187
198
|
});
|
|
188
199
|
this.server.on('error', reject);
|
|
189
200
|
});
|
|
201
|
+
// Start file watcher for sync detection
|
|
202
|
+
if (this.agentSync) {
|
|
203
|
+
this.agentSync.startFileWatcher();
|
|
204
|
+
}
|
|
190
205
|
}
|
|
191
206
|
async stop() {
|
|
207
|
+
// Stop file watcher
|
|
208
|
+
if (this.agentSync) {
|
|
209
|
+
this.agentSync.destroy();
|
|
210
|
+
}
|
|
192
211
|
await this.devServer.stop();
|
|
193
212
|
if (this.server) {
|
|
194
213
|
this.server.close();
|
|
@@ -250,6 +269,10 @@ export class LocalServer {
|
|
|
250
269
|
'/live/db/query': this.handleDbQuery,
|
|
251
270
|
'/live/db/create': this.handleDbCreate,
|
|
252
271
|
'/live/build': this.handleBuild,
|
|
272
|
+
'/live/sync-status': this.handleSyncStatus,
|
|
273
|
+
'/live/sync-diff': this.handleSyncDiff,
|
|
274
|
+
'/live/push-to-backend': this.handlePushToBackend,
|
|
275
|
+
'/live/disk-file': this.handleDiskFile,
|
|
253
276
|
'/health': this.handleHealth,
|
|
254
277
|
};
|
|
255
278
|
if (handlers[path])
|
|
@@ -264,7 +287,7 @@ export class LocalServer {
|
|
|
264
287
|
// ─── /live/sync ──────────────────────────────────────────────
|
|
265
288
|
async handleSync(_req, res, body) {
|
|
266
289
|
const data = JSON.parse(body);
|
|
267
|
-
const { files, deletedFiles, filterFiles } = data;
|
|
290
|
+
const { files, deletedFiles, filterFiles, versionId } = data;
|
|
268
291
|
let filesWritten = 0;
|
|
269
292
|
let filesDeleted = 0;
|
|
270
293
|
// Write files
|
|
@@ -276,6 +299,10 @@ export class LocalServer {
|
|
|
276
299
|
if (deletedFiles && Array.isArray(deletedFiles) && deletedFiles.length > 0) {
|
|
277
300
|
filesDeleted = await this.fileManager.deleteFiles(deletedFiles);
|
|
278
301
|
}
|
|
302
|
+
// Mark sync baseline so the file watcher knows what "clean" looks like
|
|
303
|
+
if (this.agentSync && versionId) {
|
|
304
|
+
this.agentSync.markSynced(String(versionId));
|
|
305
|
+
}
|
|
279
306
|
// Run type check
|
|
280
307
|
const filesToCheck = filterFiles || (files ? Object.keys(files) : []);
|
|
281
308
|
const { typeErrors, allErrors } = await this.checker.fullCheck(filesToCheck.length > 0 ? filesToCheck : undefined);
|
|
@@ -953,7 +980,7 @@ export class LocalServer {
|
|
|
953
980
|
const buildResult = await new Promise((resolve) => {
|
|
954
981
|
const proc = spawn('sh', ['-c', buildCmd], {
|
|
955
982
|
cwd: projectDir,
|
|
956
|
-
env: { ...process.env, ...this.options.env, NODE_OPTIONS: '--max-old-space-size=
|
|
983
|
+
env: { ...process.env, ...this.options.env, NODE_OPTIONS: '--max-old-space-size=4096' },
|
|
957
984
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
958
985
|
});
|
|
959
986
|
let stdout = '', stderr = '';
|
|
@@ -963,8 +990,9 @@ export class LocalServer {
|
|
|
963
990
|
proc.on('error', (err) => resolve({ exitCode: 1, stdout: '', stderr: err.message }));
|
|
964
991
|
});
|
|
965
992
|
if (buildResult.exitCode !== 0) {
|
|
966
|
-
|
|
967
|
-
|
|
993
|
+
const errorMsg = buildResult.stderr || buildResult.stdout || `Build failed with exit code ${buildResult.exitCode}`;
|
|
994
|
+
console.error(` [LocalServer] Build failed (exit ${buildResult.exitCode}): ${errorMsg.substring(0, 500)}`);
|
|
995
|
+
this.json(res, { success: false, error: errorMsg });
|
|
968
996
|
return;
|
|
969
997
|
}
|
|
970
998
|
// Collect dist/ files
|
|
@@ -1013,6 +1041,83 @@ export class LocalServer {
|
|
|
1013
1041
|
this.json(res, { success: false, error: err.message });
|
|
1014
1042
|
}
|
|
1015
1043
|
}
|
|
1044
|
+
// ─── /live/sync-status ────────────────────────────────────────
|
|
1045
|
+
async handleSyncStatus(_req, res, _body, url) {
|
|
1046
|
+
if (!this.agentSync) {
|
|
1047
|
+
this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const status = await this.agentSync.getSyncStatus();
|
|
1052
|
+
this.json(res, { success: true, ...status });
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
console.error(` [LocalServer] sync-status error:`, error.message);
|
|
1056
|
+
this.json(res, { success: false, error: error.message }, 500);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// ─── /live/sync-diff ─────────────────────────────────────────
|
|
1060
|
+
async handleSyncDiff(_req, res, _body, url) {
|
|
1061
|
+
if (!this.agentSync) {
|
|
1062
|
+
this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const files = await this.agentSync.getPerFileStatus();
|
|
1067
|
+
const changed = files.filter(f => f.status !== 'same');
|
|
1068
|
+
this.json(res, {
|
|
1069
|
+
success: true,
|
|
1070
|
+
totalFiles: files.length,
|
|
1071
|
+
changedCount: changed.length,
|
|
1072
|
+
files: changed,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
console.error(` [LocalServer] sync-diff error:`, error.message);
|
|
1077
|
+
this.json(res, { success: false, error: error.message }, 500);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// ─── /live/push-to-backend ───────────────────────────────────
|
|
1081
|
+
async handlePushToBackend(_req, res, _body) {
|
|
1082
|
+
if (!this.agentSync) {
|
|
1083
|
+
this.json(res, { success: false, error: 'Sync not available (no backendUrl configured)' }, 503);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const result = await this.agentSync.pushToBackend();
|
|
1088
|
+
this.json(res, result);
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
console.error(` [LocalServer] push-to-backend error:`, error.message);
|
|
1092
|
+
this.json(res, { success: false, error: error.message }, 500);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// ─── /live/disk-file ─────────────────────────────────────────
|
|
1096
|
+
async handleDiskFile(_req, res, _body, url) {
|
|
1097
|
+
const filePath = url.searchParams.get('path');
|
|
1098
|
+
if (!filePath) {
|
|
1099
|
+
this.json(res, { error: 'Missing path parameter' }, 400);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.agentSync) {
|
|
1103
|
+
this.json(res, { success: false, error: 'Sync not available' }, 503);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
const content = await this.agentSync.readDiskFile(filePath);
|
|
1108
|
+
if (content === null) {
|
|
1109
|
+
this.json(res, { error: 'File not found' }, 404);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
res.statusCode = 200;
|
|
1113
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
1114
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1115
|
+
res.end(content);
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
this.json(res, { success: false, error: error.message }, 500);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1016
1121
|
// ─── /health ─────────────────────────────────────────────────
|
|
1017
1122
|
async handleHealth(_req, res) {
|
|
1018
1123
|
this.json(res, {
|
|
@@ -1197,7 +1302,9 @@ export class LocalServer {
|
|
|
1197
1302
|
}
|
|
1198
1303
|
}
|
|
1199
1304
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
1200
|
-
json(res, data) {
|
|
1305
|
+
json(res, data, statusCode) {
|
|
1306
|
+
if (statusCode)
|
|
1307
|
+
res.statusCode = statusCode;
|
|
1201
1308
|
res.setHeader('Content-Type', 'application/json');
|
|
1202
1309
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1203
1310
|
res.end(JSON.stringify(data));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Server — IPC endpoint for the system tray app.
|
|
3
|
+
*
|
|
4
|
+
* Provides a lightweight HTTP server on a local port (18999) that exposes:
|
|
5
|
+
* GET /status — Agent connection status, active projects, uptime
|
|
6
|
+
* GET /health — Simple health check
|
|
7
|
+
* POST /quit — Graceful shutdown
|
|
8
|
+
*
|
|
9
|
+
* The tray app polls GET /status to update its icon and menu.
|
|
10
|
+
* The port is also written to ~/.nstantpage/status.json for discovery.
|
|
11
|
+
*/
|
|
12
|
+
export interface AgentStatusInfo {
|
|
13
|
+
/** Current tunnel status: connected, connecting, disconnected, error */
|
|
14
|
+
tunnelStatus: string;
|
|
15
|
+
/** Agent version */
|
|
16
|
+
version: string;
|
|
17
|
+
/** Hostname of this machine */
|
|
18
|
+
hostname: string;
|
|
19
|
+
/** Platform string */
|
|
20
|
+
platform: string;
|
|
21
|
+
/** Agent uptime in ms */
|
|
22
|
+
uptime: number;
|
|
23
|
+
/** When the agent started (ISO string) */
|
|
24
|
+
startedAt: string;
|
|
25
|
+
/** Currently active projects */
|
|
26
|
+
activeProjects: ActiveProjectInfo[];
|
|
27
|
+
/** Total requests forwarded */
|
|
28
|
+
requestsForwarded: number;
|
|
29
|
+
/** Device ID */
|
|
30
|
+
deviceId: string;
|
|
31
|
+
/** Gateway URL */
|
|
32
|
+
gatewayUrl: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ActiveProjectInfo {
|
|
35
|
+
projectId: string;
|
|
36
|
+
devPort: number;
|
|
37
|
+
apiPort: number;
|
|
38
|
+
tunnelStatus: string;
|
|
39
|
+
startedAt?: string;
|
|
40
|
+
}
|
|
41
|
+
type StatusProvider = () => AgentStatusInfo;
|
|
42
|
+
type ShutdownHandler = () => Promise<void>;
|
|
43
|
+
export declare class StatusServer {
|
|
44
|
+
private server;
|
|
45
|
+
private getStatus;
|
|
46
|
+
private onShutdown;
|
|
47
|
+
constructor(getStatus: StatusProvider, onShutdown: ShutdownHandler);
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
private writeStatusFile;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read the status file to discover the running agent's status port.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readAgentStatusFile(): {
|
|
56
|
+
port: number;
|
|
57
|
+
pid: number;
|
|
58
|
+
startedAt: string;
|
|
59
|
+
} | null;
|
|
60
|
+
export {};
|