happy-stacks 0.1.2 → 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 (91) hide show
  1. package/README.md +121 -83
  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 +560 -112
  25. package/scripts/build.mjs +24 -4
  26. package/scripts/cli-link.mjs +3 -3
  27. package/scripts/completion.mjs +15 -8
  28. package/scripts/daemon.mjs +130 -20
  29. package/scripts/dev.mjs +201 -133
  30. package/scripts/doctor.mjs +26 -21
  31. package/scripts/edison.mjs +1828 -0
  32. package/scripts/happy.mjs +3 -7
  33. package/scripts/init.mjs +43 -20
  34. package/scripts/install.mjs +14 -8
  35. package/scripts/lint.mjs +145 -0
  36. package/scripts/menubar.mjs +81 -8
  37. package/scripts/migrate.mjs +25 -15
  38. package/scripts/mobile.mjs +13 -7
  39. package/scripts/run.mjs +114 -27
  40. package/scripts/self.mjs +3 -7
  41. package/scripts/server_flavor.mjs +3 -3
  42. package/scripts/service.mjs +15 -2
  43. package/scripts/setup.mjs +790 -0
  44. package/scripts/setup_pr.mjs +182 -0
  45. package/scripts/stack.mjs +1792 -254
  46. package/scripts/stop.mjs +6 -3
  47. package/scripts/tailscale.mjs +17 -2
  48. package/scripts/test.mjs +144 -0
  49. package/scripts/tui.mjs +556 -0
  50. package/scripts/typecheck.mjs +2 -2
  51. package/scripts/ui_gateway.mjs +2 -2
  52. package/scripts/uninstall.mjs +18 -10
  53. package/scripts/utils/auth_files.mjs +58 -0
  54. package/scripts/utils/auth_login_ux.mjs +76 -0
  55. package/scripts/utils/auth_sources.mjs +12 -0
  56. package/scripts/utils/browser.mjs +22 -0
  57. package/scripts/utils/canonical_home.mjs +20 -0
  58. package/scripts/utils/{cli_registry.mjs → cli/cli_registry.mjs} +48 -0
  59. package/scripts/utils/{wizard.mjs → cli/wizard.mjs} +1 -1
  60. package/scripts/utils/config.mjs +6 -2
  61. package/scripts/utils/dev_auth_key.mjs +169 -0
  62. package/scripts/utils/dev_daemon.mjs +104 -0
  63. package/scripts/utils/dev_expo_web.mjs +112 -0
  64. package/scripts/utils/dev_server.mjs +183 -0
  65. package/scripts/utils/env.mjs +60 -11
  66. package/scripts/utils/env_file.mjs +36 -0
  67. package/scripts/utils/expo.mjs +4 -2
  68. package/scripts/utils/handy_master_secret.mjs +94 -0
  69. package/scripts/utils/happy_server_infra.mjs +100 -46
  70. package/scripts/utils/localhost_host.mjs +17 -0
  71. package/scripts/utils/ownership.mjs +135 -0
  72. package/scripts/utils/paths.mjs +5 -2
  73. package/scripts/utils/pm.mjs +121 -20
  74. package/scripts/utils/proc.mjs +29 -2
  75. package/scripts/utils/runtime.mjs +1 -3
  76. package/scripts/utils/sandbox.mjs +14 -0
  77. package/scripts/utils/server.mjs +24 -0
  78. package/scripts/utils/server_port.mjs +9 -0
  79. package/scripts/utils/server_urls.mjs +54 -0
  80. package/scripts/utils/stack_context.mjs +23 -0
  81. package/scripts/utils/stack_runtime_state.mjs +104 -0
  82. package/scripts/utils/stack_startup.mjs +208 -0
  83. package/scripts/utils/stack_stop.mjs +79 -30
  84. package/scripts/utils/stacks.mjs +38 -0
  85. package/scripts/utils/watch.mjs +63 -0
  86. package/scripts/utils/worktrees.mjs +57 -1
  87. package/scripts/where.mjs +14 -7
  88. package/scripts/worktrees.mjs +82 -8
  89. /package/scripts/utils/{args.mjs → cli/args.mjs} +0 -0
  90. /package/scripts/utils/{cli.mjs → cli/cli.mjs} +0 -0
  91. /package/scripts/utils/{smoke_help.mjs → cli/smoke_help.mjs} +0 -0
package/scripts/stop.mjs CHANGED
@@ -2,15 +2,15 @@ import './utils/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';
5
+ import { parseArgs } from './utils/cli/args.mjs';
6
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
7
7
  import { run, runCapture } from './utils/proc.mjs';
8
8
  import { getRootDir, resolveStackEnvPath } from './utils/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,8 @@
1
1
  import './utils/env.mjs';
2
- import { parseArgs } from './utils/args.mjs';
2
+ import { parseArgs } from './utils/cli/args.mjs';
3
3
  import { run, runCapture } from './utils/proc.mjs';
4
- import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
4
+ import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
5
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
5
6
  import { constants } from 'node:fs';
6
7
  import { access } from 'node:fs/promises';
7
8
 
@@ -369,6 +370,13 @@ async function main() {
369
370
  return;
370
371
  }
371
372
  case 'enable': {
373
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
374
+ throw new Error(
375
+ '[tailscale] enable is disabled in sandbox mode.\n' +
376
+ 'Reason: Tailscale Serve is global machine state.\n' +
377
+ 'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
378
+ );
379
+ }
372
380
  const res = await tailscaleServeEnable({ internalServerUrl });
373
381
  if (res?.enableUrl && !res?.httpsUrl) {
374
382
  printResult({
@@ -387,6 +395,13 @@ async function main() {
387
395
  }
388
396
  case 'disable':
389
397
  case 'reset': {
398
+ if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
399
+ throw new Error(
400
+ '[tailscale] disable/reset is disabled in sandbox mode.\n' +
401
+ 'Reason: Tailscale Serve is global machine state.\n' +
402
+ 'If you really want this, set: HAPPY_STACKS_SANDBOX_ALLOW_GLOBAL=1'
403
+ );
404
+ }
390
405
  await tailscaleServeReset();
391
406
  printResult({ json, data: { ok: true }, text: '[local] tailscale serve reset' });
392
407
  return;
@@ -0,0 +1,144 @@
1
+ import './utils/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.mjs';
5
+ import { ensureDepsInstalled, requirePnpm } from './utils/pm.mjs';
6
+ import { pathExists } from './utils/fs.mjs';
7
+ import { run } from './utils/proc.mjs';
8
+ import { join } from 'node:path';
9
+ import { readFile } from 'node:fs/promises';
10
+
11
+ const DEFAULT_COMPONENTS = ['happy', 'happy-cli', 'happy-server-light', 'happy-server'];
12
+
13
+ async function detectPackageManagerCmd(dir) {
14
+ if (await pathExists(join(dir, 'yarn.lock'))) {
15
+ return { name: 'yarn', cmd: 'yarn', argsForScript: (script) => ['-s', script] };
16
+ }
17
+ await requirePnpm();
18
+ return { name: 'pnpm', cmd: 'pnpm', argsForScript: (script) => ['--silent', script] };
19
+ }
20
+
21
+ async function readScripts(dir) {
22
+ try {
23
+ const raw = await readFile(join(dir, 'package.json'), 'utf-8');
24
+ const pkg = JSON.parse(raw);
25
+ const scripts = pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
26
+ return scripts;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function pickTestScript(scripts) {
33
+ if (!scripts) return null;
34
+ const candidates = [
35
+ 'test',
36
+ 'tst',
37
+ 'test:ci',
38
+ 'test:unit',
39
+ 'check:test',
40
+ ];
41
+ return candidates.find((k) => typeof scripts[k] === 'string' && scripts[k].trim()) ?? null;
42
+ }
43
+
44
+ async function main() {
45
+ const argv = process.argv.slice(2);
46
+ const { flags } = parseArgs(argv);
47
+ const json = wantsJson(argv, { flags });
48
+
49
+ if (wantsHelp(argv, { flags })) {
50
+ printResult({
51
+ json,
52
+ data: { components: DEFAULT_COMPONENTS, flags: ['--json'] },
53
+ text: [
54
+ '[test] usage:',
55
+ ' happys test [component...] [--json]',
56
+ '',
57
+ 'components:',
58
+ ` ${DEFAULT_COMPONENTS.join(' | ')}`,
59
+ '',
60
+ 'examples:',
61
+ ' happys test',
62
+ ' happys test happy happy-cli',
63
+ ].join('\n'),
64
+ });
65
+ return;
66
+ }
67
+
68
+ const positionals = argv.filter((a) => !a.startsWith('--'));
69
+ const requested = positionals.length ? positionals : ['all'];
70
+ const wantAll = requested.includes('all');
71
+ const components = wantAll ? DEFAULT_COMPONENTS : requested;
72
+
73
+ const rootDir = getRootDir(import.meta.url);
74
+
75
+ const results = [];
76
+ for (const component of components) {
77
+ if (!DEFAULT_COMPONENTS.includes(component)) {
78
+ results.push({ component, ok: false, skipped: false, error: `unknown component (expected one of: ${DEFAULT_COMPONENTS.join(', ')})` });
79
+ continue;
80
+ }
81
+
82
+ const dir = getComponentDir(rootDir, component);
83
+ if (!(await pathExists(dir))) {
84
+ results.push({ component, ok: false, skipped: false, dir, error: `missing component dir: ${dir}` });
85
+ continue;
86
+ }
87
+
88
+ const scripts = await readScripts(dir);
89
+ if (!scripts) {
90
+ results.push({ component, ok: true, skipped: true, dir, reason: 'no package.json' });
91
+ continue;
92
+ }
93
+
94
+ const script = pickTestScript(scripts);
95
+ if (!script) {
96
+ results.push({ component, ok: true, skipped: true, dir, reason: 'no test script found in package.json' });
97
+ continue;
98
+ }
99
+
100
+ await ensureDepsInstalled(dir, component);
101
+ const pm = await detectPackageManagerCmd(dir);
102
+
103
+ try {
104
+ // eslint-disable-next-line no-console
105
+ console.log(`[test] ${component}: running ${pm.name} ${script}`);
106
+ await run(pm.cmd, pm.argsForScript(script), { cwd: dir, env: process.env });
107
+ results.push({ component, ok: true, skipped: false, dir, pm: pm.name, script });
108
+ } catch (e) {
109
+ results.push({ component, ok: false, skipped: false, dir, pm: pm.name, script, error: String(e?.message ?? e) });
110
+ }
111
+ }
112
+
113
+ const ok = results.every((r) => r.ok);
114
+ if (json) {
115
+ printResult({ json, data: { ok, results } });
116
+ return;
117
+ }
118
+
119
+ const lines = ['[test] results:'];
120
+ for (const r of results) {
121
+ if (r.ok && r.skipped) {
122
+ lines.push(`- ↪ ${r.component}: skipped (${r.reason})`);
123
+ } else if (r.ok) {
124
+ lines.push(`- ✅ ${r.component}: ok (${r.pm} ${r.script})`);
125
+ } else {
126
+ lines.push(`- ❌ ${r.component}: failed (${r.pm ?? 'unknown'} ${r.script ?? ''})`);
127
+ if (r.error) lines.push(` - ${r.error}`);
128
+ }
129
+ }
130
+ if (!ok) {
131
+ lines.push('');
132
+ lines.push('[test] failed');
133
+ }
134
+ printResult({ json: false, text: lines.join('\n') });
135
+ if (!ok) {
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ main().catch((err) => {
141
+ console.error('[test] failed:', err);
142
+ process.exit(1);
143
+ });
144
+