@happier-dev/stack 0.1.0-preview.74.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 +138 -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 +74 -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,1324 @@
|
|
|
1
|
+
import './utils/env/env.mjs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parseArgs } from './utils/cli/args.mjs';
|
|
6
|
+
import { printResult, wantsHelp, wantsJson } from './utils/cli/cli.mjs';
|
|
7
|
+
import { getDefaultAutostartPaths, getHappyStacksHomeDir, getRepoDir, getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
8
|
+
import { isTty, prompt, promptSelect, withRl } from './utils/cli/wizard.mjs';
|
|
9
|
+
import { getCanonicalHomeDir } from './utils/env/config.mjs';
|
|
10
|
+
import { ensureEnvLocalUpdated } from './utils/env/env_local.mjs';
|
|
11
|
+
import { run, runCapture } from './utils/proc/proc.mjs';
|
|
12
|
+
import { waitForHappierHealthOk } from './utils/server/server.mjs';
|
|
13
|
+
import { tailscaleServeEnable, tailscaleServeHttpsUrlForInternalServerUrl } from './tailscale.mjs';
|
|
14
|
+
import { getRuntimeDir } from './utils/paths/runtime.mjs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { installService } from './service.mjs';
|
|
17
|
+
import { getDevAuthKeyPath } from './utils/auth/dev_key.mjs';
|
|
18
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './utils/env/sandbox.mjs';
|
|
19
|
+
import { boolFromFlags, boolFromFlagsOrKv } from './utils/cli/flags.mjs';
|
|
20
|
+
import { normalizeProfile, normalizeServerComponent } from './utils/cli/normalize.mjs';
|
|
21
|
+
import { openUrlInBrowser } from './utils/ui/browser.mjs';
|
|
22
|
+
import { commandExists } from './utils/proc/commands.mjs';
|
|
23
|
+
import { readServerPortFromEnvFile, resolveServerPortFromEnv } from './utils/server/port.mjs';
|
|
24
|
+
import { runOrchestratedGuidedAuthFlow } from './utils/auth/orchestrated_stack_auth_flow.mjs';
|
|
25
|
+
import { getVerbosityLevel } from './utils/cli/verbosity.mjs';
|
|
26
|
+
import { runCommandLogged } from './utils/cli/progress.mjs';
|
|
27
|
+
import { bold, cyan, dim, green, yellow } from './utils/ui/ansi.mjs';
|
|
28
|
+
import { expandHome } from './utils/paths/canonical_home.mjs';
|
|
29
|
+
import { listAllStackNames } from './utils/stack/stacks.mjs';
|
|
30
|
+
import { detectSwiftbarPluginInstalled } from './utils/menubar/swiftbar.mjs';
|
|
31
|
+
import { banner, bullets, cmd as cmdFmt, kv, sectionTitle } from './utils/ui/layout.mjs';
|
|
32
|
+
import { applyBindModeToEnv, resolveBindModeFromArgs } from './utils/net/bind_mode.mjs';
|
|
33
|
+
import { ensureDevCheckout } from './utils/git/dev_checkout.mjs';
|
|
34
|
+
import { parseGithubOwnerRepo } from './utils/git/worktrees.mjs';
|
|
35
|
+
import { findAnyCredentialPathInCliHome, findExistingStackCredentialPath } from './utils/auth/credentials_paths.mjs';
|
|
36
|
+
|
|
37
|
+
function resolveWorkspaceDirDefault() {
|
|
38
|
+
const explicit = (process.env.HAPPIER_STACK_WORKSPACE_DIR ?? '').toString().trim();
|
|
39
|
+
if (explicit) return expandHome(explicit);
|
|
40
|
+
return join(getHappyStacksHomeDir(process.env), 'workspace');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeWorkspaceDirInput(raw, { homeDir }) {
|
|
44
|
+
const trimmed = String(raw ?? '').trim();
|
|
45
|
+
const expanded = expandHome(trimmed);
|
|
46
|
+
if (!expanded) return '';
|
|
47
|
+
// If relative, treat it as relative to the home dir (same rule as init.mjs).
|
|
48
|
+
return expanded.startsWith('/') ? expanded : join(homeDir, expanded);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resolveMainServerPort() {
|
|
52
|
+
// Priority:
|
|
53
|
+
// - explicit env var
|
|
54
|
+
// - main stack env file (preferred)
|
|
55
|
+
// - default
|
|
56
|
+
const hasEnvOverride =
|
|
57
|
+
(process.env.HAPPIER_STACK_SERVER_PORT ?? '').toString().trim() !== '';
|
|
58
|
+
if (hasEnvOverride) {
|
|
59
|
+
return resolveServerPortFromEnv({ env: process.env, defaultPort: 3005 });
|
|
60
|
+
}
|
|
61
|
+
const envPath = resolveStackEnvPath('main').envPath;
|
|
62
|
+
return await readServerPortFromEnvFile(envPath, { defaultPort: 3005 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeGithubRepoUrl(raw) {
|
|
66
|
+
const v = String(raw ?? '').trim();
|
|
67
|
+
if (!v) return '';
|
|
68
|
+
|
|
69
|
+
// Accept full URLs and ssh URLs as-is.
|
|
70
|
+
if (v.includes('://') || v.startsWith('git@')) return v;
|
|
71
|
+
|
|
72
|
+
// Convenience: owner/repo -> https://github.com/owner/repo.git
|
|
73
|
+
const m = v.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
74
|
+
if (m) {
|
|
75
|
+
const owner = m[1];
|
|
76
|
+
const repo = m[2];
|
|
77
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback: let git try to interpret it (could be a local path).
|
|
81
|
+
return v;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function parseRemoteOwnerRepo({ repoDir, remoteName }) {
|
|
85
|
+
try {
|
|
86
|
+
const url = (await runCapture('git', ['remote', 'get-url', remoteName], { cwd: repoDir })).trim();
|
|
87
|
+
return parseGithubOwnerRepo(url);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function canPushToRemote({ repoDir, remoteName }) {
|
|
94
|
+
try {
|
|
95
|
+
// This checks for push credentials + authorization without creating anything.
|
|
96
|
+
await runCapture('git', ['push', '--dry-run', remoteName, 'HEAD:refs/heads/hstack-permission-check'], { cwd: repoDir });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function maybeConfigureForkRemoteForDevProfile({ rootDir, interactive }) {
|
|
104
|
+
const repoDir = getRepoDir(rootDir, process.env);
|
|
105
|
+
if (!existsSync(join(repoDir, '.git'))) return;
|
|
106
|
+
|
|
107
|
+
const upstream = await parseRemoteOwnerRepo({ repoDir, remoteName: 'upstream' });
|
|
108
|
+
const origin = await parseRemoteOwnerRepo({ repoDir, remoteName: 'origin' });
|
|
109
|
+
if (!upstream?.owner || !upstream?.repo || !origin?.owner || !origin?.repo) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const originIsUpstream = upstream.owner === origin.owner && upstream.repo === origin.repo;
|
|
114
|
+
if (!originIsUpstream) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// origin points at upstream and the user can't push: they almost certainly need a fork.
|
|
119
|
+
const canPushOrigin = await canPushToRemote({ repoDir, remoteName: 'origin' });
|
|
120
|
+
if (canPushOrigin) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const forkUrlFromEnv = String(process.env.HAPPIER_STACK_FORK_URL ?? '').trim();
|
|
125
|
+
if (!interactive) {
|
|
126
|
+
if (forkUrlFromEnv) {
|
|
127
|
+
await run('git', ['remote', 'set-url', 'origin', forkUrlFromEnv], { cwd: repoDir });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.log(
|
|
132
|
+
`${yellow('!')} Dev setup: origin points to upstream (${upstream.owner}/${upstream.repo}), but you don't have push access.\n` +
|
|
133
|
+
`${dim('Fix:')} re-run ${cyan('hstack setup --profile=dev')} in a TTY to configure a fork, or set ${cyan('HAPPIER_STACK_FORK_URL')} to your fork repo URL.`
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const configureFork = await withRl(async (rl) => {
|
|
139
|
+
return await promptSelect(rl, {
|
|
140
|
+
title:
|
|
141
|
+
`${bold('GitHub fork')}\n` +
|
|
142
|
+
`${dim('To contribute, you usually need a fork to push branches. Configure your fork as `origin` now?')}`,
|
|
143
|
+
options: [
|
|
144
|
+
{ label: `yes ${dim('(recommended)')}`, value: true },
|
|
145
|
+
{ label: 'no (skip)', value: false },
|
|
146
|
+
],
|
|
147
|
+
defaultIndex: 0,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
if (!configureFork) return;
|
|
151
|
+
|
|
152
|
+
// Best-effort open the upstream repo page to make forking easy.
|
|
153
|
+
await openUrlInBrowser(`https://github.com/${upstream.owner}/${upstream.repo}`).catch(() => {});
|
|
154
|
+
|
|
155
|
+
const forkUrl = await withRl(async (rl) => {
|
|
156
|
+
return (await prompt(rl, 'Paste your fork repo URL (https or ssh): ', { defaultValue: forkUrlFromEnv })).trim();
|
|
157
|
+
});
|
|
158
|
+
if (!forkUrl) {
|
|
159
|
+
throw new Error('[setup] missing fork URL.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await run('git', ['remote', 'set-url', 'origin', forkUrl], { cwd: repoDir });
|
|
163
|
+
|
|
164
|
+
// Optional: ensure the fork has a dev branch matching upstream/dev (best-effort).
|
|
165
|
+
const devBranch = String(process.env.HAPPIER_STACK_DEV_BRANCH ?? '').trim() || 'dev';
|
|
166
|
+
let forkHasDev = true;
|
|
167
|
+
try {
|
|
168
|
+
await runCapture('git', ['ls-remote', '--exit-code', '--heads', 'origin', devBranch], { cwd: repoDir });
|
|
169
|
+
} catch {
|
|
170
|
+
forkHasDev = false;
|
|
171
|
+
}
|
|
172
|
+
if (!forkHasDev) {
|
|
173
|
+
try {
|
|
174
|
+
await run('git', ['fetch', 'upstream', devBranch], { cwd: repoDir });
|
|
175
|
+
await run('git', ['push', 'origin', `refs/remotes/upstream/${devBranch}:refs/heads/${devBranch}`], { cwd: repoDir });
|
|
176
|
+
} catch {
|
|
177
|
+
// ignore: user can still contribute via feature branches
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function ensureSetupConfigPersisted({ rootDir, profile, serverComponent, tailscaleWanted, menubarMode, happierRepoUrl }) {
|
|
183
|
+
// Repo source here describes where we clone the Happier monorepo from (apps/ui + apps/cli + apps/server).
|
|
184
|
+
const repoSourceForProfile = profile === 'selfhost' ? 'upstream' : null;
|
|
185
|
+
const monoRepo = String(happierRepoUrl ?? '').trim();
|
|
186
|
+
const updates = [
|
|
187
|
+
{ key: 'HAPPIER_STACK_SERVER_COMPONENT', value: serverComponent },
|
|
188
|
+
// Default for selfhost:
|
|
189
|
+
// - monorepo: upstream (Happier)
|
|
190
|
+
// - server-light: fork-only today (handled in bootstrap)
|
|
191
|
+
...(repoSourceForProfile
|
|
192
|
+
? [
|
|
193
|
+
{ key: 'HAPPIER_STACK_REPO_SOURCE', value: repoSourceForProfile },
|
|
194
|
+
]
|
|
195
|
+
: []),
|
|
196
|
+
...(monoRepo
|
|
197
|
+
? [
|
|
198
|
+
// Override the Happier monorepo clone source.
|
|
199
|
+
// This is useful for forks that keep the same monorepo layout under a different repo name.
|
|
200
|
+
{ key: 'HAPPIER_STACK_REPO_URL', value: monoRepo },
|
|
201
|
+
]
|
|
202
|
+
: []),
|
|
203
|
+
{ key: 'HAPPIER_STACK_MENUBAR_MODE', value: menubarMode },
|
|
204
|
+
...(tailscaleWanted
|
|
205
|
+
? [
|
|
206
|
+
{ key: 'HAPPIER_STACK_TAILSCALE_SERVE', value: '1' },
|
|
207
|
+
]
|
|
208
|
+
: []),
|
|
209
|
+
];
|
|
210
|
+
await ensureEnvLocalUpdated({ rootDir, updates });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function ensureSystemdAvailable() {
|
|
214
|
+
if (process.platform !== 'linux') return true;
|
|
215
|
+
return (await commandExists('systemctl')) && (await commandExists('journalctl'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function detectDockerSupport() {
|
|
219
|
+
const installed = await commandExists('docker');
|
|
220
|
+
if (!installed) return { installed: false, running: false };
|
|
221
|
+
try {
|
|
222
|
+
// `docker info` returns non-zero quickly when the daemon isn't running.
|
|
223
|
+
await runCapture('docker', ['info'], { timeoutMs: 2500 });
|
|
224
|
+
return { installed: true, running: true };
|
|
225
|
+
} catch {
|
|
226
|
+
return { installed: true, running: false };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function detectGitSupport() {
|
|
231
|
+
return await commandExists('git');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function detectTailscaleSupport() {
|
|
235
|
+
const installed = await commandExists('tailscale');
|
|
236
|
+
return { installed };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSwiftbarAppInstalled() {
|
|
240
|
+
if (process.platform !== 'darwin') return false;
|
|
241
|
+
// Best-effort: not exhaustive, but catches the common case.
|
|
242
|
+
return existsSync('/Applications/SwiftBar.app');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function detectIosDevTools() {
|
|
246
|
+
if (process.platform !== 'darwin') return { ok: false, hasXcode: false, hasCocoapods: false };
|
|
247
|
+
const hasXcode = await commandExists('xcodebuild');
|
|
248
|
+
const hasCocoapods = await commandExists('pod');
|
|
249
|
+
return { ok: hasXcode && hasCocoapods, hasXcode, hasCocoapods };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function runSetupPreflight({ profile, serverComponent, tailscaleWanted, menubarWanted, autostartWanted }) {
|
|
253
|
+
// Fail-fast on the truly required bits (so we don't get halfway through and crash).
|
|
254
|
+
const gitOk = await detectGitSupport();
|
|
255
|
+
if (!gitOk) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`[setup] missing prerequisite: git\n` +
|
|
258
|
+
`hstack needs git to clone/update the Happier repo.\n` +
|
|
259
|
+
`Fix: install git, then re-run setup.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sandboxed = isSandboxed();
|
|
264
|
+
const allowGlobal = sandboxAllowsGlobalSideEffects();
|
|
265
|
+
|
|
266
|
+
const docker = profile === 'selfhost' ? await detectDockerSupport() : { installed: false, running: false };
|
|
267
|
+
const tailscale = tailscaleWanted ? await detectTailscaleSupport() : { installed: false };
|
|
268
|
+
const ios = profile === 'dev' ? await detectIosDevTools() : { ok: false, hasXcode: false, hasCocoapods: false };
|
|
269
|
+
|
|
270
|
+
const canInstallAutostart = autostartWanted && (!sandboxed || allowGlobal);
|
|
271
|
+
const canInstallMenubar = menubarWanted && process.platform === 'darwin' && (!sandboxed || allowGlobal);
|
|
272
|
+
const canEnableTailscale = tailscaleWanted && tailscale.installed && (!sandboxed || allowGlobal);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
gitOk,
|
|
276
|
+
docker,
|
|
277
|
+
tailscale,
|
|
278
|
+
ios,
|
|
279
|
+
sandboxed,
|
|
280
|
+
allowGlobal,
|
|
281
|
+
canInstallAutostart,
|
|
282
|
+
canInstallMenubar,
|
|
283
|
+
canEnableTailscale,
|
|
284
|
+
swiftbarAppInstalled: menubarWanted ? isSwiftbarAppInstalled() : null,
|
|
285
|
+
serverComponent,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runNodeScript({ rootDir, rel, args = [], env = process.env }) {
|
|
290
|
+
await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function spawnDetachedNodeScript({ rootDir, rel, args = [], env = process.env }) {
|
|
294
|
+
const child = spawn(process.execPath, [join(rootDir, rel), ...args], {
|
|
295
|
+
cwd: rootDir,
|
|
296
|
+
env,
|
|
297
|
+
stdio: 'ignore',
|
|
298
|
+
detached: process.platform !== 'win32',
|
|
299
|
+
});
|
|
300
|
+
child.unref();
|
|
301
|
+
return child.pid;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function mainCliHomeDirForEnvPath(envPath) {
|
|
305
|
+
const { baseDir } = resolveStackEnvPath('main');
|
|
306
|
+
// Prefer stack base dir; envPath is informational and can be legacy/new.
|
|
307
|
+
return join(baseDir, 'cli');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getMainStacksAccessKeyPath() {
|
|
311
|
+
const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
|
|
312
|
+
return findAnyCredentialPathInCliHome({ cliHomeDir });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function getDevAuthStackAccessKeyPath(stackName = 'dev-auth') {
|
|
316
|
+
const { baseDir, envPath } = resolveStackEnvPath(stackName);
|
|
317
|
+
if (!existsSync(envPath)) return null;
|
|
318
|
+
return findAnyCredentialPathInCliHome({ cliHomeDir: join(baseDir, 'cli') });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function detectAuthSources() {
|
|
322
|
+
const devKeyPath = getDevAuthKeyPath();
|
|
323
|
+
const mainAccessKeyPath = getMainStacksAccessKeyPath();
|
|
324
|
+
const devAuthAccessKeyPath = getDevAuthStackAccessKeyPath('dev-auth');
|
|
325
|
+
return {
|
|
326
|
+
devKeyPath,
|
|
327
|
+
hasDevKey: existsSync(devKeyPath),
|
|
328
|
+
mainAccessKeyPath,
|
|
329
|
+
hasMainAccessKey: existsSync(mainAccessKeyPath),
|
|
330
|
+
devAuthAccessKeyPath,
|
|
331
|
+
hasDevAuthAccessKey: Boolean(devAuthAccessKeyPath && existsSync(devAuthAccessKeyPath)),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function maybeConfigureAuthDefaults({ rootDir, profile, interactive }) {
|
|
336
|
+
if (!interactive) return;
|
|
337
|
+
if (profile !== 'dev') return;
|
|
338
|
+
|
|
339
|
+
const sources = detectAuthSources();
|
|
340
|
+
const autoSeedEnabled =
|
|
341
|
+
(process.env.HAPPIER_STACK_AUTO_AUTH_SEED ?? '').toString().trim() === '1';
|
|
342
|
+
const seedFrom = (process.env.HAPPIER_STACK_AUTH_SEED_FROM ?? '').toString().trim();
|
|
343
|
+
const linkMode =
|
|
344
|
+
(process.env.HAPPIER_STACK_AUTH_LINK ?? '').toString().trim() === '1' ||
|
|
345
|
+
(process.env.HAPPIER_STACK_AUTH_MODE ?? '').toString().trim().toLowerCase() === 'link';
|
|
346
|
+
|
|
347
|
+
// If we already have dev-auth seeded and configured, don't ask redundant questions.
|
|
348
|
+
// (User can always re-run setup or use stack/auth commands to change this.)
|
|
349
|
+
const alreadyConfiguredDevAuth = autoSeedEnabled && seedFrom === 'dev-auth' && sources.hasDevAuthAccessKey;
|
|
350
|
+
if (alreadyConfiguredDevAuth) {
|
|
351
|
+
// eslint-disable-next-line no-console
|
|
352
|
+
console.log('');
|
|
353
|
+
// eslint-disable-next-line no-console
|
|
354
|
+
console.log(bold('Authentication (development)'));
|
|
355
|
+
// eslint-disable-next-line no-console
|
|
356
|
+
console.log(`${green('✓')} dev-auth auth seeding is already configured`);
|
|
357
|
+
// eslint-disable-next-line no-console
|
|
358
|
+
console.log(`${dim('Seed from:')} ${cyan('dev-auth')}`);
|
|
359
|
+
// eslint-disable-next-line no-console
|
|
360
|
+
console.log(`${dim('Mode:')} ${linkMode ? 'symlink' : 'copy'}`);
|
|
361
|
+
if (sources.hasDevKey) {
|
|
362
|
+
// eslint-disable-next-line no-console
|
|
363
|
+
console.log(`${dim('Dev key:')} configured`);
|
|
364
|
+
}
|
|
365
|
+
// If a user wants to change or recreate:
|
|
366
|
+
// eslint-disable-next-line no-console
|
|
367
|
+
console.log(dim(`Tip: to recreate the seed stack, run: ${yellow('hstack stack create-dev-auth-seed')}`));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// eslint-disable-next-line no-console
|
|
372
|
+
console.log('');
|
|
373
|
+
// eslint-disable-next-line no-console
|
|
374
|
+
console.log(bold('Authentication (development)'));
|
|
375
|
+
// eslint-disable-next-line no-console
|
|
376
|
+
console.log(
|
|
377
|
+
dim(
|
|
378
|
+
`Recommended: set up a dedicated ${cyan('dev-auth')} seed stack so you authenticate once, then new stacks “just work”.`
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
const seedChoice = 'dev-auth';
|
|
382
|
+
const linkChoice = 'link';
|
|
383
|
+
|
|
384
|
+
if (!sources.hasDevAuthAccessKey) {
|
|
385
|
+
const wantLoginNow = await withRl(async (rl) => {
|
|
386
|
+
return await promptSelect(rl, {
|
|
387
|
+
title:
|
|
388
|
+
`${bold('Sign in now?')}\n` +
|
|
389
|
+
`${dim('This will create a dedicated dev-auth seed stack and walk you through a guided login in the browser.')}\n` +
|
|
390
|
+
`${dim('After this, new stacks can reuse your auth automatically (recommended).')}`,
|
|
391
|
+
options: [
|
|
392
|
+
{ label: `yes (${green('recommended')}) — sign in now`, value: true },
|
|
393
|
+
{ label: `no — I will do this later`, value: false },
|
|
394
|
+
],
|
|
395
|
+
defaultIndex: 0,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (!wantLoginNow) {
|
|
400
|
+
// eslint-disable-next-line no-console
|
|
401
|
+
console.log(dim(`Tip: run ${yellow('hstack stack create-dev-auth-seed dev-auth --login')} anytime to sign in.`));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Guided wizard: creates stack, starts temporary UI/server, stores dev key (optional), logs in CLI.
|
|
406
|
+
await runNodeScript({
|
|
407
|
+
rootDir,
|
|
408
|
+
rel: 'scripts/stack.mjs',
|
|
409
|
+
args: ['create-dev-auth-seed', 'dev-auth', '--login', '--skip-default-seed'],
|
|
410
|
+
});
|
|
411
|
+
} else {
|
|
412
|
+
// eslint-disable-next-line no-console
|
|
413
|
+
console.log(dim(`Found an existing ${cyan('dev-auth')} seed stack; configuring auth reuse for new stacks.`));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await ensureEnvLocalUpdated({
|
|
417
|
+
rootDir,
|
|
418
|
+
updates: [
|
|
419
|
+
{ key: 'HAPPIER_STACK_AUTO_AUTH_SEED', value: '1' },
|
|
420
|
+
{ key: 'HAPPIER_STACK_AUTH_SEED_FROM', value: seedChoice },
|
|
421
|
+
{ key: 'HAPPIER_STACK_AUTH_LINK', value: linkChoice === 'link' ? '1' : '0' },
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
{
|
|
426
|
+
const envLocalPath = join(getCanonicalHomeDir(), 'env.local');
|
|
427
|
+
// eslint-disable-next-line no-console
|
|
428
|
+
console.log('');
|
|
429
|
+
// eslint-disable-next-line no-console
|
|
430
|
+
console.log(bold('Automatic sign-in for new stacks'));
|
|
431
|
+
// eslint-disable-next-line no-console
|
|
432
|
+
console.log(dim(`Enabled: when you create a new stack, hstack will reuse auth from ${cyan(seedChoice)} automatically.`));
|
|
433
|
+
// eslint-disable-next-line no-console
|
|
434
|
+
console.log(`${dim('Seed from:')} ${cyan(seedChoice)}`);
|
|
435
|
+
// eslint-disable-next-line no-console
|
|
436
|
+
console.log(`${dim('Mode:')} ${linkChoice === 'link' ? 'symlink' : 'copy'} ${dim(linkChoice === 'link' ? '(recommended)' : '')}`.trim());
|
|
437
|
+
// eslint-disable-next-line no-console
|
|
438
|
+
console.log(dim(`Config: ${envLocalPath}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Optional: seed existing stacks now (useful if the user already has stacks).
|
|
442
|
+
const allStacks = await listAllStackNames().catch(() => ['main']);
|
|
443
|
+
const candidateTargets = allStacks.filter((s) => s !== 'main' && s !== seedChoice);
|
|
444
|
+
if (candidateTargets.length) {
|
|
445
|
+
const seedNow = await withRl(async (rl) => {
|
|
446
|
+
return await promptSelect(rl, {
|
|
447
|
+
title:
|
|
448
|
+
`${bold('Apply sign-in to existing stacks?')}\n` +
|
|
449
|
+
`${dim(`We found ${candidateTargets.length} existing stack(s) that could reuse your auth automatically.`)}\n` +
|
|
450
|
+
`${dim('This can fix “auth required / no machine” without re-login.')}`,
|
|
451
|
+
options: [
|
|
452
|
+
{ label: `yes (${green('recommended')}) — apply to ${candidateTargets.length} stack(s) now`, value: true },
|
|
453
|
+
{ label: 'no — leave them as-is', value: false },
|
|
454
|
+
],
|
|
455
|
+
defaultIndex: 0,
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
if (seedNow) {
|
|
459
|
+
const except = ['main'];
|
|
460
|
+
if (seedChoice !== 'main') except.push(seedChoice);
|
|
461
|
+
const args = [
|
|
462
|
+
'copy-from',
|
|
463
|
+
seedChoice,
|
|
464
|
+
'--all',
|
|
465
|
+
`--except=${except.join(',')}`,
|
|
466
|
+
...(linkChoice === 'link' ? ['--link'] : []),
|
|
467
|
+
];
|
|
468
|
+
await runNodeScript({ rootDir, rel: 'scripts/auth.mjs', args });
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
// eslint-disable-next-line no-console
|
|
472
|
+
console.log(dim('No existing stacks detected that need seeding (nothing to do).'));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Dev key UX (for phone/Playwright restores).
|
|
476
|
+
const sourcesAfter = detectAuthSources();
|
|
477
|
+
if (sourcesAfter.hasDevKey) {
|
|
478
|
+
// eslint-disable-next-line no-console
|
|
479
|
+
console.log('');
|
|
480
|
+
// eslint-disable-next-line no-console
|
|
481
|
+
console.log(bold('Dev key (optional, sensitive)'));
|
|
482
|
+
// eslint-disable-next-line no-console
|
|
483
|
+
console.log(dim('This lets you restore the UI account quickly (and can help automation).'));
|
|
484
|
+
// eslint-disable-next-line no-console
|
|
485
|
+
console.log(dim(`Stored at: ${sourcesAfter.devKeyPath}`));
|
|
486
|
+
// eslint-disable-next-line no-console
|
|
487
|
+
console.log(dim(`Tip: to print it later, run: ${yellow('hstack auth dev-key --print')}`));
|
|
488
|
+
} else {
|
|
489
|
+
// eslint-disable-next-line no-console
|
|
490
|
+
console.log(dim(`Tip: to store a dev key later, run: ${yellow('hstack auth dev-key --set "<key>"')}`));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function cmdSetup({ rootDir, argv }) {
|
|
495
|
+
// Alias: `hstack setup pr ...` (maintainer-friendly, idempotent PR setup).
|
|
496
|
+
// This delegates to `tools setup-pr` (implemented in setup_pr.mjs) so the logic stays centralized.
|
|
497
|
+
const firstPositional = argv.find((a) => !a.startsWith('--')) ?? '';
|
|
498
|
+
if (firstPositional === 'pr') {
|
|
499
|
+
const idx = argv.indexOf('pr');
|
|
500
|
+
const forwarded = idx >= 0 ? argv.slice(idx + 1) : [];
|
|
501
|
+
await run(process.execPath, [join(rootDir, 'scripts', 'setup_pr.mjs'), ...forwarded], { cwd: rootDir });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const { flags, kv } = parseArgs(argv);
|
|
506
|
+
const json = wantsJson(argv, { flags });
|
|
507
|
+
|
|
508
|
+
// Optional: bind mode affects how we print URLs (loopback vs LAN).
|
|
509
|
+
// We apply it early so all downstream helpers inherit the same env.
|
|
510
|
+
const bindMode = resolveBindModeFromArgs({ flags, kv });
|
|
511
|
+
if (bindMode) {
|
|
512
|
+
applyBindModeToEnv(process.env, bindMode);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (wantsHelp(argv, { flags })) {
|
|
516
|
+
printResult({
|
|
517
|
+
json,
|
|
518
|
+
data: {
|
|
519
|
+
profiles: ['selfhost', 'dev'],
|
|
520
|
+
flags: [
|
|
521
|
+
'--profile=selfhost|dev',
|
|
522
|
+
'--server=happier-server-light|happier-server',
|
|
523
|
+
'--server-flavor=light|full',
|
|
524
|
+
'--non-interactive',
|
|
525
|
+
'--happier-repo=<owner/repo|url> # override the monorepo clone source',
|
|
526
|
+
'--workspace-dir=/absolute/path # dev profile only',
|
|
527
|
+
'--no-ui-deps # bootstrap: skip UI deps',
|
|
528
|
+
'--no-ui-build # bootstrap: skip UI build',
|
|
529
|
+
'--install-path',
|
|
530
|
+
'--start-now',
|
|
531
|
+
'--bind=loopback|lan',
|
|
532
|
+
'--loopback',
|
|
533
|
+
'--lan',
|
|
534
|
+
'--auth|--no-auth',
|
|
535
|
+
'--tailscale|--no-tailscale',
|
|
536
|
+
'--autostart|--no-autostart',
|
|
537
|
+
'--menubar|--no-menubar',
|
|
538
|
+
'--json',
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
text: [
|
|
542
|
+
'[setup] usage:',
|
|
543
|
+
' hstack setup',
|
|
544
|
+
' hstack setup --profile=selfhost',
|
|
545
|
+
' hstack setup --profile=dev',
|
|
546
|
+
' hstack setup --profile=dev --workspace-dir=~/Development/happier',
|
|
547
|
+
' hstack setup --happier-repo=happier-dev/happier',
|
|
548
|
+
' hstack tools setup-pr --repo=<pr-url|number>',
|
|
549
|
+
' hstack setup --auth',
|
|
550
|
+
' hstack setup --no-auth',
|
|
551
|
+
'',
|
|
552
|
+
'notes:',
|
|
553
|
+
' - selfhost profile is a guided installer for running Happier locally (optionally with Tailscale + autostart).',
|
|
554
|
+
' - dev profile prepares a development workspace (bootstrap wizard + optional dev tooling).',
|
|
555
|
+
' - for PR review, use `hstack tools review-pr` / `hstack tools setup-pr`.',
|
|
556
|
+
' - server selection: use --server=... or the shorthand --server-flavor=light|full',
|
|
557
|
+
].join('\n'),
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const interactive = isTty() && !flags.has('--non-interactive');
|
|
563
|
+
let profile = normalizeProfile(kv.get('--profile'));
|
|
564
|
+
if (!profile && interactive) {
|
|
565
|
+
profile = await withRl(async (rl) => {
|
|
566
|
+
return await promptSelect(rl, {
|
|
567
|
+
title: bold(`✨ ${cyan('hstack')} setup ✨\n\nWhat is your goal?`),
|
|
568
|
+
options: [
|
|
569
|
+
{ label: `${cyan('Self-host')}: use Happier on this machine`, value: 'selfhost' },
|
|
570
|
+
{ label: `${cyan('Development')}: worktrees + stacks + contributor workflows`, value: 'dev' },
|
|
571
|
+
],
|
|
572
|
+
defaultIndex: 0,
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (!profile) {
|
|
577
|
+
profile = 'selfhost';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const verbosity = getVerbosityLevel(process.env);
|
|
581
|
+
const quietUi = interactive && verbosity === 0 && !json;
|
|
582
|
+
|
|
583
|
+
// Optional: override the monorepo clone source (UI + CLI + full server).
|
|
584
|
+
const happierRepoUrl = normalizeGithubRepoUrl(kv.get('--happier-repo'));
|
|
585
|
+
const bootstrapExtraArgs = [];
|
|
586
|
+
if (flags.has('--no-ui-deps')) bootstrapExtraArgs.push('--no-ui-deps');
|
|
587
|
+
if (flags.has('--no-ui-build')) bootstrapExtraArgs.push('--no-ui-build');
|
|
588
|
+
|
|
589
|
+
function isInteractiveChildCommand({ rel, args }) {
|
|
590
|
+
// If a child command needs to prompt the user, it must inherit stdin/stdout.
|
|
591
|
+
// Otherwise setup's quiet mode will break the wizard (stdin is intentionally disabled).
|
|
592
|
+
void rel;
|
|
593
|
+
return args.some((a) => String(a).trim() === '--interactive');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function runNodeScriptMaybeQuiet({ label, rel, args = [], env = process.env, interactiveChild = null }) {
|
|
597
|
+
const childIsInteractive = interactiveChild ?? isInteractiveChildCommand({ rel, args });
|
|
598
|
+
if (!quietUi || childIsInteractive) {
|
|
599
|
+
await run(process.execPath, [join(rootDir, rel), ...args], { cwd: rootDir, env });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const baseLogDir = join(getHappyStacksHomeDir(process.env), 'logs', 'setup');
|
|
603
|
+
const logPath = join(baseLogDir, `${label.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}.${Date.now()}.log`);
|
|
604
|
+
try {
|
|
605
|
+
await runCommandLogged({
|
|
606
|
+
label,
|
|
607
|
+
cmd: process.execPath,
|
|
608
|
+
args: [join(rootDir, rel), ...args],
|
|
609
|
+
cwd: rootDir,
|
|
610
|
+
env,
|
|
611
|
+
logPath,
|
|
612
|
+
quiet: true,
|
|
613
|
+
showSteps: true,
|
|
614
|
+
});
|
|
615
|
+
} catch (e) {
|
|
616
|
+
const lp = e?.logPath ? String(e.logPath) : logPath;
|
|
617
|
+
// eslint-disable-next-line no-console
|
|
618
|
+
console.error(`[setup] failed: ${label}`);
|
|
619
|
+
// eslint-disable-next-line no-console
|
|
620
|
+
console.error(`${dim('log:')} ${lp}`);
|
|
621
|
+
throw e;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function printProfileIntro({ profile }) {
|
|
626
|
+
if (!process.stdout.isTTY || json) return;
|
|
627
|
+
const header = profile === 'selfhost' ? `${cyan('Self-host')} setup` : `${cyan('Development')} setup`;
|
|
628
|
+
const lines = [
|
|
629
|
+
'',
|
|
630
|
+
bold(header),
|
|
631
|
+
profile === 'selfhost'
|
|
632
|
+
? dim('Run Happier locally (optionally with Tailscale + autostart).')
|
|
633
|
+
: dim('Prepare a contributor workspace (repo + worktrees + stacks).'),
|
|
634
|
+
'',
|
|
635
|
+
bold('How Happier runs locally:'),
|
|
636
|
+
profile === 'selfhost'
|
|
637
|
+
? [
|
|
638
|
+
`- ${cyan('server')}: stores sessions + serves the API`,
|
|
639
|
+
`- ${cyan('web UI')}: where you chat + view sessions`,
|
|
640
|
+
`- ${cyan('daemon')}: background process that runs/streams sessions and lets terminal runs show up in the UI`,
|
|
641
|
+
'',
|
|
642
|
+
dim(`A ${cyan('stack')} is one isolated instance (dirs + ports + database). Setup configures the default stack: ${cyan('main')}.`),
|
|
643
|
+
]
|
|
644
|
+
: [
|
|
645
|
+
`- ${cyan('workspace')}: your git checkouts (repo + worktrees)`,
|
|
646
|
+
`- ${cyan('stacks')}: isolated runtimes under ${cyan('~/.happier/stacks/<name>')}`,
|
|
647
|
+
`- ${cyan('daemon')}: runs sessions + connects the UI <-> terminal`,
|
|
648
|
+
],
|
|
649
|
+
'',
|
|
650
|
+
bold('What will happen:'),
|
|
651
|
+
profile === 'selfhost'
|
|
652
|
+
? [
|
|
653
|
+
`- ${cyan('init')}: set up hstack home + shims`,
|
|
654
|
+
`- ${cyan('bootstrap')}: clone/install the repo`,
|
|
655
|
+
`- ${cyan('start')}: start Happier now (recommended)`,
|
|
656
|
+
`- ${cyan('login')}: guided login (recommended)`,
|
|
657
|
+
'',
|
|
658
|
+
dim(
|
|
659
|
+
`Tip: ${cyan('Happier Self-Host default')} is the simplest local install (no Docker). ${cyan('Happier Self-Host full stack')} needs Docker (Postgres/Redis/Minio).`,
|
|
660
|
+
),
|
|
661
|
+
]
|
|
662
|
+
: [
|
|
663
|
+
`- ${cyan('workspace')}: choose where the repo + worktrees live`,
|
|
664
|
+
`- ${cyan('init')}: set up hstack home + shims`,
|
|
665
|
+
`- ${cyan('bootstrap')}: clone/install the repo + dev tooling`,
|
|
666
|
+
`- ${cyan('auth')}: (recommended) set up a ${cyan('dev-auth')} seed stack (login once, reuse everywhere)`,
|
|
667
|
+
`- ${cyan('stacks')}: (recommended) next you’ll create an isolated dev stack for day-to-day work (keeps main stable)`,
|
|
668
|
+
`- ${cyan('mobile')}: (optional) install the iOS dev-client (for phone testing)`,
|
|
669
|
+
'',
|
|
670
|
+
dim(`Tip: for PR work, use ${cyan('worktrees')} (isolated branches) + ${cyan('stacks')} (isolated runtime state).`),
|
|
671
|
+
],
|
|
672
|
+
'',
|
|
673
|
+
].flat();
|
|
674
|
+
// eslint-disable-next-line no-console
|
|
675
|
+
console.log(lines.join('\n'));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (interactive) {
|
|
679
|
+
printProfileIntro({ profile });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const platform = process.platform;
|
|
683
|
+
const supportsAutostart = platform === 'darwin' || platform === 'linux';
|
|
684
|
+
const supportsMenubar = platform === 'darwin';
|
|
685
|
+
|
|
686
|
+
// Convenience alias: allow `--server-flavor=light|full` for parity with `stack pr` and `tools setup-pr`.
|
|
687
|
+
// `--server=...` always wins when both are specified.
|
|
688
|
+
const serverFlavorFromArg = (kv.get('--server-flavor') ?? '').trim().toLowerCase();
|
|
689
|
+
if (!kv.get('--server') && serverFlavorFromArg) {
|
|
690
|
+
if (serverFlavorFromArg === 'light') kv.set('--server', 'happier-server-light');
|
|
691
|
+
else if (serverFlavorFromArg === 'full') kv.set('--server', 'happier-server');
|
|
692
|
+
else throw new Error(`[setup] invalid --server-flavor=${serverFlavorFromArg} (expected: light|full)`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const serverFromArg = normalizeServerComponent(kv.get('--server'));
|
|
696
|
+
let serverComponent = serverFromArg || normalizeServerComponent(process.env.HAPPIER_STACK_SERVER_COMPONENT) || 'happier-server-light';
|
|
697
|
+
if (profile === 'selfhost' && interactive && !serverFromArg) {
|
|
698
|
+
const docker = await detectDockerSupport();
|
|
699
|
+
if (!docker.installed) {
|
|
700
|
+
serverComponent = 'happier-server-light';
|
|
701
|
+
// eslint-disable-next-line no-console
|
|
702
|
+
console.log(`${green('✓')} Server: ${cyan('Happier Self-Host default')} ${dim('(Docker not detected; simplest local install)')}`);
|
|
703
|
+
} else if (!docker.running) {
|
|
704
|
+
serverComponent = 'happier-server-light';
|
|
705
|
+
// eslint-disable-next-line no-console
|
|
706
|
+
console.log(
|
|
707
|
+
`${green('✓')} Server: ${cyan('Happier Self-Host default')} ${dim('(Docker detected but not running; using simplest option)')}`
|
|
708
|
+
);
|
|
709
|
+
// eslint-disable-next-line no-console
|
|
710
|
+
console.log(dim(`Tip: start Docker Desktop, then re-run setup if you want ${cyan('Happier Self-Host full stack')}.`));
|
|
711
|
+
} else {
|
|
712
|
+
serverComponent = await withRl(async (rl) => {
|
|
713
|
+
const picked = await promptSelect(rl, {
|
|
714
|
+
title: `${bold('Server flavor')}\n${dim('Pick the backend you want to run locally. You can switch later.')}`,
|
|
715
|
+
options: [
|
|
716
|
+
{ label: `Happier Self-Host default (${green('recommended')}) — simplest local install (PG_Light via embedded PGlite)`, value: 'happier-server-light' },
|
|
717
|
+
{ label: `Happier Self-Host full stack — full server (Postgres/Redis/Minio via Docker)`, value: 'happier-server' },
|
|
718
|
+
],
|
|
719
|
+
defaultIndex: serverComponent === 'happier-server' ? 1 : 0,
|
|
720
|
+
});
|
|
721
|
+
return picked;
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// If the user explicitly requested full server, enforce Docker availability.
|
|
726
|
+
if (profile === 'selfhost' && serverFromArg === 'happier-server') {
|
|
727
|
+
const docker = await detectDockerSupport();
|
|
728
|
+
if (!docker.installed || !docker.running) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`[setup] --server=happier-server requires Docker (Postgres/Redis/Minio).\n` +
|
|
731
|
+
`Docker is ${!docker.installed ? 'not installed' : 'not running'}.\n` +
|
|
732
|
+
`Fix: use --server=happier-server-light (simplest), or start Docker and retry.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Dev profile: pick where to store the repo + worktrees.
|
|
738
|
+
const workspaceDirFlagRaw = (kv.get('--workspace-dir') ?? '').toString().trim();
|
|
739
|
+
const homeDirForWorkspace = getHappyStacksHomeDir(process.env);
|
|
740
|
+
let workspaceDirWanted = workspaceDirFlagRaw ? normalizeWorkspaceDirInput(workspaceDirFlagRaw, { homeDir: homeDirForWorkspace }) : '';
|
|
741
|
+
if (profile === 'dev' && interactive && !workspaceDirWanted) {
|
|
742
|
+
const defaultWorkspaceDir = resolveWorkspaceDirDefault();
|
|
743
|
+
const suggested = defaultWorkspaceDir;
|
|
744
|
+
const helpLines = [
|
|
745
|
+
bold('Workspace location'),
|
|
746
|
+
dim('This is where hstack will keep:'),
|
|
747
|
+
`- ${dim('main')}: ${cyan(join(suggested, 'main'))} ${dim('(stable checkout)')}`,
|
|
748
|
+
`- ${dim('dev')}: ${cyan(join(suggested, 'dev'))} ${dim('(development checkout)')}`,
|
|
749
|
+
`- ${dim('pr')}: ${cyan(join(suggested, 'pr'))}`,
|
|
750
|
+
`- ${dim('local')}: ${cyan(join(suggested, 'local'))}`,
|
|
751
|
+
`- ${dim('tmp')}: ${cyan(join(suggested, 'tmp'))}`,
|
|
752
|
+
'',
|
|
753
|
+
dim('Pick a stable folder that is easy to open in your editor (example: ~/Development/happier).'),
|
|
754
|
+
'',
|
|
755
|
+
].join('\n');
|
|
756
|
+
// eslint-disable-next-line no-console
|
|
757
|
+
console.log(helpLines);
|
|
758
|
+
const raw = await withRl(async (rl) => {
|
|
759
|
+
return await prompt(rl, `Workspace dir (default: ${suggested}): `, { defaultValue: suggested });
|
|
760
|
+
});
|
|
761
|
+
workspaceDirWanted = normalizeWorkspaceDirInput(raw, { homeDir: homeDirForWorkspace });
|
|
762
|
+
}
|
|
763
|
+
if (profile === 'dev' && workspaceDirWanted) {
|
|
764
|
+
// eslint-disable-next-line no-console
|
|
765
|
+
console.log(`${dim('Workspace:')} ${cyan(workspaceDirWanted)}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const defaultTailscale = false;
|
|
769
|
+
const defaultAutostart = false;
|
|
770
|
+
const defaultMenubar = false;
|
|
771
|
+
const defaultStartNow = profile === 'selfhost';
|
|
772
|
+
const defaultInstallPath = false;
|
|
773
|
+
|
|
774
|
+
let tailscaleWanted = boolFromFlags({ flags, onFlag: '--tailscale', offFlag: '--no-tailscale', defaultValue: defaultTailscale });
|
|
775
|
+
let autostartWanted = boolFromFlags({ flags, onFlag: '--autostart', offFlag: '--no-autostart', defaultValue: defaultAutostart });
|
|
776
|
+
let menubarWanted = boolFromFlags({ flags, onFlag: '--menubar', offFlag: '--no-menubar', defaultValue: defaultMenubar });
|
|
777
|
+
let startNow = boolFromFlags({ flags, onFlag: '--start-now', offFlag: '--no-start-now', defaultValue: defaultStartNow });
|
|
778
|
+
let installPath = flags.has('--install-path') ? true : defaultInstallPath;
|
|
779
|
+
let authWanted = boolFromFlagsOrKv({
|
|
780
|
+
flags,
|
|
781
|
+
kv,
|
|
782
|
+
onFlag: '--auth',
|
|
783
|
+
offFlag: '--no-auth',
|
|
784
|
+
key: '--auth',
|
|
785
|
+
defaultValue: profile === 'selfhost',
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
if (interactive) {
|
|
789
|
+
if (profile === 'selfhost') {
|
|
790
|
+
// Avoid asking questions when we can infer an existing setup state (unless the user explicitly passed flags).
|
|
791
|
+
const tailscaleExplicit = flags.has('--tailscale') || flags.has('--no-tailscale');
|
|
792
|
+
const autostartExplicit = flags.has('--autostart') || flags.has('--no-autostart');
|
|
793
|
+
const menubarExplicit = flags.has('--menubar') || flags.has('--no-menubar');
|
|
794
|
+
const authExplicit = flags.has('--auth') || flags.has('--no-auth') || kv.has('--auth');
|
|
795
|
+
|
|
796
|
+
// Auth: skip prompt if already configured.
|
|
797
|
+
const mainAccessKeyPath = getMainStacksAccessKeyPath();
|
|
798
|
+
const authAlreadyConfigured = existsSync(mainAccessKeyPath);
|
|
799
|
+
if (!authExplicit && authAlreadyConfigured) {
|
|
800
|
+
authWanted = false;
|
|
801
|
+
// eslint-disable-next-line no-console
|
|
802
|
+
console.log(`${green('✓')} Authentication: already configured ${dim(`(${mainAccessKeyPath})`)}`);
|
|
803
|
+
}
|
|
804
|
+
if (!authExplicit && !authAlreadyConfigured) {
|
|
805
|
+
// Self-host onboarding default: guide login as part of setup.
|
|
806
|
+
authWanted = true;
|
|
807
|
+
// eslint-disable-next-line no-console
|
|
808
|
+
console.log(`${green('✓')} Authentication: will guide you through login ${dim('(recommended)')}`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Tailscale: skip prompt if already enabled for the main internal URL.
|
|
812
|
+
let tailscaleDetectedHttps = null;
|
|
813
|
+
if (!tailscaleExplicit) {
|
|
814
|
+
try {
|
|
815
|
+
const port = await resolveMainServerPort();
|
|
816
|
+
const internal = `http://127.0.0.1:${port}`;
|
|
817
|
+
tailscaleDetectedHttps = await tailscaleServeHttpsUrlForInternalServerUrl(internal);
|
|
818
|
+
} catch {
|
|
819
|
+
tailscaleDetectedHttps = null;
|
|
820
|
+
}
|
|
821
|
+
if (tailscaleDetectedHttps) {
|
|
822
|
+
tailscaleWanted = true;
|
|
823
|
+
// eslint-disable-next-line no-console
|
|
824
|
+
console.log(`${green('✓')} Remote access: Tailscale Serve already enabled ${dim('→')} ${cyan(tailscaleDetectedHttps)}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!tailscaleExplicit && tailscaleDetectedHttps) {
|
|
829
|
+
// keep tailscaleWanted=true and skip the question
|
|
830
|
+
} else {
|
|
831
|
+
tailscaleWanted = await withRl(async (rl) => {
|
|
832
|
+
const v = await promptSelect(rl, {
|
|
833
|
+
title: `${bold('Remote access')}\n${dim('Optional: use Tailscale Serve to get an HTTPS URL for Happier (secure, recommended for phone access).')}`,
|
|
834
|
+
options: [
|
|
835
|
+
{ label: `yes (${green('recommended for phone')}) — enable Tailscale Serve`, value: true },
|
|
836
|
+
{ label: 'no (default)', value: false },
|
|
837
|
+
],
|
|
838
|
+
defaultIndex: tailscaleWanted ? 0 : 1,
|
|
839
|
+
});
|
|
840
|
+
return v;
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (supportsAutostart) {
|
|
845
|
+
const a = getDefaultAutostartPaths();
|
|
846
|
+
const autostartAlreadyInstalled =
|
|
847
|
+
process.platform === 'darwin'
|
|
848
|
+
? Boolean(existsSync(a.plistPath))
|
|
849
|
+
: process.platform === 'linux'
|
|
850
|
+
? Boolean(existsSync(a.systemdUnitPath))
|
|
851
|
+
: false;
|
|
852
|
+
if (!autostartExplicit && autostartAlreadyInstalled) {
|
|
853
|
+
autostartWanted = false;
|
|
854
|
+
// eslint-disable-next-line no-console
|
|
855
|
+
console.log(`${green('✓')} Autostart: already installed ${dim('(leaving as-is)')}`);
|
|
856
|
+
} else {
|
|
857
|
+
autostartWanted = await withRl(async (rl) => {
|
|
858
|
+
const detail =
|
|
859
|
+
process.platform === 'darwin'
|
|
860
|
+
? 'macOS: launchd LaunchAgent'
|
|
861
|
+
: process.platform === 'linux'
|
|
862
|
+
? 'Linux: systemd --user service'
|
|
863
|
+
: '';
|
|
864
|
+
const v = await promptSelect(rl, {
|
|
865
|
+
title:
|
|
866
|
+
`${bold('Autostart')}\n` +
|
|
867
|
+
`${dim('Optional: start Happier automatically at login.')}` +
|
|
868
|
+
(detail ? `\n${dim(detail)}` : ''),
|
|
869
|
+
options: [
|
|
870
|
+
{ label: 'yes', value: true },
|
|
871
|
+
{ label: 'no (default)', value: false },
|
|
872
|
+
],
|
|
873
|
+
defaultIndex: autostartWanted ? 0 : 1,
|
|
874
|
+
});
|
|
875
|
+
return v;
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
autostartWanted = false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (supportsMenubar) {
|
|
883
|
+
let menubarInstalled = false;
|
|
884
|
+
if (!menubarExplicit) {
|
|
885
|
+
const swift = await detectSwiftbarPluginInstalled();
|
|
886
|
+
menubarInstalled = Boolean(swift.installed);
|
|
887
|
+
if (menubarInstalled) {
|
|
888
|
+
menubarWanted = false;
|
|
889
|
+
// eslint-disable-next-line no-console
|
|
890
|
+
console.log(`${green('✓')} Menu bar: already installed ${dim('(SwiftBar plugin)')}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (!menubarExplicit && menubarInstalled) {
|
|
894
|
+
// skip question
|
|
895
|
+
} else {
|
|
896
|
+
menubarWanted = await withRl(async (rl) => {
|
|
897
|
+
const v = await promptSelect(rl, {
|
|
898
|
+
title: `${bold('Menu bar (macOS)')}\n${dim('Optional: install the SwiftBar menu to control stacks quickly.')}`,
|
|
899
|
+
options: [
|
|
900
|
+
{ label: 'yes', value: true },
|
|
901
|
+
{ label: 'no (default)', value: false },
|
|
902
|
+
],
|
|
903
|
+
defaultIndex: menubarWanted ? 0 : 1,
|
|
904
|
+
});
|
|
905
|
+
return v;
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
menubarWanted = false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Self-host onboarding default: start now (end-to-end setup).
|
|
913
|
+
const startNowExplicit = flags.has('--start-now') || flags.has('--no-start-now');
|
|
914
|
+
if (!startNowExplicit) {
|
|
915
|
+
startNow = true;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// No interactive auth prompt here: we either detected it's already configured, or we default to guiding login.
|
|
919
|
+
|
|
920
|
+
// Auth requires the stack to be running; if you chose "authenticate now", implicitly start.
|
|
921
|
+
if (authWanted) {
|
|
922
|
+
startNow = true;
|
|
923
|
+
}
|
|
924
|
+
} else if (profile === 'dev') {
|
|
925
|
+
// Dev profile: auth is handled later (after bootstrap) so we can offer the recommended
|
|
926
|
+
// dev-auth seed stack flow (and optional mobile dev-client install).
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
installPath = await withRl(async (rl) => {
|
|
930
|
+
const v = await promptSelect(rl, {
|
|
931
|
+
title:
|
|
932
|
+
`${bold('Command shortcuts')}\n` +
|
|
933
|
+
`${dim(
|
|
934
|
+
`Optional: add ${cyan(join(getCanonicalHomeDir(), 'bin'))} to your shell PATH so you can run ${cyan(
|
|
935
|
+
'hstack'
|
|
936
|
+
)} from any terminal.`
|
|
937
|
+
)}\n` +
|
|
938
|
+
`${dim(`If you skip this, you can always run commands via ${cyan('npx --yes -p @happier-dev/stack hstack ...')}.`)}`,
|
|
939
|
+
options: [
|
|
940
|
+
{ label: `yes (${green('recommended')}, default) — enable ${cyan('hstack')} in your terminal`, value: true },
|
|
941
|
+
{ label: `no — keep using ${cyan('npx --yes -p @happier-dev/stack hstack ...')}`, value: false },
|
|
942
|
+
],
|
|
943
|
+
defaultIndex: 0,
|
|
944
|
+
});
|
|
945
|
+
return v;
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Enforce OS support gates even if flags were passed.
|
|
950
|
+
if (!supportsAutostart) autostartWanted = false;
|
|
951
|
+
if (!supportsMenubar) menubarWanted = false;
|
|
952
|
+
|
|
953
|
+
const menubarMode = profile === 'selfhost' ? 'selfhost' : 'dev';
|
|
954
|
+
|
|
955
|
+
// Preflight: warn early + decide what we can actually do this run.
|
|
956
|
+
const preflight = await runSetupPreflight({ profile, serverComponent, tailscaleWanted, menubarWanted, autostartWanted });
|
|
957
|
+
if (interactive && process.stdout.isTTY && !json) {
|
|
958
|
+
// eslint-disable-next-line no-console
|
|
959
|
+
console.log('');
|
|
960
|
+
// eslint-disable-next-line no-console
|
|
961
|
+
console.log(banner('Preflight', { subtitle: profile === 'selfhost' ? 'Check prerequisites for self-hosting.' : 'Check prerequisites for development setup.' }));
|
|
962
|
+
|
|
963
|
+
const lines = [];
|
|
964
|
+
if (profile === 'selfhost') {
|
|
965
|
+
if (serverComponent === 'happier-server') {
|
|
966
|
+
lines.push(
|
|
967
|
+
preflight.docker.installed && preflight.docker.running
|
|
968
|
+
? `${green('✓')} Docker: running`
|
|
969
|
+
: `${yellow('!')} Docker: ${!preflight.docker.installed ? 'not installed' : 'not running'} (full server needs Docker)`
|
|
970
|
+
);
|
|
971
|
+
} else {
|
|
972
|
+
lines.push(
|
|
973
|
+
preflight.docker.installed
|
|
974
|
+
? `${green('✓')} Docker: detected ${dim('(not required for server-light)')}`
|
|
975
|
+
: `${dim('•')} Docker: not detected ${dim('(server-light does not need it)')}`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (tailscaleWanted) {
|
|
979
|
+
lines.push(
|
|
980
|
+
preflight.tailscale.installed
|
|
981
|
+
? `${green('✓')} Tailscale: detected`
|
|
982
|
+
: `${yellow('!')} Tailscale: not installed ${dim('(remote HTTPS will be available after install)')}`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (menubarWanted && process.platform === 'darwin') {
|
|
986
|
+
lines.push(
|
|
987
|
+
preflight.swiftbarAppInstalled
|
|
988
|
+
? `${green('✓')} SwiftBar: installed`
|
|
989
|
+
: `${yellow('!')} SwiftBar: not detected ${dim('(plugin can be installed, but you need SwiftBar to use it)')}`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
// dev profile: iOS tooling is only relevant if user chooses mobile-dev-client later.
|
|
994
|
+
if (process.platform === 'darwin') {
|
|
995
|
+
lines.push(
|
|
996
|
+
preflight.ios.ok
|
|
997
|
+
? `${green('✓')} iOS tooling: Xcode + CocoaPods detected`
|
|
998
|
+
: `${dim('•')} iOS tooling: ${!preflight.ios.hasXcode ? 'missing Xcode' : ''}${!preflight.ios.hasXcode && !preflight.ios.hasCocoapods ? ' + ' : ''}${!preflight.ios.hasCocoapods ? 'missing CocoaPods' : ''}`.trim()
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// eslint-disable-next-line no-console
|
|
1003
|
+
console.log(lines.length ? lines.join('\n') : dim('(no checks)'));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const config = {
|
|
1007
|
+
profile,
|
|
1008
|
+
platform,
|
|
1009
|
+
interactive,
|
|
1010
|
+
serverComponent,
|
|
1011
|
+
authWanted,
|
|
1012
|
+
tailscaleWanted,
|
|
1013
|
+
autostartWanted,
|
|
1014
|
+
menubarWanted,
|
|
1015
|
+
startNow,
|
|
1016
|
+
installPath,
|
|
1017
|
+
runtimeDir: getRuntimeDir(),
|
|
1018
|
+
};
|
|
1019
|
+
if (json) {
|
|
1020
|
+
printResult({ json, data: config });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (interactive && process.stdout.isTTY) {
|
|
1025
|
+
const summary = [
|
|
1026
|
+
'',
|
|
1027
|
+
bold('Ready to set up'),
|
|
1028
|
+
`${dim('Profile:')} ${cyan(profile)}`,
|
|
1029
|
+
...(profile === 'dev' && workspaceDirWanted ? [`${dim('Workspace:')} ${cyan(workspaceDirWanted)}`] : []),
|
|
1030
|
+
...(profile === 'selfhost' ? [`${dim('Server:')} ${cyan(serverComponent)}`] : []),
|
|
1031
|
+
'',
|
|
1032
|
+
bold('Press Enter to begin') + dim(' (or Ctrl+C to cancel).'),
|
|
1033
|
+
].join('\n');
|
|
1034
|
+
// eslint-disable-next-line no-console
|
|
1035
|
+
console.log(summary);
|
|
1036
|
+
await withRl(async (rl) => {
|
|
1037
|
+
await prompt(rl, '', { defaultValue: '' });
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// 1) Ensure plumbing exists (runtime + shims + pointer env). Avoid auto-bootstrap here; setup drives bootstrap explicitly.
|
|
1042
|
+
await runNodeScriptMaybeQuiet({
|
|
1043
|
+
label: 'init hstack home',
|
|
1044
|
+
rel: 'scripts/init.mjs',
|
|
1045
|
+
args: [
|
|
1046
|
+
'--no-bootstrap',
|
|
1047
|
+
...(profile === 'dev' && workspaceDirWanted ? [`--workspace-dir=${workspaceDirWanted}`] : []),
|
|
1048
|
+
...(installPath ? ['--install-path'] : []),
|
|
1049
|
+
],
|
|
1050
|
+
env: { ...process.env, HAPPIER_STACK_SETUP_CHILD: '1' },
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// 2) Persist profile defaults to stack env (server flavor, repo source, tailscale preference, menubar mode).
|
|
1054
|
+
await ensureSetupConfigPersisted({
|
|
1055
|
+
rootDir,
|
|
1056
|
+
profile,
|
|
1057
|
+
serverComponent,
|
|
1058
|
+
tailscaleWanted,
|
|
1059
|
+
menubarMode,
|
|
1060
|
+
happierRepoUrl,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// Apply repo override to this process too (so the immediately-following install step sees it),
|
|
1064
|
+
// even if env.local was already loaded earlier in this process.
|
|
1065
|
+
if (happierRepoUrl) {
|
|
1066
|
+
process.env.HAPPIER_STACK_REPO_URL = happierRepoUrl;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// 3) Bootstrap the monorepo.
|
|
1070
|
+
if (profile === 'dev') {
|
|
1071
|
+
// Developer setup: use the bootstrap wizard when a TTY is available; otherwise run non-interactively.
|
|
1072
|
+
await runNodeScriptMaybeQuiet({
|
|
1073
|
+
label: 'bootstrap repo',
|
|
1074
|
+
rootDir,
|
|
1075
|
+
rel: 'scripts/install.mjs',
|
|
1076
|
+
// Dev setup: use Expo dev server, so exporting a production web bundle is wasted work.
|
|
1077
|
+
// Users can always run `hstack build` later if they want `hstack start` to serve a prebuilt UI.
|
|
1078
|
+
args: [...(interactive ? ['--interactive'] : []), '--clone', '--no-ui-build'],
|
|
1079
|
+
interactiveChild: interactive,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (interactive) {
|
|
1083
|
+
// Recommended: dev-auth seed stack setup (login once, reuse across stacks).
|
|
1084
|
+
await maybeConfigureAuthDefaults({ rootDir, profile, interactive });
|
|
1085
|
+
|
|
1086
|
+
// Optional: mobile dev-client install (macOS only).
|
|
1087
|
+
if (process.platform === 'darwin') {
|
|
1088
|
+
const installMobile = await withRl(async (rl) => {
|
|
1089
|
+
return await promptSelect(rl, {
|
|
1090
|
+
title: `${bold('Mobile (iOS)')}\n${dim('Optional: install the shared Happier dev-client app on your iPhone (install once, reuse across stacks).')}`,
|
|
1091
|
+
options: [
|
|
1092
|
+
{ label: `yes — install iOS dev-client (${yellow('requires Xcode + CocoaPods')})`, value: true },
|
|
1093
|
+
{ label: 'no (default)', value: false },
|
|
1094
|
+
],
|
|
1095
|
+
defaultIndex: 1,
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
if (installMobile) {
|
|
1099
|
+
await runNodeScriptMaybeQuiet({
|
|
1100
|
+
label: 'install iOS dev-client',
|
|
1101
|
+
rootDir,
|
|
1102
|
+
rel: 'scripts/mobile_dev_client.mjs',
|
|
1103
|
+
args: ['--install'],
|
|
1104
|
+
});
|
|
1105
|
+
// eslint-disable-next-line no-console
|
|
1106
|
+
console.log(dim(`Tip: run any stack with ${yellow('--mobile')} to get a QR code / deep link for your phone.`));
|
|
1107
|
+
}
|
|
1108
|
+
} else {
|
|
1109
|
+
// eslint-disable-next-line no-console
|
|
1110
|
+
console.log(dim(`Tip: iOS dev-client install is macOS-only. You can still use the web UI on mobile via Tailscale.`));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Contributor UX: if the user can't push to upstream, offer to configure a fork as origin
|
|
1115
|
+
// (so later `hstack contrib extract --push` defaults to pushing feature branches to origin).
|
|
1116
|
+
await maybeConfigureForkRemoteForDevProfile({ rootDir, interactive });
|
|
1117
|
+
|
|
1118
|
+
// Ensure the dedicated dev checkout exists (workspace/dev on branch "dev").
|
|
1119
|
+
// This is the recommended place for contributors to make changes (main stays stable).
|
|
1120
|
+
await ensureDevCheckout({ rootDir, env: process.env });
|
|
1121
|
+
} else {
|
|
1122
|
+
// Selfhost setup: run non-interactively and keep it simple.
|
|
1123
|
+
await runNodeScriptMaybeQuiet({
|
|
1124
|
+
label: 'bootstrap repo',
|
|
1125
|
+
rootDir,
|
|
1126
|
+
rel: 'scripts/install.mjs',
|
|
1127
|
+
// Self-hosting: always clone the Happier monorepo from upstream.
|
|
1128
|
+
// Light flavor uses embedded Postgres (PGlite) and is handled by the server package itself.
|
|
1129
|
+
args: [`--server=${serverComponent}`, '--upstream', '--clone', ...bootstrapExtraArgs],
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// 4) Optional: install autostart (macOS launchd / Linux systemd user).
|
|
1134
|
+
if (autostartWanted) {
|
|
1135
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
1136
|
+
// eslint-disable-next-line no-console
|
|
1137
|
+
console.log(dim(`Autostart skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
|
|
1138
|
+
} else {
|
|
1139
|
+
if (process.platform === 'linux') {
|
|
1140
|
+
const ok = await ensureSystemdAvailable();
|
|
1141
|
+
if (!ok) {
|
|
1142
|
+
// eslint-disable-next-line no-console
|
|
1143
|
+
console.log('[setup] autostart skipped: systemd user services not available on this Linux distro.');
|
|
1144
|
+
} else {
|
|
1145
|
+
await installService();
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
await installService();
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// 5) Optional: install menubar assets (macOS only).
|
|
1154
|
+
if (menubarWanted && process.platform === 'darwin') {
|
|
1155
|
+
if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
1156
|
+
// eslint-disable-next-line no-console
|
|
1157
|
+
console.log(dim(`Menu bar install skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
|
|
1158
|
+
} else {
|
|
1159
|
+
await runNodeScript({ rootDir, rel: 'scripts/menubar.mjs', args: ['install'] });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// 6) Optional: enable tailscale serve (best-effort).
|
|
1164
|
+
if (tailscaleWanted) {
|
|
1165
|
+
const tailscaleOk = await commandExists('tailscale');
|
|
1166
|
+
if (!tailscaleOk) {
|
|
1167
|
+
// eslint-disable-next-line no-console
|
|
1168
|
+
console.log(`${yellow('!')} Tailscale not installed. To enable remote HTTPS later: ${cyan('hstack tailscale enable')}`);
|
|
1169
|
+
await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
|
|
1170
|
+
} else if (isSandboxed() && !sandboxAllowsGlobalSideEffects()) {
|
|
1171
|
+
// eslint-disable-next-line no-console
|
|
1172
|
+
console.log(dim(`Tailscale enable skipped in sandbox mode. To allow: ${cyan('HAPPIER_STACK_SANDBOX_ALLOW_GLOBAL=1')}`));
|
|
1173
|
+
} else {
|
|
1174
|
+
try {
|
|
1175
|
+
const internalPort = await resolveMainServerPort();
|
|
1176
|
+
const internalServerUrl = `http://127.0.0.1:${internalPort}`;
|
|
1177
|
+
const res = await tailscaleServeEnable({ internalServerUrl });
|
|
1178
|
+
if (res?.enableUrl && !res?.httpsUrl) {
|
|
1179
|
+
// eslint-disable-next-line no-console
|
|
1180
|
+
console.log('[setup] tailscale serve requires enabling in your tailnet. Open this URL to continue:');
|
|
1181
|
+
// eslint-disable-next-line no-console
|
|
1182
|
+
console.log(res.enableUrl);
|
|
1183
|
+
// Best-effort open
|
|
1184
|
+
await openUrlInBrowser(res.enableUrl).catch(() => {});
|
|
1185
|
+
}
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
// eslint-disable-next-line no-console
|
|
1188
|
+
console.log('[setup] tailscale not available. Install it from: https://tailscale.com/download');
|
|
1189
|
+
await openUrlInBrowser('https://tailscale.com/download').catch(() => {});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// 7) Optional: start now (without requiring setup to keep running).
|
|
1195
|
+
if (startNow) {
|
|
1196
|
+
// Self-host UX: ensure the prebuilt UI exists so the light server can serve it immediately.
|
|
1197
|
+
// (Without this, `hstack start` will refuse to serve UI when the build dir is missing.)
|
|
1198
|
+
const serveUiWanted = (process.env.HAPPIER_STACK_SERVE_UI ?? '1').toString().trim() !== '0';
|
|
1199
|
+
if (profile === 'selfhost' && serveUiWanted) {
|
|
1200
|
+
const uiBuildDir = process.env.HAPPIER_STACK_UI_BUILD_DIR?.trim()
|
|
1201
|
+
? process.env.HAPPIER_STACK_UI_BUILD_DIR.trim()
|
|
1202
|
+
: join(getDefaultAutostartPaths().baseDir, 'ui');
|
|
1203
|
+
if (!existsSync(uiBuildDir)) {
|
|
1204
|
+
await runNodeScriptMaybeQuiet({ label: 'build-ui', rel: 'scripts/build.mjs', args: ['--no-tauri'] });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const port = await resolveMainServerPort();
|
|
1209
|
+
const internalServerUrl = `http://127.0.0.1:${port}`;
|
|
1210
|
+
|
|
1211
|
+
if (!autostartWanted) {
|
|
1212
|
+
// Detached background start.
|
|
1213
|
+
await spawnDetachedNodeScript({ rootDir, rel: 'scripts/run.mjs', args: [] });
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const ready = await waitForHappierHealthOk(internalServerUrl, { timeoutMs: 90_000 });
|
|
1217
|
+
if (!ready) {
|
|
1218
|
+
// eslint-disable-next-line no-console
|
|
1219
|
+
console.log(`[setup] started, but server did not become healthy yet: ${internalServerUrl}`);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Prefer tailscale HTTPS URL if available.
|
|
1223
|
+
let openTarget = `http://localhost:${port}/`;
|
|
1224
|
+
if (tailscaleWanted) {
|
|
1225
|
+
const https = await tailscaleServeHttpsUrlForInternalServerUrl(internalServerUrl);
|
|
1226
|
+
if (https) {
|
|
1227
|
+
openTarget = https.replace(/\/+$/, '') + '/';
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 8) Optional: auth login (runs interactive browser flow via happier-cli).
|
|
1232
|
+
if (authWanted) {
|
|
1233
|
+
const cliHomeDir = mainCliHomeDirForEnvPath(resolveStackEnvPath('main').envPath);
|
|
1234
|
+
const existingCredentialPath = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl });
|
|
1235
|
+
if (existingCredentialPath) {
|
|
1236
|
+
// eslint-disable-next-line no-console
|
|
1237
|
+
console.log(`[setup] auth: already configured (${existingCredentialPath} exists)`);
|
|
1238
|
+
} else {
|
|
1239
|
+
const env = {
|
|
1240
|
+
...process.env,
|
|
1241
|
+
HAPPIER_STACK_SERVER_PORT: String(port),
|
|
1242
|
+
};
|
|
1243
|
+
if (interactive) {
|
|
1244
|
+
await runOrchestratedGuidedAuthFlow({
|
|
1245
|
+
rootDir,
|
|
1246
|
+
stackName: 'main',
|
|
1247
|
+
env,
|
|
1248
|
+
verbosity,
|
|
1249
|
+
json: false,
|
|
1250
|
+
});
|
|
1251
|
+
} else {
|
|
1252
|
+
await runNodeScript({ rootDir, rel: 'scripts/stack.mjs', args: ['auth', 'main', '--', 'login'], env });
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const credentialAfterLogin = findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl });
|
|
1256
|
+
if (!credentialAfterLogin) {
|
|
1257
|
+
// eslint-disable-next-line no-console
|
|
1258
|
+
console.log('[setup] auth: not completed yet (missing credentials). You can retry with: hstack auth login');
|
|
1259
|
+
} else {
|
|
1260
|
+
// eslint-disable-next-line no-console
|
|
1261
|
+
console.log('[setup] auth: complete');
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
// eslint-disable-next-line no-console
|
|
1266
|
+
console.log('[setup] tip: when you are ready, authenticate with: hstack auth login');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
await openUrlInBrowser(openTarget).catch(() => {});
|
|
1270
|
+
// eslint-disable-next-line no-console
|
|
1271
|
+
console.log(`[setup] open: ${openTarget}`);
|
|
1272
|
+
}
|
|
1273
|
+
if (profile === 'selfhost' && authWanted && !startNow) {
|
|
1274
|
+
// eslint-disable-next-line no-console
|
|
1275
|
+
console.log('[setup] auth: skipped because Happier was not started. When ready:');
|
|
1276
|
+
// eslint-disable-next-line no-console
|
|
1277
|
+
console.log(' hstack start');
|
|
1278
|
+
// eslint-disable-next-line no-console
|
|
1279
|
+
console.log(' hstack auth login');
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Final tips (keep short).
|
|
1283
|
+
if (profile === 'selfhost') {
|
|
1284
|
+
// eslint-disable-next-line no-console
|
|
1285
|
+
console.log('');
|
|
1286
|
+
// eslint-disable-next-line no-console
|
|
1287
|
+
console.log(green('✓ Setup complete'));
|
|
1288
|
+
// Keep this minimal for first-time users. Setup already started + opened the UI.
|
|
1289
|
+
// eslint-disable-next-line no-console
|
|
1290
|
+
console.log(dim('Happier is ready. If you need help later, run:'));
|
|
1291
|
+
// eslint-disable-next-line no-console
|
|
1292
|
+
console.log(` ${yellow('hstack doctor')}`);
|
|
1293
|
+
// eslint-disable-next-line no-console
|
|
1294
|
+
console.log(` ${yellow('hstack stop --yes')}`);
|
|
1295
|
+
} else {
|
|
1296
|
+
// eslint-disable-next-line no-console
|
|
1297
|
+
console.log('');
|
|
1298
|
+
// eslint-disable-next-line no-console
|
|
1299
|
+
console.log(green('✓ Setup complete'));
|
|
1300
|
+
// eslint-disable-next-line no-console
|
|
1301
|
+
console.log(dim('Next steps (development):'));
|
|
1302
|
+
// eslint-disable-next-line no-console
|
|
1303
|
+
console.log(` ${yellow('hstack stack new dev --interactive')} ${dim('# create a dedicated dev stack (recommended)')}`);
|
|
1304
|
+
// eslint-disable-next-line no-console
|
|
1305
|
+
console.log(` ${yellow('hstack stack dev dev')} ${dim('# run that stack (server + daemon + Expo web)')}`);
|
|
1306
|
+
// eslint-disable-next-line no-console
|
|
1307
|
+
console.log(` ${yellow('hstack wt new ...')} ${dim('# create a worktree for a branch/PR')}`);
|
|
1308
|
+
// eslint-disable-next-line no-console
|
|
1309
|
+
console.log(` ${yellow('hstack stack new ...')} ${dim('# create an isolated runtime stack')}`);
|
|
1310
|
+
// eslint-disable-next-line no-console
|
|
1311
|
+
console.log(` ${yellow('hstack stack dev <name>')} ${dim('# run a specific stack')}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function main() {
|
|
1316
|
+
const rootDir = getRootDir(import.meta.url);
|
|
1317
|
+
const argv = process.argv.slice(2);
|
|
1318
|
+
await cmdSetup({ rootDir, argv });
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
main().catch((err) => {
|
|
1322
|
+
console.error('[setup] failed:', err);
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
});
|