@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,181 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileHasContent } from '../fs/file_has_content.mjs';
|
|
4
|
+
|
|
5
|
+
const SERVER_ID_SAFE_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
6
|
+
|
|
7
|
+
function normalizeServerUrl(url) {
|
|
8
|
+
return String(url ?? '').trim().replace(/\/+$/, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeServerIdForFilesystem(raw, fallback = 'cloud') {
|
|
12
|
+
const value = String(raw ?? '').trim();
|
|
13
|
+
if (!value) return String(fallback ?? '').trim() || 'cloud';
|
|
14
|
+
if (value === '.' || value === '..') return String(fallback ?? '').trim() || 'cloud';
|
|
15
|
+
if (value.includes('/') || value.includes('\\')) return String(fallback ?? '').trim() || 'cloud';
|
|
16
|
+
if (!SERVER_ID_SAFE_RE.test(value)) return String(fallback ?? '').trim() || 'cloud';
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveActiveServerIdOverride(env = process.env) {
|
|
21
|
+
const raw = String(env?.HAPPIER_ACTIVE_SERVER_ID ?? '').trim();
|
|
22
|
+
if (!raw) return '';
|
|
23
|
+
return sanitizeServerIdForFilesystem(raw, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deriveServerIdFromUrl(url) {
|
|
27
|
+
const normalized = normalizeServerUrl(url);
|
|
28
|
+
let h = 2166136261;
|
|
29
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
30
|
+
h ^= normalized.charCodeAt(i);
|
|
31
|
+
h = Math.imul(h, 16777619);
|
|
32
|
+
}
|
|
33
|
+
return `env_${(h >>> 0).toString(16)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireCliHomeDir(cliHomeDir) {
|
|
37
|
+
const home = String(cliHomeDir ?? '').trim();
|
|
38
|
+
if (!home) {
|
|
39
|
+
throw new Error('cliHomeDir is required');
|
|
40
|
+
}
|
|
41
|
+
return home;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveStackCredentialPaths({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
45
|
+
const home = requireCliHomeDir(cliHomeDir);
|
|
46
|
+
const legacyPath = join(home, 'access.key');
|
|
47
|
+
const normalizedServerUrl = normalizeServerUrl(serverUrl);
|
|
48
|
+
const urlHashServerId = sanitizeServerIdForFilesystem(
|
|
49
|
+
normalizedServerUrl ? deriveServerIdFromUrl(normalizedServerUrl) : 'cloud',
|
|
50
|
+
'cloud'
|
|
51
|
+
);
|
|
52
|
+
const overrideServerId = resolveActiveServerIdOverride(env);
|
|
53
|
+
const activeServerId = overrideServerId || urlHashServerId;
|
|
54
|
+
const serverScopedPath = join(home, 'servers', activeServerId, 'access.key');
|
|
55
|
+
const urlHashServerScopedPath =
|
|
56
|
+
urlHashServerId && urlHashServerId !== activeServerId
|
|
57
|
+
? join(home, 'servers', urlHashServerId, 'access.key')
|
|
58
|
+
: '';
|
|
59
|
+
const paths = [serverScopedPath, urlHashServerScopedPath, legacyPath].filter(Boolean);
|
|
60
|
+
return {
|
|
61
|
+
activeServerId,
|
|
62
|
+
urlHashServerId,
|
|
63
|
+
legacyPath,
|
|
64
|
+
serverScopedPath,
|
|
65
|
+
urlHashServerScopedPath,
|
|
66
|
+
paths,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveStackDaemonStatePaths({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
71
|
+
const home = requireCliHomeDir(cliHomeDir);
|
|
72
|
+
const normalizedServerUrl = normalizeServerUrl(serverUrl);
|
|
73
|
+
const urlHashServerId = sanitizeServerIdForFilesystem(
|
|
74
|
+
normalizedServerUrl ? deriveServerIdFromUrl(normalizedServerUrl) : 'cloud',
|
|
75
|
+
'cloud'
|
|
76
|
+
);
|
|
77
|
+
const overrideServerId = resolveActiveServerIdOverride(env);
|
|
78
|
+
const activeServerId = overrideServerId || urlHashServerId;
|
|
79
|
+
|
|
80
|
+
const legacyStatePath = join(home, 'daemon.state.json');
|
|
81
|
+
const legacyLockPath = join(home, 'daemon.state.json.lock');
|
|
82
|
+
const serverScopedStatePath = join(home, 'servers', activeServerId, 'daemon.state.json');
|
|
83
|
+
const serverScopedLockPath = join(home, 'servers', activeServerId, 'daemon.state.json.lock');
|
|
84
|
+
const urlHashServerScopedStatePath =
|
|
85
|
+
urlHashServerId && urlHashServerId !== activeServerId
|
|
86
|
+
? join(home, 'servers', urlHashServerId, 'daemon.state.json')
|
|
87
|
+
: '';
|
|
88
|
+
const urlHashServerScopedLockPath =
|
|
89
|
+
urlHashServerId && urlHashServerId !== activeServerId
|
|
90
|
+
? join(home, 'servers', urlHashServerId, 'daemon.state.json.lock')
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
activeServerId,
|
|
95
|
+
urlHashServerId,
|
|
96
|
+
legacyStatePath,
|
|
97
|
+
legacyLockPath,
|
|
98
|
+
serverScopedStatePath,
|
|
99
|
+
serverScopedLockPath,
|
|
100
|
+
urlHashServerScopedStatePath,
|
|
101
|
+
urlHashServerScopedLockPath,
|
|
102
|
+
pairs: [
|
|
103
|
+
{ statePath: serverScopedStatePath, lockPath: serverScopedLockPath },
|
|
104
|
+
...(urlHashServerScopedStatePath
|
|
105
|
+
? [{ statePath: urlHashServerScopedStatePath, lockPath: urlHashServerScopedLockPath }]
|
|
106
|
+
: []),
|
|
107
|
+
{ statePath: legacyStatePath, lockPath: legacyLockPath },
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolvePreferredStackDaemonStatePaths({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
113
|
+
const resolved = resolveStackDaemonStatePaths({ cliHomeDir, serverUrl, env });
|
|
114
|
+
const serverScopedExists =
|
|
115
|
+
fileHasContent(resolved.serverScopedStatePath) || existsSync(resolved.serverScopedLockPath);
|
|
116
|
+
if (serverScopedExists) {
|
|
117
|
+
return { statePath: resolved.serverScopedStatePath, lockPath: resolved.serverScopedLockPath };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (resolved.urlHashServerScopedStatePath) {
|
|
121
|
+
const urlHashExists =
|
|
122
|
+
fileHasContent(resolved.urlHashServerScopedStatePath) || existsSync(resolved.urlHashServerScopedLockPath);
|
|
123
|
+
if (urlHashExists) {
|
|
124
|
+
return { statePath: resolved.urlHashServerScopedStatePath, lockPath: resolved.urlHashServerScopedLockPath };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const legacyExists = fileHasContent(resolved.legacyStatePath) || existsSync(resolved.legacyLockPath);
|
|
129
|
+
if (legacyExists) {
|
|
130
|
+
return { statePath: resolved.legacyStatePath, lockPath: resolved.legacyLockPath };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { statePath: resolved.serverScopedStatePath, lockPath: resolved.serverScopedLockPath };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function findExistingStackCredentialPath({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
137
|
+
const resolved = resolveStackCredentialPaths({ cliHomeDir, serverUrl, env });
|
|
138
|
+
if (fileHasContent(resolved.serverScopedPath)) return resolved.serverScopedPath;
|
|
139
|
+
if (resolved.urlHashServerScopedPath && fileHasContent(resolved.urlHashServerScopedPath)) {
|
|
140
|
+
return resolved.urlHashServerScopedPath;
|
|
141
|
+
}
|
|
142
|
+
if (fileHasContent(resolved.legacyPath)) return resolved.legacyPath;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function findAnyCredentialPathInCliHome({ cliHomeDir }) {
|
|
147
|
+
const home = String(cliHomeDir ?? '').trim();
|
|
148
|
+
if (!home) return null;
|
|
149
|
+
|
|
150
|
+
const serversDir = join(home, 'servers');
|
|
151
|
+
try {
|
|
152
|
+
const entries = readdirSync(serversDir, { withFileTypes: true })
|
|
153
|
+
.filter((ent) => ent.isDirectory())
|
|
154
|
+
.map((ent) => ent.name)
|
|
155
|
+
.sort();
|
|
156
|
+
let best = null;
|
|
157
|
+
let bestMtimeMs = -1;
|
|
158
|
+
for (const id of entries) {
|
|
159
|
+
const candidate = join(serversDir, id, 'access.key');
|
|
160
|
+
if (!fileHasContent(candidate)) continue;
|
|
161
|
+
let mtimeMs = 0;
|
|
162
|
+
try {
|
|
163
|
+
mtimeMs = Number(statSync(candidate).mtimeMs) || 0;
|
|
164
|
+
} catch {
|
|
165
|
+
mtimeMs = 0;
|
|
166
|
+
}
|
|
167
|
+
if (!best || mtimeMs >= bestMtimeMs) {
|
|
168
|
+
best = candidate;
|
|
169
|
+
bestMtimeMs = mtimeMs;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (best) return best;
|
|
173
|
+
} catch {
|
|
174
|
+
// ignore
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const legacy = join(home, 'access.key');
|
|
178
|
+
if (fileHasContent(legacy)) return legacy;
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
resolveStackCredentialPaths,
|
|
9
|
+
findExistingStackCredentialPath,
|
|
10
|
+
findAnyCredentialPathInCliHome,
|
|
11
|
+
resolveStackDaemonStatePaths,
|
|
12
|
+
resolvePreferredStackDaemonStatePaths,
|
|
13
|
+
} from './credentials_paths.mjs';
|
|
14
|
+
|
|
15
|
+
test('resolveStackCredentialPaths returns legacy + server-scoped paths', async () => {
|
|
16
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
17
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
18
|
+
const out = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl });
|
|
19
|
+
assert.equal(out.legacyPath, join(dir, 'access.key'));
|
|
20
|
+
assert.ok(out.serverScopedPath.startsWith(join(dir, 'servers', 'env_')));
|
|
21
|
+
assert.ok(out.serverScopedPath.endsWith('/access.key'));
|
|
22
|
+
assert.deepEqual(out.paths, [out.serverScopedPath, out.legacyPath]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('findExistingStackCredentialPath prefers server-scoped credentials', async () => {
|
|
26
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
27
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
28
|
+
const out = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl });
|
|
29
|
+
|
|
30
|
+
await mkdir(join(dir, 'servers', out.activeServerId), { recursive: true });
|
|
31
|
+
await writeFile(out.legacyPath, 'legacy\n', 'utf-8');
|
|
32
|
+
await writeFile(out.serverScopedPath, 'server\n', 'utf-8');
|
|
33
|
+
|
|
34
|
+
const found = findExistingStackCredentialPath({ cliHomeDir: dir, serverUrl });
|
|
35
|
+
assert.equal(found, out.serverScopedPath);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('findExistingStackCredentialPath falls back to legacy access.key', async () => {
|
|
39
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
40
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
41
|
+
const out = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl });
|
|
42
|
+
await writeFile(out.legacyPath, 'legacy\n', 'utf-8');
|
|
43
|
+
|
|
44
|
+
const found = findExistingStackCredentialPath({ cliHomeDir: dir, serverUrl });
|
|
45
|
+
assert.equal(found, out.legacyPath);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('findExistingStackCredentialPath keeps server-url isolation and falls back to legacy when resolved scope paths are empty', async () => {
|
|
49
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
50
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
51
|
+
const out = resolveStackCredentialPaths({
|
|
52
|
+
cliHomeDir: dir,
|
|
53
|
+
serverUrl,
|
|
54
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const otherServerScoped = join(dir, 'servers', 'stack_dev-auth__id_default', 'access.key');
|
|
58
|
+
await mkdir(join(dir, 'servers', 'stack_dev-auth__id_default'), { recursive: true });
|
|
59
|
+
await writeFile(out.legacyPath, 'legacy\n', 'utf-8');
|
|
60
|
+
await writeFile(otherServerScoped, 'server-scoped\n', 'utf-8');
|
|
61
|
+
|
|
62
|
+
const found = findExistingStackCredentialPath({
|
|
63
|
+
cliHomeDir: dir,
|
|
64
|
+
serverUrl,
|
|
65
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
66
|
+
});
|
|
67
|
+
assert.equal(found, out.legacyPath);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('findAnyCredentialPathInCliHome prefers server-scoped credentials over legacy', async () => {
|
|
71
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-any-cred-paths-'));
|
|
72
|
+
const legacyPath = join(dir, 'access.key');
|
|
73
|
+
const serverScopedPath = join(dir, 'servers', 'stack_main__id_default', 'access.key');
|
|
74
|
+
|
|
75
|
+
await mkdir(join(dir, 'servers', 'stack_main__id_default'), { recursive: true });
|
|
76
|
+
await writeFile(legacyPath, 'legacy\n', 'utf-8');
|
|
77
|
+
await writeFile(serverScopedPath, 'server\n', 'utf-8');
|
|
78
|
+
|
|
79
|
+
const found = findAnyCredentialPathInCliHome({ cliHomeDir: dir });
|
|
80
|
+
assert.equal(found, serverScopedPath);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('resolveStackCredentialPaths uses HAPPIER_ACTIVE_SERVER_ID when provided', async () => {
|
|
84
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
85
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
86
|
+
const out = resolveStackCredentialPaths({
|
|
87
|
+
cliHomeDir: dir,
|
|
88
|
+
serverUrl,
|
|
89
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
90
|
+
});
|
|
91
|
+
assert.equal(out.activeServerId, 'stack_main__id_default');
|
|
92
|
+
assert.ok(out.serverScopedPath.endsWith('/servers/stack_main__id_default/access.key'));
|
|
93
|
+
assert.ok(out.urlHashServerScopedPath.endsWith('/access.key'));
|
|
94
|
+
assert.notEqual(out.serverScopedPath, out.urlHashServerScopedPath);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('findExistingStackCredentialPath falls back to url-hash path when stable scope path is empty', async () => {
|
|
98
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-cred-paths-'));
|
|
99
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
100
|
+
const out = resolveStackCredentialPaths({
|
|
101
|
+
cliHomeDir: dir,
|
|
102
|
+
serverUrl,
|
|
103
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await mkdir(join(dir, 'servers', out.urlHashServerId), { recursive: true });
|
|
107
|
+
await writeFile(out.urlHashServerScopedPath, 'legacy-hash\n', 'utf-8');
|
|
108
|
+
|
|
109
|
+
const found = findExistingStackCredentialPath({
|
|
110
|
+
cliHomeDir: dir,
|
|
111
|
+
serverUrl,
|
|
112
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
113
|
+
});
|
|
114
|
+
assert.equal(found, out.urlHashServerScopedPath);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('resolveStackDaemonStatePaths returns legacy + server-scoped state and lock paths', async () => {
|
|
118
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-paths-'));
|
|
119
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
120
|
+
const out = resolveStackDaemonStatePaths({ cliHomeDir: dir, serverUrl });
|
|
121
|
+
|
|
122
|
+
assert.equal(out.legacyStatePath, join(dir, 'daemon.state.json'));
|
|
123
|
+
assert.equal(out.legacyLockPath, join(dir, 'daemon.state.json.lock'));
|
|
124
|
+
assert.ok(out.serverScopedStatePath.startsWith(join(dir, 'servers', 'env_')));
|
|
125
|
+
assert.ok(out.serverScopedStatePath.endsWith('/daemon.state.json'));
|
|
126
|
+
assert.ok(out.serverScopedLockPath.endsWith('/daemon.state.json.lock'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('resolvePreferredStackDaemonStatePaths prefers server-scoped paths when present', async () => {
|
|
130
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-paths-'));
|
|
131
|
+
const serverUrl = 'http://127.0.0.1:3010';
|
|
132
|
+
const out = resolveStackDaemonStatePaths({ cliHomeDir: dir, serverUrl });
|
|
133
|
+
|
|
134
|
+
await mkdir(join(dir, 'servers', out.activeServerId), { recursive: true });
|
|
135
|
+
await writeFile(out.serverScopedLockPath, `${process.pid}\n`, 'utf-8');
|
|
136
|
+
|
|
137
|
+
const preferred = resolvePreferredStackDaemonStatePaths({ cliHomeDir: dir, serverUrl });
|
|
138
|
+
assert.equal(preferred.statePath, out.serverScopedStatePath);
|
|
139
|
+
assert.equal(preferred.lockPath, out.serverScopedLockPath);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('resolvePreferredStackDaemonStatePaths falls back to legacy paths', async () => {
|
|
143
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-paths-'));
|
|
144
|
+
const serverUrl = 'http://127.0.0.1:3011';
|
|
145
|
+
const out = resolveStackDaemonStatePaths({ cliHomeDir: dir, serverUrl });
|
|
146
|
+
|
|
147
|
+
await writeFile(out.legacyLockPath, `${process.pid}\n`, 'utf-8');
|
|
148
|
+
|
|
149
|
+
const preferred = resolvePreferredStackDaemonStatePaths({ cliHomeDir: dir, serverUrl });
|
|
150
|
+
assert.equal(preferred.statePath, out.legacyStatePath);
|
|
151
|
+
assert.equal(preferred.lockPath, out.legacyLockPath);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('resolvePreferredStackDaemonStatePaths falls back to url-hash server-scoped state when stable scope is empty', async () => {
|
|
155
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-paths-'));
|
|
156
|
+
const serverUrl = 'http://127.0.0.1:3011';
|
|
157
|
+
const out = resolveStackDaemonStatePaths({
|
|
158
|
+
cliHomeDir: dir,
|
|
159
|
+
serverUrl,
|
|
160
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await mkdir(join(dir, 'servers', out.urlHashServerId), { recursive: true });
|
|
164
|
+
await writeFile(out.urlHashServerScopedLockPath, `${process.pid}\n`, 'utf-8');
|
|
165
|
+
|
|
166
|
+
const preferred = resolvePreferredStackDaemonStatePaths({
|
|
167
|
+
cliHomeDir: dir,
|
|
168
|
+
serverUrl,
|
|
169
|
+
env: { HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' },
|
|
170
|
+
});
|
|
171
|
+
assert.equal(preferred.statePath, out.urlHashServerScopedStatePath);
|
|
172
|
+
assert.equal(preferred.lockPath, out.urlHashServerScopedLockPath);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('resolveStackCredentialPaths throws when cliHomeDir is empty', () => {
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => resolveStackCredentialPaths({ cliHomeDir: '', serverUrl: 'http://127.0.0.1:3009' }),
|
|
178
|
+
/cliHomeDir is required/i
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('resolveStackDaemonStatePaths throws when cliHomeDir is empty', () => {
|
|
183
|
+
assert.throws(
|
|
184
|
+
() => resolveStackDaemonStatePaths({ cliHomeDir: '', serverUrl: 'http://127.0.0.1:3009' }),
|
|
185
|
+
/cliHomeDir is required/i
|
|
186
|
+
);
|
|
187
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { findExistingStackCredentialPath, resolveStackCredentialPaths } from './credentials_paths.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared policy for when the stack runner should start the Happier daemon.
|
|
5
|
+
*
|
|
6
|
+
* In `setup-pr` / `review-pr` guided login flows we intentionally start server+UI first,
|
|
7
|
+
* then guide authentication, then start daemon post-auth. Starting the daemon before
|
|
8
|
+
* credentials exist can strand it in its own auth flow (lock held, no machine registration),
|
|
9
|
+
* which leads to "no machines" in the UI.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function credentialsPathForCliHomeDir(cliHomeDir, serverUrl = '', env = process.env) {
|
|
13
|
+
const resolved = resolveStackCredentialPaths({ cliHomeDir, serverUrl, env });
|
|
14
|
+
return (
|
|
15
|
+
findExistingStackCredentialPath({ cliHomeDir, serverUrl, env }) ||
|
|
16
|
+
resolved.serverScopedPath
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hasStackCredentials({ cliHomeDir, serverUrl = '', env = process.env }) {
|
|
21
|
+
if (!cliHomeDir) return false;
|
|
22
|
+
return Boolean(findExistingStackCredentialPath({ cliHomeDir, serverUrl, env }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAuthFlowEnabled(env) {
|
|
26
|
+
const v = (env?.HAPPIER_STACK_AUTH_FLOW ?? '').toString().trim();
|
|
27
|
+
const wait = (env?.HAPPIER_STACK_DAEMON_WAIT_FOR_AUTH ?? '').toString().trim();
|
|
28
|
+
const isTrue = (s) => s === '1' || String(s).toLowerCase() === 'true';
|
|
29
|
+
return isTrue(v) || isTrue(wait);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns { ok: boolean, reason: string } where ok=true means it's safe to start the daemon now.
|
|
34
|
+
* When ok=false, callers should either:
|
|
35
|
+
* - run interactive auth first (TTY), or
|
|
36
|
+
* - skip daemon start without error in orchestrated auth flows, or
|
|
37
|
+
* - fail closed in non-interactive contexts.
|
|
38
|
+
*/
|
|
39
|
+
export function daemonStartGate({ env, cliHomeDir, serverUrl = '' }) {
|
|
40
|
+
const resolvedServerUrl = String(serverUrl ?? '').trim() || String(env?.HAPPIER_SERVER_URL ?? '').trim();
|
|
41
|
+
if (hasStackCredentials({ cliHomeDir, serverUrl: resolvedServerUrl, env })) {
|
|
42
|
+
return { ok: true, reason: 'credentials_present' };
|
|
43
|
+
}
|
|
44
|
+
if (isAuthFlowEnabled(env)) {
|
|
45
|
+
// Orchestrated auth flow (setup-pr/review-pr): keep server/UI up and let the orchestrator
|
|
46
|
+
// run guided login; starting the daemon now is counterproductive.
|
|
47
|
+
return { ok: false, reason: 'auth_flow_missing_credentials' };
|
|
48
|
+
}
|
|
49
|
+
return { ok: false, reason: 'missing_credentials' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatDaemonAuthRequiredError({ stackName, cliHomeDir, serverUrl = '' }) {
|
|
53
|
+
const name = (stackName ?? '').toString().trim() || 'main';
|
|
54
|
+
const resolved = resolveStackCredentialPaths({ cliHomeDir, serverUrl });
|
|
55
|
+
const path = `${resolved.legacyPath} or ${resolved.serverScopedPath}`;
|
|
56
|
+
const loginCmd =
|
|
57
|
+
name === 'main'
|
|
58
|
+
? 'hstack auth login --no-open'
|
|
59
|
+
: `hstack stack auth ${name} login --no-open`;
|
|
60
|
+
return (
|
|
61
|
+
`[local] daemon auth required: credentials not found for stack "${name}".\n` +
|
|
62
|
+
`[local] expected: ${path}\n` +
|
|
63
|
+
`[local] fix: run \`${loginCmd}\`.\n` +
|
|
64
|
+
`[local] tip (headless servers): use \`--method=mobile\` to print a QR code / deep link for the mobile app.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
|
|
7
|
+
import { daemonStartGate, formatDaemonAuthRequiredError, hasStackCredentials } from './daemon_gate.mjs';
|
|
8
|
+
import { resolveStackCredentialPaths } from './credentials_paths.mjs';
|
|
9
|
+
|
|
10
|
+
async function withTempRoot(t) {
|
|
11
|
+
const dir = await mkdtemp(join(tmpdir(), 'happy-stacks-daemon-gate-'));
|
|
12
|
+
t.after(async () => {
|
|
13
|
+
await rm(dir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('hasStackCredentials detects access.key', async (t) => {
|
|
19
|
+
const dir = await withTempRoot(t);
|
|
20
|
+
assert.equal(hasStackCredentials({ cliHomeDir: dir }), false);
|
|
21
|
+
await writeFile(join(dir, 'access.key'), 'dummy', 'utf-8');
|
|
22
|
+
assert.equal(hasStackCredentials({ cliHomeDir: dir }), true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('hasStackCredentials detects server-scoped access.key for env server urls', async (t) => {
|
|
26
|
+
const dir = await withTempRoot(t);
|
|
27
|
+
const serverUrl = 'http://127.0.0.1:3009';
|
|
28
|
+
const paths = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl });
|
|
29
|
+
const gateBefore = hasStackCredentials({ cliHomeDir: dir, serverUrl });
|
|
30
|
+
assert.equal(gateBefore, false);
|
|
31
|
+
|
|
32
|
+
await mkdir(join(dir, 'servers', paths.activeServerId), { recursive: true });
|
|
33
|
+
await writeFile(paths.serverScopedPath, 'dummy', 'utf-8');
|
|
34
|
+
|
|
35
|
+
const gateAfter = hasStackCredentials({ cliHomeDir: dir, serverUrl });
|
|
36
|
+
assert.equal(gateAfter, true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('daemonStartGate blocks daemon start in auth flow when missing credentials', async (t) => {
|
|
40
|
+
const dir = await withTempRoot(t);
|
|
41
|
+
const gate = daemonStartGate({ env: { HAPPIER_STACK_AUTH_FLOW: '1' }, cliHomeDir: dir });
|
|
42
|
+
assert.equal(gate.ok, false);
|
|
43
|
+
assert.equal(gate.reason, 'auth_flow_missing_credentials');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('daemonStartGate blocks daemon start in daemon-wait auth flow when missing credentials', async (t) => {
|
|
47
|
+
const dir = await withTempRoot(t);
|
|
48
|
+
const gate = daemonStartGate({ env: { HAPPIER_STACK_DAEMON_WAIT_FOR_AUTH: '1' }, cliHomeDir: dir });
|
|
49
|
+
assert.equal(gate.ok, false);
|
|
50
|
+
assert.equal(gate.reason, 'auth_flow_missing_credentials');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('daemonStartGate blocks daemon start when missing credentials (non-auth flow)', async (t) => {
|
|
54
|
+
const dir = await withTempRoot(t);
|
|
55
|
+
const gate = daemonStartGate({ env: {}, cliHomeDir: dir });
|
|
56
|
+
assert.equal(gate.ok, false);
|
|
57
|
+
assert.equal(gate.reason, 'missing_credentials');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('daemonStartGate allows daemon start when credentials exist', async (t) => {
|
|
61
|
+
const dir = await withTempRoot(t);
|
|
62
|
+
await writeFile(join(dir, 'access.key'), 'dummy', 'utf-8');
|
|
63
|
+
const gate = daemonStartGate({ env: {}, cliHomeDir: dir });
|
|
64
|
+
assert.equal(gate.ok, true);
|
|
65
|
+
assert.equal(gate.reason, 'credentials_present');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('daemonStartGate resolves server-scoped credentials from env server url', async (t) => {
|
|
69
|
+
const dir = await withTempRoot(t);
|
|
70
|
+
const serverUrl = 'http://127.0.0.1:4010';
|
|
71
|
+
const paths = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl });
|
|
72
|
+
await mkdir(join(dir, 'servers', paths.activeServerId), { recursive: true });
|
|
73
|
+
await writeFile(paths.serverScopedPath, 'dummy', 'utf-8');
|
|
74
|
+
|
|
75
|
+
const gate = daemonStartGate({
|
|
76
|
+
env: { HAPPIER_SERVER_URL: serverUrl },
|
|
77
|
+
cliHomeDir: dir,
|
|
78
|
+
});
|
|
79
|
+
assert.equal(gate.ok, true);
|
|
80
|
+
assert.equal(gate.reason, 'credentials_present');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('daemonStartGate resolves stable-scope credentials from HAPPIER_ACTIVE_SERVER_ID', async (t) => {
|
|
84
|
+
const dir = await withTempRoot(t);
|
|
85
|
+
const serverUrl = 'http://127.0.0.1:4010';
|
|
86
|
+
const env = { HAPPIER_SERVER_URL: serverUrl, HAPPIER_ACTIVE_SERVER_ID: 'stack_main__id_default' };
|
|
87
|
+
const paths = resolveStackCredentialPaths({ cliHomeDir: dir, serverUrl, env });
|
|
88
|
+
await mkdir(join(dir, 'servers', paths.activeServerId), { recursive: true });
|
|
89
|
+
await writeFile(paths.serverScopedPath, 'dummy', 'utf-8');
|
|
90
|
+
|
|
91
|
+
const gate = daemonStartGate({
|
|
92
|
+
env,
|
|
93
|
+
cliHomeDir: dir,
|
|
94
|
+
});
|
|
95
|
+
assert.equal(gate.ok, true);
|
|
96
|
+
assert.equal(gate.reason, 'credentials_present');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('daemonStartGate prefers credentials over auth-flow toggles', async (t) => {
|
|
100
|
+
const dir = await withTempRoot(t);
|
|
101
|
+
await writeFile(join(dir, 'access.key'), 'dummy', 'utf-8');
|
|
102
|
+
|
|
103
|
+
const gate = daemonStartGate({
|
|
104
|
+
env: { HAPPIER_STACK_AUTH_FLOW: 'true', HAPPIER_STACK_DAEMON_WAIT_FOR_AUTH: '1' },
|
|
105
|
+
cliHomeDir: dir,
|
|
106
|
+
});
|
|
107
|
+
assert.equal(gate.ok, true);
|
|
108
|
+
assert.equal(gate.reason, 'credentials_present');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('formatDaemonAuthRequiredError suggests mobile QR login for headless servers', () => {
|
|
112
|
+
const msg = formatDaemonAuthRequiredError({ stackName: 'main', cliHomeDir: '/tmp/cli-home' });
|
|
113
|
+
assert.match(msg, /hstack auth login/i);
|
|
114
|
+
assert.match(msg, /--method=mobile/i);
|
|
115
|
+
assert.match(msg, /--no-open/i);
|
|
116
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function decodeJwtPayloadUnsafe(token) {
|
|
2
|
+
const raw = String(token ?? '').trim();
|
|
3
|
+
if (!raw) return null;
|
|
4
|
+
const parts = raw.split('.');
|
|
5
|
+
if (parts.length < 2) return null;
|
|
6
|
+
try {
|
|
7
|
+
const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
8
|
+
const padLength = normalized.length % 4 === 0 ? 0 : 4 - (normalized.length % 4);
|
|
9
|
+
const padded = normalized + '='.repeat(padLength);
|
|
10
|
+
const decoded = Buffer.from(padded, 'base64').toString('utf-8');
|
|
11
|
+
const payload = JSON.parse(decoded);
|
|
12
|
+
return payload && typeof payload === 'object' ? payload : null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|