@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,478 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import {
|
|
6
|
+
ensureExpoIsolationEnv,
|
|
7
|
+
getExpoStatePaths,
|
|
8
|
+
isStateProcessRunning,
|
|
9
|
+
wantsExpoClearCache,
|
|
10
|
+
writePidState,
|
|
11
|
+
} from '../expo/expo.mjs';
|
|
12
|
+
import { pickExpoDevMetroPort } from '../expo/metro_ports.mjs';
|
|
13
|
+
import { recordStackRuntimeUpdate } from '../stack/runtime_state.mjs';
|
|
14
|
+
import { killProcessGroupOwnedByStack } from '../proc/ownership.mjs';
|
|
15
|
+
import { expoSpawn } from '../expo/command.mjs';
|
|
16
|
+
import { resolveMobileExpoConfig } from '../mobile/config.mjs';
|
|
17
|
+
import { resolveMobileReachableServerUrl } from '../server/mobile_api_url.mjs';
|
|
18
|
+
import { getTailscaleStatus } from '../tailscale/ip.mjs';
|
|
19
|
+
import { pickLanIpv4 } from '../net/lan_ip.mjs';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
|
|
24
|
+
function normalizeExpoHost(raw) {
|
|
25
|
+
const v = String(raw ?? '').trim().toLowerCase();
|
|
26
|
+
if (v === 'localhost' || v === 'lan' || v === 'tunnel') return v;
|
|
27
|
+
return 'lan';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve whether Tailscale forwarding for Expo is enabled.
|
|
32
|
+
*
|
|
33
|
+
* Can be enabled via:
|
|
34
|
+
* - --expo-tailscale flag (passed as expoTailscale option)
|
|
35
|
+
* - HAPPIER_STACK_EXPO_TAILSCALE=1 env var
|
|
36
|
+
*/
|
|
37
|
+
export function resolveExpoTailscaleEnabled({ env = process.env, expoTailscale = false } = {}) {
|
|
38
|
+
if (expoTailscale) return true;
|
|
39
|
+
const envVal = (env.HAPPIER_STACK_EXPO_TAILSCALE ?? '').toString().trim();
|
|
40
|
+
return envVal === '1' || envVal.toLowerCase() === 'true';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start a TCP forwarder process for Expo Tailscale access.
|
|
45
|
+
*
|
|
46
|
+
* Forwards from Tailscale IP:port to the LAN IP:port where Expo actually binds.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} options
|
|
49
|
+
* @param {number} options.metroPort - The Metro bundler port
|
|
50
|
+
* @param {Object} options.baseEnv - Base environment variables
|
|
51
|
+
* @param {string} options.stackName - Stack name for logging
|
|
52
|
+
* @param {Array} options.children - Array to track child processes
|
|
53
|
+
* @returns {Promise<{ ok: boolean, pid?: number, tailscaleIp?: string, lanIp?: string, error?: string }>}
|
|
54
|
+
*/
|
|
55
|
+
export async function startExpoTailscaleForwarder({ metroPort, baseEnv, stackName, children }) {
|
|
56
|
+
const ts = await getTailscaleStatus();
|
|
57
|
+
if (!ts.available || !ts.ip) {
|
|
58
|
+
// Common case: Tailscale app installed but toggle is off / not connected.
|
|
59
|
+
// This must never fail stack startup; just skip with a clear message.
|
|
60
|
+
return { ok: false, error: ts.error || 'Tailscale is not connected' };
|
|
61
|
+
}
|
|
62
|
+
const tailscaleIp = ts.ip;
|
|
63
|
+
|
|
64
|
+
// Some platforms / Tailscale variants report an IP but do not allow binding to it (EADDRNOTAVAIL).
|
|
65
|
+
// If we can't bind *at all*, don't spawn the forwarder process (it will just error noisily).
|
|
66
|
+
const canBind = await new Promise((resolve) => {
|
|
67
|
+
const srv = net.createServer();
|
|
68
|
+
const done = (ok, err) => {
|
|
69
|
+
try {
|
|
70
|
+
srv.close(() => resolve({ ok, err }));
|
|
71
|
+
} catch {
|
|
72
|
+
resolve({ ok, err });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
srv.once('error', (err) => done(false, err));
|
|
76
|
+
srv.listen(0, tailscaleIp, () => done(true, null));
|
|
77
|
+
});
|
|
78
|
+
if (!canBind.ok) {
|
|
79
|
+
const code = canBind.err && typeof canBind.err === 'object' ? canBind.err.code : '';
|
|
80
|
+
const msg = canBind.err instanceof Error ? canBind.err.message : String(canBind.err ?? '');
|
|
81
|
+
const hint =
|
|
82
|
+
code === 'EADDRNOTAVAIL'
|
|
83
|
+
? `Tailscale IP ${tailscaleIp} is not bindable on this machine (EADDRNOTAVAIL).`
|
|
84
|
+
: `Tailscale IP ${tailscaleIp} is not bindable (${code || 'error'}).`;
|
|
85
|
+
return { ok: false, error: `${hint}${msg ? ` ${msg}` : ''}`.trim() };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Determine where Expo binds (LAN IP when host=lan, localhost otherwise)
|
|
89
|
+
const host = resolveExpoDevHost({ env: baseEnv });
|
|
90
|
+
let targetHost = '127.0.0.1';
|
|
91
|
+
if (host === 'lan') {
|
|
92
|
+
const lanIp = pickLanIpv4();
|
|
93
|
+
if (lanIp) targetHost = lanIp;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const label = `expo-ts-fwd${stackName ? `-${stackName}` : ''}`;
|
|
97
|
+
const forwarderScript = join(__dirname, '..', 'net', 'tcp_forward.mjs');
|
|
98
|
+
|
|
99
|
+
// Fork the forwarder as a child process
|
|
100
|
+
// Note: fork() requires 'ipc' in stdio array
|
|
101
|
+
const forwarderProc = fork(forwarderScript, [
|
|
102
|
+
`--listen-host=${tailscaleIp}`,
|
|
103
|
+
`--listen-port=${metroPort}`,
|
|
104
|
+
`--target-host=${targetHost}`,
|
|
105
|
+
`--target-port=${metroPort}`,
|
|
106
|
+
`--label=${label}`,
|
|
107
|
+
], {
|
|
108
|
+
env: { ...baseEnv },
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
110
|
+
detached: process.platform !== 'win32',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Prefix forwarder output
|
|
114
|
+
const outPrefix = `[${label}] `;
|
|
115
|
+
forwarderProc.stdout?.on('data', (d) => process.stdout.write(outPrefix + d.toString()));
|
|
116
|
+
forwarderProc.stderr?.on('data', (d) => process.stderr.write(outPrefix + d.toString()));
|
|
117
|
+
|
|
118
|
+
// Wait until the forwarder actually starts listening (or fails) before declaring success.
|
|
119
|
+
const ready = await new Promise((resolve) => {
|
|
120
|
+
const t = setTimeout(() => resolve({ ok: false, error: 'forwarder startup timed out' }), 2000);
|
|
121
|
+
const done = (res) => {
|
|
122
|
+
clearTimeout(t);
|
|
123
|
+
resolve(res);
|
|
124
|
+
};
|
|
125
|
+
forwarderProc.once('message', (m) => {
|
|
126
|
+
if (m && typeof m === 'object' && m.type === 'ready') {
|
|
127
|
+
done({ ok: true });
|
|
128
|
+
} else if (m && typeof m === 'object' && m.type === 'error') {
|
|
129
|
+
done({ ok: false, error: m.message ? String(m.message) : 'failed to start' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
forwarderProc.once('exit', (code, sig) => {
|
|
133
|
+
done({ ok: false, error: `exited (code=${code}, sig=${sig})` });
|
|
134
|
+
});
|
|
135
|
+
forwarderProc.once('error', (e) => {
|
|
136
|
+
done({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!ready.ok) {
|
|
141
|
+
try {
|
|
142
|
+
forwarderProc.kill('SIGKILL');
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
return { ok: false, error: ready.error || 'failed to start forwarder' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
children.push(forwarderProc);
|
|
150
|
+
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.log(`[local] expo: Tailscale forwarder started (${tailscaleIp}:${metroPort} -> ${targetHost}:${metroPort})`);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
ok: true,
|
|
156
|
+
pid: forwarderProc.pid,
|
|
157
|
+
tailscaleIp,
|
|
158
|
+
lanIp: targetHost,
|
|
159
|
+
proc: forwarderProc,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveExpoDevHost({ env = process.env } = {}) {
|
|
164
|
+
// Always prefer LAN by default so phones can reach Metro.
|
|
165
|
+
const raw = (env.HAPPIER_STACK_EXPO_HOST ?? '').toString();
|
|
166
|
+
return normalizeExpoHost(raw || 'lan');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildExpoStartArgs({ port, host, wantWeb, wantDevClient, scheme, clearCache }) {
|
|
170
|
+
const metroPort = Number(port);
|
|
171
|
+
if (!Number.isFinite(metroPort) || metroPort <= 0) {
|
|
172
|
+
throw new Error(`[expo] invalid Metro port: ${String(port)}`);
|
|
173
|
+
}
|
|
174
|
+
if (!wantWeb && !wantDevClient) {
|
|
175
|
+
throw new Error('[expo] cannot build Expo args: neither web nor dev-client requested');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// IMPORTANT:
|
|
179
|
+
// - We must only run one Expo per stack.
|
|
180
|
+
// - Expo dev-client mode is known to still serve web when accessed locally, so when mobile is
|
|
181
|
+
// requested we prefer `--dev-client` as the single shared process (no second `--web` process).
|
|
182
|
+
const args = wantDevClient
|
|
183
|
+
? ['start', '--dev-client', '--host', host, '--port', String(metroPort)]
|
|
184
|
+
: ['start', '--web', '--host', host, '--port', String(metroPort)];
|
|
185
|
+
|
|
186
|
+
if (wantDevClient) {
|
|
187
|
+
const s = String(scheme ?? '').trim();
|
|
188
|
+
if (s) {
|
|
189
|
+
args.push('--scheme', s);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (clearCache && !args.includes('--clear')) {
|
|
194
|
+
args.push('--clear');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return args;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function expoModeLabel({ wantWeb, wantDevClient }) {
|
|
201
|
+
if (wantWeb && wantDevClient) return 'dev-client+web';
|
|
202
|
+
if (wantDevClient) return 'dev-client';
|
|
203
|
+
if (wantWeb) return 'web';
|
|
204
|
+
return 'disabled';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function buildExpoDevEnv({
|
|
208
|
+
baseEnv,
|
|
209
|
+
apiServerUrl,
|
|
210
|
+
wantDevClient,
|
|
211
|
+
wantWeb,
|
|
212
|
+
stackMode,
|
|
213
|
+
stackName,
|
|
214
|
+
} = {}) {
|
|
215
|
+
const env = { ...(baseEnv || process.env) };
|
|
216
|
+
delete env.CI;
|
|
217
|
+
|
|
218
|
+
// Expo app config: this is what both web + native app use to reach the Happy server.
|
|
219
|
+
// When dev-client is enabled, `localhost` / `*.localhost` are not reachable from the phone,
|
|
220
|
+
// so rewrite to LAN IP here (centralized) to avoid relying on call sites.
|
|
221
|
+
const serverPortFromEnvRaw = (env.HAPPIER_STACK_SERVER_PORT ?? '').toString().trim();
|
|
222
|
+
const serverPortFromEnv = serverPortFromEnvRaw ? Number(serverPortFromEnvRaw) : null;
|
|
223
|
+
const effectiveApiServerUrl = wantDevClient
|
|
224
|
+
? resolveMobileReachableServerUrl({
|
|
225
|
+
env,
|
|
226
|
+
serverUrl: apiServerUrl,
|
|
227
|
+
serverPort: Number.isFinite(serverPortFromEnv) ? serverPortFromEnv : null,
|
|
228
|
+
})
|
|
229
|
+
: apiServerUrl;
|
|
230
|
+
|
|
231
|
+
env.EXPO_PUBLIC_HAPPY_SERVER_URL = effectiveApiServerUrl;
|
|
232
|
+
if (stackMode && !env.EXPO_PUBLIC_HAPPY_SERVER_CONTEXT) {
|
|
233
|
+
env.EXPO_PUBLIC_HAPPY_SERVER_CONTEXT = 'stack';
|
|
234
|
+
}
|
|
235
|
+
env.EXPO_PUBLIC_DEBUG = env.EXPO_PUBLIC_DEBUG ?? '1';
|
|
236
|
+
|
|
237
|
+
// Optional: allow per-stack storage isolation inside a single dev-client build by
|
|
238
|
+
// scoping app persistence (MMKV / SecureStore) to a stack-specific namespace.
|
|
239
|
+
//
|
|
240
|
+
// This stays upstream-safe because the app behavior is unchanged unless the Expo public
|
|
241
|
+
// env var is explicitly set. hstack sets it automatically for stack-mode dev-client.
|
|
242
|
+
if (wantDevClient) {
|
|
243
|
+
const explicitScope = (
|
|
244
|
+
env.HAPPIER_STACK_STORAGE_SCOPE ??
|
|
245
|
+
env.EXPO_PUBLIC_HAPPY_STORAGE_SCOPE ??
|
|
246
|
+
''
|
|
247
|
+
)
|
|
248
|
+
.toString()
|
|
249
|
+
.trim();
|
|
250
|
+
const defaultScope = stackMode && stackName ? String(stackName).trim() : '';
|
|
251
|
+
const scope = explicitScope || defaultScope;
|
|
252
|
+
if (scope && !env.EXPO_PUBLIC_HAPPY_STORAGE_SCOPE) {
|
|
253
|
+
env.EXPO_PUBLIC_HAPPY_STORAGE_SCOPE = scope;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// We own the browser opening behavior in hstack so we can reliably open the correct origin.
|
|
258
|
+
env.EXPO_NO_BROWSER = '1';
|
|
259
|
+
env.BROWSER = 'none';
|
|
260
|
+
|
|
261
|
+
return env;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function ensureDevExpoServer({
|
|
265
|
+
startUi,
|
|
266
|
+
startMobile,
|
|
267
|
+
uiDir,
|
|
268
|
+
expoProjectDir = '',
|
|
269
|
+
autostart,
|
|
270
|
+
baseEnv,
|
|
271
|
+
apiServerUrl,
|
|
272
|
+
restart,
|
|
273
|
+
stackMode,
|
|
274
|
+
runtimeStatePath,
|
|
275
|
+
stackName,
|
|
276
|
+
envPath,
|
|
277
|
+
children,
|
|
278
|
+
spawnOptions = {},
|
|
279
|
+
expoTailscale = false,
|
|
280
|
+
quiet = false,
|
|
281
|
+
} = {}) {
|
|
282
|
+
const wantWeb = Boolean(startUi);
|
|
283
|
+
const wantDevClient = Boolean(startMobile);
|
|
284
|
+
if (!wantWeb && !wantDevClient) {
|
|
285
|
+
return { ok: true, skipped: true, reason: 'disabled' };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const env = buildExpoDevEnv({
|
|
289
|
+
baseEnv,
|
|
290
|
+
apiServerUrl,
|
|
291
|
+
wantDevClient,
|
|
292
|
+
wantWeb,
|
|
293
|
+
stackMode,
|
|
294
|
+
stackName,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Mobile config is needed for `--scheme` and for the app's environment.
|
|
298
|
+
let scheme = '';
|
|
299
|
+
if (wantDevClient) {
|
|
300
|
+
const cfg = resolveMobileExpoConfig({ env });
|
|
301
|
+
env.APP_ENV = cfg.appEnv;
|
|
302
|
+
scheme = cfg.scheme;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const projectDir = String(expoProjectDir ?? '').trim() || uiDir;
|
|
306
|
+
|
|
307
|
+
const paths = getExpoStatePaths({
|
|
308
|
+
baseDir: autostart.baseDir,
|
|
309
|
+
kind: 'expo-dev',
|
|
310
|
+
projectDir,
|
|
311
|
+
stateFileName: 'expo.state.json',
|
|
312
|
+
});
|
|
313
|
+
await ensureExpoIsolationEnv({ env, stateDir: paths.stateDir, expoHomeDir: paths.expoHomeDir, tmpDir: paths.tmpDir });
|
|
314
|
+
|
|
315
|
+
const running = await isStateProcessRunning(paths.statePath);
|
|
316
|
+
const alreadyRunning = Boolean(running.running);
|
|
317
|
+
|
|
318
|
+
// Resolve Tailscale forwarding preference
|
|
319
|
+
const wantTailscale = resolveExpoTailscaleEnabled({ env: baseEnv, expoTailscale });
|
|
320
|
+
|
|
321
|
+
// Always publish runtime metadata when we can.
|
|
322
|
+
const publishRuntime = async ({ pid, port, tailscaleForwarderPid = null, tailscaleIp = null }) => {
|
|
323
|
+
if (!stackMode || !runtimeStatePath) return;
|
|
324
|
+
const nPid = Number(pid);
|
|
325
|
+
const nPort = Number(port);
|
|
326
|
+
const nTsPid = Number(tailscaleForwarderPid);
|
|
327
|
+
await recordStackRuntimeUpdate(runtimeStatePath, {
|
|
328
|
+
processes: {
|
|
329
|
+
expoPid: Number.isFinite(nPid) && nPid > 1 ? nPid : null,
|
|
330
|
+
expoTailscaleForwarderPid: Number.isFinite(nTsPid) && nTsPid > 1 ? nTsPid : null,
|
|
331
|
+
},
|
|
332
|
+
expo: {
|
|
333
|
+
port: Number.isFinite(nPort) && nPort > 0 ? nPort : null,
|
|
334
|
+
// For now keep these populated for callers that still expect webPort/mobilePort.
|
|
335
|
+
webPort: wantWeb && Number.isFinite(nPort) && nPort > 0 ? nPort : null,
|
|
336
|
+
mobilePort: wantDevClient && Number.isFinite(nPort) && nPort > 0 ? nPort : null,
|
|
337
|
+
webEnabled: wantWeb,
|
|
338
|
+
devClientEnabled: wantDevClient,
|
|
339
|
+
host: resolveExpoDevHost({ env }),
|
|
340
|
+
scheme: wantDevClient ? scheme : null,
|
|
341
|
+
tailscaleEnabled: wantTailscale,
|
|
342
|
+
tailscaleIp: tailscaleIp ?? null,
|
|
343
|
+
},
|
|
344
|
+
}).catch(() => {});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (alreadyRunning && !restart) {
|
|
348
|
+
const pid = Number(running.state?.pid);
|
|
349
|
+
const port = Number(running.state?.port);
|
|
350
|
+
|
|
351
|
+
// Capability check: refuse to spawn a second Expo, so if the existing process doesn't match the
|
|
352
|
+
// requested capabilities we fail closed and instruct a restart with the superset.
|
|
353
|
+
const stateWeb = Boolean(running.state?.webEnabled);
|
|
354
|
+
const stateDevClient = Boolean(running.state?.devClientEnabled);
|
|
355
|
+
const stateHasCaps = 'webEnabled' in (running.state ?? {}) || 'devClientEnabled' in (running.state ?? {});
|
|
356
|
+
const missingWeb = wantWeb && stateHasCaps && !stateWeb;
|
|
357
|
+
const missingDevClient = wantDevClient && stateHasCaps && !stateDevClient;
|
|
358
|
+
if (missingWeb || missingDevClient) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`[expo] Expo already running for stack=${stackName}, but it does not match the requested mode.\n` +
|
|
361
|
+
`- running: ${expoModeLabel({ wantWeb: stateWeb, wantDevClient: stateDevClient })}\n` +
|
|
362
|
+
`- wanted: ${expoModeLabel({ wantWeb, wantDevClient })}\n` +
|
|
363
|
+
`Fix: re-run with --restart (and include --mobile if you need dev-client).`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await publishRuntime({ pid, port });
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
skipped: true,
|
|
371
|
+
reason: 'already_running',
|
|
372
|
+
pid: Number.isFinite(pid) ? pid : null,
|
|
373
|
+
port: Number.isFinite(port) ? port : null,
|
|
374
|
+
mode: expoModeLabel({ wantWeb, wantDevClient }),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const reservedMetroPorts = new Set();
|
|
379
|
+
|
|
380
|
+
if (restart && running.state?.pid) {
|
|
381
|
+
const prevPid = Number(running.state.pid);
|
|
382
|
+
const prevPort = Number(running.state?.port);
|
|
383
|
+
const res = await killProcessGroupOwnedByStack(prevPid, { stackName, envPath, label: 'expo', json: true });
|
|
384
|
+
if (!res.killed) {
|
|
385
|
+
// eslint-disable-next-line no-console
|
|
386
|
+
console.warn(
|
|
387
|
+
`[local] expo: not stopping existing Expo pid=${prevPid} because it does not look stack-owned.\n` +
|
|
388
|
+
`[local] expo: continuing by starting a new Expo process on a free port.`
|
|
389
|
+
);
|
|
390
|
+
if (Number.isFinite(prevPort) && prevPort > 0) {
|
|
391
|
+
reservedMetroPorts.add(prevPort);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const metroPort = await pickExpoDevMetroPort({
|
|
397
|
+
env: baseEnv,
|
|
398
|
+
stackMode,
|
|
399
|
+
stackName,
|
|
400
|
+
reservedPorts: reservedMetroPorts,
|
|
401
|
+
});
|
|
402
|
+
env.RCT_METRO_PORT = String(metroPort);
|
|
403
|
+
const host = resolveExpoDevHost({ env });
|
|
404
|
+
const args = buildExpoStartArgs({
|
|
405
|
+
port: metroPort,
|
|
406
|
+
host,
|
|
407
|
+
wantWeb,
|
|
408
|
+
wantDevClient,
|
|
409
|
+
scheme,
|
|
410
|
+
clearCache: wantsExpoClearCache({ env: baseEnv || process.env }),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (!quiet) {
|
|
414
|
+
// eslint-disable-next-line no-console
|
|
415
|
+
console.log(`[local] expo: starting Expo (${expoModeLabel({ wantWeb, wantDevClient })}, metro port=${metroPort}, host=${host})`);
|
|
416
|
+
}
|
|
417
|
+
// Some auth flows historically passed `stdio: ['ignore','ignore','ignore']` which drops Expo output entirely.
|
|
418
|
+
// For reliability, treat that as "use default pipes" so errors remain debuggable (and verbose can stream).
|
|
419
|
+
const normalizedSpawnOptions = { ...(spawnOptions ?? {}) };
|
|
420
|
+
const stdio = normalizedSpawnOptions.stdio;
|
|
421
|
+
if (Array.isArray(stdio) && stdio[1] === 'ignore' && stdio[2] === 'ignore') {
|
|
422
|
+
delete normalizedSpawnOptions.stdio;
|
|
423
|
+
}
|
|
424
|
+
// Run the Expo CLI from the runner dir (where deps/bins live), but target the actual Expo project dir.
|
|
425
|
+
const proc = await expoSpawn({ label: 'expo', dir: uiDir, projectDir, args, env, options: normalizedSpawnOptions, quiet });
|
|
426
|
+
children.push(proc);
|
|
427
|
+
|
|
428
|
+
// Start Tailscale forwarder if enabled
|
|
429
|
+
let tailscaleResult = null;
|
|
430
|
+
if (wantTailscale) {
|
|
431
|
+
tailscaleResult = await startExpoTailscaleForwarder({
|
|
432
|
+
metroPort,
|
|
433
|
+
baseEnv,
|
|
434
|
+
stackName,
|
|
435
|
+
children,
|
|
436
|
+
});
|
|
437
|
+
if (!tailscaleResult.ok && !quiet) {
|
|
438
|
+
// eslint-disable-next-line no-console
|
|
439
|
+
console.warn(`[local] expo: Tailscale forwarder not started: ${tailscaleResult.error}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await publishRuntime({
|
|
444
|
+
pid: proc.pid,
|
|
445
|
+
port: metroPort,
|
|
446
|
+
tailscaleForwarderPid: tailscaleResult?.pid ?? null,
|
|
447
|
+
tailscaleIp: tailscaleResult?.tailscaleIp ?? null,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await writePidState(paths.statePath, {
|
|
452
|
+
pid: proc.pid,
|
|
453
|
+
port: metroPort,
|
|
454
|
+
uiDir,
|
|
455
|
+
projectDir,
|
|
456
|
+
startedAt: new Date().toISOString(),
|
|
457
|
+
webEnabled: wantWeb,
|
|
458
|
+
devClientEnabled: wantDevClient,
|
|
459
|
+
host,
|
|
460
|
+
scheme: wantDevClient ? scheme : null,
|
|
461
|
+
tailscaleEnabled: wantTailscale,
|
|
462
|
+
tailscaleForwarderPid: tailscaleResult?.pid ?? null,
|
|
463
|
+
tailscaleIp: tailscaleResult?.tailscaleIp ?? null,
|
|
464
|
+
});
|
|
465
|
+
} catch {
|
|
466
|
+
// ignore
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
skipped: false,
|
|
472
|
+
pid: proc.pid,
|
|
473
|
+
port: metroPort,
|
|
474
|
+
proc,
|
|
475
|
+
mode: expoModeLabel({ wantWeb, wantDevClient }),
|
|
476
|
+
tailscale: tailscaleResult ?? null,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { buildExpoStartArgs, resolveExpoDevHost } from './expo_dev.mjs';
|
|
5
|
+
|
|
6
|
+
test('resolveExpoDevHost defaults to lan and normalizes values', () => {
|
|
7
|
+
assert.equal(resolveExpoDevHost({ env: {} }), 'lan');
|
|
8
|
+
assert.equal(resolveExpoDevHost({ env: { HAPPIER_STACK_EXPO_HOST: 'LAN' } }), 'lan');
|
|
9
|
+
assert.equal(resolveExpoDevHost({ env: { HAPPIER_STACK_EXPO_HOST: ' TuNnEl ' } }), 'tunnel');
|
|
10
|
+
assert.equal(resolveExpoDevHost({ env: { HAPPIER_STACK_EXPO_HOST: 'localhost' } }), 'localhost');
|
|
11
|
+
assert.equal(resolveExpoDevHost({ env: { HAPPIER_STACK_EXPO_HOST: 'tunnel' } }), 'tunnel');
|
|
12
|
+
assert.equal(resolveExpoDevHost({ env: { HAPPIER_STACK_EXPO_HOST: 'nope' } }), 'lan');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('buildExpoStartArgs builds dev-client args (preferred when mobile enabled)', () => {
|
|
16
|
+
const args = buildExpoStartArgs({
|
|
17
|
+
port: 8081,
|
|
18
|
+
host: 'lan',
|
|
19
|
+
wantWeb: true,
|
|
20
|
+
wantDevClient: true,
|
|
21
|
+
scheme: 'happy',
|
|
22
|
+
clearCache: true,
|
|
23
|
+
});
|
|
24
|
+
assert.deepEqual(args, ['start', '--dev-client', '--host', 'lan', '--port', '8081', '--scheme', 'happy', '--clear']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('buildExpoStartArgs builds web args when dev-client is not requested', () => {
|
|
28
|
+
const args = buildExpoStartArgs({
|
|
29
|
+
port: 8081,
|
|
30
|
+
host: 'lan',
|
|
31
|
+
wantWeb: true,
|
|
32
|
+
wantDevClient: false,
|
|
33
|
+
scheme: '',
|
|
34
|
+
clearCache: false,
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(args, ['start', '--web', '--host', 'lan', '--port', '8081']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('buildExpoStartArgs omits --scheme when empty', () => {
|
|
40
|
+
const args = buildExpoStartArgs({
|
|
41
|
+
port: 8081,
|
|
42
|
+
host: 'lan',
|
|
43
|
+
wantWeb: false,
|
|
44
|
+
wantDevClient: true,
|
|
45
|
+
scheme: '',
|
|
46
|
+
clearCache: false,
|
|
47
|
+
});
|
|
48
|
+
assert.deepEqual(args, ['start', '--dev-client', '--host', 'lan', '--port', '8081']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('buildExpoStartArgs accepts numeric port strings and does not add --clear when disabled', () => {
|
|
52
|
+
const args = buildExpoStartArgs({
|
|
53
|
+
port: '8082',
|
|
54
|
+
host: 'localhost',
|
|
55
|
+
wantWeb: true,
|
|
56
|
+
wantDevClient: false,
|
|
57
|
+
scheme: '',
|
|
58
|
+
clearCache: false,
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(args, ['start', '--web', '--host', 'localhost', '--port', '8082']);
|
|
61
|
+
assert.equal(args.includes('--clear'), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('buildExpoStartArgs throws on invalid requests', () => {
|
|
65
|
+
assert.throws(
|
|
66
|
+
() =>
|
|
67
|
+
buildExpoStartArgs({
|
|
68
|
+
port: 0,
|
|
69
|
+
host: 'lan',
|
|
70
|
+
wantWeb: true,
|
|
71
|
+
wantDevClient: false,
|
|
72
|
+
scheme: '',
|
|
73
|
+
clearCache: false,
|
|
74
|
+
}),
|
|
75
|
+
/invalid Metro port/i
|
|
76
|
+
);
|
|
77
|
+
assert.throws(
|
|
78
|
+
() =>
|
|
79
|
+
buildExpoStartArgs({
|
|
80
|
+
port: 8081,
|
|
81
|
+
host: 'lan',
|
|
82
|
+
wantWeb: false,
|
|
83
|
+
wantDevClient: false,
|
|
84
|
+
scheme: '',
|
|
85
|
+
clearCache: false,
|
|
86
|
+
}),
|
|
87
|
+
/neither web nor dev-client requested/i
|
|
88
|
+
);
|
|
89
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile, chmod } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
|
|
9
|
+
import { ensureDevExpoServer } from './expo_dev.mjs';
|
|
10
|
+
import { getExpoStatePaths, writePidState } from '../expo/expo.mjs';
|
|
11
|
+
|
|
12
|
+
function listenEphemeralPort() {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const srv = net.createServer();
|
|
15
|
+
srv.on('error', reject);
|
|
16
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
17
|
+
const addr = srv.address();
|
|
18
|
+
if (!addr || typeof addr === 'string') {
|
|
19
|
+
srv.close(() => reject(new Error('failed to resolve ephemeral port')));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const port = Number(addr.port);
|
|
23
|
+
srv.close((err) => {
|
|
24
|
+
if (err) reject(err);
|
|
25
|
+
else resolve(port);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function killProcessTreeByPid(pid) {
|
|
32
|
+
const n = Number(pid);
|
|
33
|
+
if (!Number.isFinite(n) || n <= 1) return;
|
|
34
|
+
try {
|
|
35
|
+
process.kill(-n, 'SIGKILL');
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
process.kill(n, 'SIGKILL');
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('ensureDevExpoServer reserves prior metro port when restart cannot kill previous pid', async () => {
|
|
46
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-expo-reserve-port-'));
|
|
47
|
+
const children = [];
|
|
48
|
+
let foreignPid = null;
|
|
49
|
+
try {
|
|
50
|
+
const uiDir = join(tmp, 'ui');
|
|
51
|
+
await mkdir(join(uiDir, 'node_modules', '.bin'), { recursive: true });
|
|
52
|
+
await mkdir(join(uiDir, 'node_modules'), { recursive: true });
|
|
53
|
+
await writeFile(join(uiDir, 'package.json'), JSON.stringify({ name: 'fake-ui', private: true }) + '\n', 'utf-8');
|
|
54
|
+
|
|
55
|
+
const expoBin = join(uiDir, 'node_modules', '.bin', 'expo');
|
|
56
|
+
await writeFile(
|
|
57
|
+
expoBin,
|
|
58
|
+
[
|
|
59
|
+
'#!/usr/bin/env node',
|
|
60
|
+
"setInterval(() => {}, 1000);",
|
|
61
|
+
].join('\n') + '\n',
|
|
62
|
+
'utf-8'
|
|
63
|
+
);
|
|
64
|
+
await chmod(expoBin, 0o755);
|
|
65
|
+
|
|
66
|
+
const foreign = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
|
|
67
|
+
detached: true,
|
|
68
|
+
stdio: 'ignore',
|
|
69
|
+
});
|
|
70
|
+
foreign.unref();
|
|
71
|
+
foreignPid = foreign.pid;
|
|
72
|
+
|
|
73
|
+
const priorPort = await listenEphemeralPort();
|
|
74
|
+
const projectDir = uiDir;
|
|
75
|
+
const paths = getExpoStatePaths({
|
|
76
|
+
baseDir: tmp,
|
|
77
|
+
kind: 'expo-dev',
|
|
78
|
+
projectDir,
|
|
79
|
+
stateFileName: 'expo.state.json',
|
|
80
|
+
});
|
|
81
|
+
await writePidState(paths.statePath, {
|
|
82
|
+
pid: foreignPid,
|
|
83
|
+
port: priorPort,
|
|
84
|
+
uiDir,
|
|
85
|
+
projectDir,
|
|
86
|
+
startedAt: new Date().toISOString(),
|
|
87
|
+
webEnabled: true,
|
|
88
|
+
devClientEnabled: false,
|
|
89
|
+
host: 'lan',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await ensureDevExpoServer({
|
|
93
|
+
startUi: true,
|
|
94
|
+
startMobile: false,
|
|
95
|
+
uiDir,
|
|
96
|
+
autostart: { baseDir: tmp },
|
|
97
|
+
baseEnv: {
|
|
98
|
+
...process.env,
|
|
99
|
+
HAPPIER_STACK_EXPO_DEV_PORT: String(priorPort),
|
|
100
|
+
},
|
|
101
|
+
apiServerUrl: 'http://127.0.0.1:1',
|
|
102
|
+
restart: true,
|
|
103
|
+
stackMode: true,
|
|
104
|
+
runtimeStatePath: null,
|
|
105
|
+
stackName: 'dev2',
|
|
106
|
+
envPath: join(tmp, 'stack.env'),
|
|
107
|
+
children,
|
|
108
|
+
quiet: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(result.ok, true);
|
|
112
|
+
assert.notEqual(result.port, priorPort);
|
|
113
|
+
} finally {
|
|
114
|
+
for (const child of children) {
|
|
115
|
+
killProcessTreeByPid(child?.pid);
|
|
116
|
+
}
|
|
117
|
+
killProcessTreeByPid(foreignPid);
|
|
118
|
+
await rm(tmp, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|