@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,79 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { ensureServerLightSchemaReady } from './startup.mjs';
|
|
7
|
+
import { buildServerLightEnv, createServerLightFixture } from './startup_server_light_testkit.mjs';
|
|
8
|
+
|
|
9
|
+
test('ensureServerLightSchemaReady bestEffort=true skips migrate:sqlite:deploy by default', async (t) => {
|
|
10
|
+
const { binDir, markerPath, root, serverDir } = await createServerLightFixture(t, {
|
|
11
|
+
prefix: 'hs-startup-light-best-effort-',
|
|
12
|
+
socketPort: 54323,
|
|
13
|
+
});
|
|
14
|
+
const env = buildServerLightEnv({ binDir, root });
|
|
15
|
+
|
|
16
|
+
const res = await ensureServerLightSchemaReady({ serverDir, env, bestEffort: true });
|
|
17
|
+
assert.equal(res.ok, true);
|
|
18
|
+
assert.equal(res.migrated, true);
|
|
19
|
+
assert.equal(res.accountCount, 0);
|
|
20
|
+
assert.equal(existsSync(markerPath), false, `expected bestEffort to skip migrate:sqlite:deploy (${markerPath})`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('ensureServerLightSchemaReady honors HAPPY_SERVER_LIGHT_DATA_DIR legacy fallback', async (t) => {
|
|
24
|
+
const { binDir, markerPath, root, serverDir } = await createServerLightFixture(t, {
|
|
25
|
+
prefix: 'hs-startup-light-legacy-data-dir-',
|
|
26
|
+
socketPort: 54324,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const dataDir = join(root, 'legacy-data');
|
|
30
|
+
const env = buildServerLightEnv({
|
|
31
|
+
binDir,
|
|
32
|
+
root,
|
|
33
|
+
extraEnv: {
|
|
34
|
+
HAPPIER_SERVER_LIGHT_DATA_DIR: undefined,
|
|
35
|
+
HAPPIER_SERVER_LIGHT_FILES_DIR: undefined,
|
|
36
|
+
HAPPIER_SERVER_LIGHT_DB_DIR: undefined,
|
|
37
|
+
HAPPY_SERVER_LIGHT_DATA_DIR: dataDir,
|
|
38
|
+
DATABASE_URL: undefined,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.equal(existsSync(dataDir), false);
|
|
43
|
+
assert.equal(Boolean(env.DATABASE_URL), false);
|
|
44
|
+
|
|
45
|
+
const res = await ensureServerLightSchemaReady({ serverDir, env });
|
|
46
|
+
assert.equal(res.ok, true);
|
|
47
|
+
assert.equal(existsSync(dataDir), true);
|
|
48
|
+
assert.equal(env.DATABASE_URL, `file:${join(dataDir, 'happier-server-light.sqlite')}`);
|
|
49
|
+
assert.equal(existsSync(markerPath), true, `expected migrate:sqlite:deploy to be invoked (${markerPath})`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('ensureServerLightSchemaReady falls back to HAPPY_SERVER_LIGHT_DATA_DIR when HAPPIER value is empty', async (t) => {
|
|
53
|
+
const { binDir, markerPath, root, serverDir } = await createServerLightFixture(t, {
|
|
54
|
+
prefix: 'hs-startup-light-empty-prefers-legacy-',
|
|
55
|
+
socketPort: 54325,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const dataDir = join(root, 'legacy-data-empty-fallback');
|
|
59
|
+
const env = buildServerLightEnv({
|
|
60
|
+
binDir,
|
|
61
|
+
root,
|
|
62
|
+
extraEnv: {
|
|
63
|
+
HAPPIER_SERVER_LIGHT_DATA_DIR: '',
|
|
64
|
+
HAPPIER_SERVER_LIGHT_FILES_DIR: '',
|
|
65
|
+
HAPPIER_SERVER_LIGHT_DB_DIR: '',
|
|
66
|
+
HAPPY_SERVER_LIGHT_DATA_DIR: dataDir,
|
|
67
|
+
DATABASE_URL: undefined,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(existsSync(dataDir), false);
|
|
72
|
+
assert.equal(Boolean(env.DATABASE_URL), false);
|
|
73
|
+
|
|
74
|
+
const res = await ensureServerLightSchemaReady({ serverDir, env });
|
|
75
|
+
assert.equal(res.ok, true);
|
|
76
|
+
assert.equal(existsSync(dataDir), true);
|
|
77
|
+
assert.equal(env.DATABASE_URL, `file:${join(dataDir, 'happier-server-light.sqlite')}`);
|
|
78
|
+
assert.equal(existsSync(markerPath), true, `expected migrate:sqlite:deploy to be invoked (${markerPath})`);
|
|
79
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { chmod, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
async function writeJson(path, obj) {
|
|
6
|
+
await writeFile(path, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function writeEsmPkg({ dir, name, body }) {
|
|
10
|
+
await mkdir(dir, { recursive: true });
|
|
11
|
+
await writeJson(join(dir, 'package.json'), { name, type: 'module', main: './index.js' });
|
|
12
|
+
await writeFile(join(dir, 'index.js'), body.trim() + '\n', 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function seedServerLightProbeDeps({ serverDir, socketPort }) {
|
|
16
|
+
await mkdir(join(serverDir, 'node_modules'), { recursive: true });
|
|
17
|
+
await writeFile(join(serverDir, 'node_modules', '.yarn-integrity'), 'ok\n', 'utf-8');
|
|
18
|
+
await writeEsmPkg({
|
|
19
|
+
dir: join(serverDir, 'node_modules', '@electric-sql', 'pglite'),
|
|
20
|
+
name: '@electric-sql/pglite',
|
|
21
|
+
body: `
|
|
22
|
+
export class PGlite {
|
|
23
|
+
constructor(_dir) { this.waitReady = Promise.resolve(); }
|
|
24
|
+
async close() {}
|
|
25
|
+
}
|
|
26
|
+
`,
|
|
27
|
+
});
|
|
28
|
+
await writeEsmPkg({
|
|
29
|
+
dir: join(serverDir, 'node_modules', '@electric-sql', 'pglite-socket'),
|
|
30
|
+
name: '@electric-sql/pglite-socket',
|
|
31
|
+
body: `
|
|
32
|
+
export class PGLiteSocketServer {
|
|
33
|
+
constructor(_opts) {}
|
|
34
|
+
async start() {}
|
|
35
|
+
getServerConn() { return '127.0.0.1:${String(socketPort)}'; }
|
|
36
|
+
async stop() {}
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
});
|
|
40
|
+
await writeEsmPkg({
|
|
41
|
+
dir: join(serverDir, 'node_modules', '@prisma', 'client'),
|
|
42
|
+
name: '@prisma/client',
|
|
43
|
+
body: `
|
|
44
|
+
export class PrismaClient {
|
|
45
|
+
constructor() { this.account = { count: async () => 0 }; }
|
|
46
|
+
async $disconnect() {}
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function writeYarnShim({ root, markerPath }) {
|
|
53
|
+
const binDir = join(root, 'bin');
|
|
54
|
+
await mkdir(binDir, { recursive: true });
|
|
55
|
+
const yarnPath = join(binDir, 'yarn');
|
|
56
|
+
await writeFile(
|
|
57
|
+
yarnPath,
|
|
58
|
+
[
|
|
59
|
+
'#!/usr/bin/env node',
|
|
60
|
+
"const fs = require('node:fs');",
|
|
61
|
+
'const args = process.argv.slice(2);',
|
|
62
|
+
"if (args.includes('--version')) { console.log('1.22.22'); process.exit(0); }",
|
|
63
|
+
`if (args[0] === '-s' && args[1] === 'migrate:sqlite:deploy') { fs.writeFileSync(${JSON.stringify(markerPath)}, 'ok\\n', 'utf-8'); process.exit(0); }`,
|
|
64
|
+
'process.exit(0);',
|
|
65
|
+
].join('\n') + '\n',
|
|
66
|
+
'utf-8'
|
|
67
|
+
);
|
|
68
|
+
await chmod(yarnPath, 0o755);
|
|
69
|
+
return { binDir, yarnPath };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function createServerLightFixture(t, { prefix, socketPort }) {
|
|
73
|
+
const root = await mkdtemp(join(tmpdir(), prefix));
|
|
74
|
+
t.after(async () => {
|
|
75
|
+
await rm(root, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const serverDir = join(root, 'server');
|
|
79
|
+
await mkdir(serverDir, { recursive: true });
|
|
80
|
+
await writeJson(join(serverDir, 'package.json'), { name: 'server', version: '0.0.0', type: 'module' });
|
|
81
|
+
await writeFile(join(serverDir, 'yarn.lock'), '# yarn\n', 'utf-8');
|
|
82
|
+
await seedServerLightProbeDeps({ serverDir, socketPort });
|
|
83
|
+
|
|
84
|
+
const markerPath = join(root, 'called-migrate-sqlite-deploy.txt');
|
|
85
|
+
const yarnShim = await writeYarnShim({ root, markerPath });
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
binDir: yarnShim.binDir,
|
|
89
|
+
markerPath,
|
|
90
|
+
root,
|
|
91
|
+
serverDir,
|
|
92
|
+
yarnPath: yarnShim.yarnPath,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildServerLightEnv({ binDir, root, extraEnv = {} }) {
|
|
97
|
+
const dataDir = join(root, 'data');
|
|
98
|
+
return {
|
|
99
|
+
...process.env,
|
|
100
|
+
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
101
|
+
HAPPIER_SERVER_LIGHT_DATA_DIR: dataDir,
|
|
102
|
+
HAPPIER_SERVER_LIGHT_FILES_DIR: join(dataDir, 'files'),
|
|
103
|
+
HAPPIER_SERVER_LIGHT_DB_DIR: join(dataDir, 'pglite'),
|
|
104
|
+
...extraEnv,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getComponentDir } from '../paths/paths.mjs';
|
|
6
|
+
import { isPidAlive, readPidState } from '../expo/expo.mjs';
|
|
7
|
+
import { stopLocalDaemon } from '../../daemon.mjs';
|
|
8
|
+
import { stopHappyServerManagedInfra } from '../server/infra/happy_server_infra.mjs';
|
|
9
|
+
import { deleteStackRuntimeStateFile, getStackRuntimeStatePath, readStackRuntimeStateFile } from './runtime_state.mjs';
|
|
10
|
+
import { killPidOwnedByStack, killProcessGroupOwnedByStack, listPidsWithEnvNeedles } from '../proc/ownership.mjs';
|
|
11
|
+
import { coercePort } from '../server/port.mjs';
|
|
12
|
+
import { resolvePreferredStackDaemonStatePaths } from '../auth/credentials_paths.mjs';
|
|
13
|
+
|
|
14
|
+
function resolveServerComponentFromStackEnv(env) {
|
|
15
|
+
const v =
|
|
16
|
+
(env.HAPPIER_STACK_SERVER_COMPONENT ?? '').toString().trim() || 'happier-server-light';
|
|
17
|
+
return v === 'happier-server' ? 'happier-server' : 'happier-server-light';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function daemonControlPost({ httpPort, path, body = {} }) {
|
|
21
|
+
const ctl = new AbortController();
|
|
22
|
+
const t = setTimeout(() => ctl.abort(), 1500);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`http://127.0.0.1:${httpPort}${path}`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/json' },
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: ctl.signal,
|
|
29
|
+
});
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`daemon control ${path} failed (http ${res.status}): ${text.trim()}`);
|
|
33
|
+
}
|
|
34
|
+
return text.trim() ? JSON.parse(text) : null;
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(t);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function stopDaemonTrackedSessions({ cliHomeDir, serverUrl, json }) {
|
|
41
|
+
// Read daemon state file written by happier-cli; needed to call control server (/list, /stop-session).
|
|
42
|
+
const { statePath } = resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl });
|
|
43
|
+
if (!existsSync(statePath)) {
|
|
44
|
+
return { ok: true, skipped: true, reason: 'missing_state', stoppedSessionIds: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let state = null;
|
|
48
|
+
try {
|
|
49
|
+
state = JSON.parse(await readFile(statePath, 'utf-8'));
|
|
50
|
+
} catch {
|
|
51
|
+
return { ok: false, skipped: true, reason: 'bad_state', stoppedSessionIds: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const httpPort = Number(state?.httpPort);
|
|
55
|
+
const pid = Number(state?.pid);
|
|
56
|
+
if (!Number.isFinite(httpPort) || httpPort <= 0) {
|
|
57
|
+
return { ok: false, skipped: true, reason: 'missing_http_port', stoppedSessionIds: [] };
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isFinite(pid) || pid <= 1) {
|
|
60
|
+
return { ok: false, skipped: true, reason: 'missing_pid', stoppedSessionIds: [] };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
} catch {
|
|
65
|
+
return { ok: true, skipped: true, reason: 'daemon_not_running', stoppedSessionIds: [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const listed = await daemonControlPost({ httpPort, path: '/list' }).catch((e) => {
|
|
69
|
+
if (!json) console.warn(`[stack] failed to list daemon sessions: ${e instanceof Error ? e.message : String(e)}`);
|
|
70
|
+
return null;
|
|
71
|
+
});
|
|
72
|
+
const children = Array.isArray(listed?.children) ? listed.children : [];
|
|
73
|
+
|
|
74
|
+
const stoppedSessionIds = [];
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
const sid = String(child?.happySessionId ?? '').trim();
|
|
77
|
+
if (!sid) continue;
|
|
78
|
+
// eslint-disable-next-line no-await-in-loop
|
|
79
|
+
const res = await daemonControlPost({ httpPort, path: '/stop-session', body: { sessionId: sid } }).catch(() => null);
|
|
80
|
+
if (res?.success) {
|
|
81
|
+
stoppedSessionIds.push(sid);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ok: true, skipped: false, stoppedSessionIds };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function stopExpoStateDir({ stackName, baseDir, kind, stateFileName, envPath, json }) {
|
|
89
|
+
const root = join(baseDir, kind);
|
|
90
|
+
let entries = [];
|
|
91
|
+
try {
|
|
92
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
93
|
+
} catch {
|
|
94
|
+
entries = [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const killed = [];
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
if (!e.isDirectory()) continue;
|
|
100
|
+
const statePath = join(root, e.name, stateFileName);
|
|
101
|
+
// eslint-disable-next-line no-await-in-loop
|
|
102
|
+
const state = await readPidState(statePath);
|
|
103
|
+
if (!state) continue;
|
|
104
|
+
const pid = Number(state.pid);
|
|
105
|
+
|
|
106
|
+
if (!Number.isFinite(pid) || pid <= 1) continue;
|
|
107
|
+
if (!isPidAlive(pid)) continue;
|
|
108
|
+
|
|
109
|
+
if (!json) {
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.log(`[stack] stopping ${kind} (pid=${pid}) for ${stackName}`);
|
|
112
|
+
}
|
|
113
|
+
// eslint-disable-next-line no-await-in-loop
|
|
114
|
+
await killProcessGroupOwnedByStack(pid, { stackName, envPath, label: kind, json });
|
|
115
|
+
killed.push({ pid, port: null, statePath });
|
|
116
|
+
}
|
|
117
|
+
return killed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function stopStackWithEnv({
|
|
121
|
+
rootDir,
|
|
122
|
+
stackName,
|
|
123
|
+
baseDir,
|
|
124
|
+
env,
|
|
125
|
+
json,
|
|
126
|
+
noDocker = false,
|
|
127
|
+
aggressive = false,
|
|
128
|
+
sweepOwned = false,
|
|
129
|
+
autoSweep = true,
|
|
130
|
+
}) {
|
|
131
|
+
const actions = {
|
|
132
|
+
stackName,
|
|
133
|
+
baseDir,
|
|
134
|
+
aggressive,
|
|
135
|
+
sweepOwned,
|
|
136
|
+
runner: null,
|
|
137
|
+
daemonSessionsStopped: null,
|
|
138
|
+
daemonStopped: false,
|
|
139
|
+
killedPorts: [],
|
|
140
|
+
expoDev: [],
|
|
141
|
+
uiDev: [],
|
|
142
|
+
mobile: [],
|
|
143
|
+
infra: null,
|
|
144
|
+
errors: [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const serverComponent = resolveServerComponentFromStackEnv(env);
|
|
148
|
+
const port = coercePort(env.HAPPIER_STACK_SERVER_PORT);
|
|
149
|
+
const backendPort = coercePort(env.HAPPIER_STACK_SERVER_BACKEND_PORT);
|
|
150
|
+
const internalServerUrl = port ? `http://127.0.0.1:${port}` : 'http://127.0.0.1:3005';
|
|
151
|
+
const cliHomeDir = (env.HAPPIER_STACK_CLI_HOME_DIR ?? join(baseDir, 'cli')).toString();
|
|
152
|
+
const cliDir = getComponentDir(rootDir, 'happier-cli', env);
|
|
153
|
+
const cliBin = join(cliDir, 'bin', 'happier.mjs');
|
|
154
|
+
const envPath = (env.HAPPIER_STACK_ENV_FILE ?? '').toString();
|
|
155
|
+
|
|
156
|
+
// Preferred: stop stack-started processes (by PID) recorded in stack.runtime.json.
|
|
157
|
+
// This is safer than killing whatever happens to listen on a port, and doesn't rely on the runner's shutdown handler.
|
|
158
|
+
const runtimeStatePath = getStackRuntimeStatePath(stackName);
|
|
159
|
+
const runtimeState = await readStackRuntimeStateFile(runtimeStatePath);
|
|
160
|
+
const runnerPid = Number(runtimeState?.ownerPid);
|
|
161
|
+
const processes = runtimeState?.processes && typeof runtimeState.processes === 'object' ? runtimeState.processes : {};
|
|
162
|
+
|
|
163
|
+
// Kill known child processes first (process groups), then stop daemon, then stop runner.
|
|
164
|
+
const killedProcessPids = [];
|
|
165
|
+
for (const [key, rawPid] of Object.entries(processes)) {
|
|
166
|
+
const pid = Number(rawPid);
|
|
167
|
+
if (!Number.isFinite(pid) || pid <= 1) continue;
|
|
168
|
+
if (!isPidAlive(pid)) continue;
|
|
169
|
+
// eslint-disable-next-line no-await-in-loop
|
|
170
|
+
const res = await killProcessGroupOwnedByStack(pid, { stackName, envPath, cliHomeDir, label: key, json });
|
|
171
|
+
if (res.killed) {
|
|
172
|
+
killedProcessPids.push({ key, pid, reason: res.reason, pgid: res.pgid ?? null });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
actions.runner = { stopped: false, pid: Number.isFinite(runnerPid) ? runnerPid : null, reason: runtimeState ? 'not_running_or_not_owned' : 'missing_state' };
|
|
176
|
+
actions.killedPorts = actions.killedPorts ?? [];
|
|
177
|
+
actions.processes = { killed: killedProcessPids };
|
|
178
|
+
|
|
179
|
+
if (aggressive) {
|
|
180
|
+
try {
|
|
181
|
+
actions.daemonSessionsStopped = await stopDaemonTrackedSessions({ cliHomeDir, serverUrl: internalServerUrl, json });
|
|
182
|
+
} catch (e) {
|
|
183
|
+
actions.errors.push({ step: 'daemon-sessions', error: e instanceof Error ? e.message : String(e) });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await stopLocalDaemon({ cliBin, internalServerUrl, cliHomeDir });
|
|
189
|
+
actions.daemonStopped = true;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
actions.errors.push({ step: 'daemon', error: e instanceof Error ? e.message : String(e) });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Now stop the runner PID last (if it exists). This should clean up any remaining state files it owns.
|
|
195
|
+
if (Number.isFinite(runnerPid) && runnerPid > 1 && isPidAlive(runnerPid)) {
|
|
196
|
+
if (!json) {
|
|
197
|
+
// eslint-disable-next-line no-console
|
|
198
|
+
console.log(`[stack] stopping runner (pid=${runnerPid}) for ${stackName}`);
|
|
199
|
+
}
|
|
200
|
+
const res = await killPidOwnedByStack(runnerPid, { stackName, envPath, cliHomeDir, label: 'runner', json });
|
|
201
|
+
actions.runner = { stopped: res.killed, pid: runnerPid, reason: res.reason };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Only delete runtime state if the runner is confirmed stopped (or not running).
|
|
205
|
+
if (!isPidAlive(runnerPid)) {
|
|
206
|
+
await deleteStackRuntimeStateFile(runtimeStatePath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
actions.expoDev = await stopExpoStateDir({ stackName, baseDir, kind: 'expo-dev', stateFileName: 'expo.state.json', envPath, json });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
actions.errors.push({ step: 'expo-dev', error: e instanceof Error ? e.message : String(e) });
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
// Legacy cleanups (best-effort): older runs used separate state dirs.
|
|
216
|
+
actions.uiDev = await stopExpoStateDir({ stackName, baseDir, kind: 'ui-dev', stateFileName: 'ui.state.json', envPath, json });
|
|
217
|
+
const killedDev = await stopExpoStateDir({ stackName, baseDir, kind: 'mobile-dev', stateFileName: 'mobile.state.json', envPath, json });
|
|
218
|
+
const killedLegacy = await stopExpoStateDir({ stackName, baseDir, kind: 'mobile', stateFileName: 'expo.state.json', envPath, json });
|
|
219
|
+
actions.mobile = [...killedDev, ...killedLegacy];
|
|
220
|
+
} catch (e) {
|
|
221
|
+
actions.errors.push({ step: 'expo-mobile', error: e instanceof Error ? e.message : String(e) });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// IMPORTANT:
|
|
225
|
+
// Never kill "whatever is listening on a port" in stack mode.
|
|
226
|
+
void backendPort;
|
|
227
|
+
void port;
|
|
228
|
+
|
|
229
|
+
const managed = (env.HAPPIER_STACK_MANAGED_INFRA ?? '1').toString().trim() !== '0';
|
|
230
|
+
if (!noDocker && serverComponent === 'happier-server' && managed) {
|
|
231
|
+
try {
|
|
232
|
+
actions.infra = await stopHappyServerManagedInfra({ stackName, baseDir, removeVolumes: false });
|
|
233
|
+
} catch (e) {
|
|
234
|
+
actions.errors.push({ step: 'infra', error: e instanceof Error ? e.message : String(e) });
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
actions.infra = { ok: true, skipped: true, reason: noDocker ? 'no_docker' : 'not_managed_or_not_happier_server' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Last resort: sweep any remaining processes that still carry this stack env file in their environment.
|
|
241
|
+
// IMPORTANT:
|
|
242
|
+
// This must NOT kill daemon-spawned sessions/LLM/agent processes. Those should survive infra restarts.
|
|
243
|
+
//
|
|
244
|
+
// We only target infra processes by requiring HAPPIER_STACK_PROCESS_KIND=infra in addition to the stack env file.
|
|
245
|
+
// We also exclude our own PID.
|
|
246
|
+
const autoSweepFromEnvRaw = (env?.HAPPIER_STACK_STOP_AUTO_SWEEP ?? '').toString().trim();
|
|
247
|
+
const autoSweepFromEnv = autoSweepFromEnvRaw ? autoSweepFromEnvRaw !== '0' : null;
|
|
248
|
+
const autoSweepResolved = typeof autoSweepFromEnv === 'boolean' ? autoSweepFromEnv : Boolean(autoSweep);
|
|
249
|
+
|
|
250
|
+
// If the runtime state exists but its owner PID is stale (e.g. runner was Ctrl+C'd),
|
|
251
|
+
// treat it like "missing" and fall back to a safe infra-only sweep.
|
|
252
|
+
const runtimeStateUsable = Boolean(runtimeState) && Number.isFinite(runnerPid) && runnerPid > 1 && isPidAlive(runnerPid);
|
|
253
|
+
const shouldAutoSweep = autoSweepResolved && envPath && !runtimeStateUsable;
|
|
254
|
+
if ((sweepOwned || shouldAutoSweep) && envPath) {
|
|
255
|
+
const envNeedle = `HAPPIER_STACK_ENV_FILE=${envPath}`;
|
|
256
|
+
const infraTagged = await listPidsWithEnvNeedles([envNeedle, 'HAPPIER_STACK_PROCESS_KIND=infra']);
|
|
257
|
+
|
|
258
|
+
// Compatibility sweep for older stacks: some long-running infra (notably server dev loops)
|
|
259
|
+
// may not have been started with HAPPIER_STACK_PROCESS_KIND=infra yet. We restrict this
|
|
260
|
+
// fallback to npm/yarn managed server processes to avoid touching daemon-spawned sessions.
|
|
261
|
+
const legacyServer = await listPidsWithEnvNeedles([
|
|
262
|
+
envNeedle,
|
|
263
|
+
'npm_lifecycle_event=',
|
|
264
|
+
'npm_package_name=@happier-dev/server',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const pids = [...new Set([...infraTagged, ...legacyServer])]
|
|
268
|
+
.filter((pid) => pid !== process.pid)
|
|
269
|
+
.filter((pid) => Number.isFinite(pid) && pid > 1);
|
|
270
|
+
|
|
271
|
+
const swept = [];
|
|
272
|
+
for (const pid of Array.from(new Set(pids))) {
|
|
273
|
+
if (!isPidAlive(pid)) continue;
|
|
274
|
+
// eslint-disable-next-line no-await-in-loop
|
|
275
|
+
const res = await killProcessGroupOwnedByStack(pid, { stackName, envPath, cliHomeDir, label: 'sweep', json });
|
|
276
|
+
if (res.killed) {
|
|
277
|
+
swept.push({ pid, reason: res.reason, pgid: res.pgid ?? null });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
actions.sweep = { pids: swept, auto: shouldAutoSweep && !sweepOwned };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return actions;
|
|
284
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './stack/context.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './stack/runtime_state.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './stack/stacks.mjs';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailscale IP detection utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides functions to detect the local Tailscale IPv4 address for port forwarding.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runCaptureResult } from '../proc/proc.mjs';
|
|
8
|
+
import { resolveCommandPath } from '../proc/commands.mjs';
|
|
9
|
+
import { access, constants } from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
const TAILSCALE_TIMEOUT_MS = 3000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a path is executable.
|
|
15
|
+
*/
|
|
16
|
+
async function isExecutable(path) {
|
|
17
|
+
try {
|
|
18
|
+
await access(path, constants.X_OK);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tailscale env: strip XPC_SERVICE_NAME which can cause hangs in LaunchAgent contexts.
|
|
27
|
+
*/
|
|
28
|
+
function tailscaleEnv() {
|
|
29
|
+
const env = { ...process.env };
|
|
30
|
+
delete env.XPC_SERVICE_NAME;
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the tailscale CLI path.
|
|
36
|
+
*
|
|
37
|
+
* Priority:
|
|
38
|
+
* 1. HAPPIER_STACK_TAILSCALE_BIN env override
|
|
39
|
+
* 2. PATH lookup
|
|
40
|
+
* 3. macOS app bundle paths
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveTailscaleCmd() {
|
|
43
|
+
// Explicit override
|
|
44
|
+
if (process.env.HAPPIER_STACK_TAILSCALE_BIN?.trim()) {
|
|
45
|
+
return process.env.HAPPIER_STACK_TAILSCALE_BIN.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try PATH first
|
|
49
|
+
try {
|
|
50
|
+
const found = await resolveCommandPath('tailscale', { env: tailscaleEnv(), timeoutMs: TAILSCALE_TIMEOUT_MS });
|
|
51
|
+
if (found) return found;
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// macOS app bundle paths
|
|
57
|
+
const appCliPath = '/Applications/Tailscale.app/Contents/MacOS/tailscale';
|
|
58
|
+
if (await isExecutable(appCliPath)) return appCliPath;
|
|
59
|
+
|
|
60
|
+
const appPath = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
61
|
+
if (await isExecutable(appPath)) return appPath;
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the local Tailscale IPv4 address.
|
|
68
|
+
*
|
|
69
|
+
* @returns {Promise<string | null>} The Tailscale IPv4 address, or null if unavailable.
|
|
70
|
+
*/
|
|
71
|
+
export async function getTailscaleIpv4() {
|
|
72
|
+
const cmd = await resolveTailscaleCmd();
|
|
73
|
+
if (!cmd) return null;
|
|
74
|
+
|
|
75
|
+
const result = await runCaptureResult(cmd, ['ip', '-4'], {
|
|
76
|
+
env: tailscaleEnv(),
|
|
77
|
+
timeoutMs: TAILSCALE_TIMEOUT_MS,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.ok) return null;
|
|
81
|
+
|
|
82
|
+
const ip = result.out.trim().split('\n')[0]?.trim();
|
|
83
|
+
// Validate IPv4 format (basic check)
|
|
84
|
+
if (!ip || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) return null;
|
|
85
|
+
|
|
86
|
+
return ip;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if Tailscale is available and connected.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Promise<boolean>}
|
|
93
|
+
*/
|
|
94
|
+
export async function isTailscaleAvailable() {
|
|
95
|
+
const ip = await getTailscaleIpv4();
|
|
96
|
+
return Boolean(ip);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get Tailscale status information.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Promise<{ available: boolean, ip: string | null, error: string | null }>}
|
|
103
|
+
*/
|
|
104
|
+
export async function getTailscaleStatus() {
|
|
105
|
+
const cmd = await resolveTailscaleCmd();
|
|
106
|
+
if (!cmd) {
|
|
107
|
+
return { available: false, ip: null, error: 'tailscale CLI not found' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ip = await getTailscaleIpv4();
|
|
111
|
+
if (!ip) {
|
|
112
|
+
return { available: false, ip: null, error: 'tailscale not connected or no IPv4 address' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { available: true, ip, error: null };
|
|
116
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function getTrimmedEnv(env, key) {
|
|
2
|
+
const raw = env?.[key];
|
|
3
|
+
if (raw === undefined || raw === null) return '';
|
|
4
|
+
return String(raw).trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function applyStackTauriOverrides({ tauriConfig, env }) {
|
|
8
|
+
const identifierOverride = getTrimmedEnv(env, 'HAPPIER_STACK_TAURI_IDENTIFIER');
|
|
9
|
+
const productNameOverride = getTrimmedEnv(env, 'HAPPIER_STACK_TAURI_PRODUCT_NAME');
|
|
10
|
+
|
|
11
|
+
tauriConfig.identifier = identifierOverride || 'com.happier.stack';
|
|
12
|
+
tauriConfig.productName = productNameOverride || tauriConfig.productName || 'Happier';
|
|
13
|
+
|
|
14
|
+
if (tauriConfig.app?.windows?.length) {
|
|
15
|
+
tauriConfig.app.windows = tauriConfig.app.windows.map((w) => ({
|
|
16
|
+
...w,
|
|
17
|
+
title: tauriConfig.productName ?? w.title,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return tauriConfig;
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function matchesAnySuffix(name, suffixes = []) {
|
|
5
|
+
return suffixes.some((suffix) => name.endsWith(suffix));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function collectTestFiles({
|
|
9
|
+
dir,
|
|
10
|
+
includeSuffixes = ['.test.mjs'],
|
|
11
|
+
excludeSuffixes = [],
|
|
12
|
+
} = {}) {
|
|
13
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (entry.name.startsWith('.')) continue;
|
|
17
|
+
const path = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
files.push(...(await collectTestFiles({ dir: path, includeSuffixes, excludeSuffixes })));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!entry.isFile()) continue;
|
|
23
|
+
if (!matchesAnySuffix(entry.name, includeSuffixes)) continue;
|
|
24
|
+
if (matchesAnySuffix(entry.name, excludeSuffixes)) continue;
|
|
25
|
+
files.push(path);
|
|
26
|
+
}
|
|
27
|
+
files.sort();
|
|
28
|
+
return files;
|
|
29
|
+
}
|