happy-stacks 0.0.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 (67) hide show
  1. package/README.md +314 -0
  2. package/bin/happys.mjs +168 -0
  3. package/docs/menubar.md +186 -0
  4. package/docs/mobile-ios.md +134 -0
  5. package/docs/remote-access.md +43 -0
  6. package/docs/server-flavors.md +79 -0
  7. package/docs/stacks.md +218 -0
  8. package/docs/tauri.md +62 -0
  9. package/docs/worktrees-and-forks.md +395 -0
  10. package/extras/swiftbar/auth-login.sh +31 -0
  11. package/extras/swiftbar/happy-stacks.5s.sh +218 -0
  12. package/extras/swiftbar/icons/happy-green.png +0 -0
  13. package/extras/swiftbar/icons/happy-orange.png +0 -0
  14. package/extras/swiftbar/icons/happy-red.png +0 -0
  15. package/extras/swiftbar/icons/logo-white.png +0 -0
  16. package/extras/swiftbar/install.sh +191 -0
  17. package/extras/swiftbar/lib/git.sh +330 -0
  18. package/extras/swiftbar/lib/icons.sh +105 -0
  19. package/extras/swiftbar/lib/render.sh +774 -0
  20. package/extras/swiftbar/lib/system.sh +190 -0
  21. package/extras/swiftbar/lib/utils.sh +205 -0
  22. package/extras/swiftbar/pnpm-term.sh +125 -0
  23. package/extras/swiftbar/pnpm.sh +21 -0
  24. package/extras/swiftbar/set-interval.sh +62 -0
  25. package/extras/swiftbar/set-server-flavor.sh +57 -0
  26. package/extras/swiftbar/wt-pr.sh +95 -0
  27. package/package.json +58 -0
  28. package/scripts/auth.mjs +272 -0
  29. package/scripts/build.mjs +204 -0
  30. package/scripts/cli-link.mjs +58 -0
  31. package/scripts/completion.mjs +364 -0
  32. package/scripts/daemon.mjs +349 -0
  33. package/scripts/dev.mjs +181 -0
  34. package/scripts/doctor.mjs +342 -0
  35. package/scripts/happy.mjs +79 -0
  36. package/scripts/init.mjs +232 -0
  37. package/scripts/install.mjs +379 -0
  38. package/scripts/menubar.mjs +107 -0
  39. package/scripts/mobile.mjs +305 -0
  40. package/scripts/run.mjs +236 -0
  41. package/scripts/self.mjs +298 -0
  42. package/scripts/server_flavor.mjs +125 -0
  43. package/scripts/service.mjs +526 -0
  44. package/scripts/stack.mjs +815 -0
  45. package/scripts/tailscale.mjs +278 -0
  46. package/scripts/uninstall.mjs +190 -0
  47. package/scripts/utils/args.mjs +17 -0
  48. package/scripts/utils/cli.mjs +24 -0
  49. package/scripts/utils/cli_registry.mjs +262 -0
  50. package/scripts/utils/config.mjs +40 -0
  51. package/scripts/utils/dotenv.mjs +30 -0
  52. package/scripts/utils/env.mjs +138 -0
  53. package/scripts/utils/env_file.mjs +59 -0
  54. package/scripts/utils/env_local.mjs +25 -0
  55. package/scripts/utils/fs.mjs +11 -0
  56. package/scripts/utils/paths.mjs +184 -0
  57. package/scripts/utils/pm.mjs +294 -0
  58. package/scripts/utils/ports.mjs +66 -0
  59. package/scripts/utils/proc.mjs +66 -0
  60. package/scripts/utils/runtime.mjs +30 -0
  61. package/scripts/utils/server.mjs +41 -0
  62. package/scripts/utils/smoke_help.mjs +45 -0
  63. package/scripts/utils/validate.mjs +47 -0
  64. package/scripts/utils/wizard.mjs +69 -0
  65. package/scripts/utils/worktrees.mjs +78 -0
  66. package/scripts/where.mjs +105 -0
  67. package/scripts/worktrees.mjs +1721 -0
@@ -0,0 +1,364 @@
1
+ import './utils/env.mjs';
2
+
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { existsSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ import { parseArgs } from './utils/args.mjs';
9
+ import { printResult, wantsHelp, wantsJson } from './utils/cli.mjs';
10
+ import { runCapture } from './utils/proc.mjs';
11
+ import { getHappysRegistry } from './utils/cli_registry.mjs';
12
+ import { getHappyStacksHomeDir, getRootDir } from './utils/paths.mjs';
13
+
14
+ function detectShell() {
15
+ const raw = (process.env.SHELL ?? '').toLowerCase();
16
+ if (raw.includes('fish')) return 'fish';
17
+ if (raw.includes('bash')) return 'bash';
18
+ return 'zsh';
19
+ }
20
+
21
+ function expandHome(p) {
22
+ return p.replace(/^~(?=\/)/, homedir());
23
+ }
24
+
25
+ function parseShellArg({ argv, kv }) {
26
+ const fromKv = (kv.get('--shell') ?? '').trim();
27
+ const fromEnv = (process.env.HAPPY_STACKS_SHELL ?? '').trim();
28
+ const raw = fromKv || fromEnv || detectShell();
29
+ const v = raw.toLowerCase();
30
+ if (v === 'zsh' || v === 'bash' || v === 'fish') return v;
31
+ throw new Error(`[completion] invalid --shell: ${raw} (expected: zsh|bash|fish)`);
32
+ }
33
+
34
+ function visibleTopLevelCommands() {
35
+ const { commands } = getHappysRegistry();
36
+ // Hide legacy aliases; include visible primary names and visible aliases.
37
+ const out = [];
38
+ for (const c of commands) {
39
+ if (c.hidden) continue;
40
+ out.push(c.name);
41
+ for (const a of c.aliases ?? []) out.push(a);
42
+ }
43
+ // Deduplicate + stable order.
44
+ return Array.from(new Set(out)).sort();
45
+ }
46
+
47
+ async function helpJsonSubcommands({ cliRootDir, scriptRelPath, fallback = [] }) {
48
+ try {
49
+ const raw = await runCapture(process.execPath, [join(cliRootDir, scriptRelPath), '--help', '--json'], { cwd: cliRootDir });
50
+ const parsed = JSON.parse(raw);
51
+ const cmds = Array.isArray(parsed?.commands) ? parsed.commands : Array.isArray(parsed?.data?.commands) ? parsed.data.commands : null;
52
+ if (Array.isArray(cmds) && cmds.length) {
53
+ return cmds;
54
+ }
55
+ } catch {
56
+ // ignore
57
+ }
58
+ return fallback;
59
+ }
60
+
61
+ function expandStarCommands(cmds, { tailscale = [], service = [] } = {}) {
62
+ const out = [];
63
+ for (const c of cmds) {
64
+ if (c === 'tailscale:*') {
65
+ out.push(...tailscale.map((s) => `tailscale:${s}`));
66
+ continue;
67
+ }
68
+ if (c === 'service:*') {
69
+ out.push(...service.map((s) => `service:${s}`));
70
+ continue;
71
+ }
72
+ out.push(c);
73
+ }
74
+ return Array.from(new Set(out));
75
+ }
76
+
77
+ async function buildCompletionModel({ cliRootDir }) {
78
+ const top = visibleTopLevelCommands();
79
+
80
+ const service = await helpJsonSubcommands({ cliRootDir, scriptRelPath: 'scripts/service.mjs', fallback: ['install', 'uninstall', 'status', 'start', 'stop', 'restart', 'enable', 'disable', 'logs', 'tail'] });
81
+ const tailscale = await helpJsonSubcommands({ cliRootDir, scriptRelPath: 'scripts/tailscale.mjs', fallback: ['status', 'enable', 'disable', 'reset', 'url'] });
82
+ const self = await helpJsonSubcommands({ cliRootDir, scriptRelPath: 'scripts/self.mjs', fallback: ['status', 'update', 'check'] });
83
+ const srv = await helpJsonSubcommands({ cliRootDir, scriptRelPath: 'scripts/server_flavor.mjs', fallback: ['status', 'use'] });
84
+ const menubar = await helpJsonSubcommands({ cliRootDir, scriptRelPath: 'scripts/menubar.mjs', fallback: ['install', 'uninstall', 'open'] });
85
+ const wt = await helpJsonSubcommands({
86
+ cliRootDir,
87
+ scriptRelPath: 'scripts/worktrees.mjs',
88
+ fallback: ['migrate', 'sync', 'sync-all', 'list', 'new', 'pr', 'use', 'status', 'update', 'update-all', 'push', 'git', 'shell', 'code', 'cursor'],
89
+ });
90
+ const stackRaw = await helpJsonSubcommands({
91
+ cliRootDir,
92
+ scriptRelPath: 'scripts/stack.mjs',
93
+ fallback: ['new', 'edit', 'list', 'migrate', 'auth', 'dev', 'start', 'build', 'doctor', 'mobile', 'srv', 'wt', 'tailscale:*', 'service:*'],
94
+ });
95
+ const stack = expandStarCommands(stackRaw, { tailscale, service });
96
+
97
+ return {
98
+ top,
99
+ groups: {
100
+ wt,
101
+ stack,
102
+ srv,
103
+ service,
104
+ tailscale,
105
+ self,
106
+ menubar,
107
+ completion: ['print', 'install'],
108
+ },
109
+ };
110
+ }
111
+
112
+ function renderZsh(model) {
113
+ const top = model.top.join(' ');
114
+ const group = (name) => (model.groups?.[name] ?? []).join(' ');
115
+
116
+ return [
117
+ '#compdef happys happy-stacks',
118
+ '',
119
+ '_happys() {',
120
+ ' local -a top',
121
+ ` top=(${top})`,
122
+ '',
123
+ ' local cmd',
124
+ ' cmd="${words[2]:-}"',
125
+ '',
126
+ ' if (( CURRENT == 2 )); then',
127
+ " _describe -t commands 'happys command' top",
128
+ ' return',
129
+ ' fi',
130
+ '',
131
+ ' case "$cmd" in',
132
+ ` wt) _describe -t subcommands 'wt subcommand' (${group('wt')}) ;;`,
133
+ ` stack) _describe -t subcommands 'stack subcommand' (${group('stack')}) ;;`,
134
+ ` srv) _describe -t subcommands 'srv subcommand' (${group('srv')}) ;;`,
135
+ ` service) _describe -t subcommands 'service subcommand' (${group('service')}) ;;`,
136
+ ` tailscale) _describe -t subcommands 'tailscale subcommand' (${group('tailscale')}) ;;`,
137
+ ` self) _describe -t subcommands 'self subcommand' (${group('self')}) ;;`,
138
+ ` menubar) _describe -t subcommands 'menubar subcommand' (${group('menubar')}) ;;`,
139
+ ` completion) _describe -t subcommands 'completion subcommand' (${group('completion')}) ;;`,
140
+ ' *) ;;',
141
+ ' esac',
142
+ '}',
143
+ '',
144
+ 'compdef _happys happys',
145
+ 'compdef _happys happy-stacks',
146
+ '',
147
+ ].join('\n');
148
+ }
149
+
150
+ function renderBash(model) {
151
+ const quoteList = (arr) => arr.map((s) => s.replace(/"/g, '\\"')).join(' ');
152
+ const top = quoteList(model.top);
153
+
154
+ const group = (name) => quoteList(model.groups?.[name] ?? []);
155
+ return [
156
+ '_happys_completions() {',
157
+ ' local cur prev cmd',
158
+ ' COMPREPLY=()',
159
+ ' cur="${COMP_WORDS[COMP_CWORD]}"',
160
+ ' prev="${COMP_WORDS[COMP_CWORD-1]}"',
161
+ ' cmd="${COMP_WORDS[1]}"',
162
+ '',
163
+ ' if [[ $COMP_CWORD -eq 1 ]]; then',
164
+ ` COMPREPLY=( $(compgen -W "${top}" -- "$cur") )`,
165
+ ' return 0',
166
+ ' fi',
167
+ '',
168
+ ' case "$cmd" in',
169
+ ` wt) COMPREPLY=( $(compgen -W "${group('wt')}" -- "$cur") ) ;;`,
170
+ ` stack) COMPREPLY=( $(compgen -W "${group('stack')}" -- "$cur") ) ;;`,
171
+ ` srv) COMPREPLY=( $(compgen -W "${group('srv')}" -- "$cur") ) ;;`,
172
+ ` service) COMPREPLY=( $(compgen -W "${group('service')}" -- "$cur") ) ;;`,
173
+ ` tailscale) COMPREPLY=( $(compgen -W "${group('tailscale')}" -- "$cur") ) ;;`,
174
+ ` self) COMPREPLY=( $(compgen -W "${group('self')}" -- "$cur") ) ;;`,
175
+ ` menubar) COMPREPLY=( $(compgen -W "${group('menubar')}" -- "$cur") ) ;;`,
176
+ ` completion) COMPREPLY=( $(compgen -W "${group('completion')}" -- "$cur") ) ;;`,
177
+ ' *) ;;',
178
+ ' esac',
179
+ ' return 0',
180
+ '}',
181
+ '',
182
+ 'complete -F _happys_completions happys happy-stacks',
183
+ '',
184
+ ].join('\n');
185
+ }
186
+
187
+ function renderFish(model) {
188
+ const lines = [];
189
+ const add = (cmd, sub = null) => {
190
+ for (const bin of ['happys', 'happy-stacks']) {
191
+ if (sub) {
192
+ lines.push(`complete -c ${bin} -n '__fish_seen_subcommand_from ${cmd}' -f -a '${sub.join(' ')}'`);
193
+ } else {
194
+ lines.push(`complete -c ${bin} -f -a '${cmd.join(' ')}'`);
195
+ }
196
+ }
197
+ };
198
+
199
+ add(model.top);
200
+ add(['wt'], model.groups.wt ?? []);
201
+ add(['stack'], model.groups.stack ?? []);
202
+ add(['srv'], model.groups.srv ?? []);
203
+ add(['service'], model.groups.service ?? []);
204
+ add(['tailscale'], model.groups.tailscale ?? []);
205
+ add(['self'], model.groups.self ?? []);
206
+ add(['menubar'], model.groups.menubar ?? []);
207
+ add(['completion'], model.groups.completion ?? []);
208
+
209
+ return lines.join('\n') + '\n';
210
+ }
211
+
212
+ function completionPaths({ homeDir, shell }) {
213
+ const dir = join(homeDir, 'completions');
214
+ if (shell === 'zsh') return { dir, file: join(dir, '_happys') };
215
+ if (shell === 'bash') return { dir, file: join(dir, 'happys.bash') };
216
+ return { dir, file: join(dir, 'happys.fish') };
217
+ }
218
+
219
+ async function ensureShellInstall({ homeDir, shell }) {
220
+ const shellPath = (process.env.SHELL ?? '').toLowerCase();
221
+ const isDarwin = process.platform === 'darwin';
222
+
223
+ const zshrc = join(homedir(), '.zshrc');
224
+ const bashrc = join(homedir(), '.bashrc');
225
+ const bashProfile = join(homedir(), '.bash_profile');
226
+
227
+ const fishDir = join(homedir(), '.config', 'fish', 'conf.d');
228
+ const fishConf = join(fishDir, 'happy-stacks.fish');
229
+
230
+ const markerStart = '# >>> happy-stacks completions >>>';
231
+ const markerEnd = '# <<< happy-stacks completions <<<';
232
+
233
+ const completionsDir = join(homeDir, 'completions');
234
+ const shBlock = [
235
+ '',
236
+ markerStart,
237
+ `export HAPPY_STACKS_COMPLETIONS_DIR="${completionsDir}"`,
238
+ `if [[ -d "$HAPPY_STACKS_COMPLETIONS_DIR" ]]; then`,
239
+ ` fpath=("$HAPPY_STACKS_COMPLETIONS_DIR" $fpath)`,
240
+ ` autoload -Uz compinit && compinit`,
241
+ 'fi',
242
+ markerEnd,
243
+ '',
244
+ ].join('\n');
245
+
246
+ const bashBlock = [
247
+ '',
248
+ markerStart,
249
+ `if [[ -f "${join(completionsDir, 'happys.bash')}" ]]; then`,
250
+ ` . "${join(completionsDir, 'happys.bash')}"`,
251
+ 'fi',
252
+ markerEnd,
253
+ '',
254
+ ].join('\n');
255
+
256
+ const writeIfMissing = async (path, block) => {
257
+ let existing = '';
258
+ try {
259
+ existing = await readFile(path, 'utf-8');
260
+ } catch {
261
+ existing = '';
262
+ }
263
+ if (existing.includes(markerStart)) {
264
+ return { updated: false, path };
265
+ }
266
+ await writeFile(path, existing.replace(/\s*$/, '') + block, 'utf-8');
267
+ return { updated: true, path };
268
+ };
269
+
270
+ if (shell === 'fish' || shellPath.includes('fish')) {
271
+ await mkdir(fishDir, { recursive: true });
272
+ const res = await writeIfMissing(fishConf, [
273
+ '',
274
+ markerStart,
275
+ `set -gx HAPPY_STACKS_COMPLETIONS_DIR "${completionsDir}"`,
276
+ markerEnd,
277
+ '',
278
+ ].join('\n'));
279
+ return res;
280
+ }
281
+
282
+ if (shell === 'bash' || shellPath.includes('bash')) {
283
+ const target = isDarwin ? bashProfile : bashrc;
284
+ return await writeIfMissing(target, bashBlock);
285
+ }
286
+
287
+ // Default to zsh.
288
+ return await writeIfMissing(zshrc, shBlock);
289
+ }
290
+
291
+ async function main() {
292
+ const rawArgv = process.argv.slice(2);
293
+ const argv = rawArgv[0] === 'completion' ? rawArgv.slice(1) : rawArgv;
294
+ const { flags, kv } = parseArgs(argv);
295
+ const json = wantsJson(argv, { flags });
296
+
297
+ const positionals = argv.filter((a) => !a.startsWith('--'));
298
+ const cmd = positionals[0] ?? 'help';
299
+
300
+ if (wantsHelp(argv, { flags }) || cmd === 'help') {
301
+ printResult({
302
+ json,
303
+ data: { commands: ['print', 'install'], flags: ['--shell=zsh|bash|fish', '--json'] },
304
+ text: [
305
+ '[completion] usage:',
306
+ ' happys completion print [--shell=zsh|bash|fish] [--json]',
307
+ ' happys completion install [--shell=zsh|bash|fish] [--json]',
308
+ '',
309
+ 'notes:',
310
+ ' - installs best-effort shell completions for happys/happy-stacks',
311
+ ' - re-run after upgrading happys to refresh completions',
312
+ ].join('\n'),
313
+ });
314
+ return;
315
+ }
316
+
317
+ const cliRootDir = getRootDir(import.meta.url);
318
+ const shell = parseShellArg({ argv, kv });
319
+
320
+ const model = await buildCompletionModel({ cliRootDir });
321
+ const contents =
322
+ shell === 'zsh' ? renderZsh(model) : shell === 'bash' ? renderBash(model) : renderFish(model);
323
+
324
+ const homeDir = getHappyStacksHomeDir();
325
+ const { dir, file } = completionPaths({ homeDir, shell });
326
+
327
+ if (cmd === 'print') {
328
+ printResult({
329
+ json,
330
+ data: { ok: true, shell, path: file, bytes: contents.length, homeDir },
331
+ text: json ? null : contents,
332
+ });
333
+ return;
334
+ }
335
+
336
+ if (cmd === 'install') {
337
+ await mkdir(dir, { recursive: true });
338
+ await writeFile(file, contents, 'utf-8');
339
+
340
+ // fish loads completions automatically; zsh/bash need a tiny shell config hook.
341
+ const hook = shell === 'fish' ? { updated: false, path: null } : await ensureShellInstall({ homeDir, shell });
342
+
343
+ printResult({
344
+ json,
345
+ data: { ok: true, shell, file, hook },
346
+ text: [
347
+ `[completion] installed: ${file}`,
348
+ hook?.path ? (hook.updated ? `[completion] enabled via: ${hook.path}` : `[completion] already enabled in: ${hook.path}`) : null,
349
+ '[completion] note: restart your terminal (or source your shell config) to pick it up.',
350
+ ]
351
+ .filter(Boolean)
352
+ .join('\n'),
353
+ });
354
+ return;
355
+ }
356
+
357
+ throw new Error(`[completion] unknown command: ${cmd}`);
358
+ }
359
+
360
+ main().catch((err) => {
361
+ console.error('[completion] failed:', err);
362
+ process.exit(1);
363
+ });
364
+