@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,39 @@
|
|
|
1
|
+
import { runWithConcurrencyLimit } from '../proc/parallel.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run a list of "slice jobs" with:
|
|
5
|
+
* - a mandatory sequential first job (preflight)
|
|
6
|
+
* - parallel execution for the remainder (bounded by `limit`)
|
|
7
|
+
* - stable, input-order results
|
|
8
|
+
* - optional early-abort after the first job (e.g. auth/credits missing)
|
|
9
|
+
*/
|
|
10
|
+
export async function runSlicedJobs({ items, limit = 1, run, shouldAbortEarly } = {}) {
|
|
11
|
+
const list = Array.isArray(items) ? items : [];
|
|
12
|
+
if (!list.length) return [];
|
|
13
|
+
if (typeof run !== 'function') throw new Error('[review] runSlicedJobs: missing run()');
|
|
14
|
+
|
|
15
|
+
const concurrency = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
|
|
16
|
+
|
|
17
|
+
const out = [];
|
|
18
|
+
// Always run the first item sequentially so we can fail fast on auth/credits problems
|
|
19
|
+
// before spinning up many long-running review jobs.
|
|
20
|
+
// eslint-disable-next-line no-await-in-loop
|
|
21
|
+
const firstRes = await run(list[0]);
|
|
22
|
+
out.push(firstRes);
|
|
23
|
+
if (typeof shouldAbortEarly === 'function' && shouldAbortEarly(firstRes)) {
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (list.length === 1) return out;
|
|
28
|
+
|
|
29
|
+
const rest = list.slice(1);
|
|
30
|
+
const restRes = await runWithConcurrencyLimit({
|
|
31
|
+
items: rest,
|
|
32
|
+
limit: concurrency,
|
|
33
|
+
fn: async (item) => await run(item),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Preserve input order.
|
|
37
|
+
return [...out, ...restRes];
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runSlicedJobs } from './sliced_runner.mjs';
|
|
4
|
+
|
|
5
|
+
test('runSlicedJobs preserves order and respects concurrency', async () => {
|
|
6
|
+
const items = Array.from({ length: 7 }, (_, i) => ({ index: i + 1 }));
|
|
7
|
+
|
|
8
|
+
let active = 0;
|
|
9
|
+
let maxActive = 0;
|
|
10
|
+
let startedParallel = 0;
|
|
11
|
+
let releaseParallel;
|
|
12
|
+
const parallelGate = new Promise((resolve) => {
|
|
13
|
+
releaseParallel = resolve;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const results = await runSlicedJobs({
|
|
17
|
+
items,
|
|
18
|
+
limit: 2,
|
|
19
|
+
run: async (item) => {
|
|
20
|
+
active += 1;
|
|
21
|
+
maxActive = Math.max(maxActive, active);
|
|
22
|
+
if (item.index > 1) {
|
|
23
|
+
startedParallel += 1;
|
|
24
|
+
if (startedParallel === 2) {
|
|
25
|
+
releaseParallel();
|
|
26
|
+
}
|
|
27
|
+
await parallelGate;
|
|
28
|
+
}
|
|
29
|
+
active -= 1;
|
|
30
|
+
return { index: item.index };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(maxActive, 2);
|
|
35
|
+
assert.deepEqual(
|
|
36
|
+
results.map((r) => r.index),
|
|
37
|
+
items.map((i) => i.index)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('runSlicedJobs can abort early after the first item', async () => {
|
|
42
|
+
const items = Array.from({ length: 5 }, (_, i) => ({ index: i + 1 }));
|
|
43
|
+
const seen = [];
|
|
44
|
+
|
|
45
|
+
const results = await runSlicedJobs({
|
|
46
|
+
items,
|
|
47
|
+
limit: 3,
|
|
48
|
+
run: async (item) => {
|
|
49
|
+
seen.push(item.index);
|
|
50
|
+
return { index: item.index, abort: item.index === 1 };
|
|
51
|
+
},
|
|
52
|
+
shouldAbortEarly: (res) => Boolean(res?.abort),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.deepEqual(seen, [1]);
|
|
56
|
+
assert.deepEqual(results.map((r) => r.index), [1]);
|
|
57
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
function normalizePath(p) {
|
|
2
|
+
return String(p ?? '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function commonPrefixParts(partsList) {
|
|
6
|
+
if (!partsList.length) return [];
|
|
7
|
+
const first = partsList[0];
|
|
8
|
+
let n = first.length;
|
|
9
|
+
for (const parts of partsList.slice(1)) {
|
|
10
|
+
n = Math.min(n, parts.length, n);
|
|
11
|
+
for (let i = 0; i < n; i += 1) {
|
|
12
|
+
if (parts[i] !== first[i]) {
|
|
13
|
+
n = i;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return first.slice(0, n);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pathPrefixLabel(parts, { maxDepth = 4 } = {}) {
|
|
22
|
+
const depth = Math.min(parts.length, Math.max(1, maxDepth));
|
|
23
|
+
return parts.slice(0, depth).join('/');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function groupByPrefix(paths, depth) {
|
|
27
|
+
const groups = new Map();
|
|
28
|
+
for (const p of paths) {
|
|
29
|
+
const parts = normalizePath(p).split('/').filter(Boolean);
|
|
30
|
+
const key = parts.slice(0, Math.max(1, Math.min(depth, parts.length))).join('/');
|
|
31
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
32
|
+
groups.get(key).push(p);
|
|
33
|
+
}
|
|
34
|
+
return groups;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Plan review slices that:
|
|
39
|
+
* - cover every changed path exactly once
|
|
40
|
+
* - keep each slice at <= maxFiles where possible
|
|
41
|
+
* - prefer directory-prefix grouping (better reviewer context) over raw batching
|
|
42
|
+
*
|
|
43
|
+
* The output is intended for "HEAD-sliced" review: the reviewer gets a focused diff
|
|
44
|
+
* while still having access to the full repo code at HEAD.
|
|
45
|
+
*/
|
|
46
|
+
export function planPathSlices({ changedPaths, maxFiles = 300, maxPrefixDepth = 6 } = {}) {
|
|
47
|
+
const unique = Array.from(new Set((Array.isArray(changedPaths) ? changedPaths : []).map(normalizePath))).filter(Boolean);
|
|
48
|
+
unique.sort();
|
|
49
|
+
if (!unique.length) return [];
|
|
50
|
+
|
|
51
|
+
const limit = Number.isFinite(maxFiles) && maxFiles > 0 ? Math.floor(maxFiles) : 300;
|
|
52
|
+
if (unique.length <= limit) {
|
|
53
|
+
const parts = unique.map((p) => p.split('/').filter(Boolean));
|
|
54
|
+
const prefix = commonPrefixParts(parts);
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
label: prefix.length ? `${pathPrefixLabel(prefix, { maxDepth: 3 })}/` : 'repo/',
|
|
58
|
+
paths: unique,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// First pass: top-level directories (plus root files).
|
|
64
|
+
const topGroups = groupByPrefix(unique, 1);
|
|
65
|
+
|
|
66
|
+
const slices = [];
|
|
67
|
+
const pushSlice = (label, paths) => {
|
|
68
|
+
const normalized = Array.from(new Set(paths.map(normalizePath))).filter(Boolean).sort();
|
|
69
|
+
if (!normalized.length) return;
|
|
70
|
+
slices.push({ label, paths: normalized });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const [top, paths] of topGroups.entries()) {
|
|
74
|
+
if (paths.length <= limit) {
|
|
75
|
+
pushSlice(top.includes('/') ? top : `${top}/`, paths);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Iteratively refine prefix depth within this group until all chunks are <= limit.
|
|
80
|
+
let pending = [{ label: top, paths }];
|
|
81
|
+
for (let depth = 2; depth <= maxPrefixDepth && pending.some((x) => x.paths.length > limit); depth += 1) {
|
|
82
|
+
const next = [];
|
|
83
|
+
for (const item of pending) {
|
|
84
|
+
if (item.paths.length <= limit) {
|
|
85
|
+
next.push(item);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const groups = groupByPrefix(item.paths, depth);
|
|
89
|
+
if (groups.size <= 1) {
|
|
90
|
+
next.push(item);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const [k, v] of groups.entries()) {
|
|
94
|
+
next.push({ label: k, paths: v });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
pending = next;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Final pass: pack refined groups into <=limit windows (greedy, stable order).
|
|
101
|
+
pending.sort((a, b) => a.label.localeCompare(b.label));
|
|
102
|
+
let bucket = [];
|
|
103
|
+
let bucketCount = 0;
|
|
104
|
+
let bucketLabelParts = [];
|
|
105
|
+
const flush = () => {
|
|
106
|
+
if (!bucket.length) return;
|
|
107
|
+
const parts = commonPrefixParts(bucketLabelParts);
|
|
108
|
+
const label = parts.length ? `${pathPrefixLabel(parts, { maxDepth: 4 })}/` : `${top}/`;
|
|
109
|
+
pushSlice(label, bucket);
|
|
110
|
+
bucket = [];
|
|
111
|
+
bucketCount = 0;
|
|
112
|
+
bucketLabelParts = [];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (const g of pending) {
|
|
116
|
+
const n = g.paths.length;
|
|
117
|
+
if (n > limit) {
|
|
118
|
+
// Fall back to raw batching for truly massive groups (rare).
|
|
119
|
+
flush();
|
|
120
|
+
for (let i = 0; i < g.paths.length; i += limit) {
|
|
121
|
+
const batch = g.paths.slice(i, i + limit);
|
|
122
|
+
pushSlice(`${g.label}/`, batch);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (bucketCount + n > limit) {
|
|
127
|
+
flush();
|
|
128
|
+
}
|
|
129
|
+
bucket.push(...g.paths);
|
|
130
|
+
bucketCount += n;
|
|
131
|
+
bucketLabelParts.push(g.label.split('/').filter(Boolean));
|
|
132
|
+
}
|
|
133
|
+
flush();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Stable ordering helps humans follow progress.
|
|
137
|
+
slices.sort((a, b) => a.label.localeCompare(b.label));
|
|
138
|
+
return slices;
|
|
139
|
+
}
|
|
140
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { planPathSlices } from './slices.mjs';
|
|
4
|
+
|
|
5
|
+
test('planPathSlices returns empty for no paths', () => {
|
|
6
|
+
assert.deepEqual(planPathSlices({ changedPaths: [], maxFiles: 3 }), []);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('planPathSlices creates a single slice when under maxFiles', () => {
|
|
10
|
+
const slices = planPathSlices({
|
|
11
|
+
changedPaths: ['apps/ui/a.txt', 'apps/cli/b.txt', 'apps/server/c.txt'],
|
|
12
|
+
maxFiles: 10,
|
|
13
|
+
});
|
|
14
|
+
assert.equal(slices.length, 1);
|
|
15
|
+
assert.deepEqual(slices[0].paths, ['apps/cli/b.txt', 'apps/server/c.txt', 'apps/ui/a.txt']);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('planPathSlices splits large groups by prefix depth and respects maxFiles', () => {
|
|
19
|
+
const changedPaths = [
|
|
20
|
+
...Array.from({ length: 6 }, (_, i) => `apps/ui/sources/a${i}.ts`),
|
|
21
|
+
...Array.from({ length: 6 }, (_, i) => `apps/ui/sources/b${i}.ts`),
|
|
22
|
+
...Array.from({ length: 2 }, (_, i) => `apps/cli/src/x${i}.ts`),
|
|
23
|
+
];
|
|
24
|
+
const slices = planPathSlices({ changedPaths, maxFiles: 5, maxPrefixDepth: 4 });
|
|
25
|
+
assert.ok(slices.length > 1);
|
|
26
|
+
for (const s of slices) {
|
|
27
|
+
assert.ok(s.paths.length <= 5, `slice ${s.label} exceeded maxFiles`);
|
|
28
|
+
}
|
|
29
|
+
const all = slices.flatMap((s) => s.paths).sort();
|
|
30
|
+
assert.deepEqual(all, Array.from(new Set(changedPaths)).sort());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('planPathSlices normalizes duplicate and absolute/windows-shaped paths', () => {
|
|
34
|
+
const slices = planPathSlices({
|
|
35
|
+
changedPaths: ['apps\\ui\\a.ts', '/apps/ui/a.ts', 'apps/ui/a.ts', 'apps/ui/b.ts'],
|
|
36
|
+
maxFiles: 5,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(slices.length, 1);
|
|
40
|
+
assert.deepEqual(slices[0].paths, ['apps/ui/a.ts', 'apps/ui/b.ts']);
|
|
41
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getRepoDir } from '../paths/paths.mjs';
|
|
2
|
+
|
|
3
|
+
export function isStackMode(env = process.env) {
|
|
4
|
+
const stack = String(env.HAPPIER_STACK_STACK ?? '').trim();
|
|
5
|
+
const envFile = String(env.HAPPIER_STACK_ENV_FILE ?? '').trim();
|
|
6
|
+
return Boolean(stack && envFile);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function defaultRepoCheckoutDir(rootDir, env = process.env) {
|
|
10
|
+
const clean = { ...env, HAPPIER_STACK_REPO_DIR: '' };
|
|
11
|
+
return getRepoDir(rootDir, clean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveDefaultStackReviewComponents({ rootDir, components, env = process.env }) {
|
|
15
|
+
const list = Array.isArray(components) ? components : [];
|
|
16
|
+
if (!list.length) return [];
|
|
17
|
+
|
|
18
|
+
const effectiveRepo = getRepoDir(rootDir, env);
|
|
19
|
+
const defaultRepo = defaultRepoCheckoutDir(rootDir, env);
|
|
20
|
+
// Repo-only model: if the stack is pinned to a non-default worktree/checkout, all
|
|
21
|
+
// logical services share that same repo pin.
|
|
22
|
+
return effectiveRepo !== defaultRepo ? list : [];
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { resolveDefaultStackReviewComponents } from './targets.mjs';
|
|
4
|
+
|
|
5
|
+
test('resolveDefaultStackReviewComponents returns only non-default pinned components', () => {
|
|
6
|
+
const rootDir = '/tmp/hs-root';
|
|
7
|
+
const env = {
|
|
8
|
+
HAPPIER_STACK_WORKSPACE_DIR: '/tmp/hs-root',
|
|
9
|
+
HAPPIER_STACK_REPO_DIR: '/tmp/custom/happier',
|
|
10
|
+
};
|
|
11
|
+
const comps = resolveDefaultStackReviewComponents({
|
|
12
|
+
rootDir,
|
|
13
|
+
env,
|
|
14
|
+
components: ['happy', 'happy-cli', 'happy-server-light', 'happy-server'],
|
|
15
|
+
});
|
|
16
|
+
assert.deepEqual(comps.sort(), ['happy', 'happy-cli', 'happy-server-light', 'happy-server'].sort());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('resolveDefaultStackReviewComponents returns empty list for default repo checkout', () => {
|
|
20
|
+
const rootDir = '/tmp/hs-root';
|
|
21
|
+
const env = {
|
|
22
|
+
HAPPIER_STACK_WORKSPACE_DIR: '/tmp/hs-root',
|
|
23
|
+
HAPPIER_STACK_REPO_DIR: '',
|
|
24
|
+
};
|
|
25
|
+
const comps = resolveDefaultStackReviewComponents({
|
|
26
|
+
rootDir,
|
|
27
|
+
env,
|
|
28
|
+
components: ['happy', 'happy-cli'],
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(comps, []);
|
|
31
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { copyFile, cp, mkdir, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
async function mergeCopyDir({ srcDir, destDir }) {
|
|
6
|
+
if (!existsSync(srcDir)) return;
|
|
7
|
+
await mkdir(destDir, { recursive: true });
|
|
8
|
+
await cp(srcDir, destDir, { recursive: true, force: false, errorOnExist: false });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function copyFileIfNewer({ srcFile, destFile }) {
|
|
12
|
+
if (!existsSync(srcFile)) return;
|
|
13
|
+
try {
|
|
14
|
+
const srcStat = await stat(srcFile);
|
|
15
|
+
let destStat = null;
|
|
16
|
+
try {
|
|
17
|
+
destStat = await stat(destFile);
|
|
18
|
+
} catch {
|
|
19
|
+
destStat = null;
|
|
20
|
+
}
|
|
21
|
+
if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
|
|
22
|
+
await mkdir(join(destFile, '..'), { recursive: true });
|
|
23
|
+
await copyFile(srcFile, destFile);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// best-effort
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Best-effort: seed CodeRabbit auth/config into an isolated home directory.
|
|
32
|
+
*
|
|
33
|
+
* We do not read or print any auth contents; we only copy the on-disk state when present.
|
|
34
|
+
*/
|
|
35
|
+
export async function seedCodeRabbitHomeFromRealHome({ realHomeDir, isolatedHomeDir } = {}) {
|
|
36
|
+
const real = String(realHomeDir ?? '').trim();
|
|
37
|
+
const isolated = String(isolatedHomeDir ?? '').trim();
|
|
38
|
+
if (!real || !isolated || real === isolated) return;
|
|
39
|
+
|
|
40
|
+
// Common CodeRabbit state locations:
|
|
41
|
+
// - ~/.coderabbit/
|
|
42
|
+
// - ~/.config/coderabbit/ (XDG config)
|
|
43
|
+
// - ~/.cache/coderabbit/ (XDG cache)
|
|
44
|
+
// - ~/.local/share/coderabbit/ (XDG data)
|
|
45
|
+
// - ~/.local/state/coderabbit/ (XDG state)
|
|
46
|
+
//
|
|
47
|
+
// We merge-copy without overwriting existing files so a user can explicitly
|
|
48
|
+
// auth in the isolated dir and we won't clobber it.
|
|
49
|
+
await mergeCopyDir({ srcDir: join(real, '.coderabbit'), destDir: join(isolated, '.coderabbit') });
|
|
50
|
+
await mergeCopyDir({ srcDir: join(real, '.config', 'coderabbit'), destDir: join(isolated, '.config', 'coderabbit') });
|
|
51
|
+
await mergeCopyDir({ srcDir: join(real, '.cache', 'coderabbit'), destDir: join(isolated, '.cache', 'coderabbit') });
|
|
52
|
+
await mergeCopyDir({ srcDir: join(real, '.local', 'share', 'coderabbit'), destDir: join(isolated, '.local', 'share', 'coderabbit') });
|
|
53
|
+
await mergeCopyDir({ srcDir: join(real, '.local', 'state', 'coderabbit'), destDir: join(isolated, '.local', 'state', 'coderabbit') });
|
|
54
|
+
|
|
55
|
+
// If the user re-authenticated recently, refresh auth.json even when it already exists.
|
|
56
|
+
await copyFileIfNewer({
|
|
57
|
+
srcFile: join(real, '.coderabbit', 'auth.json'),
|
|
58
|
+
destFile: join(isolated, '.coderabbit', 'auth.json'),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Best-effort: seed Codex auth/config into an isolated CODEX_HOME directory.
|
|
64
|
+
*
|
|
65
|
+
* Codex stores auth/config under `CODEX_HOME` (default: ~/.codex). In stack review runs we
|
|
66
|
+
* use a per-repo isolated home (e.g. .project/codex-home) to avoid polluting ~/.codex and
|
|
67
|
+
* to keep sandboxed runs self-contained.
|
|
68
|
+
*
|
|
69
|
+
* We do not read or print any auth contents; we only copy the on-disk state when present.
|
|
70
|
+
*/
|
|
71
|
+
export async function seedCodexHomeFromRealHome({ realHomeDir, isolatedHomeDir } = {}) {
|
|
72
|
+
const real = String(realHomeDir ?? '').trim();
|
|
73
|
+
const isolated = String(isolatedHomeDir ?? '').trim();
|
|
74
|
+
if (!real || !isolated || real === isolated) return;
|
|
75
|
+
|
|
76
|
+
// Ensure prior runs do not leak custom user config (MCP/plugins) into review jobs.
|
|
77
|
+
try {
|
|
78
|
+
await rm(join(isolated, 'config.toml'), { force: true });
|
|
79
|
+
} catch {
|
|
80
|
+
// best-effort
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Copy auth only. We intentionally avoid copying user config.toml so review
|
|
84
|
+
// runs do not inherit personal MCP/plugin settings that can stall batch jobs.
|
|
85
|
+
await copyFileIfNewer({ srcFile: join(real, '.codex', 'auth.json'), destFile: join(isolated, 'auth.json') });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Best-effort: seed Auggie (Augment CLI) auth/config into an isolated cache directory.
|
|
90
|
+
*
|
|
91
|
+
* Auggie uses `~/.augment` by default (see `--augment-cache-dir`), and supports
|
|
92
|
+
* providing session auth via `AUGMENT_SESSION_AUTH` (same format as `~/.augment/session.json`).
|
|
93
|
+
*
|
|
94
|
+
* We do not read or print any auth contents; we only copy the on-disk state when present.
|
|
95
|
+
*/
|
|
96
|
+
export async function seedAugmentHomeFromRealHome({ realHomeDir, isolatedHomeDir } = {}) {
|
|
97
|
+
const real = String(realHomeDir ?? '').trim();
|
|
98
|
+
const isolated = String(isolatedHomeDir ?? '').trim();
|
|
99
|
+
if (!real || !isolated || real === isolated) return;
|
|
100
|
+
|
|
101
|
+
// Copy ~/.augment/* into the isolated cache dir.
|
|
102
|
+
await mergeCopyDir({ srcDir: join(real, '.augment'), destDir: isolated });
|
|
103
|
+
|
|
104
|
+
// Refresh session.json when it is newer.
|
|
105
|
+
await copyFileIfNewer({ srcFile: join(real, '.augment', 'session.json'), destFile: join(isolated, 'session.json') });
|
|
106
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, stat, utimes, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { seedAugmentHomeFromRealHome, seedCodeRabbitHomeFromRealHome, seedCodexHomeFromRealHome } from './tool_home_seed.mjs';
|
|
7
|
+
|
|
8
|
+
test('seedCodeRabbitHomeFromRealHome copies coderabbit state into isolated home when missing', async () => {
|
|
9
|
+
const root = await mkdtemp(join(tmpdir(), 'happy-stacks-coderabbit-seed-'));
|
|
10
|
+
const realHome = join(root, 'real');
|
|
11
|
+
const isolatedHome = join(root, 'isolated');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(join(realHome, '.coderabbit'), { recursive: true });
|
|
15
|
+
await mkdir(join(realHome, '.config', 'coderabbit'), { recursive: true });
|
|
16
|
+
await writeFile(join(realHome, '.coderabbit', 'auth.json'), 'secret-ish\n', 'utf-8');
|
|
17
|
+
await writeFile(join(realHome, '.config', 'coderabbit', 'config.toml'), 'cfg\n', 'utf-8');
|
|
18
|
+
|
|
19
|
+
await mkdir(isolatedHome, { recursive: true });
|
|
20
|
+
|
|
21
|
+
await seedCodeRabbitHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: isolatedHome });
|
|
22
|
+
|
|
23
|
+
assert.equal(await readFile(join(isolatedHome, '.coderabbit', 'auth.json'), 'utf-8'), 'secret-ish\n');
|
|
24
|
+
assert.equal(await readFile(join(isolatedHome, '.config', 'coderabbit', 'config.toml'), 'utf-8'), 'cfg\n');
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(root, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('seedCodeRabbitHomeFromRealHome does not overwrite existing isolated state', async () => {
|
|
31
|
+
const root = await mkdtemp(join(tmpdir(), 'happy-stacks-coderabbit-seed-'));
|
|
32
|
+
const realHome = join(root, 'real');
|
|
33
|
+
const isolatedHome = join(root, 'isolated');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await mkdir(join(realHome, '.coderabbit'), { recursive: true });
|
|
37
|
+
await writeFile(join(realHome, '.coderabbit', 'auth.json'), 'from-real\n', 'utf-8');
|
|
38
|
+
|
|
39
|
+
await mkdir(join(isolatedHome, '.coderabbit'), { recursive: true });
|
|
40
|
+
await writeFile(join(isolatedHome, '.coderabbit', 'auth.json'), 'already\n', 'utf-8');
|
|
41
|
+
|
|
42
|
+
await seedCodeRabbitHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: isolatedHome });
|
|
43
|
+
|
|
44
|
+
assert.equal(await readFile(join(isolatedHome, '.coderabbit', 'auth.json'), 'utf-8'), 'already\n');
|
|
45
|
+
} finally {
|
|
46
|
+
await rm(root, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('seedCodeRabbitHomeFromRealHome refreshes auth.json when the real home has a newer one', async () => {
|
|
51
|
+
const root = await mkdtemp(join(tmpdir(), 'happy-stacks-coderabbit-seed-'));
|
|
52
|
+
const realHome = join(root, 'real');
|
|
53
|
+
const isolatedHome = join(root, 'isolated');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await mkdir(join(realHome, '.coderabbit'), { recursive: true });
|
|
57
|
+
await mkdir(join(isolatedHome, '.coderabbit'), { recursive: true });
|
|
58
|
+
|
|
59
|
+
const isolatedAuthPath = join(isolatedHome, '.coderabbit', 'auth.json');
|
|
60
|
+
const realAuthPath = join(realHome, '.coderabbit', 'auth.json');
|
|
61
|
+
await writeFile(isolatedAuthPath, 'old\n', 'utf-8');
|
|
62
|
+
await writeFile(realAuthPath, 'new\n', 'utf-8');
|
|
63
|
+
await utimes(isolatedAuthPath, new Date(1_000), new Date(1_000));
|
|
64
|
+
await utimes(realAuthPath, new Date(2_000), new Date(2_000));
|
|
65
|
+
|
|
66
|
+
await seedCodeRabbitHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: isolatedHome });
|
|
67
|
+
|
|
68
|
+
assert.equal(await readFile(isolatedAuthPath, 'utf-8'), 'new\n');
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('seedCodexHomeFromRealHome refreshes auth and does not copy user config.toml', async () => {
|
|
75
|
+
const root = await mkdtemp(join(tmpdir(), 'happy-stacks-codex-seed-'));
|
|
76
|
+
const realHome = join(root, 'real');
|
|
77
|
+
const isolatedHome = join(root, 'isolated');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await mkdir(join(realHome, '.codex'), { recursive: true });
|
|
81
|
+
await mkdir(isolatedHome, { recursive: true });
|
|
82
|
+
|
|
83
|
+
const isolatedAuthPath = join(isolatedHome, 'auth.json');
|
|
84
|
+
const realAuthPath = join(realHome, '.codex', 'auth.json');
|
|
85
|
+
|
|
86
|
+
await writeFile(isolatedAuthPath, 'old-auth\n', 'utf-8');
|
|
87
|
+
await writeFile(join(isolatedHome, 'config.toml'), 'old-cfg\n', 'utf-8');
|
|
88
|
+
await writeFile(realAuthPath, 'new-auth\n', 'utf-8');
|
|
89
|
+
await writeFile(join(realHome, '.codex', 'config.toml'), 'new-cfg\n', 'utf-8');
|
|
90
|
+
await utimes(isolatedAuthPath, new Date(1_000), new Date(1_000));
|
|
91
|
+
await utimes(realAuthPath, new Date(2_000), new Date(2_000));
|
|
92
|
+
|
|
93
|
+
await seedCodexHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: isolatedHome });
|
|
94
|
+
|
|
95
|
+
assert.equal(await readFile(isolatedAuthPath, 'utf-8'), 'new-auth\n');
|
|
96
|
+
let hasConfig = true;
|
|
97
|
+
try {
|
|
98
|
+
await stat(join(isolatedHome, 'config.toml'));
|
|
99
|
+
} catch {
|
|
100
|
+
hasConfig = false;
|
|
101
|
+
}
|
|
102
|
+
assert.equal(hasConfig, false);
|
|
103
|
+
} finally {
|
|
104
|
+
await rm(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('seedAugmentHomeFromRealHome copies session.json into isolated cache dir', async () => {
|
|
109
|
+
const root = await mkdtemp(join(tmpdir(), 'happy-stacks-augment-seed-'));
|
|
110
|
+
const realHome = join(root, 'real');
|
|
111
|
+
const isolatedHome = join(root, 'isolated');
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await mkdir(join(realHome, '.augment'), { recursive: true });
|
|
115
|
+
await writeFile(join(realHome, '.augment', 'session.json'), '{"ok":true}\n', 'utf-8');
|
|
116
|
+
await mkdir(isolatedHome, { recursive: true });
|
|
117
|
+
|
|
118
|
+
await seedAugmentHomeFromRealHome({ realHomeDir: realHome, isolatedHomeDir: isolatedHome });
|
|
119
|
+
|
|
120
|
+
assert.equal(await readFile(join(isolatedHome, 'session.json'), 'utf-8'), '{"ok":true}\n');
|
|
121
|
+
} finally {
|
|
122
|
+
await rm(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { runCapture } from '../proc/proc.mjs';
|
|
2
|
+
import { normalize } from 'node:path';
|
|
3
|
+
import { parseNameStatusZ } from '../git/parse_name_status_z.mjs';
|
|
4
|
+
|
|
5
|
+
function normalizePath(p) {
|
|
6
|
+
return String(p ?? '').replace(/\\/g, '/');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parsePathsZ(buf) {
|
|
10
|
+
const raw = String(buf ?? '');
|
|
11
|
+
if (!raw) return [];
|
|
12
|
+
return raw.split('\0').filter((x) => x.length);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertSafeRelativeRepoPath(rel) {
|
|
16
|
+
const raw = String(rel ?? '').trim();
|
|
17
|
+
if (!raw) {
|
|
18
|
+
throw new Error('[review] unsafe path: empty path');
|
|
19
|
+
}
|
|
20
|
+
if (raw.includes('\0')) {
|
|
21
|
+
throw new Error(`[review] unsafe path: ${raw}`);
|
|
22
|
+
}
|
|
23
|
+
if (/^[a-zA-Z]:[\\/]/.test(raw) || raw.startsWith('\\\\') || raw.startsWith('/') || raw.startsWith('\\')) {
|
|
24
|
+
throw new Error(`[review] unsafe path (absolute): ${raw}`);
|
|
25
|
+
}
|
|
26
|
+
const normalized = normalize(raw).replaceAll('\\', '/');
|
|
27
|
+
if (
|
|
28
|
+
normalized === '..' ||
|
|
29
|
+
normalized.startsWith('../') ||
|
|
30
|
+
normalized.startsWith('/'))
|
|
31
|
+
{
|
|
32
|
+
throw new Error(`[review] unsafe path traversal: ${raw}`);
|
|
33
|
+
}
|
|
34
|
+
return normalized.replace(/^\.\/+/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getUncommittedOps({ cwd, env = process.env } = {}) {
|
|
38
|
+
const checkout = new Set();
|
|
39
|
+
const remove = new Set();
|
|
40
|
+
|
|
41
|
+
let entries = [];
|
|
42
|
+
try {
|
|
43
|
+
const out = await runCapture('git', ['diff', '--name-status', '--find-renames', '-z', 'HEAD'], { cwd, env });
|
|
44
|
+
entries = parseNameStatusZ(out);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const details = `${String(error?.message ?? '')}\n${String(error?.err ?? '')}`.toLowerCase();
|
|
47
|
+
const isMissingHead =
|
|
48
|
+
details.includes("bad revision 'head'") ||
|
|
49
|
+
details.includes("ambiguous argument 'head'") ||
|
|
50
|
+
details.includes("unknown revision or path not in the working tree");
|
|
51
|
+
if (!isMissingHead) throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
if (e.code === 'A' || e.code === 'M' || e.code === 'T') {
|
|
56
|
+
checkout.add(assertSafeRelativeRepoPath(normalizePath(e.path)));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (e.code === 'D') {
|
|
60
|
+
remove.add(assertSafeRelativeRepoPath(normalizePath(e.path)));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (e.code === 'R' || e.code === 'C') {
|
|
64
|
+
if (e.code === 'R' && e.from) remove.add(assertSafeRelativeRepoPath(normalizePath(e.from)));
|
|
65
|
+
if (e.to) checkout.add(assertSafeRelativeRepoPath(normalizePath(e.to)));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const untrackedOut = await runCapture('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd, env });
|
|
71
|
+
for (const p of parsePathsZ(untrackedOut)) {
|
|
72
|
+
checkout.add(assertSafeRelativeRepoPath(normalizePath(p)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const all = new Set([...checkout, ...remove]);
|
|
76
|
+
return { checkout, remove, all };
|
|
77
|
+
}
|