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
@@ -0,0 +1,182 @@
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 { getRootDir, resolveStackEnvPath } from './utils/paths.mjs';
5
+ import { run } from './utils/proc.mjs';
6
+ import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/sandbox.mjs';
7
+ import { existsSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ function sanitizeStackName(raw) {
12
+ const s = String(raw ?? '')
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9-]+/g, '-')
16
+ .replace(/-+/g, '-')
17
+ .replace(/^-+/, '')
18
+ .replace(/-+$/, '');
19
+ return s || 'pr';
20
+ }
21
+
22
+ function parseGithubPullRequestNumber(input) {
23
+ const raw = String(input ?? '').trim();
24
+ if (!raw) return null;
25
+ if (/^\d+$/.test(raw)) return Number(raw);
26
+ const m = raw.match(/github\.com\/[^/]+\/[^/]+\/pull\/(?<num>\d+)/);
27
+ return m?.groups?.num ? Number(m.groups.num) : null;
28
+ }
29
+
30
+ function inferStackNameFromPrArgs({ happy, happyCli, server, serverLight }) {
31
+ const parts = [];
32
+ const hn = parseGithubPullRequestNumber(happy);
33
+ const cn = parseGithubPullRequestNumber(happyCli);
34
+ const sn = parseGithubPullRequestNumber(server);
35
+ const sln = parseGithubPullRequestNumber(serverLight);
36
+ if (hn) parts.push(`happy${hn}`);
37
+ if (cn) parts.push(`cli${cn}`);
38
+ if (sn) parts.push(`server${sn}`);
39
+ if (sln) parts.push(`light${sln}`);
40
+ return sanitizeStackName(parts.length ? `pr-${parts.join('-')}` : 'pr');
41
+ }
42
+
43
+ function detectBestAuthSource() {
44
+ const devAuthEnvExists = existsSync(resolveStackEnvPath('dev-auth').envPath);
45
+ const devAuthAccessKey = join(resolveStackEnvPath('dev-auth').baseDir, 'cli', 'access.key');
46
+ const mainAccessKey = join(resolveStackEnvPath('main').baseDir, 'cli', 'access.key');
47
+ const allowGlobal = sandboxAllowsGlobalSideEffects();
48
+ const legacyAccessKey = join(homedir(), '.happy', 'cli', 'access.key');
49
+
50
+ const hasDevAuth = devAuthEnvExists && existsSync(devAuthAccessKey);
51
+ const hasMain = existsSync(mainAccessKey);
52
+ const hasLegacy = (!isSandboxed() || allowGlobal) && existsSync(legacyAccessKey);
53
+
54
+ if (hasDevAuth) return { from: 'dev-auth', hasAny: true };
55
+ if (hasMain) return { from: 'main', hasAny: true };
56
+ if (hasLegacy) return { from: 'legacy', hasAny: true };
57
+ return { from: 'main', hasAny: false };
58
+ }
59
+
60
+ function detectLinkDefault() {
61
+ const rawLink = (process.env.HAPPY_STACKS_AUTH_LINK ?? process.env.HAPPY_LOCAL_AUTH_LINK ?? '').toString().trim();
62
+ if (rawLink) return rawLink !== '0';
63
+ const rawMode = (process.env.HAPPY_STACKS_AUTH_MODE ?? process.env.HAPPY_LOCAL_AUTH_MODE ?? '').toString().trim().toLowerCase();
64
+ if (rawMode) return rawMode === 'link';
65
+ // Default for setup-pr: prefer reuse/symlink to avoid stale creds and reduce re-login friction.
66
+ return true;
67
+ }
68
+
69
+ async function runNodeScript({ rootDir, rel, args = [], env = process.env }) {
70
+ await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
71
+ }
72
+
73
+ async function main() {
74
+ const rootDir = getRootDir(import.meta.url);
75
+ const argvRaw = process.argv.slice(2);
76
+ const sep = argvRaw.indexOf('--');
77
+ const argv = sep >= 0 ? argvRaw.slice(0, sep) : argvRaw;
78
+ const forwarded = sep >= 0 ? argvRaw.slice(sep + 1) : [];
79
+
80
+ const { flags, kv } = parseArgs(argv);
81
+ const json = wantsJson(argv, { flags });
82
+
83
+ if (wantsHelp(argv, { flags })) {
84
+ printResult({
85
+ json,
86
+ data: {
87
+ usage:
88
+ 'happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--happy-server=<pr-url|number>|--happy-server-light=<pr-url|number>] [--name=<stack>] [--dev|--start] [--seed-auth|--no-seed-auth] [--copy-auth-from=<stack|legacy>] [--link-auth|--copy-auth] [--update] [--force] [--json] [-- <stack dev/start args...>]',
89
+ },
90
+ text: [
91
+ '[setup-pr] usage:',
92
+ ' happys setup-pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--dev]',
93
+ ' happys setup pr --happy=<pr-url|number> [--happy-cli=<pr-url|number>] [--dev] # alias',
94
+ '',
95
+ 'What it does (idempotent):',
96
+ '- ensures happy-stacks home exists (init)',
97
+ '- bootstraps/clones missing components (upstream by default)',
98
+ '- creates or reuses a PR stack and checks out PR worktrees',
99
+ '- optionally seeds auth (best available source: dev-auth → main → legacy)',
100
+ '- starts the stack (dev by default)',
101
+ '',
102
+ 'Updating when the PR changes:',
103
+ '- re-run the same command; it will fast-forward PR worktrees when possible',
104
+ '- if the PR was force-pushed, add --force',
105
+ '',
106
+ 'example:',
107
+ ' happys setup-pr \\',
108
+ ' --happy=https://github.com/slopus/happy/pull/123 \\',
109
+ ' --happy-cli=https://github.com/slopus/happy-cli/pull/456',
110
+ ].join('\n'),
111
+ });
112
+ return;
113
+ }
114
+
115
+ const prHappy = (kv.get('--happy') ?? '').trim();
116
+ const prCli = (kv.get('--happy-cli') ?? '').trim();
117
+ const prServer = (kv.get('--happy-server') ?? '').trim();
118
+ const prServerLight = (kv.get('--happy-server-light') ?? '').trim();
119
+ if (!prHappy && !prCli && !prServer && !prServerLight) {
120
+ throw new Error('[setup-pr] missing PR inputs. Provide at least one of: --happy, --happy-cli, --happy-server, --happy-server-light');
121
+ }
122
+ if (prServer && prServerLight) {
123
+ throw new Error('[setup-pr] cannot specify both --happy-server and --happy-server-light');
124
+ }
125
+
126
+ const wantsDev = flags.has('--dev') || (!flags.has('--start') && !flags.has('--prod'));
127
+ const wantsStart = flags.has('--start') || flags.has('--prod');
128
+ if (wantsDev && wantsStart) {
129
+ throw new Error('[setup-pr] choose either --dev or --start (not both)');
130
+ }
131
+
132
+ const stackNameRaw = (kv.get('--name') ?? '').trim();
133
+ const stackName = stackNameRaw ? sanitizeStackName(stackNameRaw) : inferStackNameFromPrArgs({ happy: prHappy, happyCli: prCli, server: prServer, serverLight: prServerLight });
134
+
135
+ // Determine server flavor for bootstrap and stack creation.
136
+ const serverComponent = (kv.get('--server') ?? '').trim() || (prServer ? 'happy-server' : 'happy-server-light');
137
+ const bootstrapServer = prServer || serverComponent === 'happy-server' ? 'both' : 'happy-server-light';
138
+
139
+ // Auth defaults (avoid prompts; setup-pr should be low-friction).
140
+ const seedAuthFlag = flags.has('--seed-auth') ? true : flags.has('--no-seed-auth') ? false : null;
141
+ const authFrom = (kv.get('--copy-auth-from') ?? '').trim();
142
+ const linkAuth = flags.has('--link-auth') ? true : flags.has('--copy-auth') ? false : null;
143
+
144
+ const best = detectBestAuthSource();
145
+ const effectiveSeedAuth = seedAuthFlag != null ? seedAuthFlag : best.hasAny;
146
+ const effectiveAuthFrom = authFrom || best.from;
147
+ const effectiveLinkAuth = linkAuth != null ? linkAuth : detectLinkDefault();
148
+
149
+ // 1) Ensure happy-stacks home is initialized (idempotent).
150
+ await runNodeScript({ rootDir, rel: 'scripts/init.mjs', args: ['--no-bootstrap'] });
151
+
152
+ // 2) Bootstrap component repos and deps (idempotent; clones only if missing).
153
+ await runNodeScript({ rootDir, rel: 'scripts/install.mjs', args: ['--upstream', '--clone', `--server=${bootstrapServer}`] });
154
+
155
+ // 3) Create/reuse the PR stack and wire worktrees.
156
+ const stackArgs = [
157
+ 'stack',
158
+ 'pr',
159
+ stackName,
160
+ ...(prHappy ? [`--happy=${prHappy}`] : []),
161
+ ...(prCli ? [`--happy-cli=${prCli}`] : []),
162
+ ...(prServer ? [`--happy-server=${prServer}`] : []),
163
+ ...(prServerLight ? [`--happy-server-light=${prServerLight}`] : []),
164
+ `--server=${serverComponent}`,
165
+ '--reuse',
166
+ ...(flags.has('--update') ? ['--update'] : []),
167
+ ...(flags.has('--force') ? ['--force'] : []),
168
+ ...(effectiveSeedAuth ? ['--seed-auth', `--copy-auth-from=${effectiveAuthFrom}`, ...(effectiveLinkAuth ? ['--link-auth'] : [])] : ['--no-seed-auth']),
169
+ ...(wantsDev ? ['--dev'] : ['--start']),
170
+ ...(json ? ['--json'] : []),
171
+ ];
172
+ if (forwarded.length) {
173
+ stackArgs.push('--', ...forwarded);
174
+ }
175
+ await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: stackArgs });
176
+ }
177
+
178
+ main().catch((err) => {
179
+ console.error('[setup-pr] failed:', err);
180
+ process.exit(1);
181
+ });
182
+