@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,60 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile, chmod, readFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
import { ensureDevExpoServer } from './expo_dev.mjs';
|
|
9
|
+
|
|
10
|
+
test('ensureDevExpoServer does not drop Expo output when spawnOptions stdio is ignore', async () => {
|
|
11
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-expo-verbose-'));
|
|
12
|
+
try {
|
|
13
|
+
const uiDir = join(tmp, 'ui');
|
|
14
|
+
await mkdir(join(uiDir, 'node_modules', '.bin'), { recursive: true });
|
|
15
|
+
await mkdir(join(uiDir, 'node_modules'), { recursive: true });
|
|
16
|
+
await writeFile(join(uiDir, 'package.json'), JSON.stringify({ name: 'fake-ui', private: true }) + '\n', 'utf-8');
|
|
17
|
+
|
|
18
|
+
const expoBin = join(uiDir, 'node_modules', '.bin', 'expo');
|
|
19
|
+
await writeFile(
|
|
20
|
+
expoBin,
|
|
21
|
+
[
|
|
22
|
+
'#!/usr/bin/env node',
|
|
23
|
+
"console.log('hello-from-fake-expo');",
|
|
24
|
+
"setTimeout(() => process.exit(0), 100);",
|
|
25
|
+
].join('\n') + '\n',
|
|
26
|
+
'utf-8'
|
|
27
|
+
);
|
|
28
|
+
await chmod(expoBin, 0o755);
|
|
29
|
+
|
|
30
|
+
const teeFile = join(tmp, 'expo.log');
|
|
31
|
+
const children = [];
|
|
32
|
+
await ensureDevExpoServer({
|
|
33
|
+
startUi: true,
|
|
34
|
+
startMobile: false,
|
|
35
|
+
uiDir,
|
|
36
|
+
autostart: { baseDir: tmp },
|
|
37
|
+
baseEnv: { ...process.env, HAPPIER_STACK_VERBOSE: '1' },
|
|
38
|
+
apiServerUrl: 'http://127.0.0.1:1',
|
|
39
|
+
restart: true,
|
|
40
|
+
stackMode: false,
|
|
41
|
+
runtimeStatePath: null,
|
|
42
|
+
stackName: 'test',
|
|
43
|
+
envPath: '',
|
|
44
|
+
children,
|
|
45
|
+
spawnOptions: {
|
|
46
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
47
|
+
silent: true,
|
|
48
|
+
teeFile,
|
|
49
|
+
teeLabel: 'expo',
|
|
50
|
+
},
|
|
51
|
+
quiet: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await delay(400);
|
|
55
|
+
const log = await readFile(teeFile, 'utf-8').catch(() => '');
|
|
56
|
+
assert.match(log, /hello-from-fake-expo/);
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(tmp, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { ensureDepsInstalled, pmSpawnScript } from '../proc/pm.mjs';
|
|
4
|
+
import { applyHappyServerMigrations, ensureHappyServerManagedInfra } from '../server/infra/happy_server_infra.mjs';
|
|
5
|
+
import { applyServerLightEnvDefaults } from '../server/apply_server_light_env_defaults.mjs';
|
|
6
|
+
import { resolveServerDevScript } from '../server/flavor_scripts.mjs';
|
|
7
|
+
import { waitForServerReady } from '../server/server.mjs';
|
|
8
|
+
import { isTcpPortFree, pickNextFreeTcpPort } from '../net/ports.mjs';
|
|
9
|
+
import { readStackRuntimeStateFile, recordStackRuntimeUpdate } from '../stack/runtime_state.mjs';
|
|
10
|
+
import { killProcessGroupOwnedByStack } from '../proc/ownership.mjs';
|
|
11
|
+
import { watchDebounced } from '../proc/watch.mjs';
|
|
12
|
+
import { pickMetroPort, resolveStablePortStart } from '../expo/metro_ports.mjs';
|
|
13
|
+
|
|
14
|
+
export function resolveStackUiDevPortStart({ env = process.env, stackName }) {
|
|
15
|
+
return resolveStablePortStart({
|
|
16
|
+
env: {
|
|
17
|
+
...env,
|
|
18
|
+
HAPPIER_STACK_UI_DEV_PORT_BASE: (env.HAPPIER_STACK_UI_DEV_PORT_BASE ?? '8081').toString(),
|
|
19
|
+
HAPPIER_STACK_UI_DEV_PORT_RANGE: (env.HAPPIER_STACK_UI_DEV_PORT_RANGE ?? '1000').toString(),
|
|
20
|
+
},
|
|
21
|
+
stackName,
|
|
22
|
+
baseKey: 'HAPPIER_STACK_UI_DEV_PORT_BASE',
|
|
23
|
+
rangeKey: 'HAPPIER_STACK_UI_DEV_PORT_RANGE',
|
|
24
|
+
defaultBase: 8081,
|
|
25
|
+
defaultRange: 1000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function pickDevMetroPort({ startPort, reservedPorts = new Set(), host = '127.0.0.1' } = {}) {
|
|
30
|
+
const forcedPort = (process.env.HAPPIER_STACK_UI_DEV_PORT ?? '').toString().trim();
|
|
31
|
+
return await pickMetroPort({ startPort, forcedPort, reservedPorts, host });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function startDevServer({
|
|
35
|
+
serverComponentName,
|
|
36
|
+
serverDir,
|
|
37
|
+
autostart,
|
|
38
|
+
baseEnv,
|
|
39
|
+
serverPort,
|
|
40
|
+
internalServerUrl,
|
|
41
|
+
publicServerUrl,
|
|
42
|
+
envPath,
|
|
43
|
+
stackMode,
|
|
44
|
+
runtimeStatePath,
|
|
45
|
+
serverAlreadyRunning,
|
|
46
|
+
restart,
|
|
47
|
+
children,
|
|
48
|
+
spawnOptions = {},
|
|
49
|
+
quiet = false,
|
|
50
|
+
}) {
|
|
51
|
+
const serverEnv = {
|
|
52
|
+
...baseEnv,
|
|
53
|
+
PORT: String(serverPort),
|
|
54
|
+
PUBLIC_URL: publicServerUrl,
|
|
55
|
+
// Avoid noisy failures if a previous run left the metrics port busy.
|
|
56
|
+
METRICS_ENABLED: baseEnv.METRICS_ENABLED ?? 'false',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (serverComponentName === 'happier-server-light') {
|
|
60
|
+
applyServerLightEnvDefaults({ baseEnv, serverEnv, baseDir: autostart.baseDir });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (serverComponentName === 'happier-server') {
|
|
64
|
+
const managed = (baseEnv.HAPPIER_STACK_MANAGED_INFRA ?? '1') !== '0';
|
|
65
|
+
if (managed) {
|
|
66
|
+
const infra = await ensureHappyServerManagedInfra({
|
|
67
|
+
stackName: autostart.stackName,
|
|
68
|
+
baseDir: autostart.baseDir,
|
|
69
|
+
serverPort,
|
|
70
|
+
publicServerUrl,
|
|
71
|
+
envPath,
|
|
72
|
+
env: baseEnv,
|
|
73
|
+
});
|
|
74
|
+
Object.assign(serverEnv, infra.env);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const autoMigrate = (baseEnv.HAPPIER_STACK_PRISMA_MIGRATE ?? '1') !== '0';
|
|
78
|
+
if (autoMigrate) {
|
|
79
|
+
await applyHappyServerMigrations({ serverDir, env: serverEnv });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ensure server deps exist before any Prisma/docker work.
|
|
84
|
+
await ensureDepsInstalled(serverDir, serverComponentName, { quiet, env: serverEnv });
|
|
85
|
+
|
|
86
|
+
const prismaPush = (baseEnv.HAPPIER_STACK_PRISMA_PUSH ?? '1').toString().trim() !== '0';
|
|
87
|
+
const serverScript = resolveServerDevScript({ serverComponentName, serverDir, prismaPush });
|
|
88
|
+
|
|
89
|
+
// Restart behavior (stack-safe): only kill when we can prove ownership via runtime state.
|
|
90
|
+
if (restart && stackMode && runtimeStatePath && serverAlreadyRunning) {
|
|
91
|
+
const st = await readStackRuntimeStateFile(runtimeStatePath);
|
|
92
|
+
const pid = Number(st?.processes?.serverPid);
|
|
93
|
+
if (pid > 1) {
|
|
94
|
+
const res = await killProcessGroupOwnedByStack(pid, { stackName: autostart.stackName, envPath, label: 'server', json: true });
|
|
95
|
+
if (!res.killed) {
|
|
96
|
+
// Fail-closed if the port is still occupied.
|
|
97
|
+
const free = await isTcpPortFree(serverPort, { host: '127.0.0.1' });
|
|
98
|
+
if (!free) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`[local] restart refused: server port ${serverPort} is occupied and the PID is not provably stack-owned.\n` +
|
|
101
|
+
`[local] Fix: run 'hstack stack stop ${autostart.stackName}' then re-run, or re-run without --restart.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (serverAlreadyRunning && !restart) {
|
|
109
|
+
return { serverEnv, serverScript, serverProc: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const server = await pmSpawnScript({
|
|
113
|
+
label: 'server',
|
|
114
|
+
dir: serverDir,
|
|
115
|
+
script: serverScript,
|
|
116
|
+
env: serverEnv,
|
|
117
|
+
options: spawnOptions,
|
|
118
|
+
quiet,
|
|
119
|
+
});
|
|
120
|
+
children.push(server);
|
|
121
|
+
if (stackMode && runtimeStatePath) {
|
|
122
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: server.pid } }).catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
await waitForServerReady(internalServerUrl);
|
|
125
|
+
return { serverEnv, serverScript, serverProc: server };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function watchDevServerAndRestart({
|
|
129
|
+
enabled,
|
|
130
|
+
stackMode,
|
|
131
|
+
serverComponentName,
|
|
132
|
+
serverDir,
|
|
133
|
+
serverPort,
|
|
134
|
+
internalServerUrl,
|
|
135
|
+
serverScript,
|
|
136
|
+
serverEnv,
|
|
137
|
+
runtimeStatePath,
|
|
138
|
+
stackName,
|
|
139
|
+
envPath,
|
|
140
|
+
children,
|
|
141
|
+
serverProcRef,
|
|
142
|
+
isShuttingDown,
|
|
143
|
+
}) {
|
|
144
|
+
if (!enabled) return null;
|
|
145
|
+
|
|
146
|
+
// Only watch full server by default; server-light already has a good upstream dev loop.
|
|
147
|
+
if (serverComponentName !== 'happier-server') return null;
|
|
148
|
+
|
|
149
|
+
return watchDebounced({
|
|
150
|
+
paths: [resolve(serverDir)],
|
|
151
|
+
debounceMs: 600,
|
|
152
|
+
onChange: async () => {
|
|
153
|
+
if (isShuttingDown?.()) return;
|
|
154
|
+
const pid = Number(serverProcRef?.current?.pid);
|
|
155
|
+
if (!Number.isFinite(pid) || pid <= 1) return;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// eslint-disable-next-line no-console
|
|
159
|
+
console.log('[local] watch: server changed → restarting...');
|
|
160
|
+
await killProcessGroupOwnedByStack(pid, { stackName, envPath, label: 'server', json: false });
|
|
161
|
+
|
|
162
|
+
const next = await pmSpawnScript({ label: 'server', dir: serverDir, script: serverScript, env: serverEnv });
|
|
163
|
+
children.push(next);
|
|
164
|
+
serverProcRef.current = next;
|
|
165
|
+
if (stackMode && runtimeStatePath) {
|
|
166
|
+
await recordStackRuntimeUpdate(runtimeStatePath, { processes: { serverPid: next.pid } }).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
await waitForServerReady(internalServerUrl);
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.log(`[local] watch: server restarted (pid=${next.pid}, port=${serverPort})`);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
const msg = e instanceof Error ? e.stack || e.message : String(e);
|
|
173
|
+
// eslint-disable-next-line no-console
|
|
174
|
+
console.error('[local] watch: server restart failed; keeping existing process as-is (will retry on next change).');
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.error(msg);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export function findGitRootForPath(dir) {
|
|
5
|
+
const raw = String(dir ?? '').trim();
|
|
6
|
+
if (!raw) return '';
|
|
7
|
+
|
|
8
|
+
let cur = resolve(raw);
|
|
9
|
+
while (true) {
|
|
10
|
+
try {
|
|
11
|
+
if (existsSync(join(cur, '.git'))) {
|
|
12
|
+
return cur;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// ignore
|
|
16
|
+
}
|
|
17
|
+
const parent = dirname(cur);
|
|
18
|
+
if (parent === cur) return '';
|
|
19
|
+
cur = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeGitRoots(paths) {
|
|
24
|
+
const list = Array.isArray(paths) ? paths : [];
|
|
25
|
+
const normalized = list
|
|
26
|
+
.map((d) => findGitRootForPath(d) || String(d ?? '').trim())
|
|
27
|
+
.map((d) => resolve(d))
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
return Array.from(new Set(normalized));
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { findGitRootForPath, normalizeGitRoots } from './git_roots.mjs';
|
|
8
|
+
|
|
9
|
+
async function withTempRoot(t) {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-edison-git-roots-'));
|
|
11
|
+
t.after(async () => {
|
|
12
|
+
await rm(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('findGitRootForPath returns nearest ancestor containing .git marker', async (t) => {
|
|
18
|
+
const root = await withTempRoot(t);
|
|
19
|
+
const repoRoot = join(root, 'repo');
|
|
20
|
+
await mkdir(join(repoRoot, 'a', 'b'), { recursive: true });
|
|
21
|
+
await writeFile(join(repoRoot, '.git'), 'gitdir: /tmp/fake\n', 'utf-8');
|
|
22
|
+
|
|
23
|
+
assert.equal(findGitRootForPath(join(repoRoot, 'a', 'b')), repoRoot);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('normalizeGitRoots de-duplicates multiple paths inside the same repo', async (t) => {
|
|
27
|
+
const root = await withTempRoot(t);
|
|
28
|
+
const repoRoot = join(root, 'repo');
|
|
29
|
+
await mkdir(join(repoRoot, 'apps', 'ui'), { recursive: true });
|
|
30
|
+
await mkdir(join(repoRoot, 'apps', 'cli'), { recursive: true });
|
|
31
|
+
await writeFile(join(repoRoot, '.git'), 'gitdir: /tmp/fake\n', 'utf-8');
|
|
32
|
+
|
|
33
|
+
const roots = normalizeGitRoots([join(repoRoot, 'apps', 'ui'), join(repoRoot, 'apps', 'cli')]);
|
|
34
|
+
assert.deepEqual(roots, [repoRoot]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('findGitRootForPath returns empty string for blank path input', () => {
|
|
38
|
+
assert.equal(findGitRootForPath(''), '');
|
|
39
|
+
assert.equal(findGitRootForPath(' '), '');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('normalizeGitRoots keeps non-repo paths and de-duplicates by resolved absolute path', async (t) => {
|
|
43
|
+
const root = await withTempRoot(t);
|
|
44
|
+
const plainDir = join(root, 'plain');
|
|
45
|
+
await mkdir(plainDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const roots = normalizeGitRoots([plainDir, join(plainDir, '..', 'plain')]);
|
|
48
|
+
assert.deepEqual(roots, [plainDir]);
|
|
49
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ensureEnvFileUpdated } from './env_file.mjs';
|
|
3
|
+
import { getHappyStacksHomeDir, resolveStackEnvPath } from '../paths/paths.mjs';
|
|
4
|
+
import { getCanonicalHomeDirFromEnv } from '../paths/canonical_home.mjs';
|
|
5
|
+
|
|
6
|
+
export function getHomeEnvPath() {
|
|
7
|
+
return join(getHappyStacksHomeDir(), '.env');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getCanonicalHomeDir() {
|
|
11
|
+
return getCanonicalHomeDirFromEnv(process.env);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCanonicalHomeEnvPath() {
|
|
15
|
+
return join(getCanonicalHomeDir(), '.env');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getHomeEnvLocalPath() {
|
|
19
|
+
return join(getHappyStacksHomeDir(), 'env.local');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveUserConfigEnvPath({ cliRootDir }) {
|
|
23
|
+
const explicit = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
|
|
24
|
+
if (explicit) {
|
|
25
|
+
return explicit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// By default, persist configuration to the main stack env file so config is
|
|
29
|
+
// outside the repo and consistent across install modes.
|
|
30
|
+
//
|
|
31
|
+
// This also matches the stack env precedence in scripts/utils/env.mjs.
|
|
32
|
+
void cliRootDir;
|
|
33
|
+
return resolveStackEnvPath('main').envPath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function ensureHomeEnvUpdated({ updates }) {
|
|
37
|
+
await ensureEnvFileUpdated({ envPath: getHomeEnvPath(), updates });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function ensureCanonicalHomeEnvUpdated({ updates }) {
|
|
41
|
+
await ensureEnvFileUpdated({ envPath: getCanonicalHomeEnvPath(), updates });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function ensureHomeEnvLocalUpdated({ updates }) {
|
|
45
|
+
await ensureEnvFileUpdated({ envPath: getHomeEnvLocalPath(), updates });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function ensureUserConfigEnvUpdated({ cliRootDir, updates }) {
|
|
49
|
+
const envPath = resolveUserConfigEnvPath({ cliRootDir });
|
|
50
|
+
await ensureEnvFileUpdated({ envPath, updates });
|
|
51
|
+
return envPath;
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expandHome } from '../paths/canonical_home.mjs';
|
|
2
|
+
|
|
3
|
+
export function parseDotenv(contents) {
|
|
4
|
+
const out = new Map();
|
|
5
|
+
for (const rawLine of contents.split('\n')) {
|
|
6
|
+
const line = rawLine.trim();
|
|
7
|
+
if (!line || line.startsWith('#')) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const idx = line.indexOf('=');
|
|
11
|
+
if (idx <= 0) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const key = line.slice(0, idx).trim();
|
|
15
|
+
let value = line.slice(idx + 1).trim();
|
|
16
|
+
if (!key) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
20
|
+
value = value.slice(1, -1);
|
|
21
|
+
}
|
|
22
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
23
|
+
value = expandHome(value);
|
|
24
|
+
}
|
|
25
|
+
out.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseEnvToObject(contents) {
|
|
31
|
+
return Object.fromEntries(parseDotenv(contents));
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
import { parseDotenv, parseEnvToObject } from './dotenv.mjs';
|
|
6
|
+
|
|
7
|
+
test('parseDotenv expands ~/ paths', () => {
|
|
8
|
+
const env = parseDotenv('FOO=~/x\n');
|
|
9
|
+
assert.equal(env.get('FOO'), `${homedir()}/x`);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('parseDotenv expands ~\\ paths (Windows)', () => {
|
|
13
|
+
const env = parseDotenv('FOO=~\\x\n');
|
|
14
|
+
assert.equal(env.get('FOO'), `${homedir()}\\x`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('parseDotenv expands quoted ~/ paths and trims keys/values', () => {
|
|
18
|
+
const env = parseDotenv(" FOO = '~/x' \n");
|
|
19
|
+
assert.equal(env.get('FOO'), `${homedir()}/x`);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('parseDotenv ignores comments and malformed assignments', () => {
|
|
23
|
+
const env = parseDotenv('# comment\nNO_EQ\n=BAD\nOK=value\n');
|
|
24
|
+
assert.equal(env.get('OK'), 'value');
|
|
25
|
+
assert.equal(env.has('NO_EQ'), false);
|
|
26
|
+
assert.equal(env.has(''), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('parseEnvToObject returns the final value for duplicate keys', () => {
|
|
30
|
+
const env = parseEnvToObject('FOO=first\nFOO=second\n');
|
|
31
|
+
assert.deepEqual(env, { FOO: 'second' });
|
|
32
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { expandHome, getCanonicalHomeEnvPathFromEnv } from '../paths/canonical_home.mjs';
|
|
6
|
+
import { isSandboxed, sandboxAllowsGlobalSideEffects } from './sandbox.mjs';
|
|
7
|
+
import { loadEnvFile, loadEnvFileIgnoringPrefixes } from './load_env_file.mjs';
|
|
8
|
+
|
|
9
|
+
// Load stack env (optional). This is intentionally lightweight and does not require extra deps.
|
|
10
|
+
// This file lives under scripts/utils/env, so repo root is three directories up.
|
|
11
|
+
const __envDir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const __utilsDir = dirname(__envDir);
|
|
13
|
+
const __scriptsDir = dirname(__utilsDir);
|
|
14
|
+
const __cliRootDir = dirname(__scriptsDir);
|
|
15
|
+
|
|
16
|
+
function resolveHomeDir() {
|
|
17
|
+
const fromEnv = (process.env.HAPPIER_STACK_HOME_DIR ?? '').trim();
|
|
18
|
+
if (fromEnv) {
|
|
19
|
+
return expandHome(fromEnv);
|
|
20
|
+
}
|
|
21
|
+
return join(homedir(), '.happier-stack');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If HAPPIER_STACK_HOME_DIR isn't set, try the canonical pointer file at <canonicalHomeDir>/.env first.
|
|
25
|
+
//
|
|
26
|
+
// This allows installs where the "real" home/workspace/runtime are elsewhere, while still
|
|
27
|
+
// giving us a stable discovery location for launchd/SwiftBar/minimal shells.
|
|
28
|
+
const canonicalEnvPath = getCanonicalHomeEnvPathFromEnv(process.env);
|
|
29
|
+
if (!(process.env.HAPPIER_STACK_HOME_DIR ?? '').trim() && existsSync(canonicalEnvPath)) {
|
|
30
|
+
await loadEnvFile(canonicalEnvPath, { override: false });
|
|
31
|
+
await loadEnvFile(canonicalEnvPath, { override: true, overridePrefix: 'HAPPIER_STACK_' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const __homeDir = resolveHomeDir();
|
|
35
|
+
process.env.HAPPIER_STACK_HOME_DIR = process.env.HAPPIER_STACK_HOME_DIR ?? __homeDir;
|
|
36
|
+
|
|
37
|
+
// Prefer canonical home config:
|
|
38
|
+
// ~/.happier-stack/.env
|
|
39
|
+
// ~/.happier-stack/env.local
|
|
40
|
+
//
|
|
41
|
+
// Additionally: when running from a cloned repo, load <repo>/.env as a *fallback* even if home config exists.
|
|
42
|
+
// This helps keep repo-local dev settings (e.g. custom Codex binaries) working without requiring users to
|
|
43
|
+
// duplicate them into ~/.happier-stack/env.local.
|
|
44
|
+
const homeEnv = join(__homeDir, '.env');
|
|
45
|
+
const homeLocal = join(__homeDir, 'env.local');
|
|
46
|
+
// In sandbox mode, never load repo env.local (it can contain "real" machine paths/URLs).
|
|
47
|
+
// Treat sandbox runs as having home config even if the sandbox home env files don't exist yet.
|
|
48
|
+
const hasHomeConfig = isSandboxed() || existsSync(homeEnv) || existsSync(homeLocal);
|
|
49
|
+
const repoEnv = join(__cliRootDir, '.env');
|
|
50
|
+
|
|
51
|
+
// 1) Load defaults first (lowest precedence)
|
|
52
|
+
if (hasHomeConfig) {
|
|
53
|
+
await loadEnvFile(homeEnv, { override: false });
|
|
54
|
+
await loadEnvFile(homeLocal, { override: true, overridePrefix: 'HAPPIER_STACK_' });
|
|
55
|
+
} else {
|
|
56
|
+
await loadEnvFile(join(__cliRootDir, '.env'), { override: false });
|
|
57
|
+
await loadEnvFile(join(__cliRootDir, 'env.local'), { override: true, overridePrefix: 'HAPPIER_STACK_' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Repo-local fallback (dev convenience):
|
|
61
|
+
// If the repo has a .env, load it without overriding anything already set by the environment or home config.
|
|
62
|
+
// Note: we intentionally do NOT load repo env.local here, because env.local is treated as higher-precedence
|
|
63
|
+
// overrides and could unexpectedly fight with stack/home configuration when present.
|
|
64
|
+
if (hasHomeConfig) {
|
|
65
|
+
// IMPORTANT:
|
|
66
|
+
// When home config exists, do not let repo-local .env set HAPPIER_STACK_* keys.
|
|
67
|
+
// Otherwise a cloned repo's .env can accidentally leak global URLs/ports into every stack.
|
|
68
|
+
await loadEnvFileIgnoringPrefixes(repoEnv, { ignorePrefixes: ['HAPPIER_STACK_'] });
|
|
69
|
+
} else {
|
|
70
|
+
await loadEnvFile(repoEnv, { override: false });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no explicit env file is set, and we're on the default "main" stack, prefer the stack-scoped env file
|
|
74
|
+
// if it exists: ~/.happier/stacks/main/env
|
|
75
|
+
(() => {
|
|
76
|
+
const stacksEnv = (process.env.HAPPIER_STACK_ENV_FILE ?? '').trim();
|
|
77
|
+
if (stacksEnv) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const stackName = (process.env.HAPPIER_STACK_STACK ?? '').trim() || 'main';
|
|
81
|
+
const stacksStorageRootRaw = (process.env.HAPPIER_STACK_STORAGE_DIR ?? '').trim();
|
|
82
|
+
const stacksStorageRoot = stacksStorageRootRaw ? expandHome(stacksStorageRootRaw) : join(homedir(), '.happier', 'stacks');
|
|
83
|
+
|
|
84
|
+
const candidates = [
|
|
85
|
+
join(stacksStorageRoot, stackName, 'env'),
|
|
86
|
+
];
|
|
87
|
+
const envPath = candidates.find((p) => existsSync(p));
|
|
88
|
+
if (!envPath) return;
|
|
89
|
+
|
|
90
|
+
process.env.HAPPIER_STACK_ENV_FILE = envPath;
|
|
91
|
+
})();
|
|
92
|
+
// 3) Load explicit env file overlay (stack env, or any caller-provided env file) last (highest precedence).
|
|
93
|
+
//
|
|
94
|
+
// IMPORTANT:
|
|
95
|
+
// Stack env files intentionally include some non-prefixed keys (e.g. DATABASE_URL, HAPPIER_SERVER_LIGHT_DATA_DIR)
|
|
96
|
+
// that must apply for true per-stack isolation. Do not filter by prefix here.
|
|
97
|
+
{
|
|
98
|
+
const stacksEnv = process.env.HAPPIER_STACK_ENV_FILE?.trim() ? process.env.HAPPIER_STACK_ENV_FILE.trim() : '';
|
|
99
|
+
const unique = Array.from(new Set([stacksEnv].filter(Boolean)));
|
|
100
|
+
for (const p of unique) {
|
|
101
|
+
// eslint-disable-next-line no-await-in-loop
|
|
102
|
+
await loadEnvFile(p, { override: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Legacy Happy env prefixes are intentionally not supported here.
|
|
107
|
+
// If a user still has older prefixes exported from previous installs, scrub them to avoid accidental leakage.
|
|
108
|
+
const __legacyPrefixes = ['LOCAL_', 'STACKS_'].map((s) => `HAPPY_${s}`);
|
|
109
|
+
for (const k of Object.keys(process.env)) {
|
|
110
|
+
if (__legacyPrefixes.some((p) => k.startsWith(p))) {
|
|
111
|
+
delete process.env[k];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Corepack strictness can prevent running Yarn in subfolders when the repo root is pinned to a different manager.
|
|
116
|
+
// We intentionally keep child processes Yarn-friendly, so relax strictness for child processes.
|
|
117
|
+
process.env.COREPACK_ENABLE_STRICT = process.env.COREPACK_ENABLE_STRICT ?? '0';
|
|
118
|
+
process.env.NPM_CONFIG_PACKAGE_MANAGER_STRICT = process.env.NPM_CONFIG_PACKAGE_MANAGER_STRICT ?? 'false';
|
|
119
|
+
|
|
120
|
+
// LaunchAgents often run with a very minimal PATH which won't include NVM's bin dir, so child
|
|
121
|
+
// processes like `yarn` can look "missing" even though Node is running from NVM.
|
|
122
|
+
// Ensure the directory containing this Node binary is on PATH.
|
|
123
|
+
(() => {
|
|
124
|
+
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
125
|
+
const current = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
126
|
+
const nodeBinDir = dirname(process.execPath);
|
|
127
|
+
const want = [nodeBinDir, '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin', '/usr/bin', '/bin'];
|
|
128
|
+
const next = [...want.filter((p) => p && !current.includes(p)), ...current];
|
|
129
|
+
process.env.PATH = next.join(delimiter);
|
|
130
|
+
})();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { pathExists } from '../fs/fs.mjs';
|
|
4
|
+
|
|
5
|
+
export async function ensureEnvFileUpdated({ envPath, updates }) {
|
|
6
|
+
if (!updates.length) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
10
|
+
const existing = await readText(envPath);
|
|
11
|
+
const next = applyEnvUpdates(existing, updates);
|
|
12
|
+
await writeFileIfChanged(existing, next, envPath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function ensureEnvFilePruned({ envPath, removeKeys }) {
|
|
16
|
+
const keys = Array.from(new Set((removeKeys ?? []).map((k) => String(k).trim()).filter(Boolean)));
|
|
17
|
+
if (!keys.length) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
21
|
+
const existing = await readText(envPath);
|
|
22
|
+
const next = pruneEnvKeys(existing, keys);
|
|
23
|
+
await writeFileIfChanged(existing, next, envPath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readText(path) {
|
|
27
|
+
try {
|
|
28
|
+
return (await pathExists(path)) ? await readFile(path, 'utf-8') : '';
|
|
29
|
+
} catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function applyEnvUpdates(existing, updates) {
|
|
35
|
+
const lines = existing ? existing.split('\n') : [];
|
|
36
|
+
const next = [...lines];
|
|
37
|
+
|
|
38
|
+
const upsert = (key, value) => {
|
|
39
|
+
const line = `${key}=${value}`;
|
|
40
|
+
const idx = next.findIndex((l) => l.trim().startsWith(`${key}=`));
|
|
41
|
+
if (idx >= 0) {
|
|
42
|
+
next[idx] = line;
|
|
43
|
+
} else {
|
|
44
|
+
if (next.length && next[next.length - 1].trim() !== '') {
|
|
45
|
+
next.push('');
|
|
46
|
+
}
|
|
47
|
+
next.push(line);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const { key, value } of updates) {
|
|
52
|
+
upsert(key, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return next.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pruneEnvKeys(existing, removeKeys) {
|
|
59
|
+
const keys = new Set(removeKeys);
|
|
60
|
+
const lines = (existing ?? '').split('\n');
|
|
61
|
+
const kept = [];
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
65
|
+
kept.push(line);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Remove any "KEY=..." line for keys in removeKeys.
|
|
69
|
+
const eq = trimmed.indexOf('=');
|
|
70
|
+
if (eq <= 0) {
|
|
71
|
+
kept.push(line);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const key = trimmed.slice(0, eq).trim();
|
|
75
|
+
if (keys.has(key)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
kept.push(line);
|
|
79
|
+
}
|
|
80
|
+
return kept.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function writeFileIfChanged(existingContent, nextContent, path) {
|
|
84
|
+
const normalizedNext = nextContent.endsWith('\n') ? nextContent : nextContent + '\n';
|
|
85
|
+
const normalizedExisting = existingContent.endsWith('\n') ? existingContent : existingContent + (existingContent ? '\n' : '');
|
|
86
|
+
if (normalizedExisting === normalizedNext) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const dir = dirname(path);
|
|
91
|
+
// if dir doesn't exist, writeFile will throw; that's fine (we only target known files).
|
|
92
|
+
void dir;
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
await writeFile(path, normalizedNext, 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
|