happy-stacks 0.0.0 → 0.1.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/README.md +22 -4
- package/bin/happys.mjs +76 -5
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +16 -4
- package/extras/swiftbar/auth-login.sh +5 -5
- package/extras/swiftbar/happy-stacks.5s.sh +83 -41
- package/extras/swiftbar/happys-term.sh +151 -0
- package/extras/swiftbar/happys.sh +52 -0
- package/extras/swiftbar/lib/render.sh +74 -56
- package/extras/swiftbar/lib/system.sh +37 -6
- package/extras/swiftbar/lib/utils.sh +180 -4
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +2 -13
- package/extras/swiftbar/set-server-flavor.sh +8 -8
- package/extras/swiftbar/wt-pr.sh +1 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +374 -3
- package/scripts/daemon.mjs +78 -11
- package/scripts/dev.mjs +122 -17
- package/scripts/init.mjs +238 -32
- package/scripts/migrate.mjs +292 -0
- package/scripts/mobile.mjs +51 -19
- package/scripts/run.mjs +118 -26
- package/scripts/service.mjs +176 -37
- package/scripts/stack.mjs +665 -22
- package/scripts/stop.mjs +157 -0
- package/scripts/tailscale.mjs +147 -21
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/utils/cli_registry.mjs +23 -0
- package/scripts/utils/config.mjs +9 -1
- package/scripts/utils/env.mjs +37 -15
- package/scripts/utils/expo.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +430 -0
- package/scripts/utils/pm.mjs +11 -2
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +46 -5
- package/scripts/utils/server.mjs +37 -0
- package/scripts/utils/stack_stop.mjs +206 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/worktrees.mjs +53 -7
package/scripts/service.mjs
CHANGED
|
@@ -12,7 +12,10 @@ import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
|
12
12
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Manage the
|
|
15
|
+
* Manage the autostart service installed by `happys bootstrap -- --autostart`.
|
|
16
|
+
*
|
|
17
|
+
* - macOS: launchd LaunchAgents
|
|
18
|
+
* - Linux: systemd user services
|
|
16
19
|
*
|
|
17
20
|
* Commands:
|
|
18
21
|
* - install | uninstall
|
|
@@ -60,8 +63,8 @@ function getAutostartEnv({ rootDir }) {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export async function installService() {
|
|
63
|
-
if (process.platform !== 'darwin') {
|
|
64
|
-
throw new Error('[local] service install is only supported on macOS (
|
|
66
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
67
|
+
throw new Error('[local] service install is only supported on macOS (launchd) and Linux (systemd user).');
|
|
65
68
|
}
|
|
66
69
|
const rootDir = getRootDir(import.meta.url);
|
|
67
70
|
const { primaryLabel: label } = getDefaultAutostartPaths();
|
|
@@ -76,12 +79,21 @@ export async function installService() {
|
|
|
76
79
|
} catch {
|
|
77
80
|
// ignore
|
|
78
81
|
}
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (process.platform === 'darwin') {
|
|
83
|
+
await ensureMacAutostartEnabled({ rootDir, label, env });
|
|
84
|
+
console.log('[local] service installed (macOS launchd)');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await ensureSystemdUserServiceEnabled({ rootDir, label, env });
|
|
88
|
+
console.log('[local] service installed (Linux systemd --user)');
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export async function uninstallService() {
|
|
84
|
-
if (process.platform !== 'darwin')
|
|
92
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') return;
|
|
93
|
+
|
|
94
|
+
if (process.platform === 'linux') {
|
|
95
|
+
await ensureSystemdUserServiceDisabled({ remove: true });
|
|
96
|
+
console.log('[local] service uninstalled (systemd user unit removed)');
|
|
85
97
|
return;
|
|
86
98
|
}
|
|
87
99
|
const { primaryPlistPath, legacyPlistPath, primaryLabel, legacyLabel } = getDefaultAutostartPaths();
|
|
@@ -102,6 +114,91 @@ export async function uninstallService() {
|
|
|
102
114
|
console.log('[local] service uninstalled (plist removed)');
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
function systemdUnitName() {
|
|
118
|
+
const { label } = getDefaultAutostartPaths();
|
|
119
|
+
return `${label}.service`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function systemdUnitPath() {
|
|
123
|
+
return join(homedir(), '.config', 'systemd', 'user', systemdUnitName());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function systemdEnvLines(env) {
|
|
127
|
+
return Object.entries(env)
|
|
128
|
+
.map(([k, v]) => `Environment=${k}=${String(v)}`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function ensureSystemdUserServiceEnabled({ rootDir, label, env }) {
|
|
133
|
+
const unitPath = systemdUnitPath();
|
|
134
|
+
await mkdir(dirname(unitPath), { recursive: true });
|
|
135
|
+
const happysShim = join(homedir(), '.happy-stacks', 'bin', 'happys');
|
|
136
|
+
const entry = existsSync(happysShim) ? happysShim : join(rootDir, 'bin', 'happys.mjs');
|
|
137
|
+
const exec = existsSync(happysShim) ? entry : `${process.execPath} ${entry}`;
|
|
138
|
+
|
|
139
|
+
const unit = `[Unit]
|
|
140
|
+
Description=Happy Stacks (${label})
|
|
141
|
+
After=network-online.target
|
|
142
|
+
Wants=network-online.target
|
|
143
|
+
|
|
144
|
+
[Service]
|
|
145
|
+
Type=simple
|
|
146
|
+
WorkingDirectory=%h
|
|
147
|
+
${systemdEnvLines(env)}
|
|
148
|
+
ExecStart=${exec} start
|
|
149
|
+
Restart=always
|
|
150
|
+
RestartSec=2
|
|
151
|
+
|
|
152
|
+
[Install]
|
|
153
|
+
WantedBy=default.target
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
await writeFile(unitPath, unit, 'utf-8');
|
|
157
|
+
await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
|
|
158
|
+
await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function ensureSystemdUserServiceDisabled({ remove } = {}) {
|
|
162
|
+
await runCapture('systemctl', ['--user', 'disable', '--now', systemdUnitName()]).catch(() => {});
|
|
163
|
+
await runCapture('systemctl', ['--user', 'stop', systemdUnitName()]).catch(() => {});
|
|
164
|
+
if (remove) {
|
|
165
|
+
await rm(systemdUnitPath(), { force: true }).catch(() => {});
|
|
166
|
+
await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function systemdStatus() {
|
|
171
|
+
await run('systemctl', ['--user', 'status', systemdUnitName(), '--no-pager']);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function systemdStart({ persistent }) {
|
|
175
|
+
if (persistent) {
|
|
176
|
+
await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
|
|
177
|
+
} else {
|
|
178
|
+
await run('systemctl', ['--user', 'start', systemdUnitName()]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function systemdStop({ persistent }) {
|
|
183
|
+
if (persistent) {
|
|
184
|
+
await run('systemctl', ['--user', 'disable', '--now', systemdUnitName()]);
|
|
185
|
+
} else {
|
|
186
|
+
await run('systemctl', ['--user', 'stop', systemdUnitName()]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function systemdRestart() {
|
|
191
|
+
await run('systemctl', ['--user', 'restart', systemdUnitName()]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function systemdLogs({ lines = 120 } = {}) {
|
|
195
|
+
await run('journalctl', ['--user', '-u', systemdUnitName(), '-n', String(lines), '--no-pager']);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function systemdTail() {
|
|
199
|
+
await run('journalctl', ['--user', '-u', systemdUnitName(), '-f']);
|
|
200
|
+
}
|
|
201
|
+
|
|
105
202
|
async function launchctlTry(args) {
|
|
106
203
|
try {
|
|
107
204
|
await runCapture('launchctl', args);
|
|
@@ -306,7 +403,8 @@ async function postStartDiagnostics() {
|
|
|
306
403
|
async function stopLaunchAgent({ persistent }) {
|
|
307
404
|
const { plistPath } = getDefaultAutostartPaths();
|
|
308
405
|
if (!existsSync(plistPath)) {
|
|
309
|
-
|
|
406
|
+
// Service isn't installed for this stack (common for ad-hoc stacks). Treat as a no-op.
|
|
407
|
+
return;
|
|
310
408
|
}
|
|
311
409
|
|
|
312
410
|
const { label } = getDefaultAutostartPaths();
|
|
@@ -400,8 +498,8 @@ async function tailLogs() {
|
|
|
400
498
|
}
|
|
401
499
|
|
|
402
500
|
async function main() {
|
|
403
|
-
if (process.platform !== 'darwin') {
|
|
404
|
-
throw new Error('[local] service commands are only supported on macOS (
|
|
501
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
502
|
+
throw new Error('[local] service commands are only supported on macOS (launchd) and Linux (systemd user).');
|
|
405
503
|
}
|
|
406
504
|
|
|
407
505
|
const argv = process.argv.slice(2);
|
|
@@ -439,19 +537,6 @@ async function main() {
|
|
|
439
537
|
return;
|
|
440
538
|
case 'status':
|
|
441
539
|
if (json) {
|
|
442
|
-
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
443
|
-
let launchctlLine = null;
|
|
444
|
-
try {
|
|
445
|
-
const list = await runCapture('launchctl', ['list']);
|
|
446
|
-
launchctlLine =
|
|
447
|
-
list
|
|
448
|
-
.split('\n')
|
|
449
|
-
.map((l) => l.trim())
|
|
450
|
-
.find((l) => l.endsWith(` ${label}`) || l === label || l.includes(`\t${label}`)) ?? null;
|
|
451
|
-
} catch {
|
|
452
|
-
launchctlLine = null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
540
|
const internalUrl = getInternalUrl();
|
|
456
541
|
let health = null;
|
|
457
542
|
try {
|
|
@@ -462,46 +547,100 @@ async function main() {
|
|
|
462
547
|
health = { ok: false, status: null, body: null };
|
|
463
548
|
}
|
|
464
549
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
550
|
+
if (process.platform === 'darwin') {
|
|
551
|
+
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
552
|
+
let launchctlLine = null;
|
|
553
|
+
try {
|
|
554
|
+
const list = await runCapture('launchctl', ['list']);
|
|
555
|
+
launchctlLine =
|
|
556
|
+
list
|
|
557
|
+
.split('\n')
|
|
558
|
+
.map((l) => l.trim())
|
|
559
|
+
.find((l) => l.endsWith(` ${label}`) || l === label || l.includes(`\t${label}`)) ?? null;
|
|
560
|
+
} catch {
|
|
561
|
+
launchctlLine = null;
|
|
562
|
+
}
|
|
563
|
+
printResult({ json, data: { label, plistPath, stdoutPath, stderrPath, internalUrl, launchctlLine, health } });
|
|
564
|
+
} else {
|
|
565
|
+
const unitName = systemdUnitName();
|
|
566
|
+
const unitPath = systemdUnitPath();
|
|
567
|
+
let systemctlStatus = null;
|
|
568
|
+
try {
|
|
569
|
+
systemctlStatus = await runCapture('systemctl', ['--user', 'status', unitName, '--no-pager']);
|
|
570
|
+
} catch (e) {
|
|
571
|
+
systemctlStatus = e && typeof e === 'object' && 'out' in e ? e.out : null;
|
|
572
|
+
}
|
|
573
|
+
printResult({ json, data: { unitName, unitPath, internalUrl, systemctlStatus, health } });
|
|
574
|
+
}
|
|
469
575
|
} else {
|
|
470
|
-
|
|
576
|
+
if (process.platform === 'darwin') {
|
|
577
|
+
await showStatus();
|
|
578
|
+
} else {
|
|
579
|
+
await systemdStatus();
|
|
580
|
+
}
|
|
471
581
|
}
|
|
472
582
|
return;
|
|
473
583
|
case 'start':
|
|
474
|
-
|
|
584
|
+
if (process.platform === 'darwin') {
|
|
585
|
+
await startLaunchAgent({ persistent: false });
|
|
586
|
+
} else {
|
|
587
|
+
await systemdStart({ persistent: false });
|
|
588
|
+
}
|
|
475
589
|
await postStartDiagnostics();
|
|
476
590
|
if (json) printResult({ json, data: { ok: true, action: 'start' } });
|
|
477
591
|
return;
|
|
478
592
|
case 'stop':
|
|
479
|
-
|
|
593
|
+
if (process.platform === 'darwin') {
|
|
594
|
+
await stopLaunchAgent({ persistent: false });
|
|
595
|
+
} else {
|
|
596
|
+
await systemdStop({ persistent: false });
|
|
597
|
+
}
|
|
480
598
|
if (json) printResult({ json, data: { ok: true, action: 'stop' } });
|
|
481
599
|
return;
|
|
482
600
|
case 'restart':
|
|
483
|
-
if (
|
|
484
|
-
await
|
|
485
|
-
|
|
486
|
-
|
|
601
|
+
if (process.platform === 'darwin') {
|
|
602
|
+
if (!(await restartLaunchAgentBestEffort())) {
|
|
603
|
+
await stopLaunchAgent({ persistent: false });
|
|
604
|
+
await waitForLaunchAgentStopped();
|
|
605
|
+
await startLaunchAgent({ persistent: false });
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
await systemdRestart();
|
|
487
609
|
}
|
|
488
610
|
await postStartDiagnostics();
|
|
489
611
|
if (json) printResult({ json, data: { ok: true, action: 'restart' } });
|
|
490
612
|
return;
|
|
491
613
|
case 'enable':
|
|
492
|
-
|
|
614
|
+
if (process.platform === 'darwin') {
|
|
615
|
+
await startLaunchAgent({ persistent: true });
|
|
616
|
+
} else {
|
|
617
|
+
await systemdStart({ persistent: true });
|
|
618
|
+
}
|
|
493
619
|
await postStartDiagnostics();
|
|
494
620
|
if (json) printResult({ json, data: { ok: true, action: 'enable' } });
|
|
495
621
|
return;
|
|
496
622
|
case 'disable':
|
|
497
|
-
|
|
623
|
+
if (process.platform === 'darwin') {
|
|
624
|
+
await stopLaunchAgent({ persistent: true });
|
|
625
|
+
} else {
|
|
626
|
+
await systemdStop({ persistent: true });
|
|
627
|
+
}
|
|
498
628
|
if (json) printResult({ json, data: { ok: true, action: 'disable' } });
|
|
499
629
|
return;
|
|
500
630
|
case 'logs':
|
|
501
|
-
|
|
631
|
+
if (process.platform === 'darwin') {
|
|
632
|
+
await showLogs();
|
|
633
|
+
} else {
|
|
634
|
+
const lines = Number(process.env.HAPPY_STACKS_LOG_LINES ?? process.env.HAPPY_LOCAL_LOG_LINES ?? 120) || 120;
|
|
635
|
+
await systemdLogs({ lines });
|
|
636
|
+
}
|
|
502
637
|
return;
|
|
503
638
|
case 'tail':
|
|
504
|
-
|
|
639
|
+
if (process.platform === 'darwin') {
|
|
640
|
+
await tailLogs();
|
|
641
|
+
} else {
|
|
642
|
+
await systemdTail();
|
|
643
|
+
}
|
|
505
644
|
return;
|
|
506
645
|
default:
|
|
507
646
|
throw new Error(`[local] unknown command: ${cmd}`);
|