@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,85 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { lstat, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
detectSwiftbarPluginInstalled,
|
|
9
|
+
removeSwiftbarPlugins,
|
|
10
|
+
resolveSwiftbarPluginsDir,
|
|
11
|
+
} from './swiftbar.mjs';
|
|
12
|
+
|
|
13
|
+
test('detectSwiftbarPluginInstalled treats matching symlinks as installed', async () => {
|
|
14
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-swiftbar-'));
|
|
15
|
+
try {
|
|
16
|
+
const pluginsDir = join(tmp, 'plugins');
|
|
17
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const target = join(tmp, 'target.sh');
|
|
20
|
+
const link = join(pluginsDir, 'hstack.5m.sh');
|
|
21
|
+
await writeFile(target, '#!/bin/sh\necho ok\n', 'utf-8');
|
|
22
|
+
await symlink(target, link);
|
|
23
|
+
|
|
24
|
+
const detected = await detectSwiftbarPluginInstalled({
|
|
25
|
+
pluginsDir,
|
|
26
|
+
patterns: ['hstack.*.sh'],
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
assert.equal(detected.installed, true);
|
|
30
|
+
} finally {
|
|
31
|
+
await rm(tmp, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('removeSwiftbarPlugins removes matching symlink plugins', async () => {
|
|
36
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-swiftbar-'));
|
|
37
|
+
try {
|
|
38
|
+
const pluginsDir = join(tmp, 'plugins');
|
|
39
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const target = join(tmp, 'target.sh');
|
|
42
|
+
const link = join(pluginsDir, 'hstack.5m.sh');
|
|
43
|
+
await writeFile(target, '#!/bin/sh\necho ok\n', 'utf-8');
|
|
44
|
+
await symlink(target, link);
|
|
45
|
+
|
|
46
|
+
const res = await removeSwiftbarPlugins({
|
|
47
|
+
pluginsDir,
|
|
48
|
+
patterns: ['hstack.*.sh'],
|
|
49
|
+
env: process.env,
|
|
50
|
+
});
|
|
51
|
+
assert.equal(res.ok, true);
|
|
52
|
+
assert.equal(res.removed, true);
|
|
53
|
+
await assert.rejects(() => lstat(link), /ENOENT/i);
|
|
54
|
+
const detected = await detectSwiftbarPluginInstalled({
|
|
55
|
+
pluginsDir,
|
|
56
|
+
patterns: ['hstack.*.sh'],
|
|
57
|
+
env: process.env,
|
|
58
|
+
});
|
|
59
|
+
assert.equal(detected.installed, false);
|
|
60
|
+
} finally {
|
|
61
|
+
await rm(tmp, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('resolveSwiftbarPluginsDir ignores override on non-darwin unless explicitly enabled', async () => {
|
|
66
|
+
const tmp = await mkdtemp(join(tmpdir(), 'hstack-swiftbar-'));
|
|
67
|
+
try {
|
|
68
|
+
const pluginsDir = join(tmp, 'plugins');
|
|
69
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const env = {
|
|
72
|
+
...process.env,
|
|
73
|
+
HAPPIER_STACK_SWIFTBAR_PLUGINS_DIR: pluginsDir,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const resolved = await resolveSwiftbarPluginsDir({ env });
|
|
77
|
+
if (process.platform === 'darwin') {
|
|
78
|
+
assert.equal(resolved, pluginsDir);
|
|
79
|
+
} else {
|
|
80
|
+
assert.equal(resolved, null);
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(tmp, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { sanitizeBundleIdSegment, sanitizeUrlScheme } from './identifiers.mjs';
|
|
2
|
+
|
|
3
|
+
export function resolveMobileExpoConfig({ env = process.env } = {}) {
|
|
4
|
+
const user = sanitizeBundleIdSegment(env.USER ?? env.USERNAME ?? 'user');
|
|
5
|
+
const defaultLocalBundleId = `com.happier.local.${user}.dev`;
|
|
6
|
+
|
|
7
|
+
const appEnv = env.APP_ENV ?? env.HAPPIER_STACK_APP_ENV ?? 'development';
|
|
8
|
+
// Prefer stack-scoped config, but also support generic Expo build env vars so users can
|
|
9
|
+
// drive mobile identity purely via stack env files without learning hstack-specific keys.
|
|
10
|
+
const iosAppName = (env.HAPPIER_STACK_IOS_APP_NAME ?? env.EXPO_APP_NAME ?? '').toString();
|
|
11
|
+
const iosBundleId = (
|
|
12
|
+
env.HAPPIER_STACK_IOS_BUNDLE_ID ??
|
|
13
|
+
env.EXPO_APP_BUNDLE_ID ??
|
|
14
|
+
defaultLocalBundleId
|
|
15
|
+
).toString();
|
|
16
|
+
// hstack convention:
|
|
17
|
+
// - dev-client QR should open a dedicated "hstack Dev" app (not a per-stack release build)
|
|
18
|
+
// - so default to a stable happy-stacks-specific scheme unless explicitly overridden.
|
|
19
|
+
const scheme = sanitizeUrlScheme(
|
|
20
|
+
(env.HAPPIER_STACK_MOBILE_SCHEME ??
|
|
21
|
+
env.HAPPIER_STACK_DEV_CLIENT_SCHEME ??
|
|
22
|
+
env.EXPO_APP_SCHEME ??
|
|
23
|
+
'hstack-dev')
|
|
24
|
+
.toString()
|
|
25
|
+
);
|
|
26
|
+
const host = (env.HAPPIER_STACK_MOBILE_HOST ?? 'lan').toString();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
appEnv,
|
|
30
|
+
iosAppName,
|
|
31
|
+
iosBundleId,
|
|
32
|
+
scheme,
|
|
33
|
+
host,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getEnvValueAny } from '../env/values.mjs';
|
|
2
|
+
import { pickLanIpv4 } from '../net/lan_ip.mjs';
|
|
3
|
+
import { resolveMobileExpoConfig } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
function normalizeHostMode(raw) {
|
|
6
|
+
const v = String(raw ?? '').trim().toLowerCase();
|
|
7
|
+
if (v === 'localhost' || v === 'local') return 'localhost';
|
|
8
|
+
if (v === 'lan' || v === 'ip') return 'lan';
|
|
9
|
+
if (v === 'tunnel') return 'tunnel';
|
|
10
|
+
return v || 'lan';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveMobileHostMode(env = process.env) {
|
|
14
|
+
// Prefer explicit host vars (so TUI/setup-pr match the same knobs Expo uses).
|
|
15
|
+
const raw =
|
|
16
|
+
getEnvValueAny(env, ['HAPPIER_STACK_MOBILE_HOST']) ||
|
|
17
|
+
resolveMobileExpoConfig({ env }).host ||
|
|
18
|
+
'lan';
|
|
19
|
+
return normalizeHostMode(raw);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveMobileScheme(env = process.env) {
|
|
23
|
+
return String(resolveMobileExpoConfig({ env }).scheme || '').trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveMetroUrlForMobile({ env = process.env, port }) {
|
|
27
|
+
const p = Number(port);
|
|
28
|
+
if (!Number.isFinite(p) || p <= 0) return '';
|
|
29
|
+
|
|
30
|
+
const mode = resolveMobileHostMode(env);
|
|
31
|
+
if (mode === 'localhost') {
|
|
32
|
+
return `http://localhost:${p}`;
|
|
33
|
+
}
|
|
34
|
+
if (mode === 'lan') {
|
|
35
|
+
const ip = pickLanIpv4();
|
|
36
|
+
return `http://${ip || 'localhost'}:${p}`;
|
|
37
|
+
}
|
|
38
|
+
// Tunnel URLs are controlled by Expo; we can't reliably derive them locally.
|
|
39
|
+
// Fall back to localhost so the URL is at least correct for the host machine.
|
|
40
|
+
return `http://localhost:${p}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveDevClientDeepLink({ scheme, metroUrl }) {
|
|
44
|
+
const s = String(scheme ?? '').trim();
|
|
45
|
+
const url = String(metroUrl ?? '').trim();
|
|
46
|
+
if (!url) return '';
|
|
47
|
+
if (!s) return url;
|
|
48
|
+
return `${s}://expo-development-client/?url=${encodeURIComponent(url)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveMobileQrPayload({ env = process.env, port }) {
|
|
52
|
+
const metroUrl = resolveMetroUrlForMobile({ env, port });
|
|
53
|
+
const scheme = resolveMobileScheme(env);
|
|
54
|
+
const deepLink = resolveDevClientDeepLink({ scheme, metroUrl });
|
|
55
|
+
// Match Expo CLI / @expo/cli UrlCreator: QR encodes the dev-client deep link.
|
|
56
|
+
// Note: iOS Camera will still offer to open custom schemes when the app is installed.
|
|
57
|
+
const payload = deepLink || metroUrl;
|
|
58
|
+
return { scheme, metroUrl, deepLink, payload };
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function sanitizeToken(raw, { allowDots = false } = {}) {
|
|
2
|
+
const s = (raw ?? '').toString().trim().toLowerCase();
|
|
3
|
+
const re = allowDots ? /[^a-z0-9.-]+/g : /[^a-z0-9-]+/g;
|
|
4
|
+
const out = s.replace(re, '-').replace(/^-+|-+$/g, '').replace(/-+/g, '-');
|
|
5
|
+
return out;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function sanitizeBundleIdSegment(s) {
|
|
9
|
+
const seg = sanitizeToken(s, { allowDots: false });
|
|
10
|
+
if (!seg) return 'app';
|
|
11
|
+
// Bundle id segments should not start with a digit; prefix if needed.
|
|
12
|
+
return /^[a-z]/.test(seg) ? seg : `s${seg}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sanitizeUrlScheme(s) {
|
|
16
|
+
// iOS URL schemes must start with a letter and may contain letters/digits/+.-.
|
|
17
|
+
const raw = (s ?? '').toString().trim().toLowerCase();
|
|
18
|
+
const cleaned = raw.replace(/[^a-z0-9+.-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
|
|
19
|
+
if (!cleaned) return 'happier-dev';
|
|
20
|
+
return /^[a-z]/.test(cleaned) ? cleaned : `h${cleaned}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stackSlugForMobileIds(stackName) {
|
|
24
|
+
const raw = (stackName ?? '').toString().trim();
|
|
25
|
+
return sanitizeBundleIdSegment(raw || 'stack');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defaultDevClientIdentity({ user = null } = {}) {
|
|
29
|
+
const u = sanitizeBundleIdSegment(user ?? 'user');
|
|
30
|
+
return {
|
|
31
|
+
iosAppName: 'Happier Dev',
|
|
32
|
+
iosBundleId: `dev.happier.stack.dev.${u}`,
|
|
33
|
+
scheme: 'happier-dev',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function defaultStackReleaseIdentity({ stackName, user = null, appName = null } = {}) {
|
|
38
|
+
const slug = stackSlugForMobileIds(stackName);
|
|
39
|
+
const u = sanitizeBundleIdSegment(user ?? 'user');
|
|
40
|
+
const label = (appName ?? '').toString().trim();
|
|
41
|
+
return {
|
|
42
|
+
iosAppName: label || `Happier (${stackName})`,
|
|
43
|
+
iosBundleId: `dev.happier.stack.stack.${u}.${slug}`,
|
|
44
|
+
scheme: sanitizeUrlScheme(`happier-${slug}`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
defaultDevClientIdentity,
|
|
6
|
+
defaultStackReleaseIdentity,
|
|
7
|
+
sanitizeBundleIdSegment,
|
|
8
|
+
sanitizeUrlScheme,
|
|
9
|
+
stackSlugForMobileIds,
|
|
10
|
+
} from './identifiers.mjs';
|
|
11
|
+
|
|
12
|
+
test('sanitizeBundleIdSegment produces a safe segment', () => {
|
|
13
|
+
assert.equal(sanitizeBundleIdSegment(' PR272-107 '), 'pr272-107');
|
|
14
|
+
assert.equal(sanitizeBundleIdSegment('---'), 'app');
|
|
15
|
+
assert.equal(sanitizeBundleIdSegment('123'), 's123');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('sanitizeUrlScheme produces a safe scheme', () => {
|
|
19
|
+
assert.equal(sanitizeUrlScheme('Happier-Dev'), 'happier-dev');
|
|
20
|
+
assert.equal(sanitizeUrlScheme('123bad'), 'h123bad');
|
|
21
|
+
assert.equal(sanitizeUrlScheme(''), 'happier-dev');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('stackSlugForMobileIds derives a stable slug', () => {
|
|
25
|
+
assert.equal(stackSlugForMobileIds('pr272-107-fixes-2026-01-15'), 'pr272-107-fixes-2026-01-15');
|
|
26
|
+
assert.equal(stackSlugForMobileIds(' Weird Name '), 'weird-name');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('defaultDevClientIdentity is stable and safe', () => {
|
|
30
|
+
const id = defaultDevClientIdentity({ user: 'Leeroy' });
|
|
31
|
+
assert.equal(id.iosAppName, 'Happier Dev');
|
|
32
|
+
assert.equal(id.scheme, 'happier-dev');
|
|
33
|
+
assert.equal(id.iosBundleId, 'dev.happier.stack.dev.leeroy');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('defaultStackReleaseIdentity is per-stack', () => {
|
|
37
|
+
const id = defaultStackReleaseIdentity({ stackName: 'pr272-107', user: 'Leeroy' });
|
|
38
|
+
assert.equal(id.iosBundleId, 'dev.happier.stack.stack.leeroy.pr272-107');
|
|
39
|
+
assert.equal(id.scheme, 'happier-pr272-107');
|
|
40
|
+
assert.equal(id.iosAppName, 'Happier (pr272-107)');
|
|
41
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { pathExists } from '../fs/fs.mjs';
|
|
5
|
+
|
|
6
|
+
function sanitizeXcodeProductName(name) {
|
|
7
|
+
const raw = (name ?? '').toString().trim();
|
|
8
|
+
const out = raw
|
|
9
|
+
.replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
10
|
+
.replace(/-+/g, '-')
|
|
11
|
+
.replace(/^[-_]+|[-_]+$/g, '');
|
|
12
|
+
return out || 'Happy';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function listIosAppXcodeprojNames({ iosDir }) {
|
|
16
|
+
let entries = [];
|
|
17
|
+
try {
|
|
18
|
+
entries = await readdir(iosDir, { withFileTypes: true });
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const names = entries
|
|
24
|
+
.filter((e) => e.isDirectory() && e.name.endsWith('.xcodeproj') && e.name.startsWith('Happy'))
|
|
25
|
+
.map((e) => e.name);
|
|
26
|
+
|
|
27
|
+
// Prefer the common names first to keep behavior stable if multiple projects exist.
|
|
28
|
+
const score = (name) => {
|
|
29
|
+
if (name === 'Happydev.xcodeproj') return 0;
|
|
30
|
+
if (name === 'Happy.xcodeproj') return 1;
|
|
31
|
+
return 2;
|
|
32
|
+
};
|
|
33
|
+
names.sort((a, b) => score(a) - score(b) || a.localeCompare(b));
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resolveIosAppXcodeProjects({ uiDir }) {
|
|
38
|
+
const iosDir = join(uiDir, 'ios');
|
|
39
|
+
const projectNames = await listIosAppXcodeprojNames({ iosDir });
|
|
40
|
+
|
|
41
|
+
const projects = [];
|
|
42
|
+
for (const projectName of projectNames) {
|
|
43
|
+
const pbxprojPath = join(iosDir, projectName, 'project.pbxproj');
|
|
44
|
+
if (!(await pathExists(pbxprojPath))) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const appDirName = projectName.replace(/\.xcodeproj$/, '');
|
|
49
|
+
const infoPlistPath = join(iosDir, appDirName, 'Info.plist');
|
|
50
|
+
|
|
51
|
+
projects.push({
|
|
52
|
+
name: appDirName,
|
|
53
|
+
pbxprojPath,
|
|
54
|
+
infoPlistPath: (await pathExists(infoPlistPath)) ? infoPlistPath : null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return projects;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function patchIosXcodeProjectsForSigningAndIdentity({
|
|
62
|
+
uiDir,
|
|
63
|
+
iosBundleId,
|
|
64
|
+
iosAppName = '',
|
|
65
|
+
} = {}) {
|
|
66
|
+
const bundleId = (iosBundleId ?? '').toString().trim();
|
|
67
|
+
const appName = (iosAppName ?? '').toString().trim();
|
|
68
|
+
const productName = sanitizeXcodeProductName(appName);
|
|
69
|
+
|
|
70
|
+
if (!uiDir || !bundleId) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const projects = await resolveIosAppXcodeProjects({ uiDir });
|
|
75
|
+
if (projects.length === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const project of projects) {
|
|
80
|
+
// Patch pbxproj: clear pinned signing fields so Expo can reconfigure and include provisioning update flags,
|
|
81
|
+
// and force a per-stack bundle id + optional PRODUCT_NAME.
|
|
82
|
+
try {
|
|
83
|
+
const raw = await readFile(project.pbxprojPath, 'utf-8');
|
|
84
|
+
let next = raw;
|
|
85
|
+
|
|
86
|
+
// Clear team identifiers (both TargetAttributes and build settings variants).
|
|
87
|
+
next = next.replaceAll(/^\s*DevelopmentTeam\s*=\s*[^;]+;\s*$/gm, '');
|
|
88
|
+
next = next.replaceAll(/^\s*DEVELOPMENT_TEAM\s*=\s*[^;]+;\s*$/gm, '');
|
|
89
|
+
// Clear any pinned provisioning profiles/specifiers (manual signing).
|
|
90
|
+
next = next.replaceAll(/^\s*PROVISIONING_PROFILE\s*=\s*[^;]+;\s*$/gm, '');
|
|
91
|
+
next = next.replaceAll(/^\s*PROVISIONING_PROFILE_SPECIFIER\s*=\s*[^;]+;\s*$/gm, '');
|
|
92
|
+
// Some projects pin code signing identity; remove to let Xcode resolve based on the selected team.
|
|
93
|
+
next = next.replaceAll(/^\s*CODE_SIGN_IDENTITY\s*=\s*[^;]+;\s*$/gm, '');
|
|
94
|
+
next = next.replaceAll(/^\s*"CODE_SIGN_IDENTITY\\[sdk=iphoneos\\*\\]"\s*=\s*[^;]+;\s*$/gm, '');
|
|
95
|
+
|
|
96
|
+
next = next.replaceAll(/PRODUCT_BUNDLE_IDENTIFIER = [^;]+;/g, `PRODUCT_BUNDLE_IDENTIFIER = ${bundleId};`);
|
|
97
|
+
|
|
98
|
+
if (appName) {
|
|
99
|
+
// Expo CLI appears to treat some escaped build paths as literal (e.g. "Happy\\ (stack).app"),
|
|
100
|
+
// so keep PRODUCT_NAME free of spaces to avoid breaking post-build Info.plist parsing.
|
|
101
|
+
next = next.replaceAll(/PRODUCT_NAME = [^;]+;/g, `PRODUCT_NAME = ${productName};`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (next !== raw) {
|
|
105
|
+
await writeFile(project.pbxprojPath, next, 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore project patch errors; Expo will surface actionable failures if needed
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Patch Info.plist display name when possible (home screen label).
|
|
112
|
+
if (appName && project.infoPlistPath) {
|
|
113
|
+
try {
|
|
114
|
+
const plistRaw = await readFile(project.infoPlistPath, 'utf-8');
|
|
115
|
+
const escaped = appName.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
116
|
+
const replaced = plistRaw.replace(
|
|
117
|
+
/(<key>CFBundleDisplayName<\/key>\s*<string>)([\s\S]*?)(<\/string>)/m,
|
|
118
|
+
`$1${escaped}$3`
|
|
119
|
+
);
|
|
120
|
+
if (replaced !== plistRaw) {
|
|
121
|
+
await writeFile(project.infoPlistPath, replaced, 'utf-8');
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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 { patchIosXcodeProjectsForSigningAndIdentity } from './ios_xcodeproj_patch.mjs';
|
|
8
|
+
|
|
9
|
+
async function withTempUiDir(t) {
|
|
10
|
+
const uiDir = await mkdtemp(join(tmpdir(), 'hstack-mobile-'));
|
|
11
|
+
t.after(async () => {
|
|
12
|
+
await rm(uiDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
return uiDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('patchIosXcodeProjectsForSigningAndIdentity patches legacy ios/Happy.xcodeproj + ios/Happy/Info.plist', async (t) => {
|
|
18
|
+
const uiDir = await withTempUiDir(t);
|
|
19
|
+
const iosDir = join(uiDir, 'ios');
|
|
20
|
+
await mkdir(join(iosDir, 'Happy.xcodeproj'), { recursive: true });
|
|
21
|
+
await mkdir(join(iosDir, 'Happy'), { recursive: true });
|
|
22
|
+
|
|
23
|
+
const pbxprojPath = join(iosDir, 'Happy.xcodeproj', 'project.pbxproj');
|
|
24
|
+
await writeFile(
|
|
25
|
+
pbxprojPath,
|
|
26
|
+
[
|
|
27
|
+
'ProvisioningStyle = Automatic;',
|
|
28
|
+
'DEVELOPMENT_TEAM = 3RSYVV66F6;',
|
|
29
|
+
'CODE_SIGN_IDENTITY = "Apple Development";',
|
|
30
|
+
'"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";',
|
|
31
|
+
'PROVISIONING_PROFILE_SPECIFIER = some-profile;',
|
|
32
|
+
'PRODUCT_BUNDLE_IDENTIFIER = dev.happier.app;',
|
|
33
|
+
'PRODUCT_NAME = Happy;',
|
|
34
|
+
'',
|
|
35
|
+
].join('\n'),
|
|
36
|
+
'utf-8'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const infoPlistPath = join(iosDir, 'Happy', 'Info.plist');
|
|
40
|
+
await writeFile(
|
|
41
|
+
infoPlistPath,
|
|
42
|
+
[
|
|
43
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
44
|
+
'<plist version="1.0"><dict>',
|
|
45
|
+
'<key>CFBundleDisplayName</key><string>Happy</string>',
|
|
46
|
+
'</dict></plist>',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
'utf-8'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await patchIosXcodeProjectsForSigningAndIdentity({
|
|
53
|
+
uiDir,
|
|
54
|
+
iosBundleId: 'dev.happier.stack.stack.user.pre-pr272',
|
|
55
|
+
iosAppName: 'HAPPY LEGACY',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const pbxproj = await readFile(pbxprojPath, 'utf-8');
|
|
59
|
+
assert.match(pbxproj, /PRODUCT_BUNDLE_IDENTIFIER = dev\.happier\.stack\.stack\.user\.pre-pr272;/);
|
|
60
|
+
assert.doesNotMatch(pbxproj, /DEVELOPMENT_TEAM\s*=/);
|
|
61
|
+
assert.doesNotMatch(pbxproj, /PROVISIONING_PROFILE_SPECIFIER\s*=/);
|
|
62
|
+
assert.doesNotMatch(pbxproj, /CODE_SIGN_IDENTITY\s*=/);
|
|
63
|
+
assert.match(pbxproj, /PRODUCT_NAME = HAPPY-LEGACY;/);
|
|
64
|
+
|
|
65
|
+
const plist = await readFile(infoPlistPath, 'utf-8');
|
|
66
|
+
assert.match(plist, /<key>CFBundleDisplayName<\/key><string>HAPPY LEGACY<\/string>/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('patchIosXcodeProjectsForSigningAndIdentity patches both Happydev + Happy projects when present', async (t) => {
|
|
70
|
+
const uiDir = await withTempUiDir(t);
|
|
71
|
+
const iosDir = join(uiDir, 'ios');
|
|
72
|
+
|
|
73
|
+
await mkdir(join(iosDir, 'Happy.xcodeproj'), { recursive: true });
|
|
74
|
+
await mkdir(join(iosDir, 'Happy'), { recursive: true });
|
|
75
|
+
await writeFile(join(iosDir, 'Happy.xcodeproj', 'project.pbxproj'), 'PRODUCT_BUNDLE_IDENTIFIER = dev.happier.app;\n', 'utf-8');
|
|
76
|
+
await writeFile(join(iosDir, 'Happy', 'Info.plist'), '<key>CFBundleDisplayName</key><string>Happy</string>\n', 'utf-8');
|
|
77
|
+
|
|
78
|
+
await mkdir(join(iosDir, 'Happydev.xcodeproj'), { recursive: true });
|
|
79
|
+
await mkdir(join(iosDir, 'Happydev'), { recursive: true });
|
|
80
|
+
await writeFile(join(iosDir, 'Happydev.xcodeproj', 'project.pbxproj'), 'PRODUCT_BUNDLE_IDENTIFIER = dev.happier.app.dev;\n', 'utf-8');
|
|
81
|
+
await writeFile(join(iosDir, 'Happydev', 'Info.plist'), '<key>CFBundleDisplayName</key><string>Happy (dev)</string>\n', 'utf-8');
|
|
82
|
+
|
|
83
|
+
await patchIosXcodeProjectsForSigningAndIdentity({
|
|
84
|
+
uiDir,
|
|
85
|
+
iosBundleId: 'dev.happier.stack.stack.user.pre-pr272',
|
|
86
|
+
iosAppName: 'HAPPY LEGACY',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const pbxprojRelease = await readFile(join(iosDir, 'Happy.xcodeproj', 'project.pbxproj'), 'utf-8');
|
|
90
|
+
assert.match(pbxprojRelease, /PRODUCT_BUNDLE_IDENTIFIER = dev\.happier\.stack\.stack\.user\.pre-pr272;/);
|
|
91
|
+
|
|
92
|
+
const pbxprojDev = await readFile(join(iosDir, 'Happydev.xcodeproj', 'project.pbxproj'), 'utf-8');
|
|
93
|
+
assert.match(pbxprojDev, /PRODUCT_BUNDLE_IDENTIFIER = dev\.happier\.stack\.stack\.user\.pre-pr272;/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('patchIosXcodeProjectsForSigningAndIdentity tolerates missing Info.plist and leaves pre-patched pbxproj unchanged', async (t) => {
|
|
97
|
+
const uiDir = await withTempUiDir(t);
|
|
98
|
+
const iosDir = join(uiDir, 'ios');
|
|
99
|
+
await mkdir(join(iosDir, 'Happy.xcodeproj'), { recursive: true });
|
|
100
|
+
const pbxprojPath = join(iosDir, 'Happy.xcodeproj', 'project.pbxproj');
|
|
101
|
+
const prepatched = [
|
|
102
|
+
'PRODUCT_BUNDLE_IDENTIFIER = dev.happier.stack.already.patched;',
|
|
103
|
+
'PRODUCT_NAME = HAPPY-LEGACY;',
|
|
104
|
+
'',
|
|
105
|
+
].join('\n');
|
|
106
|
+
await writeFile(pbxprojPath, prepatched, 'utf-8');
|
|
107
|
+
|
|
108
|
+
await patchIosXcodeProjectsForSigningAndIdentity({
|
|
109
|
+
uiDir,
|
|
110
|
+
iosBundleId: 'dev.happier.stack.already.patched',
|
|
111
|
+
iosAppName: 'HAPPY LEGACY',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const next = await readFile(pbxprojPath, 'utf-8');
|
|
115
|
+
assert.equal(next, prepatched);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('patchIosXcodeProjectsForSigningAndIdentity is a no-op when no Happy*.xcodeproj exists', async (t) => {
|
|
119
|
+
const uiDir = await withTempUiDir(t);
|
|
120
|
+
await mkdir(join(uiDir, 'ios', 'Other.xcodeproj'), { recursive: true });
|
|
121
|
+
await writeFile(join(uiDir, 'ios', 'Other.xcodeproj', 'project.pbxproj'), 'PRODUCT_BUNDLE_IDENTIFIER = keep.me;\n', 'utf-8');
|
|
122
|
+
|
|
123
|
+
await patchIosXcodeProjectsForSigningAndIdentity({
|
|
124
|
+
uiDir,
|
|
125
|
+
iosBundleId: 'dev.happier.stack.noop',
|
|
126
|
+
iosAppName: 'NOOP',
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const next = await readFile(join(uiDir, 'ios', 'Other.xcodeproj', 'project.pbxproj'), 'utf-8');
|
|
130
|
+
assert.equal(next, 'PRODUCT_BUNDLE_IDENTIFIER = keep.me;\n');
|
|
131
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function normalizeBindMode(raw) {
|
|
2
|
+
const v = String(raw ?? '').trim().toLowerCase();
|
|
3
|
+
if (v === 'loopback' || v === 'lan') return v;
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveBindModeFromArgs({ flags, kv }) {
|
|
8
|
+
if (flags?.has?.('--loopback')) return 'loopback';
|
|
9
|
+
if (flags?.has?.('--lan')) return 'lan';
|
|
10
|
+
const raw = kv?.get?.('--bind');
|
|
11
|
+
return normalizeBindMode(raw);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Apply a bind mode override to an env object.
|
|
16
|
+
*
|
|
17
|
+
* Semantics:
|
|
18
|
+
* - loopback: prefer localhost-only origins/advertising (best for port-forwarded / isolated environments)
|
|
19
|
+
* - lan: prefer LAN origins/advertising (best for phones on the same network)
|
|
20
|
+
*
|
|
21
|
+
* This currently controls Expo's `--host` via HAPPIER_STACK_EXPO_HOST.
|
|
22
|
+
* Other services may optionally honor HOST=127.0.0.1 when loopback is selected.
|
|
23
|
+
*/
|
|
24
|
+
export function applyBindModeToEnv(env, mode) {
|
|
25
|
+
const m = normalizeBindMode(mode);
|
|
26
|
+
if (!m) return env;
|
|
27
|
+
|
|
28
|
+
env.HAPPIER_STACK_BIND_MODE = m;
|
|
29
|
+
|
|
30
|
+
if (m === 'loopback') {
|
|
31
|
+
env.HAPPIER_STACK_EXPO_HOST = 'localhost';
|
|
32
|
+
// Best-effort: some servers honor HOST.
|
|
33
|
+
env.HOST = '127.0.0.1';
|
|
34
|
+
} else if (m === 'lan') {
|
|
35
|
+
env.HAPPIER_STACK_EXPO_HOST = 'lan';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
|
|
3
|
+
export function pickLanIpv4() {
|
|
4
|
+
try {
|
|
5
|
+
const ifaces = networkInterfaces();
|
|
6
|
+
// Prefer en0 (typical Wi-Fi on macOS), then any non-internal IPv4.
|
|
7
|
+
const preferred = ['en0', 'en1', 'eth0', 'wlan0'];
|
|
8
|
+
for (const name of preferred) {
|
|
9
|
+
const list = ifaces[name] ?? [];
|
|
10
|
+
for (const i of list) {
|
|
11
|
+
if (i && i.family === 'IPv4' && !i.internal && i.address) return i.address;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const list of Object.values(ifaces)) {
|
|
15
|
+
for (const i of list ?? []) {
|
|
16
|
+
if (i && i.family === 'IPv4' && !i.internal && i.address) return i.address;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
|