happy-stacks 0.1.0 → 0.2.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/README.md +130 -74
- package/bin/happys.mjs +140 -9
- package/docs/edison.md +381 -0
- package/docs/happy-development.md +733 -0
- package/docs/menubar.md +54 -0
- package/docs/paths-and-env.md +141 -0
- package/docs/server-flavors.md +61 -2
- package/docs/stacks.md +55 -4
- package/extras/swiftbar/auth-login.sh +10 -7
- package/extras/swiftbar/git-cache-refresh.sh +130 -0
- package/extras/swiftbar/happy-stacks.5s.sh +175 -83
- package/extras/swiftbar/happys-term.sh +128 -0
- package/extras/swiftbar/happys.sh +35 -0
- package/extras/swiftbar/install.sh +99 -13
- package/extras/swiftbar/lib/git.sh +309 -1
- package/extras/swiftbar/lib/icons.sh +2 -2
- package/extras/swiftbar/lib/render.sh +279 -132
- package/extras/swiftbar/lib/system.sh +64 -10
- package/extras/swiftbar/lib/utils.sh +469 -10
- package/extras/swiftbar/pnpm-term.sh +2 -122
- package/extras/swiftbar/pnpm.sh +4 -14
- package/extras/swiftbar/set-interval.sh +10 -5
- package/extras/swiftbar/set-server-flavor.sh +19 -10
- package/extras/swiftbar/wt-pr.sh +10 -3
- package/package.json +2 -1
- package/scripts/auth.mjs +833 -14
- package/scripts/build.mjs +24 -4
- package/scripts/cli-link.mjs +3 -3
- package/scripts/completion.mjs +15 -8
- package/scripts/daemon.mjs +200 -23
- package/scripts/dev.mjs +230 -57
- package/scripts/doctor.mjs +26 -21
- package/scripts/edison.mjs +1828 -0
- package/scripts/happy.mjs +3 -7
- package/scripts/init.mjs +275 -46
- package/scripts/install.mjs +14 -8
- package/scripts/lint.mjs +145 -0
- package/scripts/menubar.mjs +81 -8
- package/scripts/migrate.mjs +302 -0
- package/scripts/mobile.mjs +59 -21
- package/scripts/run.mjs +222 -43
- package/scripts/self.mjs +3 -7
- package/scripts/server_flavor.mjs +3 -3
- package/scripts/service.mjs +190 -38
- package/scripts/setup.mjs +790 -0
- package/scripts/setup_pr.mjs +182 -0
- package/scripts/stack.mjs +2273 -92
- package/scripts/stop.mjs +160 -0
- package/scripts/tailscale.mjs +164 -23
- package/scripts/test.mjs +144 -0
- package/scripts/tui.mjs +556 -0
- package/scripts/typecheck.mjs +145 -0
- package/scripts/ui_gateway.mjs +248 -0
- package/scripts/uninstall.mjs +21 -13
- package/scripts/utils/auth_files.mjs +58 -0
- package/scripts/utils/auth_login_ux.mjs +76 -0
- package/scripts/utils/auth_sources.mjs +12 -0
- package/scripts/utils/browser.mjs +22 -0
- package/scripts/utils/canonical_home.mjs +20 -0
- package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
- package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
- package/scripts/utils/config.mjs +13 -1
- package/scripts/utils/dev_auth_key.mjs +169 -0
- package/scripts/utils/dev_daemon.mjs +104 -0
- package/scripts/utils/dev_expo_web.mjs +112 -0
- package/scripts/utils/dev_server.mjs +183 -0
- package/scripts/utils/env.mjs +94 -23
- package/scripts/utils/env_file.mjs +36 -0
- package/scripts/utils/expo.mjs +96 -0
- package/scripts/utils/handy_master_secret.mjs +94 -0
- package/scripts/utils/happy_server_infra.mjs +484 -0
- package/scripts/utils/localhost_host.mjs +17 -0
- package/scripts/utils/ownership.mjs +135 -0
- package/scripts/utils/paths.mjs +5 -2
- package/scripts/utils/pm.mjs +132 -22
- package/scripts/utils/ports.mjs +51 -13
- package/scripts/utils/proc.mjs +75 -7
- package/scripts/utils/runtime.mjs +1 -3
- package/scripts/utils/sandbox.mjs +14 -0
- package/scripts/utils/server.mjs +61 -0
- package/scripts/utils/server_port.mjs +9 -0
- package/scripts/utils/server_urls.mjs +54 -0
- package/scripts/utils/stack_context.mjs +23 -0
- package/scripts/utils/stack_runtime_state.mjs +104 -0
- package/scripts/utils/stack_startup.mjs +208 -0
- package/scripts/utils/stack_stop.mjs +255 -0
- package/scripts/utils/stacks.mjs +38 -0
- package/scripts/utils/validate.mjs +42 -1
- package/scripts/utils/watch.mjs +63 -0
- package/scripts/utils/worktrees.mjs +57 -1
- package/scripts/where.mjs +14 -7
- package/scripts/worktrees.mjs +135 -15
- /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
- /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
- /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
package/scripts/service.mjs
CHANGED
|
@@ -2,17 +2,22 @@ import './utils/env.mjs';
|
|
|
2
2
|
import { run, runCapture } from './utils/proc.mjs';
|
|
3
3
|
import { getDefaultAutostartPaths, getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
|
|
4
4
|
import { ensureMacAutostartDisabled, ensureMacAutostartEnabled } from './utils/pm.mjs';
|
|
5
|
+
import { getCanonicalHomeDir } from './utils/config.mjs';
|
|
6
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
|
|
5
7
|
import { spawn } from 'node:child_process';
|
|
6
8
|
import { homedir } from 'node:os';
|
|
7
9
|
import { existsSync } from 'node:fs';
|
|
8
10
|
import { rm } from 'node:fs/promises';
|
|
9
11
|
import { dirname, join, resolve } from 'node:path';
|
|
10
12
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
|
|
13
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
12
14
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Manage the
|
|
17
|
+
* Manage the autostart service installed by `happys bootstrap -- --autostart`.
|
|
18
|
+
*
|
|
19
|
+
* - macOS: launchd LaunchAgents
|
|
20
|
+
* - Linux: systemd user services
|
|
16
21
|
*
|
|
17
22
|
* Commands:
|
|
18
23
|
* - install | uninstall
|
|
@@ -60,8 +65,15 @@ function getAutostartEnv({ rootDir }) {
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export async function installService() {
|
|
63
|
-
if (
|
|
64
|
-
throw new Error(
|
|
68
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'[local] service install is disabled in sandbox mode.\n' +
|
|
71
|
+
'Reason: services are global OS state (launchd/systemd) and can affect your real installation.\n' +
|
|
72
|
+
'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
76
|
+
throw new Error('[local] service install is only supported on macOS (launchd) and Linux (systemd user).');
|
|
65
77
|
}
|
|
66
78
|
const rootDir = getRootDir(import.meta.url);
|
|
67
79
|
const { primaryLabel: label } = getDefaultAutostartPaths();
|
|
@@ -76,12 +88,25 @@ export async function installService() {
|
|
|
76
88
|
} catch {
|
|
77
89
|
// ignore
|
|
78
90
|
}
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
if (process.platform === 'darwin') {
|
|
92
|
+
await ensureMacAutostartEnabled({ rootDir, label, env });
|
|
93
|
+
console.log('[local] service installed (macOS launchd)');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await ensureSystemdUserServiceEnabled({ rootDir, label, env });
|
|
97
|
+
console.log('[local] service installed (Linux systemd --user)');
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
export async function uninstallService() {
|
|
84
|
-
if (
|
|
101
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
102
|
+
// Sandbox cleanups should be safe and should not touch global services by default.
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') return;
|
|
106
|
+
|
|
107
|
+
if (process.platform === 'linux') {
|
|
108
|
+
await ensureSystemdUserServiceDisabled({ remove: true });
|
|
109
|
+
console.log('[local] service uninstalled (systemd user unit removed)');
|
|
85
110
|
return;
|
|
86
111
|
}
|
|
87
112
|
const { primaryPlistPath, legacyPlistPath, primaryLabel, legacyLabel } = getDefaultAutostartPaths();
|
|
@@ -102,6 +127,91 @@ export async function uninstallService() {
|
|
|
102
127
|
console.log('[local] service uninstalled (plist removed)');
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
function systemdUnitName() {
|
|
131
|
+
const { label } = getDefaultAutostartPaths();
|
|
132
|
+
return `${label}.service`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function systemdUnitPath() {
|
|
136
|
+
return join(homedir(), '.config', 'systemd', 'user', systemdUnitName());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function systemdEnvLines(env) {
|
|
140
|
+
return Object.entries(env)
|
|
141
|
+
.map(([k, v]) => `Environment=${k}=${String(v)}`)
|
|
142
|
+
.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function ensureSystemdUserServiceEnabled({ rootDir, label, env }) {
|
|
146
|
+
const unitPath = systemdUnitPath();
|
|
147
|
+
await mkdir(dirname(unitPath), { recursive: true });
|
|
148
|
+
const happysShim = join(getCanonicalHomeDir(), 'bin', 'happys');
|
|
149
|
+
const entry = existsSync(happysShim) ? happysShim : join(rootDir, 'bin', 'happys.mjs');
|
|
150
|
+
const exec = existsSync(happysShim) ? entry : `${process.execPath} ${entry}`;
|
|
151
|
+
|
|
152
|
+
const unit = `[Unit]
|
|
153
|
+
Description=Happy Stacks (${label})
|
|
154
|
+
After=network-online.target
|
|
155
|
+
Wants=network-online.target
|
|
156
|
+
|
|
157
|
+
[Service]
|
|
158
|
+
Type=simple
|
|
159
|
+
WorkingDirectory=%h
|
|
160
|
+
${systemdEnvLines(env)}
|
|
161
|
+
ExecStart=${exec} start
|
|
162
|
+
Restart=always
|
|
163
|
+
RestartSec=2
|
|
164
|
+
|
|
165
|
+
[Install]
|
|
166
|
+
WantedBy=default.target
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
await writeFile(unitPath, unit, 'utf-8');
|
|
170
|
+
await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
|
|
171
|
+
await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function ensureSystemdUserServiceDisabled({ remove } = {}) {
|
|
175
|
+
await runCapture('systemctl', ['--user', 'disable', '--now', systemdUnitName()]).catch(() => {});
|
|
176
|
+
await runCapture('systemctl', ['--user', 'stop', systemdUnitName()]).catch(() => {});
|
|
177
|
+
if (remove) {
|
|
178
|
+
await rm(systemdUnitPath(), { force: true }).catch(() => {});
|
|
179
|
+
await runCapture('systemctl', ['--user', 'daemon-reload']).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function systemdStatus() {
|
|
184
|
+
await run('systemctl', ['--user', 'status', systemdUnitName(), '--no-pager']);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function systemdStart({ persistent }) {
|
|
188
|
+
if (persistent) {
|
|
189
|
+
await run('systemctl', ['--user', 'enable', '--now', systemdUnitName()]);
|
|
190
|
+
} else {
|
|
191
|
+
await run('systemctl', ['--user', 'start', systemdUnitName()]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function systemdStop({ persistent }) {
|
|
196
|
+
if (persistent) {
|
|
197
|
+
await run('systemctl', ['--user', 'disable', '--now', systemdUnitName()]);
|
|
198
|
+
} else {
|
|
199
|
+
await run('systemctl', ['--user', 'stop', systemdUnitName()]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function systemdRestart() {
|
|
204
|
+
await run('systemctl', ['--user', 'restart', systemdUnitName()]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function systemdLogs({ lines = 120 } = {}) {
|
|
208
|
+
await run('journalctl', ['--user', '-u', systemdUnitName(), '-n', String(lines), '--no-pager']);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function systemdTail() {
|
|
212
|
+
await run('journalctl', ['--user', '-u', systemdUnitName(), '-f']);
|
|
213
|
+
}
|
|
214
|
+
|
|
105
215
|
async function launchctlTry(args) {
|
|
106
216
|
try {
|
|
107
217
|
await runCapture('launchctl', args);
|
|
@@ -306,7 +416,8 @@ async function postStartDiagnostics() {
|
|
|
306
416
|
async function stopLaunchAgent({ persistent }) {
|
|
307
417
|
const { plistPath } = getDefaultAutostartPaths();
|
|
308
418
|
if (!existsSync(plistPath)) {
|
|
309
|
-
|
|
419
|
+
// Service isn't installed for this stack (common for ad-hoc stacks). Treat as a no-op.
|
|
420
|
+
return;
|
|
310
421
|
}
|
|
311
422
|
|
|
312
423
|
const { label } = getDefaultAutostartPaths();
|
|
@@ -400,8 +511,8 @@ async function tailLogs() {
|
|
|
400
511
|
}
|
|
401
512
|
|
|
402
513
|
async function main() {
|
|
403
|
-
if (process.platform !== 'darwin') {
|
|
404
|
-
throw new Error('[local] service commands are only supported on macOS (
|
|
514
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
515
|
+
throw new Error('[local] service commands are only supported on macOS (launchd) and Linux (systemd user).');
|
|
405
516
|
}
|
|
406
517
|
|
|
407
518
|
const argv = process.argv.slice(2);
|
|
@@ -439,19 +550,6 @@ async function main() {
|
|
|
439
550
|
return;
|
|
440
551
|
case 'status':
|
|
441
552
|
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
553
|
const internalUrl = getInternalUrl();
|
|
456
554
|
let health = null;
|
|
457
555
|
try {
|
|
@@ -462,46 +560,100 @@ async function main() {
|
|
|
462
560
|
health = { ok: false, status: null, body: null };
|
|
463
561
|
}
|
|
464
562
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
563
|
+
if (process.platform === 'darwin') {
|
|
564
|
+
const { plistPath, stdoutPath, stderrPath, label } = getDefaultAutostartPaths();
|
|
565
|
+
let launchctlLine = null;
|
|
566
|
+
try {
|
|
567
|
+
const list = await runCapture('launchctl', ['list']);
|
|
568
|
+
launchctlLine =
|
|
569
|
+
list
|
|
570
|
+
.split('\n')
|
|
571
|
+
.map((l) => l.trim())
|
|
572
|
+
.find((l) => l.endsWith(` ${label}`) || l === label || l.includes(`\t${label}`)) ?? null;
|
|
573
|
+
} catch {
|
|
574
|
+
launchctlLine = null;
|
|
575
|
+
}
|
|
576
|
+
printResult({ json, data: { label, plistPath, stdoutPath, stderrPath, internalUrl, launchctlLine, health } });
|
|
577
|
+
} else {
|
|
578
|
+
const unitName = systemdUnitName();
|
|
579
|
+
const unitPath = systemdUnitPath();
|
|
580
|
+
let systemctlStatus = null;
|
|
581
|
+
try {
|
|
582
|
+
systemctlStatus = await runCapture('systemctl', ['--user', 'status', unitName, '--no-pager']);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
systemctlStatus = e && typeof e === 'object' && 'out' in e ? e.out : null;
|
|
585
|
+
}
|
|
586
|
+
printResult({ json, data: { unitName, unitPath, internalUrl, systemctlStatus, health } });
|
|
587
|
+
}
|
|
469
588
|
} else {
|
|
470
|
-
|
|
589
|
+
if (process.platform === 'darwin') {
|
|
590
|
+
await showStatus();
|
|
591
|
+
} else {
|
|
592
|
+
await systemdStatus();
|
|
593
|
+
}
|
|
471
594
|
}
|
|
472
595
|
return;
|
|
473
596
|
case 'start':
|
|
474
|
-
|
|
597
|
+
if (process.platform === 'darwin') {
|
|
598
|
+
await startLaunchAgent({ persistent: false });
|
|
599
|
+
} else {
|
|
600
|
+
await systemdStart({ persistent: false });
|
|
601
|
+
}
|
|
475
602
|
await postStartDiagnostics();
|
|
476
603
|
if (json) printResult({ json, data: { ok: true, action: 'start' } });
|
|
477
604
|
return;
|
|
478
605
|
case 'stop':
|
|
479
|
-
|
|
606
|
+
if (process.platform === 'darwin') {
|
|
607
|
+
await stopLaunchAgent({ persistent: false });
|
|
608
|
+
} else {
|
|
609
|
+
await systemdStop({ persistent: false });
|
|
610
|
+
}
|
|
480
611
|
if (json) printResult({ json, data: { ok: true, action: 'stop' } });
|
|
481
612
|
return;
|
|
482
613
|
case 'restart':
|
|
483
|
-
if (
|
|
484
|
-
await
|
|
485
|
-
|
|
486
|
-
|
|
614
|
+
if (process.platform === 'darwin') {
|
|
615
|
+
if (!(await restartLaunchAgentBestEffort())) {
|
|
616
|
+
await stopLaunchAgent({ persistent: false });
|
|
617
|
+
await waitForLaunchAgentStopped();
|
|
618
|
+
await startLaunchAgent({ persistent: false });
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
await systemdRestart();
|
|
487
622
|
}
|
|
488
623
|
await postStartDiagnostics();
|
|
489
624
|
if (json) printResult({ json, data: { ok: true, action: 'restart' } });
|
|
490
625
|
return;
|
|
491
626
|
case 'enable':
|
|
492
|
-
|
|
627
|
+
if (process.platform === 'darwin') {
|
|
628
|
+
await startLaunchAgent({ persistent: true });
|
|
629
|
+
} else {
|
|
630
|
+
await systemdStart({ persistent: true });
|
|
631
|
+
}
|
|
493
632
|
await postStartDiagnostics();
|
|
494
633
|
if (json) printResult({ json, data: { ok: true, action: 'enable' } });
|
|
495
634
|
return;
|
|
496
635
|
case 'disable':
|
|
497
|
-
|
|
636
|
+
if (process.platform === 'darwin') {
|
|
637
|
+
await stopLaunchAgent({ persistent: true });
|
|
638
|
+
} else {
|
|
639
|
+
await systemdStop({ persistent: true });
|
|
640
|
+
}
|
|
498
641
|
if (json) printResult({ json, data: { ok: true, action: 'disable' } });
|
|
499
642
|
return;
|
|
500
643
|
case 'logs':
|
|
501
|
-
|
|
644
|
+
if (process.platform === 'darwin') {
|
|
645
|
+
await showLogs();
|
|
646
|
+
} else {
|
|
647
|
+
const lines = Number(process.env.HAPPY_STACKS_LOG_LINES ?? process.env.HAPPY_LOCAL_LOG_LINES ?? 120) || 120;
|
|
648
|
+
await systemdLogs({ lines });
|
|
649
|
+
}
|
|
502
650
|
return;
|
|
503
651
|
case 'tail':
|
|
504
|
-
|
|
652
|
+
if (process.platform === 'darwin') {
|
|
653
|
+
await tailLogs();
|
|
654
|
+
} else {
|
|
655
|
+
await systemdTail();
|
|
656
|
+
}
|
|
505
657
|
return;
|
|
506
658
|
default:
|
|
507
659
|
throw new Error(`[local] unknown command: ${cmd}`);
|