@juspay/shooter 1.7.1 → 1.8.0
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/bin/shooter.cjs +522 -0
- package/build/client/_app/immutable/assets/{0.B5tfAY8Y.css → 0.BZLcOr5z.css} +1 -1
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.br +0 -0
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{CBvZimn-.js → B6b4w6vf.js} +1 -1
- package/build/client/_app/immutable/chunks/B6b4w6vf.js.br +0 -0
- package/build/client/_app/immutable/chunks/B6b4w6vf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BEa4nlMF.js +3 -0
- package/build/client/_app/immutable/chunks/BEa4nlMF.js.br +0 -0
- package/build/client/_app/immutable/chunks/BEa4nlMF.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{D347PuJK.js → C7SeOWDG.js} +1 -1
- package/build/client/_app/immutable/chunks/C7SeOWDG.js.br +0 -0
- package/build/client/_app/immutable/chunks/C7SeOWDG.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DdpA9n1s.js +5 -0
- package/build/client/_app/immutable/chunks/DdpA9n1s.js.br +4 -0
- package/build/client/_app/immutable/chunks/DdpA9n1s.js.gz +0 -0
- package/build/client/_app/immutable/chunks/xs1Xl3_e.js +1 -0
- package/build/client/_app/immutable/chunks/xs1Xl3_e.js.br +0 -0
- package/build/client/_app/immutable/chunks/xs1Xl3_e.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.C_IPOstn.js → app.CP7226A7.js} +2 -2
- package/build/client/_app/immutable/entry/app.CP7226A7.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CP7226A7.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.mGPvkOah.js +1 -0
- package/build/client/_app/immutable/entry/start.mGPvkOah.js.br +2 -0
- package/build/client/_app/immutable/entry/start.mGPvkOah.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.DRy3hSaJ.js → 0.DwU44ZAj.js} +1 -1
- package/build/client/_app/immutable/nodes/0.DwU44ZAj.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DwU44ZAj.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BL16K8Rb.js → 1.CChG-n6d.js} +1 -1
- package/build/client/_app/immutable/nodes/1.CChG-n6d.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.CChG-n6d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.iOKTlmTJ.js → 2.CzexDbwp.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CzexDbwp.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CzexDbwp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.GuF9Fr8V.js → 3.DC3WghxB.js} +1 -1
- package/build/client/_app/immutable/nodes/3.DC3WghxB.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DC3WghxB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.DSC53pZZ.js → 6.C4aXlZQd.js} +1 -1
- package/build/client/_app/immutable/nodes/6.C4aXlZQd.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.C4aXlZQd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DfniCleW.js +4 -0
- package/build/client/_app/immutable/nodes/7.DfniCleW.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DfniCleW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.Dn1iVaxc.js → 8.D4AzZWcq.js} +2 -2
- package/build/client/_app/immutable/nodes/8.D4AzZWcq.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.D4AzZWcq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.D3nURtQJ.js → 9.gV8oJWv_.js} +1 -1
- package/build/client/_app/immutable/nodes/9.gV8oJWv_.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.gV8oJWv_.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-D82zFKZh.js → 0-XyVDlEyN.js} +3 -3
- package/build/server/chunks/{0-D82zFKZh.js.map → 0-XyVDlEyN.js.map} +1 -1
- package/build/server/chunks/{1-BmaGmZUH.js → 1-C3vZx9QL.js} +2 -2
- package/build/server/chunks/{1-BmaGmZUH.js.map → 1-C3vZx9QL.js.map} +1 -1
- package/build/server/chunks/{2-T5uLjTgt.js → 2-LWO3Q9-s.js} +2 -2
- package/build/server/chunks/{2-T5uLjTgt.js.map → 2-LWO3Q9-s.js.map} +1 -1
- package/build/server/chunks/{3-D6FHwoMf.js → 3-3WzO52IA.js} +2 -2
- package/build/server/chunks/{3-D6FHwoMf.js.map → 3-3WzO52IA.js.map} +1 -1
- package/build/server/chunks/{6-Yh_tXC7x.js → 6-DjPIWYcj.js} +2 -2
- package/build/server/chunks/{6-Yh_tXC7x.js.map → 6-DjPIWYcj.js.map} +1 -1
- package/build/server/chunks/{7-Dmz1unig.js → 7-DF5FUXhP.js} +3 -3
- package/build/server/chunks/{7-Dmz1unig.js.map → 7-DF5FUXhP.js.map} +1 -1
- package/build/server/chunks/{8-Bf6s5ssE.js → 8-CejJgM0l.js} +2 -2
- package/build/server/chunks/{8-Bf6s5ssE.js.map → 8-CejJgM0l.js.map} +1 -1
- package/build/server/chunks/{9-AHy0sACd.js → 9-D1YMozmH.js} +2 -2
- package/build/server/chunks/{9-AHy0sACd.js.map → 9-D1YMozmH.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-D1Iaycsm.js → _page.svelte-DpUVuiqQ.js} +3 -13
- package/build/server/chunks/_page.svelte-DpUVuiqQ.js.map +1 -0
- package/build/server/chunks/{_server.ts-BQarRaho.js → _server.ts-CilRds58.js} +14 -11
- package/build/server/chunks/_server.ts-CilRds58.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +10 -10
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/scripts/update-checker.cjs +196 -0
- package/scripts/update-state.cjs +235 -0
- package/src/lib/theme.css +33 -1
- package/src/routes/api/sessions/+server.ts +16 -10
- package/src/routes/session/[id]/+page.svelte +25 -14
- package/build/client/_app/immutable/assets/0.B5tfAY8Y.css.br +0 -0
- package/build/client/_app/immutable/assets/0.B5tfAY8Y.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bkcn25gz.js +0 -3
- package/build/client/_app/immutable/chunks/Bkcn25gz.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bkcn25gz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CBvZimn-.js.br +0 -0
- package/build/client/_app/immutable/chunks/CBvZimn-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CRXYOhph.js +0 -5
- package/build/client/_app/immutable/chunks/CRXYOhph.js.br +0 -0
- package/build/client/_app/immutable/chunks/CRXYOhph.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D347PuJK.js.br +0 -0
- package/build/client/_app/immutable/chunks/D347PuJK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dqgg-a0I.js +0 -1
- package/build/client/_app/immutable/chunks/Dqgg-a0I.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dqgg-a0I.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.C_IPOstn.js.br +0 -0
- package/build/client/_app/immutable/entry/app.C_IPOstn.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BfrjAeOs.js +0 -1
- package/build/client/_app/immutable/entry/start.BfrjAeOs.js.br +0 -2
- package/build/client/_app/immutable/entry/start.BfrjAeOs.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DRy3hSaJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DRy3hSaJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BL16K8Rb.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BL16K8Rb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.iOKTlmTJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.iOKTlmTJ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.GuF9Fr8V.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.GuF9Fr8V.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DSC53pZZ.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DSC53pZZ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.C8lL01pY.js +0 -4
- package/build/client/_app/immutable/nodes/7.C8lL01pY.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.C8lL01pY.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Dn1iVaxc.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Dn1iVaxc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.D3nURtQJ.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.D3nURtQJ.js.gz +0 -0
- package/build/server/chunks/_page.svelte-D1Iaycsm.js.map +0 -1
- package/build/server/chunks/_server.ts-BQarRaho.js.map +0 -1
package/bin/shooter.cjs
CHANGED
|
@@ -85,6 +85,15 @@ function removePid() {
|
|
|
85
85
|
} catch {}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ── Guard / auto-update constants ──────────────────────────────────
|
|
89
|
+
const GUARD_PID_FILE = path.join(SHOOTER_HOME, 'guard.pid');
|
|
90
|
+
const GUARD_LOG_FILE = path.join(LOG_DIR, 'guard.log');
|
|
91
|
+
const UPDATE_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
92
|
+
const UPDATE_FIRST_CHECK_MS = 30_000; // 30 seconds after start
|
|
93
|
+
const INSTALL_TIMEOUT_MS = 120_000;
|
|
94
|
+
const BUILD_TIMEOUT_MS = 300_000;
|
|
95
|
+
const HEALTH_TIMEOUT_MS = 30_000;
|
|
96
|
+
|
|
88
97
|
// ── CLI argument parsing ────────────────────────────────────────────
|
|
89
98
|
const args = process.argv.slice(2);
|
|
90
99
|
const command = args[0] || 'start';
|
|
@@ -108,6 +117,12 @@ switch (command) {
|
|
|
108
117
|
case 'setup':
|
|
109
118
|
runSetup();
|
|
110
119
|
break;
|
|
120
|
+
case 'update':
|
|
121
|
+
runUpdate(args[1]);
|
|
122
|
+
break;
|
|
123
|
+
case 'guard':
|
|
124
|
+
runGuard();
|
|
125
|
+
break;
|
|
111
126
|
case 'version':
|
|
112
127
|
case '--version':
|
|
113
128
|
case '-v':
|
|
@@ -279,6 +294,10 @@ function startServer() {
|
|
|
279
294
|
} else if (!noTunnel && !isCloudflaredAvailable()) {
|
|
280
295
|
console.log(' (cloudflared not found — no tunnel. Install: brew install cloudflared)');
|
|
281
296
|
}
|
|
297
|
+
|
|
298
|
+
// Spawn auto-update guard (detached) — only when launchd is managing
|
|
299
|
+
spawnGuard(child.pid, port);
|
|
300
|
+
|
|
282
301
|
// Exit immediately — daemon is running, tunnel starts async
|
|
283
302
|
process.exit(0);
|
|
284
303
|
} else {
|
|
@@ -312,9 +331,13 @@ function startServer() {
|
|
|
312
331
|
}, 3000);
|
|
313
332
|
}
|
|
314
333
|
|
|
334
|
+
// Spawn auto-update guard (detached) — only when launchd is managing
|
|
335
|
+
spawnGuard(child.pid, port);
|
|
336
|
+
|
|
315
337
|
child.on('error', (err) => {
|
|
316
338
|
removePid();
|
|
317
339
|
if (tunnelStarted) stopTunnel();
|
|
340
|
+
stopGuard();
|
|
318
341
|
console.error('Failed to start Shooter server:', err.message);
|
|
319
342
|
process.exit(1);
|
|
320
343
|
});
|
|
@@ -322,6 +345,7 @@ function startServer() {
|
|
|
322
345
|
child.on('exit', (code, signal) => {
|
|
323
346
|
removePid();
|
|
324
347
|
stopTunnel();
|
|
348
|
+
stopGuard();
|
|
325
349
|
if (signal) {
|
|
326
350
|
process.exit(128 + (signalCode(signal) || 1));
|
|
327
351
|
}
|
|
@@ -333,6 +357,7 @@ function startServer() {
|
|
|
333
357
|
process.on(sig, () => {
|
|
334
358
|
child.kill(sig);
|
|
335
359
|
stopTunnel();
|
|
360
|
+
stopGuard();
|
|
336
361
|
});
|
|
337
362
|
}
|
|
338
363
|
}
|
|
@@ -351,6 +376,7 @@ function stopServer() {
|
|
|
351
376
|
|
|
352
377
|
console.log(`Stopping Shooter (PID ${pid})...`);
|
|
353
378
|
stopTunnel();
|
|
379
|
+
stopGuard();
|
|
354
380
|
try {
|
|
355
381
|
process.kill(pid, 'SIGTERM');
|
|
356
382
|
// Wait briefly for clean shutdown
|
|
@@ -681,6 +707,493 @@ function runSetup() {
|
|
|
681
707
|
});
|
|
682
708
|
}
|
|
683
709
|
|
|
710
|
+
// ── update ──────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
function runUpdate(subcommand) {
|
|
713
|
+
const { checkForUpdate } = require(path.join(PKG_ROOT, 'scripts', 'update-checker.cjs'));
|
|
714
|
+
const { recordCheck, isVersionSuppressed } = require(path.join(PKG_ROOT, 'scripts', 'update-state.cjs'));
|
|
715
|
+
|
|
716
|
+
const result = checkForUpdate(PKG_ROOT);
|
|
717
|
+
if (!result.checkFailed) recordCheck(result.latestVersion);
|
|
718
|
+
|
|
719
|
+
if (subcommand === 'check') {
|
|
720
|
+
// Just check, don't install
|
|
721
|
+
if (result.checkFailed) {
|
|
722
|
+
console.error(`Update check failed: ${result.error}`);
|
|
723
|
+
process.exitCode = 1;
|
|
724
|
+
} else if (result.updateAvailable) {
|
|
725
|
+
console.log(`Update available: ${result.currentVersion} → ${result.latestVersion}`);
|
|
726
|
+
console.log(` Current commit: ${result.currentCommit}`);
|
|
727
|
+
console.log(` Latest commit: ${result.latestCommit}`);
|
|
728
|
+
if (isVersionSuppressed(result.latestVersion)) {
|
|
729
|
+
console.log(` (version ${result.latestVersion} is temporarily suppressed — will retry in <24h)`);
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
console.log(`Already up to date: v${result.currentVersion} (${result.currentCommit})`);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Default: check + install
|
|
738
|
+
if (result.checkFailed) {
|
|
739
|
+
console.error(`Update check failed: ${result.error}`);
|
|
740
|
+
process.exitCode = 1;
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (!result.updateAvailable) {
|
|
744
|
+
console.log(`Already up to date: v${result.currentVersion} (${result.currentCommit})`);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Only allow installs on the release branch
|
|
749
|
+
const { getCurrentBranch } = require(path.join(PKG_ROOT, 'scripts', 'update-checker.cjs'));
|
|
750
|
+
const branch = getCurrentBranch(PKG_ROOT);
|
|
751
|
+
if (branch !== 'release') {
|
|
752
|
+
console.log(`Cannot update: currently on branch '${branch || 'unknown'}', updates only apply to 'release'.`);
|
|
753
|
+
console.log('Switch to the release branch and try again.');
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (isVersionSuppressed(result.latestVersion)) {
|
|
758
|
+
console.log(`Update to ${result.latestVersion} is temporarily suppressed (previous failure).`);
|
|
759
|
+
console.log('Suppression expires within 24 hours. Use a fresh git pull to override.');
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
console.log(`Updating: ${result.currentVersion} → ${result.latestVersion}...`);
|
|
764
|
+
const success = performUpdate(result);
|
|
765
|
+
if (success) {
|
|
766
|
+
console.log(`\nUpdate complete! Now running v${result.latestVersion}.`);
|
|
767
|
+
|
|
768
|
+
// Restart if server is running
|
|
769
|
+
const pid = readPid();
|
|
770
|
+
if (pid) {
|
|
771
|
+
if (isLaunchdManaging()) {
|
|
772
|
+
const uid = process.getuid ? process.getuid() : 501;
|
|
773
|
+
try {
|
|
774
|
+
execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, {
|
|
775
|
+
timeout: 10_000,
|
|
776
|
+
stdio: 'pipe',
|
|
777
|
+
});
|
|
778
|
+
console.log('Signaled launchd to restart the server.');
|
|
779
|
+
} catch {
|
|
780
|
+
console.log('launchctl kickstart failed — the server may need a manual restart.');
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
try {
|
|
784
|
+
process.kill(pid, 'SIGTERM');
|
|
785
|
+
} catch {}
|
|
786
|
+
console.log('Server process terminated. Run "shooter start" to restart.');
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Perform the actual update: git pull → pnpm install → pnpm build.
|
|
794
|
+
* Returns true on success. On failure, rolls back and returns false.
|
|
795
|
+
*/
|
|
796
|
+
function performUpdate(result) {
|
|
797
|
+
const { suppressVersion, recordSuccessfulUpdate } = require(path.join(PKG_ROOT, 'scripts', 'update-state.cjs'));
|
|
798
|
+
|
|
799
|
+
// Save current HEAD for rollback
|
|
800
|
+
let savedHead = '';
|
|
801
|
+
try {
|
|
802
|
+
savedHead = execSync('git rev-parse HEAD', {
|
|
803
|
+
cwd: PKG_ROOT,
|
|
804
|
+
encoding: 'utf8',
|
|
805
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
806
|
+
}).trim();
|
|
807
|
+
} catch {}
|
|
808
|
+
|
|
809
|
+
// 1. Git pull (fast-forward only)
|
|
810
|
+
try {
|
|
811
|
+
console.log(' Pulling latest changes...');
|
|
812
|
+
execSync('git pull --ff-only origin release', {
|
|
813
|
+
cwd: PKG_ROOT,
|
|
814
|
+
stdio: 'inherit',
|
|
815
|
+
timeout: 30_000,
|
|
816
|
+
});
|
|
817
|
+
} catch (err) {
|
|
818
|
+
console.error(' Git pull failed:', err.message || err);
|
|
819
|
+
suppressVersion(result.latestVersion, 'pull_failed');
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 2. Install dependencies
|
|
824
|
+
try {
|
|
825
|
+
console.log(' Installing dependencies...');
|
|
826
|
+
execSync('pnpm install --frozen-lockfile', {
|
|
827
|
+
cwd: PKG_ROOT,
|
|
828
|
+
stdio: 'inherit',
|
|
829
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
830
|
+
});
|
|
831
|
+
} catch (err) {
|
|
832
|
+
console.error(' pnpm install failed:', err.message || err);
|
|
833
|
+
rollback(savedHead);
|
|
834
|
+
suppressVersion(result.latestVersion, 'install_failed');
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// 3. Build
|
|
839
|
+
try {
|
|
840
|
+
console.log(' Building...');
|
|
841
|
+
execSync('pnpm build', {
|
|
842
|
+
cwd: PKG_ROOT,
|
|
843
|
+
stdio: 'inherit',
|
|
844
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
845
|
+
});
|
|
846
|
+
} catch (err) {
|
|
847
|
+
console.error(' Build failed:', err.message || err);
|
|
848
|
+
rollback(savedHead);
|
|
849
|
+
suppressVersion(result.latestVersion, 'build_failed');
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
recordSuccessfulUpdate(result.latestVersion, result.currentVersion);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Roll back to a previous commit after a failed update.
|
|
859
|
+
*/
|
|
860
|
+
function rollback(savedHead) {
|
|
861
|
+
if (!savedHead) return;
|
|
862
|
+
console.log(' Rolling back to previous version...');
|
|
863
|
+
try {
|
|
864
|
+
execSync(`git reset --hard ${savedHead}`, {
|
|
865
|
+
cwd: PKG_ROOT,
|
|
866
|
+
stdio: 'inherit',
|
|
867
|
+
timeout: 10_000,
|
|
868
|
+
});
|
|
869
|
+
execSync('pnpm install --frozen-lockfile', {
|
|
870
|
+
cwd: PKG_ROOT,
|
|
871
|
+
stdio: 'inherit',
|
|
872
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
873
|
+
});
|
|
874
|
+
execSync('pnpm build', {
|
|
875
|
+
cwd: PKG_ROOT,
|
|
876
|
+
stdio: 'inherit',
|
|
877
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
878
|
+
});
|
|
879
|
+
} catch (rollbackErr) {
|
|
880
|
+
console.error(' WARNING: Rollback failed:', rollbackErr.message || rollbackErr);
|
|
881
|
+
console.error(' Manual intervention may be required.');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ── guard (hidden — spawned by start) ──────────────────────────────
|
|
886
|
+
|
|
887
|
+
function spawnGuard(parentPid, port) {
|
|
888
|
+
// Only spawn guard when launchd is managing the service
|
|
889
|
+
if (!isLaunchdManaging()) return;
|
|
890
|
+
|
|
891
|
+
const shooterBin = path.join(PKG_ROOT, 'bin', 'shooter.cjs');
|
|
892
|
+
try {
|
|
893
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
894
|
+
const logFd = fs.openSync(GUARD_LOG_FILE, 'a');
|
|
895
|
+
|
|
896
|
+
const child = spawn(
|
|
897
|
+
process.execPath,
|
|
898
|
+
[shooterBin, 'guard', '--parent-pid', String(parentPid), '--port', String(port)],
|
|
899
|
+
{
|
|
900
|
+
cwd: PKG_ROOT,
|
|
901
|
+
detached: true,
|
|
902
|
+
stdio: ['ignore', logFd, logFd],
|
|
903
|
+
env: {
|
|
904
|
+
...process.env,
|
|
905
|
+
SHOOTER_PKG_ROOT: PKG_ROOT,
|
|
906
|
+
SHOOTER_HOME,
|
|
907
|
+
},
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
if (child.pid) {
|
|
912
|
+
fs.mkdirSync(SHOOTER_HOME, { recursive: true });
|
|
913
|
+
fs.writeFileSync(GUARD_PID_FILE, String(child.pid));
|
|
914
|
+
}
|
|
915
|
+
child.unref();
|
|
916
|
+
fs.closeSync(logFd);
|
|
917
|
+
} catch (err) {
|
|
918
|
+
// Non-fatal — server runs fine without guard
|
|
919
|
+
console.log(` (auto-update guard failed to start: ${err.message})`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function stopGuard() {
|
|
924
|
+
try {
|
|
925
|
+
const pid = parseInt(fs.readFileSync(GUARD_PID_FILE, 'utf8').trim(), 10);
|
|
926
|
+
if (!isNaN(pid) && pid > 0) {
|
|
927
|
+
// Validate the PID is actually a guard process (prevent PID reuse attacks)
|
|
928
|
+
try {
|
|
929
|
+
const cmdline = execSync(`ps -p ${pid} -o command= 2>/dev/null`, {
|
|
930
|
+
encoding: 'utf8',
|
|
931
|
+
}).trim();
|
|
932
|
+
if (!cmdline.includes('shooter.cjs') || !cmdline.includes('guard')) {
|
|
933
|
+
// PID reused by an unrelated process — just clean up the stale pidfile
|
|
934
|
+
fs.unlinkSync(GUARD_PID_FILE);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
// ps failed — process likely already exited
|
|
939
|
+
fs.unlinkSync(GUARD_PID_FILE);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
process.kill(pid, 'SIGTERM');
|
|
944
|
+
} catch {}
|
|
945
|
+
}
|
|
946
|
+
} catch {}
|
|
947
|
+
try {
|
|
948
|
+
fs.unlinkSync(GUARD_PID_FILE);
|
|
949
|
+
} catch {}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function isLaunchdManaging() {
|
|
953
|
+
if (os.platform() !== 'darwin') return false;
|
|
954
|
+
try {
|
|
955
|
+
const uid = process.getuid ? process.getuid() : 501;
|
|
956
|
+
execSync(`launchctl print gui/${uid}/${LAUNCHD_LABEL} 2>/dev/null`, {
|
|
957
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
958
|
+
timeout: 5_000,
|
|
959
|
+
});
|
|
960
|
+
return true;
|
|
961
|
+
} catch {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function runGuard() {
|
|
967
|
+
// Parse guard-specific args
|
|
968
|
+
let parentPid = 0;
|
|
969
|
+
let port = DEFAULT_PORT;
|
|
970
|
+
for (let i = 1; i < args.length; i++) {
|
|
971
|
+
if (args[i] === '--parent-pid' && args[i + 1]) {
|
|
972
|
+
parentPid = parseInt(args[i + 1], 10);
|
|
973
|
+
i++;
|
|
974
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
975
|
+
port = parseInt(args[i + 1], 10);
|
|
976
|
+
i++;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!parentPid || parentPid <= 0) {
|
|
981
|
+
console.error('[guard] Invalid --parent-pid');
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const { checkForUpdate, getCurrentBranch } = require(path.join(PKG_ROOT, 'scripts', 'update-checker.cjs'));
|
|
986
|
+
const {
|
|
987
|
+
recordCheck,
|
|
988
|
+
isVersionSuppressed,
|
|
989
|
+
suppressVersion,
|
|
990
|
+
recordSuccessfulUpdate,
|
|
991
|
+
} = require(path.join(PKG_ROOT, 'scripts', 'update-state.cjs'));
|
|
992
|
+
|
|
993
|
+
const log = (msg) => console.log(`[guard] ${new Date().toISOString()} ${msg}`);
|
|
994
|
+
|
|
995
|
+
// Check if parent is alive
|
|
996
|
+
function isParentAlive() {
|
|
997
|
+
try {
|
|
998
|
+
process.kill(parentPid, 0);
|
|
999
|
+
return true;
|
|
1000
|
+
} catch {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
let updateInProgress = false;
|
|
1006
|
+
let updateRestartInProgress = false;
|
|
1007
|
+
|
|
1008
|
+
async function runUpdateCheck() {
|
|
1009
|
+
if (updateInProgress) return;
|
|
1010
|
+
updateInProgress = true;
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
// Only auto-update on release branch
|
|
1014
|
+
const branch = getCurrentBranch(PKG_ROOT);
|
|
1015
|
+
if (branch !== 'release') {
|
|
1016
|
+
log(`skipping update check — on branch '${branch}', not 'release'`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// 1. Check for update
|
|
1021
|
+
const result = checkForUpdate(PKG_ROOT);
|
|
1022
|
+
if (result.checkFailed) {
|
|
1023
|
+
log(`update check failed: ${result.error}`);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
recordCheck(result.latestVersion);
|
|
1027
|
+
|
|
1028
|
+
if (!result.updateAvailable) {
|
|
1029
|
+
log(`up to date: v${result.currentVersion}`);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (isVersionSuppressed(result.latestVersion)) {
|
|
1034
|
+
log(`version ${result.latestVersion} is suppressed, skipping`);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
log(`update available: ${result.currentVersion} → ${result.latestVersion}`);
|
|
1039
|
+
|
|
1040
|
+
// 2. Save current HEAD for rollback
|
|
1041
|
+
let savedHead = '';
|
|
1042
|
+
try {
|
|
1043
|
+
savedHead = execSync('git rev-parse HEAD', {
|
|
1044
|
+
cwd: PKG_ROOT,
|
|
1045
|
+
encoding: 'utf8',
|
|
1046
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1047
|
+
}).trim();
|
|
1048
|
+
} catch {}
|
|
1049
|
+
|
|
1050
|
+
// 3. Git pull
|
|
1051
|
+
try {
|
|
1052
|
+
execSync('git pull --ff-only origin release', {
|
|
1053
|
+
cwd: PKG_ROOT,
|
|
1054
|
+
encoding: 'utf8',
|
|
1055
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1056
|
+
timeout: 30_000,
|
|
1057
|
+
});
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
const stderr = err.stderr ? err.stderr.toString().trim().slice(-500) : '';
|
|
1060
|
+
log(`git pull failed: ${err.message}${stderr ? '\n' + stderr : ''}`);
|
|
1061
|
+
suppressVersion(result.latestVersion, 'pull_failed');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 4. Install dependencies
|
|
1066
|
+
try {
|
|
1067
|
+
log('installing dependencies...');
|
|
1068
|
+
execSync('pnpm install --frozen-lockfile', {
|
|
1069
|
+
cwd: PKG_ROOT,
|
|
1070
|
+
encoding: 'utf8',
|
|
1071
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1072
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
1073
|
+
});
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
const stderr = err.stderr ? err.stderr.toString().trim().slice(-500) : '';
|
|
1076
|
+
log(`pnpm install failed: ${err.message}${stderr ? '\n' + stderr : ''}`);
|
|
1077
|
+
guardRollback(savedHead);
|
|
1078
|
+
suppressVersion(result.latestVersion, 'install_failed');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// 5. Build
|
|
1083
|
+
try {
|
|
1084
|
+
log('building...');
|
|
1085
|
+
execSync('pnpm build', {
|
|
1086
|
+
cwd: PKG_ROOT,
|
|
1087
|
+
encoding: 'utf8',
|
|
1088
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1089
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
1090
|
+
});
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
const stderr = err.stderr ? err.stderr.toString().trim().slice(-500) : '';
|
|
1093
|
+
log(`build failed: ${err.message}${stderr ? '\n' + stderr : ''}`);
|
|
1094
|
+
guardRollback(savedHead);
|
|
1095
|
+
suppressVersion(result.latestVersion, 'build_failed');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// 6. Restart via launchctl
|
|
1100
|
+
updateRestartInProgress = true;
|
|
1101
|
+
log('restarting via launchctl...');
|
|
1102
|
+
const uid = process.getuid ? process.getuid() : 501;
|
|
1103
|
+
try {
|
|
1104
|
+
execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, {
|
|
1105
|
+
timeout: 10_000,
|
|
1106
|
+
stdio: 'pipe',
|
|
1107
|
+
});
|
|
1108
|
+
} catch {
|
|
1109
|
+
log('WARNING: launchctl kickstart failed');
|
|
1110
|
+
suppressVersion(result.latestVersion, 'restart_failed');
|
|
1111
|
+
updateRestartInProgress = false;
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// 7. Wait for healthy restart
|
|
1116
|
+
let healthy = false;
|
|
1117
|
+
const restartStart = Date.now();
|
|
1118
|
+
while (Date.now() - restartStart < HEALTH_TIMEOUT_MS) {
|
|
1119
|
+
await sleep(2000);
|
|
1120
|
+
try {
|
|
1121
|
+
const resp = await fetch(`http://localhost:${port}/api/health`, {
|
|
1122
|
+
signal: AbortSignal.timeout(3000),
|
|
1123
|
+
});
|
|
1124
|
+
if (resp.ok) {
|
|
1125
|
+
const data = await resp.json();
|
|
1126
|
+
if (data.version === result.latestVersion) {
|
|
1127
|
+
healthy = true;
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} catch {
|
|
1132
|
+
/* retry */
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (healthy) {
|
|
1137
|
+
log(`update successful: now running v${result.latestVersion}`);
|
|
1138
|
+
recordSuccessfulUpdate(result.latestVersion, result.currentVersion);
|
|
1139
|
+
// New server will spawn its own guard — exit this one
|
|
1140
|
+
process.exit(0);
|
|
1141
|
+
} else {
|
|
1142
|
+
log(`WARNING: server unhealthy after update to ${result.latestVersion}`);
|
|
1143
|
+
suppressVersion(result.latestVersion, 'unhealthy_after_restart');
|
|
1144
|
+
updateRestartInProgress = false;
|
|
1145
|
+
}
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
log(`update check error: ${err.message || err}`);
|
|
1148
|
+
} finally {
|
|
1149
|
+
updateInProgress = false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function guardRollback(savedHead) {
|
|
1154
|
+
if (!savedHead) return;
|
|
1155
|
+
log('rolling back...');
|
|
1156
|
+
try {
|
|
1157
|
+
execSync(`git reset --hard ${savedHead}`, {
|
|
1158
|
+
cwd: PKG_ROOT,
|
|
1159
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1160
|
+
timeout: 10_000,
|
|
1161
|
+
});
|
|
1162
|
+
execSync('pnpm install --frozen-lockfile', {
|
|
1163
|
+
cwd: PKG_ROOT,
|
|
1164
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1165
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
1166
|
+
});
|
|
1167
|
+
execSync('pnpm build', {
|
|
1168
|
+
cwd: PKG_ROOT,
|
|
1169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1170
|
+
timeout: BUILD_TIMEOUT_MS,
|
|
1171
|
+
});
|
|
1172
|
+
} catch (rollbackErr) {
|
|
1173
|
+
log(`WARNING: rollback failed: ${rollbackErr.message}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Parent process health monitor
|
|
1178
|
+
const parentCheckInterval = setInterval(() => {
|
|
1179
|
+
if (!isParentAlive() && !updateRestartInProgress) {
|
|
1180
|
+
log('parent process died, exiting guard');
|
|
1181
|
+
clearInterval(parentCheckInterval);
|
|
1182
|
+
process.exit(0);
|
|
1183
|
+
}
|
|
1184
|
+
}, 5_000);
|
|
1185
|
+
|
|
1186
|
+
// Schedule update checks
|
|
1187
|
+
setTimeout(() => void runUpdateCheck(), UPDATE_FIRST_CHECK_MS);
|
|
1188
|
+
setInterval(() => void runUpdateCheck(), UPDATE_CHECK_INTERVAL_MS);
|
|
1189
|
+
|
|
1190
|
+
log(`started (parent PID: ${parentPid}, port: ${port})`);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function sleep(ms) {
|
|
1194
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
684
1197
|
// ── help ────────────────────────────────────────────────────────────
|
|
685
1198
|
|
|
686
1199
|
function showHelp() {
|
|
@@ -699,6 +1212,8 @@ Commands:
|
|
|
699
1212
|
logs Tail server logs
|
|
700
1213
|
setup Quick setup (API key + build, ~60 seconds)
|
|
701
1214
|
setup --push Add/reconfigure push notifications
|
|
1215
|
+
update Check for updates and install if available
|
|
1216
|
+
update check Check for updates without installing
|
|
702
1217
|
version Show version number
|
|
703
1218
|
help Show this help message
|
|
704
1219
|
|
|
@@ -706,6 +1221,11 @@ Start options:
|
|
|
706
1221
|
-d, --daemon Run in background (detach from terminal)
|
|
707
1222
|
--no-tunnel Don't start a Cloudflare Tunnel
|
|
708
1223
|
|
|
1224
|
+
Auto-update:
|
|
1225
|
+
When running as a LaunchAgent, Shooter automatically checks for updates
|
|
1226
|
+
every 2 hours. Updates are pulled from origin/release, built, and the
|
|
1227
|
+
server is restarted via launchctl. Terminal sessions survive restarts.
|
|
1228
|
+
|
|
709
1229
|
Examples:
|
|
710
1230
|
shooter Start the server + tunnel (foreground)
|
|
711
1231
|
shooter start -d Start in background (daemon mode)
|
|
@@ -715,6 +1235,8 @@ Examples:
|
|
|
715
1235
|
shooter logs Follow server logs
|
|
716
1236
|
shooter setup Quick setup (~60s, push deferred)
|
|
717
1237
|
shooter setup --push Add iOS/Android push notifications
|
|
1238
|
+
shooter update Check and install updates
|
|
1239
|
+
shooter update check Check for updates only
|
|
718
1240
|
`.trim()
|
|
719
1241
|
);
|
|
720
1242
|
}
|