@happier-dev/stack 0.1.0-preview.5.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 +159 -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 +94 -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,110 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { runCaptureIfCommandExists } from '../proc/commands.mjs';
|
|
4
|
+
|
|
5
|
+
export async function listListenPids(port) {
|
|
6
|
+
if (!Number.isFinite(port) || port <= 0) return [];
|
|
7
|
+
if (process.platform === 'win32') return [];
|
|
8
|
+
|
|
9
|
+
let raw = '';
|
|
10
|
+
try {
|
|
11
|
+
// `lsof` exits non-zero if no matches; normalize to empty output.
|
|
12
|
+
raw = await runCaptureIfCommandExists('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']);
|
|
13
|
+
if (!raw && process.platform === 'darwin') {
|
|
14
|
+
// Some non-interactive shells (launchd/GUI apps) have a PATH that omits /usr/sbin,
|
|
15
|
+
// which makes `command -v lsof` fail even though lsof exists. Fall back to absolute paths.
|
|
16
|
+
raw =
|
|
17
|
+
(await runCaptureIfCommandExists('/usr/sbin/lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'])) ||
|
|
18
|
+
(await runCaptureIfCommandExists('/usr/bin/lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'])) ||
|
|
19
|
+
'';
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
raw = '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Array.from(
|
|
26
|
+
new Set(
|
|
27
|
+
raw
|
|
28
|
+
.split(/\s+/g)
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((s) => Number(s))
|
|
32
|
+
.filter((n) => Number.isInteger(n) && n > 1)
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Best-effort: kill any processes LISTENing on a TCP port.
|
|
39
|
+
* Used to avoid EADDRINUSE when a previous run left a server behind.
|
|
40
|
+
*/
|
|
41
|
+
export async function killPortListeners(port, { label = 'port' } = {}) {
|
|
42
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pids = await listListenPids(port);
|
|
50
|
+
|
|
51
|
+
if (!pids.length) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log(`[local] ${label}: freeing tcp:${port} (killing pids: ${pids.join(', ')})`);
|
|
57
|
+
|
|
58
|
+
for (const pid of pids) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 'SIGTERM');
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await delay(500);
|
|
67
|
+
|
|
68
|
+
for (const pid of pids) {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
process.kill(pid, 'SIGKILL');
|
|
72
|
+
} catch {
|
|
73
|
+
// not running / no permission
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return pids;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function isTcpPortFree(port, { host = '127.0.0.1' } = {}) {
|
|
81
|
+
if (!Number.isFinite(port) || port <= 0) return false;
|
|
82
|
+
|
|
83
|
+
// Prefer lsof-based detection to catch IPv6 listeners (e.g. TCP *:8081 (LISTEN))
|
|
84
|
+
// which can make a "bind 127.0.0.1" probe incorrectly report "free" on macOS.
|
|
85
|
+
const pids = await listListenPids(port);
|
|
86
|
+
if (pids.length) return false;
|
|
87
|
+
|
|
88
|
+
// Fallback: attempt to bind.
|
|
89
|
+
return await new Promise((resolvePromise) => {
|
|
90
|
+
const srv = net.createServer();
|
|
91
|
+
srv.unref();
|
|
92
|
+
srv.on('error', () => resolvePromise(false));
|
|
93
|
+
srv.listen({ port, host }, () => {
|
|
94
|
+
srv.close(() => resolvePromise(true));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function pickNextFreeTcpPort(startPort, { reservedPorts = new Set(), host = '127.0.0.1', tries = 200 } = {}) {
|
|
100
|
+
let port = startPort;
|
|
101
|
+
for (let i = 0; i < tries; i++) {
|
|
102
|
+
// eslint-disable-next-line no-await-in-loop
|
|
103
|
+
if (!reservedPorts.has(port) && (await isTcpPortFree(port, { host }))) {
|
|
104
|
+
return port;
|
|
105
|
+
}
|
|
106
|
+
port += 1;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`[local] unable to find a free TCP port starting at ${startPort}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight TCP port forwarder using Node.js built-in `net` module.
|
|
3
|
+
*
|
|
4
|
+
* Used to expose Expo dev server (which binds to LAN IP) on the Tailscale interface,
|
|
5
|
+
* enabling remote device access over Tailscale without modifying Expo's binding behavior.
|
|
6
|
+
*
|
|
7
|
+
* Can be run standalone:
|
|
8
|
+
* node tcp_forward.mjs --listen-host=100.x.y.z --listen-port=8081 --target-host=192.168.1.50 --target-port=8081
|
|
9
|
+
*
|
|
10
|
+
* Or imported and spawned as a managed child process.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import net from 'node:net';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a TCP forwarding server.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {string} options.listenHost - Host/IP to listen on (e.g., Tailscale IP)
|
|
20
|
+
* @param {number} options.listenPort - Port to listen on
|
|
21
|
+
* @param {string} options.targetHost - Host/IP to forward to (e.g., LAN IP or 127.0.0.1)
|
|
22
|
+
* @param {number} options.targetPort - Port to forward to
|
|
23
|
+
* @param {string} [options.label] - Label for logging (default: 'tcp-forward')
|
|
24
|
+
* @returns {net.Server}
|
|
25
|
+
*/
|
|
26
|
+
export function createTcpForwarder({ listenHost, listenPort, targetHost, targetPort, label = 'tcp-forward' }) {
|
|
27
|
+
const server = net.createServer((clientSocket) => {
|
|
28
|
+
const targetSocket = net.createConnection({ host: targetHost, port: targetPort }, () => {
|
|
29
|
+
// Connection established, pipe data both ways
|
|
30
|
+
clientSocket.pipe(targetSocket);
|
|
31
|
+
targetSocket.pipe(clientSocket);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Handle errors on both sockets
|
|
35
|
+
clientSocket.on('error', (err) => {
|
|
36
|
+
if (err.code !== 'ECONNRESET') {
|
|
37
|
+
process.stderr.write(`[${label}] client error: ${err.message}\n`);
|
|
38
|
+
}
|
|
39
|
+
targetSocket.destroy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
targetSocket.on('error', (err) => {
|
|
43
|
+
if (err.code !== 'ECONNRESET' && err.code !== 'ECONNREFUSED') {
|
|
44
|
+
process.stderr.write(`[${label}] target error: ${err.message}\n`);
|
|
45
|
+
}
|
|
46
|
+
clientSocket.destroy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Clean up on close
|
|
50
|
+
clientSocket.on('close', () => targetSocket.destroy());
|
|
51
|
+
targetSocket.on('close', () => clientSocket.destroy());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
server.on('error', (err) => {
|
|
55
|
+
process.stderr.write(`[${label}] server error: ${err.message}\n`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return server;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start a TCP forwarder and return a promise that resolves when listening.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} options - Same as createTcpForwarder
|
|
65
|
+
* @returns {Promise<{ server: net.Server, address: string, port: number }>}
|
|
66
|
+
*/
|
|
67
|
+
export async function startTcpForwarder(options) {
|
|
68
|
+
const { listenHost, listenPort, label = 'tcp-forward' } = options;
|
|
69
|
+
const server = createTcpForwarder(options);
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
server.once('error', reject);
|
|
73
|
+
server.listen(listenPort, listenHost, () => {
|
|
74
|
+
server.removeListener('error', reject);
|
|
75
|
+
const addr = server.address();
|
|
76
|
+
const address = typeof addr === 'object' ? addr.address : listenHost;
|
|
77
|
+
const port = typeof addr === 'object' ? addr.port : listenPort;
|
|
78
|
+
process.stdout.write(`[${label}] forwarding ${address}:${port} -> ${options.targetHost}:${options.targetPort}\n`);
|
|
79
|
+
resolve({ server, address, port });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function trySendIpc(msg) {
|
|
85
|
+
try {
|
|
86
|
+
if (typeof process.send === 'function') {
|
|
87
|
+
process.send(msg);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gracefully stop a TCP forwarder server.
|
|
96
|
+
*
|
|
97
|
+
* @param {net.Server} server
|
|
98
|
+
* @param {string} [label]
|
|
99
|
+
* @returns {Promise<void>}
|
|
100
|
+
*/
|
|
101
|
+
export async function stopTcpForwarder(server, label = 'tcp-forward') {
|
|
102
|
+
if (!server) return;
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
server.close(() => {
|
|
105
|
+
process.stdout.write(`[${label}] stopped\n`);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
// Force-close after timeout
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
resolve();
|
|
111
|
+
}, 2000);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Standalone CLI mode
|
|
116
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
const kv = new Map();
|
|
119
|
+
for (const arg of args) {
|
|
120
|
+
const m = arg.match(/^--([^=]+)=(.*)$/);
|
|
121
|
+
if (m) kv.set(m[1], m[2]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const listenHost = kv.get('listen-host') || kv.get('listenHost');
|
|
125
|
+
const listenPort = Number(kv.get('listen-port') || kv.get('listenPort'));
|
|
126
|
+
const targetHost = kv.get('target-host') || kv.get('targetHost') || '127.0.0.1';
|
|
127
|
+
const targetPort = Number(kv.get('target-port') || kv.get('targetPort'));
|
|
128
|
+
const label = kv.get('label') || 'tcp-forward';
|
|
129
|
+
|
|
130
|
+
if (!listenHost || !listenPort || !targetPort) {
|
|
131
|
+
console.error('Usage: node tcp_forward.mjs --listen-host=<ip> --listen-port=<port> --target-host=<ip> --target-port=<port> [--label=<label>]');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const shutdown = () => {
|
|
136
|
+
process.stdout.write(`\n[${label}] shutting down...\n`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
process.on('SIGINT', shutdown);
|
|
141
|
+
process.on('SIGTERM', shutdown);
|
|
142
|
+
|
|
143
|
+
startTcpForwarder({ listenHost, listenPort, targetHost, targetPort, label })
|
|
144
|
+
.then(() => {
|
|
145
|
+
trySendIpc({ type: 'ready', listenHost, listenPort, targetHost, targetPort, label });
|
|
146
|
+
// Keep running until signal
|
|
147
|
+
})
|
|
148
|
+
.catch((err) => {
|
|
149
|
+
trySendIpc({
|
|
150
|
+
type: 'error',
|
|
151
|
+
code: err && typeof err === 'object' ? err.code : null,
|
|
152
|
+
message: err instanceof Error ? err.message : String(err ?? 'unknown error'),
|
|
153
|
+
listenHost,
|
|
154
|
+
listenPort,
|
|
155
|
+
targetHost,
|
|
156
|
+
targetPort,
|
|
157
|
+
label,
|
|
158
|
+
});
|
|
159
|
+
console.error(`[${label}] failed to start: ${err.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function normalizeUrlNoTrailingSlash(raw) {
|
|
2
|
+
const s = String(raw ?? '').trim();
|
|
3
|
+
if (!s) return '';
|
|
4
|
+
|
|
5
|
+
let u;
|
|
6
|
+
try {
|
|
7
|
+
u = new URL(s);
|
|
8
|
+
} catch {
|
|
9
|
+
// Best-effort: if it's a plain string with trailing slash, trim it.
|
|
10
|
+
return s.endsWith('/') ? s.replace(/\/+$/, '') : s;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Only normalize "base" URLs without search/hash.
|
|
14
|
+
// If search/hash is present, removing slashes can change semantics.
|
|
15
|
+
if (u.search || u.hash) {
|
|
16
|
+
return u.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Normalize multiple trailing slashes down to none (root) or one-less (non-root).
|
|
20
|
+
const path = u.pathname || '/';
|
|
21
|
+
if (path === '/' || path === '') {
|
|
22
|
+
return u.origin;
|
|
23
|
+
}
|
|
24
|
+
if (path.endsWith('/')) {
|
|
25
|
+
const nextPath = path.replace(/\/+$/, '');
|
|
26
|
+
return `${u.origin}${nextPath}`;
|
|
27
|
+
}
|
|
28
|
+
return u.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { normalizeUrlNoTrailingSlash } from './url.mjs';
|
|
5
|
+
|
|
6
|
+
test('normalizeUrlNoTrailingSlash removes trailing slash from origins', () => {
|
|
7
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/'), 'https://example.com');
|
|
8
|
+
assert.equal(normalizeUrlNoTrailingSlash('http://localhost:3005/'), 'http://localhost:3005');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('normalizeUrlNoTrailingSlash removes trailing slash from path-only base URLs', () => {
|
|
12
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api/'), 'https://example.com/api');
|
|
13
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api///'), 'https://example.com/api');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('normalizeUrlNoTrailingSlash preserves query/hash URLs', () => {
|
|
17
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/?q=1'), 'https://example.com/?q=1');
|
|
18
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/api/?q=1'), 'https://example.com/api/?q=1');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('normalizeUrlNoTrailingSlash handles invalid/blank values defensively', () => {
|
|
22
|
+
assert.equal(normalizeUrlNoTrailingSlash(''), '');
|
|
23
|
+
assert.equal(normalizeUrlNoTrailingSlash(' '), '');
|
|
24
|
+
assert.equal(normalizeUrlNoTrailingSlash('not-a-url///'), 'not-a-url');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('normalizeUrlNoTrailingSlash preserves hash-only URL semantics', () => {
|
|
28
|
+
assert.equal(normalizeUrlNoTrailingSlash('https://example.com/path/#hash'), 'https://example.com/path/#hash');
|
|
29
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function expandHome(p) {
|
|
5
|
+
return String(p ?? '').replace(/^~(?=[/\\])/, homedir());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getCanonicalHomeDirFromEnv(env = process.env) {
|
|
9
|
+
const fromEnv = (env.HAPPIER_STACK_CANONICAL_HOME_DIR ?? '').trim();
|
|
10
|
+
return fromEnv ? expandHome(fromEnv) : join(homedir(), '.happier-stack');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCanonicalHomeEnvPathFromEnv(env = process.env) {
|
|
14
|
+
return join(getCanonicalHomeDirFromEnv(env), '.env');
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import { expandHome, getCanonicalHomeDirFromEnv, getCanonicalHomeEnvPathFromEnv } from './canonical_home.mjs';
|
|
6
|
+
|
|
7
|
+
test('expandHome expands ~/ paths', () => {
|
|
8
|
+
assert.equal(expandHome('~/x'), `${homedir()}/x`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('expandHome expands ~\\ paths (Windows)', () => {
|
|
12
|
+
assert.equal(expandHome('~\\x'), `${homedir()}\\x`);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('expandHome leaves non-home-prefixed paths unchanged', () => {
|
|
16
|
+
assert.equal(expandHome('~user/x'), '~user/x');
|
|
17
|
+
assert.equal(expandHome('/tmp/x'), '/tmp/x');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('getCanonicalHomeDirFromEnv expands override and defaults to ~/.happier-stack', () => {
|
|
21
|
+
assert.equal(getCanonicalHomeDirFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' }), `${homedir()}/custom-home`);
|
|
22
|
+
assert.equal(getCanonicalHomeDirFromEnv({}), `${homedir()}/.happier-stack`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('getCanonicalHomeEnvPathFromEnv resolves .env under canonical home', () => {
|
|
26
|
+
const envPath = getCanonicalHomeEnvPathFromEnv({ HAPPIER_STACK_CANONICAL_HOME_DIR: '~/custom-home' });
|
|
27
|
+
assert.equal(envPath, `${homedir()}/custom-home/.env`);
|
|
28
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getStackName } from './paths.mjs';
|
|
2
|
+
import { networkInterfaces } from 'node:os';
|
|
3
|
+
import { sanitizeDnsLabel } from '../net/dns.mjs';
|
|
4
|
+
|
|
5
|
+
function resolveBindMode(env) {
|
|
6
|
+
const raw = (env.HAPPIER_STACK_BIND_MODE ?? '').toString().trim().toLowerCase();
|
|
7
|
+
return raw === 'lan' ? 'lan' : raw === 'loopback' ? 'loopback' : '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveLocalhostSubdomainPrefix(env) {
|
|
11
|
+
const raw = (env.HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX ?? '').toString().trim().toLowerCase();
|
|
12
|
+
if (!raw) return 'happier';
|
|
13
|
+
// Keep legacy compatibility (older installs used happy-<stack>.localhost).
|
|
14
|
+
if (raw === 'happy') return 'happy';
|
|
15
|
+
if (raw === 'happier') return 'happier';
|
|
16
|
+
return 'happier';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function detectLanHost({ env = process.env } = {}) {
|
|
20
|
+
const override = (env.HAPPIER_STACK_LAN_HOST ?? '').toString().trim();
|
|
21
|
+
if (override) return override;
|
|
22
|
+
|
|
23
|
+
const nets = networkInterfaces();
|
|
24
|
+
const candidates = [];
|
|
25
|
+
for (const [ifName, addrs] of Object.entries(nets)) {
|
|
26
|
+
for (const a of addrs ?? []) {
|
|
27
|
+
if (!a || a.family !== 'IPv4' || a.internal) continue;
|
|
28
|
+
const ip = String(a.address ?? '').trim();
|
|
29
|
+
if (!ip) continue;
|
|
30
|
+
if (ip.startsWith('127.')) continue;
|
|
31
|
+
// Drop link-local IPv4 (usually not host-reachable).
|
|
32
|
+
if (ip.startsWith('169.254.')) continue;
|
|
33
|
+
let score = 0;
|
|
34
|
+
if (ifName === 'lima0' || ifName.startsWith('lima')) score += 50;
|
|
35
|
+
if (ifName.startsWith('en') || ifName.startsWith('eth')) score += 10;
|
|
36
|
+
candidates.push({ ip, ifName, score });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
41
|
+
return candidates[0]?.ip ?? '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveLocalhostHost({ stackMode, stackName = null, env = process.env } = {}) {
|
|
45
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
46
|
+
if (!stackMode) return 'localhost';
|
|
47
|
+
const bindMode = resolveBindMode(env);
|
|
48
|
+
if (bindMode === 'lan') {
|
|
49
|
+
const lanHost = detectLanHost({ env });
|
|
50
|
+
if (lanHost) return lanHost;
|
|
51
|
+
}
|
|
52
|
+
if (!name || name === 'main') return 'localhost';
|
|
53
|
+
const prefix = resolveLocalhostSubdomainPrefix(env);
|
|
54
|
+
return `${prefix}-${sanitizeDnsLabel(name)}.localhost`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function preferStackLocalhostHost({ stackName = null, env = process.env } = {}) {
|
|
58
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
59
|
+
if (!name || name === 'main') return 'localhost';
|
|
60
|
+
// IMPORTANT:
|
|
61
|
+
// We intentionally do NOT gate on `dns.lookup()` here.
|
|
62
|
+
//
|
|
63
|
+
// On some systems (notably macOS), Node's DNS resolver may return ENOTFOUND for `*.localhost`
|
|
64
|
+
// even though browsers treat `*.localhost` as loopback and will load it fine.
|
|
65
|
+
//
|
|
66
|
+
// Since this hostname is primarily used for browser-facing URLs and origin isolation, we
|
|
67
|
+
// prefer a stable `<prefix>-<stack>.localhost` form by default and allow opting out via env.
|
|
68
|
+
const modeRaw = (env.HAPPIER_STACK_LOCALHOST_SUBDOMAINS ?? '')
|
|
69
|
+
.toString()
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
const disabled = modeRaw === '0' || modeRaw === 'false' || modeRaw === 'no' || modeRaw === 'off';
|
|
73
|
+
if (disabled) return 'localhost';
|
|
74
|
+
|
|
75
|
+
const preferredHost = resolveLocalhostHost({ stackMode: true, stackName: name, env });
|
|
76
|
+
return preferredHost || 'localhost';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Best-effort: for stacks, prefer `<prefix>-<stack>.localhost` over `localhost` when it's reachable.
|
|
80
|
+
// This keeps URLs stable and stack-scoped while still failing closed to plain localhost.
|
|
81
|
+
export async function preferStackLocalhostUrl(url, { stackName = null, env = process.env } = {}) {
|
|
82
|
+
const raw = String(url ?? '').trim();
|
|
83
|
+
if (!raw) return '';
|
|
84
|
+
const name = (stackName ?? '').toString().trim() || getStackName(env);
|
|
85
|
+
if (!name || name === 'main') return raw;
|
|
86
|
+
|
|
87
|
+
let u = null;
|
|
88
|
+
try {
|
|
89
|
+
u = new URL(raw);
|
|
90
|
+
} catch {
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return raw;
|
|
94
|
+
|
|
95
|
+
const bindMode = resolveBindMode(env);
|
|
96
|
+
const isLoopbackHost =
|
|
97
|
+
u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname.toLowerCase().endsWith('.localhost');
|
|
98
|
+
if (!isLoopbackHost) return raw;
|
|
99
|
+
|
|
100
|
+
// In LAN bind mode, prefer an IP address that is reachable from other devices/hosts.
|
|
101
|
+
// This is especially useful inside VMs (e.g. Lima vzNAT) where host-local `*.localhost`
|
|
102
|
+
// URLs are not reachable without explicit port forwarding.
|
|
103
|
+
if (bindMode === 'lan') {
|
|
104
|
+
const lanHost = detectLanHost({ env });
|
|
105
|
+
if (lanHost) return raw.replace(`://${u.hostname}`, `://${lanHost}`);
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const preferredHost = await preferStackLocalhostHost({ stackName: name, env });
|
|
110
|
+
if (!preferredHost || preferredHost === 'localhost') return raw;
|
|
111
|
+
return raw.replace(`://${u.hostname}`, `://${preferredHost}`);
|
|
112
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { preferStackLocalhostUrl, resolveLocalhostHost } from './localhost_host.mjs';
|
|
4
|
+
|
|
5
|
+
test('preferStackLocalhostUrl rewrites *.localhost to LAN IP when bind mode is lan', async () => {
|
|
6
|
+
const env = {
|
|
7
|
+
HAPPIER_STACK_STACK: 'dev-auth',
|
|
8
|
+
HAPPIER_STACK_BIND_MODE: 'lan',
|
|
9
|
+
// Override LAN host so test is deterministic.
|
|
10
|
+
HAPPIER_STACK_LAN_HOST: '192.168.5.15',
|
|
11
|
+
};
|
|
12
|
+
const url = await preferStackLocalhostUrl('http://happy-dev-auth.localhost:18137', { stackName: 'dev-auth', env });
|
|
13
|
+
assert.equal(url, 'http://192.168.5.15:18137');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('resolveLocalhostHost returns LAN IP when bind mode is lan', () => {
|
|
17
|
+
const env = { HAPPIER_STACK_BIND_MODE: 'lan', HAPPIER_STACK_LAN_HOST: '192.168.5.15' };
|
|
18
|
+
assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), '192.168.5.15');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('resolveLocalhostHost uses happier-<stack>.localhost by default', () => {
|
|
22
|
+
const env = {};
|
|
23
|
+
assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happier-dev-auth.localhost');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('resolveLocalhostHost supports legacy prefix via HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX=happy', () => {
|
|
27
|
+
const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX: 'happy' };
|
|
28
|
+
assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happy-dev-auth.localhost');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resolveLocalhostHost falls back to happier prefix for invalid override values', () => {
|
|
32
|
+
const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAIN_PREFIX: 'invalid-prefix' };
|
|
33
|
+
assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'dev-auth', env }), 'happier-dev-auth.localhost');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('resolveLocalhostHost returns localhost for non-stack and main stack contexts', () => {
|
|
37
|
+
assert.equal(resolveLocalhostHost({ stackMode: false, stackName: 'dev-auth', env: {} }), 'localhost');
|
|
38
|
+
assert.equal(resolveLocalhostHost({ stackMode: true, stackName: 'main', env: {} }), 'localhost');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('preferStackLocalhostUrl respects localhost subdomain disable policy', async () => {
|
|
42
|
+
const env = { HAPPIER_STACK_LOCALHOST_SUBDOMAINS: 'false' };
|
|
43
|
+
const raw = 'http://localhost:18137';
|
|
44
|
+
const url = await preferStackLocalhostUrl(raw, { stackName: 'dev-auth', env });
|
|
45
|
+
assert.equal(url, raw);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('preferStackLocalhostUrl preserves non-http schemes and non-loopback hosts', async () => {
|
|
49
|
+
const env = { HAPPIER_STACK_BIND_MODE: 'lan', HAPPIER_STACK_LAN_HOST: '192.168.5.15' };
|
|
50
|
+
assert.equal(
|
|
51
|
+
await preferStackLocalhostUrl('ws://localhost:18137', { stackName: 'dev-auth', env }),
|
|
52
|
+
'ws://localhost:18137'
|
|
53
|
+
);
|
|
54
|
+
assert.equal(
|
|
55
|
+
await preferStackLocalhostUrl('https://example.com:18137', { stackName: 'dev-auth', env }),
|
|
56
|
+
'https://example.com:18137'
|
|
57
|
+
);
|
|
58
|
+
});
|