@rakay-technology/kraft 0.7.4
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 +263 -0
- package/dist/bare-HL2GUYE2.js +27 -0
- package/dist/bowser-HNOTPGJO.js +2825 -0
- package/dist/chunk-3CJLKL6E.js +124 -0
- package/dist/chunk-3NQQXMDR.js +2098 -0
- package/dist/chunk-42WQABXM.js +439 -0
- package/dist/chunk-5GPXMTE4.js +25 -0
- package/dist/chunk-AJ4QZLZ2.js +1069 -0
- package/dist/chunk-AO252K7Z.js +137 -0
- package/dist/chunk-BQMFIXHI.js +975 -0
- package/dist/chunk-DM3PKWDC.js +2453 -0
- package/dist/chunk-DOD2BXHQ.js +1364 -0
- package/dist/chunk-DRK3MEDS.js +2870 -0
- package/dist/chunk-EW4PHKB2.js +13914 -0
- package/dist/chunk-HMJUFZK7.js +340 -0
- package/dist/chunk-I2TOOF2A.js +517 -0
- package/dist/chunk-I3DPMTSF.js +29 -0
- package/dist/chunk-JSCZT76F.js +2588 -0
- package/dist/chunk-MNV2QFHW.js +138 -0
- package/dist/chunk-MOLPYPVH.js +2529 -0
- package/dist/chunk-N4OALTFK.js +295 -0
- package/dist/chunk-OFXBNFNL.js +458 -0
- package/dist/chunk-ONKEYSMY.js +1388 -0
- package/dist/chunk-PDW4L3NV.js +17 -0
- package/dist/chunk-PNNXWA2A.js +1947 -0
- package/dist/chunk-PNRPKRJU.js +11105 -0
- package/dist/chunk-PVOWHIND.js +26781 -0
- package/dist/chunk-PXKCZB2I.js +46 -0
- package/dist/chunk-PZVKPCKE.js +475 -0
- package/dist/chunk-QWHBRY2K.js +4945 -0
- package/dist/chunk-RFOBMHQ7.js +338 -0
- package/dist/chunk-RHLNVZMS.js +28 -0
- package/dist/chunk-RVER2EOO.js +577 -0
- package/dist/chunk-S3QBGGVP.js +81 -0
- package/dist/chunk-TAML5WMA.js +198 -0
- package/dist/chunk-THTEUDBC.js +3188 -0
- package/dist/chunk-WYNM7L22.js +329 -0
- package/dist/chunk-XGCG4MD5.js +102 -0
- package/dist/chunk-XVKUFMIB.js +148 -0
- package/dist/chunk-XZYFZ6A5.js +50 -0
- package/dist/chunk-Y7PVJUXJ.js +2275 -0
- package/dist/chunk-YRSZPVMB.js +306 -0
- package/dist/chunk-YZUKHG3S.js +362 -0
- package/dist/chunk-ZVFS4LUA.js +116 -0
- package/dist/chunk-ZWLDLTJP.js +1417 -0
- package/dist/detect-676SNZK4.js +48 -0
- package/dist/dist-es-2YRDXMUU.js +73 -0
- package/dist/dist-es-CCVZEFRZ.js +91 -0
- package/dist/dist-es-EDUSMF6Q.js +488 -0
- package/dist/dist-es-IOH6G4GM.js +380 -0
- package/dist/dist-es-KWSMDPIP.js +26 -0
- package/dist/dist-es-UYDDOVZ3.js +173 -0
- package/dist/dist-es-XUOR4NQN.js +321 -0
- package/dist/docker-GOT3Q7DJ.js +41 -0
- package/dist/docker-edge-executor-VO6JHHW2.js +19 -0
- package/dist/edge-import-NFCHIW7G.js +50 -0
- package/dist/edge-preflight-QUJ757C6.js +45 -0
- package/dist/ensure-container-edge-Q7IZXHGM.js +38 -0
- package/dist/event-streams-TR4GAC53.js +46 -0
- package/dist/executor-GKXKN52K.js +42 -0
- package/dist/host-channel-preflight-Z6HYSPKO.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9469 -0
- package/dist/loadSso-UGF4JMRK.js +581 -0
- package/dist/local-executor-OMMDKXCM.js +13 -0
- package/dist/nginx-VITTWZZU.js +42 -0
- package/dist/node-bootstrap.js +277 -0
- package/dist/node-entry.js +55 -0
- package/dist/noop-CLLJHCPL.js +11 -0
- package/dist/openresty-lua-6I3Z6PEQ.js +90 -0
- package/dist/privilege-UQAI2HV4.js +20 -0
- package/dist/server/engine/ChangeLog +2289 -0
- package/dist/server/engine/Documentations +5 -0
- package/dist/server/engine/LICENSE +674 -0
- package/dist/server/engine/README.md +34 -0
- package/dist/server/engine/conf/amavisd +150 -0
- package/dist/server/engine/conf/clamav +71 -0
- package/dist/server/engine/conf/core +632 -0
- package/dist/server/engine/conf/dovecot +151 -0
- package/dist/server/engine/conf/fail2ban +72 -0
- package/dist/server/engine/conf/global +698 -0
- package/dist/server/engine/conf/iredapd +52 -0
- package/dist/server/engine/conf/logwatch +28 -0
- package/dist/server/engine/conf/postfix +97 -0
- package/dist/server/engine/conf/postgresql +93 -0
- package/dist/server/engine/conf/spamassassin +39 -0
- package/dist/server/engine/dialog/config_via_dialog.sh +258 -0
- package/dist/server/engine/dialog/optional_components.sh +122 -0
- package/dist/server/engine/dialog/pgsql_config.sh +52 -0
- package/dist/server/engine/dialog/virtual_domain_config.sh +94 -0
- package/dist/server/engine/functions/amavisd.sh +267 -0
- package/dist/server/engine/functions/backend.sh +85 -0
- package/dist/server/engine/functions/clamav.sh +100 -0
- package/dist/server/engine/functions/cleanup.sh +393 -0
- package/dist/server/engine/functions/dovecot.sh +583 -0
- package/dist/server/engine/functions/fail2ban.sh +209 -0
- package/dist/server/engine/functions/iredapd.sh +356 -0
- package/dist/server/engine/functions/optional_components.sh +24 -0
- package/dist/server/engine/functions/packages.sh +522 -0
- package/dist/server/engine/functions/packages_freebsd.sh +179 -0
- package/dist/server/engine/functions/postfix.sh +457 -0
- package/dist/server/engine/functions/postgresql.sh +239 -0
- package/dist/server/engine/functions/spamassassin.sh +49 -0
- package/dist/server/engine/functions/system_accounts.sh +128 -0
- package/dist/server/engine/iRedMail.sh +144 -0
- package/dist/server/engine/pkgs/get_all.sh +338 -0
- package/dist/server/engine/pkgs/pkgs.freebsd.sha256 +3 -0
- package/dist/server/engine/pkgs/pkgs.openbsd.sha256 +6 -0
- package/dist/server/engine/pkgs/pkgs.sha256 +1 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +702 -0
- package/dist/server/engine/samples/amavisd/amavisd.mysql +278 -0
- package/dist/server/engine/samples/amavisd/amavisd.pgsql +253 -0
- package/dist/server/engine/samples/amavisd/amavisd.schema +942 -0
- package/dist/server/engine/samples/amavisd/default_spam_policy.sql +46 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-mariadb.conf +758 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-openldap.conf +767 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-pgsql.conf +762 -0
- package/dist/server/engine/samples/dovecot/dovecot-last-login.conf +50 -0
- package/dist/server/engine/samples/dovecot/dovecot-ldap.conf +21 -0
- package/dist/server/engine/samples/dovecot/dovecot-share-folder.conf +22 -0
- package/dist/server/engine/samples/dovecot/dovecot-sql.conf +28 -0
- package/dist/server/engine/samples/dovecot/dovecot-used-quota.conf +13 -0
- package/dist/server/engine/samples/dovecot/dovecot.conf +484 -0
- package/dist/server/engine/samples/dovecot/dovecot.sieve +7 -0
- package/dist/server/engine/samples/dovecot/dovecot2-quota-warning.sh +38 -0
- package/dist/server/engine/samples/dovecot/sql/imap_share_folder.mysql +16 -0
- package/dist/server/engine/samples/dovecot/sql/last_login.mysql +12 -0
- package/dist/server/engine/samples/dovecot/sql/used_quota.mysql +26 -0
- package/dist/server/engine/samples/dovecot/systemd/override.conf +3 -0
- package/dist/server/engine/samples/fail2ban/action.d/banned_db.conf +9 -0
- package/dist/server/engine/samples/fail2ban/bin/fail2ban_banned_db +268 -0
- package/dist/server/engine/samples/fail2ban/fail2ban.local +17 -0
- package/dist/server/engine/samples/fail2ban/filter.d/dovecot.iredmail.conf +5 -0
- package/dist/server/engine/samples/fail2ban/filter.d/postfix-pregreet.iredmail.conf +16 -0
- package/dist/server/engine/samples/fail2ban/filter.d/postfix.iredmail.conf +18 -0
- package/dist/server/engine/samples/fail2ban/filter.d/roundcube.iredmail.conf +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/dovecot.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/nginx-http-auth.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/postfix-pregreet.local +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/postfix.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/roundcube.local +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sogo.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +11 -0
- package/dist/server/engine/samples/fail2ban/jail.local +6 -0
- package/dist/server/engine/samples/fail2ban/openbsd/rc +27 -0
- package/dist/server/engine/samples/fail2ban/sql/fail2ban.mysql +49 -0
- package/dist/server/engine/samples/fail2ban/sql/fail2ban.pgsql +48 -0
- package/dist/server/engine/samples/firewall/firewalld/services/ssh.xml +6 -0
- package/dist/server/engine/samples/firewall/firewalld/zones/iredmail.xml +15 -0
- package/dist/server/engine/samples/firewall/iptables/ip6tables.init.debian +362 -0
- package/dist/server/engine/samples/firewall/iptables/ip6tables.rules +53 -0
- package/dist/server/engine/samples/firewall/iptables/iptables.init.debian +363 -0
- package/dist/server/engine/samples/firewall/iptables/iptables.rules +54 -0
- package/dist/server/engine/samples/firewall/nftables.conf +58 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/dovecot.conf +3 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/iredapd.conf +1 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/mlmmjadmin.conf +1 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/php-fpm.conf +2 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/slapd.conf +1 -0
- package/dist/server/engine/samples/freebsd/syslog.d/dovecot.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/fail2ban.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/iredapd.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/mlmmjadmin.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/php-fpm.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/slapd.conf +2 -0
- package/dist/server/engine/samples/iredmail/iredmail.mysql +491 -0
- package/dist/server/engine/samples/iredmail/iredmail.pgsql +465 -0
- package/dist/server/engine/samples/iredmail/iredmail.schema +456 -0
- package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.3.pgsql +40 -0
- package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.4.pgsql +18 -0
- package/dist/server/engine/samples/logrotate/dovecot +13 -0
- package/dist/server/engine/samples/logrotate/iredapd +13 -0
- package/dist/server/engine/samples/logrotate/mlmmjadmin +13 -0
- package/dist/server/engine/samples/logrotate/openldap +19 -0
- package/dist/server/engine/samples/logrotate/php-fpm +13 -0
- package/dist/server/engine/samples/openbsd/ldapd.conf +58 -0
- package/dist/server/engine/samples/openbsd/pf.conf +28 -0
- package/dist/server/engine/samples/postfix/command_filter.pcre +3 -0
- package/dist/server/engine/samples/postfix/freebsd/mailer.conf +9 -0
- package/dist/server/engine/samples/postfix/helo_access.pcre +180 -0
- package/dist/server/engine/samples/postfix/ldap/catchall_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/relay_domains.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_login_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/transport_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/transport_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_alias_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_group_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_group_members_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_domains.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_maps.cf +12 -0
- package/dist/server/engine/samples/postfix/main.cf +282 -0
- package/dist/server/engine/samples/postfix/main.cf.amavisd +7 -0
- package/dist/server/engine/samples/postfix/main.cf.dovecot +8 -0
- package/dist/server/engine/samples/postfix/main.cf.ldap +39 -0
- package/dist/server/engine/samples/postfix/main.cf.mlmmj +5 -0
- package/dist/server/engine/samples/postfix/main.cf.mysql +39 -0
- package/dist/server/engine/samples/postfix/main.cf.pgsql +39 -0
- package/dist/server/engine/samples/postfix/main.cf.postscreen +30 -0
- package/dist/server/engine/samples/postfix/master.cf +101 -0
- package/dist/server/engine/samples/postfix/mysql/catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/domain_alias_catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/domain_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/relay_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_dependent_relayhost_maps.cf +6 -0
- package/dist/server/engine/samples/postfix/mysql/sender_login_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_maillist.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/domain_alias_catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/domain_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/relay_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_dependent_relayhost_maps.cf +6 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_login_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_maillist.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/postscreen_access.cidr +6 -0
- package/dist/server/engine/samples/postgresql/sql/add_first_domain_and_user.sql +34 -0
- package/dist/server/engine/samples/postgresql/sql/grant_permissions.sql +14 -0
- package/dist/server/engine/samples/postgresql/sql/init_vmail_db.sql +15 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-dovecot.conf +23 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-fail2ban.conf +2 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-iredapd.conf +12 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-mlmmjadmin.conf +12 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-openldap.conf +2 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-phpfpm.conf +3 -0
- package/dist/server/engine/samples/spamassassin/local.cf +186 -0
- package/dist/server/engine/samples/spamassassin/razor.conf +1 -0
- package/dist/server/engine/samples/systemd/clamd.service.d/override.conf +2 -0
- package/dist/server/engine/samples/systemd/mariadb.service.d/override.conf +3 -0
- package/dist/server/engine/samples/systemd/slapd.service.d/override.conf +6 -0
- package/dist/server/engine/samples/yum/CentOS-Linux-AppStream.repo +6 -0
- package/dist/server/engine/samples/yum/CentOS-Linux-PowerTools.repo +6 -0
- package/dist/server/engine/samples/yum/CentOS-Stream-AppStream.repo +7 -0
- package/dist/server/engine/samples/yum/CentOS-Stream-PowerTools.repo +7 -0
- package/dist/server/engine/samples/yum/symas-openldap.repo +6 -0
- package/dist/server/engine/tools/add_enabled_service.py +60 -0
- package/dist/server/engine/tools/backup_mysql.sh +238 -0
- package/dist/server/engine/tools/backup_openldap.sh +230 -0
- package/dist/server/engine/tools/backup_pgsql.sh +217 -0
- package/dist/server/engine/tools/backup_sogo.sh +156 -0
- package/dist/server/engine/tools/create_mail_user_OpenLDAP.py +335 -0
- package/dist/server/engine/tools/create_mail_user_OpenLDAP.sh +287 -0
- package/dist/server/engine/tools/create_mail_user_SQL.sh +127 -0
- package/dist/server/engine/tools/enable_postscreen.sh +144 -0
- package/dist/server/engine/tools/fail2ban_unban_ip.sh +39 -0
- package/dist/server/engine/tools/find_sasl_login_ip.sh +20 -0
- package/dist/server/engine/tools/find_top_sasl_usernames.sh +28 -0
- package/dist/server/engine/tools/generate_ssl_keys.sh +85 -0
- package/dist/server/engine/tools/kill_high_cpu_sogo_process.sh +32 -0
- package/dist/server/engine/tools/ldap_assign_user_to_groups.py +151 -0
- package/dist/server/engine/tools/ldap_move_members_to_another_group.py +129 -0
- package/dist/server/engine/tools/migrate_sql_alias_table.py +177 -0
- package/dist/server/engine/update/0.9.8/amavisd.mysql +29 -0
- package/dist/server/engine/update/0.9.8/amavisd.pgsql +39 -0
- package/dist/server/engine/update/0.9.8/iredmail.mysql +53 -0
- package/dist/server/engine/update/0.9.8/iredmail.pgsql +61 -0
- package/dist/server/engine/update/0.9.9/iredmail.mysql +2 -0
- package/dist/server/engine/update/0.9.9/iredmail.pgsql +2 -0
- package/dist/server/engine/update/1.0/iredmail.mysql +2 -0
- package/dist/server/engine/update/1.0/iredmail.pgsql +2 -0
- package/dist/server/engine/update/1.2/last_login.mysql +16 -0
- package/dist/server/engine/update/1.4.0/iredmail.mysql +22 -0
- package/dist/server/engine/update/1.4.0/iredmail.pgsql +20 -0
- package/dist/server/engine/update/1.4.1/iredmail.mysql +3 -0
- package/dist/server/engine/update/1.4.1/iredmail.pgsql +3 -0
- package/dist/server/engine/update/1.4.1/sogo.mysql +11 -0
- package/dist/server/engine/update/1.4.1/sogo.pgsql +30 -0
- package/dist/server/engine/update/1.4.1/update-ldap.py +66 -0
- package/dist/server/engine/update/1.4.2/iredmail.mysql +9 -0
- package/dist/server/engine/update/1.4.2/iredmail.pgsql +9 -0
- package/dist/server/engine/update/1.4.2/sogo.pgsql +30 -0
- package/dist/server/engine/update/1.6.0/sogo_view.pgsql +27 -0
- package/dist/server/engine/update/1.6.3/iredmail.mysql +1 -0
- package/dist/server/engine/update/1.7.0/fail2ban.mysql +11 -0
- package/dist/server/engine/update/1.7.0/fail2ban.pgsql +12 -0
- package/dist/server/engine/update/1.7.1/amavisd.mysql +1 -0
- package/dist/server/engine/update/1.7.2/vmail.mysql +30 -0
- package/dist/server/engine/update/1.7.3/deleted_mailboxes.mysql +43 -0
- package/dist/server/engine/update/1.7.3/iredadmin.mysql +26 -0
- package/dist/server/engine/update/1.7.3/vmail.mysql +79 -0
- package/dist/server/engine/update/1.7.3/vmail.pgsql +9 -0
- package/dist/server/engine/update/README.md +1 -0
- package/dist/server/engine/update/ldap/README.md +39 -0
- package/dist/server/engine/update/ldap/update-ldap-dovecot-2.3.py +69 -0
- package/dist/server/index.js +349016 -0
- package/dist/server/lua/geo_country.lua +127 -0
- package/dist/server/lua/maxminddb.lua +403 -0
- package/dist/server/lua/mgmt_api.lua +654 -0
- package/dist/server/lua/pipe_stream.lua +103 -0
- package/dist/server/lua/rules_guard.lua +132 -0
- package/dist/server/lua/rules_lib.lua +176 -0
- package/dist/server/lua/site_logger.lua +405 -0
- package/dist/server/lua/webhook_handler.lua +126 -0
- package/dist/server/migrations/0000_init.sql +666 -0
- package/dist/server/migrations/0001_brief_tomorrow_man.sql +47 -0
- package/dist/server/migrations/0002_funny_dazzler.sql +2 -0
- package/dist/server/migrations/0003_third_killer_shrike.sql +1 -0
- package/dist/server/migrations/0004_session_active_org_not_null.sql +10 -0
- package/dist/server/migrations/0005_warm_amazoness.sql +3 -0
- package/dist/server/migrations/0006_lumpy_joystick.sql +1 -0
- package/dist/server/migrations/0007_outgoing_maginty.sql +2 -0
- package/dist/server/migrations/0008_frozen_cutover.sql +3 -0
- package/dist/server/migrations/0009_credits_billing.sql +142 -0
- package/dist/server/migrations/0010_credit_grant_dedupe.sql +14 -0
- package/dist/server/migrations/0011_remove_credit_ledger.sql +23 -0
- package/dist/server/migrations/0012_smart_deploys_and_git_rollback.sql +81 -0
- package/dist/server/migrations/0013_fix_check_run_rollup_unique.sql +38 -0
- package/dist/server/migrations/0014_default_rollback_git.sql +9 -0
- package/dist/server/migrations/0015_billing_idempotency_grants.sql +41 -0
- package/dist/server/migrations/0016_github_auth_high_findings.sql +30 -0
- package/dist/server/migrations/0017_spooky_rumiko_fujikawa.sql +6 -0
- package/dist/server/migrations/0018_calm_shatterstar.sql +1 -0
- package/dist/server/migrations/0019_rename_workspace_install_to_prepare.sql +1 -0
- package/dist/server/migrations/0020_uneven_the_renegades.sql +13 -0
- package/dist/server/migrations/0021_shiny_firestar.sql +2 -0
- package/dist/server/migrations/0022_version_on_success_backfill.sql +6 -0
- package/dist/server/migrations/0023_productive_gorilla_man.sql +17 -0
- package/dist/server/migrations/0024_sleepy_peter_quill.sql +2 -0
- package/dist/server/migrations/0025_smooth_blue_shield.sql +1 -0
- package/dist/server/migrations/0026_mail_backup_source.sql +11 -0
- package/dist/server/migrations/0027_opposite_anthem.sql +6 -0
- package/dist/server/migrations/0028_stale_rogue.sql +18 -0
- package/dist/server/migrations/0029_bouncy_loners.sql +16 -0
- package/dist/server/migrations/0030_bright_darkstar.sql +6 -0
- package/dist/server/migrations/0031_conscious_catseye.sql +13 -0
- package/dist/server/migrations/0032_wet_mimic.sql +51 -0
- package/dist/server/migrations/0033_eminent_molly_hayes.sql +2 -0
- package/dist/server/migrations/0034_nervous_surge.sql +1 -0
- package/dist/server/migrations/0035_thankful_morgan_stark.sql +1 -0
- package/dist/server/migrations/0036_gorgeous_scalphunter.sql +17 -0
- package/dist/server/migrations/0037_clever_makkari.sql +28 -0
- package/dist/server/migrations/0038_young_magneto.sql +1 -0
- package/dist/server/migrations/0039_secret_lady_ursula.sql +15 -0
- package/dist/server/migrations/0040_sharp_red_wolf.sql +16 -0
- package/dist/server/migrations/0041_round_spyke.sql +4 -0
- package/dist/server/migrations/0042_dapper_havok.sql +1 -0
- package/dist/server/migrations/0043_fast_sharon_ventura.sql +15 -0
- package/dist/server/migrations/0044_amused_shape.sql +8 -0
- package/dist/server/migrations/0045_amusing_korath.sql +1 -0
- package/dist/server/migrations/0046_white_harrier.sql +35 -0
- package/dist/server/migrations/0047_sloppy_strong_guy.sql +2 -0
- package/dist/server/migrations/0048_funny_emma_frost.sql +2 -0
- package/dist/server/migrations/0049_nappy_piledriver.sql +3 -0
- package/dist/server/migrations/0050_add_service_deployment_image_digest.sql +1 -0
- package/dist/server/migrations/0051_add_update_status.sql +19 -0
- package/dist/server/migrations/0052_add_notice_target.sql +2 -0
- package/dist/server/migrations/0053_colossal_sandman.sql +14 -0
- package/dist/server/migrations/0054_add_instance_smtp.sql +5 -0
- package/dist/server/migrations/0055_rainy_klaw.sql +22 -0
- package/dist/server/migrations/0056_moaning_daredevil.sql +2 -0
- package/dist/server/migrations/0057_complete_prism.sql +18 -0
- package/dist/server/migrations/0058_add_server_is_local.sql +1 -0
- package/dist/server/migrations/0059_legal_fixer.sql +18 -0
- package/dist/server/migrations/0060_add_route_strategy_host_port.sql +3 -0
- package/dist/server/migrations/0061_notification_default_multi_kind.sql +3 -0
- package/dist/server/migrations/0062_add_incoming_webhook.sql +21 -0
- package/dist/server/migrations/0063_add_migration_logs.sql +1 -0
- package/dist/server/migrations/0064_add_migration_partial.sql +2 -0
- package/dist/server/migrations/0065_custom_app_template.sql +14 -0
- package/dist/server/migrations/0066_ensure_migration_partial_columns.sql +2 -0
- package/dist/server/migrations/0067_webhook_delivery.sql +28 -0
- package/dist/server/migrations/0068_add_migration_target_volumes.sql +1 -0
- package/dist/server/migrations/0069_add_project_composite_routes.sql +1 -0
- package/dist/server/migrations/0070_forward_git_to_server.sql +1 -0
- package/dist/server/migrations/0071_gh_device_token.sql +3 -0
- package/dist/server/migrations/0072_gh_device_token_method.sql +1 -0
- package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
- package/dist/server/migrations/0074_project_compose_path.sql +1 -0
- package/dist/server/migrations/0075_service_command_argv.sql +1 -0
- package/dist/server/migrations/0076_rollback_retention.sql +17 -0
- package/dist/server/migrations/0077_domain_redirect.sql +13 -0
- package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
- package/dist/server/migrations/0079_project_readiness.sql +18 -0
- package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
- package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
- package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
- package/dist/server/migrations/0083_service_incident.sql +84 -0
- package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
- package/dist/server/migrations/0085_resource_usage.sql +68 -0
- package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
- package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
- package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
- package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
- package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
- package/dist/server/migrations/0091_project_server_id.sql +26 -0
- package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
- package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
- package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
- package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
- package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
- package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
- package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
- package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
- package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
- package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
- package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
- package/dist/server/migrations/0103_dns_credential.sql +41 -0
- package/dist/server/migrations/0104_instance_host_control.sql +15 -0
- package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
- package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
- package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
- package/dist/server/migrations/0108_credential.sql +68 -0
- package/dist/server/migrations/0109_service_build_args.sql +9 -0
- package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
- package/dist/server/migrations/0111_host_port_claim.sql +381 -0
- package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
- package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
- package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
- package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
- package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
- package/dist/server/migrations/0117_github_installation_workspace_scope.sql +24 -0
- package/dist/server/migrations/0118_git_sources.sql +66 -0
- package/dist/server/migrations/0119_backup_run_batch.sql +8 -0
- package/dist/server/migrations/0120_domain_primary_unique.sql +24 -0
- package/dist/server/migrations/0121_host_port_claim_overlap.sql +14 -0
- package/dist/server/migrations/0122_data_transfer_staging.sql +45 -0
- package/dist/server/migrations/0123_drop_cloud_infra.sql +7 -0
- package/dist/server/migrations/0124_mail_dmarc_report.sql +23 -0
- package/dist/server/migrations/0125_server_security_snapshot.sql +22 -0
- package/dist/server/migrations/meta/0000_snapshot.json +4848 -0
- package/dist/server/migrations/meta/0001_snapshot.json +4991 -0
- package/dist/server/migrations/meta/0002_snapshot.json +5013 -0
- package/dist/server/migrations/meta/0003_snapshot.json +5020 -0
- package/dist/server/migrations/meta/0004_snapshot.json +5020 -0
- package/dist/server/migrations/meta/0005_snapshot.json +5039 -0
- package/dist/server/migrations/meta/0006_snapshot.json +5045 -0
- package/dist/server/migrations/meta/0007_snapshot.json +5057 -0
- package/dist/server/migrations/meta/0016_snapshot.json +6053 -0
- package/dist/server/migrations/meta/0017_snapshot.json +6091 -0
- package/dist/server/migrations/meta/0018_snapshot.json +6098 -0
- package/dist/server/migrations/meta/0019_snapshot.json +6098 -0
- package/dist/server/migrations/meta/0020_snapshot.json +6201 -0
- package/dist/server/migrations/meta/0021_snapshot.json +6213 -0
- package/dist/server/migrations/meta/0022_snapshot.json +6213 -0
- package/dist/server/migrations/meta/0023_snapshot.json +6331 -0
- package/dist/server/migrations/meta/0024_snapshot.json +6343 -0
- package/dist/server/migrations/meta/0025_snapshot.json +6350 -0
- package/dist/server/migrations/meta/0026_snapshot.json +6440 -0
- package/dist/server/migrations/meta/0027_snapshot.json +6447 -0
- package/dist/server/migrations/meta/0028_snapshot.json +6581 -0
- package/dist/server/migrations/meta/0029_snapshot.json +6710 -0
- package/dist/server/migrations/meta/0030_snapshot.json +6748 -0
- package/dist/server/migrations/meta/0031_snapshot.json +6859 -0
- package/dist/server/migrations/meta/0032_snapshot.json +7222 -0
- package/dist/server/migrations/meta/0033_snapshot.json +7249 -0
- package/dist/server/migrations/meta/0034_snapshot.json +7255 -0
- package/dist/server/migrations/meta/0035_snapshot.json +7262 -0
- package/dist/server/migrations/meta/0036_snapshot.json +7392 -0
- package/dist/server/migrations/meta/0037_snapshot.json +7602 -0
- package/dist/server/migrations/meta/0038_snapshot.json +7609 -0
- package/dist/server/migrations/meta/0039_snapshot.json +7714 -0
- package/dist/server/migrations/meta/0040_snapshot.json +7820 -0
- package/dist/server/migrations/meta/0041_snapshot.json +7833 -0
- package/dist/server/migrations/meta/0042_snapshot.json +7839 -0
- package/dist/server/migrations/meta/0043_snapshot.json +7938 -0
- package/dist/server/migrations/meta/0044_snapshot.json +7982 -0
- package/dist/server/migrations/meta/0045_snapshot.json +7989 -0
- package/dist/server/migrations/meta/0046_snapshot.json +8263 -0
- package/dist/server/migrations/meta/0047_snapshot.json +8275 -0
- package/dist/server/migrations/meta/0048_snapshot.json +8289 -0
- package/dist/server/migrations/meta/0049_snapshot.json +8308 -0
- package/dist/server/migrations/meta/0050_snapshot.json +8314 -0
- package/dist/server/migrations/meta/0051_snapshot.json +8460 -0
- package/dist/server/migrations/meta/0052_snapshot.json +8472 -0
- package/dist/server/migrations/meta/0053_snapshot.json +8556 -0
- package/dist/server/migrations/meta/0054_snapshot.json +8586 -0
- package/dist/server/migrations/meta/0055_snapshot.json +8756 -0
- package/dist/server/migrations/meta/0056_snapshot.json +8777 -0
- package/dist/server/migrations/meta/0057_snapshot.json +8904 -0
- package/dist/server/migrations/meta/0058_snapshot.json +8911 -0
- package/dist/server/migrations/meta/0059_snapshot.json +9061 -0
- package/dist/server/migrations/meta/0060_snapshot.json +9081 -0
- package/dist/server/migrations/meta/_journal.json +888 -0
- package/dist/server/pglite/pglite.data +0 -0
- package/dist/server/pglite/pglite.wasm +0 -0
- package/dist/setup-JWKZIECM.js +25 -0
- package/dist/signin-3M3UPB6K.js +667 -0
- package/dist/ssh-client-NWQVRCV7.js +29 -0
- package/dist/sso-oidc-3VDIYOEJ.js +834 -0
- package/dist/sts-B3WIYLXR.js +1426 -0
- package/dist/up-P5DMQ5YO.js +55 -0
- package/package.json +58 -0
|
@@ -0,0 +1,2453 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
BuildCancelledError,
|
|
7
|
+
BuildLogger,
|
|
8
|
+
detectBuildKillHint,
|
|
9
|
+
droppedRuntimeEnvMessage,
|
|
10
|
+
isArtifactPathRef,
|
|
11
|
+
isExcludedDocRootEntry,
|
|
12
|
+
killProcessesUnderDir,
|
|
13
|
+
parseLogLevel,
|
|
14
|
+
removeManagedArtifact,
|
|
15
|
+
runBuildPipeline,
|
|
16
|
+
splitRuntimeEnv,
|
|
17
|
+
sq,
|
|
18
|
+
transferLocalDirectory
|
|
19
|
+
} from "./chunk-AJ4QZLZ2.js";
|
|
20
|
+
import {
|
|
21
|
+
LocalExecutor
|
|
22
|
+
} from "./chunk-XVKUFMIB.js";
|
|
23
|
+
import {
|
|
24
|
+
privilegedExecutor
|
|
25
|
+
} from "./chunk-S3QBGGVP.js";
|
|
26
|
+
import {
|
|
27
|
+
portOccupantDetails,
|
|
28
|
+
probeListeningPort
|
|
29
|
+
} from "./chunk-WYNM7L22.js";
|
|
30
|
+
import {
|
|
31
|
+
managedDeploymentUnitName,
|
|
32
|
+
probeExec,
|
|
33
|
+
withReason
|
|
34
|
+
} from "./chunk-N4OALTFK.js";
|
|
35
|
+
import {
|
|
36
|
+
HOST_STATE_DIR,
|
|
37
|
+
envOps,
|
|
38
|
+
hostRefusal,
|
|
39
|
+
opScript
|
|
40
|
+
} from "./chunk-YRSZPVMB.js";
|
|
41
|
+
import {
|
|
42
|
+
ensureOwnedDir,
|
|
43
|
+
resolveEnvironment
|
|
44
|
+
} from "./chunk-OFXBNFNL.js";
|
|
45
|
+
import {
|
|
46
|
+
formatDuration,
|
|
47
|
+
isRemoteConnectionError,
|
|
48
|
+
isRetryableRemoteConnectionError,
|
|
49
|
+
systemDebug
|
|
50
|
+
} from "./chunk-3CJLKL6E.js";
|
|
51
|
+
import {
|
|
52
|
+
sq as sq2,
|
|
53
|
+
wrapLocalBuildCommand
|
|
54
|
+
} from "./chunk-MNV2QFHW.js";
|
|
55
|
+
import {
|
|
56
|
+
DeployError,
|
|
57
|
+
LANGUAGES,
|
|
58
|
+
STACKS,
|
|
59
|
+
answered,
|
|
60
|
+
appVolumeTargets,
|
|
61
|
+
buildOutputTransferExcludes,
|
|
62
|
+
missingOutputDirectoryMessage,
|
|
63
|
+
nodeBinPathExport,
|
|
64
|
+
packageManagerEnsureCommand,
|
|
65
|
+
refused,
|
|
66
|
+
safeErrorMessage,
|
|
67
|
+
shellQuote
|
|
68
|
+
} from "./chunk-PVOWHIND.js";
|
|
69
|
+
|
|
70
|
+
// ../../packages/adapters/src/system/remote-journal.ts
|
|
71
|
+
var OPSH_RUN_VERSION = 1;
|
|
72
|
+
var DEFAULT_JOURNAL_BASE = HOST_STATE_DIR;
|
|
73
|
+
var REMOTE_ENV_PREFIX = "export DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew && ";
|
|
74
|
+
var OPSH_RUN_SCRIPT = `#!/bin/sh
|
|
75
|
+
# opsh-run \u2014 kraft reliable command journal. Managed file; do not edit.
|
|
76
|
+
VERSION=${OPSH_RUN_VERSION}
|
|
77
|
+
|
|
78
|
+
BASE="$OPSH_BASE"
|
|
79
|
+
[ -z "$BASE" ] && BASE=${HOST_STATE_DIR}
|
|
80
|
+
OPS="$BASE/ops"
|
|
81
|
+
|
|
82
|
+
if [ "$1" = "--version" ]; then echo "$VERSION"; exit 0; fi
|
|
83
|
+
|
|
84
|
+
if [ "$1" = "--gc" ]; then
|
|
85
|
+
TTLMIN=1440
|
|
86
|
+
[ -n "$2" ] && TTLMIN="$2"
|
|
87
|
+
[ -d "$OPS" ] || exit 0
|
|
88
|
+
for d in "$OPS"/*; do
|
|
89
|
+
[ -d "$d" ] || continue
|
|
90
|
+
if [ -f "$d/exit" ]; then
|
|
91
|
+
[ -n "$(find "$d/exit" -mmin +"$TTLMIN" 2>/dev/null)" ] && rm -rf "$d"
|
|
92
|
+
else
|
|
93
|
+
if [ -n "$(find "$d" -maxdepth 0 -mmin +1440 2>/dev/null)" ]; then
|
|
94
|
+
p="$(cat "$d/pid" 2>/dev/null)"
|
|
95
|
+
{ [ -z "$p" ] || ! kill -0 "$p" 2>/dev/null; } && rm -rf "$d"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
done
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
OPID="$1"
|
|
103
|
+
CMDB64="$2"
|
|
104
|
+
shift 2
|
|
105
|
+
WAIT=25
|
|
106
|
+
ENVPREFIX=""
|
|
107
|
+
while [ $# -gt 0 ]; do
|
|
108
|
+
case "$1" in
|
|
109
|
+
--wait) [ $# -ge 2 ] && WAIT="$2"; shift 2 2>/dev/null || shift ;;
|
|
110
|
+
--env-prefix) [ $# -ge 2 ] && ENVPREFIX="$(printf %s "$2" | openssl base64 -d -A 2>/dev/null)"; shift 2 2>/dev/null || shift ;;
|
|
111
|
+
*) shift ;;
|
|
112
|
+
esac
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
DIR="$OPS/$OPID"
|
|
116
|
+
CMD="$(printf %s "$CMDB64" | openssl base64 -d -A 2>/dev/null)"
|
|
117
|
+
[ -z "$CMD" ] && { echo "OPSH-EIO badcmd"; exit 0; }
|
|
118
|
+
HASH="$(printf %s "$CMD" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')"
|
|
119
|
+
|
|
120
|
+
if mkdir -p "$OPS" 2>/dev/null && mkdir "$DIR" 2>/dev/null; then
|
|
121
|
+
# Launcher: exactly one invocation per opId reaches here (mkdir is atomic).
|
|
122
|
+
if ! printf %s "$HASH" > "$DIR/cmdhash" 2>/dev/null; then
|
|
123
|
+
rm -rf "$DIR" 2>/dev/null; echo "OPSH-EIO nowrite"; exit 0
|
|
124
|
+
fi
|
|
125
|
+
printf %s "$CMD" > "$DIR/cmd" 2>/dev/null
|
|
126
|
+
# Subshell (not brace group) so a command that calls \`exit\` terminates only
|
|
127
|
+
# the subshell \u2014 the exit code is still captured and journaled.
|
|
128
|
+
RUN="( $ENVPREFIX $CMD ) > $DIR/stdout 2> $DIR/stderr; "'c=$?; echo $c'" > $DIR/exit.tmp && mv $DIR/exit.tmp $DIR/exit"
|
|
129
|
+
# nohup + </dev/null + & detaches from the SSH channel: a dropped connection
|
|
130
|
+
# (channel SIGHUP) can't kill the work. $! stays the running shell's pid.
|
|
131
|
+
nohup sh -c "$RUN" </dev/null >/dev/null 2>&1 &
|
|
132
|
+
echo $! > "$DIR/pid" 2>/dev/null
|
|
133
|
+
else
|
|
134
|
+
# Attach/harvest. Guard against a reused opId carrying a different command.
|
|
135
|
+
[ -d "$DIR" ] || { echo "OPSH-EIO nodir"; exit 0; }
|
|
136
|
+
prev="$(cat "$DIR/cmdhash" 2>/dev/null)"
|
|
137
|
+
[ -n "$prev" ] && [ "$prev" != "$HASH" ] && { echo "OPSH-COLLISION"; exit 0; }
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Wait up to WAIT seconds for the exit file (published atomically AFTER the
|
|
141
|
+
# stdout/stderr fds close \u2192 a visible exit means output is final).
|
|
142
|
+
i=0
|
|
143
|
+
max=$((WAIT * 5))
|
|
144
|
+
while [ ! -f "$DIR/exit" ]; do
|
|
145
|
+
p="$(cat "$DIR/pid" 2>/dev/null)"
|
|
146
|
+
if [ -n "$p" ] && ! kill -0 "$p" 2>/dev/null; then
|
|
147
|
+
[ -f "$DIR/exit" ] && break
|
|
148
|
+
echo "OPSH-DEAD"; exit 0
|
|
149
|
+
fi
|
|
150
|
+
i=$((i + 1))
|
|
151
|
+
if [ "$i" -ge "$max" ]; then echo "OPSH-RUNNING $p"; exit 0; fi
|
|
152
|
+
sleep 0.2
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
CODE="$(cat "$DIR/exit" 2>/dev/null)"
|
|
156
|
+
[ -z "$CODE" ] && CODE=1
|
|
157
|
+
OUT="$(openssl base64 -A < "$DIR/stdout" 2>/dev/null)"
|
|
158
|
+
ERR="$(openssl base64 -A < "$DIR/stderr" 2>/dev/null)"
|
|
159
|
+
echo "OPSH1 $CODE $OUT $ERR"
|
|
160
|
+
exit 0
|
|
161
|
+
`;
|
|
162
|
+
async function resolveJournalBase(exec) {
|
|
163
|
+
try {
|
|
164
|
+
return envOps(await resolveEnvironment(exec)).scratchDir();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (isRemoteConnectionError(err)) throw err;
|
|
167
|
+
return DEFAULT_JOURNAL_BASE;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function ensureRemoteJournal(exec, baseDir) {
|
|
171
|
+
const runner = `${baseDir}/bin/opsh-run`;
|
|
172
|
+
let current = -1;
|
|
173
|
+
try {
|
|
174
|
+
const out = await exec.exec(
|
|
175
|
+
`OPSH_BASE=${sq2(baseDir)} sh ${sq2(runner)} --version 2>/dev/null`,
|
|
176
|
+
{ timeout: 1e4 }
|
|
177
|
+
);
|
|
178
|
+
current = Number.parseInt(out.trim(), 10);
|
|
179
|
+
} catch {
|
|
180
|
+
current = -1;
|
|
181
|
+
}
|
|
182
|
+
if (current !== OPSH_RUN_VERSION) {
|
|
183
|
+
const tmp = `${runner}.tmp`;
|
|
184
|
+
await exec.writeFile(tmp, OPSH_RUN_SCRIPT);
|
|
185
|
+
await exec.exec(
|
|
186
|
+
`mkdir -p ${sq2(`${baseDir}/ops`)} && chmod 0700 ${sq2(tmp)} && mv -f ${sq2(tmp)} ${sq2(runner)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
await exec.exec(
|
|
191
|
+
`OPSH_BASE=${sq2(baseDir)} sh ${sq2(runner)} --gc 2>/dev/null`,
|
|
192
|
+
{
|
|
193
|
+
timeout: 15e3
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function runJournaled(exec, opId, command, opts) {
|
|
200
|
+
const waitSecs = Math.max(1, Math.min(300, opts.waitSecs ?? 25));
|
|
201
|
+
const runner = `${opts.baseDir}/bin/opsh-run`;
|
|
202
|
+
const cmdB64 = Buffer.from(command, "utf8").toString("base64");
|
|
203
|
+
const envPrefix = opts.envPrefix ?? REMOTE_ENV_PREFIX;
|
|
204
|
+
const envB64 = Buffer.from(envPrefix, "utf8").toString("base64");
|
|
205
|
+
let invocation = `OPSH_BASE=${sq2(opts.baseDir)} sh ${sq2(runner)} ${sq2(opId)} ${cmdB64} --wait ${waitSecs}`;
|
|
206
|
+
if (envB64) invocation += ` --env-prefix ${envB64}`;
|
|
207
|
+
const raw = await exec.exec(invocation, { timeout: (waitSecs + 20) * 1e3 });
|
|
208
|
+
return parseFrame(raw);
|
|
209
|
+
}
|
|
210
|
+
function parseFrame(raw) {
|
|
211
|
+
const line = raw.trim().split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
212
|
+
if (line.startsWith("OPSH1 ")) {
|
|
213
|
+
const parts = line.split(" ");
|
|
214
|
+
const code = Number.parseInt(parts[1] ?? "", 10);
|
|
215
|
+
const decode = (b64) => b64 ? Buffer.from(b64, "base64").toString("utf8") : "";
|
|
216
|
+
return {
|
|
217
|
+
status: "done",
|
|
218
|
+
code: Number.isFinite(code) ? code : 1,
|
|
219
|
+
stdout: decode(parts[2]),
|
|
220
|
+
stderr: decode(parts[3])
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (line.startsWith("OPSH-RUNNING")) return { status: "running" };
|
|
224
|
+
if (line === "OPSH-DEAD") return { status: "dead" };
|
|
225
|
+
if (line === "OPSH-COLLISION") return { status: "collision" };
|
|
226
|
+
if (line.startsWith("OPSH-EIO")) {
|
|
227
|
+
return {
|
|
228
|
+
status: "eio",
|
|
229
|
+
message: line.slice("OPSH-EIO ".length).trim() || "journal I/O error"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
status: "eio",
|
|
234
|
+
message: `unrecognized journal frame: ${line.slice(0, 160)}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var OpInterruptedError = class extends Error {
|
|
238
|
+
constructor(message) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = "OpInterruptedError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var DEFAULT_RELIABLE_TIMEOUT_MS = 15 * 6e4;
|
|
244
|
+
var DEFAULT_RECONNECT_MIN_MS = 1e3;
|
|
245
|
+
var DEFAULT_RECONNECT_MAX_MS = 3e4;
|
|
246
|
+
function sleep(ms) {
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const t = setTimeout(resolve, ms);
|
|
249
|
+
t.unref?.();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function runReliable(acquire, opId, command, opts) {
|
|
253
|
+
let baseDir = opts.baseDir ?? null;
|
|
254
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_RELIABLE_TIMEOUT_MS;
|
|
255
|
+
const deadline = Date.now() + timeoutMs;
|
|
256
|
+
const minMs = opts.reconnectMinMs ?? DEFAULT_RECONNECT_MIN_MS;
|
|
257
|
+
const maxMs = opts.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
258
|
+
const envPrefix = opts.envPrefix ?? REMOTE_ENV_PREFIX;
|
|
259
|
+
let backoff = minMs;
|
|
260
|
+
for (; ; ) {
|
|
261
|
+
if (Date.now() >= deadline) {
|
|
262
|
+
throw new Error(`Op '${opId}' timed out after ${timeoutMs}ms`);
|
|
263
|
+
}
|
|
264
|
+
let executor;
|
|
265
|
+
try {
|
|
266
|
+
executor = await acquire();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const wait = Math.min(backoff, deadline - Date.now());
|
|
269
|
+
if (wait <= 0) throw err;
|
|
270
|
+
await sleep(wait);
|
|
271
|
+
backoff = Math.min(backoff * 2, maxMs);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
baseDir ??= await resolveJournalBase(executor);
|
|
276
|
+
if (!opts.ensured || !opts.ensured.has(executor)) {
|
|
277
|
+
await ensureRemoteJournal(executor, baseDir);
|
|
278
|
+
opts.ensured?.add(executor);
|
|
279
|
+
}
|
|
280
|
+
const waitSecs = Math.min(
|
|
281
|
+
opts.waitSecs ?? 25,
|
|
282
|
+
Math.max(1, Math.ceil((deadline - Date.now()) / 1e3))
|
|
283
|
+
);
|
|
284
|
+
const r = await runJournaled(executor, opId, command, {
|
|
285
|
+
baseDir,
|
|
286
|
+
waitSecs,
|
|
287
|
+
envPrefix
|
|
288
|
+
});
|
|
289
|
+
switch (r.status) {
|
|
290
|
+
case "done":
|
|
291
|
+
opts.onSuccess?.();
|
|
292
|
+
return {
|
|
293
|
+
code: r.code ?? 0,
|
|
294
|
+
stdout: r.stdout ?? "",
|
|
295
|
+
stderr: r.stderr ?? ""
|
|
296
|
+
};
|
|
297
|
+
case "running":
|
|
298
|
+
backoff = minMs;
|
|
299
|
+
continue;
|
|
300
|
+
case "dead":
|
|
301
|
+
throw new OpInterruptedError(
|
|
302
|
+
`Op '${opId}' was interrupted and did not complete (no exit recorded).`
|
|
303
|
+
);
|
|
304
|
+
case "collision":
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Op id '${opId}' was reused with a different command.`
|
|
307
|
+
);
|
|
308
|
+
case "eio":
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Remote journal error for op '${opId}': ${r.message ?? "unknown"}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err instanceof OpInterruptedError) throw err;
|
|
315
|
+
if (isRetryableRemoteConnectionError(err)) {
|
|
316
|
+
opts.onRetryableDrop?.(err);
|
|
317
|
+
const wait = Math.min(backoff, deadline - Date.now());
|
|
318
|
+
if (wait <= 0) throw err;
|
|
319
|
+
await sleep(wait);
|
|
320
|
+
backoff = Math.min(backoff * 2, maxMs);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function execReliable(executor, opId, command, opts = {}) {
|
|
328
|
+
if (executor instanceof LocalExecutor) {
|
|
329
|
+
return executor.exec(command);
|
|
330
|
+
}
|
|
331
|
+
const r = await runReliable(
|
|
332
|
+
() => Promise.resolve(executor),
|
|
333
|
+
opId,
|
|
334
|
+
command,
|
|
335
|
+
opts
|
|
336
|
+
);
|
|
337
|
+
if (r.code !== 0) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
r.stderr.trim() || r.stdout.trim() || `Exit code ${r.code}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return r.stdout.trim();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ../../packages/adapters/src/toolchain/catalog.ts
|
|
346
|
+
var CHECKS = {
|
|
347
|
+
node: {
|
|
348
|
+
label: "Node.js",
|
|
349
|
+
versionCommand: "node --version",
|
|
350
|
+
parseVersion: (output) => output.replace(/^v/, "").trim(),
|
|
351
|
+
missingMessage: "Node.js is not installed",
|
|
352
|
+
installable: true
|
|
353
|
+
},
|
|
354
|
+
bun: {
|
|
355
|
+
label: "Bun",
|
|
356
|
+
versionCommand: "bun --version",
|
|
357
|
+
parseVersion: (output) => output.trim(),
|
|
358
|
+
missingMessage: "Bun is not installed",
|
|
359
|
+
installable: true
|
|
360
|
+
},
|
|
361
|
+
npm: {
|
|
362
|
+
label: "npm",
|
|
363
|
+
versionCommand: "npm --version",
|
|
364
|
+
parseVersion: (output) => output.trim(),
|
|
365
|
+
missingMessage: "npm is not installed",
|
|
366
|
+
installable: false,
|
|
367
|
+
providedBy: "node"
|
|
368
|
+
},
|
|
369
|
+
go: {
|
|
370
|
+
label: "Go",
|
|
371
|
+
versionCommand: "go version",
|
|
372
|
+
parseVersion: (output) => output.match(/go(\d+\.\d+(?:\.\d+)?)/)?.[1] ?? output.trim(),
|
|
373
|
+
missingMessage: "Go is not installed",
|
|
374
|
+
installable: true
|
|
375
|
+
},
|
|
376
|
+
rustc: {
|
|
377
|
+
label: "Rust compiler",
|
|
378
|
+
versionCommand: "rustc --version",
|
|
379
|
+
parseVersion: (output) => output.match(/rustc (\S+)/)?.[1] ?? output.trim(),
|
|
380
|
+
missingMessage: "Rust compiler is not installed",
|
|
381
|
+
installable: true
|
|
382
|
+
},
|
|
383
|
+
cargo: {
|
|
384
|
+
label: "Cargo",
|
|
385
|
+
versionCommand: "cargo --version",
|
|
386
|
+
parseVersion: (output) => output.match(/cargo (\S+)/)?.[1] ?? output.trim(),
|
|
387
|
+
missingMessage: "Cargo is not installed",
|
|
388
|
+
installable: false,
|
|
389
|
+
providedBy: "rustc"
|
|
390
|
+
},
|
|
391
|
+
python3: {
|
|
392
|
+
label: "Python 3",
|
|
393
|
+
versionCommand: "python3 --version",
|
|
394
|
+
parseVersion: (output) => output.match(/Python (\S+)/)?.[1] ?? output.trim(),
|
|
395
|
+
missingMessage: "Python 3 is not installed",
|
|
396
|
+
installable: true
|
|
397
|
+
},
|
|
398
|
+
pip: {
|
|
399
|
+
label: "pip",
|
|
400
|
+
versionCommand: "pip3 --version || pip --version",
|
|
401
|
+
parseVersion: (output) => output.match(/pip (\S+)/)?.[1] ?? output.trim(),
|
|
402
|
+
missingMessage: "pip is not installed",
|
|
403
|
+
installable: true,
|
|
404
|
+
providedBy: "python3"
|
|
405
|
+
},
|
|
406
|
+
ruby: {
|
|
407
|
+
label: "Ruby",
|
|
408
|
+
versionCommand: "ruby --version",
|
|
409
|
+
parseVersion: (output) => output.match(/ruby (\S+)/)?.[1] ?? output.trim(),
|
|
410
|
+
missingMessage: "Ruby is not installed",
|
|
411
|
+
installable: true
|
|
412
|
+
},
|
|
413
|
+
bundler: {
|
|
414
|
+
label: "Bundler",
|
|
415
|
+
versionCommand: "bundler --version",
|
|
416
|
+
parseVersion: (output) => output.match(/(\d+\.\d+\.\d+)/)?.[1] ?? output.trim(),
|
|
417
|
+
missingMessage: "Bundler is not installed",
|
|
418
|
+
installable: true,
|
|
419
|
+
providedBy: "ruby"
|
|
420
|
+
},
|
|
421
|
+
php: {
|
|
422
|
+
label: "PHP",
|
|
423
|
+
versionCommand: "php --version",
|
|
424
|
+
parseVersion: (output) => output.match(/PHP (\S+)/)?.[1] ?? output.trim(),
|
|
425
|
+
missingMessage: "PHP is not installed",
|
|
426
|
+
installable: true
|
|
427
|
+
},
|
|
428
|
+
composer: {
|
|
429
|
+
label: "Composer",
|
|
430
|
+
versionCommand: "composer --version",
|
|
431
|
+
parseVersion: (output) => output.match(/(\d+\.\d+\.\d+)/)?.[1] ?? output.trim(),
|
|
432
|
+
missingMessage: "Composer is not installed",
|
|
433
|
+
installable: true,
|
|
434
|
+
providedBy: "php"
|
|
435
|
+
},
|
|
436
|
+
java: {
|
|
437
|
+
label: "Java",
|
|
438
|
+
versionCommand: "java --version 2>&1 || java -version 2>&1",
|
|
439
|
+
parseVersion: (output) => output.match(/(\d+\.\d+)/)?.[1] ?? output.trim(),
|
|
440
|
+
missingMessage: "Java is not installed",
|
|
441
|
+
installable: true
|
|
442
|
+
},
|
|
443
|
+
javac: {
|
|
444
|
+
label: "Java compiler",
|
|
445
|
+
versionCommand: "javac --version 2>&1 || javac -version 2>&1",
|
|
446
|
+
parseVersion: (output) => output.match(/javac (\S+)/)?.[1] ?? output.trim(),
|
|
447
|
+
missingMessage: "Java compiler (javac) is not installed",
|
|
448
|
+
installable: false,
|
|
449
|
+
providedBy: "java"
|
|
450
|
+
},
|
|
451
|
+
maven: {
|
|
452
|
+
label: "Maven",
|
|
453
|
+
versionCommand: "mvn -version 2>&1",
|
|
454
|
+
parseVersion: (output) => output.match(/Apache Maven (\S+)/)?.[1] ?? output.trim(),
|
|
455
|
+
missingMessage: "Maven is not installed",
|
|
456
|
+
installable: true
|
|
457
|
+
},
|
|
458
|
+
gradle: {
|
|
459
|
+
label: "Gradle",
|
|
460
|
+
versionCommand: "gradle --version 2>&1",
|
|
461
|
+
parseVersion: (output) => output.match(/Gradle (\S+)/)?.[1] ?? output.trim(),
|
|
462
|
+
missingMessage: "Gradle is not installed",
|
|
463
|
+
installable: true
|
|
464
|
+
},
|
|
465
|
+
dotnet: {
|
|
466
|
+
label: ".NET SDK",
|
|
467
|
+
versionCommand: "dotnet --version",
|
|
468
|
+
parseVersion: (output) => output.trim(),
|
|
469
|
+
missingMessage: ".NET SDK is not installed",
|
|
470
|
+
installable: true
|
|
471
|
+
},
|
|
472
|
+
elixir: {
|
|
473
|
+
label: "Elixir",
|
|
474
|
+
versionCommand: "elixir --version 2>&1",
|
|
475
|
+
parseVersion: (output) => output.match(/Elixir (\S+)/)?.[1] ?? output.trim(),
|
|
476
|
+
missingMessage: "Elixir is not installed",
|
|
477
|
+
installable: true
|
|
478
|
+
},
|
|
479
|
+
mix: {
|
|
480
|
+
label: "Mix",
|
|
481
|
+
versionCommand: "mix --version 2>&1",
|
|
482
|
+
parseVersion: (output) => output.match(/Mix (\S+)/)?.[1] ?? output.trim(),
|
|
483
|
+
missingMessage: "Mix is not installed",
|
|
484
|
+
installable: false,
|
|
485
|
+
providedBy: "elixir"
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
function label(as, parts) {
|
|
489
|
+
const steps = [];
|
|
490
|
+
for (const part of parts) {
|
|
491
|
+
if (typeof part === "string") {
|
|
492
|
+
steps.push({ command: part, as });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!part.supported) return part;
|
|
496
|
+
for (const command of part.value) steps.push({ command, as });
|
|
497
|
+
}
|
|
498
|
+
return answered(steps);
|
|
499
|
+
}
|
|
500
|
+
function asRoot(...parts) {
|
|
501
|
+
return label("root", parts);
|
|
502
|
+
}
|
|
503
|
+
function asLogin(...parts) {
|
|
504
|
+
return label("login", parts);
|
|
505
|
+
}
|
|
506
|
+
function chain(...parts) {
|
|
507
|
+
const steps = [];
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
if (!part.supported) return part;
|
|
510
|
+
steps.push(...part.value);
|
|
511
|
+
}
|
|
512
|
+
return answered(steps);
|
|
513
|
+
}
|
|
514
|
+
function isAmazon(profile) {
|
|
515
|
+
return profile.distroId?.trim().toLowerCase() === "amzn";
|
|
516
|
+
}
|
|
517
|
+
var NODE_LTS_MAJOR = 22;
|
|
518
|
+
var GO_VERSION = "1.22.5";
|
|
519
|
+
var BUN_VERSION = "1.2.0";
|
|
520
|
+
var DOTNET_CHANNEL = "8.0";
|
|
521
|
+
var COMPOSER_INSTALL = "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer";
|
|
522
|
+
var NO_EPEL = "and Kraft does not enable third-party repositories (EPEL) on a host it did not provision. Install it yourself, or build in a container.";
|
|
523
|
+
var NO_GRADLE_RPM = `The RHEL family ships no gradle package ${NO_EPEL} Most projects include a ./gradlew wrapper, which the build prefers anyway and which needs only a JDK.`;
|
|
524
|
+
var NO_ELIXIR_RPM = `The RHEL family ships no elixir or erlang package ${NO_EPEL}`;
|
|
525
|
+
function ensureUnzip(ops) {
|
|
526
|
+
if (ops.profile.os === "darwin") return answered([]);
|
|
527
|
+
const install = ops.pkgInstall(["unzip"]);
|
|
528
|
+
if (!install.supported) return answered([]);
|
|
529
|
+
return asRoot(
|
|
530
|
+
`command -v unzip >/dev/null 2>&1 || { ${opScript(install.value)}; }`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
var RECIPES = {
|
|
534
|
+
node: {
|
|
535
|
+
verify: "node --version",
|
|
536
|
+
// Total over `SystemPackageManager` with no `default`, so a new manager is a
|
|
537
|
+
// "function lacks ending return statement" error rather than a silent fallthrough.
|
|
538
|
+
steps: (ops) => {
|
|
539
|
+
switch (ops.profile.packageManager) {
|
|
540
|
+
case "apt":
|
|
541
|
+
return asRoot(
|
|
542
|
+
"rm -f /etc/apt/sources.list.d/nodesource.list",
|
|
543
|
+
ops.pkgInstall(["curl", "gnupg", "ca-certificates"]),
|
|
544
|
+
"curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /usr/share/keyrings/nodesource.gpg",
|
|
545
|
+
`echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_LTS_MAJOR}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list`,
|
|
546
|
+
ops.pkgInstall(["nodejs"])
|
|
547
|
+
);
|
|
548
|
+
case "dnf":
|
|
549
|
+
return asRoot(
|
|
550
|
+
`curl -fsSL https://rpm.nodesource.com/setup_${NODE_LTS_MAJOR}.x | bash -`,
|
|
551
|
+
ops.pkgInstall(["nodejs"])
|
|
552
|
+
);
|
|
553
|
+
case "yum":
|
|
554
|
+
return refused(
|
|
555
|
+
`Node ${NODE_LTS_MAJOR} needs glibc 2.28 or newer and this host's release (${ops.profile.prettyName ?? ops.profile.distroId ?? "unknown"}) ships an older one, so the vendor package would install and then fail to load. Use a newer OS release, or build in a container.`
|
|
556
|
+
);
|
|
557
|
+
case "apk":
|
|
558
|
+
return asRoot(ops.pkgInstall(["nodejs", "npm"]));
|
|
559
|
+
case "brew":
|
|
560
|
+
return asRoot(ops.pkgInstall(["node"]));
|
|
561
|
+
case "none":
|
|
562
|
+
return asRoot(ops.pkgInstall(["nodejs"]));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
bun: {
|
|
567
|
+
verify: "bun --version",
|
|
568
|
+
steps: (ops) => {
|
|
569
|
+
if (ops.profile.libc === "musl") {
|
|
570
|
+
return refused(
|
|
571
|
+
"Bun's official installer is a bash script and this host is musl-based (Alpine), which ships busybox ash and no bash. Kraft will not add a shell to a host to satisfy an installer. Use Node for this project, or build in a container."
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return chain(
|
|
575
|
+
ensureUnzip(ops),
|
|
576
|
+
// The download lands in the login user's own home, so it must NOT be elevated:
|
|
577
|
+
// under sudo, HOME becomes root's and root would own ~/.bun, leaving `bun install`
|
|
578
|
+
// unable to write its own cache while `verify` still passes because it only reads.
|
|
579
|
+
//
|
|
580
|
+
// HOME is pinned to `home()` rather than left to `$HOME` so the install prefix and
|
|
581
|
+
// the symlink target below are derived from the same value and cannot diverge. The
|
|
582
|
+
// assignment sits on the `bash`, not the `curl` — it prefixes exactly one command,
|
|
583
|
+
// and it is the interpreter that chooses the prefix.
|
|
584
|
+
asLogin(
|
|
585
|
+
`curl -fsSL https://bun.sh/install | HOME=${shellQuote(ops.home())} bash -s 'bun-v${BUN_VERSION}'`
|
|
586
|
+
),
|
|
587
|
+
// /usr/local/bin is root-owned, and this is what puts bun on PATH for a build.
|
|
588
|
+
asRoot(
|
|
589
|
+
`ln -sf ${shellQuote(`${ops.home()}/.bun/bin/bun`)} /usr/local/bin/bun`
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
go: {
|
|
595
|
+
verify: "go version",
|
|
596
|
+
steps: (ops) => {
|
|
597
|
+
if (ops.profile.libc === "musl") return asRoot(ops.pkgInstall(["go"]));
|
|
598
|
+
if (ops.profile.os === "darwin") return asRoot(ops.pkgInstall(["go"]));
|
|
599
|
+
const arch = ops.releaseArch();
|
|
600
|
+
if (!arch.supported) return arch;
|
|
601
|
+
return asRoot(
|
|
602
|
+
`curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-${arch.value}.tar.gz -o /tmp/go.tar.gz`,
|
|
603
|
+
"rm -rf /usr/local/go",
|
|
604
|
+
"tar -C /usr/local -xzf /tmp/go.tar.gz",
|
|
605
|
+
"rm -f /tmp/go.tar.gz",
|
|
606
|
+
// The profile.d line is for an operator's interactive login only. A build runs
|
|
607
|
+
// through a NON-login `ssh host 'go build'`, which sources nothing, so this was
|
|
608
|
+
// the only recipe of the fifteen whose payload never reached a build's PATH: the
|
|
609
|
+
// install went green and `go build` then exited 127. The symlink is what actually
|
|
610
|
+
// puts it there — Go resolves GOROOT from /proc/self/exe, so a link is transparent.
|
|
611
|
+
"ln -sf /usr/local/go/bin/go /usr/local/bin/go",
|
|
612
|
+
"ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt",
|
|
613
|
+
"echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh"
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
rustc: {
|
|
618
|
+
verify: ". $HOME/.cargo/env 2>/dev/null; rustc --version",
|
|
619
|
+
steps: (ops) => (
|
|
620
|
+
// rustup's default host triple is gnu, so on musl it fetches a toolchain that
|
|
621
|
+
// cannot run. Alpine's rust package is built for the box it's on.
|
|
622
|
+
ops.profile.libc === "musl" ? asRoot(ops.pkgInstall(["rust", "cargo"])) : chain(
|
|
623
|
+
// Unelevated for the same reason as bun, and doubly so here: rustup owns BOTH
|
|
624
|
+
// ~/.cargo and ~/.rustup, and `cargo build` writes into the registry cache
|
|
625
|
+
// under ~/.cargo on every build. Pinned HOME also makes `verify`
|
|
626
|
+
// (`. $HOME/.cargo/env`, run unelevated) look where the toolchain landed.
|
|
627
|
+
asLogin(
|
|
628
|
+
`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | HOME=${shellQuote(ops.home())} sh -s -- -y --no-modify-path`
|
|
629
|
+
),
|
|
630
|
+
asRoot(
|
|
631
|
+
`ln -sf ${shellQuote(`${ops.home()}/.cargo/bin/cargo`)} ${shellQuote(`${ops.home()}/.cargo/bin/rustc`)} /usr/local/bin/`
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
)
|
|
635
|
+
},
|
|
636
|
+
python3: {
|
|
637
|
+
verify: "python3 --version",
|
|
638
|
+
steps: (ops) => asRoot(
|
|
639
|
+
ops.pkgInstallVariants({
|
|
640
|
+
apt: answered(["python3", "python3-pip", "python3-venv"]),
|
|
641
|
+
// No `python3-venv` on the RPM side — venv lives in `python3-libs` there.
|
|
642
|
+
dnf: answered(["python3", "python3-pip"]),
|
|
643
|
+
yum: answered(["python3", "python3-pip"]),
|
|
644
|
+
apk: answered(["python3", "py3-pip"]),
|
|
645
|
+
brew: answered(["python3"])
|
|
646
|
+
})
|
|
647
|
+
)
|
|
648
|
+
},
|
|
649
|
+
pip: {
|
|
650
|
+
verify: "pip3 --version || pip --version",
|
|
651
|
+
steps: (ops) => asRoot(
|
|
652
|
+
ops.pkgInstallVariants({
|
|
653
|
+
apt: answered(["python3-pip"]),
|
|
654
|
+
dnf: answered(["python3-pip"]),
|
|
655
|
+
yum: answered(["python3-pip"]),
|
|
656
|
+
apk: answered(["py3-pip"]),
|
|
657
|
+
// Homebrew has no `pip` formula; pip arrives with its python.
|
|
658
|
+
brew: refused(
|
|
659
|
+
"Homebrew installs pip as part of Python \u2014 install the python3 tool instead."
|
|
660
|
+
)
|
|
661
|
+
})
|
|
662
|
+
)
|
|
663
|
+
},
|
|
664
|
+
ruby: {
|
|
665
|
+
verify: "ruby --version",
|
|
666
|
+
steps: (ops) => asRoot(
|
|
667
|
+
ops.pkgInstallVariants({
|
|
668
|
+
apt: answered(["ruby-full", "build-essential"]),
|
|
669
|
+
// AL2023 has no unversioned `ruby`; Rocky/Alma/RHEL/Fedora do. Both are dnf,
|
|
670
|
+
// which is exactly the split a package-manager-keyed table cannot express.
|
|
671
|
+
dnf: answered(
|
|
672
|
+
isAmazon(ops.profile) ? ["ruby3.2", "ruby3.2-devel"] : ["ruby", "ruby-devel"]
|
|
673
|
+
),
|
|
674
|
+
yum: answered(["ruby", "ruby-devel"]),
|
|
675
|
+
// `build-base` because musl has no prebuilt native gems to fall back on.
|
|
676
|
+
apk: answered(["ruby", "ruby-dev", "build-base"]),
|
|
677
|
+
brew: answered(["ruby"])
|
|
678
|
+
})
|
|
679
|
+
)
|
|
680
|
+
},
|
|
681
|
+
bundler: {
|
|
682
|
+
verify: "bundler --version",
|
|
683
|
+
// Ships with every supported Ruby as a default gem; `gem install` only upgrades it.
|
|
684
|
+
// No distro package is worth preferring here.
|
|
685
|
+
steps: () => asRoot("gem install bundler")
|
|
686
|
+
},
|
|
687
|
+
php: {
|
|
688
|
+
verify: "php --version",
|
|
689
|
+
steps: (ops) => asRoot(
|
|
690
|
+
ops.pkgInstallVariants({
|
|
691
|
+
apt: answered([
|
|
692
|
+
"php-cli",
|
|
693
|
+
"php-mbstring",
|
|
694
|
+
"php-xml",
|
|
695
|
+
"php-curl",
|
|
696
|
+
"unzip"
|
|
697
|
+
]),
|
|
698
|
+
// There is no `php-curl` anywhere on the RPM side — curl support ships in
|
|
699
|
+
// `php-common`, which the cli package pulls in. AL2023 versions every name.
|
|
700
|
+
dnf: answered(
|
|
701
|
+
isAmazon(ops.profile) ? ["php8.2-cli", "php8.2-mbstring", "php8.2-xml"] : ["php-cli", "php-mbstring", "php-xml"]
|
|
702
|
+
),
|
|
703
|
+
yum: answered(["php-cli", "php-mbstring", "php-xml"]),
|
|
704
|
+
// Alpine versions the package and symlinks /usr/bin/php → php83, so `verify`
|
|
705
|
+
// holds without the recipe naming a version.
|
|
706
|
+
apk: answered(["php83"]),
|
|
707
|
+
brew: answered(["php"])
|
|
708
|
+
})
|
|
709
|
+
)
|
|
710
|
+
},
|
|
711
|
+
composer: {
|
|
712
|
+
verify: "composer --version",
|
|
713
|
+
steps: (ops) => {
|
|
714
|
+
if (ops.profile.packageManager === "brew")
|
|
715
|
+
return asRoot(ops.pkgInstall(["composer"]));
|
|
716
|
+
if (ops.profile.libc === "musl") {
|
|
717
|
+
return refused(
|
|
718
|
+
"Composer needs PHP's phar and openssl extensions, and on musl hosts (Alpine) those ship as separate packages that Kraft does not install. Add php-phar and php-openssl for your PHP version yourself, or build in a container."
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return asRoot(COMPOSER_INSTALL);
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
java: {
|
|
725
|
+
verify: "java --version 2>&1",
|
|
726
|
+
steps: (ops) => asRoot(
|
|
727
|
+
ops.pkgInstallVariants({
|
|
728
|
+
apt: answered(["openjdk-21-jdk"]),
|
|
729
|
+
// Amazon ships Corretto in place of OpenJDK — `java-21-openjdk-devel` does not
|
|
730
|
+
// exist on AL2023, so a Java build there installed nothing and then failed on a
|
|
731
|
+
// missing `javac`.
|
|
732
|
+
dnf: answered(
|
|
733
|
+
isAmazon(ops.profile) ? ["java-21-amazon-corretto-devel"] : ["java-21-openjdk-devel"]
|
|
734
|
+
),
|
|
735
|
+
// AL2 and CentOS 7 top out far below 21 (AL2's newest is
|
|
736
|
+
// `java-1.8.0-openjdk-devel`). Installing Java 8 for a recipe that promises 21 is
|
|
737
|
+
// the silent wrong answer, so name the release as the problem instead.
|
|
738
|
+
yum: refused(
|
|
739
|
+
"This host's release carries no OpenJDK 21 package \u2014 the newest it has is Java 8/11, and Kraft will not install a JDK older than the toolchain asks for. Use a newer OS release, or build in a container."
|
|
740
|
+
),
|
|
741
|
+
apk: answered(["openjdk21-jdk"]),
|
|
742
|
+
brew: answered(["openjdk@21"])
|
|
743
|
+
})
|
|
744
|
+
)
|
|
745
|
+
},
|
|
746
|
+
maven: {
|
|
747
|
+
verify: "mvn -version",
|
|
748
|
+
steps: (ops) => asRoot(ops.pkgInstall(["maven"]))
|
|
749
|
+
},
|
|
750
|
+
gradle: {
|
|
751
|
+
verify: "gradle --version",
|
|
752
|
+
steps: (ops) => asRoot(
|
|
753
|
+
ops.pkgInstallVariants({
|
|
754
|
+
apt: answered(["gradle"]),
|
|
755
|
+
dnf: refused(NO_GRADLE_RPM),
|
|
756
|
+
yum: refused(NO_GRADLE_RPM),
|
|
757
|
+
apk: answered(["gradle"]),
|
|
758
|
+
brew: answered(["gradle"])
|
|
759
|
+
})
|
|
760
|
+
)
|
|
761
|
+
},
|
|
762
|
+
dotnet: {
|
|
763
|
+
verify: "dotnet --version",
|
|
764
|
+
steps: (ops) => {
|
|
765
|
+
if (ops.profile.libc === "musl")
|
|
766
|
+
return asRoot(ops.pkgInstall(["dotnet8-sdk"]));
|
|
767
|
+
if (ops.profile.packageManager === "brew")
|
|
768
|
+
return asRoot(ops.pkgInstall(["dotnet"]));
|
|
769
|
+
if (ops.profile.os !== "linux") {
|
|
770
|
+
return refused(
|
|
771
|
+
`Kraft installs the .NET SDK on Linux, and on macOS through Homebrew. This host reports os=${ops.profile.os}.`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
return asRoot(
|
|
775
|
+
"curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh",
|
|
776
|
+
`bash /tmp/dotnet-install.sh --channel ${DOTNET_CHANNEL} --install-dir /usr/local/share/dotnet`,
|
|
777
|
+
"rm -f /tmp/dotnet-install.sh",
|
|
778
|
+
"ln -sf /usr/local/share/dotnet/dotnet /usr/local/bin/dotnet"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
elixir: {
|
|
783
|
+
verify: "elixir --version 2>&1",
|
|
784
|
+
steps: (ops) => asRoot(
|
|
785
|
+
ops.pkgInstallVariants({
|
|
786
|
+
apt: answered(["erlang", "elixir"]),
|
|
787
|
+
// Keyed on the distro, not the package manager, because the two disagree here:
|
|
788
|
+
// Fedora carries elixir and erlang in its own repos, while AL2023 and Rocky 9
|
|
789
|
+
// — same `dnf` — carry neither. Refusing all of dnf told a Fedora operator to go
|
|
790
|
+
// enable EPEL for a package Fedora already has, and EPEL does not target Fedora
|
|
791
|
+
// at all, so the advice was not just unnecessary but impossible to follow.
|
|
792
|
+
//
|
|
793
|
+
// This is the first production reader of `profile.distro`.
|
|
794
|
+
dnf: ops.profile.distro === "fedora" ? answered(["erlang", "elixir"]) : refused(NO_ELIXIR_RPM),
|
|
795
|
+
yum: refused(NO_ELIXIR_RPM),
|
|
796
|
+
apk: answered(["elixir", "erlang"]),
|
|
797
|
+
brew: answered(["elixir"])
|
|
798
|
+
})
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var INSTALLS = Object.fromEntries(
|
|
803
|
+
Object.entries(RECIPES).map(([tool, recipe]) => [
|
|
804
|
+
tool,
|
|
805
|
+
(profile) => {
|
|
806
|
+
const gate = hostRefusal(profile);
|
|
807
|
+
if (gate !== null) return refused(gate);
|
|
808
|
+
const ops = envOps(profile);
|
|
809
|
+
const steps = recipe.steps(ops);
|
|
810
|
+
return steps.supported ? answered({ steps: steps.value, verifyCommand: recipe.verify }) : steps;
|
|
811
|
+
}
|
|
812
|
+
])
|
|
813
|
+
);
|
|
814
|
+
var toolchainCatalog = {
|
|
815
|
+
/**
|
|
816
|
+
* Check recipes — how to detect each tool and parse its version.
|
|
817
|
+
*
|
|
818
|
+
* Every tool named in `LANGUAGES[lang].requiredTools` must have an entry here.
|
|
819
|
+
*/
|
|
820
|
+
checks: CHECKS,
|
|
821
|
+
/**
|
|
822
|
+
* Install plan factories — how to install each tool on a given host.
|
|
823
|
+
*
|
|
824
|
+
* Only `installable` tools have one. npm, cargo, javac and mix arrive with their
|
|
825
|
+
* parent and say so through `providedBy`.
|
|
826
|
+
*/
|
|
827
|
+
installs: INSTALLS
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// ../../packages/adapters/src/toolchain/checks.ts
|
|
831
|
+
function parseVersionParts(version) {
|
|
832
|
+
const match = version.match(/\d+(?:\.\d+)*/)?.[0] ?? "0";
|
|
833
|
+
return match.split(".").map((part) => parseInt(part, 10) || 0);
|
|
834
|
+
}
|
|
835
|
+
function compareVersions(actual, minimum) {
|
|
836
|
+
const actualParts = parseVersionParts(actual);
|
|
837
|
+
const minimumParts = parseVersionParts(minimum);
|
|
838
|
+
const maxLength = Math.max(actualParts.length, minimumParts.length);
|
|
839
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
840
|
+
const left = actualParts[index] ?? 0;
|
|
841
|
+
const right = minimumParts[index] ?? 0;
|
|
842
|
+
if (left > right) return 1;
|
|
843
|
+
if (left < right) return -1;
|
|
844
|
+
}
|
|
845
|
+
return 0;
|
|
846
|
+
}
|
|
847
|
+
function meetsMinVersion(actual, minimum) {
|
|
848
|
+
return compareVersions(actual, minimum) >= 0;
|
|
849
|
+
}
|
|
850
|
+
async function checkTool(executor, name, opts) {
|
|
851
|
+
const recipe = toolchainCatalog.checks[name];
|
|
852
|
+
if (!recipe) {
|
|
853
|
+
return {
|
|
854
|
+
name,
|
|
855
|
+
label: name,
|
|
856
|
+
installed: false,
|
|
857
|
+
healthy: false,
|
|
858
|
+
message: `Unknown tool: ${name}`
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const probe = await probeExec(executor, recipe.versionCommand, "toolchain");
|
|
862
|
+
if (!probe.output) {
|
|
863
|
+
systemDebug(
|
|
864
|
+
"toolchain",
|
|
865
|
+
`${name}:missing ${probe.error ?? "exited 0 with no version"}`
|
|
866
|
+
);
|
|
867
|
+
return {
|
|
868
|
+
name,
|
|
869
|
+
label: recipe.label,
|
|
870
|
+
installed: false,
|
|
871
|
+
healthy: false,
|
|
872
|
+
// A tool that isn't there and a tool whose `--version` was refused ("permission
|
|
873
|
+
// denied", "Command timed out") both land here, and only the second can say why —
|
|
874
|
+
// #408, the same conflation `system/checks.ts` reports its way out of.
|
|
875
|
+
message: withReason(recipe.missingMessage, probe.error)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const version = recipe.parseVersion(probe.output);
|
|
879
|
+
if (opts?.minVersion && compareVersions(version, opts.minVersion) < 0) {
|
|
880
|
+
systemDebug(
|
|
881
|
+
"toolchain",
|
|
882
|
+
`${name}:outdated v${version} < ${opts.minVersion}`
|
|
883
|
+
);
|
|
884
|
+
return {
|
|
885
|
+
name,
|
|
886
|
+
label: recipe.label,
|
|
887
|
+
installed: true,
|
|
888
|
+
version,
|
|
889
|
+
requiredVersion: opts.minVersion,
|
|
890
|
+
healthy: false,
|
|
891
|
+
message: `${recipe.label} ${version} is installed but ${opts.minVersion}+ is required`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
systemDebug("toolchain", `${name}:healthy v${version}`);
|
|
895
|
+
return {
|
|
896
|
+
name,
|
|
897
|
+
label: recipe.label,
|
|
898
|
+
installed: true,
|
|
899
|
+
version,
|
|
900
|
+
requiredVersion: opts?.minVersion,
|
|
901
|
+
healthy: true,
|
|
902
|
+
message: `${recipe.label} ${version}`
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function checkTools(executor, toolNames, requiredVersions) {
|
|
906
|
+
const startedAt = Date.now();
|
|
907
|
+
systemDebug("toolchain", `checkTools:start [${toolNames.join(", ")}]`);
|
|
908
|
+
const tools = await Promise.all(
|
|
909
|
+
toolNames.map(
|
|
910
|
+
(name) => checkTool(executor, name, { minVersion: requiredVersions?.[name] })
|
|
911
|
+
)
|
|
912
|
+
);
|
|
913
|
+
const missing = tools.filter((t) => !t.installed).map((t) => t.name);
|
|
914
|
+
const outdated = tools.filter((t) => t.installed && !t.healthy).map((t) => t.name);
|
|
915
|
+
const ready = missing.length === 0 && outdated.length === 0;
|
|
916
|
+
systemDebug(
|
|
917
|
+
"toolchain",
|
|
918
|
+
`checkTools:done (${formatDuration(startedAt)}) ready=${ready} missing=[${missing.join(", ")}] outdated=[${outdated.join(", ")}]`
|
|
919
|
+
);
|
|
920
|
+
return { tools, ready, missing, outdated };
|
|
921
|
+
}
|
|
922
|
+
async function checkToolchain(executor, language, requiredVersions) {
|
|
923
|
+
const lang = LANGUAGES[language];
|
|
924
|
+
if (!lang || lang.requiredTools.length === 0) {
|
|
925
|
+
return { tools: [], ready: true, missing: [], outdated: [] };
|
|
926
|
+
}
|
|
927
|
+
return checkTools(executor, lang.requiredTools, requiredVersions);
|
|
928
|
+
}
|
|
929
|
+
async function checkToolchainForStack(executor, stackId) {
|
|
930
|
+
const stack = STACKS[stackId];
|
|
931
|
+
if (!stack) {
|
|
932
|
+
return { tools: [], ready: true, missing: [], outdated: [] };
|
|
933
|
+
}
|
|
934
|
+
if (stack.requiredTools && stack.requiredTools.length > 0) {
|
|
935
|
+
return checkTools(
|
|
936
|
+
executor,
|
|
937
|
+
stack.requiredTools,
|
|
938
|
+
stack.requiredToolVersions
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
return checkToolchain(executor, stack.language, stack.requiredToolVersions);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ../../packages/adapters/src/toolchain/installer.ts
|
|
945
|
+
function groupStepsByAuthority(steps, hostNeedsRoot) {
|
|
946
|
+
const groups = [];
|
|
947
|
+
for (const step of steps) {
|
|
948
|
+
const as = step.as === "root" && hostNeedsRoot ? "root" : "login";
|
|
949
|
+
const open = groups.at(-1);
|
|
950
|
+
if (open && open.as === as) open.commands.push(step.command);
|
|
951
|
+
else groups.push({ as, commands: [step.command] });
|
|
952
|
+
}
|
|
953
|
+
return groups;
|
|
954
|
+
}
|
|
955
|
+
async function installTool(executor, name, onLog, requiredVersion) {
|
|
956
|
+
const startedAt = Date.now();
|
|
957
|
+
const recipe = toolchainCatalog.checks[name];
|
|
958
|
+
if (!recipe) {
|
|
959
|
+
return { tool: name, success: false, error: `Unknown tool: ${name}` };
|
|
960
|
+
}
|
|
961
|
+
if (recipe.providedBy && !toolchainCatalog.installs[name]) {
|
|
962
|
+
const status = await checkTool(executor, name, {
|
|
963
|
+
minVersion: requiredVersion
|
|
964
|
+
});
|
|
965
|
+
if (status.healthy) {
|
|
966
|
+
return { tool: name, success: true, version: status.version };
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
tool: name,
|
|
970
|
+
success: false,
|
|
971
|
+
error: `${recipe.label} should be installed by ${recipe.providedBy}`
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
const factory = toolchainCatalog.installs[name];
|
|
975
|
+
if (!factory) {
|
|
976
|
+
return { tool: name, success: false, error: `No installer for ${name}` };
|
|
977
|
+
}
|
|
978
|
+
const profile = await resolveEnvironment(executor);
|
|
979
|
+
const plan = factory(profile);
|
|
980
|
+
if (!plan.supported) {
|
|
981
|
+
onLog?.({
|
|
982
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
983
|
+
message: `Cannot install ${recipe.label} on ${envOps(profile).describe()}: ${plan.reason}`,
|
|
984
|
+
level: "error"
|
|
985
|
+
});
|
|
986
|
+
return { tool: name, success: false, error: plan.reason };
|
|
987
|
+
}
|
|
988
|
+
const groups = groupStepsByAuthority(
|
|
989
|
+
plan.value.steps,
|
|
990
|
+
envOps(profile).installsNeedRoot()
|
|
991
|
+
);
|
|
992
|
+
let elevated = null;
|
|
993
|
+
if (groups.some((group) => group.as === "root")) {
|
|
994
|
+
const grant = await privilegedExecutor(
|
|
995
|
+
executor,
|
|
996
|
+
`Installing ${recipe.label}`
|
|
997
|
+
);
|
|
998
|
+
if (!grant.supported) {
|
|
999
|
+
onLog?.({
|
|
1000
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1001
|
+
message: grant.reason,
|
|
1002
|
+
level: "error"
|
|
1003
|
+
});
|
|
1004
|
+
return { tool: name, success: false, error: grant.reason };
|
|
1005
|
+
}
|
|
1006
|
+
elevated = grant.value.executor;
|
|
1007
|
+
}
|
|
1008
|
+
systemDebug("toolchain", `install:start ${name}`);
|
|
1009
|
+
onLog?.({
|
|
1010
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1011
|
+
message: `Installing ${recipe.label}...`,
|
|
1012
|
+
level: "info"
|
|
1013
|
+
});
|
|
1014
|
+
try {
|
|
1015
|
+
for (const group of groups) {
|
|
1016
|
+
const runner = group.as === "root" ? elevated : executor;
|
|
1017
|
+
const { code } = await runner.streamExec(
|
|
1018
|
+
opScript(group.commands),
|
|
1019
|
+
(entry) => {
|
|
1020
|
+
onLog?.(entry);
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
if (code !== 0) {
|
|
1024
|
+
throw new Error(`Install command failed with exit code ${code}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const verifyResult = await executor.exec(plan.value.verifyCommand, { timeout: 1e4 }).catch(() => null);
|
|
1028
|
+
if (!verifyResult) {
|
|
1029
|
+
throw new Error(`${recipe.label} installed but verification failed`);
|
|
1030
|
+
}
|
|
1031
|
+
const version = recipe.parseVersion(verifyResult);
|
|
1032
|
+
if (requiredVersion && !meetsMinVersion(version, requiredVersion)) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`${recipe.label} ${version} was installed but this project needs ${requiredVersion} or newer, and this host has no newer package. Use a newer OS release, or build in a container.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
systemDebug(
|
|
1038
|
+
"toolchain",
|
|
1039
|
+
`install:done ${name} v${version} (${formatDuration(startedAt)})`
|
|
1040
|
+
);
|
|
1041
|
+
onLog?.({
|
|
1042
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1043
|
+
message: `${recipe.label} ${version} installed successfully`,
|
|
1044
|
+
level: "info"
|
|
1045
|
+
});
|
|
1046
|
+
return { tool: name, success: true, version };
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
const msg = safeErrorMessage(err);
|
|
1049
|
+
systemDebug(
|
|
1050
|
+
"toolchain",
|
|
1051
|
+
`install:fail ${name} (${formatDuration(startedAt)}) ${msg}`
|
|
1052
|
+
);
|
|
1053
|
+
onLog?.({
|
|
1054
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1055
|
+
message: `Failed to install ${recipe.label}: ${msg}`,
|
|
1056
|
+
level: "error"
|
|
1057
|
+
});
|
|
1058
|
+
return { tool: name, success: false, error: msg };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async function installTools(executor, toolNames, onLog, requiredVersions) {
|
|
1062
|
+
const sorted = [...toolNames].sort((a, b) => {
|
|
1063
|
+
const aEntry = toolchainCatalog.checks[a];
|
|
1064
|
+
const bEntry = toolchainCatalog.checks[b];
|
|
1065
|
+
if (aEntry?.providedBy === b) return 1;
|
|
1066
|
+
if (bEntry?.providedBy === a) return -1;
|
|
1067
|
+
if (aEntry?.installable && !bEntry?.installable) return -1;
|
|
1068
|
+
if (!aEntry?.installable && bEntry?.installable) return 1;
|
|
1069
|
+
return 0;
|
|
1070
|
+
});
|
|
1071
|
+
const results = [];
|
|
1072
|
+
for (const name of sorted) {
|
|
1073
|
+
const status = await checkTool(executor, name, {
|
|
1074
|
+
minVersion: requiredVersions?.[name]
|
|
1075
|
+
});
|
|
1076
|
+
if (status.healthy) {
|
|
1077
|
+
onLog?.({
|
|
1078
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1079
|
+
message: `${status.label} ${status.version ?? ""} already installed, skipping`,
|
|
1080
|
+
level: "info"
|
|
1081
|
+
});
|
|
1082
|
+
results.push({ tool: name, success: true, version: status.version });
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const result = await installTool(
|
|
1086
|
+
executor,
|
|
1087
|
+
name,
|
|
1088
|
+
onLog,
|
|
1089
|
+
requiredVersions?.[name]
|
|
1090
|
+
);
|
|
1091
|
+
results.push(result);
|
|
1092
|
+
}
|
|
1093
|
+
return results;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ../../packages/adapters/src/runtime/local-build.ts
|
|
1097
|
+
var LOCAL_BUILD_DIR = "/tmp/kraft/.builds";
|
|
1098
|
+
async function runLocalBuild(opts) {
|
|
1099
|
+
const { config, logger, abort } = opts;
|
|
1100
|
+
const localExec = new LocalExecutor();
|
|
1101
|
+
const buildDir = `${LOCAL_BUILD_DIR}/${config.sessionId}`;
|
|
1102
|
+
await localExec.rm(buildDir);
|
|
1103
|
+
await localExec.mkdir(buildDir);
|
|
1104
|
+
const buildEnv = {
|
|
1105
|
+
projectDir: buildDir,
|
|
1106
|
+
exec: async (command, logCb) => {
|
|
1107
|
+
if (abort?.aborted) throw new Error("Build cancelled");
|
|
1108
|
+
const { code, output } = await localExec.streamExec(
|
|
1109
|
+
wrapLocalBuildCommand(command),
|
|
1110
|
+
logCb
|
|
1111
|
+
);
|
|
1112
|
+
if (abort?.aborted) throw new Error("Build cancelled");
|
|
1113
|
+
if (code !== 0) {
|
|
1114
|
+
const hint = detectBuildKillHint(output);
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`Command failed with exit code ${code}${hint ? ` - ${hint}` : ""}`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
preflight: async (cfg, plog) => {
|
|
1121
|
+
if (abort?.aborted) throw new Error("Build cancelled");
|
|
1122
|
+
if (opts.preflight) {
|
|
1123
|
+
await opts.preflight(cfg, plog, localExec);
|
|
1124
|
+
}
|
|
1125
|
+
if (cfg.localPath) {
|
|
1126
|
+
await transferLocalDirectory(
|
|
1127
|
+
cfg.localPath,
|
|
1128
|
+
{
|
|
1129
|
+
kind: "executor",
|
|
1130
|
+
executor: localExec,
|
|
1131
|
+
path: buildDir
|
|
1132
|
+
},
|
|
1133
|
+
plog
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
try {
|
|
1139
|
+
const result = await runBuildPipeline(buildEnv, config, logger);
|
|
1140
|
+
if (result.status === "deploying") {
|
|
1141
|
+
await opts.transferOutput(buildDir);
|
|
1142
|
+
}
|
|
1143
|
+
return result;
|
|
1144
|
+
} finally {
|
|
1145
|
+
await localExec.rm(buildDir).catch(() => {
|
|
1146
|
+
});
|
|
1147
|
+
await localExec.dispose();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ../../packages/adapters/src/runtime/stack-output.ts
|
|
1152
|
+
import { join as join2, posix as pathPosix } from "path";
|
|
1153
|
+
|
|
1154
|
+
// ../../packages/adapters/src/runtime/nextjs-standalone.ts
|
|
1155
|
+
import { cp, stat } from "fs/promises";
|
|
1156
|
+
import { join } from "path";
|
|
1157
|
+
async function exists(p) {
|
|
1158
|
+
try {
|
|
1159
|
+
await stat(p);
|
|
1160
|
+
return true;
|
|
1161
|
+
} catch {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async function prepareNextStandalone(projectDir) {
|
|
1166
|
+
const bundleDir = join(projectDir, ".next", "standalone");
|
|
1167
|
+
if (!await exists(join(bundleDir, "server.js"))) return null;
|
|
1168
|
+
const staticSrc = join(projectDir, ".next", "static");
|
|
1169
|
+
if (await exists(staticSrc)) {
|
|
1170
|
+
await cp(staticSrc, join(bundleDir, ".next", "static"), {
|
|
1171
|
+
recursive: true
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
const publicSrc = join(projectDir, "public");
|
|
1175
|
+
if (await exists(publicSrc)) {
|
|
1176
|
+
await cp(publicSrc, join(bundleDir, "public"), { recursive: true });
|
|
1177
|
+
}
|
|
1178
|
+
return { bundleDir, startCommand: "node server.js" };
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// ../../packages/adapters/src/runtime/stack-output.ts
|
|
1182
|
+
var STACK_OUTPUT_PREPARERS = {
|
|
1183
|
+
nextjs: prepareNextStandalone
|
|
1184
|
+
};
|
|
1185
|
+
async function prepareStackOutput(stack, projectDir) {
|
|
1186
|
+
const prepare = STACK_OUTPUT_PREPARERS[stack];
|
|
1187
|
+
return prepare ? prepare(projectDir) : null;
|
|
1188
|
+
}
|
|
1189
|
+
function resolveProjectDir(buildDir, rootDirectory) {
|
|
1190
|
+
const normalized = rootDirectory?.trim().replace(/^\/+|\/+$/g, "");
|
|
1191
|
+
return normalized ? join2(buildDir, normalized) : buildDir;
|
|
1192
|
+
}
|
|
1193
|
+
function resolveStaticOutputPath(base, outputDirectory) {
|
|
1194
|
+
const normalizedBase = base.replace(/\/+$/, "");
|
|
1195
|
+
if (!outputDirectory || outputDirectory === ".") {
|
|
1196
|
+
return normalizedBase;
|
|
1197
|
+
}
|
|
1198
|
+
if (outputDirectory.startsWith("/")) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`Invalid outputDirectory "${outputDirectory}": must be relative to the project (absolute paths are not allowed).`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
const resolved = pathPosix.normalize(
|
|
1204
|
+
pathPosix.join(normalizedBase, outputDirectory)
|
|
1205
|
+
);
|
|
1206
|
+
if (resolved !== normalizedBase && !resolved.startsWith(`${normalizedBase}/`)) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`Invalid outputDirectory "${outputDirectory}": escapes the deployment directory.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
return resolved;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// ../../packages/adapters/src/runtime/supervisor/usage.ts
|
|
1215
|
+
var SAMPLE_MS = 250;
|
|
1216
|
+
var ZERO_USAGE = {
|
|
1217
|
+
cpuPercent: 0,
|
|
1218
|
+
memoryMb: 0,
|
|
1219
|
+
diskMb: 0,
|
|
1220
|
+
networkRxBytes: 0,
|
|
1221
|
+
networkTxBytes: 0
|
|
1222
|
+
};
|
|
1223
|
+
function buildProbe(pid, cgroupCandidates) {
|
|
1224
|
+
const p = sq(String(pid));
|
|
1225
|
+
const cgList = cgroupCandidates.map(sq).join(" ");
|
|
1226
|
+
return [
|
|
1227
|
+
`P=${p}`,
|
|
1228
|
+
// Locate a readable cgroup v2 dir for this unit, if any.
|
|
1229
|
+
`CG=""`,
|
|
1230
|
+
`for c in ${cgList}; do if [ -r "$c/cpu.stat" ]; then CG="$c"; break; fi; done`,
|
|
1231
|
+
// ── cgroup v2 ──
|
|
1232
|
+
`if [ -n "$CG" ]; then`,
|
|
1233
|
+
` C1=$(awk '/^usage_usec/{print $2}' "$CG/cpu.stat" 2>/dev/null)`,
|
|
1234
|
+
` sleep ${(SAMPLE_MS / 1e3).toFixed(3)}`,
|
|
1235
|
+
` C2=$(awk '/^usage_usec/{print $2}' "$CG/cpu.stat" 2>/dev/null)`,
|
|
1236
|
+
` M=$(cat "$CG/memory.current" 2>/dev/null)`,
|
|
1237
|
+
// io.stat is "<maj:min> rbytes=N wbytes=N rios=N wios=N" per device — summed
|
|
1238
|
+
// across devices. Read and write are kept apart so the parser adds each once.
|
|
1239
|
+
` R=$(awk '{for(i=1;i<=NF;i++){split($i,kv,"=");if(kv[1]=="rbytes")r+=kv[2]}}END{print r+0}' "$CG/io.stat" 2>/dev/null)`,
|
|
1240
|
+
` W=$(awk '{for(i=1;i<=NF;i++){split($i,kv,"=");if(kv[1]=="wbytes")w+=kv[2]}}END{print w+0}' "$CG/io.stat" 2>/dev/null)`,
|
|
1241
|
+
` echo "cgroup \${C1:-0} \${C2:-0} \${M:-0} \${R:-0} \${W:-0}"`,
|
|
1242
|
+
// ── /proc/<pid> ──
|
|
1243
|
+
`elif [ -r "/proc/$P/stat" ]; then`,
|
|
1244
|
+
// utime (14) + stime (15) in clock ticks. comm can contain spaces, so cut
|
|
1245
|
+
// everything up to the closing paren before counting fields.
|
|
1246
|
+
` T=$(getconf CLK_TCK 2>/dev/null || echo 100)`,
|
|
1247
|
+
` C1=$(sed 's/.*) //' "/proc/$P/stat" 2>/dev/null | awk -v t="$T" '{print int((($12+$13)/t)*1000000)}')`,
|
|
1248
|
+
` sleep ${(SAMPLE_MS / 1e3).toFixed(3)}`,
|
|
1249
|
+
` C2=$(sed 's/.*) //' "/proc/$P/stat" 2>/dev/null | awk -v t="$T" '{print int((($12+$13)/t)*1000000)}')`,
|
|
1250
|
+
` M=$(awk '/^VmRSS:/{print $2*1024}' "/proc/$P/status" 2>/dev/null)`,
|
|
1251
|
+
` R=$(awk '/^read_bytes:/{print $2}' "/proc/$P/io" 2>/dev/null)`,
|
|
1252
|
+
` W=$(awk '/^write_bytes:/{print $2}' "/proc/$P/io" 2>/dev/null)`,
|
|
1253
|
+
` echo "proc \${C1:-0} \${C2:-0} \${M:-0} \${R:-0} \${W:-0}"`,
|
|
1254
|
+
// ── ps (macOS / no procfs) ──
|
|
1255
|
+
`else`,
|
|
1256
|
+
// ps %cpu is a LIFETIME average, not a rate — reported as-is and flagged by the
|
|
1257
|
+
// `ps` source so the parser doesn't pretend it's an instantaneous sample.
|
|
1258
|
+
` X=$(ps -o rss=,%cpu= -p "$P" 2>/dev/null | awk '{print ($1*1024)" "$2}')`,
|
|
1259
|
+
` echo "ps \${X:-0 0}"`,
|
|
1260
|
+
`fi`
|
|
1261
|
+
].join("; ");
|
|
1262
|
+
}
|
|
1263
|
+
function parseProbe(out) {
|
|
1264
|
+
const parts = out.trim().split(/\s+/);
|
|
1265
|
+
const source = parts[0];
|
|
1266
|
+
const n = (i) => {
|
|
1267
|
+
const v = Number(parts[i]);
|
|
1268
|
+
return Number.isFinite(v) && v >= 0 ? v : 0;
|
|
1269
|
+
};
|
|
1270
|
+
let cpuPercent;
|
|
1271
|
+
let memoryMb;
|
|
1272
|
+
let diskMb = 0;
|
|
1273
|
+
if (source === "ps") {
|
|
1274
|
+
if (parts.length < 3) return { ...ZERO_USAGE, source: "none" };
|
|
1275
|
+
memoryMb = n(1) / (1024 * 1024);
|
|
1276
|
+
cpuPercent = n(2);
|
|
1277
|
+
} else if (source === "cgroup" || source === "proc") {
|
|
1278
|
+
if (parts.length < 6) return { ...ZERO_USAGE, source: "none" };
|
|
1279
|
+
const deltaUsec = Math.max(0, n(2) - n(1));
|
|
1280
|
+
cpuPercent = deltaUsec / (SAMPLE_MS * 1e3) * 100;
|
|
1281
|
+
memoryMb = n(3) / (1024 * 1024);
|
|
1282
|
+
diskMb = (n(4) + n(5)) / (1024 * 1024);
|
|
1283
|
+
} else {
|
|
1284
|
+
return { ...ZERO_USAGE, source: "none" };
|
|
1285
|
+
}
|
|
1286
|
+
return {
|
|
1287
|
+
cpuPercent: Math.round(cpuPercent * 100) / 100,
|
|
1288
|
+
memoryMb: Math.round(memoryMb * 100) / 100,
|
|
1289
|
+
diskMb: Math.round(diskMb * 100) / 100,
|
|
1290
|
+
// Per-process network accounting needs eBPF or a per-unit netns, neither of
|
|
1291
|
+
// which a bare deploy has. Zero here means "not measured", and the dashboard
|
|
1292
|
+
// shows network only for container runtimes.
|
|
1293
|
+
networkRxBytes: 0,
|
|
1294
|
+
networkTxBytes: 0,
|
|
1295
|
+
source
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
async function sampleBareUsage(executor, pid, cgroupCandidates = []) {
|
|
1299
|
+
if (!Number.isInteger(pid) || pid <= 0) return { ...ZERO_USAGE };
|
|
1300
|
+
try {
|
|
1301
|
+
const out = await executor.exec(buildProbe(pid, cgroupCandidates));
|
|
1302
|
+
const { source: _source, ...usage } = parseProbe(out);
|
|
1303
|
+
return usage;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return { ...ZERO_USAGE };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// ../../packages/adapters/src/runtime/supervisor/systemd.ts
|
|
1310
|
+
function escapeSystemdEnvValue(value) {
|
|
1311
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1312
|
+
}
|
|
1313
|
+
var SystemdSupervisor = class {
|
|
1314
|
+
constructor(executor, workDir) {
|
|
1315
|
+
this.executor = executor;
|
|
1316
|
+
this.workDir = workDir;
|
|
1317
|
+
}
|
|
1318
|
+
name = "systemd";
|
|
1319
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
1320
|
+
unitName(deploymentId) {
|
|
1321
|
+
return managedDeploymentUnitName(deploymentId);
|
|
1322
|
+
}
|
|
1323
|
+
unitPath(deploymentId) {
|
|
1324
|
+
return `/etc/systemd/system/${this.unitName(deploymentId)}`;
|
|
1325
|
+
}
|
|
1326
|
+
artifactFile(id) {
|
|
1327
|
+
return `${this.workDir}/.artifacts/${id}.path`;
|
|
1328
|
+
}
|
|
1329
|
+
async writeArtifactPath(id, path) {
|
|
1330
|
+
await this.executor.mkdir(`${this.workDir}/.artifacts`);
|
|
1331
|
+
await this.executor.writeFile(this.artifactFile(id), path);
|
|
1332
|
+
}
|
|
1333
|
+
async readArtifactPath(id) {
|
|
1334
|
+
try {
|
|
1335
|
+
const content = await this.executor.readFile(this.artifactFile(id));
|
|
1336
|
+
return content.trim() || null;
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
async removeArtifactPath(id) {
|
|
1342
|
+
await this.executor.rm(this.artifactFile(id));
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Build a systemd unit file contents string.
|
|
1346
|
+
*
|
|
1347
|
+
* Uses Type=exec so systemd tracks the actual process (not the shell wrapper).
|
|
1348
|
+
* Environment vars are set via Environment= directives (one per line), which
|
|
1349
|
+
* avoids any SHELL quoting issues - but systemd does its own parsing, so each
|
|
1350
|
+
* assignment is double-quoted and escaped (see escapeSystemdEnvValue).
|
|
1351
|
+
*/
|
|
1352
|
+
buildUnitFile(opts) {
|
|
1353
|
+
const envLines = Object.entries(opts.env).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)).map(([k, v]) => `Environment="${k}=${escapeSystemdEnvValue(v)}"`).join("\n");
|
|
1354
|
+
return `[Unit]
|
|
1355
|
+
Description=Kraft deployment ${opts.deploymentId}
|
|
1356
|
+
After=network.target
|
|
1357
|
+
|
|
1358
|
+
[Service]
|
|
1359
|
+
Type=exec
|
|
1360
|
+
WorkingDirectory=${opts.workDir}
|
|
1361
|
+
ExecStart=/bin/sh -lc ${sq(opts.startCommand)}
|
|
1362
|
+
${envLines}
|
|
1363
|
+
Restart=on-failure
|
|
1364
|
+
RestartSec=3
|
|
1365
|
+
StandardOutput=journal
|
|
1366
|
+
StandardError=journal
|
|
1367
|
+
SyslogIdentifier=${this.unitName(opts.deploymentId).replace(/\.service$/, "")}
|
|
1368
|
+
|
|
1369
|
+
[Install]
|
|
1370
|
+
WantedBy=multi-user.target
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
1374
|
+
async deploy(opts) {
|
|
1375
|
+
const unitContent = this.buildUnitFile(opts);
|
|
1376
|
+
const unitPath = this.unitPath(opts.deploymentId);
|
|
1377
|
+
const unitName = this.unitName(opts.deploymentId);
|
|
1378
|
+
await this.executor.writeFile(unitPath, unitContent);
|
|
1379
|
+
await this.executor.exec("systemctl daemon-reload");
|
|
1380
|
+
await execReliable(
|
|
1381
|
+
this.executor,
|
|
1382
|
+
`deploy:${opts.deploymentId}:activate`,
|
|
1383
|
+
`systemctl enable --now ${sq(unitName)}`
|
|
1384
|
+
);
|
|
1385
|
+
await this.writeArtifactPath(opts.deploymentId, opts.workDir);
|
|
1386
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1387
|
+
if (!await this.isRunning(opts.deploymentId)) {
|
|
1388
|
+
let hint = "";
|
|
1389
|
+
try {
|
|
1390
|
+
const tail = await this.executor.exec(
|
|
1391
|
+
`journalctl -u ${sq(unitName)} -n 10 --no-pager 2>/dev/null`
|
|
1392
|
+
);
|
|
1393
|
+
hint = tail.trim();
|
|
1394
|
+
} catch {
|
|
1395
|
+
}
|
|
1396
|
+
if (hint.includes("EADDRINUSE")) {
|
|
1397
|
+
const occupant = await probeListeningPort(this.executor, opts.port);
|
|
1398
|
+
throw new DeployError(
|
|
1399
|
+
`Port ${opts.port} is already in use` + (occupant ? ` by ${occupant.command}` : "") + ". Stop the existing process before deploying.",
|
|
1400
|
+
"PORT_IN_USE",
|
|
1401
|
+
portOccupantDetails(opts.port, occupant)
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const msg = hint ? `Process exited immediately after start. Last output:
|
|
1405
|
+
${hint}` : "Process exited immediately after start (no output captured)";
|
|
1406
|
+
throw new Error(msg);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async stop(deploymentId) {
|
|
1410
|
+
const unitName = this.unitName(deploymentId);
|
|
1411
|
+
try {
|
|
1412
|
+
await this.executor.exec(
|
|
1413
|
+
`systemctl stop ${sq(unitName)} 2>/dev/null || true`
|
|
1414
|
+
);
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async start(deploymentId) {
|
|
1419
|
+
const unitName = this.unitName(deploymentId);
|
|
1420
|
+
await this.executor.exec(`systemctl start ${sq(unitName)}`);
|
|
1421
|
+
}
|
|
1422
|
+
async restart(deploymentId) {
|
|
1423
|
+
const unitName = this.unitName(deploymentId);
|
|
1424
|
+
await this.executor.exec(`systemctl restart ${sq(unitName)}`);
|
|
1425
|
+
}
|
|
1426
|
+
async destroy(deploymentId) {
|
|
1427
|
+
const unitName = this.unitName(deploymentId);
|
|
1428
|
+
const unitPath = this.unitPath(deploymentId);
|
|
1429
|
+
await this.executor.exec(
|
|
1430
|
+
`systemctl disable --now ${sq(unitName)} 2>/dev/null || true`
|
|
1431
|
+
);
|
|
1432
|
+
await this.executor.rm(unitPath);
|
|
1433
|
+
await this.executor.exec("systemctl daemon-reload");
|
|
1434
|
+
const artifactPath = await this.readArtifactPath(deploymentId);
|
|
1435
|
+
if (artifactPath) {
|
|
1436
|
+
await this.executor.rm(artifactPath);
|
|
1437
|
+
}
|
|
1438
|
+
await this.removeArtifactPath(deploymentId);
|
|
1439
|
+
}
|
|
1440
|
+
async isRunning(deploymentId) {
|
|
1441
|
+
const unitName = this.unitName(deploymentId);
|
|
1442
|
+
try {
|
|
1443
|
+
const result = await this.executor.exec(
|
|
1444
|
+
`systemctl is-active ${sq(unitName)} 2>/dev/null || true`
|
|
1445
|
+
);
|
|
1446
|
+
return result.trim() === "active";
|
|
1447
|
+
} catch {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Usage for the unit. Prefers the unit's cgroup, which accounts for the whole
|
|
1453
|
+
* process tree — a `npm start` that forks the real server would otherwise report
|
|
1454
|
+
* only the wrapper's near-zero usage.
|
|
1455
|
+
*
|
|
1456
|
+
* `MainPID` is fetched as the fallback identity (and as the liveness check) for a
|
|
1457
|
+
* host without a readable cgroup; the probe picks the tier itself. `--value` is
|
|
1458
|
+
* used so a stopped unit yields "0" rather than a `MainPID=0` line to parse.
|
|
1459
|
+
*/
|
|
1460
|
+
async getUsage(deploymentId) {
|
|
1461
|
+
const unitName = this.unitName(deploymentId);
|
|
1462
|
+
try {
|
|
1463
|
+
const out = await this.executor.exec(
|
|
1464
|
+
`systemctl show ${sq(unitName)} -p MainPID --value 2>/dev/null || true`
|
|
1465
|
+
);
|
|
1466
|
+
const pid = Number.parseInt(out.trim(), 10);
|
|
1467
|
+
if (!Number.isFinite(pid) || pid <= 0) return { ...ZERO_USAGE };
|
|
1468
|
+
return sampleBareUsage(this.executor, pid, [
|
|
1469
|
+
// Where systemd places a unit started from /etc/systemd/system. The delegate
|
|
1470
|
+
// path appears when the unit sets Delegate=yes; harmless to probe either way.
|
|
1471
|
+
`/sys/fs/cgroup/system.slice/${unitName}`,
|
|
1472
|
+
`/sys/fs/cgroup/system.slice/${unitName}/init.scope`
|
|
1473
|
+
]);
|
|
1474
|
+
} catch {
|
|
1475
|
+
return { ...ZERO_USAGE };
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async getLogs(deploymentId, tail) {
|
|
1479
|
+
const unitName = this.unitName(deploymentId);
|
|
1480
|
+
const tailArg = tail ? `-n ${tail}` : `-n 200`;
|
|
1481
|
+
try {
|
|
1482
|
+
const output = await this.executor.exec(
|
|
1483
|
+
`journalctl -u ${sq(unitName)} ${tailArg} --no-pager -o short-iso 2>/dev/null`
|
|
1484
|
+
);
|
|
1485
|
+
return output.split("\n").filter(Boolean).map((line) => {
|
|
1486
|
+
const match = line.match(/^(\S+)\s+\S+\s+\S+\s+(.*)/);
|
|
1487
|
+
return {
|
|
1488
|
+
timestamp: match?.[1] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1489
|
+
message: match?.[2] ?? line,
|
|
1490
|
+
level: parseLogLevel(line)
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
} catch {
|
|
1494
|
+
return [];
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
async streamLogs(deploymentId, onLog, opts) {
|
|
1498
|
+
const unitName = this.unitName(deploymentId);
|
|
1499
|
+
const tailN = opts?.tail ?? 100;
|
|
1500
|
+
let stopped = false;
|
|
1501
|
+
const promise = this.executor.streamExec(
|
|
1502
|
+
`journalctl -u ${sq(unitName)} -n ${tailN} -f --no-pager -o short-iso 2>/dev/null`,
|
|
1503
|
+
(entry) => {
|
|
1504
|
+
if (stopped) return;
|
|
1505
|
+
const match = entry.message.match(/^(\S+)\s+\S+\s+\S+\s+(.*)/);
|
|
1506
|
+
const cleaned = match ? { ...entry, timestamp: match[1], message: match[2] + "\r\n" } : { ...entry, message: entry.message + "\r\n" };
|
|
1507
|
+
onLog(cleaned);
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
promise.catch(() => {
|
|
1511
|
+
});
|
|
1512
|
+
return () => {
|
|
1513
|
+
stopped = true;
|
|
1514
|
+
this.executor.exec(`pkill -f ${sq(`journalctl.*${unitName}`)} 2>/dev/null || true`).catch(() => {
|
|
1515
|
+
});
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
// ../../packages/adapters/src/runtime/supervisor/nohup.ts
|
|
1521
|
+
var NohupSupervisor = class {
|
|
1522
|
+
constructor(executor, workDir) {
|
|
1523
|
+
this.executor = executor;
|
|
1524
|
+
this.workDir = workDir;
|
|
1525
|
+
}
|
|
1526
|
+
name = "nohup";
|
|
1527
|
+
// ── Path helpers ─────────────────────────────────────────────────────
|
|
1528
|
+
pidFile(id) {
|
|
1529
|
+
return `${this.workDir}/.pids/${id}.pid`;
|
|
1530
|
+
}
|
|
1531
|
+
logFile(id) {
|
|
1532
|
+
return `${this.workDir}/.logs/${id}.log`;
|
|
1533
|
+
}
|
|
1534
|
+
artifactFile(id) {
|
|
1535
|
+
return `${this.workDir}/.artifacts/${id}.path`;
|
|
1536
|
+
}
|
|
1537
|
+
// ── PID helpers ──────────────────────────────────────────────────────
|
|
1538
|
+
async readPid(id) {
|
|
1539
|
+
try {
|
|
1540
|
+
const content = await this.executor.readFile(this.pidFile(id));
|
|
1541
|
+
const pid = parseInt(content.trim(), 10);
|
|
1542
|
+
return isNaN(pid) ? null : pid;
|
|
1543
|
+
} catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async writePid(id, pid) {
|
|
1548
|
+
await this.executor.mkdir(`${this.workDir}/.pids`);
|
|
1549
|
+
await this.executor.writeFile(this.pidFile(id), String(pid));
|
|
1550
|
+
}
|
|
1551
|
+
async removePid(id) {
|
|
1552
|
+
await this.executor.rm(this.pidFile(id));
|
|
1553
|
+
}
|
|
1554
|
+
async isAlive(pid) {
|
|
1555
|
+
try {
|
|
1556
|
+
await this.executor.exec(`kill -0 ${pid} 2>/dev/null`);
|
|
1557
|
+
return true;
|
|
1558
|
+
} catch {
|
|
1559
|
+
return false;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
// ── Artifact path tracking ───────────────────────────────────────────
|
|
1563
|
+
async readArtifactPath(id) {
|
|
1564
|
+
try {
|
|
1565
|
+
const content = await this.executor.readFile(this.artifactFile(id));
|
|
1566
|
+
return content.trim() || null;
|
|
1567
|
+
} catch {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async writeArtifactPath(id, path) {
|
|
1572
|
+
await this.executor.mkdir(`${this.workDir}/.artifacts`);
|
|
1573
|
+
await this.executor.writeFile(this.artifactFile(id), path);
|
|
1574
|
+
}
|
|
1575
|
+
async removeArtifactPath(id) {
|
|
1576
|
+
await this.executor.rm(this.artifactFile(id));
|
|
1577
|
+
}
|
|
1578
|
+
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
1579
|
+
async deploy(opts) {
|
|
1580
|
+
await this.executor.mkdir(`${this.workDir}/.logs`);
|
|
1581
|
+
const envExports = Object.entries(opts.env).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)).map(([k, v]) => `export ${k}=${sq(v)}`).join("; ");
|
|
1582
|
+
const logPath = this.logFile(opts.deploymentId);
|
|
1583
|
+
const shellBody = envExports ? `${envExports}; cd ${opts.workDir} && ${opts.startCommand}` : `cd ${opts.workDir} && ${opts.startCommand}`;
|
|
1584
|
+
const runner = `nohup sh -lc ${sq(shellBody)} >> ${sq(logPath)} 2>&1 < /dev/null`;
|
|
1585
|
+
const hasSetsid = await this.executor.exec(
|
|
1586
|
+
"command -v setsid >/dev/null 2>&1 && echo y || echo n"
|
|
1587
|
+
);
|
|
1588
|
+
const detach = hasSetsid.trim() === "y" ? `setsid ${runner}` : runner;
|
|
1589
|
+
const spawnCmd = `{ ${detach} & echo $!; }`;
|
|
1590
|
+
const pidStr = await execReliable(
|
|
1591
|
+
this.executor,
|
|
1592
|
+
`deploy:${opts.deploymentId}:spawn`,
|
|
1593
|
+
spawnCmd
|
|
1594
|
+
);
|
|
1595
|
+
const pid = parseInt(pidStr.trim(), 10);
|
|
1596
|
+
if (isNaN(pid) || pid <= 0) {
|
|
1597
|
+
throw new Error("Deploy failed: could not capture process ID");
|
|
1598
|
+
}
|
|
1599
|
+
await this.writePid(opts.deploymentId, pid);
|
|
1600
|
+
await this.writeArtifactPath(opts.deploymentId, opts.workDir);
|
|
1601
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1602
|
+
if (!await this.isAlive(pid)) {
|
|
1603
|
+
let hint = "";
|
|
1604
|
+
try {
|
|
1605
|
+
const tail = await this.executor.exec(`tail -n 10 ${sq(logPath)}`);
|
|
1606
|
+
hint = tail.trim();
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
if (hint.includes("EADDRINUSE")) {
|
|
1610
|
+
const occupant = await probeListeningPort(this.executor, opts.port);
|
|
1611
|
+
throw new DeployError(
|
|
1612
|
+
`Port ${opts.port} is already in use` + (occupant ? ` by ${occupant.command}` : "") + ". Stop the existing process before deploying.",
|
|
1613
|
+
"PORT_IN_USE",
|
|
1614
|
+
portOccupantDetails(opts.port, occupant)
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
const msg = hint ? `Process exited immediately after spawn. Last output:
|
|
1618
|
+
${hint}` : "Process exited immediately after spawn (no output captured)";
|
|
1619
|
+
throw new Error(msg);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
async stop(deploymentId) {
|
|
1623
|
+
const pid = await this.readPid(deploymentId);
|
|
1624
|
+
if (!pid) return;
|
|
1625
|
+
if (await this.isAlive(pid)) {
|
|
1626
|
+
await this.executor.exec(
|
|
1627
|
+
`kill -- -${pid} 2>/dev/null || kill ${pid} 2>/dev/null || true`
|
|
1628
|
+
);
|
|
1629
|
+
const deadline = Date.now() + 1e4;
|
|
1630
|
+
while (Date.now() < deadline && await this.isAlive(pid)) {
|
|
1631
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1632
|
+
}
|
|
1633
|
+
if (await this.isAlive(pid)) {
|
|
1634
|
+
await this.executor.exec(
|
|
1635
|
+
`kill -9 -- -${pid} 2>/dev/null || kill -9 ${pid} 2>/dev/null || true`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async start(_deploymentId) {
|
|
1641
|
+
throw new Error(
|
|
1642
|
+
"Nohup processes cannot be started after stopping. Trigger a new deployment."
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
async restart(deploymentId) {
|
|
1646
|
+
await this.stop(deploymentId);
|
|
1647
|
+
}
|
|
1648
|
+
async destroy(deploymentId) {
|
|
1649
|
+
await this.stop(deploymentId);
|
|
1650
|
+
await this.removePid(deploymentId);
|
|
1651
|
+
await this.executor.rm(this.logFile(deploymentId));
|
|
1652
|
+
const artifactPath = await this.readArtifactPath(deploymentId);
|
|
1653
|
+
if (artifactPath) {
|
|
1654
|
+
await this.executor.rm(artifactPath);
|
|
1655
|
+
}
|
|
1656
|
+
await this.removeArtifactPath(deploymentId);
|
|
1657
|
+
}
|
|
1658
|
+
async isRunning(deploymentId) {
|
|
1659
|
+
const pid = await this.readPid(deploymentId);
|
|
1660
|
+
if (!pid) return false;
|
|
1661
|
+
return this.isAlive(pid);
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Usage for the tracked process.
|
|
1665
|
+
*
|
|
1666
|
+
* No cgroup candidates: nohup does not create one (that's the documented
|
|
1667
|
+
* limitation above), so the probe uses /proc on Linux and `ps` on macOS. Both
|
|
1668
|
+
* measure the LEADER only — a start command that forks will under-report, which
|
|
1669
|
+
* is the same tradeoff as this supervisor's lack of restart and reboot handling.
|
|
1670
|
+
*/
|
|
1671
|
+
async getUsage(deploymentId) {
|
|
1672
|
+
const pid = await this.readPid(deploymentId);
|
|
1673
|
+
if (!pid) return { ...ZERO_USAGE };
|
|
1674
|
+
return sampleBareUsage(this.executor, pid);
|
|
1675
|
+
}
|
|
1676
|
+
async getLogs(deploymentId, tail) {
|
|
1677
|
+
const logPath = this.logFile(deploymentId);
|
|
1678
|
+
try {
|
|
1679
|
+
const lines = tail ? await this.executor.exec(`tail -n ${tail} ${sq(logPath)}`) : await this.executor.readFile(logPath);
|
|
1680
|
+
return lines.split("\n").filter(Boolean).map((line) => ({
|
|
1681
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1682
|
+
message: line,
|
|
1683
|
+
level: parseLogLevel(line)
|
|
1684
|
+
}));
|
|
1685
|
+
} catch {
|
|
1686
|
+
return [];
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
async streamLogs(deploymentId, onLog, opts) {
|
|
1690
|
+
const logPath = this.logFile(deploymentId);
|
|
1691
|
+
const tailN = opts?.tail ?? 100;
|
|
1692
|
+
let stopped = false;
|
|
1693
|
+
if (!await this.executor.exists(logPath).catch(() => true)) {
|
|
1694
|
+
onLog({
|
|
1695
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1696
|
+
message: "No supervisor log for this deployment \u2014 the process is supervised elsewhere (container logs, or `kraft logs` on the host). Streaming anyway in case it starts writing.\r\n",
|
|
1697
|
+
level: "info"
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
const promise = this.executor.streamExec(
|
|
1701
|
+
`mkdir -p ${sq(`${this.workDir}/.logs`)} 2>/dev/null; touch ${sq(logPath)} 2>/dev/null; tail -n ${tailN} -F ${sq(logPath)} 2>/dev/null`,
|
|
1702
|
+
(entry) => {
|
|
1703
|
+
if (!stopped) onLog({ ...entry, message: entry.message + "\r\n" });
|
|
1704
|
+
}
|
|
1705
|
+
);
|
|
1706
|
+
promise.catch(() => {
|
|
1707
|
+
});
|
|
1708
|
+
return () => {
|
|
1709
|
+
stopped = true;
|
|
1710
|
+
this.executor.exec(`pkill -f ${sq(`tail.*${logPath}`)} 2>/dev/null || true`).catch(() => {
|
|
1711
|
+
});
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
// ../../packages/adapters/src/runtime/supervisor/detect.ts
|
|
1717
|
+
async function detectSupervisor(executor, workDir) {
|
|
1718
|
+
const profile = await resolveEnvironment(executor).catch((err) => {
|
|
1719
|
+
systemDebug(
|
|
1720
|
+
"supervisor",
|
|
1721
|
+
`detect: host unreachable, using nohup \u2014 ${safeErrorMessage(err)}`
|
|
1722
|
+
);
|
|
1723
|
+
return null;
|
|
1724
|
+
});
|
|
1725
|
+
if (profile?.serviceManager === "systemd")
|
|
1726
|
+
return new SystemdSupervisor(executor, workDir);
|
|
1727
|
+
const unmeasured = profile?.probeError ?? (profile?.supported === false ? profile.unsupportedReason : null);
|
|
1728
|
+
if (unmeasured) {
|
|
1729
|
+
console.warn(
|
|
1730
|
+
`[supervisor] could not confirm this host's init system, so the service will run under nohup and will NOT restart after a reboot: ${unmeasured}`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
return new NohupSupervisor(executor, workDir);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// ../../packages/adapters/src/runtime/bare.ts
|
|
1737
|
+
function parentPath(path) {
|
|
1738
|
+
const idx = path.replace(/\/+$/, "").lastIndexOf("/");
|
|
1739
|
+
return idx > 0 ? path.slice(0, idx) : "/";
|
|
1740
|
+
}
|
|
1741
|
+
var DEFAULT_WORK_DIR = "/opt/kraft";
|
|
1742
|
+
var DEFAULT_BUILD_TIMEOUT = 10 * 60 * 1e3;
|
|
1743
|
+
var STATIC_RELEASE_BASE = "/opt/kraft/static";
|
|
1744
|
+
var BareRuntime = class {
|
|
1745
|
+
name = "bare";
|
|
1746
|
+
capabilities = /* @__PURE__ */ new Set([
|
|
1747
|
+
"build",
|
|
1748
|
+
"deploy",
|
|
1749
|
+
"stop",
|
|
1750
|
+
"start",
|
|
1751
|
+
"restart",
|
|
1752
|
+
"destroy",
|
|
1753
|
+
"runtimeLogs",
|
|
1754
|
+
"streamLogs",
|
|
1755
|
+
// Real measurements now (cgroup → /proc → ps, via the supervisor), so this can
|
|
1756
|
+
// finally be declared. It was absent while getUsage returned a zeros stub — but
|
|
1757
|
+
// nothing checked, so callers rendered those zeros as data. Anything reading
|
|
1758
|
+
// usage should gate on supports("usage") rather than trust the numbers.
|
|
1759
|
+
// Network is the one gap: per-process rx/tx needs eBPF or a netns, so it stays 0.
|
|
1760
|
+
"usage",
|
|
1761
|
+
"containerIp",
|
|
1762
|
+
"rollback",
|
|
1763
|
+
// The release dir + supervisor unit survive a redeploy, so restoring a
|
|
1764
|
+
// past release really is an in-place unit swap (see makeActive).
|
|
1765
|
+
"unitRestore",
|
|
1766
|
+
"inContainerExec"
|
|
1767
|
+
]);
|
|
1768
|
+
workDir;
|
|
1769
|
+
buildTimeout;
|
|
1770
|
+
executor;
|
|
1771
|
+
systemManager;
|
|
1772
|
+
/** True if we created the executor ourselves (must dispose on cleanup) */
|
|
1773
|
+
ownsExecutor;
|
|
1774
|
+
/** Track active builds by sessionId for cancellation */
|
|
1775
|
+
activeBuilds = /* @__PURE__ */ new Map();
|
|
1776
|
+
/** Process lifecycle delegate - resolved lazily on first deploy/stop/etc. */
|
|
1777
|
+
_supervisor = null;
|
|
1778
|
+
_supervisorPromise = null;
|
|
1779
|
+
constructor(opts) {
|
|
1780
|
+
this.workDir = opts?.workDir ?? DEFAULT_WORK_DIR;
|
|
1781
|
+
this.buildTimeout = opts?.buildTimeout ?? DEFAULT_BUILD_TIMEOUT;
|
|
1782
|
+
if (opts?.executor) {
|
|
1783
|
+
this.executor = opts.executor;
|
|
1784
|
+
this.ownsExecutor = false;
|
|
1785
|
+
} else {
|
|
1786
|
+
this.executor = new LocalExecutor();
|
|
1787
|
+
this.ownsExecutor = true;
|
|
1788
|
+
}
|
|
1789
|
+
this.systemManager = opts?.systemManager ?? null;
|
|
1790
|
+
}
|
|
1791
|
+
/** The underlying command executor (local or SSH). Exposed so the
|
|
1792
|
+
* backup subsystem's bare executor can stream commands over the same
|
|
1793
|
+
* connection (mirrors how DockerRuntime exposes its client). */
|
|
1794
|
+
get commandExecutor() {
|
|
1795
|
+
return this.executor;
|
|
1796
|
+
}
|
|
1797
|
+
/** A bare deployment is a host process, so "inside the instance" == the host.
|
|
1798
|
+
* The host executor already sees the process's listeners (shared netns). */
|
|
1799
|
+
async inContainerExecutor() {
|
|
1800
|
+
return this.executor;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Resolve a retained application release for an env-only refresh.
|
|
1804
|
+
* Deployment ids originate in our database, but reject path-shaped input at
|
|
1805
|
+
* this boundary so an imported/corrupt row cannot escape releases/.
|
|
1806
|
+
*/
|
|
1807
|
+
async retainedReleaseArtifact(deploymentId) {
|
|
1808
|
+
if (!deploymentId || deploymentId.includes("/") || deploymentId.includes("\\"))
|
|
1809
|
+
return null;
|
|
1810
|
+
const path = this.releaseDir(deploymentId);
|
|
1811
|
+
return await this.executor.exists(path).catch(() => false) ? path : null;
|
|
1812
|
+
}
|
|
1813
|
+
/** Get or lazily initialise the process supervisor. */
|
|
1814
|
+
async supervisor() {
|
|
1815
|
+
if (this._supervisor) return this._supervisor;
|
|
1816
|
+
if (!this._supervisorPromise) {
|
|
1817
|
+
this._supervisorPromise = detectSupervisor(
|
|
1818
|
+
this.executor,
|
|
1819
|
+
this.workDir
|
|
1820
|
+
).then((s) => {
|
|
1821
|
+
this._supervisor = s;
|
|
1822
|
+
return s;
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
return this._supervisorPromise;
|
|
1826
|
+
}
|
|
1827
|
+
supports(cap) {
|
|
1828
|
+
return this.capabilities.has(cap);
|
|
1829
|
+
}
|
|
1830
|
+
async dispose() {
|
|
1831
|
+
if (this.ownsExecutor) {
|
|
1832
|
+
await this.executor.dispose();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
// ─── Path helpers ────────────────────────────────────────────────────
|
|
1836
|
+
projectDir(projectId) {
|
|
1837
|
+
return `${this.workDir}/${projectId}`;
|
|
1838
|
+
}
|
|
1839
|
+
buildDir(sessionId) {
|
|
1840
|
+
return `${this.workDir}/.builds/${sessionId}`;
|
|
1841
|
+
}
|
|
1842
|
+
releaseDir(deploymentId) {
|
|
1843
|
+
return `${this.workDir}/releases/${deploymentId}`;
|
|
1844
|
+
}
|
|
1845
|
+
/** Per-project directory holding the paths that must survive a release swap.
|
|
1846
|
+
* The `shared/` half of the Capistrano layout `releases/` already implements. */
|
|
1847
|
+
sharedDir(projectId) {
|
|
1848
|
+
return `${this.workDir}/shared/${projectId}`;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Repoint the release's persistent paths at `shared/`, so a redeploy doesn't
|
|
1852
|
+
* take the app's data with the old release.
|
|
1853
|
+
*
|
|
1854
|
+
* Docker gets this from a volume; bare has no mount to hang it on, so the
|
|
1855
|
+
* equivalent is the deploy convention: keep the state outside the release tree
|
|
1856
|
+
* and symlink it in. The first release that ships the path SEEDS the shared
|
|
1857
|
+
* copy from it — frameworks ship a skeleton there (Laravel's `storage/` has
|
|
1858
|
+
* `framework/cache`, `framework/sessions`, …) and an app handed an empty
|
|
1859
|
+
* directory instead would fail on write.
|
|
1860
|
+
*
|
|
1861
|
+
* Best-effort per path: a box without the shared dir writable is a storage
|
|
1862
|
+
* problem to report, not a reason to fail an otherwise good release.
|
|
1863
|
+
*/
|
|
1864
|
+
async linkPersistentPaths(releaseDir, projectId, volumes, log) {
|
|
1865
|
+
const targets = appVolumeTargets(volumes ?? []);
|
|
1866
|
+
if (targets.length === 0) return;
|
|
1867
|
+
const shared = this.sharedDir(projectId);
|
|
1868
|
+
for (const relative of targets) {
|
|
1869
|
+
const sharedPath = `${shared}/${relative}`;
|
|
1870
|
+
const releasePath = `${releaseDir}/${relative}`;
|
|
1871
|
+
try {
|
|
1872
|
+
if (!await this.executor.exists(sharedPath)) {
|
|
1873
|
+
await this.executor.mkdir(parentPath(sharedPath));
|
|
1874
|
+
if (await this.executor.exists(releasePath)) {
|
|
1875
|
+
await this.executor.exec(
|
|
1876
|
+
`cp -a ${sq(releasePath)} ${sq(sharedPath)}`
|
|
1877
|
+
);
|
|
1878
|
+
} else {
|
|
1879
|
+
await this.executor.mkdir(sharedPath);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
await this.executor.rm(releasePath);
|
|
1883
|
+
await this.executor.mkdir(parentPath(releasePath));
|
|
1884
|
+
await this.executor.exec(
|
|
1885
|
+
`ln -sfn ${sq(sharedPath)} ${sq(releasePath)}`
|
|
1886
|
+
);
|
|
1887
|
+
log?.({
|
|
1888
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1889
|
+
message: `Persistent path ${relative} \u2192 ${sharedPath}
|
|
1890
|
+
`,
|
|
1891
|
+
level: "info"
|
|
1892
|
+
});
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
log?.({
|
|
1895
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1896
|
+
message: `Could not persist ${relative}: ${safeErrorMessage(err)}
|
|
1897
|
+
`,
|
|
1898
|
+
level: "warn"
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async promoteBuildArtifact(artifactPath, deploymentId, previousDeploymentId) {
|
|
1904
|
+
const releaseDir = this.releaseDir(deploymentId);
|
|
1905
|
+
if (artifactPath === releaseDir) return releaseDir;
|
|
1906
|
+
const consumeSource = !artifactPath.startsWith(`${this.workDir}/releases/`);
|
|
1907
|
+
await ensureOwnedDir(this.executor, `${this.workDir}/releases`);
|
|
1908
|
+
await this.executor.rm(releaseDir);
|
|
1909
|
+
const previousReleaseDir = previousDeploymentId ? this.releaseDir(previousDeploymentId) : void 0;
|
|
1910
|
+
if (previousReleaseDir && await this.executor.exists(previousReleaseDir)) {
|
|
1911
|
+
try {
|
|
1912
|
+
await execReliable(
|
|
1913
|
+
this.executor,
|
|
1914
|
+
`deploy:${deploymentId}:promote-rsync`,
|
|
1915
|
+
`rsync -a --delete --link-dest=${sq(previousReleaseDir)} ${sq(artifactPath)}/ ${sq(releaseDir)}/`
|
|
1916
|
+
);
|
|
1917
|
+
if (consumeSource) await this.executor.rm(artifactPath).catch(() => {
|
|
1918
|
+
});
|
|
1919
|
+
return releaseDir;
|
|
1920
|
+
} catch {
|
|
1921
|
+
await this.executor.rm(releaseDir).catch(() => {
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (!consumeSource) {
|
|
1926
|
+
await execReliable(
|
|
1927
|
+
this.executor,
|
|
1928
|
+
`deploy:${deploymentId}:promote-copy`,
|
|
1929
|
+
`mkdir -p ${sq(releaseDir)} && cp -a ${sq(artifactPath)}/. ${sq(releaseDir)}/`
|
|
1930
|
+
);
|
|
1931
|
+
return releaseDir;
|
|
1932
|
+
}
|
|
1933
|
+
await execReliable(
|
|
1934
|
+
this.executor,
|
|
1935
|
+
`deploy:${deploymentId}:promote-mv`,
|
|
1936
|
+
`mv ${sq(artifactPath)} ${sq(releaseDir)}`
|
|
1937
|
+
);
|
|
1938
|
+
return releaseDir;
|
|
1939
|
+
}
|
|
1940
|
+
// ── File transfer ──────────────────────────────────────────────────────
|
|
1941
|
+
/**
|
|
1942
|
+
* Transfer files from a local path on the API server into the build/deploy dir.
|
|
1943
|
+
*
|
|
1944
|
+
* Delegates entirely to the executor - LocalExecutor does cp,
|
|
1945
|
+
* SshExecutor does tar+pipe. No branching here.
|
|
1946
|
+
*/
|
|
1947
|
+
async transferFiles(localPath, remotePath, logger) {
|
|
1948
|
+
await transferLocalDirectory(
|
|
1949
|
+
localPath,
|
|
1950
|
+
{
|
|
1951
|
+
kind: "executor",
|
|
1952
|
+
executor: this.executor,
|
|
1953
|
+
path: remotePath
|
|
1954
|
+
},
|
|
1955
|
+
logger
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
// ── Build lifecycle ────────────────────────────────────────────────────
|
|
1959
|
+
async build(config, logger) {
|
|
1960
|
+
const log = logger ?? new BuildLogger();
|
|
1961
|
+
const buildLocally = config.buildStrategy === "local" && !(this.executor instanceof LocalExecutor);
|
|
1962
|
+
const abort = new AbortController();
|
|
1963
|
+
this.activeBuilds.set(config.sessionId, abort);
|
|
1964
|
+
try {
|
|
1965
|
+
if (buildLocally) {
|
|
1966
|
+
return await this.buildLocally(config, log, abort);
|
|
1967
|
+
}
|
|
1968
|
+
return await this.buildOnTarget(config, log, abort);
|
|
1969
|
+
} finally {
|
|
1970
|
+
this.activeBuilds.delete(config.sessionId);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/** Build on the API host, then transfer output to the target server. */
|
|
1974
|
+
async buildLocally(config, log, abort) {
|
|
1975
|
+
log.log("Build strategy: local (build on API host, transfer to server)\n");
|
|
1976
|
+
const remoteDir = this.buildDir(config.sessionId);
|
|
1977
|
+
const stackDef = STACKS[config.stack];
|
|
1978
|
+
let standaloneStartCommand;
|
|
1979
|
+
let result;
|
|
1980
|
+
try {
|
|
1981
|
+
result = await runLocalBuild({
|
|
1982
|
+
config,
|
|
1983
|
+
logger: log,
|
|
1984
|
+
abort: abort.signal,
|
|
1985
|
+
preflight: async (cfg, plog, localExec) => {
|
|
1986
|
+
await this.ensureToolchain(localExec, cfg.stack, plog);
|
|
1987
|
+
plog.log("Checking runtime tools on target server...\n");
|
|
1988
|
+
await this.ensureToolchain(this.executor, cfg.stack, plog);
|
|
1989
|
+
if (this.systemManager) {
|
|
1990
|
+
plog.log("Ensuring rsync is installed on target server...\n");
|
|
1991
|
+
await this.systemManager.ensureComponents(
|
|
1992
|
+
["rsync"],
|
|
1993
|
+
(entry) => plog.callback(entry)
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
},
|
|
1997
|
+
transferOutput: async (buildDir) => {
|
|
1998
|
+
await this.executor.rm(remoteDir);
|
|
1999
|
+
await this.executor.mkdir(remoteDir);
|
|
2000
|
+
const selfContained = await prepareStackOutput(
|
|
2001
|
+
config.stack,
|
|
2002
|
+
resolveProjectDir(buildDir, config.rootDirectory)
|
|
2003
|
+
);
|
|
2004
|
+
if (selfContained) {
|
|
2005
|
+
log.log(
|
|
2006
|
+
"Detected self-contained build output \u2014 shipping the bundle (no install on target).\n"
|
|
2007
|
+
);
|
|
2008
|
+
await transferLocalDirectory(
|
|
2009
|
+
selfContained.bundleDir,
|
|
2010
|
+
{ kind: "executor", executor: this.executor, path: remoteDir },
|
|
2011
|
+
log,
|
|
2012
|
+
{ excludes: [] }
|
|
2013
|
+
// ship everything, incl. traced node_modules
|
|
2014
|
+
);
|
|
2015
|
+
standaloneStartCommand = selfContained.startCommand;
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
if (stackDef?.productionPaths?.length) {
|
|
2019
|
+
log.log(
|
|
2020
|
+
`Transferring production paths: ${stackDef.productionPaths.join(", ")}
|
|
2021
|
+
`
|
|
2022
|
+
);
|
|
2023
|
+
await transferLocalDirectory(
|
|
2024
|
+
buildDir,
|
|
2025
|
+
{ kind: "executor", executor: this.executor, path: remoteDir },
|
|
2026
|
+
log,
|
|
2027
|
+
{ includes: [...stackDef.productionPaths] }
|
|
2028
|
+
);
|
|
2029
|
+
} else {
|
|
2030
|
+
await transferLocalDirectory(
|
|
2031
|
+
buildDir,
|
|
2032
|
+
{ kind: "executor", executor: this.executor, path: remoteDir },
|
|
2033
|
+
log,
|
|
2034
|
+
{
|
|
2035
|
+
excludes: buildOutputTransferExcludes(stackDef),
|
|
2036
|
+
alsoInclude: stackDef?.outputDirectory ? [stackDef.outputDirectory] : void 0
|
|
2037
|
+
}
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
const installCmd = config.installCommand?.trim();
|
|
2041
|
+
if (installCmd) {
|
|
2042
|
+
const pmEnsure = packageManagerEnsureCommand(config.packageManager);
|
|
2043
|
+
const fullInstall = pmEnsure ? `${pmEnsure} && ${installCmd}` : installCmd;
|
|
2044
|
+
log.log("Installing production dependencies on target...\n");
|
|
2045
|
+
const { code } = await this.executor.streamExec(
|
|
2046
|
+
`cd ${sq(remoteDir)} && ${fullInstall}`,
|
|
2047
|
+
log.callback
|
|
2048
|
+
);
|
|
2049
|
+
if (code !== 0) {
|
|
2050
|
+
throw new Error(
|
|
2051
|
+
"Failed to install production dependencies on target"
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
log.log("Production dependencies installed.\n");
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
} catch (err) {
|
|
2059
|
+
const msg = safeErrorMessage(err);
|
|
2060
|
+
log.log(`Failed to transfer local build output: ${msg}`, "error");
|
|
2061
|
+
return {
|
|
2062
|
+
sessionId: config.sessionId,
|
|
2063
|
+
status: "failed",
|
|
2064
|
+
imageRef: remoteDir,
|
|
2065
|
+
errorMessage: `Failed to transfer build output: ${msg}`
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
sessionId: config.sessionId,
|
|
2070
|
+
status: result.status,
|
|
2071
|
+
imageRef: remoteDir,
|
|
2072
|
+
durationMs: result.durationMs,
|
|
2073
|
+
errorMessage: result.errorMessage,
|
|
2074
|
+
startCommand: standaloneStartCommand
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
/** Build directly on the target machine via the executor. */
|
|
2078
|
+
async buildOnTarget(config, log, abort) {
|
|
2079
|
+
log.log("Build strategy: server (build on target)\n");
|
|
2080
|
+
const dir = this.buildDir(config.sessionId);
|
|
2081
|
+
await this.executor.rm(dir);
|
|
2082
|
+
await this.executor.mkdir(dir);
|
|
2083
|
+
const buildEnv = {
|
|
2084
|
+
projectDir: dir,
|
|
2085
|
+
exec: async (command, logCb) => {
|
|
2086
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
2087
|
+
const effectiveCommand = this.executor instanceof LocalExecutor ? wrapLocalBuildCommand(command) : command;
|
|
2088
|
+
const { code, output } = await this.executor.streamExec(
|
|
2089
|
+
effectiveCommand,
|
|
2090
|
+
logCb
|
|
2091
|
+
);
|
|
2092
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
2093
|
+
if (code !== 0) {
|
|
2094
|
+
const hint = detectBuildKillHint(output);
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
`Command failed with exit code ${code}${hint ? ` - ${hint}` : ""}`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
preflight: async (cfg, plog) => {
|
|
2101
|
+
if (abort.signal.aborted) throw new BuildCancelledError();
|
|
2102
|
+
await this.ensureToolchain(this.executor, cfg.stack, plog);
|
|
2103
|
+
if (cfg.localPath) {
|
|
2104
|
+
await this.transferFiles(cfg.localPath, dir, plog);
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
// Out-of-band secret write (SSH key + known_hosts) — goes through the
|
|
2108
|
+
// executor's file channel, never the streamed `exec`, so key bytes never
|
|
2109
|
+
// reach the build log. Works for both local and SSH executors.
|
|
2110
|
+
writeSecretFile: (p, content) => this.executor.writeFile(p, content)
|
|
2111
|
+
};
|
|
2112
|
+
const result = await runBuildPipeline(buildEnv, config, log);
|
|
2113
|
+
return {
|
|
2114
|
+
sessionId: config.sessionId,
|
|
2115
|
+
status: result.status,
|
|
2116
|
+
imageRef: dir,
|
|
2117
|
+
durationMs: result.durationMs,
|
|
2118
|
+
errorMessage: result.errorMessage
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Check that the target executor has the required toolchain for a stack,
|
|
2123
|
+
* and install any missing or outdated tools.
|
|
2124
|
+
*/
|
|
2125
|
+
async ensureToolchain(executor, stack, plog) {
|
|
2126
|
+
const toolcheck = await checkToolchainForStack(executor, stack);
|
|
2127
|
+
if (toolcheck.ready) return;
|
|
2128
|
+
const requiredTools = toolcheck.tools.filter((tool) => !tool.healthy);
|
|
2129
|
+
plog.log("Installing build tools (one-time server setup)\u2026\n");
|
|
2130
|
+
plog.log(`${requiredTools.map((tool) => tool.message).join("\n")}
|
|
2131
|
+
`);
|
|
2132
|
+
const results = await installTools(
|
|
2133
|
+
executor,
|
|
2134
|
+
requiredTools.map((tool) => tool.name),
|
|
2135
|
+
plog.callback,
|
|
2136
|
+
Object.fromEntries(
|
|
2137
|
+
requiredTools.filter((tool) => tool.requiredVersion).map((tool) => [tool.name, tool.requiredVersion])
|
|
2138
|
+
)
|
|
2139
|
+
);
|
|
2140
|
+
const failed = results.filter((r) => !r.success);
|
|
2141
|
+
if (failed.length > 0) {
|
|
2142
|
+
throw new Error(
|
|
2143
|
+
`Failed to install required tools: ${failed.map((f) => `${f.tool} (${f.error})`).join(", ")}`
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
async cancelBuild(sessionId) {
|
|
2148
|
+
const abort = this.activeBuilds.get(sessionId);
|
|
2149
|
+
if (abort) {
|
|
2150
|
+
abort.abort();
|
|
2151
|
+
this.activeBuilds.delete(sessionId);
|
|
2152
|
+
}
|
|
2153
|
+
await killProcessesUnderDir(this.executor, this.buildDir(sessionId));
|
|
2154
|
+
}
|
|
2155
|
+
async getBuildLogs(sessionId) {
|
|
2156
|
+
void sessionId;
|
|
2157
|
+
return [];
|
|
2158
|
+
}
|
|
2159
|
+
// ── Deploy lifecycle ───────────────────────────────────────────────────
|
|
2160
|
+
async deploy(config, _onLog) {
|
|
2161
|
+
if (config.adopt) {
|
|
2162
|
+
const occupant = await probeListeningPort(this.executor, config.port);
|
|
2163
|
+
_onLog?.({
|
|
2164
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2165
|
+
level: occupant ? "info" : "warn",
|
|
2166
|
+
message: occupant ? `[adopt] attached to process on port ${config.port}: ${occupant.command}
|
|
2167
|
+
` : `[adopt] no process is listening on port ${config.port} yet \u2014 routing will attach when it comes up
|
|
2168
|
+
`
|
|
2169
|
+
});
|
|
2170
|
+
return {
|
|
2171
|
+
deploymentId: config.deploymentId,
|
|
2172
|
+
containerId: config.deploymentId,
|
|
2173
|
+
status: "running"
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const stagedDir = config.imageRef ?? this.projectDir(config.projectId);
|
|
2177
|
+
const workDir = config.imageRef ? await this.promoteBuildArtifact(
|
|
2178
|
+
stagedDir,
|
|
2179
|
+
config.deploymentId,
|
|
2180
|
+
config.previousDeploymentId
|
|
2181
|
+
) : stagedDir;
|
|
2182
|
+
await this.linkPersistentPaths(
|
|
2183
|
+
workDir,
|
|
2184
|
+
config.projectId,
|
|
2185
|
+
config.volumes,
|
|
2186
|
+
_onLog
|
|
2187
|
+
);
|
|
2188
|
+
const sv = await this.supervisor();
|
|
2189
|
+
const projectEnv = splitRuntimeEnv(
|
|
2190
|
+
Object.fromEntries(
|
|
2191
|
+
Object.entries(config.envVars ?? {}).map(([k, v]) => [k, String(v)])
|
|
2192
|
+
)
|
|
2193
|
+
);
|
|
2194
|
+
if (projectEnv.dropped.length > 0) {
|
|
2195
|
+
_onLog?.({
|
|
2196
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2197
|
+
level: "warn",
|
|
2198
|
+
message: droppedRuntimeEnvMessage(projectEnv.dropped)
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
const env = {
|
|
2202
|
+
...Object.fromEntries(projectEnv.entries),
|
|
2203
|
+
PORT: String(config.port),
|
|
2204
|
+
NODE_ENV: config.environment === "production" ? "production" : "development"
|
|
2205
|
+
};
|
|
2206
|
+
const startBinPath = nodeBinPathExport(config.packageManager, [workDir]);
|
|
2207
|
+
const resolvedStart = config.startCommand || "npm start";
|
|
2208
|
+
try {
|
|
2209
|
+
await sv.deploy({
|
|
2210
|
+
deploymentId: config.deploymentId,
|
|
2211
|
+
projectId: config.projectId,
|
|
2212
|
+
workDir,
|
|
2213
|
+
startCommand: startBinPath ? `${startBinPath} && ${resolvedStart}` : resolvedStart,
|
|
2214
|
+
port: config.port,
|
|
2215
|
+
env
|
|
2216
|
+
});
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
if (workDir !== stagedDir) {
|
|
2219
|
+
await sv.destroy(config.deploymentId).catch(() => {
|
|
2220
|
+
});
|
|
2221
|
+
await this.executor.rm(workDir).catch(() => {
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
throw err;
|
|
2225
|
+
}
|
|
2226
|
+
return {
|
|
2227
|
+
deploymentId: config.deploymentId,
|
|
2228
|
+
containerId: config.deploymentId,
|
|
2229
|
+
status: "running"
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
async deployStatic(config) {
|
|
2233
|
+
const workDir = await this.promoteStaticRelease({
|
|
2234
|
+
artifactPath: config.imageRef ?? this.projectDir(config.projectId),
|
|
2235
|
+
releaseId: config.deploymentId,
|
|
2236
|
+
previousReleaseId: config.previousDeploymentId,
|
|
2237
|
+
outputDirectory: config.outputDirectory,
|
|
2238
|
+
promote: Boolean(config.imageRef)
|
|
2239
|
+
});
|
|
2240
|
+
return {
|
|
2241
|
+
deploymentId: config.deploymentId,
|
|
2242
|
+
containerId: workDir,
|
|
2243
|
+
status: "running"
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Promote an extracted static build into a stable release directory and PROVE it
|
|
2248
|
+
* can serve, returning the release dir.
|
|
2249
|
+
*
|
|
2250
|
+
* Split out of `deployStatic` so the compose/monorepo static path reuses the
|
|
2251
|
+
* identical promote + validation instead of registering a vhost straight at the
|
|
2252
|
+
* `.builds/<session>` staging dir. Two things came from that shortcut: the
|
|
2253
|
+
* public doc-root of a compose static sub-app was a per-build-session scratch
|
|
2254
|
+
* directory (nothing owned its lifetime, so superseded copies accumulated and
|
|
2255
|
+
* any sweep of `.builds` would take the live site down), and it skipped the only
|
|
2256
|
+
* HARD deploy-time output gate we have — so an extract that produced nothing
|
|
2257
|
+
* deployed green and 404'd.
|
|
2258
|
+
*
|
|
2259
|
+
* `releaseId` is the release's identity, not necessarily a deployment id: the
|
|
2260
|
+
* compose path passes `<deploymentId>-<serviceId>` so each static sub-app owns
|
|
2261
|
+
* its own release dir under one deployment.
|
|
2262
|
+
*/
|
|
2263
|
+
async promoteStaticRelease(opts) {
|
|
2264
|
+
const stagedDir = opts.artifactPath;
|
|
2265
|
+
const workDir = opts.promote === false ? stagedDir : await this.promoteBuildArtifact(
|
|
2266
|
+
stagedDir,
|
|
2267
|
+
opts.releaseId,
|
|
2268
|
+
opts.previousReleaseId
|
|
2269
|
+
);
|
|
2270
|
+
const staticRoot = resolveStaticOutputPath(workDir, opts.outputDirectory);
|
|
2271
|
+
const abort = async (message) => {
|
|
2272
|
+
if (workDir !== stagedDir) {
|
|
2273
|
+
await this.executor.rm(workDir).catch(() => {
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
throw new Error(message);
|
|
2277
|
+
};
|
|
2278
|
+
if (!await this.executor.exists(staticRoot)) {
|
|
2279
|
+
return abort(missingOutputDirectoryMessage(opts.outputDirectory));
|
|
2280
|
+
}
|
|
2281
|
+
if (staticRoot === workDir) {
|
|
2282
|
+
const undeleted = await this.pruneBuildFilesFromDocRoot(staticRoot);
|
|
2283
|
+
if (undeleted.length > 0) {
|
|
2284
|
+
return abort(
|
|
2285
|
+
`Refusing to publish ${staticRoot}: could not remove ${undeleted.join(", ")} from the document root. Serving them would expose the repository (and, for a private repo, the access token in .git/config) at the site's public URL.`
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (await this.isEmptyDir(staticRoot)) {
|
|
2290
|
+
return abort(
|
|
2291
|
+
`The output directory "${opts.outputDirectory || "."}" is empty \u2014 the build produced no files to serve, so every request would 404. Check the build command and the Output Directory setting.`
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
return workDir;
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Remove top-level build inputs from a doc root, returning the names it could NOT
|
|
2298
|
+
* remove (empty on success, and on "there was nothing to remove").
|
|
2299
|
+
*
|
|
2300
|
+
* Reuses `isExcludedDocRootEntry` rather than re-listing the names, because the
|
|
2301
|
+
* Docker sandbox already owns that list and two copies of "what must not be
|
|
2302
|
+
* served" is precisely how the bare path came to serve `.git`.
|
|
2303
|
+
*
|
|
2304
|
+
* An unreadable listing returns empty — a probe that cannot read must not fail a
|
|
2305
|
+
* deploy — but a listed entry that survives its `rm` is reported, because the
|
|
2306
|
+
* caller must refuse to publish rather than expose a token.
|
|
2307
|
+
*/
|
|
2308
|
+
async pruneBuildFilesFromDocRoot(root) {
|
|
2309
|
+
const listed = await this.executor.exec(`ls -A ${sq(root)} 2>/dev/null; true`).catch(() => null);
|
|
2310
|
+
if (listed === null) return [];
|
|
2311
|
+
const doomed = listed.split("\n").map((l) => l.trim()).filter(Boolean).filter(isExcludedDocRootEntry);
|
|
2312
|
+
if (doomed.length === 0) return [];
|
|
2313
|
+
await this.executor.exec(`rm -rf ${doomed.map((n) => sq(`${root}/${n}`)).join(" ")}`).catch(() => {
|
|
2314
|
+
});
|
|
2315
|
+
const survivors = [];
|
|
2316
|
+
for (const name of doomed) {
|
|
2317
|
+
if (await this.executor.exists(`${root}/${name}`)) survivors.push(name);
|
|
2318
|
+
}
|
|
2319
|
+
return survivors;
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Is this an existing directory with no entries? Any inconclusive answer (not a
|
|
2323
|
+
* directory, unreadable, exec failed) returns false — a probe that can't read
|
|
2324
|
+
* must never be the thing that fails a deploy.
|
|
2325
|
+
*/
|
|
2326
|
+
async isEmptyDir(path) {
|
|
2327
|
+
const p = sq(path);
|
|
2328
|
+
const out = await this.executor.exec(
|
|
2329
|
+
`if [ -d ${p} ]; then echo DIR; ls -A ${p} 2>/dev/null | head -1; fi; true`
|
|
2330
|
+
).catch(() => null);
|
|
2331
|
+
if (out === null) return false;
|
|
2332
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2333
|
+
return lines.length === 1 && lines[0] === "DIR";
|
|
2334
|
+
}
|
|
2335
|
+
async stop(containerId) {
|
|
2336
|
+
const sv = await this.supervisor();
|
|
2337
|
+
await sv.stop(containerId);
|
|
2338
|
+
}
|
|
2339
|
+
async start(containerId) {
|
|
2340
|
+
const sv = await this.supervisor();
|
|
2341
|
+
if (await sv.isRunning(containerId)) return;
|
|
2342
|
+
await sv.start(containerId);
|
|
2343
|
+
}
|
|
2344
|
+
async restart(containerId) {
|
|
2345
|
+
const sv = await this.supervisor();
|
|
2346
|
+
await sv.restart(containerId);
|
|
2347
|
+
}
|
|
2348
|
+
async destroy(containerId) {
|
|
2349
|
+
if (isArtifactPathRef(containerId)) {
|
|
2350
|
+
await removeManagedArtifact(this.executor, containerId);
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const sv = await this.supervisor();
|
|
2354
|
+
await sv.destroy(containerId);
|
|
2355
|
+
}
|
|
2356
|
+
// ── Rollback primitives ──────────────────────────────────────────────
|
|
2357
|
+
//
|
|
2358
|
+
// Bare semantics:
|
|
2359
|
+
// The release dir at workDir/releases/<deploymentId> IS the artifact.
|
|
2360
|
+
// The supervisor unit is the activation. Rollback flips which
|
|
2361
|
+
// release the supervisor unit serves by stop/start sequencing.
|
|
2362
|
+
//
|
|
2363
|
+
// makeActive — stop `from`'s supervisor unit, then start `to`'s.
|
|
2364
|
+
// The release dirs are stable on disk; we're just changing which
|
|
2365
|
+
// unit is running. Matches the user's "mv path + reload" mental
|
|
2366
|
+
// model — the path doesn't physically move, but the active one
|
|
2367
|
+
// swaps via the supervisor.
|
|
2368
|
+
// archive — stop the supervisor unit. Release dir stays on disk
|
|
2369
|
+
// (the actual rollback-restorable artifact).
|
|
2370
|
+
// purge — destroy the supervisor unit + rm -rf the release dir.
|
|
2371
|
+
async makeActive(input) {
|
|
2372
|
+
if (input.from?.containerId) {
|
|
2373
|
+
try {
|
|
2374
|
+
await this.stop(input.from.containerId);
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (!input.to.containerId) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`Cannot make deployment ${input.to.id} active: supervisor unit is gone. Artifact has been purged.`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
await this.start(input.to.containerId);
|
|
2384
|
+
return { containerId: input.to.containerId };
|
|
2385
|
+
}
|
|
2386
|
+
async archive(deployment) {
|
|
2387
|
+
if (!deployment.containerId) return;
|
|
2388
|
+
try {
|
|
2389
|
+
await this.stop(deployment.containerId);
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async purge(deployment) {
|
|
2394
|
+
const failures = [];
|
|
2395
|
+
if (deployment.containerId) {
|
|
2396
|
+
await this.destroy(deployment.containerId).catch(
|
|
2397
|
+
(err) => failures.push(err)
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
const releaseDir = this.releaseDir(deployment.id);
|
|
2401
|
+
if (releaseDir !== deployment.containerId) {
|
|
2402
|
+
await removeManagedArtifact(this.executor, releaseDir).catch(
|
|
2403
|
+
(err) => failures.push(err)
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
if (failures.length === 1) throw failures[0];
|
|
2407
|
+
if (failures.length > 1) {
|
|
2408
|
+
throw new Error(
|
|
2409
|
+
failures.map((err) => err instanceof Error ? err.message : String(err)).join("; ")
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// ── Observability ──────────────────────────────────────────────────────
|
|
2414
|
+
async getContainerInfo(containerId) {
|
|
2415
|
+
const sv = await this.supervisor();
|
|
2416
|
+
const running = await sv.isRunning(containerId);
|
|
2417
|
+
return {
|
|
2418
|
+
containerId,
|
|
2419
|
+
status: running ? "running" : "stopped"
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
async getRuntimeLogs(containerId, tail) {
|
|
2423
|
+
const sv = await this.supervisor();
|
|
2424
|
+
return sv.getLogs(containerId, tail);
|
|
2425
|
+
}
|
|
2426
|
+
async streamRuntimeLogs(containerId, onLog, opts) {
|
|
2427
|
+
const sv = await this.supervisor();
|
|
2428
|
+
return sv.streamLogs(containerId, onLog, opts);
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* CPU / memory / disk-IO for the deployment's process.
|
|
2432
|
+
*
|
|
2433
|
+
* `containerId` is the DEPLOYMENT id here, not a docker container id — that's the
|
|
2434
|
+
* bare runtime's convention throughout (it's what the supervisor keys units and
|
|
2435
|
+
* PID files on).
|
|
2436
|
+
*
|
|
2437
|
+
* The supervisor owns the measurement because the identity differs: systemd has a
|
|
2438
|
+
* unit and a cgroup, nohup has a PID file. Both land on the same probe.
|
|
2439
|
+
*/
|
|
2440
|
+
async getUsage(containerId) {
|
|
2441
|
+
const sv = await this.supervisor();
|
|
2442
|
+
return sv.getUsage(containerId);
|
|
2443
|
+
}
|
|
2444
|
+
// ── Network ────────────────────────────────────────────────────────────
|
|
2445
|
+
async getContainerIp(_containerId) {
|
|
2446
|
+
return "127.0.0.1";
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
|
|
2450
|
+
export {
|
|
2451
|
+
STATIC_RELEASE_BASE,
|
|
2452
|
+
BareRuntime
|
|
2453
|
+
};
|