@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,883 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
chmod,
|
|
6
|
+
copyFile,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
symlink,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from 'node:fs/promises';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
20
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
21
|
+
import { banner, sectionTitle } from './utils/ui/layout.mjs';
|
|
22
|
+
import { cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
|
|
23
|
+
|
|
24
|
+
const SUPPORTED_CHANNELS = new Set(['stable', 'preview']);
|
|
25
|
+
const DEFAULTS = Object.freeze({
|
|
26
|
+
githubRepo: 'happier-dev/happier',
|
|
27
|
+
minisignPubKeyUrl: 'https://happier.dev/happier-release.pub',
|
|
28
|
+
installRoot: '/opt/happier',
|
|
29
|
+
binDir: '/usr/local/bin',
|
|
30
|
+
configDir: '/etc/happier',
|
|
31
|
+
dataDir: '/var/lib/happier',
|
|
32
|
+
logDir: '/var/log/happier',
|
|
33
|
+
serviceName: 'happier-server',
|
|
34
|
+
serverHost: '127.0.0.1',
|
|
35
|
+
serverPort: 3005,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function parseBoolean(raw, fallback = false) {
|
|
39
|
+
const value = String(raw ?? '').trim().toLowerCase();
|
|
40
|
+
if (!value) return fallback;
|
|
41
|
+
if (value === '1' || value === 'true' || value === 'yes' || value === 'y' || value === 'on') return true;
|
|
42
|
+
if (value === '0' || value === 'false' || value === 'no' || value === 'n' || value === 'off') return false;
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parsePort(raw, fallback = DEFAULTS.serverPort) {
|
|
47
|
+
const value = Number(String(raw ?? '').trim());
|
|
48
|
+
if (!Number.isFinite(value)) return fallback;
|
|
49
|
+
const port = Math.floor(value);
|
|
50
|
+
return port > 0 && port <= 65535 ? port : fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertLinux() {
|
|
54
|
+
if (process.platform !== 'linux') {
|
|
55
|
+
throw new Error('[self-host] Happier Self-Host currently supports Linux only.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertRoot() {
|
|
60
|
+
if (typeof process.getuid !== 'function') return;
|
|
61
|
+
if (process.getuid() !== 0) {
|
|
62
|
+
throw new Error('[self-host] root privileges are required for this command.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runCommand(cmd, args, { cwd, env, allowFail = false, stdio = 'pipe' } = {}) {
|
|
67
|
+
const result = spawnSync(cmd, args, {
|
|
68
|
+
cwd,
|
|
69
|
+
env: env ?? process.env,
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
stdio,
|
|
72
|
+
});
|
|
73
|
+
if (result.error) {
|
|
74
|
+
if (!allowFail) throw result.error;
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (!allowFail && (result.status ?? 1) !== 0) {
|
|
78
|
+
const stderr = String(result.stderr ?? '').trim();
|
|
79
|
+
throw new Error(`[self-host] command failed: ${cmd} ${args.join(' ')}${stderr ? `\n${stderr}` : ''}`);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function commandExists(cmd) {
|
|
85
|
+
const result = runCommand('bash', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { allowFail: true, stdio: 'ignore' });
|
|
86
|
+
return (result.status ?? 1) === 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeArch() {
|
|
90
|
+
const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : '';
|
|
91
|
+
if (!arch) {
|
|
92
|
+
throw new Error(`[self-host] unsupported architecture: ${process.arch}`);
|
|
93
|
+
}
|
|
94
|
+
return arch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeChannel(raw) {
|
|
98
|
+
const channel = String(raw ?? '').trim() || 'stable';
|
|
99
|
+
if (!SUPPORTED_CHANNELS.has(channel)) {
|
|
100
|
+
throw new Error(`[self-host] invalid channel: ${channel} (expected stable|preview)`);
|
|
101
|
+
}
|
|
102
|
+
return channel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseSelfHostInvocation(argv) {
|
|
106
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
107
|
+
if (args[0] === 'self-host' || args[0] === 'selfhost') {
|
|
108
|
+
args.shift();
|
|
109
|
+
}
|
|
110
|
+
const subcommand = args.find((arg) => arg && !arg.startsWith('-')) ?? 'help';
|
|
111
|
+
const subcommandIndex = args.indexOf(subcommand);
|
|
112
|
+
return {
|
|
113
|
+
subcommand,
|
|
114
|
+
rest: subcommandIndex >= 0 ? args.slice(subcommandIndex + 1) : [],
|
|
115
|
+
argv: args,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function escapeRegex(s) {
|
|
120
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function pickReleaseAsset({ assets, product, os, arch }) {
|
|
124
|
+
const list = Array.isArray(assets) ? assets : [];
|
|
125
|
+
const archiveRe = new RegExp(`^${escapeRegex(product)}-v.+-${escapeRegex(os)}-${escapeRegex(arch)}\\.tar\\.gz$`);
|
|
126
|
+
const checksumsRe = new RegExp(`^checksums-${escapeRegex(product)}-v.+\\.txt$`);
|
|
127
|
+
const signatureRe = new RegExp(`^checksums-${escapeRegex(product)}-v.+\\.txt\\.minisig$`);
|
|
128
|
+
|
|
129
|
+
const archive = list.find((asset) => archiveRe.test(String(asset?.name ?? '')));
|
|
130
|
+
const checksums = list.find((asset) => checksumsRe.test(String(asset?.name ?? '')));
|
|
131
|
+
const signature = list.find((asset) => signatureRe.test(String(asset?.name ?? '')));
|
|
132
|
+
if (!archive || !checksums || !signature) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`[self-host] release assets not found for ${product} ${os}-${arch} (archive/checksums/signature missing)`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const archiveName = String(archive.name);
|
|
138
|
+
const versionMatch = archiveName.match(/-v([^-/]+)-/);
|
|
139
|
+
return {
|
|
140
|
+
archiveUrl: String(archive.browser_download_url ?? ''),
|
|
141
|
+
archiveName,
|
|
142
|
+
checksumsUrl: String(checksums.browser_download_url ?? ''),
|
|
143
|
+
signatureUrl: String(signature.browser_download_url ?? ''),
|
|
144
|
+
version: versionMatch ? versionMatch[1] : '',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function sha256(path) {
|
|
149
|
+
const bytes = await readFile(path);
|
|
150
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseChecksums(raw) {
|
|
154
|
+
const map = new Map();
|
|
155
|
+
for (const line of String(raw ?? '').split('\n')) {
|
|
156
|
+
const trimmed = line.trim();
|
|
157
|
+
if (!trimmed) continue;
|
|
158
|
+
const match = /^([a-fA-F0-9]{64})\s{2}(.+)$/.exec(trimmed);
|
|
159
|
+
if (!match) continue;
|
|
160
|
+
map.set(match[2], match[1].toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
return map;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function downloadToFile(url, targetPath) {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
headers: {
|
|
168
|
+
'user-agent': 'happier-self-host-installer',
|
|
169
|
+
accept: 'application/json',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`[self-host] download failed: ${url} (${response.status} ${response.statusText})`);
|
|
174
|
+
}
|
|
175
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
176
|
+
await writeFile(targetPath, bytes);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readRelease(tag, githubRepo) {
|
|
180
|
+
const url = `https://api.github.com/repos/${githubRepo}/releases/tags/${tag}`;
|
|
181
|
+
const response = await fetch(url, {
|
|
182
|
+
headers: {
|
|
183
|
+
'user-agent': 'happier-self-host-installer',
|
|
184
|
+
accept: 'application/vnd.github+json',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`[self-host] failed to resolve GitHub release tag ${tag} (${response.status})`);
|
|
189
|
+
}
|
|
190
|
+
return await response.json();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function findExecutableByName(rootDir, binaryName) {
|
|
194
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const fullPath = join(rootDir, entry.name);
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
const nested = await findExecutableByName(fullPath, binaryName);
|
|
199
|
+
if (nested) return nested;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!entry.isFile()) continue;
|
|
203
|
+
if (entry.name !== binaryName) continue;
|
|
204
|
+
const info = await stat(fullPath);
|
|
205
|
+
if ((info.mode & 0o111) !== 0) return fullPath;
|
|
206
|
+
}
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveConfig({ channel }) {
|
|
211
|
+
const installRoot = String(process.env.HAPPIER_SELF_HOST_INSTALL_ROOT ?? DEFAULTS.installRoot).trim();
|
|
212
|
+
const binDir = String(process.env.HAPPIER_SELF_HOST_BIN_DIR ?? DEFAULTS.binDir).trim();
|
|
213
|
+
const configDir = String(process.env.HAPPIER_SELF_HOST_CONFIG_DIR ?? DEFAULTS.configDir).trim();
|
|
214
|
+
const dataDir = String(process.env.HAPPIER_SELF_HOST_DATA_DIR ?? DEFAULTS.dataDir).trim();
|
|
215
|
+
const logDir = String(process.env.HAPPIER_SELF_HOST_LOG_DIR ?? DEFAULTS.logDir).trim();
|
|
216
|
+
const serviceName = String(process.env.HAPPIER_SELF_HOST_SERVICE_NAME ?? DEFAULTS.serviceName).trim();
|
|
217
|
+
const serverHost = String(process.env.HAPPIER_SERVER_HOST ?? DEFAULTS.serverHost).trim();
|
|
218
|
+
const serverPort = parsePort(process.env.HAPPIER_SERVER_PORT, DEFAULTS.serverPort);
|
|
219
|
+
const githubRepo = String(process.env.HAPPIER_GITHUB_REPO ?? DEFAULTS.githubRepo).trim();
|
|
220
|
+
const autoUpdate = parseBoolean(process.env.HAPPIER_SELF_HOST_AUTO_UPDATE, true);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
channel,
|
|
224
|
+
installRoot,
|
|
225
|
+
versionsDir: join(installRoot, 'versions'),
|
|
226
|
+
installBinDir: join(installRoot, 'bin'),
|
|
227
|
+
serverBinaryPath: join(installRoot, 'bin', 'happier-server'),
|
|
228
|
+
serverPreviousBinaryPath: join(installRoot, 'bin', 'happier-server.previous'),
|
|
229
|
+
statePath: join(installRoot, 'self-host-state.json'),
|
|
230
|
+
binDir,
|
|
231
|
+
configDir,
|
|
232
|
+
configEnvPath: join(configDir, 'server.env'),
|
|
233
|
+
dataDir,
|
|
234
|
+
filesDir: join(dataDir, 'files'),
|
|
235
|
+
dbDir: join(dataDir, 'pglite'),
|
|
236
|
+
logDir,
|
|
237
|
+
serverLogPath: join(logDir, 'server.log'),
|
|
238
|
+
serviceName,
|
|
239
|
+
serviceUnitName: `${serviceName}.service`,
|
|
240
|
+
serviceUnitPath: join('/etc/systemd/system', `${serviceName}.service`),
|
|
241
|
+
updaterServiceName: `${serviceName}-updater`,
|
|
242
|
+
updaterServiceUnitPath: join('/etc/systemd/system', `${serviceName}-updater.service`),
|
|
243
|
+
updaterTimerUnitName: `${serviceName}-updater.timer`,
|
|
244
|
+
updaterTimerUnitPath: join('/etc/systemd/system', `${serviceName}-updater.timer`),
|
|
245
|
+
serverHost,
|
|
246
|
+
serverPort,
|
|
247
|
+
githubRepo,
|
|
248
|
+
autoUpdate,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderServerEnvFile({ port, host, dataDir, filesDir, dbDir }) {
|
|
253
|
+
return [
|
|
254
|
+
`PORT=${port}`,
|
|
255
|
+
`HAPPIER_SERVER_HOST=${host}`,
|
|
256
|
+
'HAPPIER_DB_PROVIDER=sqlite',
|
|
257
|
+
'HAPPIER_FILES_BACKEND=local',
|
|
258
|
+
`HAPPIER_SERVER_LIGHT_DATA_DIR=${dataDir}`,
|
|
259
|
+
`HAPPIER_SERVER_LIGHT_FILES_DIR=${filesDir}`,
|
|
260
|
+
`HAPPIER_SERVER_LIGHT_DB_DIR=${dbDir}`,
|
|
261
|
+
'',
|
|
262
|
+
].join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function renderServerServiceUnit({ serviceName, binaryPath, envFilePath, workingDirectory, logPath }) {
|
|
266
|
+
return [
|
|
267
|
+
'[Unit]',
|
|
268
|
+
`Description=${serviceName}`,
|
|
269
|
+
'After=network-online.target',
|
|
270
|
+
'Wants=network-online.target',
|
|
271
|
+
'',
|
|
272
|
+
'[Service]',
|
|
273
|
+
'Type=simple',
|
|
274
|
+
`EnvironmentFile=${envFilePath}`,
|
|
275
|
+
`WorkingDirectory=${workingDirectory}`,
|
|
276
|
+
`ExecStart=${binaryPath}`,
|
|
277
|
+
'Restart=on-failure',
|
|
278
|
+
'RestartSec=5',
|
|
279
|
+
'LimitNOFILE=65535',
|
|
280
|
+
`StandardOutput=append:${logPath}`,
|
|
281
|
+
`StandardError=append:${logPath}`,
|
|
282
|
+
'',
|
|
283
|
+
'[Install]',
|
|
284
|
+
'WantedBy=multi-user.target',
|
|
285
|
+
'',
|
|
286
|
+
].join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function renderUpdaterServiceUnit({ updaterServiceName, hstackPath, channel }) {
|
|
290
|
+
return [
|
|
291
|
+
'[Unit]',
|
|
292
|
+
`Description=${updaterServiceName}`,
|
|
293
|
+
'After=network-online.target',
|
|
294
|
+
'',
|
|
295
|
+
'[Service]',
|
|
296
|
+
'Type=oneshot',
|
|
297
|
+
`ExecStart=${hstackPath} self-host update --channel=${channel} --non-interactive`,
|
|
298
|
+
'',
|
|
299
|
+
].join('\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderUpdaterTimerUnit({ updaterServiceName, updaterTimerName }) {
|
|
303
|
+
return [
|
|
304
|
+
'[Unit]',
|
|
305
|
+
`Description=${updaterTimerName}`,
|
|
306
|
+
'',
|
|
307
|
+
'[Timer]',
|
|
308
|
+
'OnCalendar=weekly',
|
|
309
|
+
'RandomizedDelaySec=30m',
|
|
310
|
+
'Persistent=true',
|
|
311
|
+
`Unit=${updaterServiceName}.service`,
|
|
312
|
+
'',
|
|
313
|
+
'[Install]',
|
|
314
|
+
'WantedBy=timers.target',
|
|
315
|
+
'',
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function restartAndCheckHealth({ serviceUnitName, serverPort }) {
|
|
320
|
+
runCommand('systemctl', ['restart', serviceUnitName], { stdio: 'inherit' });
|
|
321
|
+
const startedAt = Date.now();
|
|
322
|
+
while (Date.now() - startedAt < 45_000) {
|
|
323
|
+
const active = runCommand('systemctl', ['is-active', '--quiet', serviceUnitName], { allowFail: true, stdio: 'ignore' });
|
|
324
|
+
if ((active.status ?? 1) === 0) {
|
|
325
|
+
const ok = await checkHealth({ port: serverPort });
|
|
326
|
+
if (ok) return true;
|
|
327
|
+
}
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function checkHealth({ port }) {
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(`http://127.0.0.1:${port}/v1/version`, {
|
|
336
|
+
headers: { accept: 'application/json' },
|
|
337
|
+
});
|
|
338
|
+
if (!response.ok) return false;
|
|
339
|
+
const payload = await response.json().catch(() => ({}));
|
|
340
|
+
return payload?.ok === true;
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveMinisignPublicKey() {
|
|
347
|
+
const inline = String(process.env.HAPPIER_MINISIGN_PUBKEY ?? '').trim();
|
|
348
|
+
if (inline) return inline;
|
|
349
|
+
const publicKeyUrl = String(
|
|
350
|
+
process.env.HAPPIER_MINISIGN_PUBKEY_URL ?? DEFAULTS.minisignPubKeyUrl
|
|
351
|
+
).trim();
|
|
352
|
+
if (!publicKeyUrl) {
|
|
353
|
+
throw new Error('[self-host] HAPPIER_MINISIGN_PUBKEY_URL is empty');
|
|
354
|
+
}
|
|
355
|
+
const response = await fetch(publicKeyUrl, {
|
|
356
|
+
headers: {
|
|
357
|
+
'user-agent': 'happier-self-host-installer',
|
|
358
|
+
accept: 'text/plain,application/octet-stream;q=0.9,*/*;q=0.8',
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`[self-host] failed to download minisign public key (${response.status} ${response.statusText})`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const raw = String(await response.text()).trim();
|
|
367
|
+
if (!raw) {
|
|
368
|
+
throw new Error('[self-host] downloaded minisign public key was empty');
|
|
369
|
+
}
|
|
370
|
+
return raw;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function verifySignature({ checksumsPath, signatureUrl, publicKey }) {
|
|
374
|
+
if (!signatureUrl) {
|
|
375
|
+
throw new Error('[self-host] release signature URL is missing');
|
|
376
|
+
}
|
|
377
|
+
if (!publicKey) {
|
|
378
|
+
throw new Error('[self-host] minisign public key is missing');
|
|
379
|
+
}
|
|
380
|
+
if (!commandExists('minisign')) {
|
|
381
|
+
throw new Error('[self-host] minisign is required for self-host signature verification');
|
|
382
|
+
}
|
|
383
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happier-self-host-signature-'));
|
|
384
|
+
const pubKeyPath = join(tmp, 'minisign.pub');
|
|
385
|
+
const signaturePath = join(tmp, 'checksums.txt.minisig');
|
|
386
|
+
try {
|
|
387
|
+
await writeFile(pubKeyPath, `${publicKey}\n`, 'utf-8');
|
|
388
|
+
await downloadToFile(signatureUrl, signaturePath);
|
|
389
|
+
runCommand('minisign', ['-Vm', checksumsPath, '-x', signaturePath, '-p', pubKeyPath], { stdio: 'ignore' });
|
|
390
|
+
} finally {
|
|
391
|
+
await rm(tmp, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function installBinaryAtomically({ sourceBinaryPath, targetBinaryPath, previousBinaryPath, versionedTargetPath }) {
|
|
396
|
+
await mkdir(dirname(targetBinaryPath), { recursive: true });
|
|
397
|
+
await mkdir(dirname(versionedTargetPath), { recursive: true });
|
|
398
|
+
const stagedPath = `${targetBinaryPath}.new`;
|
|
399
|
+
await copyFile(sourceBinaryPath, stagedPath);
|
|
400
|
+
await chmod(stagedPath, 0o755);
|
|
401
|
+
if (existsSync(targetBinaryPath)) {
|
|
402
|
+
await copyFile(targetBinaryPath, previousBinaryPath);
|
|
403
|
+
await chmod(previousBinaryPath, 0o755);
|
|
404
|
+
}
|
|
405
|
+
await copyFile(stagedPath, versionedTargetPath);
|
|
406
|
+
await chmod(versionedTargetPath, 0o755);
|
|
407
|
+
await rm(stagedPath, { force: true });
|
|
408
|
+
await copyFile(versionedTargetPath, targetBinaryPath);
|
|
409
|
+
await chmod(targetBinaryPath, 0o755);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function installFromRelease({ product, binaryName, config, explicitBinaryPath = '' }) {
|
|
413
|
+
if (explicitBinaryPath) {
|
|
414
|
+
const srcPath = explicitBinaryPath;
|
|
415
|
+
if (!existsSync(srcPath)) {
|
|
416
|
+
throw new Error(`[self-host] missing --server-binary path: ${srcPath}`);
|
|
417
|
+
}
|
|
418
|
+
const version = `local-${Date.now()}`;
|
|
419
|
+
await installBinaryAtomically({
|
|
420
|
+
sourceBinaryPath: srcPath,
|
|
421
|
+
targetBinaryPath: config.serverBinaryPath,
|
|
422
|
+
previousBinaryPath: config.serverPreviousBinaryPath,
|
|
423
|
+
versionedTargetPath: join(config.versionsDir, `${binaryName}-${version}`),
|
|
424
|
+
});
|
|
425
|
+
return { version, source: 'local' };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const channelTag = config.channel === 'preview' ? 'server-preview' : 'server-stable';
|
|
429
|
+
const release = await readRelease(channelTag, config.githubRepo);
|
|
430
|
+
const asset = pickReleaseAsset({
|
|
431
|
+
assets: release?.assets,
|
|
432
|
+
product,
|
|
433
|
+
os: 'linux',
|
|
434
|
+
arch: normalizeArch(),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'happier-self-host-release-'));
|
|
438
|
+
try {
|
|
439
|
+
const archivePath = join(tempDir, 'artifact.tar.gz');
|
|
440
|
+
const checksumsPath = join(tempDir, 'checksums.txt');
|
|
441
|
+
await downloadToFile(asset.archiveUrl, archivePath);
|
|
442
|
+
await downloadToFile(asset.checksumsUrl, checksumsPath);
|
|
443
|
+
const checksumsMap = parseChecksums(await readFile(checksumsPath, 'utf-8'));
|
|
444
|
+
const expected = checksumsMap.get(asset.archiveName);
|
|
445
|
+
if (!expected) {
|
|
446
|
+
throw new Error(`[self-host] checksum entry missing for ${asset.archiveName}`);
|
|
447
|
+
}
|
|
448
|
+
const actual = await sha256(archivePath);
|
|
449
|
+
if (actual !== expected) {
|
|
450
|
+
throw new Error('[self-host] checksum verification failed for downloaded server artifact');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const publicKey = await resolveMinisignPublicKey();
|
|
454
|
+
await verifySignature({
|
|
455
|
+
checksumsPath,
|
|
456
|
+
signatureUrl: asset.signatureUrl,
|
|
457
|
+
publicKey,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const extractDir = join(tempDir, 'extract');
|
|
461
|
+
await mkdir(extractDir, { recursive: true });
|
|
462
|
+
runCommand('tar', ['-xzf', archivePath, '-C', extractDir], { stdio: 'ignore' });
|
|
463
|
+
const extractedBinary = await findExecutableByName(extractDir, binaryName);
|
|
464
|
+
if (!extractedBinary) {
|
|
465
|
+
throw new Error('[self-host] failed to locate extracted server binary');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const version = asset.version || String(release?.tag_name ?? '').replace(/^server-v/, '') || `${Date.now()}`;
|
|
469
|
+
await installBinaryAtomically({
|
|
470
|
+
sourceBinaryPath: extractedBinary,
|
|
471
|
+
targetBinaryPath: config.serverBinaryPath,
|
|
472
|
+
previousBinaryPath: config.serverPreviousBinaryPath,
|
|
473
|
+
versionedTargetPath: join(config.versionsDir, `${binaryName}-${version}`),
|
|
474
|
+
});
|
|
475
|
+
return { version, source: asset.archiveUrl };
|
|
476
|
+
} finally {
|
|
477
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function writeSelfHostState(config, statePatch) {
|
|
482
|
+
const existing = existsSync(config.statePath)
|
|
483
|
+
? JSON.parse(await readFile(config.statePath, 'utf-8').catch(() => '{}'))
|
|
484
|
+
: {};
|
|
485
|
+
const next = {
|
|
486
|
+
...existing,
|
|
487
|
+
...statePatch,
|
|
488
|
+
updatedAt: new Date().toISOString(),
|
|
489
|
+
};
|
|
490
|
+
await mkdir(dirname(config.statePath), { recursive: true });
|
|
491
|
+
await writeFile(config.statePath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function maybeInstallCompanionCli({ channel, nonInteractive, withCli }) {
|
|
495
|
+
if (!withCli) return { installed: false, reason: 'disabled' };
|
|
496
|
+
if (commandExists('happier')) {
|
|
497
|
+
return { installed: false, reason: 'already-installed' };
|
|
498
|
+
}
|
|
499
|
+
if (!commandExists('curl') || !commandExists('bash')) {
|
|
500
|
+
return { installed: false, reason: 'missing-curl-or-bash' };
|
|
501
|
+
}
|
|
502
|
+
const result = runCommand(
|
|
503
|
+
'bash',
|
|
504
|
+
['-lc', 'curl -fsSL https://happier.dev/install | bash'],
|
|
505
|
+
{
|
|
506
|
+
allowFail: true,
|
|
507
|
+
env: {
|
|
508
|
+
...process.env,
|
|
509
|
+
HAPPIER_CHANNEL: channel,
|
|
510
|
+
HAPPIER_NONINTERACTIVE: nonInteractive ? '1' : '0',
|
|
511
|
+
},
|
|
512
|
+
stdio: 'inherit',
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
return {
|
|
516
|
+
installed: (result.status ?? 1) === 0,
|
|
517
|
+
reason: (result.status ?? 1) === 0 ? 'installed' : 'installer-failed',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function cmdInstall({ channel, argv, json }) {
|
|
522
|
+
assertLinux();
|
|
523
|
+
assertRoot();
|
|
524
|
+
if (!commandExists('systemctl')) {
|
|
525
|
+
throw new Error('[self-host] systemctl is required');
|
|
526
|
+
}
|
|
527
|
+
if (!commandExists('tar')) {
|
|
528
|
+
throw new Error('[self-host] tar is required');
|
|
529
|
+
}
|
|
530
|
+
const config = resolveConfig({ channel });
|
|
531
|
+
const withoutCli = argv.includes('--without-cli') || parseBoolean(process.env.HAPPIER_WITH_CLI, true) === false;
|
|
532
|
+
const nonInteractive = argv.includes('--non-interactive') || parseBoolean(process.env.HAPPIER_NONINTERACTIVE, false);
|
|
533
|
+
const serverBinaryOverride = String(process.env.HAPPIER_SELF_HOST_SERVER_BINARY ?? '').trim();
|
|
534
|
+
|
|
535
|
+
await mkdir(config.installRoot, { recursive: true });
|
|
536
|
+
await mkdir(config.installBinDir, { recursive: true });
|
|
537
|
+
await mkdir(config.versionsDir, { recursive: true });
|
|
538
|
+
await mkdir(config.configDir, { recursive: true });
|
|
539
|
+
await mkdir(config.dataDir, { recursive: true });
|
|
540
|
+
await mkdir(config.filesDir, { recursive: true });
|
|
541
|
+
await mkdir(config.dbDir, { recursive: true });
|
|
542
|
+
await mkdir(config.logDir, { recursive: true });
|
|
543
|
+
|
|
544
|
+
const installResult = await installFromRelease({
|
|
545
|
+
product: 'happier-server',
|
|
546
|
+
binaryName: 'happier-server',
|
|
547
|
+
config,
|
|
548
|
+
explicitBinaryPath: serverBinaryOverride,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await writeFile(
|
|
552
|
+
config.configEnvPath,
|
|
553
|
+
renderServerEnvFile({
|
|
554
|
+
port: config.serverPort,
|
|
555
|
+
host: config.serverHost,
|
|
556
|
+
dataDir: config.dataDir,
|
|
557
|
+
filesDir: config.filesDir,
|
|
558
|
+
dbDir: config.dbDir,
|
|
559
|
+
}),
|
|
560
|
+
'utf-8'
|
|
561
|
+
);
|
|
562
|
+
await writeFile(
|
|
563
|
+
config.serviceUnitPath,
|
|
564
|
+
renderServerServiceUnit({
|
|
565
|
+
serviceName: config.serviceName,
|
|
566
|
+
binaryPath: config.serverBinaryPath,
|
|
567
|
+
envFilePath: config.configEnvPath,
|
|
568
|
+
workingDirectory: config.installRoot,
|
|
569
|
+
logPath: config.serverLogPath,
|
|
570
|
+
}),
|
|
571
|
+
'utf-8'
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const hstackPath = existsSync(join(config.binDir, 'hstack'))
|
|
575
|
+
? join(config.binDir, 'hstack')
|
|
576
|
+
: join(config.installRoot, 'bin', 'hstack');
|
|
577
|
+
await writeFile(
|
|
578
|
+
config.updaterServiceUnitPath,
|
|
579
|
+
renderUpdaterServiceUnit({
|
|
580
|
+
updaterServiceName: config.updaterServiceName,
|
|
581
|
+
hstackPath,
|
|
582
|
+
channel,
|
|
583
|
+
}),
|
|
584
|
+
'utf-8'
|
|
585
|
+
);
|
|
586
|
+
await writeFile(
|
|
587
|
+
config.updaterTimerUnitPath,
|
|
588
|
+
renderUpdaterTimerUnit({
|
|
589
|
+
updaterServiceName: config.updaterServiceName,
|
|
590
|
+
updaterTimerName: config.updaterTimerUnitName,
|
|
591
|
+
}),
|
|
592
|
+
'utf-8'
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const serverShimPath = join(config.binDir, 'happier-server');
|
|
596
|
+
await mkdir(config.binDir, { recursive: true });
|
|
597
|
+
await rm(serverShimPath, { force: true });
|
|
598
|
+
await symlink(config.serverBinaryPath, serverShimPath).catch(async () => {
|
|
599
|
+
await copyFile(config.serverBinaryPath, serverShimPath);
|
|
600
|
+
await chmod(serverShimPath, 0o755);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
runCommand('systemctl', ['daemon-reload'], { stdio: 'inherit' });
|
|
604
|
+
runCommand('systemctl', ['enable', '--now', config.serviceUnitName], { stdio: 'inherit' });
|
|
605
|
+
|
|
606
|
+
if (config.autoUpdate) {
|
|
607
|
+
runCommand('systemctl', ['enable', '--now', config.updaterTimerUnitName], { allowFail: true, stdio: 'inherit' });
|
|
608
|
+
} else {
|
|
609
|
+
runCommand('systemctl', ['disable', '--now', config.updaterTimerUnitName], { allowFail: true, stdio: 'ignore' });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const healthy = await restartAndCheckHealth({ serviceUnitName: config.serviceUnitName, serverPort: config.serverPort });
|
|
613
|
+
if (!healthy) {
|
|
614
|
+
throw new Error('[self-host] service failed health checks after install');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const cliResult = await maybeInstallCompanionCli({
|
|
618
|
+
channel,
|
|
619
|
+
nonInteractive,
|
|
620
|
+
withCli: !withoutCli,
|
|
621
|
+
});
|
|
622
|
+
await writeSelfHostState(config, {
|
|
623
|
+
channel,
|
|
624
|
+
version: installResult.version,
|
|
625
|
+
source: installResult.source,
|
|
626
|
+
autoUpdate: config.autoUpdate,
|
|
627
|
+
withCli: !withoutCli,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
printResult({
|
|
631
|
+
json,
|
|
632
|
+
data: {
|
|
633
|
+
ok: true,
|
|
634
|
+
channel,
|
|
635
|
+
version: installResult.version,
|
|
636
|
+
service: config.serviceUnitName,
|
|
637
|
+
serverPort: config.serverPort,
|
|
638
|
+
cli: cliResult,
|
|
639
|
+
},
|
|
640
|
+
text: [
|
|
641
|
+
`${green('✓')} Happier Self-Host installed`,
|
|
642
|
+
`- service: ${cyan(config.serviceUnitName)}`,
|
|
643
|
+
`- version: ${cyan(installResult.version || 'unknown')}`,
|
|
644
|
+
`- server: ${cyan(`http://127.0.0.1:${config.serverPort}`)}`,
|
|
645
|
+
`- cli: ${cliResult.installed ? green('installed') : dim(cliResult.reason)}`,
|
|
646
|
+
].join('\n'),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function cmdStatus({ channel, json }) {
|
|
651
|
+
assertLinux();
|
|
652
|
+
const config = resolveConfig({ channel });
|
|
653
|
+
const serviceState = runCommand('systemctl', ['is-active', config.serviceUnitName], { allowFail: true, stdio: 'pipe' });
|
|
654
|
+
const active = (serviceState.status ?? 1) === 0;
|
|
655
|
+
const enabledState = runCommand('systemctl', ['is-enabled', config.serviceUnitName], { allowFail: true, stdio: 'pipe' });
|
|
656
|
+
const enabled = (enabledState.status ?? 1) === 0;
|
|
657
|
+
const healthy = active ? await checkHealth({ port: config.serverPort }) : false;
|
|
658
|
+
const state = existsSync(config.statePath)
|
|
659
|
+
? JSON.parse(await readFile(config.statePath, 'utf-8').catch(() => '{}'))
|
|
660
|
+
: {};
|
|
661
|
+
|
|
662
|
+
printResult({
|
|
663
|
+
json,
|
|
664
|
+
data: {
|
|
665
|
+
ok: true,
|
|
666
|
+
channel,
|
|
667
|
+
service: {
|
|
668
|
+
name: config.serviceUnitName,
|
|
669
|
+
active,
|
|
670
|
+
enabled,
|
|
671
|
+
},
|
|
672
|
+
healthy,
|
|
673
|
+
state,
|
|
674
|
+
},
|
|
675
|
+
text: [
|
|
676
|
+
`${cyan('service')}: ${config.serviceUnitName}`,
|
|
677
|
+
`${cyan('active')}: ${active ? green('yes') : yellow('no')}`,
|
|
678
|
+
`${cyan('enabled')}: ${enabled ? green('yes') : yellow('no')}`,
|
|
679
|
+
`${cyan('health')}: ${healthy ? green('ok') : yellow('failed')}`,
|
|
680
|
+
state?.version ? `${cyan('version')}: ${state.version}` : null,
|
|
681
|
+
].filter(Boolean).join('\n'),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function cmdUpdate({ channel, json }) {
|
|
686
|
+
assertLinux();
|
|
687
|
+
assertRoot();
|
|
688
|
+
const config = resolveConfig({ channel });
|
|
689
|
+
const installResult = await installFromRelease({
|
|
690
|
+
product: 'happier-server',
|
|
691
|
+
binaryName: 'happier-server',
|
|
692
|
+
config,
|
|
693
|
+
});
|
|
694
|
+
const healthy = await restartAndCheckHealth({ serviceUnitName: config.serviceUnitName, serverPort: config.serverPort });
|
|
695
|
+
if (!healthy) {
|
|
696
|
+
if (existsSync(config.serverPreviousBinaryPath)) {
|
|
697
|
+
await copyFile(config.serverPreviousBinaryPath, config.serverBinaryPath);
|
|
698
|
+
await chmod(config.serverBinaryPath, 0o755);
|
|
699
|
+
await restartAndCheckHealth({ serviceUnitName: config.serviceUnitName, serverPort: config.serverPort });
|
|
700
|
+
}
|
|
701
|
+
throw new Error('[self-host] update failed health checks and was rolled back to previous binary');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
await writeSelfHostState(config, {
|
|
705
|
+
channel,
|
|
706
|
+
version: installResult.version,
|
|
707
|
+
source: installResult.source,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
printResult({
|
|
711
|
+
json,
|
|
712
|
+
data: { ok: true, version: installResult.version, service: config.serviceUnitName },
|
|
713
|
+
text: `${green('✓')} updated self-host runtime to ${cyan(installResult.version || 'latest')}`,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function parseRollbackVersion(argv) {
|
|
718
|
+
const { kv } = parseArgs(argv);
|
|
719
|
+
const fromEq = String(kv.get('--to') ?? '').trim();
|
|
720
|
+
if (fromEq) return fromEq;
|
|
721
|
+
const idx = argv.indexOf('--to');
|
|
722
|
+
if (idx >= 0 && argv[idx + 1]) return String(argv[idx + 1]).trim();
|
|
723
|
+
return '';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function cmdRollback({ channel, argv, json }) {
|
|
727
|
+
assertLinux();
|
|
728
|
+
assertRoot();
|
|
729
|
+
const config = resolveConfig({ channel });
|
|
730
|
+
const to = parseRollbackVersion(argv);
|
|
731
|
+
const target = to
|
|
732
|
+
? join(config.versionsDir, `happier-server-${to}`)
|
|
733
|
+
: config.serverPreviousBinaryPath;
|
|
734
|
+
if (!existsSync(target)) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
to
|
|
737
|
+
? `[self-host] rollback target version not found: ${to}`
|
|
738
|
+
: '[self-host] no previous binary is available for rollback'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
await copyFile(target, config.serverBinaryPath);
|
|
742
|
+
await chmod(config.serverBinaryPath, 0o755);
|
|
743
|
+
const healthy = await restartAndCheckHealth({ serviceUnitName: config.serviceUnitName, serverPort: config.serverPort });
|
|
744
|
+
if (!healthy) {
|
|
745
|
+
throw new Error('[self-host] rollback completed binary swap but health checks failed');
|
|
746
|
+
}
|
|
747
|
+
await writeSelfHostState(config, {
|
|
748
|
+
channel,
|
|
749
|
+
version: to || 'previous',
|
|
750
|
+
rolledBackAt: new Date().toISOString(),
|
|
751
|
+
});
|
|
752
|
+
printResult({
|
|
753
|
+
json,
|
|
754
|
+
data: { ok: true, version: to || 'previous' },
|
|
755
|
+
text: `${green('✓')} rollback completed (${cyan(to || 'previous')})`,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function cmdUninstall({ channel, argv, json }) {
|
|
760
|
+
assertLinux();
|
|
761
|
+
assertRoot();
|
|
762
|
+
const config = resolveConfig({ channel });
|
|
763
|
+
const purgeData = argv.includes('--purge-data');
|
|
764
|
+
const yes = argv.includes('--yes') || parseBoolean(process.env.HAPPIER_NONINTERACTIVE, false);
|
|
765
|
+
if (!yes) {
|
|
766
|
+
throw new Error('[self-host] uninstall requires --yes (or HAPPIER_NONINTERACTIVE=1)');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
runCommand('systemctl', ['disable', '--now', config.serviceUnitName], { allowFail: true, stdio: 'ignore' });
|
|
770
|
+
runCommand('systemctl', ['disable', '--now', config.updaterTimerUnitName], { allowFail: true, stdio: 'ignore' });
|
|
771
|
+
|
|
772
|
+
await rm(config.serviceUnitPath, { force: true });
|
|
773
|
+
await rm(config.updaterServiceUnitPath, { force: true });
|
|
774
|
+
await rm(config.updaterTimerUnitPath, { force: true });
|
|
775
|
+
runCommand('systemctl', ['daemon-reload'], { allowFail: true, stdio: 'ignore' });
|
|
776
|
+
|
|
777
|
+
await rm(config.serverBinaryPath, { force: true });
|
|
778
|
+
await rm(config.serverPreviousBinaryPath, { force: true });
|
|
779
|
+
await rm(join(config.binDir, 'happier-server'), { force: true });
|
|
780
|
+
await rm(config.statePath, { force: true });
|
|
781
|
+
|
|
782
|
+
if (purgeData) {
|
|
783
|
+
await rm(config.installRoot, { recursive: true, force: true });
|
|
784
|
+
await rm(config.configDir, { recursive: true, force: true });
|
|
785
|
+
await rm(config.dataDir, { recursive: true, force: true });
|
|
786
|
+
await rm(config.logDir, { recursive: true, force: true });
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
printResult({
|
|
790
|
+
json,
|
|
791
|
+
data: { ok: true, purgeData },
|
|
792
|
+
text: `${green('✓')} self-host uninstalled${purgeData ? ' (data purged)' : ''}`,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function cmdDoctor({ channel, json }) {
|
|
797
|
+
const config = resolveConfig({ channel });
|
|
798
|
+
const checks = [
|
|
799
|
+
{ name: 'linux', ok: process.platform === 'linux' },
|
|
800
|
+
{ name: 'systemctl', ok: commandExists('systemctl') },
|
|
801
|
+
{ name: 'curl', ok: commandExists('curl') },
|
|
802
|
+
{ name: 'tar', ok: commandExists('tar') },
|
|
803
|
+
{ name: 'server-binary', ok: existsSync(config.serverBinaryPath) },
|
|
804
|
+
{ name: 'service-unit', ok: existsSync(config.serviceUnitPath) },
|
|
805
|
+
];
|
|
806
|
+
const ok = checks.every((check) => check.ok);
|
|
807
|
+
printResult({
|
|
808
|
+
json,
|
|
809
|
+
data: { ok, checks },
|
|
810
|
+
text: [
|
|
811
|
+
banner('self-host doctor', { subtitle: 'Self-host diagnostics.' }),
|
|
812
|
+
'',
|
|
813
|
+
...checks.map((check) => `${check.ok ? green('✓') : yellow('!')} ${check.name}`),
|
|
814
|
+
].join('\n'),
|
|
815
|
+
});
|
|
816
|
+
if (!ok) {
|
|
817
|
+
process.exitCode = 1;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export function usageText() {
|
|
822
|
+
return [
|
|
823
|
+
banner('self-host', { subtitle: 'Happier Self-Host guided installation flow.' }),
|
|
824
|
+
'',
|
|
825
|
+
sectionTitle('usage:'),
|
|
826
|
+
` ${cyan('hstack self-host')} install [--without-cli] [--channel=stable|preview] [--non-interactive] [--json]`,
|
|
827
|
+
` ${cyan('hstack self-host')} status [--channel=stable|preview] [--json]`,
|
|
828
|
+
` ${cyan('hstack self-host')} update [--channel=stable|preview] [--json]`,
|
|
829
|
+
` ${cyan('hstack self-host')} rollback [--to=<version>] [--channel=stable|preview] [--json]`,
|
|
830
|
+
` ${cyan('hstack self-host')} uninstall [--purge-data] [--yes] [--json]`,
|
|
831
|
+
` ${cyan('hstack self-host')} doctor [--json]`,
|
|
832
|
+
'',
|
|
833
|
+
sectionTitle('notes:'),
|
|
834
|
+
'- works without a repository checkout (binary-safe flow).',
|
|
835
|
+
`- runtime paths are configurable via env vars (${dim('HAPPIER_SELF_HOST_*')}).`,
|
|
836
|
+
].join('\n');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export async function runSelfHostCli(argv = process.argv.slice(2)) {
|
|
840
|
+
const parsed = parseSelfHostInvocation(argv);
|
|
841
|
+
const { flags, kv } = parseArgs(argv);
|
|
842
|
+
const json = wantsJson(argv, { flags });
|
|
843
|
+
const channel = normalizeChannel(String(kv.get('--channel') ?? process.env.HAPPIER_CHANNEL ?? 'stable'));
|
|
844
|
+
|
|
845
|
+
if (wantsHelp(argv, { flags }) || parsed.subcommand === 'help') {
|
|
846
|
+
printResult({
|
|
847
|
+
json,
|
|
848
|
+
data: {
|
|
849
|
+
ok: true,
|
|
850
|
+
commands: ['install', 'status', 'update', 'rollback', 'uninstall', 'doctor'],
|
|
851
|
+
},
|
|
852
|
+
text: usageText(),
|
|
853
|
+
});
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (parsed.subcommand === 'install') {
|
|
858
|
+
await cmdInstall({ channel, argv: parsed.rest, json });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (parsed.subcommand === 'status') {
|
|
862
|
+
await cmdStatus({ channel, json });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (parsed.subcommand === 'update') {
|
|
866
|
+
await cmdUpdate({ channel, json });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (parsed.subcommand === 'rollback') {
|
|
870
|
+
await cmdRollback({ channel, argv: parsed.rest, json });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (parsed.subcommand === 'uninstall') {
|
|
874
|
+
await cmdUninstall({ channel, argv: parsed.rest, json });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (parsed.subcommand === 'doctor' || parsed.subcommand === 'migrate-from-npm') {
|
|
878
|
+
await cmdDoctor({ channel, json });
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
throw new Error(`[self-host] unknown command: ${parsed.subcommand}`);
|
|
883
|
+
}
|