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.
Files changed (95) hide show
  1. package/README.md +130 -74
  2. package/bin/happys.mjs +140 -9
  3. package/docs/edison.md +381 -0
  4. package/docs/happy-development.md +733 -0
  5. package/docs/menubar.md +54 -0
  6. package/docs/paths-and-env.md +141 -0
  7. package/docs/server-flavors.md +61 -2
  8. package/docs/stacks.md +55 -4
  9. package/extras/swiftbar/auth-login.sh +10 -7
  10. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +175 -83
  12. package/extras/swiftbar/happys-term.sh +128 -0
  13. package/extras/swiftbar/happys.sh +35 -0
  14. package/extras/swiftbar/install.sh +99 -13
  15. package/extras/swiftbar/lib/git.sh +309 -1
  16. package/extras/swiftbar/lib/icons.sh +2 -2
  17. package/extras/swiftbar/lib/render.sh +279 -132
  18. package/extras/swiftbar/lib/system.sh +64 -10
  19. package/extras/swiftbar/lib/utils.sh +469 -10
  20. package/extras/swiftbar/pnpm-term.sh +2 -122
  21. package/extras/swiftbar/pnpm.sh +4 -14
  22. package/extras/swiftbar/set-interval.sh +10 -5
  23. package/extras/swiftbar/set-server-flavor.sh +19 -10
  24. package/extras/swiftbar/wt-pr.sh +10 -3
  25. package/package.json +2 -1
  26. package/scripts/auth.mjs +833 -14
  27. package/scripts/build.mjs +24 -4
  28. package/scripts/cli-link.mjs +3 -3
  29. package/scripts/completion.mjs +15 -8
  30. package/scripts/daemon.mjs +200 -23
  31. package/scripts/dev.mjs +230 -57
  32. package/scripts/doctor.mjs +26 -21
  33. package/scripts/edison.mjs +1828 -0
  34. package/scripts/happy.mjs +3 -7
  35. package/scripts/init.mjs +275 -46
  36. package/scripts/install.mjs +14 -8
  37. package/scripts/lint.mjs +145 -0
  38. package/scripts/menubar.mjs +81 -8
  39. package/scripts/migrate.mjs +302 -0
  40. package/scripts/mobile.mjs +59 -21
  41. package/scripts/run.mjs +222 -43
  42. package/scripts/self.mjs +3 -7
  43. package/scripts/server_flavor.mjs +3 -3
  44. package/scripts/service.mjs +190 -38
  45. package/scripts/setup.mjs +790 -0
  46. package/scripts/setup_pr.mjs +182 -0
  47. package/scripts/stack.mjs +2273 -92
  48. package/scripts/stop.mjs +160 -0
  49. package/scripts/tailscale.mjs +164 -23
  50. package/scripts/test.mjs +144 -0
  51. package/scripts/tui.mjs +556 -0
  52. package/scripts/typecheck.mjs +145 -0
  53. package/scripts/ui_gateway.mjs +248 -0
  54. package/scripts/uninstall.mjs +21 -13
  55. package/scripts/utils/auth_files.mjs +58 -0
  56. package/scripts/utils/auth_login_ux.mjs +76 -0
  57. package/scripts/utils/auth_sources.mjs +12 -0
  58. package/scripts/utils/browser.mjs +22 -0
  59. package/scripts/utils/canonical_home.mjs +20 -0
  60. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +71 -0
  61. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  62. package/scripts/utils/config.mjs +13 -1
  63. package/scripts/utils/dev_auth_key.mjs +169 -0
  64. package/scripts/utils/dev_daemon.mjs +104 -0
  65. package/scripts/utils/dev_expo_web.mjs +112 -0
  66. package/scripts/utils/dev_server.mjs +183 -0
  67. package/scripts/utils/env.mjs +94 -23
  68. package/scripts/utils/env_file.mjs +36 -0
  69. package/scripts/utils/expo.mjs +96 -0
  70. package/scripts/utils/handy_master_secret.mjs +94 -0
  71. package/scripts/utils/happy_server_infra.mjs +484 -0
  72. package/scripts/utils/localhost_host.mjs +17 -0
  73. package/scripts/utils/ownership.mjs +135 -0
  74. package/scripts/utils/paths.mjs +5 -2
  75. package/scripts/utils/pm.mjs +132 -22
  76. package/scripts/utils/ports.mjs +51 -13
  77. package/scripts/utils/proc.mjs +75 -7
  78. package/scripts/utils/runtime.mjs +1 -3
  79. package/scripts/utils/sandbox.mjs +14 -0
  80. package/scripts/utils/server.mjs +61 -0
  81. package/scripts/utils/server_port.mjs +9 -0
  82. package/scripts/utils/server_urls.mjs +54 -0
  83. package/scripts/utils/stack_context.mjs +23 -0
  84. package/scripts/utils/stack_runtime_state.mjs +104 -0
  85. package/scripts/utils/stack_startup.mjs +208 -0
  86. package/scripts/utils/stack_stop.mjs +255 -0
  87. package/scripts/utils/stacks.mjs +38 -0
  88. package/scripts/utils/validate.mjs +42 -1
  89. package/scripts/utils/watch.mjs +63 -0
  90. package/scripts/utils/worktrees.mjs +57 -1
  91. package/scripts/where.mjs +14 -7
  92. package/scripts/worktrees.mjs +135 -15
  93. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  94. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  95. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
@@ -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 macOS LaunchAgent installed by `happys bootstrap -- --autostart`.
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 (process.platform !== 'darwin') {
64
- throw new Error('[local] service install is only supported on macOS (LaunchAgents).');
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
- await ensureMacAutostartEnabled({ rootDir, label, env });
80
- console.log('[local] service installed (macOS LaunchAgent)');
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 (process.platform !== 'darwin') {
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
- throw new Error(`[local] LaunchAgent plist not found at ${plistPath}. Run: happys service:install (or happys bootstrap -- --autostart)`);
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 (LaunchAgents).');
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
- printResult({
466
- json,
467
- data: { label, plistPath, stdoutPath, stderrPath, internalUrl, launchctlLine, health },
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
- await showStatus();
589
+ if (process.platform === 'darwin') {
590
+ await showStatus();
591
+ } else {
592
+ await systemdStatus();
593
+ }
471
594
  }
472
595
  return;
473
596
  case 'start':
474
- await startLaunchAgent({ persistent: false });
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
- await stopLaunchAgent({ persistent: false });
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 (!(await restartLaunchAgentBestEffort())) {
484
- await stopLaunchAgent({ persistent: false });
485
- await waitForLaunchAgentStopped();
486
- await startLaunchAgent({ persistent: false });
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
- await startLaunchAgent({ persistent: true });
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
- await stopLaunchAgent({ persistent: true });
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
- await showLogs();
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
- await tailLogs();
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}`);