@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,1160 @@
|
|
|
1
|
+
import { spawnProc, run, runCapture } from './utils/proc/proc.mjs';
|
|
2
|
+
import { resolveAuthSeedFromEnv, resolveAutoCopyFromMainEnabled } from './utils/stack/startup.mjs';
|
|
3
|
+
import { getStacksStorageRoot } from './utils/paths/paths.mjs';
|
|
4
|
+
import { runCaptureIfCommandExists } from './utils/proc/commands.mjs';
|
|
5
|
+
import { readLastLines } from './utils/fs/tail.mjs';
|
|
6
|
+
import { ensureCliBuilt } from './utils/proc/pm.mjs';
|
|
7
|
+
import {
|
|
8
|
+
findAnyCredentialPathInCliHome,
|
|
9
|
+
findExistingStackCredentialPath,
|
|
10
|
+
resolvePreferredStackDaemonStatePaths,
|
|
11
|
+
resolveStackDaemonStatePaths,
|
|
12
|
+
resolveStackCredentialPaths,
|
|
13
|
+
} from './utils/auth/credentials_paths.mjs';
|
|
14
|
+
import { decodeJwtPayloadUnsafe } from './utils/auth/decode_jwt_payload_unsafe.mjs';
|
|
15
|
+
import { applyStackActiveServerScopeEnv } from './utils/auth/stable_scope_id.mjs';
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
17
|
+
import { chmod, copyFile, mkdir } from 'node:fs/promises';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
20
|
+
import { getRootDir, resolveStackEnvPath } from './utils/paths/paths.mjs';
|
|
21
|
+
import { parseEnvToObject } from './utils/env/dotenv.mjs';
|
|
22
|
+
import { getCliHomeDirFromEnvOrDefault } from './utils/stack/dirs.mjs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Daemon lifecycle helpers for hstack.
|
|
26
|
+
*
|
|
27
|
+
* Centralizes:
|
|
28
|
+
* - stopping old daemons (stack-scoped)
|
|
29
|
+
* - cleaning stale lock/state
|
|
30
|
+
* - starting daemon and handling first-time auth
|
|
31
|
+
* - printing actionable diagnostics
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
function resolveServerUrlFromOptions(options) {
|
|
35
|
+
if (typeof options === 'string') {
|
|
36
|
+
return options.trim();
|
|
37
|
+
}
|
|
38
|
+
return String(options?.serverUrl ?? '').trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveEnvFromOptions(options) {
|
|
42
|
+
if (options && typeof options === 'object' && options.env && typeof options.env === 'object') {
|
|
43
|
+
return options.env;
|
|
44
|
+
}
|
|
45
|
+
return process.env;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function cleanupStaleDaemonState(homeDir, options = {}) {
|
|
49
|
+
const serverUrl = resolveServerUrlFromOptions(options);
|
|
50
|
+
const env = resolveEnvFromOptions(options);
|
|
51
|
+
const { statePath, lockPath } = resolvePreferredStackDaemonStatePaths({ cliHomeDir: homeDir, serverUrl, env });
|
|
52
|
+
|
|
53
|
+
if (!existsSync(lockPath)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lsofHasPath = async (pid, pathNeedle) => {
|
|
58
|
+
try {
|
|
59
|
+
const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
|
|
60
|
+
return out.includes(pathNeedle);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// If lock PID exists and is running, keep lock/state ONLY if it still owns the lock file path.
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync(lockPath, 'utf-8').trim();
|
|
69
|
+
const pid = Number(raw);
|
|
70
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 0);
|
|
73
|
+
// If PID was recycled, refuse to trust it unless we can prove it's associated with this home dir.
|
|
74
|
+
// This prevents cross-stack daemon kills due to stale lock files.
|
|
75
|
+
if (await lsofHasPath(pid, lockPath)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// stale pid
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If state PID exists and is running, keep lock/state.
|
|
87
|
+
if (existsSync(statePath)) {
|
|
88
|
+
try {
|
|
89
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
90
|
+
const pid = typeof state?.pid === 'number' ? state.pid : null;
|
|
91
|
+
if (pid) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0);
|
|
94
|
+
// Only keep if we can prove it still uses this home dir (via state path).
|
|
95
|
+
if (await lsofHasPath(pid, statePath)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// stale pid
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try { unlinkSync(lockPath); } catch { /* ignore */ }
|
|
108
|
+
try { unlinkSync(statePath); } catch { /* ignore */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function checkDaemonState(cliHomeDir, options = {}) {
|
|
112
|
+
const serverUrl = resolveServerUrlFromOptions(options);
|
|
113
|
+
const env = resolveEnvFromOptions(options);
|
|
114
|
+
const { statePath, lockPath } = resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl, env });
|
|
115
|
+
|
|
116
|
+
const alive = isPidAlive;
|
|
117
|
+
|
|
118
|
+
if (existsSync(statePath)) {
|
|
119
|
+
try {
|
|
120
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
121
|
+
const pid = Number(state?.pid);
|
|
122
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
123
|
+
return alive(pid) ? { status: 'running', pid } : { status: 'stale_state', pid };
|
|
124
|
+
}
|
|
125
|
+
return { status: 'bad_state', pid: null };
|
|
126
|
+
} catch {
|
|
127
|
+
return { status: 'bad_state', pid: null };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (existsSync(lockPath)) {
|
|
132
|
+
try {
|
|
133
|
+
const pid = Number(readFileSync(lockPath, 'utf-8').trim());
|
|
134
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
135
|
+
return alive(pid) ? { status: 'starting', pid } : { status: 'stale_lock', pid };
|
|
136
|
+
}
|
|
137
|
+
return { status: 'bad_lock', pid: null };
|
|
138
|
+
} catch {
|
|
139
|
+
return { status: 'bad_lock', pid: null };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const fallback = findRunningDaemonStateInHome(cliHomeDir, alive);
|
|
144
|
+
if (fallback) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { status: 'stopped', pid: null };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isPidAlive(pid) {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(pid, 0);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findRunningDaemonStateInHome(cliHomeDir, alive) {
|
|
161
|
+
try {
|
|
162
|
+
const serversDir = join(cliHomeDir, 'servers');
|
|
163
|
+
const entries = readdirSync(serversDir, { withFileTypes: true }).filter((ent) => ent.isDirectory());
|
|
164
|
+
const matches = [];
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const statePath = join(serversDir, entry.name, 'daemon.state.json');
|
|
167
|
+
if (!existsSync(statePath)) continue;
|
|
168
|
+
let state;
|
|
169
|
+
try {
|
|
170
|
+
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const pid = Number(state?.pid);
|
|
175
|
+
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
176
|
+
if (!alive(pid)) continue;
|
|
177
|
+
matches.push({ status: 'running', pid });
|
|
178
|
+
}
|
|
179
|
+
if (matches.length === 1) return matches[0];
|
|
180
|
+
if (matches.length > 1 && process.env.DEBUG) {
|
|
181
|
+
const pids = matches.map((m) => m.pid).join(', ');
|
|
182
|
+
console.warn(`[daemon] multiple running daemons detected for ${cliHomeDir} (pids: ${pids}); reporting stopped`);
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isDaemonRunning(cliHomeDir, options = {}) {
|
|
191
|
+
const s = checkDaemonState(cliHomeDir, options);
|
|
192
|
+
return s.status === 'running' || s.status === 'starting';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readDaemonPsEnv(pid) {
|
|
196
|
+
const n = Number(pid);
|
|
197
|
+
if (!Number.isFinite(n) || n <= 1) return null;
|
|
198
|
+
if (process.platform === 'win32') return null;
|
|
199
|
+
try {
|
|
200
|
+
const out = await runCapture('ps', ['eww', '-p', String(n)]);
|
|
201
|
+
const lines = out.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
202
|
+
// Usually: header + one line.
|
|
203
|
+
return lines.length >= 2 ? lines[1] : lines[0] ?? null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function daemonEnvMatches({ pid, cliHomeDir, internalServerUrl, publicServerUrl }) {
|
|
210
|
+
const line = await readDaemonPsEnv(pid);
|
|
211
|
+
if (!line) return null; // unknown
|
|
212
|
+
const home = String(cliHomeDir ?? '').trim();
|
|
213
|
+
const server = String(internalServerUrl ?? '').trim();
|
|
214
|
+
const web = String(publicServerUrl ?? '').trim();
|
|
215
|
+
|
|
216
|
+
// Must be for the same stack home dir.
|
|
217
|
+
if (home && !line.includes(`HAPPIER_HOME_DIR=${home}`)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
// If we have a desired server URL, require it (prevents ephemeral port mismatches).
|
|
221
|
+
if (server && !line.includes(`HAPPIER_SERVER_URL=${server}`)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
// Public URL mismatch is less fatal, but prefer it stable too when provided.
|
|
225
|
+
if (web && !line.includes(`HAPPIER_WEBAPP_URL=${web}`)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getLatestDaemonLogPath(homeDir) {
|
|
232
|
+
try {
|
|
233
|
+
const logsDir = join(homeDir, 'logs');
|
|
234
|
+
const files = readdirSync(logsDir).filter((f) => f.endsWith('-daemon.log')).sort();
|
|
235
|
+
if (!files.length) return null;
|
|
236
|
+
return join(logsDir, files[files.length - 1]);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveHappierCliDistEntrypoint(cliBin) {
|
|
243
|
+
const bin = String(cliBin ?? '').trim();
|
|
244
|
+
if (!bin) return null;
|
|
245
|
+
// In component checkouts/worktrees we launch via <cliDir>/bin/happier.mjs, which expects dist output.
|
|
246
|
+
// Use this to protect restarts from bricking the running daemon if dist disappears mid-build.
|
|
247
|
+
try {
|
|
248
|
+
const binDir = dirname(bin);
|
|
249
|
+
return join(binDir, '..', 'dist', 'index.mjs');
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function extractRelativeMjsImportSpecifiers(source) {
|
|
256
|
+
const specs = new Set();
|
|
257
|
+
const patterns = [
|
|
258
|
+
/(?:^|[^\w$])import\s+(?:[^'"]*?\s+from\s*)?['"]([^'"]+)['"]/gm,
|
|
259
|
+
/(?:^|[^\w$])export\s+[^'"]*?\s+from\s*['"]([^'"]+)['"]/gm,
|
|
260
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
|
|
261
|
+
];
|
|
262
|
+
for (const re of patterns) {
|
|
263
|
+
for (const match of source.matchAll(re)) {
|
|
264
|
+
const spec = String(match?.[1] ?? '').trim();
|
|
265
|
+
if (!spec || !spec.startsWith('.')) continue;
|
|
266
|
+
if (!spec.endsWith('.mjs')) continue;
|
|
267
|
+
specs.add(spec);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [...specs];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function findMissingDistModules(entrypoint, maxFiles = 400) {
|
|
274
|
+
const missing = [];
|
|
275
|
+
const seen = new Set();
|
|
276
|
+
const queue = [entrypoint];
|
|
277
|
+
while (queue.length > 0 && seen.size < maxFiles) {
|
|
278
|
+
const filePath = queue.shift();
|
|
279
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
280
|
+
seen.add(filePath);
|
|
281
|
+
|
|
282
|
+
let source = '';
|
|
283
|
+
try {
|
|
284
|
+
source = readFileSync(filePath, 'utf-8');
|
|
285
|
+
} catch {
|
|
286
|
+
missing.push(filePath);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const imports = extractRelativeMjsImportSpecifiers(source);
|
|
291
|
+
for (const spec of imports) {
|
|
292
|
+
const target = join(dirname(filePath), spec);
|
|
293
|
+
if (!existsSync(target)) {
|
|
294
|
+
missing.push(target);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (!seen.has(target)) {
|
|
298
|
+
queue.push(target);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return missing;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function ensureHappierCliDistExists({ cliBin }) {
|
|
306
|
+
const distEntrypoint = resolveHappierCliDistEntrypoint(cliBin);
|
|
307
|
+
if (!distEntrypoint) return { ok: false, distEntrypoint: null, built: false, reason: 'unknown_cli_bin' };
|
|
308
|
+
const cliDir = join(dirname(cliBin), '..');
|
|
309
|
+
const buildCli =
|
|
310
|
+
(process.env.HAPPIER_STACK_CLI_BUILD ?? '1').toString().trim() !== '0';
|
|
311
|
+
|
|
312
|
+
const readIntegrity = () => {
|
|
313
|
+
if (!existsSync(distEntrypoint)) return { ok: false, reason: 'missing_entrypoint' };
|
|
314
|
+
const missing = findMissingDistModules(distEntrypoint);
|
|
315
|
+
if (missing.length === 0) return { ok: true, reason: 'exists' };
|
|
316
|
+
return { ok: false, reason: `incomplete:${missing[0]}` };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Fast path: if dist exists and import graph is complete, never trigger rebuild here.
|
|
320
|
+
// Rebuilding inside daemon restart can race with live restarts and transiently remove dist/.
|
|
321
|
+
const before = readIntegrity();
|
|
322
|
+
if (before.ok) {
|
|
323
|
+
return { ok: true, distEntrypoint, built: false, reason: before.reason };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Try to recover automatically: missing dist is a common first-run worktree issue.
|
|
327
|
+
// We build in-place using the cliDir that owns this cliBin (../ from bin/).
|
|
328
|
+
if (!buildCli) {
|
|
329
|
+
return { ok: false, distEntrypoint, built: false, reason: before.reason };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let buildRes = null;
|
|
333
|
+
try {
|
|
334
|
+
// In auto mode, ensureCliBuilt() is a fast no-op when nothing changed.
|
|
335
|
+
buildRes = await ensureCliBuilt(cliDir, { buildCli: true });
|
|
336
|
+
if (buildRes?.built) {
|
|
337
|
+
// eslint-disable-next-line no-console
|
|
338
|
+
console.warn(`[local] happier-cli: rebuilt (${cliDir})`);
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return { ok: false, distEntrypoint, built: false, reason: String(e?.message ?? e) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const after = readIntegrity();
|
|
345
|
+
if (after.ok) {
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
distEntrypoint,
|
|
349
|
+
built: Boolean(buildRes?.built),
|
|
350
|
+
reason: buildRes?.built ? (buildRes.reason ?? 'rebuilt') : 'exists',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
distEntrypoint,
|
|
356
|
+
built: Boolean(buildRes?.built),
|
|
357
|
+
reason: after.reason,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function excerptIndicatesMissingAuth(excerpt) {
|
|
362
|
+
if (!excerpt) return false;
|
|
363
|
+
return (
|
|
364
|
+
excerpt.includes('[AUTH] No credentials found') ||
|
|
365
|
+
excerpt.includes('No credentials found, starting authentication flow')
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function excerptIndicatesInvalidAuth(excerpt) {
|
|
370
|
+
if (!excerpt) return false;
|
|
371
|
+
return (
|
|
372
|
+
excerpt.includes('Auth failed - invalid token') ||
|
|
373
|
+
excerpt.includes('Request failed with status code 401') ||
|
|
374
|
+
excerpt.includes('"status":401') ||
|
|
375
|
+
excerpt.includes('[DAEMON RUN][FATAL]') && excerpt.includes('status code 401')
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function allowDaemonWaitForAuthWithoutTty() {
|
|
380
|
+
const raw = (process.env.HAPPIER_STACK_DAEMON_WAIT_FOR_AUTH ?? '').toString().trim().toLowerCase();
|
|
381
|
+
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'y';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function authLoginHint({ stackName, cliIdentity }) {
|
|
385
|
+
const id = (cliIdentity ?? '').toString().trim();
|
|
386
|
+
const suffix = id && id !== 'default' ? ` --identity=${id} --no-open` : '';
|
|
387
|
+
return stackName === 'main' ? `hstack auth login${suffix}` : `hstack stack auth ${stackName} login${suffix}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function authCopyFromSeedHint({ stackName, cliIdentity, env = process.env }) {
|
|
391
|
+
if (stackName === 'main') return null;
|
|
392
|
+
// For multi-identity daemons, copying credentials defeats the purpose (multiple accounts).
|
|
393
|
+
const id = (cliIdentity ?? '').toString().trim();
|
|
394
|
+
if (id && id !== 'default') return null;
|
|
395
|
+
const seed = resolveAuthSeedFromEnv(env);
|
|
396
|
+
return `hstack stack auth ${stackName} copy-from ${seed}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function maybeAutoReseedInvalidAuth({
|
|
400
|
+
stackName,
|
|
401
|
+
cliHomeDir,
|
|
402
|
+
internalServerUrl,
|
|
403
|
+
env = process.env,
|
|
404
|
+
quiet = false,
|
|
405
|
+
}) {
|
|
406
|
+
if (stackName === 'main') return { ok: false, skipped: true, reason: 'main' };
|
|
407
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
408
|
+
const enabled = resolveAutoCopyFromMainEnabled({ env, stackName, isInteractive });
|
|
409
|
+
if (!enabled) return { ok: false, skipped: true, reason: 'disabled' };
|
|
410
|
+
|
|
411
|
+
const seed = resolveAuthSeedFromEnv(env);
|
|
412
|
+
const allowAccountSwitch =
|
|
413
|
+
(env.HAPPIER_STACK_AUTO_AUTH_RESEED_ALLOW_ACCOUNT_SWITCH ?? '').toString().trim() === '1';
|
|
414
|
+
const guard = shouldSkipAutoReseedForDifferentAccount({
|
|
415
|
+
stackName,
|
|
416
|
+
seed,
|
|
417
|
+
cliHomeDir,
|
|
418
|
+
internalServerUrl,
|
|
419
|
+
env,
|
|
420
|
+
});
|
|
421
|
+
if (guard.skip && !allowAccountSwitch) {
|
|
422
|
+
return { ok: false, skipped: true, reason: guard.reason, seed };
|
|
423
|
+
}
|
|
424
|
+
if (!quiet) {
|
|
425
|
+
console.log(`[local] auth: invalid token detected; re-seeding ${stackName} from ${seed}...`);
|
|
426
|
+
}
|
|
427
|
+
const rootDir = getRootDir(import.meta.url);
|
|
428
|
+
|
|
429
|
+
// Use stack-scoped auth copy so env/database resolution is correct for the target stack.
|
|
430
|
+
await run(process.execPath, [join(rootDir, 'scripts', 'stack.mjs'), 'auth', stackName, '--', 'copy-from', seed, '--force'], {
|
|
431
|
+
cwd: rootDir,
|
|
432
|
+
env,
|
|
433
|
+
});
|
|
434
|
+
return { ok: true, skipped: false, seed };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function readAuthTokenFromCredentialFile(path) {
|
|
438
|
+
const p = String(path ?? '').trim();
|
|
439
|
+
if (!p || !existsSync(p)) return null;
|
|
440
|
+
try {
|
|
441
|
+
const raw = readFileSync(p, 'utf-8').trim();
|
|
442
|
+
if (!raw) return null;
|
|
443
|
+
try {
|
|
444
|
+
const parsed = JSON.parse(raw);
|
|
445
|
+
if (typeof parsed?.token === 'string' && parsed.token.trim()) return parsed.token.trim();
|
|
446
|
+
} catch {
|
|
447
|
+
// fall back below
|
|
448
|
+
}
|
|
449
|
+
// Legacy fallback: treat plain file content as token.
|
|
450
|
+
return raw;
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resolveStackCliHomeDirFromStackEnv({ stackName, env = process.env }) {
|
|
457
|
+
const { baseDir, envPath } = resolveStackEnvPath(stackName, env);
|
|
458
|
+
let stackEnv = {};
|
|
459
|
+
try {
|
|
460
|
+
if (existsSync(envPath)) {
|
|
461
|
+
stackEnv = parseEnvToObject(readFileSync(envPath, 'utf-8'));
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
stackEnv = {};
|
|
465
|
+
}
|
|
466
|
+
return getCliHomeDirFromEnvOrDefault({ stackBaseDir: baseDir, env: stackEnv });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function shouldSkipAutoReseedForDifferentAccount({
|
|
470
|
+
stackName,
|
|
471
|
+
seed,
|
|
472
|
+
cliHomeDir,
|
|
473
|
+
internalServerUrl,
|
|
474
|
+
env = process.env,
|
|
475
|
+
}) {
|
|
476
|
+
const targetCredentialPath =
|
|
477
|
+
findExistingStackCredentialPath({ cliHomeDir, serverUrl: internalServerUrl, env }) ??
|
|
478
|
+
findAnyCredentialPathInCliHome({ cliHomeDir });
|
|
479
|
+
if (!targetCredentialPath) return { skip: false, reason: null };
|
|
480
|
+
|
|
481
|
+
const sourceCliHomeDir = resolveStackCliHomeDirFromStackEnv({ stackName: seed, env });
|
|
482
|
+
const sourceCredentialPath =
|
|
483
|
+
findAnyCredentialPathInCliHome({ cliHomeDir: sourceCliHomeDir }) ??
|
|
484
|
+
findExistingStackCredentialPath({ cliHomeDir: sourceCliHomeDir, serverUrl: internalServerUrl, env });
|
|
485
|
+
if (!sourceCredentialPath) return { skip: false, reason: null };
|
|
486
|
+
|
|
487
|
+
const targetToken = readAuthTokenFromCredentialFile(targetCredentialPath);
|
|
488
|
+
const sourceToken = readAuthTokenFromCredentialFile(sourceCredentialPath);
|
|
489
|
+
if (!targetToken || !sourceToken) return { skip: false, reason: null };
|
|
490
|
+
|
|
491
|
+
const targetPayload = decodeJwtPayloadUnsafe(targetToken);
|
|
492
|
+
const sourcePayload = decodeJwtPayloadUnsafe(sourceToken);
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
targetPayload?.sub &&
|
|
496
|
+
sourcePayload?.sub &&
|
|
497
|
+
String(targetPayload.sub) !== String(sourcePayload.sub)
|
|
498
|
+
) {
|
|
499
|
+
return { skip: true, reason: 'different-account' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Conservative guard for non-JWT/opaque tokens: if values differ, avoid silently overwriting
|
|
503
|
+
// potentially manual credentials.
|
|
504
|
+
if (!targetPayload?.sub && !sourcePayload?.sub && targetToken !== sourceToken) {
|
|
505
|
+
return { skip: true, reason: 'different-token' };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return { skip: false, reason: null };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function seedCredentialsIfMissing({ cliHomeDir }) {
|
|
512
|
+
const stacksRoot = getStacksStorageRoot();
|
|
513
|
+
|
|
514
|
+
const sources = [
|
|
515
|
+
// New layout: main stack credentials (preferred).
|
|
516
|
+
join(stacksRoot, 'main', 'cli'),
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
const copyIfMissing = async ({ relPath, mode, label }) => {
|
|
520
|
+
const target = join(cliHomeDir, relPath);
|
|
521
|
+
if (existsSync(target)) {
|
|
522
|
+
return { copied: false, source: null, target };
|
|
523
|
+
}
|
|
524
|
+
const sourceDir = sources.find((d) => existsSync(join(d, relPath)));
|
|
525
|
+
if (!sourceDir) {
|
|
526
|
+
return { copied: false, source: null, target };
|
|
527
|
+
}
|
|
528
|
+
const source = join(sourceDir, relPath);
|
|
529
|
+
await mkdir(cliHomeDir, { recursive: true });
|
|
530
|
+
await copyFile(source, target);
|
|
531
|
+
await chmod(target, mode).catch(() => {});
|
|
532
|
+
console.log(`[local] migrated ${label}: ${source} -> ${target}`);
|
|
533
|
+
return { copied: true, source, target };
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const copyCredentialIfMissing = async () => {
|
|
537
|
+
const target = join(cliHomeDir, 'access.key');
|
|
538
|
+
if (existsSync(target)) {
|
|
539
|
+
return { copied: false, source: null, target };
|
|
540
|
+
}
|
|
541
|
+
const source = sources
|
|
542
|
+
.map((sourceCli) => findAnyCredentialPathInCliHome({ cliHomeDir: sourceCli }))
|
|
543
|
+
.find(Boolean);
|
|
544
|
+
if (!source) {
|
|
545
|
+
return { copied: false, source: null, target };
|
|
546
|
+
}
|
|
547
|
+
await mkdir(cliHomeDir, { recursive: true });
|
|
548
|
+
await copyFile(source, target);
|
|
549
|
+
await chmod(target, 0o600).catch(() => {});
|
|
550
|
+
console.log(`[local] migrated CLI credentials (access.key): ${source} -> ${target}`);
|
|
551
|
+
return { copied: true, source, target };
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// access.key holds the auth token + encryption material (keep tight permissions)
|
|
555
|
+
const access = await copyCredentialIfMissing().catch((err) => {
|
|
556
|
+
console.warn(`[local] failed to migrate CLI credentials into ${cliHomeDir}:`, err);
|
|
557
|
+
return { copied: false, source: null, target: join(cliHomeDir, 'access.key') };
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// settings.json holds machineId and other client state; migrate to keep your machine identity stable.
|
|
561
|
+
const settings = await copyIfMissing({ relPath: 'settings.json', mode: 0o600, label: 'CLI settings (settings.json)' })
|
|
562
|
+
.catch((err) => {
|
|
563
|
+
console.warn(`[local] failed to migrate CLI settings into ${cliHomeDir}:`, err);
|
|
564
|
+
return { copied: false, source: null, target: join(cliHomeDir, 'settings.json') };
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return { ok: true, copied: access.copied || settings.copied, access, settings };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function ensureServerScopedCredentialsFromLegacy({ cliHomeDir, internalServerUrl, env = process.env }) {
|
|
571
|
+
const resolved = resolveStackCredentialPaths({ cliHomeDir, serverUrl: internalServerUrl, env });
|
|
572
|
+
if (existsSync(resolved.serverScopedPath) || !existsSync(resolved.legacyPath)) {
|
|
573
|
+
return { copied: false, source: null, target: resolved.serverScopedPath, paths: resolved.paths };
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
await mkdir(dirname(resolved.serverScopedPath), { recursive: true });
|
|
577
|
+
await copyFile(resolved.legacyPath, resolved.serverScopedPath);
|
|
578
|
+
await chmod(resolved.serverScopedPath, 0o600).catch(() => {});
|
|
579
|
+
return { copied: true, source: resolved.legacyPath, target: resolved.serverScopedPath, paths: resolved.paths };
|
|
580
|
+
} catch {
|
|
581
|
+
return { copied: false, source: null, target: resolved.serverScopedPath, paths: resolved.paths };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function killDaemonFromLockFile({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
586
|
+
const { statePath, lockPath } = resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl, env });
|
|
587
|
+
const daemonStatePaths = resolveStackDaemonStatePaths({ cliHomeDir, serverUrl, env });
|
|
588
|
+
if (!existsSync(lockPath)) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let pid = null;
|
|
593
|
+
try {
|
|
594
|
+
const raw = readFileSync(lockPath, 'utf-8').trim();
|
|
595
|
+
const n = Number(raw);
|
|
596
|
+
if (Number.isFinite(n) && n > 0) {
|
|
597
|
+
pid = n;
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
// ignore
|
|
601
|
+
}
|
|
602
|
+
if (!pid) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// If pid is alive, confirm it looks like a happier daemon and terminate it.
|
|
607
|
+
try {
|
|
608
|
+
process.kill(pid, 0);
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let cmd = '';
|
|
614
|
+
try {
|
|
615
|
+
cmd = await runCapture('ps', ['-p', String(pid), '-o', 'command=']);
|
|
616
|
+
} catch {
|
|
617
|
+
cmd = '';
|
|
618
|
+
}
|
|
619
|
+
const looksLikeDaemon = cmd.includes(' daemon ') || cmd.includes('daemon start') || cmd.includes('daemon start-sync');
|
|
620
|
+
if (!looksLikeDaemon) {
|
|
621
|
+
console.warn(`[local] refusing to kill pid ${pid} from lock file (doesn't look like daemon): ${cmd.trim()}`);
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Hard safety: only kill if we can prove the PID is associated with this stack home dir.
|
|
626
|
+
// We do this by checking that `lsof -p <pid>` includes the lock path (or state file path).
|
|
627
|
+
let ownsLock = false;
|
|
628
|
+
try {
|
|
629
|
+
const out = await runCaptureIfCommandExists('lsof', ['-nP', '-p', String(pid)]);
|
|
630
|
+
ownsLock =
|
|
631
|
+
out.includes(lockPath) ||
|
|
632
|
+
out.includes(statePath) ||
|
|
633
|
+
out.includes(daemonStatePaths.legacyStatePath) ||
|
|
634
|
+
out.includes(daemonStatePaths.serverScopedStatePath) ||
|
|
635
|
+
out.includes(join(cliHomeDir, 'logs'));
|
|
636
|
+
} catch {
|
|
637
|
+
ownsLock = false;
|
|
638
|
+
}
|
|
639
|
+
if (!ownsLock) {
|
|
640
|
+
console.warn(
|
|
641
|
+
`[local] refusing to kill pid ${pid} from lock file (could be unrelated; lsof did not show ownership of ${cliHomeDir})`
|
|
642
|
+
);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
process.kill(pid, 'SIGTERM');
|
|
648
|
+
} catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
await delay(500);
|
|
652
|
+
try {
|
|
653
|
+
process.kill(pid, 0);
|
|
654
|
+
// Still alive: hard kill.
|
|
655
|
+
process.kill(pid, 'SIGKILL');
|
|
656
|
+
} catch {
|
|
657
|
+
// exited
|
|
658
|
+
}
|
|
659
|
+
console.log(`[local] killed stuck daemon pid ${pid} (from ${lockPath})`);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function waitForCredentialsFiles({ paths, timeoutMs, isShuttingDown }) {
|
|
664
|
+
const uniquePaths = Array.from(new Set((paths ?? []).map((p) => String(p ?? '').trim()).filter(Boolean)));
|
|
665
|
+
const deadline = Date.now() + timeoutMs;
|
|
666
|
+
while (!isShuttingDown() && Date.now() < deadline) {
|
|
667
|
+
for (const path of uniquePaths) {
|
|
668
|
+
try {
|
|
669
|
+
if (existsSync(path)) {
|
|
670
|
+
const raw = readFileSync(path, 'utf-8').trim();
|
|
671
|
+
if (raw.length > 0) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
// ignore
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await delay(500);
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function getDaemonEnv({
|
|
685
|
+
baseEnv,
|
|
686
|
+
cliHomeDir,
|
|
687
|
+
internalServerUrl,
|
|
688
|
+
publicServerUrl,
|
|
689
|
+
stackName = null,
|
|
690
|
+
cliIdentity = null,
|
|
691
|
+
}) {
|
|
692
|
+
const scopedEnv = applyStackActiveServerScopeEnv({
|
|
693
|
+
env: baseEnv,
|
|
694
|
+
stackName,
|
|
695
|
+
cliIdentity,
|
|
696
|
+
});
|
|
697
|
+
return {
|
|
698
|
+
...scopedEnv,
|
|
699
|
+
HAPPIER_SERVER_URL: internalServerUrl,
|
|
700
|
+
HAPPIER_WEBAPP_URL: publicServerUrl,
|
|
701
|
+
HAPPIER_HOME_DIR: cliHomeDir,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export async function stopLocalDaemon({
|
|
706
|
+
cliBin,
|
|
707
|
+
internalServerUrl,
|
|
708
|
+
cliHomeDir,
|
|
709
|
+
publicServerUrl = '',
|
|
710
|
+
env = process.env,
|
|
711
|
+
stackName = null,
|
|
712
|
+
cliIdentity = null,
|
|
713
|
+
}) {
|
|
714
|
+
const daemonEnv = getDaemonEnv({
|
|
715
|
+
baseEnv: env,
|
|
716
|
+
cliHomeDir,
|
|
717
|
+
internalServerUrl,
|
|
718
|
+
publicServerUrl: publicServerUrl || internalServerUrl,
|
|
719
|
+
stackName,
|
|
720
|
+
cliIdentity,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
try {
|
|
724
|
+
await new Promise((resolve) => {
|
|
725
|
+
const proc = spawnProc('daemon', process.execPath, [cliBin, 'daemon', 'stop'], daemonEnv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
726
|
+
proc.on('exit', () => resolve());
|
|
727
|
+
});
|
|
728
|
+
} catch {
|
|
729
|
+
// ignore
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// If the daemon never wrote daemon.state.json (e.g. it got stuck in auth in a non-interactive context),
|
|
733
|
+
// stopLocalDaemon() can't find it. Fall back to the lock file PID.
|
|
734
|
+
await killDaemonFromLockFile({ cliHomeDir, serverUrl: internalServerUrl, env: daemonEnv });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export async function startLocalDaemonWithAuth({
|
|
738
|
+
cliBin,
|
|
739
|
+
cliHomeDir,
|
|
740
|
+
internalServerUrl,
|
|
741
|
+
publicServerUrl,
|
|
742
|
+
isShuttingDown,
|
|
743
|
+
forceRestart = false,
|
|
744
|
+
env = process.env,
|
|
745
|
+
stackName = null,
|
|
746
|
+
cliIdentity = 'default',
|
|
747
|
+
}) {
|
|
748
|
+
const resolvedStackName =
|
|
749
|
+
(stackName ?? '').toString().trim() ||
|
|
750
|
+
(env.HAPPIER_STACK_STACK ?? '').toString().trim() ||
|
|
751
|
+
'main';
|
|
752
|
+
const resolvedCliIdentity =
|
|
753
|
+
(cliIdentity ?? '').toString().trim() ||
|
|
754
|
+
(env.HAPPIER_STACK_CLI_IDENTITY ?? '').toString().trim() ||
|
|
755
|
+
'default';
|
|
756
|
+
const baseEnv = { ...env };
|
|
757
|
+
const daemonEnv = getDaemonEnv({
|
|
758
|
+
baseEnv,
|
|
759
|
+
cliHomeDir,
|
|
760
|
+
internalServerUrl,
|
|
761
|
+
publicServerUrl,
|
|
762
|
+
stackName: resolvedStackName,
|
|
763
|
+
cliIdentity: resolvedCliIdentity,
|
|
764
|
+
});
|
|
765
|
+
const parseNonNegativeInt = (value, fallback) => {
|
|
766
|
+
const n = Number(value);
|
|
767
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
|
|
768
|
+
};
|
|
769
|
+
const startVerifyTimeoutMs = parseNonNegativeInt(baseEnv.HAPPIER_STACK_DAEMON_START_VERIFY_TIMEOUT_MS, 5_000);
|
|
770
|
+
const startVerifyPollMs = parseNonNegativeInt(baseEnv.HAPPIER_STACK_DAEMON_START_VERIFY_POLL_MS, 125);
|
|
771
|
+
const startVerifyStableMs = parseNonNegativeInt(baseEnv.HAPPIER_STACK_DAEMON_START_VERIFY_STABLE_MS, 750);
|
|
772
|
+
const isTui = (baseEnv.HAPPIER_STACK_TUI ?? '').toString().trim() === '1';
|
|
773
|
+
|
|
774
|
+
const distEntrypoint = resolveHappierCliDistEntrypoint(cliBin);
|
|
775
|
+
const distCheck = await ensureHappierCliDistExists({ cliBin });
|
|
776
|
+
if (!distCheck.ok) {
|
|
777
|
+
const reason = String(distCheck.reason ?? '').trim();
|
|
778
|
+
const missingModule = reason.startsWith('incomplete:') ? reason.slice('incomplete:'.length) : '';
|
|
779
|
+
const detail = missingModule
|
|
780
|
+
? `[local] Missing module referenced by dist entrypoint: ${missingModule}\n`
|
|
781
|
+
: '';
|
|
782
|
+
throw new Error(
|
|
783
|
+
`[local] happier-cli dist entrypoint is missing or incomplete (${distEntrypoint}).\n` +
|
|
784
|
+
`[local] Refusing to start/restart daemon because it would crash with MODULE_NOT_FOUND.\n` +
|
|
785
|
+
detail +
|
|
786
|
+
`[local] Fix: rebuild happier-cli in the active checkout/worktree.\n` +
|
|
787
|
+
(distCheck.reason ? `[local] Detail: ${distCheck.reason}\n` : '')
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// If this is a migrated/new stack home dir, seed credentials from the user's existing login (best-effort)
|
|
792
|
+
// to avoid requiring an interactive auth flow under launchd.
|
|
793
|
+
const migrateCreds = (baseEnv.HAPPIER_STACK_MIGRATE_CREDENTIALS ?? '1').trim() !== '0';
|
|
794
|
+
if (migrateCreds) {
|
|
795
|
+
await seedCredentialsIfMissing({ cliHomeDir });
|
|
796
|
+
}
|
|
797
|
+
const credentialPaths = resolveStackCredentialPaths({ cliHomeDir, serverUrl: internalServerUrl, env: baseEnv });
|
|
798
|
+
const mirrored = await ensureServerScopedCredentialsFromLegacy({ cliHomeDir, internalServerUrl, env: baseEnv });
|
|
799
|
+
if (mirrored.copied) {
|
|
800
|
+
console.log(`[local] migrated daemon credentials to server profile: ${mirrored.source} -> ${mirrored.target}`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const existing = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl, env: daemonEnv });
|
|
804
|
+
// If the daemon is already running and we're restarting it, refuse to stop it unless the
|
|
805
|
+
// happier-cli dist entrypoint exists. Otherwise a rebuild (rm -rf dist) can brick the stack.
|
|
806
|
+
if (
|
|
807
|
+
distEntrypoint &&
|
|
808
|
+
!existsSync(distEntrypoint) &&
|
|
809
|
+
(existing.status === 'running' || existing.status === 'starting')
|
|
810
|
+
) {
|
|
811
|
+
console.warn(
|
|
812
|
+
`[local] happier-cli dist entrypoint is missing (${distEntrypoint}).\n` +
|
|
813
|
+
`[local] Refusing to restart daemon to avoid downtime. Rebuild happier-cli first.`
|
|
814
|
+
);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!forceRestart && existing.status === 'running') {
|
|
819
|
+
const pid = existing.pid;
|
|
820
|
+
const matches = await daemonEnvMatches({ pid, cliHomeDir, internalServerUrl, publicServerUrl });
|
|
821
|
+
if (matches === true) {
|
|
822
|
+
// eslint-disable-next-line no-console
|
|
823
|
+
console.log(`[local] daemon already running for stack home (pid=${pid})`);
|
|
824
|
+
if (isTui) {
|
|
825
|
+
// Emit a daemon-labeled line so `hstack tui` can route it to the daemon pane.
|
|
826
|
+
// (The daemon itself logs to cliHomeDir/logs/*-daemon.log.)
|
|
827
|
+
// eslint-disable-next-line no-console
|
|
828
|
+
console.log(`[daemon] already running (pid=${pid})`);
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (matches === false) {
|
|
833
|
+
// eslint-disable-next-line no-console
|
|
834
|
+
console.warn(
|
|
835
|
+
`[local] daemon is running but pointed at a different server URL; restarting (pid=${pid}).\n` +
|
|
836
|
+
`[local] expected: ${internalServerUrl}\n`
|
|
837
|
+
);
|
|
838
|
+
} else {
|
|
839
|
+
// unknown: best-effort keep running to avoid killing an unrelated process
|
|
840
|
+
// eslint-disable-next-line no-console
|
|
841
|
+
console.warn(`[local] daemon status is running but could not verify env; not restarting (pid=${pid})`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (!forceRestart && existing.status === 'starting') {
|
|
846
|
+
// A lock file without a stable daemon.state.json usually means the daemon never finished starting
|
|
847
|
+
// (common when auth is required but daemon start is non-interactive). Attempt a safe restart.
|
|
848
|
+
// eslint-disable-next-line no-console
|
|
849
|
+
console.warn(`[local] daemon appears stuck starting for stack home (pid=${existing.pid}); restarting...`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Stop any existing daemon for THIS stack home dir.
|
|
853
|
+
try {
|
|
854
|
+
await new Promise((resolve) => {
|
|
855
|
+
const proc = spawnProc('daemon', process.execPath, [cliBin, 'daemon', 'stop'], daemonEnv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
856
|
+
proc.on('exit', () => resolve());
|
|
857
|
+
});
|
|
858
|
+
} catch {
|
|
859
|
+
// ignore
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// If state is missing and stop couldn't find it, force-stop the lock PID (otherwise repeated restarts accumulate daemons).
|
|
863
|
+
await killDaemonFromLockFile({ cliHomeDir, serverUrl: internalServerUrl, env: daemonEnv });
|
|
864
|
+
|
|
865
|
+
// Clean up stale lock/state files that can block daemon start.
|
|
866
|
+
await cleanupStaleDaemonState(cliHomeDir, { serverUrl: internalServerUrl, env: daemonEnv });
|
|
867
|
+
|
|
868
|
+
const startOnce = async () => {
|
|
869
|
+
const waitForRunningStable = async () => {
|
|
870
|
+
const deadline = Date.now() + startVerifyTimeoutMs;
|
|
871
|
+
while (Date.now() < deadline) {
|
|
872
|
+
const stateNow = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl, env: daemonEnv });
|
|
873
|
+
if (stateNow.status === 'running') {
|
|
874
|
+
if (startVerifyStableMs <= 0) return true;
|
|
875
|
+
await delay(startVerifyStableMs);
|
|
876
|
+
const stableState = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl, env: daemonEnv });
|
|
877
|
+
if (stableState.status === 'running') return true;
|
|
878
|
+
}
|
|
879
|
+
await delay(startVerifyPollMs);
|
|
880
|
+
}
|
|
881
|
+
return false;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const exitCode = await new Promise((resolve) => {
|
|
885
|
+
const proc = spawnProc('daemon', process.execPath, [cliBin, 'daemon', 'start'], daemonEnv, {
|
|
886
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
887
|
+
// In TUI mode, stream the daemon-start output so it routes to the daemon pane.
|
|
888
|
+
// (The background daemon itself still logs to files.)
|
|
889
|
+
silent: !isTui,
|
|
890
|
+
});
|
|
891
|
+
proc.on('exit', (code) => resolve(code ?? 0));
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
if (exitCode === 0) {
|
|
895
|
+
const runningStable = await waitForRunningStable();
|
|
896
|
+
if (runningStable) {
|
|
897
|
+
return { ok: true, exitCode, excerpt: null, logPath: null };
|
|
898
|
+
}
|
|
899
|
+
const logPath = getLatestDaemonLogPath(cliHomeDir);
|
|
900
|
+
const excerpt = logPath ? await readLastLines(logPath, 120) : null;
|
|
901
|
+
return { ok: false, exitCode, excerpt, logPath };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Some daemon versions (or transient races) can return non-zero even if the daemon
|
|
905
|
+
// is already running / starting for this stack home dir (e.g. "lock already held").
|
|
906
|
+
// In those cases, fail-open and keep the stack running; callers can still surface
|
|
907
|
+
// daemon status separately.
|
|
908
|
+
await delay(500);
|
|
909
|
+
const stateAfter = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl });
|
|
910
|
+
if (stateAfter.status === 'running') {
|
|
911
|
+
return { ok: true, exitCode, excerpt: null, logPath: null };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const logPath = getLatestDaemonLogPath(cliHomeDir);
|
|
915
|
+
const excerpt = logPath ? await readLastLines(logPath, 120) : null;
|
|
916
|
+
return { ok: false, exitCode, excerpt, logPath };
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const first = await startOnce();
|
|
920
|
+
if (!first.ok) {
|
|
921
|
+
if (first.excerpt) {
|
|
922
|
+
console.error(`[local] daemon failed to start; last daemon log (${first.logPath}):\n${first.excerpt}`);
|
|
923
|
+
} else {
|
|
924
|
+
console.error('[local] daemon failed to start; no daemon log found');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (excerptIndicatesMissingAuth(first.excerpt)) {
|
|
928
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY) || allowDaemonWaitForAuthWithoutTty();
|
|
929
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity, env: baseEnv });
|
|
930
|
+
const hint =
|
|
931
|
+
`[local] daemon is not authenticated yet (expected on first run).\n` +
|
|
932
|
+
`[local] In another terminal, run:\n` +
|
|
933
|
+
`${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}\n` +
|
|
934
|
+
(copyHint ? `[local] Or (recommended if main is already logged in):\n${copyHint}\n` : '');
|
|
935
|
+
if (!isInteractive) {
|
|
936
|
+
throw new Error(`${hint}[local] Non-interactive mode: refusing to wait for credentials.`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.error(
|
|
940
|
+
`${hint}[local] Keeping the server running so you can login.\n` +
|
|
941
|
+
`[local] Waiting for credentials at one of:\n` +
|
|
942
|
+
`${credentialPaths.paths.map((p) => `[local] - ${p}`).join('\n')}`
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const ok = await waitForCredentialsFiles({
|
|
946
|
+
paths: credentialPaths.paths,
|
|
947
|
+
timeoutMs: 10 * 60_000,
|
|
948
|
+
isShuttingDown,
|
|
949
|
+
});
|
|
950
|
+
if (!ok) {
|
|
951
|
+
throw new Error('Timed out waiting for daemon credentials (auth login not completed)');
|
|
952
|
+
}
|
|
953
|
+
await ensureServerScopedCredentialsFromLegacy({ cliHomeDir, internalServerUrl });
|
|
954
|
+
|
|
955
|
+
// If a daemon start attempt was already in-flight (or a previous daemon is already running),
|
|
956
|
+
// avoid a second concurrent start and treat it as success.
|
|
957
|
+
await delay(500);
|
|
958
|
+
const stateAfterCreds = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl });
|
|
959
|
+
if (stateAfterCreds.status === 'running' || stateAfterCreds.status === 'starting') {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
console.log('[local] credentials detected, retrying daemon start...');
|
|
964
|
+
const second = await startOnce();
|
|
965
|
+
if (!second.ok) {
|
|
966
|
+
if (second.excerpt) {
|
|
967
|
+
console.error(`[local] daemon still failed to start; last daemon log (${second.logPath}):\n${second.excerpt}`);
|
|
968
|
+
}
|
|
969
|
+
throw new Error('Failed to start daemon (after credentials were created)');
|
|
970
|
+
}
|
|
971
|
+
} else if (excerptIndicatesInvalidAuth(first.excerpt)) {
|
|
972
|
+
// Credentials exist but are rejected by this server (common when a stack's env/DB was reset,
|
|
973
|
+
// or credentials were copied from a different stack identity).
|
|
974
|
+
let reseedResult = null;
|
|
975
|
+
try {
|
|
976
|
+
reseedResult = await maybeAutoReseedInvalidAuth({
|
|
977
|
+
stackName: resolvedStackName,
|
|
978
|
+
cliHomeDir,
|
|
979
|
+
internalServerUrl,
|
|
980
|
+
env: baseEnv,
|
|
981
|
+
});
|
|
982
|
+
} catch (e) {
|
|
983
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity, env: baseEnv });
|
|
984
|
+
console.error(
|
|
985
|
+
`[local] daemon credentials were rejected by the server (401).\n` +
|
|
986
|
+
`[local] Fix:\n` +
|
|
987
|
+
(copyHint ? `- ${copyHint}\n` : '') +
|
|
988
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
989
|
+
);
|
|
990
|
+
throw e;
|
|
991
|
+
}
|
|
992
|
+
if (!reseedResult?.ok || reseedResult?.skipped) {
|
|
993
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity, env: baseEnv });
|
|
994
|
+
const skippedReason = reseedResult?.reason ?? 'unknown';
|
|
995
|
+
const guardedSkip =
|
|
996
|
+
skippedReason === 'different-account' || skippedReason === 'different-token';
|
|
997
|
+
console.error(
|
|
998
|
+
`[local] daemon credentials were rejected by the server (401).\n` +
|
|
999
|
+
(guardedSkip
|
|
1000
|
+
? `[local] Auto re-seed was skipped to avoid overwriting credentials that do not match the configured seed (${skippedReason}).\n`
|
|
1001
|
+
: `[local] Auto re-seed was skipped (${skippedReason}).\n`) +
|
|
1002
|
+
`[local] Fix:\n` +
|
|
1003
|
+
(guardedSkip ? '' : copyHint ? `- ${copyHint} --force\n` : '') +
|
|
1004
|
+
(guardedSkip && copyHint ? `- ${copyHint} --force # only if you explicitly want to replace this stack auth\n` : '') +
|
|
1005
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
1006
|
+
);
|
|
1007
|
+
throw new Error(`Failed to auto re-seed daemon credentials (${skippedReason})`);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
console.log(`[local] auth re-seeded from ${reseedResult.seed}, retrying daemon start...`);
|
|
1011
|
+
const second = await startOnce();
|
|
1012
|
+
if (!second.ok) {
|
|
1013
|
+
if (excerptIndicatesInvalidAuth(second.excerpt)) {
|
|
1014
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity, env: baseEnv });
|
|
1015
|
+
console.error(
|
|
1016
|
+
`[local] auth re-seed source "${reseedResult.seed}" appears stale (still 401).\n` +
|
|
1017
|
+
`[local] Auto fallback to another auth source is disabled.\n` +
|
|
1018
|
+
`[local] Fix:\n` +
|
|
1019
|
+
(copyHint ? `- ${copyHint} --force\n` : '') +
|
|
1020
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (second.excerpt) {
|
|
1025
|
+
console.error(`[local] daemon still failed to start; last daemon log (${second.logPath}):\n${second.excerpt}`);
|
|
1026
|
+
}
|
|
1027
|
+
throw new Error('Failed to start daemon (after auth re-seed)');
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
const copyHint = authCopyFromSeedHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity, env: baseEnv });
|
|
1031
|
+
console.error(
|
|
1032
|
+
`[local] daemon failed to start (server returned an error).\n` +
|
|
1033
|
+
`[local] Try:\n` +
|
|
1034
|
+
`- hstack doctor\n` +
|
|
1035
|
+
(copyHint ? `- ${copyHint}\n` : '') +
|
|
1036
|
+
`- ${authLoginHint({ stackName: resolvedStackName, cliIdentity: resolvedCliIdentity })}`
|
|
1037
|
+
);
|
|
1038
|
+
throw new Error('Failed to start daemon');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Confirm daemon status (best-effort)
|
|
1043
|
+
try {
|
|
1044
|
+
await run(process.execPath, [cliBin, 'daemon', 'status'], { env: daemonEnv, stdio: 'ignore' });
|
|
1045
|
+
} catch {
|
|
1046
|
+
// ignore
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export async function daemonStatusSummary({
|
|
1051
|
+
cliBin,
|
|
1052
|
+
cliHomeDir,
|
|
1053
|
+
internalServerUrl,
|
|
1054
|
+
publicServerUrl,
|
|
1055
|
+
env = process.env,
|
|
1056
|
+
stackName = null,
|
|
1057
|
+
cliIdentity = null,
|
|
1058
|
+
}) {
|
|
1059
|
+
const daemonEnv = getDaemonEnv({
|
|
1060
|
+
baseEnv: env,
|
|
1061
|
+
cliHomeDir,
|
|
1062
|
+
internalServerUrl,
|
|
1063
|
+
publicServerUrl,
|
|
1064
|
+
stackName,
|
|
1065
|
+
cliIdentity,
|
|
1066
|
+
});
|
|
1067
|
+
const distEntrypoint = resolveHappierCliDistEntrypoint(cliBin);
|
|
1068
|
+
try {
|
|
1069
|
+
return await runCapture(process.execPath, [cliBin, 'daemon', 'status'], { env: daemonEnv });
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
if (isMissingDistStatusError({ error, distEntrypoint })) {
|
|
1072
|
+
return buildDistMissingStatusFallback({
|
|
1073
|
+
cliHomeDir,
|
|
1074
|
+
internalServerUrl,
|
|
1075
|
+
env: daemonEnv,
|
|
1076
|
+
distEntrypoint,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function isMissingDistStatusError({ error, distEntrypoint }) {
|
|
1084
|
+
const text = String(error?.message ?? error ?? '');
|
|
1085
|
+
if (!text.includes('MODULE_NOT_FOUND') && !text.includes('ERR_MODULE_NOT_FOUND')) return false;
|
|
1086
|
+
if (distEntrypoint && text.includes(distEntrypoint)) return true;
|
|
1087
|
+
return text.includes('/dist/index.mjs');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function buildDistMissingStatusFallback({ cliHomeDir, internalServerUrl, env, distEntrypoint }) {
|
|
1091
|
+
const state = checkDaemonState(cliHomeDir, { serverUrl: internalServerUrl, env });
|
|
1092
|
+
const { statePath } = resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl: internalServerUrl, env });
|
|
1093
|
+
|
|
1094
|
+
let stateData = null;
|
|
1095
|
+
try {
|
|
1096
|
+
if (existsSync(statePath)) {
|
|
1097
|
+
stateData = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
stateData = null;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const statusLine =
|
|
1104
|
+
state.status === 'running'
|
|
1105
|
+
? 'â Daemon is running'
|
|
1106
|
+
: state.status === 'starting'
|
|
1107
|
+
? 'â Daemon is starting'
|
|
1108
|
+
: 'â Daemon is not running';
|
|
1109
|
+
|
|
1110
|
+
const redactedState =
|
|
1111
|
+
stateData && typeof stateData === 'object'
|
|
1112
|
+
? {
|
|
1113
|
+
...stateData,
|
|
1114
|
+
...(Object.prototype.hasOwnProperty.call(stateData, 'controlToken') ? { controlToken: '<redacted>' } : {}),
|
|
1115
|
+
}
|
|
1116
|
+
: null;
|
|
1117
|
+
|
|
1118
|
+
const lines = [
|
|
1119
|
+
'đŠē Happier CLI Doctor',
|
|
1120
|
+
'',
|
|
1121
|
+
'',
|
|
1122
|
+
'đ¤ Daemon Status',
|
|
1123
|
+
statusLine,
|
|
1124
|
+
];
|
|
1125
|
+
|
|
1126
|
+
const pid = Number(stateData?.pid ?? state?.pid);
|
|
1127
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
1128
|
+
lines.push(` PID: ${pid}`);
|
|
1129
|
+
}
|
|
1130
|
+
const startedAtRaw = stateData?.startedAt;
|
|
1131
|
+
const startedAtNum =
|
|
1132
|
+
typeof startedAtRaw === 'string'
|
|
1133
|
+
? (() => {
|
|
1134
|
+
const trimmed = startedAtRaw.trim();
|
|
1135
|
+
const asNumber = Number(trimmed);
|
|
1136
|
+
if (Number.isFinite(asNumber)) return asNumber;
|
|
1137
|
+
return Date.parse(trimmed);
|
|
1138
|
+
})()
|
|
1139
|
+
: Number(startedAtRaw);
|
|
1140
|
+
if (Number.isFinite(startedAtNum) && startedAtNum > 0) {
|
|
1141
|
+
lines.push(` Started: ${new Date(startedAtNum).toLocaleString()}`);
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof stateData?.startedWithCliVersion === 'string' && stateData.startedWithCliVersion.trim()) {
|
|
1144
|
+
lines.push(` CLI Version: ${stateData.startedWithCliVersion}`);
|
|
1145
|
+
}
|
|
1146
|
+
const httpPort = Number(stateData?.httpPort);
|
|
1147
|
+
if (Number.isFinite(httpPort) && httpPort > 0) {
|
|
1148
|
+
lines.push(` HTTP Port: ${httpPort}`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
lines.push('');
|
|
1152
|
+
lines.push('đ Daemon State:');
|
|
1153
|
+
lines.push(`Location: ${statePath}`);
|
|
1154
|
+
lines.push(redactedState ? JSON.stringify(redactedState, null, 2) : '(missing or unreadable)');
|
|
1155
|
+
lines.push('');
|
|
1156
|
+
lines.push(`âšī¸ Fallback status used because CLI dist entrypoint is missing: ${distEntrypoint ?? 'unknown'}`);
|
|
1157
|
+
lines.push('');
|
|
1158
|
+
lines.push('â
Doctor diagnosis complete!');
|
|
1159
|
+
return lines.join('\n');
|
|
1160
|
+
}
|