@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,78 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
importPrismaClientForHappyServerLight,
|
|
9
|
+
importPrismaClientFromGeneratedSqlite,
|
|
10
|
+
importPrismaClientFromNodeModules,
|
|
11
|
+
} from './prisma_import.mjs';
|
|
12
|
+
|
|
13
|
+
async function writeJson(path, obj) {
|
|
14
|
+
await writeFile(path, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function withPrismaFixture(run) {
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-prisma-import-'));
|
|
19
|
+
try {
|
|
20
|
+
await run(dir);
|
|
21
|
+
} finally {
|
|
22
|
+
await rm(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function writeModulePackageJson(dir) {
|
|
27
|
+
await writeJson(join(dir, 'package.json'), { type: 'module' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writePrismaNodeModule(dir, body = 'export class PrismaClient {}\n') {
|
|
31
|
+
await mkdir(join(dir, 'node_modules', '@prisma', 'client'), { recursive: true });
|
|
32
|
+
await writeJson(join(dir, 'node_modules', '@prisma', 'client', 'package.json'), {
|
|
33
|
+
name: '@prisma/client',
|
|
34
|
+
type: 'module',
|
|
35
|
+
main: './index.js',
|
|
36
|
+
});
|
|
37
|
+
await writeFile(join(dir, 'node_modules', '@prisma', 'client', 'index.js'), body, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('importPrismaClientFromNodeModules imports PrismaClient via node_modules resolution', async () => {
|
|
41
|
+
await withPrismaFixture(async (dir) => {
|
|
42
|
+
await writePrismaNodeModule(dir);
|
|
43
|
+
const PrismaClient = await importPrismaClientFromNodeModules({ dir });
|
|
44
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
45
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('importPrismaClientFromGeneratedSqlite imports PrismaClient from generated/sqlite-client', async () => {
|
|
50
|
+
await withPrismaFixture(async (dir) => {
|
|
51
|
+
await writeModulePackageJson(dir);
|
|
52
|
+
await mkdir(join(dir, 'generated', 'sqlite-client'), { recursive: true });
|
|
53
|
+
await writeFile(join(dir, 'generated', 'sqlite-client', 'index.js'), 'export class PrismaClient {}\n', 'utf-8');
|
|
54
|
+
|
|
55
|
+
const PrismaClient = await importPrismaClientFromGeneratedSqlite({ dir });
|
|
56
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
57
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('importPrismaClientForHappyServerLight uses node_modules Prisma client even if legacy sqlite artifacts exist', async () => {
|
|
62
|
+
await withPrismaFixture(async (dir) => {
|
|
63
|
+
await writeModulePackageJson(dir);
|
|
64
|
+
await mkdir(join(dir, 'prisma', 'sqlite'), { recursive: true });
|
|
65
|
+
await writeFile(join(dir, 'prisma', 'sqlite', 'schema.prisma'), 'datasource db { provider = "sqlite" }\n', 'utf-8');
|
|
66
|
+
|
|
67
|
+
await mkdir(join(dir, 'generated', 'sqlite-client'), { recursive: true });
|
|
68
|
+
await writeFile(join(dir, 'generated', 'sqlite-client', 'index.js'), 'export class PrismaClient {}\n', 'utf-8');
|
|
69
|
+
|
|
70
|
+
// Also create a node_modules PrismaClient; the light flavor should use it.
|
|
71
|
+
await writePrismaNodeModule(dir, 'export class PrismaClient { static which = "node_modules"; }\n');
|
|
72
|
+
|
|
73
|
+
const PrismaClient = await importPrismaClientForHappyServerLight({ serverDir: dir });
|
|
74
|
+
assert.equal(typeof PrismaClient, 'function');
|
|
75
|
+
assert.equal(PrismaClient.name, 'PrismaClient');
|
|
76
|
+
assert.equal(PrismaClient.which, 'node_modules');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
|
|
3
|
+
export function getServerComponentName({ kv } = {}) {
|
|
4
|
+
const fromArgRaw = kv?.get('--server')?.trim() ? kv.get('--server').trim() : '';
|
|
5
|
+
const fromEnvRaw = process.env.HAPPIER_STACK_SERVER_COMPONENT?.trim() ? process.env.HAPPIER_STACK_SERVER_COMPONENT.trim() : '';
|
|
6
|
+
const raw = fromArgRaw || fromEnvRaw || 'happier-server-light';
|
|
7
|
+
const v = raw.toLowerCase();
|
|
8
|
+
if (v === 'light' || v === 'server-light' || v === 'happier-server-light' || v === 'happy-server-light') {
|
|
9
|
+
return 'happier-server-light';
|
|
10
|
+
}
|
|
11
|
+
if (v === 'server' || v === 'full' || v === 'happier-server' || v === 'happy-server') {
|
|
12
|
+
return 'happier-server';
|
|
13
|
+
}
|
|
14
|
+
if (v === 'both') {
|
|
15
|
+
return 'both';
|
|
16
|
+
}
|
|
17
|
+
// Allow explicit component dir names (advanced).
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function fetchHappierHealth(baseUrl) {
|
|
22
|
+
const ctl = new AbortController();
|
|
23
|
+
const t = setTimeout(() => ctl.abort(), 1500);
|
|
24
|
+
try {
|
|
25
|
+
const url = baseUrl.replace(/\/+$/, '') + '/health';
|
|
26
|
+
const res = await fetch(url, { method: 'GET', signal: ctl.signal });
|
|
27
|
+
const text = await res.text();
|
|
28
|
+
let json = null;
|
|
29
|
+
try {
|
|
30
|
+
json = text ? JSON.parse(text) : null;
|
|
31
|
+
} catch {
|
|
32
|
+
json = null;
|
|
33
|
+
}
|
|
34
|
+
return { ok: res.ok, status: res.status, json, text };
|
|
35
|
+
} catch {
|
|
36
|
+
return { ok: false, status: null, json: null, text: null };
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(t);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function isHappierServerRunning(baseUrl) {
|
|
43
|
+
const health = await fetchHappierHealth(baseUrl);
|
|
44
|
+
if (!health.ok) return false;
|
|
45
|
+
// Both happier-server and happier-server-light use `service: 'happier-server'` today.
|
|
46
|
+
// Treat any ok health response as "running" to avoid duplicate spawns.
|
|
47
|
+
const svc = typeof health.json?.service === 'string' ? health.json.service : '';
|
|
48
|
+
const status = typeof health.json?.status === 'string' ? health.json.status : '';
|
|
49
|
+
if (svc && svc !== 'happier-server') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (status && status !== 'ok') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function waitForHappierHealthOk(baseUrl, { timeoutMs = 60_000, intervalMs = 300 } = {}) {
|
|
59
|
+
const deadline = Date.now() + timeoutMs;
|
|
60
|
+
while (Date.now() < deadline) {
|
|
61
|
+
// eslint-disable-next-line no-await-in-loop
|
|
62
|
+
const health = await fetchHappierHealth(baseUrl);
|
|
63
|
+
if (health.ok) return true;
|
|
64
|
+
// eslint-disable-next-line no-await-in-loop
|
|
65
|
+
await delay(intervalMs);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function waitForServerReady(url) {
|
|
71
|
+
const deadline = Date.now() + 60_000;
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(url, { method: 'GET' });
|
|
75
|
+
const text = await res.text();
|
|
76
|
+
if (res.ok && text.includes('Welcome to Happier Server!')) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore
|
|
81
|
+
}
|
|
82
|
+
await delay(300);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Timed out waiting for server at ${url}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Used for UI readiness checks (Expo / gateway / server). Treat any HTTP response as "up".
|
|
88
|
+
export async function waitForHttpOk(url, { timeoutMs = 15_000, intervalMs = 250 } = {}) {
|
|
89
|
+
const deadline = Date.now() + timeoutMs;
|
|
90
|
+
while (Date.now() < deadline) {
|
|
91
|
+
try {
|
|
92
|
+
const ctl = new AbortController();
|
|
93
|
+
const t = setTimeout(() => ctl.abort(), Math.min(2500, Math.max(250, intervalMs)));
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(url, { method: 'GET', signal: ctl.signal });
|
|
96
|
+
if (res.status >= 100 && res.status < 600) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(t);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
106
|
+
await delay(intervalMs);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Timed out waiting for HTTP response from ${url} after ${timeoutMs}ms`);
|
|
109
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cmd } from '../ui/layout.mjs';
|
|
2
|
+
|
|
3
|
+
export function validateUiServingConfig({
|
|
4
|
+
serverComponentName,
|
|
5
|
+
serveUiWanted,
|
|
6
|
+
uiRequired,
|
|
7
|
+
uiBuildDir,
|
|
8
|
+
uiBuildDirExists,
|
|
9
|
+
uiIndexExists,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const serve = Boolean(serveUiWanted);
|
|
12
|
+
if (!serve) {
|
|
13
|
+
return { serveUi: false, warning: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const dir = String(uiBuildDir ?? '').trim();
|
|
17
|
+
const exists = Boolean(uiBuildDirExists);
|
|
18
|
+
const hasIndex = Boolean(uiIndexExists);
|
|
19
|
+
const required = uiRequired == null ? true : Boolean(uiRequired);
|
|
20
|
+
|
|
21
|
+
if (!dir || !exists) {
|
|
22
|
+
if (required || serverComponentName === 'happier-server-light') {
|
|
23
|
+
throw new Error(`[local] UI build directory not found at ${dir || '(unset)'}.\nRun: ${cmd('hstack build')}`);
|
|
24
|
+
}
|
|
25
|
+
return { serveUi: false, warning: dir ? `UI build dir missing at ${dir}; UI serving will be disabled` : 'UI build dir unset; UI serving will be disabled' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!hasIndex) {
|
|
29
|
+
const indexPath = `${dir.replace(/\/+$/, '')}/index.html`;
|
|
30
|
+
if (required || serverComponentName === 'happier-server-light') {
|
|
31
|
+
throw new Error(`[local] UI build is incomplete (missing ${indexPath}).\nRun: ${cmd('hstack build')}`);
|
|
32
|
+
}
|
|
33
|
+
return { serveUi: false, warning: `UI index.html missing at ${indexPath}; UI serving will be disabled` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { serveUi: true, warning: null };
|
|
37
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { validateUiServingConfig } from './ui_build_check.mjs';
|
|
5
|
+
|
|
6
|
+
test('validateUiServingConfig returns disabled/no-warning when UI serving is not requested', () => {
|
|
7
|
+
const res = validateUiServingConfig({
|
|
8
|
+
serverComponentName: 'happier-server',
|
|
9
|
+
serveUiWanted: false,
|
|
10
|
+
uiBuildDir: '',
|
|
11
|
+
uiBuildDirExists: false,
|
|
12
|
+
uiIndexExists: false,
|
|
13
|
+
});
|
|
14
|
+
assert.deepEqual(res, { serveUi: false, warning: null });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('validateUiServingConfig fails closed for server-light when index.html is missing', () => {
|
|
18
|
+
assert.throws(
|
|
19
|
+
() =>
|
|
20
|
+
validateUiServingConfig({
|
|
21
|
+
serverComponentName: 'happier-server-light',
|
|
22
|
+
serveUiWanted: true,
|
|
23
|
+
uiBuildDir: '/tmp/ui',
|
|
24
|
+
uiBuildDirExists: true,
|
|
25
|
+
uiIndexExists: false,
|
|
26
|
+
}),
|
|
27
|
+
/hstack build/i
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('validateUiServingConfig warns+disables UI for full server when index.html is missing', () => {
|
|
32
|
+
const res = validateUiServingConfig({
|
|
33
|
+
serverComponentName: 'happier-server',
|
|
34
|
+
serveUiWanted: true,
|
|
35
|
+
uiRequired: false,
|
|
36
|
+
uiBuildDir: '/tmp/ui',
|
|
37
|
+
uiBuildDirExists: true,
|
|
38
|
+
uiIndexExists: false,
|
|
39
|
+
});
|
|
40
|
+
assert.equal(res.serveUi, false);
|
|
41
|
+
assert.match(res.warning ?? '', /index\.html/i);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('validateUiServingConfig warns+disables UI for full server when build dir is missing', () => {
|
|
45
|
+
const res = validateUiServingConfig({
|
|
46
|
+
serverComponentName: 'happier-server',
|
|
47
|
+
serveUiWanted: true,
|
|
48
|
+
uiRequired: false,
|
|
49
|
+
uiBuildDir: '/tmp/missing-ui',
|
|
50
|
+
uiBuildDirExists: false,
|
|
51
|
+
uiIndexExists: false,
|
|
52
|
+
});
|
|
53
|
+
assert.equal(res.serveUi, false);
|
|
54
|
+
assert.match(res.warning ?? '', /build dir/i);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('validateUiServingConfig fails closed when UI is required (even for full server)', () => {
|
|
58
|
+
assert.throws(
|
|
59
|
+
() =>
|
|
60
|
+
validateUiServingConfig({
|
|
61
|
+
serverComponentName: 'happier-server',
|
|
62
|
+
serveUiWanted: true,
|
|
63
|
+
uiRequired: true,
|
|
64
|
+
uiBuildDir: '/tmp/ui',
|
|
65
|
+
uiBuildDirExists: true,
|
|
66
|
+
uiIndexExists: false,
|
|
67
|
+
}),
|
|
68
|
+
/hstack build/i
|
|
69
|
+
);
|
|
70
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function resolveServerUiEnv({ serveUi, uiBuildDir, uiPrefix, uiBuildDirExists }) {
|
|
2
|
+
if (!serveUi) return {};
|
|
3
|
+
if (!uiBuildDirExists) return {};
|
|
4
|
+
if (!uiBuildDir) return {};
|
|
5
|
+
|
|
6
|
+
// Set both full and light UI env vars (full/light share UI config resolution).
|
|
7
|
+
return {
|
|
8
|
+
HAPPIER_SERVER_UI_DIR: uiBuildDir,
|
|
9
|
+
HAPPIER_SERVER_UI_PREFIX: uiPrefix,
|
|
10
|
+
HAPPIER_SERVER_LIGHT_UI_DIR: uiBuildDir,
|
|
11
|
+
HAPPIER_SERVER_LIGHT_UI_PREFIX: uiPrefix,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { resolveServerUiEnv } from './ui_env.mjs';
|
|
5
|
+
|
|
6
|
+
test('resolveServerUiEnv returns empty when UI serving is disabled', () => {
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
resolveServerUiEnv({
|
|
9
|
+
serveUi: false,
|
|
10
|
+
uiBuildDir: '/tmp/ui',
|
|
11
|
+
uiPrefix: '/',
|
|
12
|
+
uiBuildDirExists: true,
|
|
13
|
+
}),
|
|
14
|
+
{}
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('resolveServerUiEnv returns empty when UI build dir is missing', () => {
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
resolveServerUiEnv({
|
|
21
|
+
serveUi: true,
|
|
22
|
+
uiBuildDir: '/tmp/ui',
|
|
23
|
+
uiPrefix: '/',
|
|
24
|
+
uiBuildDirExists: false,
|
|
25
|
+
}),
|
|
26
|
+
{}
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('resolveServerUiEnv returns empty when UI build dir is empty', () => {
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
resolveServerUiEnv({
|
|
33
|
+
serveUi: true,
|
|
34
|
+
uiBuildDir: '',
|
|
35
|
+
uiPrefix: '/',
|
|
36
|
+
uiBuildDirExists: true,
|
|
37
|
+
}),
|
|
38
|
+
{}
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('resolveServerUiEnv sets both full and light env keys when enabled', () => {
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
resolveServerUiEnv({
|
|
45
|
+
serveUi: true,
|
|
46
|
+
uiBuildDir: '/tmp/ui',
|
|
47
|
+
uiPrefix: '/ui',
|
|
48
|
+
uiBuildDirExists: true,
|
|
49
|
+
}),
|
|
50
|
+
{
|
|
51
|
+
HAPPIER_SERVER_UI_DIR: '/tmp/ui',
|
|
52
|
+
HAPPIER_SERVER_UI_PREFIX: '/ui',
|
|
53
|
+
HAPPIER_SERVER_LIGHT_UI_DIR: '/tmp/ui',
|
|
54
|
+
HAPPIER_SERVER_LIGHT_UI_PREFIX: '/ui',
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { getStackName, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
4
|
+
import { preferStackLocalhostUrl } from '../paths/localhost_host.mjs';
|
|
5
|
+
import { resolvePublicServerUrl } from '../../tailscale.mjs';
|
|
6
|
+
import { resolveServerPortFromEnv } from './port.mjs';
|
|
7
|
+
import { normalizeUrlNoTrailingSlash } from '../net/url.mjs';
|
|
8
|
+
|
|
9
|
+
function stackEnvExplicitlySetsPublicUrl({ env, stackName }) {
|
|
10
|
+
try {
|
|
11
|
+
const envPath =
|
|
12
|
+
(env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim() ||
|
|
13
|
+
resolveStackEnvPath(stackName).envPath;
|
|
14
|
+
if (!envPath || !existsSync(envPath)) return false;
|
|
15
|
+
const raw = readFileSync(envPath, 'utf-8');
|
|
16
|
+
return /^HAPPIER_STACK_SERVER_URL=/m.test(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stackEnvExplicitlySetsWebappUrl({ env, stackName }) {
|
|
23
|
+
try {
|
|
24
|
+
const envPath =
|
|
25
|
+
(env.HAPPIER_STACK_ENV_FILE ?? '').toString().trim() ||
|
|
26
|
+
resolveStackEnvPath(stackName).envPath;
|
|
27
|
+
if (!envPath || !existsSync(envPath)) return false;
|
|
28
|
+
const raw = readFileSync(envPath, 'utf-8');
|
|
29
|
+
return /^HAPPIER_WEBAPP_URL=/m.test(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPublicServerUrlEnvOverride({ env = process.env, serverPort, stackName = null } = {}) {
|
|
36
|
+
const name =
|
|
37
|
+
(stackName ?? '').toString().trim() ||
|
|
38
|
+
(env.HAPPIER_STACK_STACK ?? '').toString().trim() ||
|
|
39
|
+
getStackName(env);
|
|
40
|
+
const defaultPublicUrl = `http://localhost:${serverPort}`;
|
|
41
|
+
|
|
42
|
+
let envPublicUrl = (env.HAPPIER_STACK_SERVER_URL ?? '').toString().trim() || '';
|
|
43
|
+
envPublicUrl = normalizeUrlNoTrailingSlash(envPublicUrl);
|
|
44
|
+
|
|
45
|
+
// Safety: for non-main stacks, ignore a global SERVER_URL unless it was explicitly set in the stack env file.
|
|
46
|
+
if (name !== 'main' && envPublicUrl && !stackEnvExplicitlySetsPublicUrl({ env, stackName: name })) {
|
|
47
|
+
envPublicUrl = '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { defaultPublicUrl, envPublicUrl, publicServerUrl: envPublicUrl || defaultPublicUrl };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getWebappUrlEnvOverride({ env = process.env, stackName = null } = {}) {
|
|
54
|
+
const name =
|
|
55
|
+
(stackName ?? '').toString().trim() ||
|
|
56
|
+
(env.HAPPIER_STACK_STACK ?? '').toString().trim() ||
|
|
57
|
+
getStackName(env);
|
|
58
|
+
|
|
59
|
+
let envWebappUrl = (env.HAPPIER_WEBAPP_URL ?? '').toString().trim() || '';
|
|
60
|
+
|
|
61
|
+
// Safety: ignore a global HAPPIER_WEBAPP_URL unless it was explicitly set in the stack env file.
|
|
62
|
+
// This prevents surprising launches of the hosted app due to shell env leakage.
|
|
63
|
+
if (envWebappUrl && !stackEnvExplicitlySetsWebappUrl({ env, stackName: name })) {
|
|
64
|
+
envWebappUrl = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { envWebappUrl };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function resolveServerUrls({ env = process.env, serverPort, allowEnable = true } = {}) {
|
|
71
|
+
const internalServerUrl = `http://127.0.0.1:${serverPort}`;
|
|
72
|
+
const stackName =
|
|
73
|
+
(env.HAPPIER_STACK_STACK ?? '').toString().trim() ||
|
|
74
|
+
getStackName(env);
|
|
75
|
+
const { defaultPublicUrl, envPublicUrl } = getPublicServerUrlEnvOverride({ env, serverPort });
|
|
76
|
+
const resolved = await resolvePublicServerUrl({
|
|
77
|
+
internalServerUrl,
|
|
78
|
+
defaultPublicUrl,
|
|
79
|
+
envPublicUrl,
|
|
80
|
+
allowEnable,
|
|
81
|
+
stackName,
|
|
82
|
+
});
|
|
83
|
+
const publicServerUrl = normalizeUrlNoTrailingSlash(
|
|
84
|
+
await preferStackLocalhostUrl(resolved.publicServerUrl, { stackName })
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
internalServerUrl,
|
|
88
|
+
defaultPublicUrl,
|
|
89
|
+
envPublicUrl,
|
|
90
|
+
publicServerUrl,
|
|
91
|
+
publicServerUrlSource: resolved.source,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getInternalServerUrl({ env = process.env, defaultPort = 3005 } = {}) {
|
|
96
|
+
const port = resolveServerPortFromEnv({ env, defaultPort });
|
|
97
|
+
return { port, internalServerUrl: `http://127.0.0.1:${port}` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { resolveServerPortFromEnv };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function detectServerComponentDirMismatch({ rootDir, serverComponentName, serverDir }) {
|
|
5
|
+
// Repo-only model: server flavors share the same monorepo checkout/worktree.
|
|
6
|
+
// The previous "mismatch" check was specific to the legacy multi-repo components layout.
|
|
7
|
+
void rootDir;
|
|
8
|
+
void serverComponentName;
|
|
9
|
+
void serverDir;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertServerComponentDirMatches({ rootDir, serverComponentName, serverDir }) {
|
|
14
|
+
void rootDir;
|
|
15
|
+
void serverComponentName;
|
|
16
|
+
void serverDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function detectPrismaProvider(schemaText) {
|
|
20
|
+
// Best-effort parse of:
|
|
21
|
+
// datasource db { provider = "sqlite" ... }
|
|
22
|
+
const m = schemaText.match(/datasource\s+db\s*\{[\s\S]*?\bprovider\s*=\s*\"([a-zA-Z0-9_-]+)\"/m);
|
|
23
|
+
return m?.[1] ?? '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertServerPrismaProviderMatches({ serverComponentName, serverDir }) {
|
|
27
|
+
const schemaPath = join(serverDir, 'prisma', 'schema.prisma');
|
|
28
|
+
|
|
29
|
+
let schemaText = '';
|
|
30
|
+
try {
|
|
31
|
+
schemaText = readFileSync(schemaPath, 'utf-8');
|
|
32
|
+
} catch {
|
|
33
|
+
// If it doesn't exist, skip validation; not every server component necessarily uses Prisma.
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const provider = detectPrismaProvider(schemaText);
|
|
38
|
+
if (!provider) return;
|
|
39
|
+
|
|
40
|
+
// Happier server flavors share a single server package in the monorepo.
|
|
41
|
+
// Both the full server and the current light server use a Postgres schema (light runs via embedded PGlite).
|
|
42
|
+
if (serverComponentName === 'happier-server-light') {
|
|
43
|
+
if (provider !== 'postgresql') {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[server] happier-server-light expects Prisma datasource provider \"postgresql\", but found \"${provider}\" in:\n` +
|
|
46
|
+
`- ${schemaPath}\n` +
|
|
47
|
+
`Fix: point happier-server-light at a checkout that includes the current light flavor implementation (PGlite) and uses the Postgres schema.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (serverComponentName === 'happier-server' && provider === 'sqlite') {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`[server] happier-server expects Prisma datasource provider \"postgresql\", but found \"sqlite\" in:\n` +
|
|
56
|
+
`- ${schemaPath}\n` +
|
|
57
|
+
`Fix: point happier-server at a checkout that uses the Postgres schema.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { assertServerPrismaProviderMatches } from './validate.mjs';
|
|
8
|
+
|
|
9
|
+
const PG_SCHEMA = `
|
|
10
|
+
datasource db {
|
|
11
|
+
provider = "postgresql"
|
|
12
|
+
url = env("DATABASE_URL")
|
|
13
|
+
}
|
|
14
|
+
`.trim();
|
|
15
|
+
|
|
16
|
+
const SQLITE_SCHEMA = `
|
|
17
|
+
datasource db {
|
|
18
|
+
provider = "sqlite"
|
|
19
|
+
url = env("DATABASE_URL")
|
|
20
|
+
}
|
|
21
|
+
`.trim();
|
|
22
|
+
|
|
23
|
+
async function writeSchemas({ dir, schemaPrisma, schemaSqlitePrisma }) {
|
|
24
|
+
const prismaDir = join(dir, 'prisma');
|
|
25
|
+
await mkdir(prismaDir, { recursive: true });
|
|
26
|
+
if (schemaPrisma != null) {
|
|
27
|
+
await writeFile(join(prismaDir, 'schema.prisma'), schemaPrisma + '\n', 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
if (schemaSqlitePrisma != null) {
|
|
30
|
+
await mkdir(join(prismaDir, 'sqlite'), { recursive: true });
|
|
31
|
+
await writeFile(join(prismaDir, 'sqlite', 'schema.prisma'), schemaSqlitePrisma + '\n', 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function withValidateDir(run) {
|
|
36
|
+
const dir = await mkdtemp(join(tmpdir(), 'hs-validate-'));
|
|
37
|
+
try {
|
|
38
|
+
await run(dir);
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('assertServerPrismaProviderMatches accepts happier-server-light when schema.prisma is postgresql', async () => {
|
|
45
|
+
await withValidateDir(async (dir) => {
|
|
46
|
+
await writeSchemas({ dir, schemaPrisma: PG_SCHEMA, schemaSqlitePrisma: SQLITE_SCHEMA });
|
|
47
|
+
assert.doesNotThrow(() => assertServerPrismaProviderMatches({ serverComponentName: 'happier-server-light', serverDir: dir }));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('assertServerPrismaProviderMatches rejects happier-server-light when schema.prisma is sqlite', async () => {
|
|
52
|
+
await withValidateDir(async (dir) => {
|
|
53
|
+
await writeSchemas({ dir, schemaPrisma: SQLITE_SCHEMA, schemaSqlitePrisma: SQLITE_SCHEMA });
|
|
54
|
+
assert.throws(() => assertServerPrismaProviderMatches({ serverComponentName: 'happier-server-light', serverDir: dir }));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('assertServerPrismaProviderMatches rejects happier-server when schema.prisma is sqlite', async () => {
|
|
59
|
+
await withValidateDir(async (dir) => {
|
|
60
|
+
await writeSchemas({ dir, schemaPrisma: SQLITE_SCHEMA, schemaSqlitePrisma: null });
|
|
61
|
+
assert.throws(() => assertServerPrismaProviderMatches({ serverComponentName: 'happier-server', serverDir: dir }));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('assertServerPrismaProviderMatches accepts happier-server when schema.prisma is postgresql', async () => {
|
|
66
|
+
await withValidateDir(async (dir) => {
|
|
67
|
+
await writeSchemas({ dir, schemaPrisma: PG_SCHEMA, schemaSqlitePrisma: null });
|
|
68
|
+
assert.doesNotThrow(() => assertServerPrismaProviderMatches({ serverComponentName: 'happier-server', serverDir: dir }));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('assertServerPrismaProviderMatches is a no-op when schema.prisma is missing', async () => {
|
|
73
|
+
await withValidateDir(async (dir) => {
|
|
74
|
+
assert.doesNotThrow(() => assertServerPrismaProviderMatches({ serverComponentName: 'happier-server-light', serverDir: dir }));
|
|
75
|
+
});
|
|
76
|
+
});
|