happy-stacks 0.1.2 → 0.3.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 (116) hide show
  1. package/README.md +164 -89
  2. package/bin/happys.mjs +70 -10
  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/stacks.md +39 -0
  8. package/extras/swiftbar/auth-login.sh +5 -2
  9. package/extras/swiftbar/git-cache-refresh.sh +130 -0
  10. package/extras/swiftbar/happy-stacks.5s.sh +131 -81
  11. package/extras/swiftbar/happys-term.sh +15 -38
  12. package/extras/swiftbar/happys.sh +15 -32
  13. package/extras/swiftbar/install.sh +99 -13
  14. package/extras/swiftbar/lib/git.sh +309 -1
  15. package/extras/swiftbar/lib/icons.sh +2 -2
  16. package/extras/swiftbar/lib/render.sh +209 -80
  17. package/extras/swiftbar/lib/system.sh +27 -4
  18. package/extras/swiftbar/lib/utils.sh +311 -28
  19. package/extras/swiftbar/pnpm.sh +2 -1
  20. package/extras/swiftbar/set-interval.sh +10 -5
  21. package/extras/swiftbar/set-server-flavor.sh +11 -2
  22. package/extras/swiftbar/wt-pr.sh +9 -2
  23. package/package.json +2 -1
  24. package/scripts/auth.mjs +521 -226
  25. package/scripts/build.mjs +29 -10
  26. package/scripts/cli-link.mjs +6 -6
  27. package/scripts/completion.mjs +18 -11
  28. package/scripts/daemon.mjs +133 -31
  29. package/scripts/dev.mjs +196 -137
  30. package/scripts/doctor.mjs +44 -55
  31. package/scripts/edison.mjs +1853 -0
  32. package/scripts/happy.mjs +10 -25
  33. package/scripts/init.mjs +46 -31
  34. package/scripts/install.mjs +21 -15
  35. package/scripts/lint.mjs +124 -0
  36. package/scripts/menubar.mjs +76 -10
  37. package/scripts/migrate.mjs +35 -35
  38. package/scripts/mobile.mjs +24 -17
  39. package/scripts/run.mjs +122 -35
  40. package/scripts/self.mjs +13 -35
  41. package/scripts/server_flavor.mjs +7 -7
  42. package/scripts/service.mjs +31 -28
  43. package/scripts/setup.mjs +694 -0
  44. package/scripts/setup_pr.mjs +165 -0
  45. package/scripts/stack.mjs +1851 -363
  46. package/scripts/stop.mjs +9 -6
  47. package/scripts/tailscale.mjs +23 -11
  48. package/scripts/test.mjs +123 -0
  49. package/scripts/tui.mjs +526 -0
  50. package/scripts/typecheck.mjs +10 -31
  51. package/scripts/ui_gateway.mjs +3 -3
  52. package/scripts/uninstall.mjs +21 -13
  53. package/scripts/utils/auth/dev_key.mjs +163 -0
  54. package/scripts/utils/auth/files.mjs +56 -0
  55. package/scripts/utils/auth/handy_master_secret.mjs +68 -0
  56. package/scripts/utils/auth/login_ux.mjs +76 -0
  57. package/scripts/utils/auth/sources.mjs +12 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/cli/flags.mjs +17 -0
  60. package/scripts/utils/cli/normalize.mjs +16 -0
  61. package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +2 -2
  62. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  63. package/scripts/utils/crypto/tokens.mjs +14 -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/{config.mjs → env/config.mjs} +8 -3
  68. package/scripts/utils/{dotenv.mjs → env/dotenv.mjs} +3 -0
  69. package/scripts/utils/{env.mjs → env/env.mjs} +64 -13
  70. package/scripts/utils/{env_file.mjs → env/env_file.mjs} +38 -1
  71. package/scripts/utils/{env_local.mjs → env/env_local.mjs} +1 -0
  72. package/scripts/utils/env/read.mjs +30 -0
  73. package/scripts/utils/env/sandbox.mjs +14 -0
  74. package/scripts/utils/env/values.mjs +13 -0
  75. package/scripts/utils/{expo.mjs → expo/expo.mjs} +7 -11
  76. package/scripts/utils/fs/json.mjs +25 -0
  77. package/scripts/utils/fs/ops.mjs +29 -0
  78. package/scripts/utils/fs/package_json.mjs +8 -0
  79. package/scripts/utils/fs/tail.mjs +12 -0
  80. package/scripts/utils/git/refs.mjs +26 -0
  81. package/scripts/utils/{worktrees.mjs → git/worktrees.mjs} +60 -4
  82. package/scripts/utils/net/dns.mjs +10 -0
  83. package/scripts/utils/{ports.mjs → net/ports.mjs} +3 -5
  84. package/scripts/utils/paths/canonical_home.mjs +20 -0
  85. package/scripts/utils/paths/localhost_host.mjs +9 -0
  86. package/scripts/utils/{paths.mjs → paths/paths.mjs} +14 -8
  87. package/scripts/utils/{runtime.mjs → paths/runtime.mjs} +4 -4
  88. package/scripts/utils/proc/commands.mjs +34 -0
  89. package/scripts/utils/proc/ownership.mjs +135 -0
  90. package/scripts/utils/proc/package_scripts.mjs +31 -0
  91. package/scripts/utils/proc/pids.mjs +11 -0
  92. package/scripts/utils/proc/pm.mjs +317 -0
  93. package/scripts/utils/{proc.mjs → proc/proc.mjs} +30 -2
  94. package/scripts/utils/proc/watch.mjs +63 -0
  95. package/scripts/utils/{happy_server_infra.mjs → server/infra/happy_server_infra.mjs} +109 -94
  96. package/scripts/utils/server/port.mjs +68 -0
  97. package/scripts/utils/{server.mjs → server/server.mjs} +36 -0
  98. package/scripts/utils/server/urls.mjs +91 -0
  99. package/scripts/utils/{validate.mjs → server/validate.mjs} +1 -1
  100. package/scripts/utils/service/autostart_darwin.mjs +142 -0
  101. package/scripts/utils/stack/context.mjs +23 -0
  102. package/scripts/utils/stack/dirs.mjs +27 -0
  103. package/scripts/utils/stack/editor_workspace.mjs +152 -0
  104. package/scripts/utils/stack/names.mjs +12 -0
  105. package/scripts/utils/stack/runtime_state.mjs +87 -0
  106. package/scripts/utils/stack/stacks.mjs +45 -0
  107. package/scripts/utils/stack/startup.mjs +208 -0
  108. package/scripts/utils/{stack_stop.mjs → stack/stop.mjs} +85 -42
  109. package/scripts/utils/ui/browser.mjs +22 -0
  110. package/scripts/utils/ui/text.mjs +16 -0
  111. package/scripts/where.mjs +17 -10
  112. package/scripts/worktrees.mjs +110 -64
  113. package/scripts/utils/pm.mjs +0 -303
  114. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  115. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  116. /package/scripts/utils/{fs.mjs → fs/fs.mjs} +0 -0
package/scripts/stop.mjs CHANGED
@@ -1,16 +1,16 @@
1
- import './utils/env.mjs';
1
+ import './utils/env/env.mjs';
2
2
  import { join } from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
4
 
5
- import { parseArgs } from './utils/args.mjs';
6
- import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
7
- import { run, runCapture } from './utils/proc.mjs';
8
- import { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
5
+ import { parseArgs } from './utils/cli/args.mjs';
6
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
7
+ import { run, runCapture } from './utils/proc/proc.mjs';
8
+ import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
9
9
 
10
10
  function usage() {
11
11
  return [
12
12
  '[stop] usage:',
13
- ' happys stop [--except-stacks=main,exp1] [--yes] [--aggressive] [--no-docker] [--no-service] [--json]',
13
+ ' happys stop [--except-stacks=main,exp1] [--yes] [--aggressive] [--sweep-owned] [--no-docker] [--no-service] [--json]',
14
14
  '',
15
15
  'Stops stacks and related local processes (server, daemon, Expo, managed infra) using stack-scoped commands.',
16
16
  '',
@@ -18,6 +18,7 @@ function usage() {
18
18
  ' happys stop --except-stacks=main --yes',
19
19
  ' happys stop --yes --no-docker',
20
20
  ' happys stop --except-stacks=main --yes --aggressive',
21
+ ' happys stop --except-stacks=main --yes --aggressive --sweep-owned',
21
22
  ].join('\n');
22
23
  }
23
24
 
@@ -59,6 +60,7 @@ async function main() {
59
60
  const exceptStacks = new Set(parseCsv(kv.get('--except-stacks')));
60
61
  const yes = flags.has('--yes');
61
62
  const aggressive = flags.has('--aggressive');
63
+ const sweepOwned = flags.has('--sweep-owned');
62
64
  const noDocker = flags.has('--no-docker');
63
65
  const noService = flags.has('--no-service');
64
66
 
@@ -108,6 +110,7 @@ async function main() {
108
110
  'stop',
109
111
  stackName,
110
112
  ...(aggressive ? ['--aggressive'] : []),
113
+ ...(sweepOwned ? ['--sweep-owned'] : []),
111
114
  ...(noDocker ? ['--no-docker'] : []),
112
115
  ];
113
116
  if (json) {
@@ -1,7 +1,10 @@
1
- import './utils/env.mjs';
2
- import { parseArgs } from './utils/args.mjs';
3
- import { run, runCapture } from './utils/proc.mjs';
4
- import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
1
+ import './utils/env/env.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
+ import { run, runCapture } from './utils/proc/proc.mjs';
4
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
5
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
6
+ import { getInternalServerUrl } from './utils/server/urls.mjs';
7
+ import { resolveCommandPath } from './utils/proc/commands.mjs';
5
8
  import { constants } from 'node:fs';
6
9
  import { access } from 'node:fs/promises';
7
10
 
@@ -20,11 +23,6 @@ import { access } from 'node:fs/promises';
20
23
  * - url (print the first https:// URL from status output)
21
24
  */
22
25
 
23
- function getInternalServerUrl() {
24
- const port = process.env.HAPPY_LOCAL_SERVER_PORT?.trim() ? Number(process.env.HAPPY_LOCAL_SERVER_PORT) : 3005;
25
- return `http://127.0.0.1:${port}`;
26
- }
27
-
28
26
  function getServeConfig(internalServerUrl) {
29
27
  const upstream = process.env.HAPPY_LOCAL_TAILSCALE_UPSTREAM?.trim()
30
28
  ? process.env.HAPPY_LOCAL_TAILSCALE_UPSTREAM.trim()
@@ -133,7 +131,7 @@ async function resolveTailscaleCmd() {
133
131
 
134
132
  // Try PATH first (without executing `tailscale`, which can hang in some environments).
135
133
  try {
136
- const found = (await runCapture('sh', ['-lc', 'command -v tailscale 2>/dev/null || true'], { env: tailscaleEnv(), timeoutMs: tailscaleProbeTimeoutMs() })).trim();
134
+ const found = await resolveCommandPath('tailscale', { env: tailscaleEnv(), timeoutMs: tailscaleProbeTimeoutMs() });
137
135
  if (found) {
138
136
  return found;
139
137
  }
@@ -341,7 +339,7 @@ async function main() {
341
339
  return;
342
340
  }
343
341
 
344
- const internalServerUrl = getInternalServerUrl();
342
+ const internalServerUrl = getInternalServerUrl({ env: process.env, defaultPort: 3005 }).internalServerUrl;
345
343
  if (flags.has('--upstream') || kv.get('--upstream')) {
346
344
  process.env.HAPPY_LOCAL_TAILSCALE_UPSTREAM = kv.get('--upstream') ?? internalServerUrl;
347
345
  }
@@ -369,6 +367,13 @@ async function main() {
369
367
  return;
370
368
  }
371
369
  case 'enable': {
370
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
371
+ throw new Error(
372
+ '[tailscale] enable is disabled in sandbox mode.\n' +
373
+ 'Reason: Tailscale Serve is global machine state.\n' +
374
+ 'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
375
+ );
376
+ }
372
377
  const res = await tailscaleServeEnable({ internalServerUrl });
373
378
  if (res?.enableUrl && !res?.httpsUrl) {
374
379
  printResult({
@@ -387,6 +392,13 @@ async function main() {
387
392
  }
388
393
  case 'disable':
389
394
  case 'reset': {
395
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
396
+ throw new Error(
397
+ '[tailscale] disable/reset is disabled in sandbox mode.\n' +
398
+ 'Reason: Tailscale Serve is global machine state.\n' +
399
+ 'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
400
+ );
401
+ }
390
402
  await tailscaleServeReset();
391
403
  printResult({ json, data: { ok: true }, text: '[local] tailscale serve reset' });
392
404
  return;
@@ -0,0 +1,123 @@
1
+ import './utils/env/env.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
4
+ import { getComponentDir, getRootDir } from './utils/paths/paths.mjs';
5
+ import { ensureDepsInstalled } from './utils/proc/pm.mjs';
6
+ import { pathExists } from './utils/fs/fs.mjs';
7
+ import { run } from './utils/proc/proc.mjs';
8
+ import { detectPackageManagerCmd, pickFirstScript, readPackageJsonScripts } from './utils/proc/package_scripts.mjs';
9
+
10
+ const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
11
+
12
+ function pickTestScript(scripts) {
13
+ const candidates = [
14
+ 'test',
15
+ 'tst',
16
+ 'test:ci',
17
+ 'test:unit',
18
+ 'check:test',
19
+ ];
20
+ return pickFirstScript(scripts, candidates);
21
+ }
22
+
23
+ async function main() {
24
+ const argv = process.argv.slice(2);
25
+ const { flags } = parseArgs(argv);
26
+ const json = wantsJson(argv, { flags });
27
+
28
+ if (wantsHelp(argv, { flags })) {
29
+ printResult({
30
+ json,
31
+ data: { components: DEFAULT_COMPONENTS, flags: ['--json'] },
32
+ text: [
33
+ '[test] usage:',
34
+ ' happys test [component...] [--json]',
35
+ '',
36
+ 'components:',
37
+ ` ${DEFAULT_COMPONENTS.join(' | ')}`,
38
+ '',
39
+ 'examples:',
40
+ ' happys test',
41
+ ' happys test happy happy-cli',
42
+ ].join('\n'),
43
+ });
44
+ return;
45
+ }
46
+
47
+ const positionals = argv.filter((a) => !a.startsWith('--'));
48
+ const requested = positionals.length ? positionals : ['all'];
49
+ const wantAll = requested.includes('all');
50
+ const components = wantAll ? DEFAULT_COMPONENTS : requested;
51
+
52
+ const rootDir = getRootDir(import.meta.url);
53
+
54
+ const results = [];
55
+ for (const component of components) {
56
+ if (!DEFAULT_COMPONENTS.includes(component)) {
57
+ results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${DEFAULT_COMPONENTS.join(', ')})` });
58
+ continue;
59
+ }
60
+
61
+ const dir = getComponentDir(rootDir, component);
62
+ if (!(await pathExists(dir))) {
63
+ results.push({ component, ok: false, skipped: false, dir, error: `missing component dir: ${dir}` });
64
+ continue;
65
+ }
66
+
67
+ const scripts = await readPackageJsonScripts(dir);
68
+ if (!scripts) {
69
+ results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
70
+ continue;
71
+ }
72
+
73
+ const script = pickTestScript(scripts);
74
+ if (!script) {
75
+ results.push({ component, ok: true, skipped: true, dir, reason: 'no test script found in package.json' });
76
+ continue;
77
+ }
78
+
79
+ await ensureDepsInstalled(dir, component);
80
+ const pm = await detectPackageManagerCmd(dir);
81
+
82
+ try {
83
+ // eslint-disable-next-line no-console
84
+ console.log(`[test] ${component}: running ${pm.name} ${script}`);
85
+ await run(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
86
+ results.push({ component, ok: true, skipped: false, dir, pm: pm.name, script });
87
+ } catch (e) {
88
+ results.push({ component, ok: false, skipped: false, dir, pm: pm.name, script, error: String(e?.message ?? e) });
89
+ }
90
+ }
91
+
92
+ const ok = results.every((r) => r.ok);
93
+ if (json) {
94
+ printResult({ json, data: { ok, results } });
95
+ return;
96
+ }
97
+
98
+ const lines = ['[test] results:'];
99
+ for (const r of results) {
100
+ if (r.ok && r.skipped) {
101
+ lines.push(`- ↪ ${r.component}: skipped (${r.reason})`);
102
+ } else if (r.ok) {
103
+ lines.push(`- ✅ ${r.component}: ok (${r.pm} ${r.script})`);
104
+ } else {
105
+ lines.push(`- ❌ ${r.component}: failed (${r.pm ?? 'unknown'} ${r.script ?? ''})`);
106
+ if (r.error) lines.push(` - ${r.error}`);
107
+ }
108
+ }
109
+ if (!ok) {
110
+ lines.push('');
111
+ lines.push('[test] failed');
112
+ }
113
+ printResult({ json: false, text: lines.join('\n') });
114
+ if (!ok) {
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ main().catch((err) => {
120
+ console.error('[test] failed:', err);
121
+ process.exit(1);
122
+ });
123
+