@happier-dev/stack 0.1.0-preview.74.1
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.
- package/README.md +501 -0
- package/bin/hstack.mjs +348 -0
- package/docs/codex-mcp-resume.md +129 -0
- package/docs/edison.md +74 -0
- package/docs/forking-and-branding.md +189 -0
- package/docs/happy-development.md +22 -0
- package/docs/isolated-linux-vm.md +243 -0
- package/docs/menubar.md +244 -0
- package/docs/mobile-ios.md +322 -0
- package/docs/monorepo-migration.md +20 -0
- package/docs/paths-and-env.md +154 -0
- package/docs/remote-access.md +43 -0
- package/docs/server-flavors.md +147 -0
- package/docs/stacks.md +330 -0
- package/docs/tauri.md +60 -0
- package/docs/worktrees-and-forks.md +133 -0
- package/extras/swiftbar/auth-login.sh +29 -0
- package/extras/swiftbar/git-cache-refresh.sh +122 -0
- package/extras/swiftbar/hstack-term.sh +133 -0
- package/extras/swiftbar/hstack.5s.sh +296 -0
- package/extras/swiftbar/hstack.sh +35 -0
- package/extras/swiftbar/icons/happy-green.png +0 -0
- package/extras/swiftbar/icons/happy-orange.png +0 -0
- package/extras/swiftbar/icons/happy-red.png +0 -0
- package/extras/swiftbar/icons/logo-white.png +0 -0
- package/extras/swiftbar/install.sh +265 -0
- package/extras/swiftbar/lib/git.sh +629 -0
- package/extras/swiftbar/lib/icons.sh +92 -0
- package/extras/swiftbar/lib/render.sh +999 -0
- package/extras/swiftbar/lib/system.sh +244 -0
- package/extras/swiftbar/lib/utils.sh +717 -0
- package/extras/swiftbar/set-interval.sh +65 -0
- package/extras/swiftbar/set-server-flavor.sh +61 -0
- package/extras/swiftbar/wt-pr.sh +140 -0
- package/node_modules/@happier-dev/cli-common/README.md +6 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +26 -0
- package/package.json +77 -0
- package/scripts/auth.mjs +1829 -0
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
- package/scripts/auth_help_cmd.test.mjs +28 -0
- package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
- package/scripts/auth_login_force_default.test.mjs +66 -0
- package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
- package/scripts/auth_login_method_override.test.mjs +67 -0
- package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
- package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
- package/scripts/build.mjs +266 -0
- package/scripts/bundleWorkspaceDeps.mjs +38 -0
- package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
- package/scripts/ci.mjs +135 -0
- package/scripts/ci.test.mjs +50 -0
- package/scripts/cli-link.mjs +57 -0
- package/scripts/completion.mjs +395 -0
- package/scripts/contrib.mjs +333 -0
- package/scripts/daemon.mjs +1160 -0
- package/scripts/daemon.status_scope.test.mjs +51 -0
- package/scripts/daemon_cmd.mjs +26 -0
- package/scripts/daemon_dist_guard.test.mjs +171 -0
- package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
- package/scripts/daemon_server_scoped_state.test.mjs +49 -0
- package/scripts/daemon_start_verification.integration.test.mjs +296 -0
- package/scripts/dev.mjs +545 -0
- package/scripts/doctor.mjs +340 -0
- package/scripts/doctor_cmd.test.mjs +22 -0
- package/scripts/doctor_ui_index_missing.test.mjs +37 -0
- package/scripts/eas.mjs +367 -0
- package/scripts/eas_platform_parsing.test.mjs +63 -0
- package/scripts/edison.mjs +1848 -0
- package/scripts/env.mjs +149 -0
- package/scripts/env_cmd.test.mjs +118 -0
- package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
- package/scripts/happier.mjs +82 -0
- package/scripts/import.mjs +1327 -0
- package/scripts/init.mjs +464 -0
- package/scripts/install.mjs +550 -0
- package/scripts/lint.mjs +177 -0
- package/scripts/menubar.mjs +202 -0
- package/scripts/migrate.mjs +318 -0
- package/scripts/mobile.mjs +353 -0
- package/scripts/mobile_dev_client.mjs +87 -0
- package/scripts/monorepo.mjs +2234 -0
- package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
- package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
- package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
- package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
- package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
- package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
- package/scripts/pack.mjs +257 -0
- package/scripts/pack.test.mjs +68 -0
- package/scripts/pglite_lock.integration.test.mjs +152 -0
- package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
- package/scripts/provision/macos-lima-happy-vm.sh +192 -0
- package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
- package/scripts/release.mjs +53 -0
- package/scripts/release_binary_smoke.integration.test.mjs +138 -0
- package/scripts/review.mjs +1752 -0
- package/scripts/review_pr.mjs +435 -0
- package/scripts/run.mjs +561 -0
- package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
- package/scripts/self.mjs +465 -0
- package/scripts/self_host.mjs +9 -0
- package/scripts/self_host_binary_smoke.integration.test.mjs +74 -0
- package/scripts/self_host_runtime.mjs +883 -0
- package/scripts/self_host_runtime.test.mjs +82 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
- package/scripts/server_flavor.mjs +148 -0
- package/scripts/service.mjs +868 -0
- package/scripts/service_mode_help.test.mjs +27 -0
- package/scripts/setup.mjs +1324 -0
- package/scripts/setup_non_interactive_flag.test.mjs +60 -0
- package/scripts/setup_pr.mjs +605 -0
- package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
- package/scripts/stack/command_arguments.mjs +91 -0
- package/scripts/stack/copy_auth_from_stack.mjs +111 -0
- package/scripts/stack/delegated_script_commands.mjs +92 -0
- package/scripts/stack/help_text.mjs +110 -0
- package/scripts/stack/port_reservation.mjs +74 -0
- package/scripts/stack/repo_checkout_resolution.mjs +31 -0
- package/scripts/stack/run_script_with_stack_env.mjs +634 -0
- package/scripts/stack/stack_daemon_command.mjs +219 -0
- package/scripts/stack/stack_delegated_help.mjs +81 -0
- package/scripts/stack/stack_environment.mjs +151 -0
- package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
- package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
- package/scripts/stack/stack_info_snapshot.mjs +167 -0
- package/scripts/stack/stack_mobile_install_command.mjs +61 -0
- package/scripts/stack/stack_resume_command.mjs +76 -0
- package/scripts/stack/stack_stop_command.mjs +34 -0
- package/scripts/stack/stack_workspace_command.mjs +83 -0
- package/scripts/stack/transient_repo_overrides.mjs +29 -0
- package/scripts/stack.mjs +2388 -0
- package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
- package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
- package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
- package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
- package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
- package/scripts/stack_eas_help.test.mjs +72 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
- package/scripts/stack_env_cmd.test.mjs +107 -0
- package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
- package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
- package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
- package/scripts/stack_monorepo_defaults.test.mjs +31 -0
- package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
- package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
- package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
- package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
- package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
- package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
- package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
- package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
- package/scripts/stack_wt_list.test.mjs +117 -0
- package/scripts/start_ui_required_default.test.mjs +63 -0
- package/scripts/stop.mjs +190 -0
- package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
- package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
- package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
- package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
- package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
- package/scripts/systemd_unit_info.test.mjs +24 -0
- package/scripts/tailscale.mjs +490 -0
- package/scripts/test_ci.mjs +36 -0
- package/scripts/test_cmd.mjs +274 -0
- package/scripts/test_cmd.test.mjs +133 -0
- package/scripts/test_integration.mjs +33 -0
- package/scripts/testkit/auth_testkit.mjs +121 -0
- package/scripts/testkit/doctor_testkit.mjs +68 -0
- package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
- package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
- package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
- package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
- package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
- package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
- package/scripts/tools.mjs +70 -0
- package/scripts/tui.mjs +914 -0
- package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
- package/scripts/typecheck.mjs +178 -0
- package/scripts/ui_gateway.mjs +247 -0
- package/scripts/uninstall.mjs +179 -0
- package/scripts/utils/auth/credentials_paths.mjs +181 -0
- package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
- package/scripts/utils/auth/daemon_gate.mjs +66 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
- package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/auth/files.mjs +56 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
- package/scripts/utils/auth/handy_master_secret.mjs +42 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
- package/scripts/utils/auth/login_ux.mjs +105 -0
- package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
- package/scripts/utils/auth/sources.mjs +28 -0
- package/scripts/utils/auth/stable_scope_id.mjs +91 -0
- package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
- package/scripts/utils/auth/stack_guided_login.mjs +438 -0
- package/scripts/utils/cli/arg_values.mjs +23 -0
- package/scripts/utils/cli/arg_values.test.mjs +43 -0
- package/scripts/utils/cli/args.mjs +17 -0
- package/scripts/utils/cli/cli.mjs +24 -0
- package/scripts/utils/cli/cli_registry.mjs +440 -0
- package/scripts/utils/cli/cwd_scope.mjs +158 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +103 -0
- package/scripts/utils/cli/prereqs.test.mjs +33 -0
- package/scripts/utils/cli/progress.mjs +141 -0
- package/scripts/utils/cli/smoke_help.mjs +44 -0
- package/scripts/utils/cli/verbosity.mjs +11 -0
- package/scripts/utils/cli/wizard.mjs +139 -0
- package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
- package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/dev/daemon.mjs +232 -0
- package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
- package/scripts/utils/dev/expo_dev.mjs +478 -0
- package/scripts/utils/dev/expo_dev.test.mjs +89 -0
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
- package/scripts/utils/dev/server.mjs +180 -0
- package/scripts/utils/dev_auth_key.mjs +7 -0
- package/scripts/utils/edison/git_roots.mjs +30 -0
- package/scripts/utils/edison/git_roots.test.mjs +49 -0
- package/scripts/utils/env/config.mjs +52 -0
- package/scripts/utils/env/dotenv.mjs +32 -0
- package/scripts/utils/env/dotenv.test.mjs +32 -0
- package/scripts/utils/env/env.mjs +130 -0
- package/scripts/utils/env/env_file.mjs +98 -0
- package/scripts/utils/env/env_file.test.mjs +49 -0
- package/scripts/utils/env/env_local.mjs +25 -0
- package/scripts/utils/env/load_env_file.mjs +34 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/sandbox.mjs +13 -0
- package/scripts/utils/env/scrub_env.mjs +69 -0
- package/scripts/utils/env/scrub_env.test.mjs +102 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +65 -0
- package/scripts/utils/expo/expo.mjs +139 -0
- package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
- package/scripts/utils/expo/metro_ports.mjs +101 -0
- package/scripts/utils/expo/metro_ports.test.mjs +35 -0
- package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
- package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
- package/scripts/utils/fs/file_has_content.mjs +10 -0
- package/scripts/utils/fs/fs.mjs +11 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/dev_checkout.mjs +127 -0
- package/scripts/utils/git/dev_checkout.test.mjs +115 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/parse_name_status_z.mjs +21 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/git/worktrees.mjs +323 -0
- package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
- package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
- package/scripts/utils/llm/assist.mjs +260 -0
- package/scripts/utils/llm/codex_exec.mjs +61 -0
- package/scripts/utils/llm/codex_exec.test.mjs +46 -0
- package/scripts/utils/llm/hstack_runner.mjs +59 -0
- package/scripts/utils/llm/tools.mjs +56 -0
- package/scripts/utils/llm/tools.test.mjs +67 -0
- package/scripts/utils/menubar/swiftbar.mjs +121 -0
- package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
- package/scripts/utils/mobile/config.mjs +35 -0
- package/scripts/utils/mobile/dev_client_links.mjs +59 -0
- package/scripts/utils/mobile/identifiers.mjs +46 -0
- package/scripts/utils/mobile/identifiers.test.mjs +41 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
- package/scripts/utils/net/bind_mode.mjs +39 -0
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +110 -0
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +29 -0
- package/scripts/utils/paths/canonical_home.mjs +15 -0
- package/scripts/utils/paths/canonical_home.test.mjs +28 -0
- package/scripts/utils/paths/localhost_host.mjs +112 -0
- package/scripts/utils/paths/localhost_host.test.mjs +58 -0
- package/scripts/utils/paths/paths.mjs +302 -0
- package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
- package/scripts/utils/paths/runtime.mjs +41 -0
- package/scripts/utils/pglite_lock.mjs +107 -0
- package/scripts/utils/proc/commands.mjs +33 -0
- package/scripts/utils/proc/exit_cleanup.mjs +57 -0
- package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
- package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
- package/scripts/utils/proc/ownership.mjs +217 -0
- package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
- package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
- package/scripts/utils/proc/package_scripts.mjs +38 -0
- package/scripts/utils/proc/package_scripts.test.mjs +58 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/proc/pm.mjs +478 -0
- package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
- package/scripts/utils/proc/proc.mjs +331 -0
- package/scripts/utils/proc/proc.test.mjs +85 -0
- package/scripts/utils/proc/terminate.mjs +69 -0
- package/scripts/utils/proc/terminate.test.mjs +54 -0
- package/scripts/utils/proc/watch.mjs +63 -0
- package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
- package/scripts/utils/review/base_ref.mjs +82 -0
- package/scripts/utils/review/base_ref.test.mjs +89 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +107 -0
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +61 -0
- package/scripts/utils/review/findings.mjs +278 -0
- package/scripts/utils/review/findings.test.mjs +203 -0
- package/scripts/utils/review/head_slice.mjs +132 -0
- package/scripts/utils/review/head_slice.test.mjs +117 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/prompts.mjs +279 -0
- package/scripts/utils/review/prompts.test.mjs +77 -0
- package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
- package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
- package/scripts/utils/review/runners/augment.mjs +91 -0
- package/scripts/utils/review/runners/augment.test.mjs +64 -0
- package/scripts/utils/review/runners/claude.mjs +92 -0
- package/scripts/utils/review/runners/claude.test.mjs +47 -0
- package/scripts/utils/review/runners/coderabbit.mjs +105 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
- package/scripts/utils/review/runners/codex.mjs +129 -0
- package/scripts/utils/review/runners/codex.test.mjs +115 -0
- package/scripts/utils/review/slice_mode.mjs +20 -0
- package/scripts/utils/review/slice_mode.test.mjs +69 -0
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +57 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +41 -0
- package/scripts/utils/review/targets.mjs +23 -0
- package/scripts/utils/review/targets.test.mjs +31 -0
- package/scripts/utils/review/tool_home_seed.mjs +106 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
- package/scripts/utils/review/uncommitted_ops.mjs +77 -0
- package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
- package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
- package/scripts/utils/server/flavor_scripts.mjs +138 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
- package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
- package/scripts/utils/server/mobile_api_url.mjs +60 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
- package/scripts/utils/server/port.mjs +55 -0
- package/scripts/utils/server/prisma_import.mjs +36 -0
- package/scripts/utils/server/prisma_import.test.mjs +78 -0
- package/scripts/utils/server/server.mjs +109 -0
- package/scripts/utils/server/ui_build_check.mjs +37 -0
- package/scripts/utils/server/ui_build_check.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +13 -0
- package/scripts/utils/server/ui_env.test.mjs +57 -0
- package/scripts/utils/server/urls.mjs +100 -0
- package/scripts/utils/server/validate.mjs +60 -0
- package/scripts/utils/server/validate.test.mjs +76 -0
- package/scripts/utils/service/autostart_darwin.mjs +198 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
- package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/context.mjs +19 -0
- package/scripts/utils/stack/dirs.mjs +26 -0
- package/scripts/utils/stack/editor_workspace.mjs +126 -0
- package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
- package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
- package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
- package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
- package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
- package/scripts/utils/stack/names.mjs +27 -0
- package/scripts/utils/stack/names.test.mjs +26 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +40 -0
- package/scripts/utils/stack/startup.mjs +370 -0
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
- package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
- package/scripts/utils/stack/stop.mjs +284 -0
- package/scripts/utils/stack_context.mjs +1 -0
- package/scripts/utils/stack_runtime_state.mjs +1 -0
- package/scripts/utils/stacks.mjs +1 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/tauri/stack_overrides.mjs +22 -0
- package/scripts/utils/test/collect_test_files.mjs +29 -0
- package/scripts/utils/time/get_today_ymd.mjs +7 -0
- package/scripts/utils/tui/cleanup.mjs +38 -0
- package/scripts/utils/ui/ansi.mjs +47 -0
- package/scripts/utils/ui/browser.mjs +31 -0
- package/scripts/utils/ui/browser.test.mjs +56 -0
- package/scripts/utils/ui/clipboard.mjs +38 -0
- package/scripts/utils/ui/layout.mjs +44 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/terminal_launcher.mjs +129 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/update/auto_update_notice.mjs +93 -0
- package/scripts/utils/validate.mjs +5 -0
- package/scripts/where.mjs +138 -0
- package/scripts/worktrees.mjs +2174 -0
- package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
- package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
|
|
6
|
+
import { ensureEnvFileUpdated } from '../../env/env_file.mjs';
|
|
7
|
+
import { readEnvObjectFromFile } from '../../env/read.mjs';
|
|
8
|
+
import { sanitizeDnsLabel } from '../../net/dns.mjs';
|
|
9
|
+
import { pickNextFreeTcpPort } from '../../net/ports.mjs';
|
|
10
|
+
import { pmExecBin } from '../../proc/pm.mjs';
|
|
11
|
+
import { run, runCapture } from '../../proc/proc.mjs';
|
|
12
|
+
import { randomToken } from '../../crypto/tokens.mjs';
|
|
13
|
+
import { coercePort, INFRA_RESERVED_PORT_KEYS } from '../port.mjs';
|
|
14
|
+
|
|
15
|
+
const readEnvObject = readEnvObjectFromFile;
|
|
16
|
+
|
|
17
|
+
async function ensureTextFile({ path, generate }) {
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
const v = (await readFile(path, 'utf-8')).trim();
|
|
20
|
+
if (v) return v;
|
|
21
|
+
}
|
|
22
|
+
const next = String(generate()).trim();
|
|
23
|
+
await mkdir(join(path, '..'), { recursive: true }).catch(() => {});
|
|
24
|
+
await writeFile(path, next + '\n', 'utf-8');
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function composeProjectName(stackName) {
|
|
29
|
+
return sanitizeDnsLabel(`happier-stacks-${stackName}-happier-server`, { fallback: 'happier-stacks-happier-server' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function stopHappyServerManagedInfra({ stackName, baseDir, removeVolumes = false }) {
|
|
33
|
+
const infraDir = join(baseDir, 'happier-server', 'infra');
|
|
34
|
+
const composePath = join(infraDir, 'docker-compose.yml');
|
|
35
|
+
if (!existsSync(composePath)) {
|
|
36
|
+
return { ok: true, skipped: true, reason: 'missing_compose', composePath };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await ensureDockerCompose();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
skipped: true,
|
|
45
|
+
reason: 'docker_unavailable',
|
|
46
|
+
error: e instanceof Error ? e.message : String(e),
|
|
47
|
+
composePath,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const projectName = composeProjectName(stackName);
|
|
52
|
+
const args = ['down', '--remove-orphans', ...(removeVolumes ? ['--volumes'] : [])];
|
|
53
|
+
await dockerCompose({ composePath, projectName, args, options: { cwd: baseDir } });
|
|
54
|
+
return { ok: true, skipped: false, projectName, composePath };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildComposeYaml({
|
|
58
|
+
infraDir,
|
|
59
|
+
pgPort,
|
|
60
|
+
pgUser,
|
|
61
|
+
pgPassword,
|
|
62
|
+
pgDb,
|
|
63
|
+
redisPort,
|
|
64
|
+
minioPort,
|
|
65
|
+
minioConsolePort,
|
|
66
|
+
s3AccessKey,
|
|
67
|
+
s3SecretKey,
|
|
68
|
+
s3Bucket,
|
|
69
|
+
}) {
|
|
70
|
+
// Keep it explicit (no env substitution); we generate this file per stack.
|
|
71
|
+
return `services:
|
|
72
|
+
postgres:
|
|
73
|
+
image: postgres:16-alpine
|
|
74
|
+
environment:
|
|
75
|
+
POSTGRES_USER: ${pgUser}
|
|
76
|
+
POSTGRES_PASSWORD: ${pgPassword}
|
|
77
|
+
POSTGRES_DB: ${pgDb}
|
|
78
|
+
ports:
|
|
79
|
+
- "127.0.0.1:${pgPort}:5432"
|
|
80
|
+
volumes:
|
|
81
|
+
- "${join(infraDir, 'pgdata')}:/var/lib/postgresql/data"
|
|
82
|
+
healthcheck:
|
|
83
|
+
test: ["CMD-SHELL", "pg_isready -U ${pgUser} -d ${pgDb}"]
|
|
84
|
+
interval: 2s
|
|
85
|
+
timeout: 3s
|
|
86
|
+
retries: 30
|
|
87
|
+
|
|
88
|
+
redis:
|
|
89
|
+
image: redis:7-alpine
|
|
90
|
+
command: ["redis-server", "--appendonly", "yes"]
|
|
91
|
+
ports:
|
|
92
|
+
- "127.0.0.1:${redisPort}:6379"
|
|
93
|
+
volumes:
|
|
94
|
+
- "${join(infraDir, 'redis')}:/data"
|
|
95
|
+
healthcheck:
|
|
96
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
97
|
+
interval: 2s
|
|
98
|
+
timeout: 3s
|
|
99
|
+
retries: 30
|
|
100
|
+
|
|
101
|
+
minio:
|
|
102
|
+
image: minio/minio:latest
|
|
103
|
+
command: ["server", "/data", "--console-address", ":9001"]
|
|
104
|
+
environment:
|
|
105
|
+
MINIO_ROOT_USER: ${s3AccessKey}
|
|
106
|
+
MINIO_ROOT_PASSWORD: ${s3SecretKey}
|
|
107
|
+
ports:
|
|
108
|
+
- "127.0.0.1:${minioPort}:9000"
|
|
109
|
+
- "127.0.0.1:${minioConsolePort}:9001"
|
|
110
|
+
volumes:
|
|
111
|
+
- "${join(infraDir, 'minio')}:/data"
|
|
112
|
+
|
|
113
|
+
minio-init:
|
|
114
|
+
image: minio/mc:latest
|
|
115
|
+
depends_on:
|
|
116
|
+
- minio
|
|
117
|
+
entrypoint: ["/bin/sh", "-lc"]
|
|
118
|
+
command: >
|
|
119
|
+
mc alias set local http://minio:9000 ${s3AccessKey} ${s3SecretKey} &&
|
|
120
|
+
mc mb -p local/${s3Bucket} || true &&
|
|
121
|
+
mc anonymous set download local/${s3Bucket} || true
|
|
122
|
+
restart: "no"
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function ensureDockerCompose() {
|
|
127
|
+
const waitMsRaw = (process.env.HAPPIER_STACK_DOCKER_WAIT_MS ?? '').trim();
|
|
128
|
+
const waitMs = waitMsRaw ? Number(waitMsRaw) : process.stdout.isTTY ? 0 : 60_000;
|
|
129
|
+
const deadline = waitMs > 0 ? Date.now() + waitMs : Date.now();
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await runCapture('docker', ['compose', 'version'], { timeoutMs: 10_000 });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
const msg = e?.message ? String(e.message) : String(e);
|
|
135
|
+
throw new Error(
|
|
136
|
+
`[infra] docker compose is required for managed happier-server stacks.\n` +
|
|
137
|
+
`Fix: install Docker Desktop and ensure \`docker compose\` works.\n` +
|
|
138
|
+
`Details: ${msg}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const autostartRaw = (process.env.HAPPIER_STACK_DOCKER_AUTOSTART ?? '').trim();
|
|
143
|
+
const autostart = autostartRaw ? autostartRaw !== '0' : !process.stdout.isTTY;
|
|
144
|
+
|
|
145
|
+
// Ensure the Docker daemon is ready (launchd/SwiftBar often runs before Docker Desktop starts).
|
|
146
|
+
// If not ready, wait up to waitMs (non-interactive default: 60s) to avoid restart loops.
|
|
147
|
+
while (true) {
|
|
148
|
+
try {
|
|
149
|
+
await runCapture('docker', ['info'], { timeoutMs: 10_000 });
|
|
150
|
+
return;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
if (autostart) {
|
|
153
|
+
await maybeStartDockerDaemon().catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
if (Date.now() >= deadline) {
|
|
156
|
+
const msg = e?.message ? String(e.message) : String(e);
|
|
157
|
+
throw new Error(
|
|
158
|
+
`[infra] docker is installed but the daemon is not ready.\n` +
|
|
159
|
+
`Fix: start Docker Desktop, or disable managed infra (HAPPIER_STACK_MANAGED_INFRA=0).\n` +
|
|
160
|
+
`You can also increase wait time with HAPPIER_STACK_DOCKER_WAIT_MS, or disable auto-start with HAPPIER_STACK_DOCKER_AUTOSTART=0.\n` +
|
|
161
|
+
`Details: ${msg}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
// eslint-disable-next-line no-await-in-loop
|
|
165
|
+
await delay(1000);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function maybeStartDockerDaemon() {
|
|
171
|
+
// Best-effort. This may be a no-op depending on platform/permissions.
|
|
172
|
+
if (process.platform === 'darwin') {
|
|
173
|
+
const app = (process.env.HAPPIER_STACK_DOCKER_APP ?? '/Applications/Docker.app').trim();
|
|
174
|
+
// `open` exits quickly; Docker Desktop will start in the background.
|
|
175
|
+
await runCapture('open', ['-gj', '-a', app], { timeoutMs: 5_000 }).catch(() => {});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (process.platform === 'linux') {
|
|
180
|
+
// Rootless / Docker Desktop / system Docker can differ. Try a few user-scope units first.
|
|
181
|
+
const candidates = ['docker.service', 'docker.socket', 'docker-desktop.service', 'docker-desktop'];
|
|
182
|
+
for (const unit of candidates) {
|
|
183
|
+
// eslint-disable-next-line no-await-in-loop
|
|
184
|
+
await runCapture('systemctl', ['--user', 'start', unit], { timeoutMs: 5_000 }).catch(() => {});
|
|
185
|
+
}
|
|
186
|
+
// As a last resort, try system scope (may fail without sudo; ignore).
|
|
187
|
+
await runCapture('systemctl', ['start', 'docker'], { timeoutMs: 5_000 }).catch(() => {});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function dockerCompose({ composePath, projectName, args, options = {}, quiet = false, retries = 0 }) {
|
|
192
|
+
const cmdArgs = ['compose', '-f', composePath, '-p', projectName, ...args];
|
|
193
|
+
let attempt = 0;
|
|
194
|
+
// eslint-disable-next-line no-constant-condition
|
|
195
|
+
while (true) {
|
|
196
|
+
try {
|
|
197
|
+
if (quiet) {
|
|
198
|
+
// Capture stderr so callers can surface it in structured JSON errors.
|
|
199
|
+
await runCapture('docker', cmdArgs, { timeoutMs: 120_000, ...options });
|
|
200
|
+
} else {
|
|
201
|
+
await run('docker', cmdArgs, { ...options, stdio: options?.stdio ?? 'inherit' });
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (attempt >= retries) throw e;
|
|
206
|
+
attempt += 1;
|
|
207
|
+
// eslint-disable-next-line no-await-in-loop
|
|
208
|
+
await delay(800);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function waitForHealthyPostgres({ composePath, projectName, pgUser, pgDb }) {
|
|
214
|
+
const deadline = Date.now() + 60_000;
|
|
215
|
+
while (Date.now() < deadline) {
|
|
216
|
+
try {
|
|
217
|
+
await runCapture(
|
|
218
|
+
'docker',
|
|
219
|
+
['compose', '-f', composePath, '-p', projectName, 'exec', '-T', 'postgres', 'pg_isready', '-U', pgUser, '-d', pgDb],
|
|
220
|
+
{ timeoutMs: 5_000 }
|
|
221
|
+
);
|
|
222
|
+
return;
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore
|
|
225
|
+
}
|
|
226
|
+
// eslint-disable-next-line no-await-in-loop
|
|
227
|
+
await delay(800);
|
|
228
|
+
}
|
|
229
|
+
throw new Error('[infra] timed out waiting for postgres to become ready');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function waitForHealthyRedis({ composePath, projectName }) {
|
|
233
|
+
const deadline = Date.now() + 30_000;
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
try {
|
|
236
|
+
const out = await runCapture('docker', ['compose', '-f', composePath, '-p', projectName, 'exec', '-T', 'redis', 'redis-cli', 'ping'], {
|
|
237
|
+
timeoutMs: 5_000,
|
|
238
|
+
});
|
|
239
|
+
if (out.trim().toUpperCase().includes('PONG')) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// ignore
|
|
244
|
+
}
|
|
245
|
+
// eslint-disable-next-line no-await-in-loop
|
|
246
|
+
await delay(600);
|
|
247
|
+
}
|
|
248
|
+
throw new Error('[infra] timed out waiting for redis to become ready');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function waitForMinioReady({ composePath, projectName }) {
|
|
252
|
+
const deadline = Date.now() + 30_000;
|
|
253
|
+
while (Date.now() < deadline) {
|
|
254
|
+
try {
|
|
255
|
+
// Minio doesn't ship a healthcheck in our compose; exec'ing a trivial command is a good enough
|
|
256
|
+
// readiness proxy for running/accepting execs before we run minio-init.
|
|
257
|
+
await runCapture('docker', ['compose', '-f', composePath, '-p', projectName, 'exec', '-T', 'minio', 'sh', '-lc', 'echo ok'], {
|
|
258
|
+
timeoutMs: 5_000,
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
} catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
// eslint-disable-next-line no-await-in-loop
|
|
265
|
+
await delay(600);
|
|
266
|
+
}
|
|
267
|
+
throw new Error('[infra] timed out waiting for minio to become ready');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function ensureHappyServerManagedInfra({
|
|
271
|
+
stackName,
|
|
272
|
+
baseDir,
|
|
273
|
+
serverPort,
|
|
274
|
+
publicServerUrl,
|
|
275
|
+
envPath,
|
|
276
|
+
env = process.env,
|
|
277
|
+
quiet = false,
|
|
278
|
+
skipMinioInit = false,
|
|
279
|
+
}) {
|
|
280
|
+
await ensureDockerCompose();
|
|
281
|
+
|
|
282
|
+
const infraDir = join(baseDir, 'happier-server', 'infra');
|
|
283
|
+
await mkdir(infraDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
const existingEnv = envPath ? await readEnvObject(envPath) : {};
|
|
286
|
+
const reservedPorts = new Set();
|
|
287
|
+
|
|
288
|
+
// Reserve known ports (if present) to avoid picking duplicates when auto-filling.
|
|
289
|
+
for (const key of INFRA_RESERVED_PORT_KEYS) {
|
|
290
|
+
const p = coercePort(existingEnv[key] ?? env[key]);
|
|
291
|
+
if (p) reservedPorts.add(p);
|
|
292
|
+
}
|
|
293
|
+
if (Number.isFinite(serverPort) && serverPort > 0) reservedPorts.add(serverPort);
|
|
294
|
+
|
|
295
|
+
const pgPort =
|
|
296
|
+
coercePort(existingEnv.HAPPIER_STACK_PG_PORT ?? env.HAPPIER_STACK_PG_PORT) ??
|
|
297
|
+
(await pickNextFreeTcpPort(serverPort + 1000, { reservedPorts }));
|
|
298
|
+
reservedPorts.add(pgPort);
|
|
299
|
+
const redisPort =
|
|
300
|
+
coercePort(existingEnv.HAPPIER_STACK_REDIS_PORT ?? env.HAPPIER_STACK_REDIS_PORT) ??
|
|
301
|
+
(await pickNextFreeTcpPort(pgPort + 1, { reservedPorts }));
|
|
302
|
+
reservedPorts.add(redisPort);
|
|
303
|
+
const minioPort =
|
|
304
|
+
coercePort(existingEnv.HAPPIER_STACK_MINIO_PORT ?? env.HAPPIER_STACK_MINIO_PORT) ??
|
|
305
|
+
(await pickNextFreeTcpPort(redisPort + 1, { reservedPorts }));
|
|
306
|
+
reservedPorts.add(minioPort);
|
|
307
|
+
const minioConsolePort =
|
|
308
|
+
coercePort(existingEnv.HAPPIER_STACK_MINIO_CONSOLE_PORT ?? env.HAPPIER_STACK_MINIO_CONSOLE_PORT) ??
|
|
309
|
+
(await pickNextFreeTcpPort(minioPort + 1, { reservedPorts }));
|
|
310
|
+
reservedPorts.add(minioConsolePort);
|
|
311
|
+
|
|
312
|
+
const pgUser = (existingEnv.HAPPIER_STACK_PG_USER ?? env.HAPPIER_STACK_PG_USER ?? 'handy').trim() || 'handy';
|
|
313
|
+
const pgPassword = (existingEnv.HAPPIER_STACK_PG_PASSWORD ?? env.HAPPIER_STACK_PG_PASSWORD ?? '').trim() || randomToken(24);
|
|
314
|
+
const pgDb = (existingEnv.HAPPIER_STACK_PG_DATABASE ?? env.HAPPIER_STACK_PG_DATABASE ?? 'handy').trim() || 'handy';
|
|
315
|
+
|
|
316
|
+
const s3Bucket =
|
|
317
|
+
(existingEnv.S3_BUCKET ?? env.S3_BUCKET ?? '').trim() || sanitizeDnsLabel(`happier-${stackName}`, { fallback: 'happier' });
|
|
318
|
+
const s3AccessKey = (existingEnv.S3_ACCESS_KEY ?? env.S3_ACCESS_KEY ?? '').trim() || randomToken(12);
|
|
319
|
+
const s3SecretKey = (existingEnv.S3_SECRET_KEY ?? env.S3_SECRET_KEY ?? '').trim() || randomToken(24);
|
|
320
|
+
|
|
321
|
+
const secretFile = (existingEnv.HAPPIER_STACK_HANDY_MASTER_SECRET_FILE ?? env.HAPPIER_STACK_HANDY_MASTER_SECRET_FILE ?? '').trim()
|
|
322
|
+
? (existingEnv.HAPPIER_STACK_HANDY_MASTER_SECRET_FILE ?? env.HAPPIER_STACK_HANDY_MASTER_SECRET_FILE).trim()
|
|
323
|
+
: join(baseDir, 'happier-server', 'handy-master-secret.txt');
|
|
324
|
+
const handyMasterSecret = (existingEnv.HANDY_MASTER_SECRET ?? env.HANDY_MASTER_SECRET ?? '').trim()
|
|
325
|
+
? (existingEnv.HANDY_MASTER_SECRET ?? env.HANDY_MASTER_SECRET).trim()
|
|
326
|
+
: await ensureTextFile({ path: secretFile, generate: () => randomToken(32) });
|
|
327
|
+
|
|
328
|
+
const databaseUrl = `postgresql://${encodeURIComponent(pgUser)}:${encodeURIComponent(pgPassword)}@127.0.0.1:${pgPort}/${encodeURIComponent(pgDb)}`;
|
|
329
|
+
const redisUrl = `redis://127.0.0.1:${redisPort}`;
|
|
330
|
+
const s3Host = '127.0.0.1';
|
|
331
|
+
const s3UseSsl = 'false';
|
|
332
|
+
const pub = String(publicServerUrl ?? '').trim().replace(/\/+$/, '');
|
|
333
|
+
if (!pub) {
|
|
334
|
+
throw new Error('[infra] publicServerUrl is required for managed infra (to set S3_PUBLIC_URL)');
|
|
335
|
+
}
|
|
336
|
+
const s3PublicUrl = `${pub}/files`;
|
|
337
|
+
|
|
338
|
+
if (envPath) {
|
|
339
|
+
// Ephemeral stacks should not pin ports in env files. In stack runtime, callers set
|
|
340
|
+
// HAPPIER_STACK_EPHEMERAL_PORTS=1 (via stack.runtime.json overlay) while the stack owner is alive.
|
|
341
|
+
//
|
|
342
|
+
// For offline tooling (e.g. auth seeding) we still want to preserve the invariant:
|
|
343
|
+
// - non-main stacks are ephemeral-by-default unless the user explicitly pinned ports already.
|
|
344
|
+
const runtimeEphemeral = (env.HAPPIER_STACK_EPHEMERAL_PORTS ?? '').toString().trim() === '1';
|
|
345
|
+
const alreadyPinnedPorts =
|
|
346
|
+
Boolean((existingEnv.HAPPIER_STACK_PG_PORT ?? '').trim()) ||
|
|
347
|
+
Boolean((existingEnv.HAPPIER_STACK_REDIS_PORT ?? '').trim()) ||
|
|
348
|
+
Boolean((existingEnv.HAPPIER_STACK_MINIO_PORT ?? '').trim()) ||
|
|
349
|
+
Boolean((existingEnv.HAPPIER_STACK_MINIO_CONSOLE_PORT ?? '').trim());
|
|
350
|
+
const ephemeralPorts = runtimeEphemeral || (stackName !== 'main' && !alreadyPinnedPorts);
|
|
351
|
+
await ensureEnvFileUpdated({
|
|
352
|
+
envPath,
|
|
353
|
+
updates: [
|
|
354
|
+
// Stable credentials/files: persist these so restarts keep the same DB/user and S3 creds.
|
|
355
|
+
{ key: 'HAPPIER_STACK_PG_USER', value: pgUser },
|
|
356
|
+
{ key: 'HAPPIER_STACK_PG_PASSWORD', value: pgPassword },
|
|
357
|
+
{ key: 'HAPPIER_STACK_PG_DATABASE', value: pgDb },
|
|
358
|
+
{ key: 'HAPPIER_STACK_HANDY_MASTER_SECRET_FILE', value: secretFile },
|
|
359
|
+
{ key: 'S3_ACCESS_KEY', value: s3AccessKey },
|
|
360
|
+
{ key: 'S3_SECRET_KEY', value: s3SecretKey },
|
|
361
|
+
{ key: 'S3_BUCKET', value: s3Bucket },
|
|
362
|
+
// Ports + derived URLs: persist only when ports are explicitly pinned (non-ephemeral mode).
|
|
363
|
+
...(ephemeralPorts
|
|
364
|
+
? []
|
|
365
|
+
: [
|
|
366
|
+
{ key: 'HAPPIER_STACK_PG_PORT', value: String(pgPort) },
|
|
367
|
+
{ key: 'HAPPIER_STACK_REDIS_PORT', value: String(redisPort) },
|
|
368
|
+
{ key: 'HAPPIER_STACK_MINIO_PORT', value: String(minioPort) },
|
|
369
|
+
{ key: 'HAPPIER_STACK_MINIO_CONSOLE_PORT', value: String(minioConsolePort) },
|
|
370
|
+
// Vars consumed by happier-server:
|
|
371
|
+
{ key: 'DATABASE_URL', value: databaseUrl },
|
|
372
|
+
{ key: 'REDIS_URL', value: redisUrl },
|
|
373
|
+
{ key: 'S3_HOST', value: s3Host },
|
|
374
|
+
{ key: 'S3_PORT', value: String(minioPort) },
|
|
375
|
+
{ key: 'S3_USE_SSL', value: s3UseSsl },
|
|
376
|
+
{ key: 'S3_PUBLIC_URL', value: s3PublicUrl },
|
|
377
|
+
]),
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const composePath = join(infraDir, 'docker-compose.yml');
|
|
383
|
+
const projectName = composeProjectName(stackName);
|
|
384
|
+
const yaml = buildComposeYaml({
|
|
385
|
+
infraDir,
|
|
386
|
+
pgPort,
|
|
387
|
+
pgUser,
|
|
388
|
+
pgPassword,
|
|
389
|
+
pgDb,
|
|
390
|
+
redisPort,
|
|
391
|
+
minioPort,
|
|
392
|
+
minioConsolePort,
|
|
393
|
+
s3AccessKey,
|
|
394
|
+
s3SecretKey,
|
|
395
|
+
s3Bucket,
|
|
396
|
+
});
|
|
397
|
+
await writeFile(composePath, yaml, 'utf-8');
|
|
398
|
+
|
|
399
|
+
await dockerCompose({
|
|
400
|
+
composePath,
|
|
401
|
+
projectName,
|
|
402
|
+
args: ['up', '-d', '--remove-orphans'],
|
|
403
|
+
options: { cwd: baseDir, stdio: quiet ? 'ignore' : 'inherit' },
|
|
404
|
+
quiet,
|
|
405
|
+
});
|
|
406
|
+
await waitForHealthyPostgres({ composePath, projectName, pgUser, pgDb });
|
|
407
|
+
await waitForHealthyRedis({ composePath, projectName });
|
|
408
|
+
|
|
409
|
+
if (!skipMinioInit) {
|
|
410
|
+
// Ensure bucket exists (idempotent). This can race with Minio startup; retry a few times.
|
|
411
|
+
await waitForMinioReady({ composePath, projectName });
|
|
412
|
+
await dockerCompose({
|
|
413
|
+
composePath,
|
|
414
|
+
projectName,
|
|
415
|
+
args: ['run', '--rm', '--no-deps', 'minio-init'],
|
|
416
|
+
options: { cwd: baseDir, stdio: quiet ? 'ignore' : 'inherit' },
|
|
417
|
+
quiet,
|
|
418
|
+
retries: 3,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
composePath,
|
|
424
|
+
projectName,
|
|
425
|
+
infraDir,
|
|
426
|
+
env: {
|
|
427
|
+
DATABASE_URL: databaseUrl,
|
|
428
|
+
REDIS_URL: redisUrl,
|
|
429
|
+
S3_HOST: s3Host,
|
|
430
|
+
S3_PORT: String(minioPort),
|
|
431
|
+
S3_USE_SSL: s3UseSsl,
|
|
432
|
+
S3_ACCESS_KEY: s3AccessKey,
|
|
433
|
+
S3_SECRET_KEY: s3SecretKey,
|
|
434
|
+
S3_BUCKET: s3Bucket,
|
|
435
|
+
S3_PUBLIC_URL: s3PublicUrl,
|
|
436
|
+
HANDY_MASTER_SECRET: handyMasterSecret,
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function applyHappyServerMigrations({ serverDir, env, quiet = false }) {
|
|
442
|
+
// Non-interactive + idempotent. Safe for dev; also safe for managed stacks on start.
|
|
443
|
+
await pmExecBin({ dir: serverDir, bin: 'prisma', args: ['migrate', 'deploy'], env, quiet });
|
|
444
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { pickLanIpv4 } from '../net/lan_ip.mjs';
|
|
2
|
+
import { normalizeUrlNoTrailingSlash } from '../net/url.mjs';
|
|
3
|
+
|
|
4
|
+
function resolveLanIp({ env = process.env } = {}) {
|
|
5
|
+
const raw = (env.HAPPIER_STACK_LAN_IP ?? '').toString().trim();
|
|
6
|
+
return raw || pickLanIpv4() || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isLocalHostName(hostname) {
|
|
10
|
+
const h = String(hostname ?? '').trim().toLowerCase();
|
|
11
|
+
if (!h) return false;
|
|
12
|
+
if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0') return true;
|
|
13
|
+
if (h.endsWith('.localhost')) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* For mobile devices, `localhost` and `*.localhost` are not reachable.
|
|
19
|
+
*
|
|
20
|
+
* This helper rewrites any local server URL to a LAN-reachable URL using the machine's LAN IPv4.
|
|
21
|
+
* It preserves protocol, port, and path/query.
|
|
22
|
+
*
|
|
23
|
+
* Notes:
|
|
24
|
+
* - If the URL is already non-local (e.g. Tailscale HTTPS), it is returned unchanged.
|
|
25
|
+
* - If LAN IP cannot be determined, it returns the original URL unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveMobileReachableServerUrl({
|
|
28
|
+
env = process.env,
|
|
29
|
+
serverUrl,
|
|
30
|
+
serverPort,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const raw = String(serverUrl ?? '').trim();
|
|
33
|
+
const fallbackPort = Number(serverPort);
|
|
34
|
+
const fallback = Number.isFinite(fallbackPort) && fallbackPort > 0 ? `http://localhost:${fallbackPort}` : '';
|
|
35
|
+
const base = raw || fallback;
|
|
36
|
+
if (!base) return '';
|
|
37
|
+
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(base);
|
|
41
|
+
} catch {
|
|
42
|
+
return base;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
46
|
+
return base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!isLocalHostName(parsed.hostname)) {
|
|
50
|
+
return normalizeUrlNoTrailingSlash(parsed.toString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lanIp = resolveLanIp({ env });
|
|
54
|
+
if (!lanIp) {
|
|
55
|
+
return normalizeUrlNoTrailingSlash(parsed.toString());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parsed.hostname = lanIp;
|
|
59
|
+
return normalizeUrlNoTrailingSlash(parsed.toString());
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { resolveMobileReachableServerUrl } from './mobile_api_url.mjs';
|
|
5
|
+
|
|
6
|
+
test('resolveMobileReachableServerUrl rewrites localhost to LAN IP (env override)', () => {
|
|
7
|
+
const out = resolveMobileReachableServerUrl({
|
|
8
|
+
env: { HAPPIER_STACK_LAN_IP: '192.168.0.50' },
|
|
9
|
+
serverUrl: 'http://localhost:3005',
|
|
10
|
+
serverPort: 3005,
|
|
11
|
+
});
|
|
12
|
+
assert.equal(out, 'http://192.168.0.50:3005');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('resolveMobileReachableServerUrl rewrites *.localhost to LAN IP (env override)', () => {
|
|
16
|
+
const out = resolveMobileReachableServerUrl({
|
|
17
|
+
env: { HAPPIER_STACK_LAN_IP: '10.0.0.12' },
|
|
18
|
+
serverUrl: 'http://happy-exp1.localhost:3009/',
|
|
19
|
+
serverPort: 3009,
|
|
20
|
+
});
|
|
21
|
+
assert.equal(out, 'http://10.0.0.12:3009');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('resolveMobileReachableServerUrl preserves path and query', () => {
|
|
25
|
+
const out = resolveMobileReachableServerUrl({
|
|
26
|
+
env: { HAPPIER_STACK_LAN_IP: '10.0.0.12' },
|
|
27
|
+
serverUrl: 'http://127.0.0.1:3005/api?x=1',
|
|
28
|
+
serverPort: 3005,
|
|
29
|
+
});
|
|
30
|
+
assert.equal(out, 'http://10.0.0.12:3005/api?x=1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolveMobileReachableServerUrl does not rewrite non-local URLs', () => {
|
|
34
|
+
const out = resolveMobileReachableServerUrl({
|
|
35
|
+
env: { HAPPIER_STACK_LAN_IP: '192.168.0.50' },
|
|
36
|
+
serverUrl: 'https://my-machine.tailnet.ts.net',
|
|
37
|
+
serverPort: 3005,
|
|
38
|
+
});
|
|
39
|
+
assert.equal(out, 'https://my-machine.tailnet.ts.net');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('resolveMobileReachableServerUrl falls back to LAN IP:port when serverUrl is empty', () => {
|
|
43
|
+
const out = resolveMobileReachableServerUrl({
|
|
44
|
+
env: { HAPPIER_STACK_LAN_IP: '10.1.2.3' },
|
|
45
|
+
serverUrl: '',
|
|
46
|
+
serverPort: 3010,
|
|
47
|
+
});
|
|
48
|
+
assert.equal(out, 'http://10.1.2.3:3010');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('resolveMobileReachableServerUrl preserves invalid URL-like input strings', () => {
|
|
52
|
+
const out = resolveMobileReachableServerUrl({
|
|
53
|
+
env: { HAPPIER_STACK_LAN_IP: '10.1.2.3' },
|
|
54
|
+
serverUrl: 'localhost:3005',
|
|
55
|
+
serverPort: 3005,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(out, 'localhost:3005');
|
|
58
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readEnvValueFromFile } from '../env/read.mjs';
|
|
2
|
+
|
|
3
|
+
export const STACK_RESERVED_PORT_KEYS = [
|
|
4
|
+
'HAPPIER_STACK_SERVER_PORT',
|
|
5
|
+
'HAPPIER_STACK_SERVER_BACKEND_PORT',
|
|
6
|
+
'HAPPIER_STACK_PG_PORT',
|
|
7
|
+
'HAPPIER_STACK_REDIS_PORT',
|
|
8
|
+
'HAPPIER_STACK_MINIO_PORT',
|
|
9
|
+
'HAPPIER_STACK_MINIO_CONSOLE_PORT',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const INFRA_RESERVED_PORT_KEYS = [
|
|
13
|
+
'HAPPIER_STACK_SERVER_PORT',
|
|
14
|
+
'HAPPIER_STACK_PG_PORT',
|
|
15
|
+
'HAPPIER_STACK_REDIS_PORT',
|
|
16
|
+
'HAPPIER_STACK_MINIO_PORT',
|
|
17
|
+
'HAPPIER_STACK_MINIO_CONSOLE_PORT',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function coercePort(v) {
|
|
21
|
+
const s = String(v ?? '').trim();
|
|
22
|
+
if (!s) return null;
|
|
23
|
+
const n = Number(s);
|
|
24
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveServerPortFromEnv({ env = process.env, defaultPort = 3005 } = {}) {
|
|
28
|
+
const raw = (env.HAPPIER_STACK_SERVER_PORT ?? '').toString().trim() || '';
|
|
29
|
+
const n = raw ? Number(raw) : Number(defaultPort);
|
|
30
|
+
return Number.isFinite(n) && n > 0 ? n : Number(defaultPort);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function listPortsFromEnvObject(env, keys) {
|
|
34
|
+
const obj = env && typeof env === 'object' ? env : {};
|
|
35
|
+
const list = Array.isArray(keys) ? keys : [];
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const k of list) {
|
|
38
|
+
const p = coercePort(obj[k]);
|
|
39
|
+
if (p) out.push(p);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readServerPortFromEnvFile(envPath, { defaultPort = 3005 } = {}) {
|
|
45
|
+
const v = (await readEnvValueFromFile(envPath, 'HAPPIER_STACK_SERVER_PORT')) || '';
|
|
46
|
+
const n = v ? Number(String(v).trim()) : Number(defaultPort);
|
|
47
|
+
return Number.isFinite(n) && n > 0 ? n : Number(defaultPort);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// For stack env files, "missing" means "ephemeral stack" (no pinned port).
|
|
51
|
+
export async function readPinnedServerPortFromEnvFile(envPath) {
|
|
52
|
+
const v = (await readEnvValueFromFile(envPath, 'HAPPIER_STACK_SERVER_PORT')) || '';
|
|
53
|
+
const n = v ? Number(String(v).trim()) : NaN;
|
|
54
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { isUnifiedHappyServerLight } from './flavor_scripts.mjs';
|
|
6
|
+
|
|
7
|
+
function extractPrismaClient(mod) {
|
|
8
|
+
return mod?.PrismaClient ?? mod?.default?.PrismaClient ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function importPrismaClientFromFile(path) {
|
|
12
|
+
const mod = await import(pathToFileURL(path).href);
|
|
13
|
+
const PrismaClient = extractPrismaClient(mod);
|
|
14
|
+
if (!PrismaClient) {
|
|
15
|
+
throw new Error(`[prisma] PrismaClient export not found in: ${path}`);
|
|
16
|
+
}
|
|
17
|
+
return PrismaClient;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function importPrismaClientFromNodeModules({ dir }) {
|
|
21
|
+
const req = createRequire(import.meta.url);
|
|
22
|
+
const resolved = req.resolve('@prisma/client', { paths: [dir] });
|
|
23
|
+
return await importPrismaClientFromFile(resolved);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function importPrismaClientFromGeneratedSqlite({ dir }) {
|
|
27
|
+
const path = join(dir, 'generated', 'sqlite-client', 'index.js');
|
|
28
|
+
return await importPrismaClientFromFile(path);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function importPrismaClientForHappyServerLight({ serverDir }) {
|
|
32
|
+
// Current Happier light flavor (PG_Light via embedded PGlite) uses the standard Prisma client.
|
|
33
|
+
// Legacy SQLite-era generated clients are not used by default.
|
|
34
|
+
void isUnifiedHappyServerLight;
|
|
35
|
+
return await importPrismaClientFromNodeModules({ dir: serverDir });
|
|
36
|
+
}
|