@happier-dev/stack 0.1.0-preview.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -0
- package/bin/hstack.mjs +348 -0
- package/docs/codex-mcp-resume.md +129 -0
- package/docs/edison.md +74 -0
- package/docs/forking-and-branding.md +189 -0
- package/docs/happy-development.md +22 -0
- package/docs/isolated-linux-vm.md +243 -0
- package/docs/menubar.md +244 -0
- package/docs/mobile-ios.md +322 -0
- package/docs/monorepo-migration.md +20 -0
- package/docs/paths-and-env.md +154 -0
- package/docs/remote-access.md +43 -0
- package/docs/server-flavors.md +147 -0
- package/docs/stacks.md +330 -0
- package/docs/tauri.md +60 -0
- package/docs/worktrees-and-forks.md +133 -0
- package/extras/swiftbar/auth-login.sh +29 -0
- package/extras/swiftbar/git-cache-refresh.sh +122 -0
- package/extras/swiftbar/hstack-term.sh +133 -0
- package/extras/swiftbar/hstack.5s.sh +296 -0
- package/extras/swiftbar/hstack.sh +35 -0
- package/extras/swiftbar/icons/happy-green.png +0 -0
- package/extras/swiftbar/icons/happy-orange.png +0 -0
- package/extras/swiftbar/icons/happy-red.png +0 -0
- package/extras/swiftbar/icons/logo-white.png +0 -0
- package/extras/swiftbar/install.sh +265 -0
- package/extras/swiftbar/lib/git.sh +629 -0
- package/extras/swiftbar/lib/icons.sh +92 -0
- package/extras/swiftbar/lib/render.sh +999 -0
- package/extras/swiftbar/lib/system.sh +244 -0
- package/extras/swiftbar/lib/utils.sh +717 -0
- package/extras/swiftbar/set-interval.sh +65 -0
- package/extras/swiftbar/set-server-flavor.sh +61 -0
- package/extras/swiftbar/wt-pr.sh +140 -0
- package/node_modules/@happier-dev/cli-common/README.md +6 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +4 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js +4 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts +18 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.js +25 -0
- package/node_modules/@happier-dev/cli-common/dist/links/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/links.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/links.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/links.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts +67 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.js +259 -0
- package/node_modules/@happier-dev/cli-common/dist/update/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts +17 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js +80 -0
- package/node_modules/@happier-dev/cli-common/dist/workspaces/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +26 -0
- package/package.json +77 -0
- package/scripts/auth.mjs +1829 -0
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +90 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +447 -0
- package/scripts/auth_help_cmd.test.mjs +28 -0
- package/scripts/auth_login_flow_in_tty.test.mjs +100 -0
- package/scripts/auth_login_force_default.test.mjs +66 -0
- package/scripts/auth_login_guided_server_no_expo.test.mjs +126 -0
- package/scripts/auth_login_method_override.test.mjs +67 -0
- package/scripts/auth_login_print_includes_configure_links.test.mjs +99 -0
- package/scripts/auth_status_server_validation.integration.test.mjs +140 -0
- package/scripts/build.mjs +266 -0
- package/scripts/bundleWorkspaceDeps.mjs +38 -0
- package/scripts/bundleWorkspaceDeps.test.mjs +77 -0
- package/scripts/ci.mjs +135 -0
- package/scripts/ci.test.mjs +50 -0
- package/scripts/cli-link.mjs +57 -0
- package/scripts/completion.mjs +395 -0
- package/scripts/contrib.mjs +333 -0
- package/scripts/daemon.mjs +1160 -0
- package/scripts/daemon.status_scope.test.mjs +51 -0
- package/scripts/daemon_cmd.mjs +26 -0
- package/scripts/daemon_dist_guard.test.mjs +171 -0
- package/scripts/daemon_invalid_auth_reseed_stack_name.integration.test.mjs +608 -0
- package/scripts/daemon_server_scoped_state.test.mjs +49 -0
- package/scripts/daemon_start_verification.integration.test.mjs +296 -0
- package/scripts/dev.mjs +545 -0
- package/scripts/doctor.mjs +340 -0
- package/scripts/doctor_cmd.test.mjs +22 -0
- package/scripts/doctor_ui_index_missing.test.mjs +37 -0
- package/scripts/eas.mjs +367 -0
- package/scripts/eas_platform_parsing.test.mjs +63 -0
- package/scripts/edison.mjs +1848 -0
- package/scripts/env.mjs +149 -0
- package/scripts/env_cmd.test.mjs +118 -0
- package/scripts/exit_cleanup_kills_detached_children_on_crash.integration.test.mjs +80 -0
- package/scripts/happier.mjs +82 -0
- package/scripts/import.mjs +1327 -0
- package/scripts/init.mjs +464 -0
- package/scripts/install.mjs +550 -0
- package/scripts/lint.mjs +177 -0
- package/scripts/menubar.mjs +202 -0
- package/scripts/migrate.mjs +318 -0
- package/scripts/mobile.mjs +353 -0
- package/scripts/mobile_dev_client.mjs +87 -0
- package/scripts/monorepo.mjs +2234 -0
- package/scripts/monorepo_port.apply.integration.test.mjs +680 -0
- package/scripts/monorepo_port.conflicts.integration.test.mjs +454 -0
- package/scripts/monorepo_port.validation.integration.test.mjs +486 -0
- package/scripts/orchestrated_stack_auth_flow.test.mjs +134 -0
- package/scripts/orchestrated_stack_auth_flow_resolve_port.test.mjs +98 -0
- package/scripts/orchestrated_stack_auth_flow_webapp_url.test.mjs +119 -0
- package/scripts/pack.mjs +257 -0
- package/scripts/pack.test.mjs +68 -0
- package/scripts/pglite_lock.integration.test.mjs +152 -0
- package/scripts/provision/linux-ubuntu-e2e.sh +132 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +66 -0
- package/scripts/provision/macos-lima-happy-vm.sh +192 -0
- package/scripts/provision/macos-lima-hstack-e2e.sh +100 -0
- package/scripts/release.mjs +53 -0
- package/scripts/release_binary_smoke.integration.test.mjs +159 -0
- package/scripts/review.mjs +1752 -0
- package/scripts/review_pr.mjs +435 -0
- package/scripts/run.mjs +561 -0
- package/scripts/run_script_with_stack_env.restart_port_reuse.test.mjs +30 -0
- package/scripts/self.mjs +465 -0
- package/scripts/self_host.mjs +9 -0
- package/scripts/self_host_binary_smoke.integration.test.mjs +94 -0
- package/scripts/self_host_runtime.mjs +883 -0
- package/scripts/self_host_runtime.test.mjs +82 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +367 -0
- package/scripts/server_flavor.mjs +148 -0
- package/scripts/service.mjs +868 -0
- package/scripts/service_mode_help.test.mjs +27 -0
- package/scripts/setup.mjs +1324 -0
- package/scripts/setup_non_interactive_flag.test.mjs +60 -0
- package/scripts/setup_pr.mjs +605 -0
- package/scripts/setup_pr_orchestrated_auth_flow_util_import.test.mjs +117 -0
- package/scripts/stack/command_arguments.mjs +91 -0
- package/scripts/stack/copy_auth_from_stack.mjs +111 -0
- package/scripts/stack/delegated_script_commands.mjs +92 -0
- package/scripts/stack/help_text.mjs +110 -0
- package/scripts/stack/port_reservation.mjs +74 -0
- package/scripts/stack/repo_checkout_resolution.mjs +31 -0
- package/scripts/stack/run_script_with_stack_env.mjs +634 -0
- package/scripts/stack/stack_daemon_command.mjs +219 -0
- package/scripts/stack/stack_delegated_help.mjs +81 -0
- package/scripts/stack/stack_environment.mjs +151 -0
- package/scripts/stack/stack_environment.sanitization.test.mjs +75 -0
- package/scripts/stack/stack_happier_passthrough_command.mjs +63 -0
- package/scripts/stack/stack_info_snapshot.mjs +167 -0
- package/scripts/stack/stack_mobile_install_command.mjs +61 -0
- package/scripts/stack/stack_resume_command.mjs +76 -0
- package/scripts/stack/stack_stop_command.mjs +34 -0
- package/scripts/stack/stack_workspace_command.mjs +83 -0
- package/scripts/stack/transient_repo_overrides.mjs +29 -0
- package/scripts/stack.mjs +2388 -0
- package/scripts/stack_archive_cmd.integration.test.mjs +31 -0
- package/scripts/stack_audit_fix_light_env.test.mjs +129 -0
- package/scripts/stack_background_pinned_stack_json.test.mjs +81 -0
- package/scripts/stack_copy_auth_server_scoped.test.mjs +243 -0
- package/scripts/stack_daemon_cmd.integration.test.mjs +484 -0
- package/scripts/stack_eas_help.test.mjs +72 -0
- package/scripts/stack_editor_workspace_monorepo_root.test.mjs +102 -0
- package/scripts/stack_env_cmd.test.mjs +107 -0
- package/scripts/stack_guided_login_bundle_error_parse.test.mjs +20 -0
- package/scripts/stack_guided_login_inner_invocation.test.mjs +46 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +263 -0
- package/scripts/stack_info_snapshot_running_status.test.mjs +186 -0
- package/scripts/stack_interactive_monorepo_group.test.mjs +128 -0
- package/scripts/stack_monorepo_defaults.test.mjs +31 -0
- package/scripts/stack_monorepo_repo_dev_token.test.mjs +32 -0
- package/scripts/stack_monorepo_server_light_from_happy_spec.test.mjs +37 -0
- package/scripts/stack_new_name_normalize_cmd.test.mjs +38 -0
- package/scripts/stack_pr_name_normalize_cmd.test.mjs +84 -0
- package/scripts/stack_resume_cmd.integration.test.mjs +134 -0
- package/scripts/stack_server_flavors_defaults.test.mjs +64 -0
- package/scripts/stack_shorthand_cmd.integration.test.mjs +74 -0
- package/scripts/stack_stop_sweeps_legacy_infra_without_kind.integration.test.mjs +44 -0
- package/scripts/stack_stop_sweeps_when_runtime_missing.integration.test.mjs +42 -0
- package/scripts/stack_stop_sweeps_when_runtime_stale.integration.test.mjs +50 -0
- package/scripts/stack_wt_list.test.mjs +117 -0
- package/scripts/start_ui_required_default.test.mjs +63 -0
- package/scripts/stop.mjs +190 -0
- package/scripts/stopStackWithEnv_no_autosweep_when_runtime_missing.integration.test.mjs +95 -0
- package/scripts/swiftbar_git_monorepo_cmd.test.mjs +75 -0
- package/scripts/swiftbar_render_monorepo_wt_actions.integration.test.mjs +116 -0
- package/scripts/swiftbar_utils_cmd.test.mjs +92 -0
- package/scripts/swiftbar_wt_pr_backcompat.test.mjs +162 -0
- package/scripts/systemd_unit_info.test.mjs +24 -0
- package/scripts/tailscale.mjs +490 -0
- package/scripts/test_ci.mjs +36 -0
- package/scripts/test_cmd.mjs +274 -0
- package/scripts/test_cmd.test.mjs +133 -0
- package/scripts/test_integration.mjs +33 -0
- package/scripts/testkit/auth_testkit.mjs +121 -0
- package/scripts/testkit/doctor_testkit.mjs +68 -0
- package/scripts/testkit/monorepo_port_testkit.mjs +157 -0
- package/scripts/testkit/stack_archive_command_testkit.mjs +55 -0
- package/scripts/testkit/stack_new_monorepo_testkit.mjs +83 -0
- package/scripts/testkit/stack_script_command_testkit.mjs +27 -0
- package/scripts/testkit/stack_stop_sweeps_testkit.mjs +172 -0
- package/scripts/testkit/worktrees_monorepo_testkit.mjs +53 -0
- package/scripts/tools.mjs +70 -0
- package/scripts/tui.mjs +914 -0
- package/scripts/tui_stopStackForTuiExit_no_autosweep.integration.test.mjs +95 -0
- package/scripts/typecheck.mjs +178 -0
- package/scripts/ui_gateway.mjs +247 -0
- package/scripts/uninstall.mjs +179 -0
- package/scripts/utils/auth/credentials_paths.mjs +181 -0
- package/scripts/utils/auth/credentials_paths.test.mjs +187 -0
- package/scripts/utils/auth/daemon_gate.mjs +66 -0
- package/scripts/utils/auth/daemon_gate.test.mjs +116 -0
- package/scripts/utils/auth/decode_jwt_payload_unsafe.mjs +16 -0
- package/scripts/utils/auth/dev_key.mjs +163 -0
- package/scripts/utils/auth/files.mjs +56 -0
- package/scripts/utils/auth/guided_pr_auth.mjs +86 -0
- package/scripts/utils/auth/guided_stack_web_login.mjs +56 -0
- package/scripts/utils/auth/handy_master_secret.mjs +42 -0
- package/scripts/utils/auth/interactive_stack_auth.mjs +70 -0
- package/scripts/utils/auth/login_ux.mjs +105 -0
- package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +291 -0
- package/scripts/utils/auth/sources.mjs +28 -0
- package/scripts/utils/auth/stable_scope_id.mjs +91 -0
- package/scripts/utils/auth/stable_scope_id.test.mjs +51 -0
- package/scripts/utils/auth/stack_guided_login.mjs +438 -0
- package/scripts/utils/cli/arg_values.mjs +23 -0
- package/scripts/utils/cli/arg_values.test.mjs +43 -0
- package/scripts/utils/cli/args.mjs +17 -0
- package/scripts/utils/cli/cli.mjs +24 -0
- package/scripts/utils/cli/cli_registry.mjs +440 -0
- package/scripts/utils/cli/cwd_scope.mjs +158 -0
- package/scripts/utils/cli/cwd_scope.test.mjs +154 -0
- package/scripts/utils/cli/flags.mjs +17 -0
- package/scripts/utils/cli/log_forwarder.mjs +157 -0
- package/scripts/utils/cli/normalize.mjs +16 -0
- package/scripts/utils/cli/prereqs.mjs +103 -0
- package/scripts/utils/cli/prereqs.test.mjs +33 -0
- package/scripts/utils/cli/progress.mjs +141 -0
- package/scripts/utils/cli/smoke_help.mjs +44 -0
- package/scripts/utils/cli/verbosity.mjs +11 -0
- package/scripts/utils/cli/wizard.mjs +139 -0
- package/scripts/utils/cli/wizard_promptSelect.test.mjs +44 -0
- package/scripts/utils/cli/wizard_prompt_worktree_source_lazy.test.mjs +132 -0
- package/scripts/utils/cli/wizard_worktree_slug.test.mjs +33 -0
- package/scripts/utils/crypto/tokens.mjs +14 -0
- package/scripts/utils/dev/daemon.mjs +232 -0
- package/scripts/utils/dev/daemon_watch_resilience.test.mjs +224 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +35 -0
- package/scripts/utils/dev/expo_dev.mjs +478 -0
- package/scripts/utils/dev/expo_dev.test.mjs +89 -0
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +120 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +60 -0
- package/scripts/utils/dev/server.mjs +180 -0
- package/scripts/utils/dev_auth_key.mjs +7 -0
- package/scripts/utils/edison/git_roots.mjs +30 -0
- package/scripts/utils/edison/git_roots.test.mjs +49 -0
- package/scripts/utils/env/config.mjs +52 -0
- package/scripts/utils/env/dotenv.mjs +32 -0
- package/scripts/utils/env/dotenv.test.mjs +32 -0
- package/scripts/utils/env/env.mjs +130 -0
- package/scripts/utils/env/env_file.mjs +98 -0
- package/scripts/utils/env/env_file.test.mjs +49 -0
- package/scripts/utils/env/env_local.mjs +25 -0
- package/scripts/utils/env/load_env_file.mjs +34 -0
- package/scripts/utils/env/read.mjs +30 -0
- package/scripts/utils/env/sandbox.mjs +13 -0
- package/scripts/utils/env/scrub_env.mjs +69 -0
- package/scripts/utils/env/scrub_env.test.mjs +102 -0
- package/scripts/utils/env/values.mjs +13 -0
- package/scripts/utils/expo/command.mjs +65 -0
- package/scripts/utils/expo/expo.mjs +139 -0
- package/scripts/utils/expo/expo_state_running.test.mjs +48 -0
- package/scripts/utils/expo/metro_ports.mjs +101 -0
- package/scripts/utils/expo/metro_ports.test.mjs +35 -0
- package/scripts/utils/fs/atomic_dir_swap.mjs +55 -0
- package/scripts/utils/fs/atomic_dir_swap.test.mjs +54 -0
- package/scripts/utils/fs/file_has_content.mjs +10 -0
- package/scripts/utils/fs/fs.mjs +11 -0
- package/scripts/utils/fs/json.mjs +25 -0
- package/scripts/utils/fs/ops.mjs +29 -0
- package/scripts/utils/fs/package_json.mjs +8 -0
- package/scripts/utils/fs/tail.mjs +12 -0
- package/scripts/utils/git/dev_checkout.mjs +127 -0
- package/scripts/utils/git/dev_checkout.test.mjs +115 -0
- package/scripts/utils/git/git.mjs +67 -0
- package/scripts/utils/git/parse_name_status_z.mjs +21 -0
- package/scripts/utils/git/refs.mjs +26 -0
- package/scripts/utils/git/worktrees.mjs +323 -0
- package/scripts/utils/git/worktrees_monorepo.test.mjs +60 -0
- package/scripts/utils/git/worktrees_pathstyle.test.mjs +53 -0
- package/scripts/utils/llm/assist.mjs +260 -0
- package/scripts/utils/llm/codex_exec.mjs +61 -0
- package/scripts/utils/llm/codex_exec.test.mjs +46 -0
- package/scripts/utils/llm/hstack_runner.mjs +59 -0
- package/scripts/utils/llm/tools.mjs +56 -0
- package/scripts/utils/llm/tools.test.mjs +67 -0
- package/scripts/utils/menubar/swiftbar.mjs +121 -0
- package/scripts/utils/menubar/swiftbar.test.mjs +85 -0
- package/scripts/utils/mobile/config.mjs +35 -0
- package/scripts/utils/mobile/dev_client_links.mjs +59 -0
- package/scripts/utils/mobile/identifiers.mjs +46 -0
- package/scripts/utils/mobile/identifiers.test.mjs +41 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.mjs +128 -0
- package/scripts/utils/mobile/ios_xcodeproj_patch.test.mjs +131 -0
- package/scripts/utils/net/bind_mode.mjs +39 -0
- package/scripts/utils/net/dns.mjs +10 -0
- package/scripts/utils/net/lan_ip.mjs +24 -0
- package/scripts/utils/net/ports.mjs +110 -0
- package/scripts/utils/net/tcp_forward.mjs +162 -0
- package/scripts/utils/net/url.mjs +30 -0
- package/scripts/utils/net/url.test.mjs +29 -0
- package/scripts/utils/paths/canonical_home.mjs +15 -0
- package/scripts/utils/paths/canonical_home.test.mjs +28 -0
- package/scripts/utils/paths/localhost_host.mjs +112 -0
- package/scripts/utils/paths/localhost_host.test.mjs +58 -0
- package/scripts/utils/paths/paths.mjs +302 -0
- package/scripts/utils/paths/paths_env_win32.test.mjs +36 -0
- package/scripts/utils/paths/paths_monorepo.test.mjs +58 -0
- package/scripts/utils/paths/paths_server_flavors.test.mjs +50 -0
- package/scripts/utils/paths/runtime.mjs +41 -0
- package/scripts/utils/pglite_lock.mjs +107 -0
- package/scripts/utils/proc/commands.mjs +33 -0
- package/scripts/utils/proc/exit_cleanup.mjs +57 -0
- package/scripts/utils/proc/happy_monorepo_deps.mjs +37 -0
- package/scripts/utils/proc/happy_monorepo_deps.test.mjs +89 -0
- package/scripts/utils/proc/ownership.mjs +217 -0
- package/scripts/utils/proc/ownership_killProcessGroupOwnedByStack.test.mjs +216 -0
- package/scripts/utils/proc/ownership_listPidsWithEnvNeedles.test.mjs +88 -0
- package/scripts/utils/proc/package_scripts.mjs +38 -0
- package/scripts/utils/proc/package_scripts.test.mjs +58 -0
- package/scripts/utils/proc/parallel.mjs +25 -0
- package/scripts/utils/proc/pids.mjs +11 -0
- package/scripts/utils/proc/pm.mjs +478 -0
- package/scripts/utils/proc/pm_spawn.integration.test.mjs +131 -0
- package/scripts/utils/proc/pm_stack_cache_env.test.mjs +313 -0
- package/scripts/utils/proc/proc.mjs +331 -0
- package/scripts/utils/proc/proc.test.mjs +85 -0
- package/scripts/utils/proc/terminate.mjs +69 -0
- package/scripts/utils/proc/terminate.test.mjs +54 -0
- package/scripts/utils/proc/watch.mjs +63 -0
- package/scripts/utils/review/augment_runner_integration.test.mjs +105 -0
- package/scripts/utils/review/base_ref.mjs +82 -0
- package/scripts/utils/review/base_ref.test.mjs +89 -0
- package/scripts/utils/review/chunks.mjs +55 -0
- package/scripts/utils/review/chunks.test.mjs +107 -0
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +61 -0
- package/scripts/utils/review/findings.mjs +278 -0
- package/scripts/utils/review/findings.test.mjs +203 -0
- package/scripts/utils/review/head_slice.mjs +132 -0
- package/scripts/utils/review/head_slice.test.mjs +117 -0
- package/scripts/utils/review/instructions/deep.md +20 -0
- package/scripts/utils/review/prompts.mjs +279 -0
- package/scripts/utils/review/prompts.test.mjs +77 -0
- package/scripts/utils/review/run_reviewers_safe.mjs +12 -0
- package/scripts/utils/review/run_reviewers_safe.test.mjs +45 -0
- package/scripts/utils/review/runners/augment.mjs +91 -0
- package/scripts/utils/review/runners/augment.test.mjs +64 -0
- package/scripts/utils/review/runners/claude.mjs +92 -0
- package/scripts/utils/review/runners/claude.test.mjs +47 -0
- package/scripts/utils/review/runners/coderabbit.mjs +105 -0
- package/scripts/utils/review/runners/coderabbit.test.mjs +32 -0
- package/scripts/utils/review/runners/codex.mjs +129 -0
- package/scripts/utils/review/runners/codex.test.mjs +115 -0
- package/scripts/utils/review/slice_mode.mjs +20 -0
- package/scripts/utils/review/slice_mode.test.mjs +69 -0
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +57 -0
- package/scripts/utils/review/slices.mjs +140 -0
- package/scripts/utils/review/slices.test.mjs +41 -0
- package/scripts/utils/review/targets.mjs +23 -0
- package/scripts/utils/review/targets.test.mjs +31 -0
- package/scripts/utils/review/tool_home_seed.mjs +106 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +124 -0
- package/scripts/utils/review/uncommitted_ops.mjs +77 -0
- package/scripts/utils/review/uncommitted_ops.test.mjs +117 -0
- package/scripts/utils/sandbox/review_pr_sandbox.mjs +105 -0
- package/scripts/utils/server/apply_server_light_env_defaults.mjs +14 -0
- package/scripts/utils/server/flavor_scripts.mjs +138 -0
- package/scripts/utils/server/flavor_scripts.test.mjs +115 -0
- package/scripts/utils/server/infra/happy_server_infra.mjs +444 -0
- package/scripts/utils/server/mobile_api_url.mjs +60 -0
- package/scripts/utils/server/mobile_api_url.test.mjs +58 -0
- package/scripts/utils/server/port.mjs +55 -0
- package/scripts/utils/server/prisma_import.mjs +36 -0
- package/scripts/utils/server/prisma_import.test.mjs +78 -0
- package/scripts/utils/server/server.mjs +109 -0
- package/scripts/utils/server/ui_build_check.mjs +37 -0
- package/scripts/utils/server/ui_build_check.test.mjs +70 -0
- package/scripts/utils/server/ui_env.mjs +13 -0
- package/scripts/utils/server/ui_env.test.mjs +57 -0
- package/scripts/utils/server/urls.mjs +100 -0
- package/scripts/utils/server/validate.mjs +60 -0
- package/scripts/utils/server/validate.test.mjs +76 -0
- package/scripts/utils/service/autostart_darwin.mjs +198 -0
- package/scripts/utils/service/autostart_darwin.test.mjs +49 -0
- package/scripts/utils/service/autostart_darwin_keepalive.test.mjs +19 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/context.mjs +19 -0
- package/scripts/utils/stack/dirs.mjs +26 -0
- package/scripts/utils/stack/editor_workspace.mjs +126 -0
- package/scripts/utils/stack/interactive_stack_config.mjs +266 -0
- package/scripts/utils/stack/interactive_stack_config.port_validation.test.mjs +93 -0
- package/scripts/utils/stack/interactive_stack_config.remote_validation.test.mjs +122 -0
- package/scripts/utils/stack/interactive_stack_config.stack_name_validation.test.mjs +76 -0
- package/scripts/utils/stack/interactive_stack_config_testkit.mjs +18 -0
- package/scripts/utils/stack/names.mjs +27 -0
- package/scripts/utils/stack/names.test.mjs +26 -0
- package/scripts/utils/stack/pr_stack_name.mjs +16 -0
- package/scripts/utils/stack/runtime_state.mjs +88 -0
- package/scripts/utils/stack/stacks.mjs +40 -0
- package/scripts/utils/stack/startup.mjs +370 -0
- package/scripts/utils/stack/startup_server_light_dirs.test.mjs +119 -0
- package/scripts/utils/stack/startup_server_light_generate.test.mjs +20 -0
- package/scripts/utils/stack/startup_server_light_legacy.test.mjs +79 -0
- package/scripts/utils/stack/startup_server_light_testkit.mjs +106 -0
- package/scripts/utils/stack/stop.mjs +284 -0
- package/scripts/utils/stack_context.mjs +1 -0
- package/scripts/utils/stack_runtime_state.mjs +1 -0
- package/scripts/utils/stacks.mjs +1 -0
- package/scripts/utils/tailscale/ip.mjs +116 -0
- package/scripts/utils/tauri/stack_overrides.mjs +22 -0
- package/scripts/utils/test/collect_test_files.mjs +29 -0
- package/scripts/utils/time/get_today_ymd.mjs +7 -0
- package/scripts/utils/tui/cleanup.mjs +38 -0
- package/scripts/utils/ui/ansi.mjs +47 -0
- package/scripts/utils/ui/browser.mjs +31 -0
- package/scripts/utils/ui/browser.test.mjs +56 -0
- package/scripts/utils/ui/clipboard.mjs +38 -0
- package/scripts/utils/ui/layout.mjs +44 -0
- package/scripts/utils/ui/qr.mjs +17 -0
- package/scripts/utils/ui/terminal_launcher.mjs +129 -0
- package/scripts/utils/ui/text.mjs +16 -0
- package/scripts/utils/update/auto_update_notice.mjs +93 -0
- package/scripts/utils/validate.mjs +5 -0
- package/scripts/where.mjs +138 -0
- package/scripts/worktrees.mjs +2174 -0
- package/scripts/worktrees_archive_cmd.integration.test.mjs +228 -0
- package/scripts/worktrees_cursor_monorepo_root.test.mjs +23 -0
- package/scripts/worktrees_list_specs_no_recurse.test.mjs +32 -0
- package/scripts/worktrees_monorepo_testkit.test.mjs +29 -0
- package/scripts/worktrees_monorepo_use_group.test.mjs +41 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isTcpPortFree, pickNextFreeTcpPort } from '../net/ports.mjs';
|
|
2
|
+
|
|
3
|
+
function hashStringToInt(s) {
|
|
4
|
+
let h = 0;
|
|
5
|
+
const str = String(s ?? '');
|
|
6
|
+
for (let i = 0; i < str.length; i++) {
|
|
7
|
+
h = (h * 31 + str.charCodeAt(i)) >>> 0;
|
|
8
|
+
}
|
|
9
|
+
return h >>> 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function coercePositiveInt(v) {
|
|
13
|
+
const n = Number(v);
|
|
14
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveStablePortStart({
|
|
18
|
+
env = process.env,
|
|
19
|
+
stackName,
|
|
20
|
+
baseKey,
|
|
21
|
+
rangeKey,
|
|
22
|
+
defaultBase,
|
|
23
|
+
defaultRange,
|
|
24
|
+
}) {
|
|
25
|
+
const baseRaw = (env[baseKey] ?? '').toString().trim();
|
|
26
|
+
const rangeRaw = (env[rangeKey] ?? '').toString().trim();
|
|
27
|
+
const base = coercePositiveInt(baseRaw) ?? defaultBase;
|
|
28
|
+
const range = coercePositiveInt(rangeRaw) ?? defaultRange;
|
|
29
|
+
return base + (hashStringToInt(stackName) % range);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function pickMetroPort({
|
|
33
|
+
startPort,
|
|
34
|
+
forcedPort,
|
|
35
|
+
reservedPorts = new Set(),
|
|
36
|
+
host = '127.0.0.1',
|
|
37
|
+
} = {}) {
|
|
38
|
+
const forced = coercePositiveInt(forcedPort);
|
|
39
|
+
if (forced && !reservedPorts.has(forced)) {
|
|
40
|
+
const ok = await isTcpPortFree(forced, { host });
|
|
41
|
+
if (ok) return forced;
|
|
42
|
+
}
|
|
43
|
+
return await pickNextFreeTcpPort(startPort, { reservedPorts, host });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function wantsStablePortStrategy({ env = process.env, strategyKey, legacyStrategyKey } = {}) {
|
|
47
|
+
void legacyStrategyKey;
|
|
48
|
+
const raw = (env[strategyKey] ?? 'ephemeral').toString().trim() || 'ephemeral';
|
|
49
|
+
return raw === 'stable';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function pickUiDevMetroPort({
|
|
53
|
+
env = process.env,
|
|
54
|
+
stackMode,
|
|
55
|
+
stackName,
|
|
56
|
+
reservedPorts = new Set(),
|
|
57
|
+
host = '127.0.0.1',
|
|
58
|
+
} = {}) {
|
|
59
|
+
// Legacy alias: UI dev Metro is now the unified Expo dev server port.
|
|
60
|
+
return await pickExpoDevMetroPort({ env, stackMode, stackName, reservedPorts, host });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function pickMobileDevMetroPort({
|
|
64
|
+
env = process.env,
|
|
65
|
+
stackMode,
|
|
66
|
+
stackName,
|
|
67
|
+
reservedPorts = new Set(),
|
|
68
|
+
host = '127.0.0.1',
|
|
69
|
+
} = {}) {
|
|
70
|
+
// Legacy alias: mobile dev Metro is now the unified Expo dev server port.
|
|
71
|
+
return await pickExpoDevMetroPort({ env, stackMode, stackName, reservedPorts, host });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function pickExpoDevMetroPort({
|
|
75
|
+
env = process.env,
|
|
76
|
+
stackMode,
|
|
77
|
+
stackName,
|
|
78
|
+
reservedPorts = new Set(),
|
|
79
|
+
host = '127.0.0.1',
|
|
80
|
+
} = {}) {
|
|
81
|
+
const forcedPort = (env.HAPPIER_STACK_EXPO_DEV_PORT ?? '').toString().trim() || '';
|
|
82
|
+
|
|
83
|
+
const stable =
|
|
84
|
+
stackMode &&
|
|
85
|
+
wantsStablePortStrategy({
|
|
86
|
+
env,
|
|
87
|
+
strategyKey: 'HAPPIER_STACK_EXPO_DEV_PORT_STRATEGY',
|
|
88
|
+
});
|
|
89
|
+
const startPort = stable
|
|
90
|
+
? resolveStablePortStart({
|
|
91
|
+
env,
|
|
92
|
+
stackName,
|
|
93
|
+
baseKey: 'HAPPIER_STACK_EXPO_DEV_PORT_BASE',
|
|
94
|
+
rangeKey: 'HAPPIER_STACK_EXPO_DEV_PORT_RANGE',
|
|
95
|
+
defaultBase: 8081,
|
|
96
|
+
defaultRange: 1000,
|
|
97
|
+
})
|
|
98
|
+
: 8081;
|
|
99
|
+
|
|
100
|
+
return await pickMetroPort({ startPort, forcedPort, reservedPorts, host });
|
|
101
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
|
|
5
|
+
import { pickMetroPort } from './metro_ports.mjs';
|
|
6
|
+
|
|
7
|
+
function listenEphemeralPort() {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const srv = net.createServer();
|
|
10
|
+
srv.on('error', reject);
|
|
11
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
12
|
+
const addr = srv.address();
|
|
13
|
+
if (!addr || typeof addr === 'string') {
|
|
14
|
+
srv.close(() => reject(new Error('failed to allocate port')));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const port = Number(addr.port);
|
|
18
|
+
srv.close((err) => {
|
|
19
|
+
if (err) reject(err);
|
|
20
|
+
else resolve(port);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('pickMetroPort does not reuse forced port when it is reserved', async () => {
|
|
27
|
+
const forced = await listenEphemeralPort();
|
|
28
|
+
const picked = await pickMetroPort({
|
|
29
|
+
startPort: forced,
|
|
30
|
+
forcedPort: String(forced),
|
|
31
|
+
reservedPorts: new Set([forced]),
|
|
32
|
+
host: '127.0.0.1',
|
|
33
|
+
});
|
|
34
|
+
assert.notEqual(picked, forced);
|
|
35
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function rand() {
|
|
5
|
+
return Math.random().toString(16).slice(2);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function buildIntoTempThenReplace(targetDir, buildFn) {
|
|
9
|
+
const outDir = String(targetDir ?? '').trim();
|
|
10
|
+
if (!outDir) throw new Error('[fs] buildIntoTempThenReplace: missing targetDir');
|
|
11
|
+
if (typeof buildFn !== 'function') throw new Error('[fs] buildIntoTempThenReplace: buildFn must be a function');
|
|
12
|
+
|
|
13
|
+
const parent = dirname(outDir);
|
|
14
|
+
const tmpDir = join(parent, `.tmp.${Date.now()}.${process.pid}.${rand()}`);
|
|
15
|
+
|
|
16
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
17
|
+
await mkdir(tmpDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
let ok = false;
|
|
20
|
+
try {
|
|
21
|
+
await buildFn(tmpDir);
|
|
22
|
+
ok = true;
|
|
23
|
+
} finally {
|
|
24
|
+
if (!ok) {
|
|
25
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Swap only after a successful build.
|
|
30
|
+
const backupDir = join(parent, `.backup.${Date.now()}.${process.pid}.${rand()}`);
|
|
31
|
+
let hadExisting = false;
|
|
32
|
+
try {
|
|
33
|
+
await rename(outDir, backupDir);
|
|
34
|
+
hadExisting = true;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err?.code !== 'ENOENT') throw err;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await rename(tmpDir, outDir);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (hadExisting) {
|
|
43
|
+
await rename(backupDir, outDir).catch((restoreErr) => {
|
|
44
|
+
if (err && typeof err === 'object') {
|
|
45
|
+
err.restoreError = restoreErr;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hadExisting) {
|
|
53
|
+
await rm(backupDir, { recursive: true, force: true }).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { buildIntoTempThenReplace } from './atomic_dir_swap.mjs';
|
|
8
|
+
|
|
9
|
+
async function withTempRoot(t) {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'hstack-atomic-dir-'));
|
|
11
|
+
t.after(async () => {
|
|
12
|
+
await rm(root, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('buildIntoTempThenReplace preserves existing dir when build fails', async (t) => {
|
|
18
|
+
const root = await withTempRoot(t);
|
|
19
|
+
const outDir = join(root, 'ui');
|
|
20
|
+
await mkdir(outDir, { recursive: true });
|
|
21
|
+
await writeFile(join(outDir, 'marker.txt'), 'old\n', 'utf-8');
|
|
22
|
+
|
|
23
|
+
await assert.rejects(
|
|
24
|
+
async () => {
|
|
25
|
+
await buildIntoTempThenReplace(outDir, async (tmp) => {
|
|
26
|
+
await writeFile(join(tmp, 'marker.txt'), 'new\n', 'utf-8');
|
|
27
|
+
throw new Error('boom');
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
/boom/
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const after = await readFile(join(outDir, 'marker.txt'), 'utf-8');
|
|
34
|
+
assert.equal(after, 'old\n');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('buildIntoTempThenReplace replaces dir on success', async (t) => {
|
|
38
|
+
const root = await withTempRoot(t);
|
|
39
|
+
const outDir = join(root, 'ui');
|
|
40
|
+
await mkdir(outDir, { recursive: true });
|
|
41
|
+
await writeFile(join(outDir, 'marker.txt'), 'old\n', 'utf-8');
|
|
42
|
+
|
|
43
|
+
await buildIntoTempThenReplace(outDir, async (tmp) => {
|
|
44
|
+
await writeFile(join(tmp, 'marker.txt'), 'new\n', 'utf-8');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const after = await readFile(join(outDir, 'marker.txt'), 'utf-8');
|
|
48
|
+
assert.equal(after, 'new\n');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('buildIntoTempThenReplace validates required arguments', async () => {
|
|
52
|
+
await assert.rejects(async () => buildIntoTempThenReplace('', async () => {}), /missing targetDir/i);
|
|
53
|
+
await assert.rejects(async () => buildIntoTempThenReplace('/tmp/out', null), /buildFn must be a function/i);
|
|
54
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
export async function readJsonIfExists(path, { defaultValue = null } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
const p = String(path ?? '').trim();
|
|
8
|
+
if (!p || !existsSync(p)) return defaultValue;
|
|
9
|
+
const raw = await readFile(p, 'utf-8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return defaultValue;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeJsonAtomic(path, value) {
|
|
17
|
+
const p = String(path ?? '').trim();
|
|
18
|
+
if (!p) throw new Error('writeJsonAtomic: path is required');
|
|
19
|
+
const dir = dirname(p);
|
|
20
|
+
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
21
|
+
const tmp = join(dir, `.tmp.${Date.now()}.${Math.random().toString(16).slice(2)}.json`);
|
|
22
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
23
|
+
await rename(tmp, p);
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export async function ensureDir(path) {
|
|
5
|
+
await mkdir(path, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function readTextIfExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
const p = String(path ?? '').trim();
|
|
11
|
+
if (!p || !existsSync(p)) return null;
|
|
12
|
+
const raw = await readFile(p, 'utf-8');
|
|
13
|
+
const t = raw.trim();
|
|
14
|
+
return t ? t : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readTextOrEmpty(path) {
|
|
21
|
+
try {
|
|
22
|
+
const p = String(path ?? '').trim();
|
|
23
|
+
if (!p || !existsSync(p)) return '';
|
|
24
|
+
return await readFile(p, 'utf-8');
|
|
25
|
+
} catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function readLastLines(path, lines = 60) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(path, 'utf-8');
|
|
6
|
+
const parts = raw.split('\n');
|
|
7
|
+
return parts.slice(Math.max(0, parts.length - lines)).join('\n');
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { getDevRepoDir, getRepoDir } from '../paths/paths.mjs';
|
|
6
|
+
import { runCapture } from '../proc/proc.mjs';
|
|
7
|
+
|
|
8
|
+
async function gitHasRemote({ repoDir, remote }) {
|
|
9
|
+
try {
|
|
10
|
+
const r = String(remote ?? '').trim();
|
|
11
|
+
if (!r) return false;
|
|
12
|
+
await runCapture('git', ['remote', 'get-url', r], { cwd: repoDir });
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveDevBranchName(env = process.env) {
|
|
20
|
+
return String(env.HAPPIER_STACK_DEV_BRANCH ?? '').trim() || 'dev';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getRemoteUrl({ repoDir, remoteName }) {
|
|
24
|
+
const r = String(remoteName ?? '').trim();
|
|
25
|
+
if (!r) return '';
|
|
26
|
+
try {
|
|
27
|
+
return (await runCapture('git', ['remote', 'get-url', r], { cwd: repoDir })).trim();
|
|
28
|
+
} catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Remote used to *read* the canonical dev branch from (sync/reset defaults).
|
|
34
|
+
export async function resolveDevSyncRemote({ repoDir, env = process.env, preferred = '' } = {}) {
|
|
35
|
+
const want = String(preferred ?? '').trim();
|
|
36
|
+
if (want) return want;
|
|
37
|
+
|
|
38
|
+
// Default preference: upstream (if configured), else origin.
|
|
39
|
+
if (await gitHasRemote({ repoDir, remote: 'upstream' })) return 'upstream';
|
|
40
|
+
if (await gitHasRemote({ repoDir, remote: 'origin' })) return 'origin';
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Remote used to *push* feature branches to (extract defaults).
|
|
45
|
+
// Conventional git behavior: push to origin (fork), PR against upstream.
|
|
46
|
+
export async function resolveDevPushRemote({ repoDir, env = process.env, preferred = '' } = {}) {
|
|
47
|
+
const want = String(preferred ?? '').trim();
|
|
48
|
+
if (want) return want;
|
|
49
|
+
|
|
50
|
+
if (await gitHasRemote({ repoDir, remote: 'origin' })) return 'origin';
|
|
51
|
+
if (await gitHasRemote({ repoDir, remote: 'upstream' })) return 'upstream';
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function ensureDevCheckout({ rootDir, env = process.env, remote = '' } = {}) {
|
|
56
|
+
const mainDir = getRepoDir(rootDir, { ...env, HAPPIER_STACK_REPO_DIR: '' });
|
|
57
|
+
const devDir = getDevRepoDir(rootDir, env);
|
|
58
|
+
const devBranch = resolveDevBranchName(env);
|
|
59
|
+
|
|
60
|
+
if (!existsSync(mainDir) || !existsSync(join(mainDir, '.git'))) {
|
|
61
|
+
throw new Error(`[dev] missing main checkout at ${mainDir}\nFix: run \`hstack bootstrap --clone\` (or \`hstack setup\`).`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const syncRemote = await resolveDevSyncRemote({ repoDir: mainDir, env, preferred: remote });
|
|
65
|
+
if (!syncRemote) {
|
|
66
|
+
throw new Error(`[dev] missing git remotes in ${mainDir}\nFix: ensure at least one of {upstream, origin} exists.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const preferred = String(remote ?? '').trim();
|
|
70
|
+
const hasUpstream = await gitHasRemote({ repoDir: mainDir, remote: 'upstream' });
|
|
71
|
+
const hasOrigin = await gitHasRemote({ repoDir: mainDir, remote: 'origin' });
|
|
72
|
+
let trackingRemote = preferred;
|
|
73
|
+
if (!trackingRemote) {
|
|
74
|
+
// Maintainership UX:
|
|
75
|
+
// Contributor UX:
|
|
76
|
+
// - if origin differs from upstream, assume origin is a fork and track origin/dev (pushable).
|
|
77
|
+
// - otherwise, track upstream/dev (canonical).
|
|
78
|
+
if (hasUpstream && hasOrigin) {
|
|
79
|
+
const upstreamUrl = await getRemoteUrl({ repoDir: mainDir, remoteName: 'upstream' });
|
|
80
|
+
const originUrl = await getRemoteUrl({ repoDir: mainDir, remoteName: 'origin' });
|
|
81
|
+
trackingRemote = upstreamUrl && originUrl && upstreamUrl !== originUrl ? 'origin' : 'upstream';
|
|
82
|
+
} else if (hasOrigin) {
|
|
83
|
+
trackingRemote = 'origin';
|
|
84
|
+
} else if (hasUpstream) {
|
|
85
|
+
trackingRemote = 'upstream';
|
|
86
|
+
} else {
|
|
87
|
+
trackingRemote = syncRemote;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Already exists: treat as ok, but report the actual tracking remote best-effort.
|
|
92
|
+
if (existsSync(devDir) && existsSync(join(devDir, '.git'))) {
|
|
93
|
+
let inferred = '';
|
|
94
|
+
try {
|
|
95
|
+
const upstreamRef = (await runCapture('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { cwd: devDir })).trim();
|
|
96
|
+
inferred = upstreamRef.split('/')[0] ?? '';
|
|
97
|
+
} catch {
|
|
98
|
+
inferred = '';
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, created: false, mainDir, devDir, devBranch, syncRemote, trackingRemote: inferred || trackingRemote };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await mkdir(devDir, { recursive: true }).catch(() => {});
|
|
104
|
+
|
|
105
|
+
// Create/overwrite a local branch "dev" from <remote>/<devBranch>, then add the worktree.
|
|
106
|
+
// NOTE: `-B` resets the local branch name to the requested start-point (safe for a fresh workspace).
|
|
107
|
+
const startPoint = `${trackingRemote}/${devBranch}`;
|
|
108
|
+
try {
|
|
109
|
+
await runCapture('git', ['fetch', trackingRemote, devBranch], { cwd: mainDir });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// Common contributor case: origin points to a fresh fork that doesn't have dev yet.
|
|
112
|
+
// Best-effort: seed origin/dev from upstream/dev (or the chosen sync remote).
|
|
113
|
+
if (trackingRemote === 'origin' && syncRemote && syncRemote !== 'origin') {
|
|
114
|
+
await runCapture('git', ['fetch', syncRemote, devBranch], { cwd: mainDir });
|
|
115
|
+
await runCapture('git', ['push', 'origin', `refs/remotes/${syncRemote}/${devBranch}:refs/heads/${devBranch}`], { cwd: mainDir });
|
|
116
|
+
await runCapture('git', ['fetch', 'origin', devBranch], { cwd: mainDir });
|
|
117
|
+
} else {
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await runCapture('git', ['worktree', 'add', '-B', devBranch, devDir, startPoint], { cwd: mainDir });
|
|
123
|
+
// Ensure the branch explicitly tracks the chosen remote for clarity (git status shows upstream correctly).
|
|
124
|
+
await runCapture('git', ['-C', devDir, 'branch', '--set-upstream-to', startPoint, devBranch], { cwd: mainDir }).catch(() => {});
|
|
125
|
+
|
|
126
|
+
return { ok: true, created: true, mainDir, devDir, devBranch, syncRemote, trackingRemote };
|
|
127
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { ensureDevCheckout } from './dev_checkout.mjs';
|
|
10
|
+
|
|
11
|
+
function runCapture(cmd, args, { cwd, env } = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const proc = spawn(cmd, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
14
|
+
let stdout = '';
|
|
15
|
+
let stderr = '';
|
|
16
|
+
proc.stdout.on('data', (d) => (stdout += String(d)));
|
|
17
|
+
proc.stderr.on('data', (d) => (stderr += String(d)));
|
|
18
|
+
proc.on('error', reject);
|
|
19
|
+
proc.on('exit', (code) => {
|
|
20
|
+
if (code === 0) return resolve({ stdout, stderr });
|
|
21
|
+
const e = new Error(`${cmd} failed (code=${code})`);
|
|
22
|
+
e.stdout = stdout;
|
|
23
|
+
e.stderr = stderr;
|
|
24
|
+
reject(e);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function withTempRoot(t) {
|
|
30
|
+
const dir = await mkdtemp(join(tmpdir(), 'hstack-dev-checkout-'));
|
|
31
|
+
t.after(async () => {
|
|
32
|
+
await rm(dir, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function initBareRepo(dir) {
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
39
|
+
await runCapture('git', ['init', '--bare'], { cwd: dir });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function seedRepoWithBranches({ seedDir, upstreamBareDir, originBareDir }) {
|
|
43
|
+
await mkdir(seedDir, { recursive: true });
|
|
44
|
+
await runCapture('git', ['init'], { cwd: seedDir });
|
|
45
|
+
await runCapture('git', ['config', 'user.email', 'test@example.com'], { cwd: seedDir });
|
|
46
|
+
await runCapture('git', ['config', 'user.name', 'Test'], { cwd: seedDir });
|
|
47
|
+
|
|
48
|
+
await writeFile(join(seedDir, 'README.md'), 'seed\n', 'utf-8');
|
|
49
|
+
await runCapture('git', ['add', '.'], { cwd: seedDir });
|
|
50
|
+
await runCapture('git', ['commit', '-m', 'seed'], { cwd: seedDir });
|
|
51
|
+
|
|
52
|
+
await runCapture('git', ['branch', '-M', 'main'], { cwd: seedDir });
|
|
53
|
+
await runCapture('git', ['checkout', '-b', 'dev'], { cwd: seedDir });
|
|
54
|
+
|
|
55
|
+
await runCapture('git', ['remote', 'add', 'upstream', upstreamBareDir], { cwd: seedDir });
|
|
56
|
+
await runCapture('git', ['remote', 'add', 'origin', originBareDir], { cwd: seedDir });
|
|
57
|
+
await runCapture('git', ['push', 'upstream', 'main:main', 'dev:dev'], { cwd: seedDir });
|
|
58
|
+
await runCapture('git', ['push', 'origin', 'main:main', 'dev:dev'], { cwd: seedDir });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function cloneIntoWorkspaceMain({ workspaceDir, upstreamBareDir, originBareDir }) {
|
|
62
|
+
const mainDir = join(workspaceDir, 'main');
|
|
63
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
64
|
+
await runCapture('git', ['clone', upstreamBareDir, mainDir], { cwd: workspaceDir });
|
|
65
|
+
|
|
66
|
+
// Simulate the common contributor layout:
|
|
67
|
+
// - upstream = canonical repo
|
|
68
|
+
// - origin = fork
|
|
69
|
+
await runCapture('git', ['remote', 'set-url', 'origin', originBareDir], { cwd: mainDir });
|
|
70
|
+
await runCapture('git', ['remote', 'add', 'upstream', upstreamBareDir], { cwd: mainDir });
|
|
71
|
+
await runCapture('git', ['fetch', '--all'], { cwd: mainDir });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function createDevCheckoutFixture(t, { forkWorkflow }) {
|
|
75
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const stackRootDir = dirname(dirname(dirname(scriptsDir)));
|
|
77
|
+
const tempRoot = await withTempRoot(t);
|
|
78
|
+
const workspaceDir = join(tempRoot, 'workspace');
|
|
79
|
+
const upstreamBareDir = join(tempRoot, 'upstream.git');
|
|
80
|
+
const originBareDir = forkWorkflow ? join(tempRoot, 'origin.git') : upstreamBareDir;
|
|
81
|
+
const seedDir = join(tempRoot, 'seed');
|
|
82
|
+
|
|
83
|
+
await initBareRepo(upstreamBareDir);
|
|
84
|
+
if (forkWorkflow) {
|
|
85
|
+
await initBareRepo(originBareDir);
|
|
86
|
+
}
|
|
87
|
+
await seedRepoWithBranches({ seedDir, upstreamBareDir, originBareDir });
|
|
88
|
+
await cloneIntoWorkspaceMain({ workspaceDir, upstreamBareDir, originBareDir });
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
stackRootDir,
|
|
92
|
+
workspaceDir,
|
|
93
|
+
originBareDir,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('ensureDevCheckout prefers upstream/dev when upstream is pushable', async (t) => {
|
|
98
|
+
const fixture = await createDevCheckoutFixture(t, { forkWorkflow: false });
|
|
99
|
+
const env = { ...process.env, HAPPIER_STACK_WORKSPACE_DIR: fixture.workspaceDir };
|
|
100
|
+
const res = await ensureDevCheckout({ rootDir: fixture.stackRootDir, env });
|
|
101
|
+
assert.equal(res.ok, true);
|
|
102
|
+
assert.equal(res.trackingRemote, 'upstream');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('ensureDevCheckout uses origin/dev when upstream is not pushable (fork workflow)', async (t) => {
|
|
106
|
+
const fixture = await createDevCheckoutFixture(t, { forkWorkflow: true });
|
|
107
|
+
const env = { ...process.env, HAPPIER_STACK_WORKSPACE_DIR: fixture.workspaceDir };
|
|
108
|
+
const res = await ensureDevCheckout({ rootDir: fixture.stackRootDir, env });
|
|
109
|
+
assert.equal(res.ok, true);
|
|
110
|
+
assert.equal(res.trackingRemote, 'origin');
|
|
111
|
+
|
|
112
|
+
const devDir = join(fixture.workspaceDir, 'dev');
|
|
113
|
+
const { stdout } = await runCapture('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { cwd: devDir });
|
|
114
|
+
assert.equal(stdout.trim(), 'origin/dev');
|
|
115
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { runCapture } from '../proc/proc.mjs';
|
|
2
|
+
|
|
3
|
+
export async function gitCapture({ cwd, args }) {
|
|
4
|
+
return String(await runCapture('git', args, { cwd }));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function gitOk({ cwd, args }) {
|
|
8
|
+
try {
|
|
9
|
+
await runCapture('git', args, { cwd });
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function normalizeRemoteName({ cwd, remote }) {
|
|
17
|
+
const want = String(remote ?? '').trim();
|
|
18
|
+
if (!want) return want;
|
|
19
|
+
|
|
20
|
+
if (await gitOk({ cwd, args: ['remote', 'get-url', want] })) return want;
|
|
21
|
+
|
|
22
|
+
// Treat origin/fork as interchangeable if one exists.
|
|
23
|
+
if (want === 'origin' && (await gitOk({ cwd, args: ['remote', 'get-url', 'fork'] }))) return 'fork';
|
|
24
|
+
if (want === 'fork' && (await gitOk({ cwd, args: ['remote', 'get-url', 'origin'] }))) return 'origin';
|
|
25
|
+
|
|
26
|
+
return want;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function resolveRemoteDefaultBranch({ cwd, remote }) {
|
|
30
|
+
const r = String(remote ?? '').trim();
|
|
31
|
+
if (!r) return 'main';
|
|
32
|
+
|
|
33
|
+
// Prefer refs/remotes/<remote>/HEAD when available.
|
|
34
|
+
try {
|
|
35
|
+
const headRef = (await gitCapture({ cwd, args: ['symbolic-ref', '-q', '--short', `refs/remotes/${r}/HEAD`] })).trim();
|
|
36
|
+
if (headRef.startsWith(`${r}/`)) {
|
|
37
|
+
return headRef.slice(r.length + 1);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback: parse `git remote show` output.
|
|
44
|
+
try {
|
|
45
|
+
const out = await gitCapture({ cwd, args: ['remote', 'show', r] });
|
|
46
|
+
for (const line of out.split('\n')) {
|
|
47
|
+
const m = line.match(/^\s*HEAD branch:\s*(.+)\s*$/);
|
|
48
|
+
if (m?.[1]) return m[1].trim();
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 'main';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function ensureRemoteRefAvailable({ cwd, remote, branch }) {
|
|
58
|
+
const r = String(remote ?? '').trim();
|
|
59
|
+
const b = String(branch ?? '').trim();
|
|
60
|
+
if (!r || !b) return false;
|
|
61
|
+
const ref = `refs/remotes/${r}/${b}`;
|
|
62
|
+
if (await gitOk({ cwd, args: ['show-ref', '--verify', '--quiet', ref] })) return true;
|
|
63
|
+
// Best-effort fetch of the default branch.
|
|
64
|
+
await gitCapture({ cwd, args: ['fetch', '--quiet', r, b] }).catch(() => '');
|
|
65
|
+
return await gitOk({ cwd, args: ['show-ref', '--verify', '--quiet', ref] });
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function parseNameStatusZ(buf) {
|
|
2
|
+
const raw = String(buf ?? '');
|
|
3
|
+
if (!raw) return [];
|
|
4
|
+
const parts = raw.split('\0').filter((x) => x.length);
|
|
5
|
+
const entries = [];
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (i < parts.length) {
|
|
8
|
+
const status = parts[i++];
|
|
9
|
+
const code = status[0] ?? '';
|
|
10
|
+
if (!code) break;
|
|
11
|
+
if (code === 'R' || code === 'C') {
|
|
12
|
+
const from = parts[i++] ?? '';
|
|
13
|
+
const to = parts[i++] ?? '';
|
|
14
|
+
entries.push({ code, status, from, to });
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const path = parts[i++] ?? '';
|
|
18
|
+
entries.push({ code, status, path });
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|