From 91738a464de95bfcaddcfc4ba3fcbf0f43ed6e23 Mon Sep 17 00:00:00 2001 From: Yohe-Am <56622350+Yohe-Am@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:11:34 +0300 Subject: [PATCH] refactor(envs,cli)!: envs first cli (#48) * refactor(envs): envs first CLI * wip: missing file * wip: wip * wip: wip 2 * feat(tests): basic tests * docs: some snippets for `README.md` * doc: typo * fix: `Deno.Command` troubles * fix: cross platform shell fn for getting ctime * fix: `!` as instId separator * fix(tests): missing flag * wip: wip * feat: `ghjk p resolve` * refactor: polish CLI code * wip: clearEnv fix * fix: vendor dax patch * fix: forgotten change * fix: use `@ghjk/dax` fork * fix: remove vendor dir from Dockerfile --- .ghjk/deno.lock | 114 ++++- .ghjk/lock.json | 105 ++--- .github/workflows/tests.yml | 2 + .pre-commit-config.yaml | 5 + README.md | 130 +++++- check.ts | 2 + deno.jsonc | 12 +- deno.lock | 287 +++++++----- deps/cli.ts | 2 +- deps/common.ts | 7 +- ghjk.ts | 7 +- host/deno.ts => ghjkfiles/deno/mod.ts | 2 +- {host => ghjkfiles/deno}/worker.ts | 6 +- ghjkfiles/mod.ts | 603 ++++++++++++++++++++++++++ host/mod.ts | 401 ++++++++++------- install.ts | 24 +- install/ghjk.sh | 5 +- install/hook.fish | 49 ++- install/hook.sh | 55 ++- install/mod.ts | 51 ++- mod.ts | 568 +----------------------- modules/envs/mod.ts | 300 +++++++++++-- modules/envs/posix.ts | 44 +- modules/envs/reducer.ts | 6 + modules/envs/types.ts | 1 + modules/mod.ts | 16 +- modules/ports/ambient.ts | 2 +- modules/ports/inter.ts | 49 +++ modules/ports/mod.ts | 136 +++--- modules/ports/reducers.ts | 41 +- modules/ports/sync.ts | 189 +------- modules/ports/types.ts | 47 +- modules/ports/worker.ts | 9 +- modules/tasks/exec.ts | 12 +- modules/tasks/mod.ts | 15 +- ports/asdf.ts | 8 +- ports/asdf_plugin_git.ts | 2 +- ports/cargobi.ts | 4 +- ports/cpy_bs.ts | 2 +- ports/dummy.ts | 24 +- ports/infisical.ts | 1 - ports/jq_ghrel.ts | 2 +- ports/meta_cli_ghrel.ts | 2 +- ports/mold.ts | 2 +- ports/node.ts | 2 +- ports/npmi.ts | 2 +- ports/pipi.ts | 2 +- ports/ruff.ts | 2 +- ports/rust.ts | 2 +- ports/rustup.ts | 2 +- ports/temporal_cli.ts | 1 - ports/terraform.ts | 1 - ports/wasmedge.ts | 2 +- tests/ambient.ts | 3 +- tests/envs.ts | 204 +++++++++ tests/hooks.ts | 8 +- tests/ports.ts | 28 +- tests/tasks.ts | 18 +- tests/test-alpine.Dockerfile | 4 +- tests/test.Dockerfile | 4 +- tests/utils.ts | 80 ++-- utils/logger.ts | 7 +- utils/mod.ts | 75 +++- utils/unarchive.ts | 12 +- 64 files changed, 2430 insertions(+), 1380 deletions(-) rename host/deno.ts => ghjkfiles/deno/mod.ts (97%) rename {host => ghjkfiles/deno}/worker.ts (97%) create mode 100644 ghjkfiles/mod.ts create mode 100644 modules/ports/inter.ts create mode 100644 tests/envs.ts diff --git a/.ghjk/deno.lock b/.ghjk/deno.lock index 885e603d..93382d85 100644 --- a/.ghjk/deno.lock +++ b/.ghjk/deno.lock @@ -1,5 +1,22 @@ { "version": "3", + "packages": { + "specifiers": { + "npm:zod-validation-error": "npm:zod-validation-error@3.1.0_zod@3.22.4" + }, + "npm": { + "zod-validation-error@3.1.0_zod@3.22.4": { + "integrity": "sha512-zujS6HqJjMZCsvjfbnRs7WI3PXN39ovTcY1n8a+KTm4kOH0ZXYsNiJkH1odZf4xZKMkBDL7M2rmQ913FCS1p9w==", + "dependencies": { + "zod": "zod@3.22.4" + } + }, + "zod@3.22.4": { + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "dependencies": {} + } + } + }, "remote": { "https://deno.land/std@0.116.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", "https://deno.land/std@0.116.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", @@ -242,6 +259,14 @@ "https://deno.land/std@0.213.0/url/join.ts": "00c7e9088cafaa24963ce4081119e58b3afe2c58f033701383f359ea02620dd2", "https://deno.land/std@0.213.0/url/mod.ts": "e2621f6a0db6fdbe7fbbd240064095bb203014657e5e1ab81db1c44d80dce6c9", "https://deno.land/std@0.213.0/url/normalize.ts": "6328c75df0fab300f74bc4a1c255062a0db882240e15ab646606d0009e7e40d7", + "https://deno.land/std@0.221.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.221.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.221.0/console/_data.json": "cf2cc9d039a192b3adbfe64627167c7e6212704c888c25c769fc8f1709e1e1b8", + "https://deno.land/std@0.221.0/console/_run_length.ts": "7da8642a0f4f41ac27c0adb1364e18886be856c1d08c5cce6c6b5c00543c8722", + "https://deno.land/std@0.221.0/console/unicode_width.ts": "d92f085c0ab9c7ab171e4e7862dfd9d3a36ffd369939be5d3e1140ec58bc820f", + "https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/std@0.221.0/text/closest_string.ts": "8a91ee8b6d69ff96addcb7c251dad53b476ac8be9c756a0ef786abe9e13a93a5", + "https://deno.land/std@0.221.0/text/levenshtein_distance.ts": "24be5cc88326bbba83ca7c1ea89259af0050cffda2817ff3a6d240ad6495eae2", "https://deno.land/std@0.76.0/encoding/base64.ts": "b1d8f99b778981548457ec74bc6273ad785ffd6f61b2233bd5b30925345b565d", "https://deno.land/std@0.76.0/encoding/hex.ts": "07a03ba41c96060a4ed4ba272e50b9e23f3c5b3839f4b069cdebc24d57434386", "https://deno.land/std@0.76.0/hash/_wasm/hash.ts": "005f64c4d9343ecbc91e0da9ae5e800f146c20930ad829bbb872c5c06bd89c5f", @@ -306,6 +331,65 @@ "https://deno.land/x/cliffy@v1.0.0-rc.3/table/deps.ts": "1226c4d39d53edc81d7c3e661fb8a79f2e704937c276c60355cd4947a0fe9153", "https://deno.land/x/cliffy@v1.0.0-rc.3/table/row.ts": "79eb1468aafdd951e5963898cdafe0752d4ab4c519d5f847f3d8ecb8fe857d4f", "https://deno.land/x/cliffy@v1.0.0-rc.3/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_errors.ts": "d78e1b4d69d84b8b476b5f3c0b028e3906d48f21b8f1ca1d36d5abe9ccfe48bc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_spread.ts": "0cc6eb70a6df97b5d7d26008822d39f3e8a1232ee0a27f395aa19e68de738245", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_type_utils.ts": "820004a59bc858e355b11f80e5b3ff1be2c87e66f31f53f253610170795602f0", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_utils.ts": "fa0e88cc4215b18554a7308e8e2ae3a12be0fb91c54d1473c54c530dbd4adfcb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/command.ts": "83cbece11c1459d5bc5add32c3cad0bf49e92c4ddd3ef00f22f80efdae30994e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_bash_completions_generator.ts": "0c6cb1df4d378d22f001155781d97a9c3519fd10c48187a198fef2cc63b0f84a", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_fish_completions_generator.ts": "8ba4455f7f76a756e05c3db4ce35332b2951af65a2891f2750b530e06880f495", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_zsh_completions_generator.ts": "9df79fbac17a32b9645d01628c41a2bfd295d7976b87b0ae235f50a9c8975fbc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/bash.ts": "53fe78994eb2359110dc4fa79235bdd86800a38c1d6b1c4fe673c81756f3a0e2", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/completions_command.ts": "506f97f1c6b0b1c3e9956e5069070028b818942310600d4157f64c9b644d3c49", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/fish.ts": "6f0b44b4067740b2931c9ec8863b6619b1d3410fea0c5a3988525a4c53059197", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/mod.ts": "8dda715ca25f3f66d5ec232b76d7c9a96dd4c64b5029feff91738cc0c9586fb1", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/zsh.ts": "f1263c3946975e090d4aadc8681db811d86b52a8ae680f246e03248025885c21", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/deps.ts": "a58ea2fa4e2ed9b39bb8dd8c35dd0498c74f05392517ff230a9a4d04c4c766b7", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/_help_generator.ts": "98619da83ff25523280a6fdcad89af3f13a6fafefc81b71f8230f3344b5ff2c5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/help_command.ts": "fbbf0c0827dd21d3cec7bcc68c00c20b55f53e2b621032891b9d23ac4191231c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/mod.ts": "8369b292761dcc9ddaf41f2d34bfb06fb6800b69efe80da4fc9752c3b890275b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts": "4b708df1b97152522bee0e3828f06abbbc1d2250168910e5cf454950d7b7404b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/type.ts": "f588f5d9635b79100044e62aced4b00e510e75b83801f9b089c40c2d98674de2", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types.ts": "bc9ff7459b9cc1079eeb95ff101690a51b4b4afa4af5623340076ee361d08dbb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/enum.ts": "8a7cd2898e03089234083bb78c8b1d9b7172254c53c32d4710321638165a48ec", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/_check_version.ts": "6cfa7dc26bc0dc46381500e8d4b130fb224f4c5456152dada15bd3793edca89b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/mod.ts": "4eff69c489467be17dea27fb95a795396111ee385d170ac0cbcc82f0ea38156c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider.ts": "c23253334097dc4b8a147ccdeb3aa44f5a95aa953a6386cb5396f830d95d77a5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/upgrade_command.ts": "27191f4b1ce93581b6d5ee2fff6003fe4fca437f476ecb98b6eae92f2b4d0716", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_utils.ts": "25e519ce1f35acc8b43c75d1ca1c4ab591e7dab08327b7b408705b591e27d8bd", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_validate_flags.ts": "e60b9038c0136ab7e6bd1baf0e993a07bf23f18afbfb6e12c59adf665a622957", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deps.ts": "bed26afff36eeb25509440edec9d5d141b3411e08cc7a90e38a370969b5166bb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/flags.ts": "3e62c4a9756b5705aada29e7e94847001356b3a83cd18ad56f4207387a71cf51", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types.ts": "9e2f75edff2217d972fc711a21676a59dfd88378da2f1ace440ea84c07db1dcc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/_layout.ts": "73a9bcb8a87b3a6817c4c9d2a31a21b874a7dd690ade1c64c9a1f066d628d626", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/_utils.ts": "13390db3f11977b7a4fc1202fa8386be14696b475a7f46a65178354f9a6640b7", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/border.ts": "5c6e9ef5078c6930169aacb668b274bdbb498461c724a7693ac9270fe9d3f5d5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/cell.ts": "65e3ee699c3cebeb4d4d44e8f156e37a8532a0f317359d73178a95724d3f9267", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/column.ts": "cf14009f2cb14bad156f879946186c1893acdc6a2fee6845db152edddb6a2714", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/consume_words.ts": "369d065dbf7f15c664ea8523e0ef750fb952aea6d88e146c375e64aec9503052", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/deps.ts": "cbb896e8d7a6b5e3c2b9dda7d16638c202d9b46eb738c2dae1fa9480d8091486", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/row.ts": "79eb1468aafdd951e5963898cdafe0752d4ab4c519d5f847f3d8ecb8fe857d4f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684", "https://deno.land/x/dax@0.38.0/mod.ts": "3a5d7e6ac12547feec5d3c0c96717f14276891a3802fbbc73e5901e4f20eb08d", "https://deno.land/x/dax@0.38.0/src/command.ts": "f20135ef7188a0fc9f773d50e88775dee8653044a7f536fb2fb885b293c26ec4", "https://deno.land/x/dax@0.38.0/src/command_handler.ts": "a9e40f0f1ec57318e62904b5785fede82dcdf1101922ebb3ebfad8f1c4d9c8df", @@ -344,6 +428,9 @@ "https://deno.land/x/dax@0.38.0/src/result.ts": "719a9b4bc6bafeec785106744381cd5f37927c973334fcba6a33b6418fb9e7be", "https://deno.land/x/dax@0.38.0/src/shell.ts": "9b59a63de62003a0575f9c3300b5fff83cd7e5487582eceaa5f071a684d75e0e", "https://deno.land/x/deep_eql@v5.0.1/index.js": "60e1547b99d4ae08df387067c2ac0a1b9ab42f212f0d8a11b8b0b61270d2b1c4", + "https://deno.land/x/diff_kit@v2.0.4/mod.ts": "3d88f6b8132feabe4c0863a5c65fdad05d44d52488de91205fc76abcbfd2eadd", + "https://deno.land/x/diff_kit@v2.0.4/private/diff.ts": "bc270998702ba73c8d2b1810feb54d3973615ce56a33d2ec64432e698f2f2613", + "https://deno.land/x/diff_kit@v2.0.4/private/diff_handler.ts": "2f96831bde217d6a84691abfe7d4580057aee4e9fe1cf753101a2eb703cef9aa", "https://deno.land/x/foras@v2.1.4/src/deno/mod.ts": "c350ea5f32938e6dcb694df3761615f316d730dafc57440e9afd5f36f8e309fd", "https://deno.land/x/foras@v2.1.4/src/deno/mods/mod.ts": "cc099bbce378f3cdaa94303e8aff2611e207442e5ac2d5161aba636bb4a95b46", "https://deno.land/x/foras@v2.1.4/wasm/pkg/foras.js": "06f8875b456918b9671d52133f64f3047f1c95540feda87fdd4a55ba3d30091d", @@ -373,6 +460,31 @@ "https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", "https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e", "https://esm.sh/jszip@3.7.1": "f3872a819b015715edb05f81d973b5cd05d3d213d8eb28293ca5471fe7a71773", - "https://esm.sh/v135/jszip@3.7.1/denonext/jszip.mjs": "d31d7f9e0de9c6db3c07ca93f7301b756273d4dccb41b600461978fc313504c9" + "https://esm.sh/v135/jszip@3.7.1/denonext/jszip.mjs": "d31d7f9e0de9c6db3c07ca93f7301b756273d4dccb41b600461978fc313504c9", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/deps/cli.ts": "4eacc555cf80686b487e7502db63a4cfbc2060a7b847d15b14cf1cc008a3b65c", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/deps/common.ts": "46d30782086ccc79e4a2633fe859723e7686ebc5adb4101e76c4bf2d6d2e94ff", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/host/deno.ts": "330c62197c7af0a01d3ec96705367b789d538f3c820b730c63bb2820fabda7d7", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/host/mod.ts": "2bc9f273262e1c4fb434b1a0389f24464f8b986816ce9480e8e2d63d910e8253", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/host/types.ts": "22c06b190172d08092717ad788ed04b050af58af0cf3f8c78b1511984101e9e4", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/main.ts": "8d6985e59db0b5baf67c9dc330bf8b25ad556341b9ef6088038e8ebb37ed75e5", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/mod.ts": "6aa0b765ce5684842ea531e026926836ffde7d2513e62457bffe9cb4ec7eb0df", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/ambient.ts": "25623410c535e2bfaf51fca1e582e7325a00a7690d5b5e763a12be9407f619cf", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/base.ts": "8ef8a8de372420bddcd63a1b363937f43d898059e99478a58621e8432bcd5891", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/db.ts": "3f4541d6874c434f2f869774a17fd41c3d86914ed190d412e2f63f564b58ce95", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/mod.ts": "e38ad2d3599b6a5522da436b52e5945bb85cabba2aca27f633eae43e465b5794", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/sync.ts": "46447c2c51c085193f567ddcd2451b14bb33ee2d761edeb91a6153e2ba642f42", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/types.ts": "b3967d9d75def187b3b55f2b0b1357c9cb69a70e475a9280fc66717193b8b43c", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/types/platform.ts": "0ecffeda71919293f9ffdb6c564ddea4f23bc85c4e640b08ea78225d34387fdc", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/ports/worker.ts": "25c01e3afddd97d48af89d9c97a9a5188e7db09fceb26a69eac4dabacd8ac4fc", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/std.ts": "ddb2c134c080bb0e762a78f2f2edd69536991cc4257bd29a6fc95944b2f105a9", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/tasks/deno.ts": "f988a4d1062364b99272087fa0c7d54e699944ead3790c5b83140577bda089de", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/tasks/exec.ts": "7a07f2cce79fe16e86f0b74df6d57f0160bac75a8c6d58a03f2883a5ecccddf0", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/tasks/mod.ts": "0edbe1ce953a44b6b0fd45aa9c9dd52c11b12053eef21307eac3b24b6db4745e", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/tasks/types.ts": "536495a17c7a917bdd1c316ecc98ce2947b4959a713f92a175d372196dcaafc0", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/modules/types.ts": "b44609942d7ad66c925c24485057c5b4b2ffcad20c0a94e14dc6af34cf9e8241", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/setup_logger.ts": "f8a206bda0595497d6f4718032d4a959000b32ef3346d4b507777eec6a169458", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/utils/logger.ts": "86fdf651123d00ea1081bf8001ed9039cd41a79940e6ebadb8484952ab390e73", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/utils/mod.ts": "1ee68d9390259c065144c10663f6e360d29aec36db2af38d02647e304eeeaedc", + "https://raw.githubusercontent.com/metatypedev/ghjk/423d38e/utils/url.ts": "e1ada6fd30fc796b8918c88456ea1b5bbd87a07d0a0538b092b91fd2bb9b7623" } } diff --git a/.ghjk/lock.json b/.ghjk/lock.json index 6e0b86c9..509afa1c 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -7,23 +7,23 @@ "configResolutions": { "95dbc2b8c604a5996b88c5b1b4fb0c10b3e0d9cac68f57eb915b012c44288e93": { "version": "v0.2.61", - "depConfigs": {}, + "buildDepConfigs": {}, "portRef": "act_ghrel@0.1.0" }, "076a5b8ee3bdc68ebf20a696378458465042bb7dc1e49ac2dc98e5fa0dab3e25": { "version": "3.7.0", - "depConfigs": { + "buildDepConfigs": { "cpy_bs_ghrel": { - "version": "3.12.1", - "depConfigs": { + "version": "3.12.3", + "buildDepConfigs": { "tar_aa": { - "version": "1.35", - "depConfigs": {}, + "version": "1.34", + "buildDepConfigs": {}, "portRef": "tar_aa@0.1.0" }, "zstd_aa": { "version": "v1.5.5,", - "depConfigs": {}, + "buildDepConfigs": {}, "portRef": "zstd_aa@0.1.0" } }, @@ -34,51 +34,34 @@ "packageName": "pre-commit" }, "84ecde630296f01e7cb8443c58d1596d668c357a0d9837c0a678b8a541ed0a39": { - "version": "3.12.1", - "depConfigs": { + "version": "3.12.3", + "buildDepConfigs": { "tar_aa": { - "version": "1.35", - "depConfigs": {}, + "version": "1.34", + "buildDepConfigs": {}, "portRef": "tar_aa@0.1.0" }, "zstd_aa": { "version": "v1.5.5,", - "depConfigs": {}, + "buildDepConfigs": {}, "portRef": "zstd_aa@0.1.0" } }, "portRef": "cpy_bs_ghrel@0.1.0" }, "9e3fa7742c431c34ae7ba8d1e907e50c937ccfb631fb4dcfb7a1773742abe267": { - "version": "1.35", - "depConfigs": {}, + "version": "1.34", + "buildDepConfigs": {}, "portRef": "tar_aa@0.1.0" }, "4f16c72030e922711abf15474d30e3cb232b18144beb73322b297edecfcdb86f": { "version": "v1.5.5,", - "depConfigs": {}, + "buildDepConfigs": {}, "portRef": "zstd_aa@0.1.0" }, - "c58811a17645c26b5a55a2de02f930945e04adf37e408846bf6b5c72bf707146": { - "version": "3.12.0", - "depConfigs": { - "tar_aa": { - "version": "1.35", - "depConfigs": {}, - "portRef": "tar_aa@0.1.0" - }, - "zstd_aa": { - "version": "v1.5.5,", - "depConfigs": {}, - "portRef": "zstd_aa@0.1.0" - } - }, - "portRef": "cpy_bs_ghrel@0.1.0", - "releaseTag": "20231002" - }, "a79698808eea53aedd8e83387b2f44e90a1a48d76193c5ccf0fc6efe29bd70f6": { "version": "v26.1", - "depConfigs": {}, + "buildDepConfigs": {}, "portRef": "protoc_ghrel@0.1.0" } } @@ -99,19 +82,19 @@ "ghjkEnvProvInstSet___main": { "installs": [ "c4cf06e095dadfbdd5e26070bc2b7baffc5ff45f", - "9283b97b5499e8da4dcfb7f14c1306c25e8e8a44", - "7d7b0f4b9ec5375688fceab016687f3ac3fbc94c" + "2a0176fec803325cc31d4a9b15f77f4e07938cc4", + "b6c49b375643a285e20b6ec0f7a692214bd0f392" ], - "allowedDeps": "48a429761f3837562b097b47afe07601ba4ffca5" + "allowedDeps": "3c71ccb92f3785a685b27d7b897fef4b80ad6b24" }, "ghjkEnvProvInstSet___test": { "installs": [ "aa103d26454710ca5d7f43358123341380389864", "c4cf06e095dadfbdd5e26070bc2b7baffc5ff45f", - "9283b97b5499e8da4dcfb7f14c1306c25e8e8a44", - "7d7b0f4b9ec5375688fceab016687f3ac3fbc94c" + "2a0176fec803325cc31d4a9b15f77f4e07938cc4", + "b6c49b375643a285e20b6ec0f7a692214bd0f392" ], - "allowedDeps": "48a429761f3837562b097b47afe07601ba4ffca5" + "allowedDeps": "3c71ccb92f3785a685b27d7b897fef4b80ad6b24" } } } @@ -128,6 +111,7 @@ "config": { "envs": { "main": { + "desc": "the default default environment.", "provides": [ { "ty": "ghjk.ports.InstallSetRef", @@ -165,7 +149,7 @@ "moduleSpecifier": "file:///ports/act.ts" } }, - "9283b97b5499e8da4dcfb7f14c1306c25e8e8a44": { + "2a0176fec803325cc31d4a9b15f77f4e07938cc4": { "port": { "ty": "denoWorker@v1", "name": "pipi_pypi", @@ -190,7 +174,7 @@ "aarch64-android" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "cpy_bs_ghrel" } @@ -199,7 +183,7 @@ }, "packageName": "pre-commit" }, - "7d7b0f4b9ec5375688fceab016687f3ac3fbc94c": { + "b6c49b375643a285e20b6ec0f7a692214bd0f392": { "port": { "ty": "denoWorker@v1", "name": "cpy_bs_ghrel", @@ -212,7 +196,7 @@ "aarch64-windows" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "tar_aa" }, @@ -221,8 +205,7 @@ } ], "moduleSpecifier": "file:///ports/cpy_bs.ts" - }, - "releaseTag": "20231002" + } }, "e0d1f160d2d7755765f6f01a27a0c33a02ff98d2": { "manifest": { @@ -354,7 +337,7 @@ "portRef": "zstd_aa@0.1.0" } }, - "8f14cde4f25c276d5e54538d91a6ac6d3eec3e8d": { + "5314c90de340dfd1ef21421dcbdcba726b4d03b9": { "manifest": { "ty": "denoWorker@v1", "name": "rustup_rustlang", @@ -369,7 +352,7 @@ "x86_64-netbsd" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "git_aa" } @@ -385,7 +368,7 @@ "portRef": "rustup_rustlang@0.1.0" } }, - "9fc8f32a0f79253defdb8845e2d6a4df69b526b9": { + "ebba9b42698f7f065a359575f195153ca1adba7b": { "manifest": { "ty": "denoWorker@v1", "name": "rust_rustup", @@ -410,7 +393,7 @@ "aarch64-android" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "rustup_rustlang" } @@ -457,7 +440,7 @@ "portRef": "pnpm_ghrel@0.1.0" } }, - "a36b37f4eda81bf51a50d00362637690c7fea473": { + "16e0e281e0f961fcc805896fc146d2c011c8d694": { "manifest": { "ty": "denoWorker@v1", "name": "asdf_plugin_git", @@ -470,7 +453,7 @@ "x86_64-windows" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "git_aa" } @@ -486,7 +469,7 @@ "portRef": "asdf_plugin_git@0.1.0" } }, - "5843605c861f0b7307c0192a1628c3823fe28ed9": { + "65ca6fb1b829a92d6423b3ea701d9602d84cf6f8": { "manifest": { "ty": "denoWorker@v1", "name": "node_org", @@ -499,7 +482,7 @@ "x86_64-windows" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "tar_aa" } @@ -510,7 +493,7 @@ "portRef": "node_org@0.1.0" } }, - "7a33163826283c47b52964a23b87a4762662c746": { + "d82c92542f0ed9c49a0383922c1d968ba88f0c4b": { "manifest": { "ty": "denoWorker@v1", "name": "cpy_bs_ghrel", @@ -523,7 +506,7 @@ "aarch64-windows" ], "version": "0.1.0", - "deps": [ + "buildDeps": [ { "name": "tar_aa" }, @@ -537,19 +520,19 @@ "portRef": "cpy_bs_ghrel@0.1.0" } }, - "48a429761f3837562b097b47afe07601ba4ffca5": { + "3c71ccb92f3785a685b27d7b897fef4b80ad6b24": { "tar_aa": "e0d1f160d2d7755765f6f01a27a0c33a02ff98d2", "git_aa": "9d26d0d90f6ecdd69d0705a042b01a344aa626ee", "curl_aa": "3c447f912abf18883bd05314f946740975ee0dd3", "unzip_aa": "dfb0f5e74666817e6ab8cbceca0c9da271142bca", "zstd_aa": "d9122eff1fe3ef56872e53dae725ff3ccb37472e", - "rustup_rustlang": "8f14cde4f25c276d5e54538d91a6ac6d3eec3e8d", - "rust_rustup": "9fc8f32a0f79253defdb8845e2d6a4df69b526b9", + "rustup_rustlang": "5314c90de340dfd1ef21421dcbdcba726b4d03b9", + "rust_rustup": "ebba9b42698f7f065a359575f195153ca1adba7b", "cargo_binstall_ghrel": "45999e7561d7f6a661191f58ee35e67755d375e0", "pnpm_ghrel": "b80f4de14adc81c11569bf5f3a2d10b92ad5f1a7", - "asdf_plugin_git": "a36b37f4eda81bf51a50d00362637690c7fea473", - "node_org": "5843605c861f0b7307c0192a1628c3823fe28ed9", - "cpy_bs_ghrel": "7a33163826283c47b52964a23b87a4762662c746" + "asdf_plugin_git": "16e0e281e0f961fcc805896fc146d2c011c8d694", + "node_org": "65ca6fb1b829a92d6423b3ea701d9602d84cf6f8", + "cpy_bs_ghrel": "d82c92542f0ed9c49a0383922c1d968ba88f0c4b" }, "aa103d26454710ca5d7f43358123341380389864": { "port": { diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ceab6301..fbeda562 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,8 @@ jobs: with: deno-version: ${{ env.DENO_VERSION }} - uses: pre-commit/action@v3.0.1 + env: + SKIP: ghjk-resolve test-e2e: runs-on: "${{ matrix.os }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9790a165..69817abc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,11 @@ repos: - commit-msg - repo: local hooks: + - id: ghjk-resolve + name: Ghjk resolve + language: system + entry: bash -c 'deno run --unstable -A main.ts p resolve' + pass_filenames: false - id: deno-fmt name: Deno format language: system diff --git a/README.md b/README.md index 3c75880a..c15ecf6e 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,139 @@ curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/main/install.sh | In your project, create a configuration file `ghjk.ts`: ```ts +// NOTE: All the calls in your `ghjk.ts` file are ultimately modifying the ghjk object +// exported here. export { ghjk } from "https://raw.githubusercontent.com/metatypedev/ghjk/main/mod.ts"; -import * as ghjk from "https://raw.githubusercontent.com/metatypedev/ghjk/main/mod.ts"; +import { + install, + task, +} from "https://raw.githubusercontent.com/metatypedev/ghjk/main/mod.ts"; import node from "https://raw.githubusercontent.com/metatypedev/ghjk/main/ports/node.ts"; -ghjk.install( +// install programs into your env +install( node({ version: "14.17.0" }), ); + +// write simple scripts and execute them through +// `$ ghjk x greet` +task("greet", async ({ $, argv: [name] }) => { + await $`echo Hello ${name}!`; +}); +``` + +Use the following command to then access your environment: + +```shell +$ ghjk sync +``` + +### Environments + +Ghjk is primarily configured through constructs called "environments" or "envs" +for short. They serve as recipes for making reproducable (mostly) posix shells. + +```ts +export { ghjk } from "https://raw.githubusercontent.com/metatypedev/ghjk/mod.ts"; +import * as ghjk from "https://raw.githubusercontent.com/metatypedev/ghjk/mod.ts"; +import * as ports from "https://raw.githubusercontent.com/metatypedev/ghjk/ports/mod.ts"; + +// top level `install`s go to the `main` env +ghjk.install(ports.protoc()); +ghjk.install(ports.rust()); + +// the previous block is equivalent to +ghjk.env("main", { + installs: [ + ports.protoc(), + ports.rust(), + ], +}); + +ghjk.env("dev", { + // by default, all envs are additively based on `main` + // pass false here to make env indiependent. + base: false, + // envs can specify standard env vars + vars: { CARGO_TARGET_DIR: "my_target" }, + installs: [ + ports.cargobi({ crateName: "cargo-insta" }), + ports.act(), + ], +}); + +ghjk.env({ + name: "docker", + desc: "for Dockerfile usage", + // NOTE: env references are order-independent + base: "ci", + installs: [ + ports.cargobi({ crateName: "cargo-chef" }), + ports.zstd(), + ], +}); + +// builder syntax is also availaible +ghjk.env("ci") + .var("CI", "1") + .install( + ports.opentofu_ghrel(), + ); + +// each task describes it's own env as well +ghjk.task({ + name: "run", + base: "dev", + fn: () => console.log("online"), +}); +``` + +Once you've configured your environments: + +- `$ ghjk envs cook $name` to reify and install an environment. +- `$ ghjk envs activate $name` to switch to an environment. +- And **most** usefully, `$ ghjk sync $name` to cook and _then_ activate an + environment. + - If shell is already in the specified env, it only does cooking. + - Make sure to `sync` or `cook` your envs after changes. +- If no `$name` is provided, most of these commands will operate on the default + or currently active environment. + +### Ports + +TBD: this feature is in development. + +### Tasks + +TBD: this feature is still in development. + +### Secure configs + +Certain options are configured through the `secureConfig` object. + +```ts +import { env, stdSecureConfig } from "https://.../ghjk/mod.ts"; +import * as ports from "https://.../ports/mod.ts"; + +env("trueBase") + .install( + ports.act(), + ports.pipi({ packageName: "ruff" }), + ); + +env("test").vars({ DEBUG: 1 }); + +// `stdSecureConfig` is a quick way to make an up to spec `secureConfig`. +export const secureConfig = stdSecureConfig({ + defaultBaseEnv: "trueBase", + defaultEnv: "test", + // by default, nodejs, python and other runtime + // ports are not allowed to be used + // during the build process of other ports. + // Disable this security measure here. + // (More security features inbound!.) + enableRuntimes: true, +}); ``` ## Development diff --git a/check.ts b/check.ts index e2341541..f82503a3 100755 --- a/check.ts +++ b/check.ts @@ -7,8 +7,10 @@ import { $ } from "./utils/mod.ts"; const files = (await Array.fromAsync( $.path(import.meta.url).parentOrThrow().expandGlob("**/*.ts", { exclude: [ + "play.ts", ".ghjk/**", ".deno-dir/**", + "vendor/**", ], }), )).map((ref) => ref.path.toString()); diff --git a/deno.jsonc b/deno.jsonc index 801f17cc..dafb6678 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "tasks": { - "test": "GHJK_LOG=debug deno test --parallel --unstable-worker-options --unstable-kv -A tests/*", + "test": "GHJK_LOG=info deno test --parallel --unstable-worker-options --unstable-kv -A tests/*", "cache": "deno cache deps/*", "check": "deno run -A check.ts" }, @@ -8,16 +8,22 @@ "exclude": [ "**/*.md", ".ghjk/**", - ".deno-dir/**" + ".deno-dir/**", + "vendor/**" ] }, "lint": { "exclude": [ ".deno-dir/**", "ghjk.ts", - "play.ts" + "play.ts", + "vendor/**" ], "rules": { + "include": [ + "no-console", + "no-sync-fn-in-async-fn" + ], "exclude": [ "no-explicit-any" ] diff --git a/deno.lock b/deno.lock index b2acfc4a..60d8a39e 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,108 @@ { "version": "3", + "packages": { + "specifiers": { + "jsr:@david/dax@0.40.1": "jsr:@david/dax@0.40.1", + "jsr:@david/which@0.3": "jsr:@david/which@0.3.0", + "jsr:@ghjk/dax@0.40.2-alpha-ghjk": "jsr:@ghjk/dax@0.40.2-alpha-ghjk", + "jsr:@std/assert@^0.221.0": "jsr:@std/assert@0.221.0", + "jsr:@std/bytes@^0.221.0": "jsr:@std/bytes@0.221.0", + "jsr:@std/fmt@^0.221.0": "jsr:@std/fmt@0.221.0", + "jsr:@std/fs@0.221.0": "jsr:@std/fs@0.221.0", + "jsr:@std/io@0.221.0": "jsr:@std/io@0.221.0", + "jsr:@std/io@^0.221.0": "jsr:@std/io@0.221.0", + "jsr:@std/path@0.221.0": "jsr:@std/path@0.221.0", + "jsr:@std/path@^0.221.0": "jsr:@std/path@0.221.0", + "jsr:@std/streams@0.221.0": "jsr:@std/streams@0.221.0", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:zod-validation-error": "npm:zod-validation-error@3.1.0_zod@3.23.3", + "npm:zod-validation-error@3.2.0": "npm:zod-validation-error@3.2.0_zod@3.23.3" + }, + "jsr": { + "@david/dax@0.40.1": { + "integrity": "0c71d32a0484d3904f586417995f8ec26d45144f0eba95d3e5bb03b640b6df59", + "dependencies": [ + "jsr:@david/which@0.3", + "jsr:@std/fmt@^0.221.0", + "jsr:@std/fs@0.221.0", + "jsr:@std/io@0.221.0", + "jsr:@std/path@0.221.0", + "jsr:@std/streams@0.221.0" + ] + }, + "@david/which@0.3.0": { + "integrity": "6bdb62c40ac90edcf328e854fa8103a8db21e7c326089cbe3c3a1cf7887d3204" + }, + "@ghjk/dax@0.40.2-alpha-ghjk": { + "integrity": "87bc93e9947779cb2f3922fe277e21ea8c716de804b2627f80ba9e7bc3d0d019", + "dependencies": [ + "jsr:@david/which@0.3", + "jsr:@std/fmt@^0.221.0", + "jsr:@std/fs@0.221.0", + "jsr:@std/io@0.221.0", + "jsr:@std/path@0.221.0", + "jsr:@std/streams@0.221.0" + ] + }, + "@std/assert@0.221.0": { + "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/fmt@0.221.0": { + "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" + }, + "@std/fs@0.221.0": { + "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", + "dependencies": [ + "jsr:@std/assert@^0.221.0", + "jsr:@std/path@^0.221.0" + ] + }, + "@std/io@0.221.0": { + "integrity": "faf7f8700d46ab527fa05cc6167f4b97701a06c413024431c6b4d207caa010da", + "dependencies": [ + "jsr:@std/assert@^0.221.0", + "jsr:@std/bytes@^0.221.0" + ] + }, + "@std/path@0.221.0": { + "integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095", + "dependencies": [ + "jsr:@std/assert@^0.221.0" + ] + }, + "@std/streams@0.221.0": { + "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", + "dependencies": [ + "jsr:@std/io@^0.221.0" + ] + } + }, + "npm": { + "@types/node@18.16.19": { + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", + "dependencies": {} + }, + "zod-validation-error@3.1.0_zod@3.23.3": { + "integrity": "sha512-zujS6HqJjMZCsvjfbnRs7WI3PXN39ovTcY1n8a+KTm4kOH0ZXYsNiJkH1odZf4xZKMkBDL7M2rmQ913FCS1p9w==", + "dependencies": { + "zod": "zod@3.23.3" + } + }, + "zod-validation-error@3.2.0_zod@3.23.3": { + "integrity": "sha512-cYlPR6zuyrgmu2wRTdumEAJGuwI7eHVHGT+VyneAQxmRAKtGRL1/7pjz4wfLhz4J05f5qoSZc3rGacswgyTjjw==", + "dependencies": { + "zod": "zod@3.23.3" + } + }, + "zod@3.23.3": { + "integrity": "sha512-tPvq1B/2Yu/dh2uAIH2/BhUlUeLIUvAjr6dpL/75I0pCYefHgjhXk1o1Kob3kTU8C7yU1j396jFHlsVWFi9ogg==", + "dependencies": {} + } + } + }, "remote": { "https://deno.land/std@0.116.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", "https://deno.land/std@0.116.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", @@ -19,12 +122,6 @@ "https://deno.land/std@0.120.0/crypto/mod.ts": "5760510eaa0b250f78cce81ce92d83cf8c40e9bb3c3efeedd4ef1a5bb0801ef4", "https://deno.land/std@0.120.0/encoding/ascii85.ts": "b42b041e9c668afa356dd07ccf69a6b3ee49b9ae080fdf3b03f0ac3981f4d1e6", "https://deno.land/std@0.120.0/encoding/base64.ts": "0b58bd6477214838bf711eef43eac21e47ba9e5c81b2ce185fe25d9ecab3ebb3", - "https://deno.land/std@0.196.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.196.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.196.0/console/_data.json": "cf2cc9d039a192b3adbfe64627167c7e6212704c888c25c769fc8f1709e1e1b8", - "https://deno.land/std@0.196.0/console/_rle.ts": "56668d5c44f964f1b4ff93f21c9896df42d6ee4394e814db52d6d13f5bb247c7", - "https://deno.land/std@0.196.0/console/unicode_width.ts": "10661c0f2eeab802d16b8b85ed8825bbc573991bbfb6affed32dc1ff994f54f9", - "https://deno.land/std@0.196.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", "https://deno.land/std@0.213.0/archive/_common.ts": "85edd5cdd4324833f613c1bc055f8e2f935cc9229c6b3044421268d9959997ef", "https://deno.land/std@0.213.0/archive/untar.ts": "7677c136f2188cd8c33363ccaaee6e77d4ca656cca3e2093d08de8f294d4353d", "https://deno.land/std@0.213.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", @@ -281,108 +378,79 @@ "https://deno.land/std@0.213.0/url/join.ts": "00c7e9088cafaa24963ce4081119e58b3afe2c58f033701383f359ea02620dd2", "https://deno.land/std@0.213.0/url/mod.ts": "e2621f6a0db6fdbe7fbbd240064095bb203014657e5e1ab81db1c44d80dce6c9", "https://deno.land/std@0.213.0/url/normalize.ts": "6328c75df0fab300f74bc4a1c255062a0db882240e15ab646606d0009e7e40d7", + "https://deno.land/std@0.221.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.221.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.221.0/console/_data.json": "cf2cc9d039a192b3adbfe64627167c7e6212704c888c25c769fc8f1709e1e1b8", + "https://deno.land/std@0.221.0/console/_run_length.ts": "7da8642a0f4f41ac27c0adb1364e18886be856c1d08c5cce6c6b5c00543c8722", + "https://deno.land/std@0.221.0/console/unicode_width.ts": "d92f085c0ab9c7ab171e4e7862dfd9d3a36ffd369939be5d3e1140ec58bc820f", + "https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/std@0.221.0/text/closest_string.ts": "8a91ee8b6d69ff96addcb7c251dad53b476ac8be9c756a0ef786abe9e13a93a5", + "https://deno.land/std@0.221.0/text/levenshtein_distance.ts": "24be5cc88326bbba83ca7c1ea89259af0050cffda2817ff3a6d240ad6495eae2", "https://deno.land/std@0.76.0/encoding/base64.ts": "b1d8f99b778981548457ec74bc6273ad785ffd6f61b2233bd5b30925345b565d", "https://deno.land/std@0.76.0/encoding/hex.ts": "07a03ba41c96060a4ed4ba272e50b9e23f3c5b3839f4b069cdebc24d57434386", "https://deno.land/std@0.76.0/hash/_wasm/hash.ts": "005f64c4d9343ecbc91e0da9ae5e800f146c20930ad829bbb872c5c06bd89c5f", "https://deno.land/std@0.76.0/hash/_wasm/wasm.js": "5ac48aa0c3931d7f31dba628be5ab0aa4e786354197eb4d7d0583f9b50be1397", "https://deno.land/std@0.76.0/hash/hasher.ts": "099c9e2a91b9f106b9f01379705e17e7d9de392ee1ea2b8684a2adfa82ac3bfc", "https://deno.land/std@0.76.0/hash/mod.ts": "e764a6a9ab2f5519a97f928e17cc13d984e3dd5c7f742ff9c1c8fb3114790f0c", - "https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_errors.ts": "12d513ff401020287a344e0830e1297ce1c80c077ecb91e0ac5db44d04a6019c", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_spread.ts": "0cc6eb70a6df97b5d7d26008822d39f3e8a1232ee0a27f395aa19e68de738245", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_type_utils.ts": "820004a59bc858e355b11f80e5b3ff1be2c87e66f31f53f253610170795602f0", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_utils.ts": "3c88ff4f36eba298beb07de08068fdce5e5cb7b9d82c8a319f09596d8279be64", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/command.ts": "ae690745759524082776b7f271f66d5b93933170b1b132f888bd4ac12e9fdd7d", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/_bash_completions_generator.ts": "0c6cb1df4d378d22f001155781d97a9c3519fd10c48187a198fef2cc63b0f84a", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/_fish_completions_generator.ts": "8ba4455f7f76a756e05c3db4ce35332b2951af65a2891f2750b530e06880f495", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/_zsh_completions_generator.ts": "c74525feaf570fe8c14433c30d192622c25603f1fc64694ef69f2a218b41f230", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/bash.ts": "53fe78994eb2359110dc4fa79235bdd86800a38c1d6b1c4fe673c81756f3a0e2", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/completions_command.ts": "506f97f1c6b0b1c3e9956e5069070028b818942310600d4157f64c9b644d3c49", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/fish.ts": "6f0b44b4067740b2931c9ec8863b6619b1d3410fea0c5a3988525a4c53059197", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/mod.ts": "8dda715ca25f3f66d5ec232b76d7c9a96dd4c64b5029feff91738cc0c9586fb1", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/completions/zsh.ts": "f1263c3946975e090d4aadc8681db811d86b52a8ae680f246e03248025885c21", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/deps.ts": "7473ebd5625bf901becd7ff80afdde3b8a50ae5d1bbfa2f43805cfacf4559d5a", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/help/_help_generator.ts": "532dd4a928baab8b45ce46bb6d20e2ebacfdf3da141ce9d12da796652b1de478", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/help/help_command.ts": "fbbf0c0827dd21d3cec7bcc68c00c20b55f53e2b621032891b9d23ac4191231c", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/help/mod.ts": "8369b292761dcc9ddaf41f2d34bfb06fb6800b69efe80da4fc9752c3b890275b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts": "4b708df1b97152522bee0e3828f06abbbc1d2250168910e5cf454950d7b7404b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/type.ts": "f588f5d9635b79100044e62aced4b00e510e75b83801f9b089c40c2d98674de2", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types.ts": "bc9ff7459b9cc1079eeb95ff101690a51b4b4afa4af5623340076ee361d08dbb", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/enum.ts": "8a7cd2898e03089234083bb78c8b1d9b7172254c53c32d4710321638165a48ec", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/_check_version.ts": "6cfa7dc26bc0dc46381500e8d4b130fb224f4c5456152dada15bd3793edca89b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/mod.ts": "4eff69c489467be17dea27fb95a795396111ee385d170ac0cbcc82f0ea38156c", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/provider.ts": "c23253334097dc4b8a147ccdeb3aa44f5a95aa953a6386cb5396f830d95d77a5", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e", - "https://deno.land/x/cliffy@v1.0.0-rc.3/command/upgrade/upgrade_command.ts": "3640a287d914190241ea1e636774b1b4b0e1828fa75119971dd5304784061e05", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/_utils.ts": "340d3ecab43cde9489187e1f176504d2c58485df6652d1cdd907c0e9c3ce4cc2", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/_validate_flags.ts": "e60b9038c0136ab7e6bd1baf0e993a07bf23f18afbfb6e12c59adf665a622957", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/flags.ts": "3e62c4a9756b5705aada29e7e94847001356b3a83cd18ad56f4207387a71cf51", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types.ts": "9e2f75edff2217d972fc711a21676a59dfd88378da2f1ace440ea84c07db1dcc", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", - "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/_layout.ts": "e4a518da28333de95ad791208b9930025987c8b93d5f8b7f30b377b3e26b24e1", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/_utils.ts": "fd48d1a524a42e72aa3ad2eec858a92f5a00728d306c7e8436fba6c34314fee6", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/border.ts": "5c6e9ef5078c6930169aacb668b274bdbb498461c724a7693ac9270fe9d3f5d5", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/cell.ts": "1ffabd43b6b7fddfac9625cb0d015532e144702a9bfed03b358b79375115d06b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/column.ts": "cf14009f2cb14bad156f879946186c1893acdc6a2fee6845db152edddb6a2714", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/consume_words.ts": "456e75755fdf6966abdefb8b783df2855e2a8bad6ddbdf21bd748547c5fc1d4b", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/deps.ts": "1226c4d39d53edc81d7c3e661fb8a79f2e704937c276c60355cd4947a0fe9153", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/row.ts": "79eb1468aafdd951e5963898cdafe0752d4ab4c519d5f847f3d8ecb8fe857d4f", - "https://deno.land/x/cliffy@v1.0.0-rc.3/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684", - "https://deno.land/x/dax@0.38.0/mod.ts": "3a5d7e6ac12547feec5d3c0c96717f14276891a3802fbbc73e5901e4f20eb08d", - "https://deno.land/x/dax@0.38.0/src/command.ts": "f20135ef7188a0fc9f773d50e88775dee8653044a7f536fb2fb885b293c26ec4", - "https://deno.land/x/dax@0.38.0/src/command_handler.ts": "a9e40f0f1ec57318e62904b5785fede82dcdf1101922ebb3ebfad8f1c4d9c8df", - "https://deno.land/x/dax@0.38.0/src/commands/args.ts": "a138aef24294e3cbf13cef08f4836d018e8dd99fd06ad82e7e7f08ef680bbc1d", - "https://deno.land/x/dax@0.38.0/src/commands/cat.ts": "a136e9fe729d6b89c9bab469e6367f557bcddf3a4a3240b2ac280ff6da540b88", - "https://deno.land/x/dax@0.38.0/src/commands/cd.ts": "3d70605c6f8606008072f52763dbf4a979fa501975d006cf7f50eed0576936ab", - "https://deno.land/x/dax@0.38.0/src/commands/cp_mv.ts": "d57102f05f8eb6fb8f705e532a0e01c0dc7ba960c1e0828d4a5bef7ff411215f", - "https://deno.land/x/dax@0.38.0/src/commands/echo.ts": "8ca19f63779f8fa9cf2a29e21bdb31cfd6a3a09a820e5a83d6244325dea5f360", - "https://deno.land/x/dax@0.38.0/src/commands/exit.ts": "ef83eefb99270872ac679e38cee9aec345da9a345a3873fe6660f05aa577f937", - "https://deno.land/x/dax@0.38.0/src/commands/export.ts": "c10d1dc6a45fd00e40afa6b19d7ecd29d09333f422b5b0fc75863baf13350969", - "https://deno.land/x/dax@0.38.0/src/commands/mkdir.ts": "828a2d356fcff05d022f0e5ef76ed4a899b5370485fa4144fe378040a0f05aef", - "https://deno.land/x/dax@0.38.0/src/commands/printenv.ts": "4fc09ecf88e35bc9d810e3f45d1d8e808613e73701466ca6e48fca8d1810a48a", - "https://deno.land/x/dax@0.38.0/src/commands/pwd.ts": "6507d70bf02026bde8f58da166c37cdc2f7e1fda807b003f096aab077b866ee5", - "https://deno.land/x/dax@0.38.0/src/commands/rm.ts": "43ef496c34b722d007b945232d51273fcc6d7315f6198f6a6291bb7151941426", - "https://deno.land/x/dax@0.38.0/src/commands/sleep.ts": "413bacfd3bebf2a1397cda223776baadef8596f40558d6c2686ffd9b6ad80e54", - "https://deno.land/x/dax@0.38.0/src/commands/test.ts": "b0f56b3d1d038b47fe826bb3dab056746aefe12df6222e29150e7e6f78a51d9c", - "https://deno.land/x/dax@0.38.0/src/commands/touch.ts": "40a0292e5e4f35c057ac50445a124703355d2955a25b53a223aebf0b3b016e4e", - "https://deno.land/x/dax@0.38.0/src/commands/unset.ts": "1ffec8b32bbac8ef7b90b2ba1fc4d9339d3563ef8e302b14d119f9c220564985", - "https://deno.land/x/dax@0.38.0/src/common.ts": "37449926d3bc874aac4e4ff4ea06d46251dc54ad0bbb5721c7eb4920e2d5b591", - "https://deno.land/x/dax@0.38.0/src/console/confirm.ts": "d9128d10b77fcc0a8df2784f71c79df68f5c8e00a34b04547b9ba9ddf1c97f96", - "https://deno.land/x/dax@0.38.0/src/console/logger.ts": "e0ab5025915cef70df03681c756e211f25bb2e4331f82ed4256b17ddd9e794ea", - "https://deno.land/x/dax@0.38.0/src/console/mod.ts": "de8af7d646f6cb222eee6560171993690247941b13ed9d757789d16f019d73ee", - "https://deno.land/x/dax@0.38.0/src/console/multiSelect.ts": "31003744e58f45f720271bd034d8cfba1055c954ba02d77a2f2eb21e4c1ed55a", - "https://deno.land/x/dax@0.38.0/src/console/progress/format.ts": "15ddbb8051580f88ed499281e12ca6f881f875ab73268d7451d7113ee130bd7d", - "https://deno.land/x/dax@0.38.0/src/console/progress/interval.ts": "80188d980a27c2eb07c31324365118af549641442f0752fe7c3b0c91832e5046", - "https://deno.land/x/dax@0.38.0/src/console/progress/mod.ts": "dd9330c3edd1790d70808d043f417f0eaf80a4442a945545c38e47ce11e907b6", - "https://deno.land/x/dax@0.38.0/src/console/prompt.ts": "1ad65c8a5a27fb58ce6138f8ebefe2fca4cd12015fea550fbdc62f875d4b31f7", - "https://deno.land/x/dax@0.38.0/src/console/select.ts": "c9d7124d975bf34d52ea1ac88fd610ed39db8ee6505b9bb53f371cef2f56c6ab", - "https://deno.land/x/dax@0.38.0/src/console/utils.ts": "24b840d4e55eba0d5b2f79337d2940d5f9456d4d6836f35316e6495b7cb827b4", - "https://deno.land/x/dax@0.38.0/src/deps.ts": "c1e16434a805285d27c30c70a825473f88117dfa7e1d308408db1b1ab4fe743f", - "https://deno.land/x/dax@0.38.0/src/lib/mod.ts": "c992db99c8259ae3bf2d35666585dfefda84cf7cf4e624e42ea2ac7367900fe0", - "https://deno.land/x/dax@0.38.0/src/lib/rs_lib.generated.js": "0a1a482c4387379106ef0da69534ebc5b0c2a1ec9f6dab76833fe84a7e6bbdf6", - "https://deno.land/x/dax@0.38.0/src/path.ts": "451589cc3ad49cab084c50ad0ec07f7e2492a20d2f0ee7cfd80ab36360e6aa55", - "https://deno.land/x/dax@0.38.0/src/pipes.ts": "bbfc7d6bf0f0bfc363daa2f4d3c5ebf17025d82c4114d5b0ea444cf69d805670", - "https://deno.land/x/dax@0.38.0/src/request.ts": "461e16f53367c73c0ec16091c2fd6cb97a219f6af07a3a2a10029139bf404879", - "https://deno.land/x/dax@0.38.0/src/result.ts": "719a9b4bc6bafeec785106744381cd5f37927c973334fcba6a33b6418fb9e7be", - "https://deno.land/x/dax@0.38.0/src/shell.ts": "9b59a63de62003a0575f9c3300b5fff83cd7e5487582eceaa5f071a684d75e0e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_errors.ts": "d78e1b4d69d84b8b476b5f3c0b028e3906d48f21b8f1ca1d36d5abe9ccfe48bc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_spread.ts": "0cc6eb70a6df97b5d7d26008822d39f3e8a1232ee0a27f395aa19e68de738245", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_type_utils.ts": "820004a59bc858e355b11f80e5b3ff1be2c87e66f31f53f253610170795602f0", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/_utils.ts": "fa0e88cc4215b18554a7308e8e2ae3a12be0fb91c54d1473c54c530dbd4adfcb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/command.ts": "83cbece11c1459d5bc5add32c3cad0bf49e92c4ddd3ef00f22f80efdae30994e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_bash_completions_generator.ts": "0c6cb1df4d378d22f001155781d97a9c3519fd10c48187a198fef2cc63b0f84a", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_fish_completions_generator.ts": "8ba4455f7f76a756e05c3db4ce35332b2951af65a2891f2750b530e06880f495", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/_zsh_completions_generator.ts": "9df79fbac17a32b9645d01628c41a2bfd295d7976b87b0ae235f50a9c8975fbc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/bash.ts": "53fe78994eb2359110dc4fa79235bdd86800a38c1d6b1c4fe673c81756f3a0e2", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/complete.ts": "58df61caa5e6220ff2768636a69337923ad9d4b8c1932aeb27165081c4d07d8b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/completions_command.ts": "506f97f1c6b0b1c3e9956e5069070028b818942310600d4157f64c9b644d3c49", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/fish.ts": "6f0b44b4067740b2931c9ec8863b6619b1d3410fea0c5a3988525a4c53059197", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/mod.ts": "8dda715ca25f3f66d5ec232b76d7c9a96dd4c64b5029feff91738cc0c9586fb1", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/completions/zsh.ts": "f1263c3946975e090d4aadc8681db811d86b52a8ae680f246e03248025885c21", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/deprecated.ts": "bbe6670f1d645b773d04b725b8b8e7814c862c9f1afba460c4d599ffe9d4983c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/deps.ts": "a58ea2fa4e2ed9b39bb8dd8c35dd0498c74f05392517ff230a9a4d04c4c766b7", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/_help_generator.ts": "98619da83ff25523280a6fdcad89af3f13a6fafefc81b71f8230f3344b5ff2c5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/help_command.ts": "fbbf0c0827dd21d3cec7bcc68c00c20b55f53e2b621032891b9d23ac4191231c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/help/mod.ts": "8369b292761dcc9ddaf41f2d34bfb06fb6800b69efe80da4fc9752c3b890275b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts": "4b708df1b97152522bee0e3828f06abbbc1d2250168910e5cf454950d7b7404b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/type.ts": "f588f5d9635b79100044e62aced4b00e510e75b83801f9b089c40c2d98674de2", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types.ts": "bc9ff7459b9cc1079eeb95ff101690a51b4b4afa4af5623340076ee361d08dbb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/action_list.ts": "33c98d449617c7a563a535c9ceb3741bde9f6363353fd492f90a74570c611c27", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/boolean.ts": "3879ec16092b4b5b1a0acb8675f8c9250c0b8a972e1e4c7adfba8335bd2263ed", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/child_command.ts": "f1fca390c7fbfa7a713ca15ef55c2c7656bcbb394d50e8ef54085bdf6dc22559", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/command.ts": "325d0382e383b725fd8d0ef34ebaeae082c5b76a1f6f2e843fee5dbb1a4fe3ac", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/enum.ts": "8a7cd2898e03089234083bb78c8b1d9b7172254c53c32d4710321638165a48ec", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/file.ts": "8618f16ac9015c8589cbd946b3de1988cc4899b90ea251f3325c93c46745140e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/integer.ts": "29864725fd48738579d18123d7ee78fed37515e6dc62146c7544c98a82f1778d", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/number.ts": "aeba96e6f470309317a16b308c82e0e4138a830ec79c9877e4622c682012bc1f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/types/string.ts": "e4dadb08a11795474871c7967beab954593813bb53d9f69ea5f9b734e43dc0e0", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/_check_version.ts": "6cfa7dc26bc0dc46381500e8d4b130fb224f4c5456152dada15bd3793edca89b", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/mod.ts": "4eff69c489467be17dea27fb95a795396111ee385d170ac0cbcc82f0ea38156c", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider.ts": "c23253334097dc4b8a147ccdeb3aa44f5a95aa953a6386cb5396f830d95d77a5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/deno_land.ts": "24f8d82e38c51e09be989f30f8ad21f9dd41ac1bb1973b443a13883e8ba06d6d", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/github.ts": "99e1b133dd446c6aa79f69e69c46eb8bc1c968dd331c2a7d4064514a317c7b59", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/provider/nest_land.ts": "0e07936cea04fa41ac9297f32d87f39152ea873970c54cb5b4934b12fee1885e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/command/upgrade/upgrade_command.ts": "27191f4b1ce93581b6d5ee2fff6003fe4fca437f476ecb98b6eae92f2b4d0716", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_errors.ts": "f1fbb6bfa009e7950508c9d491cfb4a5551027d9f453389606adb3f2327d048f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_utils.ts": "25e519ce1f35acc8b43c75d1ca1c4ab591e7dab08327b7b408705b591e27d8bd", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/_validate_flags.ts": "e60b9038c0136ab7e6bd1baf0e993a07bf23f18afbfb6e12c59adf665a622957", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deprecated.ts": "a72a35de3cc7314e5ebea605ca23d08385b218ef171c32a3f135fb4318b08126", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/deps.ts": "bed26afff36eeb25509440edec9d5d141b3411e08cc7a90e38a370969b5166bb", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/flags.ts": "3e62c4a9756b5705aada29e7e94847001356b3a83cd18ad56f4207387a71cf51", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types.ts": "9e2f75edff2217d972fc711a21676a59dfd88378da2f1ace440ea84c07db1dcc", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/boolean.ts": "4c026dd66ec9c5436860dc6d0241427bdb8d8e07337ad71b33c08193428a2236", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", + "https://deno.land/x/cliffy@v1.0.0-rc.4/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/_layout.ts": "73a9bcb8a87b3a6817c4c9d2a31a21b874a7dd690ade1c64c9a1f066d628d626", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/_utils.ts": "13390db3f11977b7a4fc1202fa8386be14696b475a7f46a65178354f9a6640b7", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/border.ts": "5c6e9ef5078c6930169aacb668b274bdbb498461c724a7693ac9270fe9d3f5d5", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/cell.ts": "65e3ee699c3cebeb4d4d44e8f156e37a8532a0f317359d73178a95724d3f9267", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/column.ts": "cf14009f2cb14bad156f879946186c1893acdc6a2fee6845db152edddb6a2714", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/consume_words.ts": "369d065dbf7f15c664ea8523e0ef750fb952aea6d88e146c375e64aec9503052", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/deps.ts": "cbb896e8d7a6b5e3c2b9dda7d16638c202d9b46eb738c2dae1fa9480d8091486", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/row.ts": "79eb1468aafdd951e5963898cdafe0752d4ab4c519d5f847f3d8ecb8fe857d4f", + "https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts": "298671e72e61f1ab18b42ae36643181993f79e29b39dc411fdc6ffd53aa04684", "https://deno.land/x/deep_eql@v5.0.1/index.js": "60e1547b99d4ae08df387067c2ac0a1b9ab42f212f0d8a11b8b0b61270d2b1c4", "https://deno.land/x/foras@v2.1.4/src/deno/mod.ts": "c350ea5f32938e6dcb694df3761615f316d730dafc57440e9afd5f36f8e309fd", "https://deno.land/x/foras@v2.1.4/src/deno/mods/mod.ts": "cc099bbce378f3cdaa94303e8aff2611e207442e5ac2d5161aba636bb4a95b46", @@ -398,8 +466,6 @@ "https://deno.land/x/jszip@0.11.0/types.ts": "1528d1279fbb64dd118c371331c641a3a5eff2b594336fb38a7659cf4c53b2d1", "https://deno.land/x/object_hash@2.0.3/index.ts": "74b20a0065dc0066c60510174626db1d18e53ec966edb6f76fa33a67aa0c44e3", "https://deno.land/x/object_hash@2.0.3/mod.ts": "648559bcafb54b930d4b6a283cc2eef20afa54de471371a97c2ccf8116941148", - "https://deno.land/x/outdent@v0.8.0/src/index.ts": "6dc3df4108d5d6fedcdb974844d321037ca81eaaa16be6073235ff3268841a22", - "https://deno.land/x/which@0.3.0/mod.ts": "3e10d07953c14e4ddc809742a3447cef14202cdfe9be6678a1dfc8769c4487e6", "https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", "https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", "https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", @@ -413,6 +479,19 @@ "https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", "https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", "https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e", + "https://deno.land/x/zod@v3.23.5/ZodError.ts": "528da200fbe995157b9ae91498b103c4ef482217a5c086249507ac850bd78f52", + "https://deno.land/x/zod@v3.23.5/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.23.5/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.23.5/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.23.5/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.23.5/helpers/parseUtil.ts": "c14814d167cc286972b6e094df88d7d982572a08424b7cd50f862036b6fcaa77", + "https://deno.land/x/zod@v3.23.5/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.23.5/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.23.5/helpers/util.ts": "3301a69867c9e589ac5b3bc4d7a518b5212858cd6a25e8b02d635c9c32ba331c", + "https://deno.land/x/zod@v3.23.5/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.23.5/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.23.5/mod.ts": "ec6e2b1255c1a350b80188f97bd0a6bac45801bb46fc48f50b9763aa66046039", + "https://deno.land/x/zod@v3.23.5/types.ts": "78d3f06eb313ea754fad0ee389d3c0fa55bc01cf708e6ce0ea7fddd41f31eca2", "https://esm.sh/jszip@3.7.1": "f3872a819b015715edb05f81d973b5cd05d3d213d8eb28293ca5471fe7a71773", "https://esm.sh/v135/jszip@3.7.1/denonext/jszip.mjs": "d31d7f9e0de9c6db3c07ca93f7301b756273d4dccb41b600461978fc313504c9" } diff --git a/deps/cli.ts b/deps/cli.ts index ee7e2deb..01057a2b 100644 --- a/deps/cli.ts +++ b/deps/cli.ts @@ -2,4 +2,4 @@ export * from "./common.ts"; -export * as cliffy_cmd from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts"; +export * as cliffy_cmd from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts"; diff --git a/deps/common.ts b/deps/common.ts index ff55ede8..13627211 100644 --- a/deps/common.ts +++ b/deps/common.ts @@ -2,7 +2,8 @@ //! FIXME: move files in this module to files called deps.ts //! and located close to their users -export { z as zod } from "https://deno.land/x/zod@v3.22.4/mod.ts"; +export { z as zod } from "https://deno.land/x/zod@v3.23.5/mod.ts"; +export * as zod_val_err from "npm:zod-validation-error@3.2.0"; export * as semver from "https://deno.land/std@0.213.0/semver/mod.ts"; export * as std_log from "https://deno.land/std@0.213.0/log/mod.ts"; export * as std_log_levels from "https://deno.land/std@0.213.0/log/levels.ts"; @@ -10,7 +11,9 @@ export * as std_fmt_colors from "https://deno.land/std@0.213.0/fmt/colors.ts"; export * as std_url from "https://deno.land/std@0.213.0/url/mod.ts"; export * as std_path from "https://deno.land/std@0.213.0/path/mod.ts"; export * as std_fs from "https://deno.land/std@0.213.0/fs/mod.ts"; -export * as dax from "https://deno.land/x/dax@0.38.0/mod.ts"; +// export * as dax from "jsr:@david/dax@0.40.1"; +export * as dax from "jsr:@ghjk/dax@0.40.2-alpha-ghjk"; + export * as jsonHash from "https://deno.land/x/json_hash@0.2.0/mod.ts"; export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3/mod.ts"; export { default as deep_eql } from "https://deno.land/x/deep_eql@v5.0.1/index.js"; diff --git a/ghjk.ts b/ghjk.ts index c841dd78..cc7acabf 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -9,11 +9,12 @@ install(); install( ports.act(), ports.pipi({ packageName: "pre-commit" })[0], - ports.cpy_bs({ releaseTag: "20231002" }), + ports.cpy_bs({}), ); -env("test") - .install(ports.protoc()); +env("test", { + installs: [ports.protoc()], +}); export const secureConfig = stdSecureConfig({ enableRuntimes: true, diff --git a/host/deno.ts b/ghjkfiles/deno/mod.ts similarity index 97% rename from host/deno.ts rename to ghjkfiles/deno/mod.ts index 5d055528..4b566ceb 100644 --- a/host/deno.ts +++ b/ghjkfiles/deno/mod.ts @@ -1,6 +1,6 @@ //! this loads the ghjk.ts module and provides a program for it -import { std_url } from "../deps/common.ts"; +import { std_url } from "../../deps/common.ts"; export type DriverRequests = { ty: "serialize"; diff --git a/host/worker.ts b/ghjkfiles/deno/worker.ts similarity index 97% rename from host/worker.ts rename to ghjkfiles/deno/worker.ts index 55c24066..c53be849 100644 --- a/host/worker.ts +++ b/ghjkfiles/deno/worker.ts @@ -7,7 +7,7 @@ // modify the Deno namespace before anyone touches it // NOTE: only import types -import type { DriverRequests, DriverResponse } from "./deno.ts"; +import type { DriverRequests, DriverResponse } from "./mod.ts"; self.onmessage = onMsg; @@ -34,9 +34,7 @@ async function onMsg(msg: MessageEvent) { async function serializeConfig(uri: string, envVars: Record) { const shimHandle = shimDenoNamespace(envVars); - const { setup: setupLogger } = await import( - "../utils/logger.ts" - ); + const { setup: setupLogger } = await import("../../utils/logger.ts"); setupLogger(); const mod = await import(uri); const rawConfig = await mod.ghjk.getConfig(mod.secureConfig); diff --git a/ghjkfiles/mod.ts b/ghjkfiles/mod.ts new file mode 100644 index 00000000..1885cd3f --- /dev/null +++ b/ghjkfiles/mod.ts @@ -0,0 +1,603 @@ +// NOTE: avoid adding sources of randomness +// here to make the resulting config reasonably stable +// across serializaiton. No random identifiers. + +// ports specific imports +import portsValidators from "../modules/ports/types.ts"; +import type { + AllowedPortDep, + InstallConfigFat, + InstallSet, + InstallSetRefProvision, + PortsModuleConfigHashed, + PortsModuleSecureConfig, +} from "../modules/ports/types.ts"; +import logger from "../utils/logger.ts"; +import { + $, + defaultCommandBuilder, + Path, + thinInstallConfig, + unwrapParseRes, +} from "../utils/mod.ts"; +import * as std_ports from "../modules/ports/std.ts"; +import * as cpy from "../ports/cpy_bs.ts"; +import * as node from "../ports/node.ts"; +// host +import type { SerializedConfig } from "../host/types.ts"; +import * as std_modules from "../modules/std.ts"; +// tasks +import { dax, jsonHash, objectHash } from "../deps/common.ts"; +// WARN: this module has side-effects and only ever import +// types from it +import type { ExecTaskArgs } from "../modules/tasks/deno.ts"; +import { TasksModuleConfig } from "../modules/tasks/types.ts"; +// envs +import { + EnvRecipe, + EnvsModuleConfig, + WellKnownProvision, +} from "../modules/envs/types.ts"; + +export type EnvDefArgs = { + name: string; + installs?: InstallConfigFat[]; + allowedPortDeps?: AllowedPortDep[]; + /* + * If true or not set, will base the task's env on top + * of the default env (usually `main`). If false, will build on + * top of a new env. If given a string, will use the identified env as a base + * for the task env. + */ + base?: string | boolean; + desc?: string; + vars?: Record; +}; + +export type TaskFnArgs = { + $: dax.$Type; + argv: string[]; + env: Record; +}; + +export type TaskFn = (args: TaskFnArgs) => Promise | any; + +/* + * Configuration for a task. + */ +export type TaskDefArgs = { + name: string; + fn: TaskFn; + desc?: string; + dependsOn?: string[]; + workingDir?: string | Path; + envVars?: Record; + allowedPortDeps?: AllowedPortDep[]; + installs?: InstallConfigFat[]; + base?: string | boolean; +}; + +export class GhjkfileBuilder { + #installSets = new Map(); + #tasks = {} as Record; + #bb = new Map(); + #seenEnvs: Record = {}; + + addInstall(setId: string, configUnclean: InstallConfigFat) { + const config = unwrapParseRes( + portsValidators.installConfigFat.safeParse(configUnclean), + { + config: configUnclean, + }, + `error parsing InstallConfig`, + ); + + const set = this.#getSet(setId); + set.installs.push(config); + logger().debug("install added", config); + } + + setAllowedPortDeps(setId: string, deps: AllowedPortDep[]) { + const set = this.#getSet(setId); + set.allowedDeps = Object.fromEntries( + deps.map(( + dep, + ) => [dep.manifest.name, dep]), + ); + } + + addTask(args: TaskDefArgs) { + // NOTE: we make sure the env base declared here exists + // this call is necessary to make sure that a `task` can + // be declared before the `env` but still depend on it. + // Order-indepency like this makes the `ghjk.ts` way less + // brittle. + if (typeof args.base == "string") { + this.addEnv({ name: args.base }); + } + + this.#tasks[args.name] = { + ...args, + name, + }; + return args.name; + } + + addEnv(args: EnvDefArgs) { + let env = this.#seenEnvs[args.name]?.[0]; + if (!env) { + let finalizer: EnvFinalizer; + env = new EnvBuilder(this, (fin) => finalizer = fin, args.name); + this.#seenEnvs[args.name] = [env, finalizer!]; + } + if (args.base !== undefined) { + env.base(args.base); + } + if (args.installs) { + env.install(...args.installs); + } + if (args.allowedPortDeps) { + env.allowedPortDeps(args.allowedPortDeps); + } + if (args.desc) { + env.desc(args.desc); + } + if (args.vars) { + env.vars(args.vars); + } + return env; + } + + async execTask( + { name, workingDir, envVars, argv }: ExecTaskArgs, + ) { + const task = this.#tasks[name]; + if (!task) { + throw new Error(`no task defined under "${name}"`); + } + const custom$ = $.build$({ + commandBuilder: defaultCommandBuilder().env(envVars).cwd(workingDir), + }); + await task.fn({ argv, env: envVars, $: custom$ }); + } + + toConfig( + { defaultEnv, defaultBaseEnv, secureConfig }: { + defaultEnv: string; + defaultBaseEnv: string; + secureConfig: PortsModuleSecureConfig | undefined; + }, + ) { + try { + const envsConfig = this.#processEnvs( + defaultEnv, + defaultBaseEnv, + ); + const tasksConfig = this.#processTasks(envsConfig, defaultBaseEnv); + const portsConfig = this.#processInstalls( + secureConfig?.masterPortDepAllowList ?? stdDeps(), + ); + + const config: SerializedConfig = { + modules: [{ + id: std_modules.ports, + config: portsConfig, + }, { + id: std_modules.tasks, + config: tasksConfig, + }, { + id: std_modules.envs, + config: envsConfig, + }], + blackboard: Object.fromEntries(this.#bb.entries()), + }; + return config; + } catch (cause) { + throw new Error(`error constructing config for serialization`, { cause }); + } + } + + #getSet(setId: string) { + let set = this.#installSets.get(setId); + if (!set) { + set = { installs: [], allowedDeps: {} }; + this.#installSets.set(setId, set); + } + return set; + } + + #addToBlackboard(inp: unknown) { + // jsonHash.digest is async + const hash = objectHash(jsonHash.canonicalize(inp as jsonHash.Tree)); + + if (!this.#bb.has(hash)) { + this.#bb.set(hash, inp); + } + return hash; + } + + // this processes the defined envs, normalizing dependency (i.e. "envBase") + // relationships to produce the standard EnvsModuleConfig + #processEnvs( + defaultEnv: string, + defaultBaseEnv: string, + ) { + const all = {} as Record< + string, + ReturnType & { envBaseResolved: null | string } + >; + const indie = [] as string[]; + const revDeps = new Map(); + for ( + const [_name, [_builder, finalizer]] of Object.entries(this.#seenEnvs) + ) { + const final = finalizer(); + const { name, base } = final; + const envBaseResolved = typeof base === "string" + ? base + : base + ? defaultBaseEnv + : null; + all[name] = { ...final, envBaseResolved }; + if (envBaseResolved) { + let parentRevDeps = revDeps.get(envBaseResolved); + if (!parentRevDeps) { + parentRevDeps = []; + revDeps.set(envBaseResolved, parentRevDeps); + } + parentRevDeps.push(final.name); + } else { + indie.push(name); + } + } + const processed = {} as Record< + string, + { installSetId?: string; vars: Record } + >; + const out: EnvsModuleConfig = { envs: {}, defaultEnv }; + const workingSet = [...indie]; + while (workingSet.length > 0) { + const item = workingSet.pop()!; + const final = all[item]; + + const base = final.envBaseResolved + ? processed[final.envBaseResolved] + : null; + + const processedVars = { + ...(base?.vars ?? {}), + ...final.vars, + }; + + let processedInstallSetId: string | undefined; + { + const installSet = this.#installSets.get(final.installSetId); + if (installSet) { + // if base also has an install set + if (base?.installSetId) { + // merge the parent's installs into this one + const baseSet = this.#installSets.get( + base.installSetId, + )!; + const mergedInstallsSet = new Set([ + ...installSet.installs, + ...baseSet.installs, + ]); + installSet.installs = [...mergedInstallsSet.values()]; + for ( + const [key, val] of Object.entries(baseSet.allowedDeps) + ) { + // prefer the port dep config of the child over any + // similar deps in the parent + if (!installSet.allowedDeps[key]) { + installSet.allowedDeps[key] = val; + } + } + } + processedInstallSetId = final.installSetId; + } // if there's no install set found under the id + else { + // implies that the env has not ports explicitly configured + if (base) { + processedInstallSetId = base.installSetId; + } + } + } + processed[final.name] = { + installSetId: processedInstallSetId, + vars: processedVars, + }; + out.envs[final.name] = { + desc: final.desc, + provides: [ + ...Object.entries(processedVars).map(( + [key, val], + ) => { + const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; + return prov; + }), + ], + }; + if (processedInstallSetId) { + const prov: InstallSetRefProvision = { + ty: "ghjk.ports.InstallSetRef", + setId: processedInstallSetId, + }; + out.envs[final.name].provides.push(prov); + } + + const curRevDeps = revDeps.get(final.name); + if (curRevDeps) { + workingSet.push(...curRevDeps); + revDeps.delete(final.name); + } + } + return out; + } + + #processTasks(envsConfig: EnvsModuleConfig, defaultBaseEnv: string) { + const out: TasksModuleConfig = { + envs: {}, + tasks: {}, + }; + for ( + const [name, args] of Object + .entries( + this.#tasks, + ) + ) { + const { workingDir, desc, dependsOn, base } = args; + const envBaseResolved = typeof base === "string" + ? base + : base + ? defaultBaseEnv + : null; + + const envBaseRecipe = envBaseResolved + ? envsConfig.envs[envBaseResolved] + : null; + + const taskEnvRecipe: EnvRecipe = { + provides: [], + }; + + const taskInstallSet: InstallSet = { + installs: args.installs ?? [], + allowedDeps: Object.fromEntries( + (args.allowedPortDeps ?? []).map((dep) => [dep.manifest.name, dep]), + ), + }; + + const mergedEnvVars = args.envVars ?? {}; + if (envBaseRecipe) { + for ( + const prov of envBaseRecipe + .provides as ( + | WellKnownProvision + | InstallSetRefProvision + )[] + ) { + if (prov.ty == "posix.envVar") { + if (!mergedEnvVars[prov.key]) { + mergedEnvVars[prov.key] = prov.val; + } + } else if (prov.ty == "ghjk.ports.InstallSetRef") { + const baseSet = this.#installSets.get(prov.setId)!; + const mergedInstallsSet = new Set([ + ...taskInstallSet.installs, + ...baseSet.installs, + ]); + taskInstallSet.installs = [...mergedInstallsSet.values()]; + for ( + const [key, val] of Object.entries(baseSet.allowedDeps) + ) { + // prefer the port dep config of the child over any + // similar deps in the base + if (!taskInstallSet.allowedDeps[key]) { + taskInstallSet.allowedDeps[key] = val; + } + } + } else { + taskEnvRecipe.provides.push(prov); + } + } + } + if (taskInstallSet.installs.length > 0) { + const setId = `ghjkTaskInstSet___${name}`; + this.#installSets.set(setId, taskInstallSet); + const prov: InstallSetRefProvision = { + ty: "ghjk.ports.InstallSetRef", + setId, + }; + taskEnvRecipe.provides.push(prov); + } + + taskEnvRecipe.provides.push( + ...Object.entries(mergedEnvVars).map(( + [key, val], + ) => { + const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; + return prov; + }), + ); + + const envHash = objectHash( + jsonHash.canonicalize(taskEnvRecipe as jsonHash.Tree), + ); + out.envs[envHash] = taskEnvRecipe; + + out.tasks[name] = { + name, + workingDir: typeof workingDir == "object" + ? workingDir.toString() + : workingDir, + desc, + dependsOn, + envHash, + }; + } + for (const [name, { dependsOn }] of Object.entries(out.tasks)) { + for (const depName of dependsOn ?? []) { + if (!out.tasks[depName]) { + throw new Error( + `task "${name}" depend on non-existent task "${depName}"`, + ); + } + } + } + + return out; + } + + #processInstalls(masterAllowList: AllowedPortDep[]) { + const out: PortsModuleConfigHashed = { + sets: {}, + }; + const masterPortDepAllowList = Object.fromEntries( + masterAllowList.map((dep) => [dep.manifest.name, dep] as const), + ); + for ( + const [setId, set] of this.#installSets.entries() + ) { + for (const [portName, _] of Object.entries(set.allowedDeps)) { + if (!masterPortDepAllowList[portName]) { + throw new Error( + `"${portName}" is in allowedPortDeps list of install set "${setId}" but not in the masterPortDepAllowList`, + ); + } + } + for (const [name, hash] of Object.entries(masterPortDepAllowList)) { + if (!set.allowedDeps[name]) { + set.allowedDeps[name] = hash; + } + } + out.sets[setId] = { + installs: set.installs.map((inst) => this.#addToBlackboard(inst)), + allowedDeps: this.#addToBlackboard(Object.fromEntries( + Object.entries(set.allowedDeps).map( + ([key, dep]) => [key, this.#addToBlackboard(dep)], + ), + )), + }; + } + return out; + } +} + +type EnvFinalizer = () => { + name: string; + installSetId: string; + base: string | boolean; + vars: Record; + desc?: string; +}; + +// this class will be exposed to users and thus features +// a contrived implementation of the `build`/`finalize` method +// all to avoid exposing the function in the public api +export class EnvBuilder { + #installSetId: string; + #file: GhjkfileBuilder; + #base: string | boolean = true; + #vars: Record = {}; + #desc?: string; + + constructor( + file: GhjkfileBuilder, + setFinalizer: (fin: EnvFinalizer) => void, + public name: string, + ) { + this.#file = file; + this.#installSetId = `ghjkEnvProvInstSet___${name}`; + setFinalizer(() => ({ + name: this.name, + installSetId: this.#installSetId, + base: this.#base, + vars: this.#vars, + desc: this.#desc, + })); + } + + base(base: string | boolean) { + this.#base = base; + return this; + } + + /* + * Provision a port install in the environment. + */ + install(...configs: InstallConfigFat[]) { + for (const config of configs) { + this.#file.addInstall(this.#installSetId, config); + } + return this; + } + + /* + * This is treated as a single set and will replace previously any configured set. + */ + allowedPortDeps(deps: AllowedPortDep[]) { + this.#file.setAllowedPortDeps(this.#installSetId, deps); + return this; + } + + /* + * Add an environment variable. + */ + var(key: string, val: string) { + this.vars({ [key]: val }); + return this; + } + + /* + * Add multiple environment variable. + */ + vars(envVars: Record) { + Object.assign(this.#vars, envVars); + return this; + } + + /* + * Description of the environment. + */ + desc(str: string) { + this.#desc = str; + return this; + } +} + +export function stdSecureConfig( + args: { + additionalAllowedPorts?: PortsModuleSecureConfig["masterPortDepAllowList"]; + enableRuntimes?: boolean; + } & Pick, +): PortsModuleSecureConfig { + const { additionalAllowedPorts, enableRuntimes = false } = args; + const out: PortsModuleSecureConfig = { + masterPortDepAllowList: [ + ...stdDeps({ enableRuntimes }), + ...additionalAllowedPorts ?? [], + ], + }; + return out; +} + +export function stdDeps(args = { enableRuntimes: false }) { + const out: AllowedPortDep[] = [ + ...Object.values(std_ports.map), + ]; + if (args.enableRuntimes) { + out.push( + ...[ + node.default(), + cpy.default(), + ].map((fatInst) => { + return portsValidators.allowedPortDep.parse({ + manifest: fatInst.port, + defaultInst: thinInstallConfig(fatInst), + }); + }), + ); + } + return out; +} diff --git a/host/mod.ts b/host/mod.ts index 62a4b5e7..d56afa84 100644 --- a/host/mod.ts +++ b/host/mod.ts @@ -1,4 +1,10 @@ -import { cliffy_cmd, deep_eql, jsonHash, zod } from "../deps/cli.ts"; +import { + cliffy_cmd, + deep_eql, + jsonHash, + zod, + zod_val_err, +} from "../deps/cli.ts"; import logger, { isColorfulTty } from "../utils/logger.ts"; import { @@ -6,15 +12,16 @@ import { bufferHashHex, Json, objectHashHex, - PathRef, + Path, stringHashHex, } from "../utils/mod.ts"; import validators, { SerializedConfig } from "./types.ts"; import * as std_modules from "../modules/std.ts"; -import * as deno from "./deno.ts"; +import * as denoFile from "../ghjkfiles/deno/mod.ts"; import type { ModuleBase } from "../modules/mod.ts"; import { GhjkCtx } from "../modules/types.ts"; import { serializePlatform } from "../modules/ports/types/platform.ts"; +import { DePromisify } from "../port.ts"; export interface CliArgs { ghjkShareDir: string; @@ -28,93 +35,139 @@ type HostCtx = { export async function cli(args: CliArgs) { const ghjkShareDir = $.path(args.ghjkShareDir).resolve().normalize() .toString(); + // items to run at end of function + const defer = [] as (() => Promise)[]; - const subcmds = { - print: new cliffy_cmd.Command() - .description("Emit different discovered and built values to stdout.") - .action(function () { - this.showHelp(); - }) - .command( - "share-dir-path", - new cliffy_cmd.Command() - .description("Print the path where ghjk is installed in.") - .action(function () { - console.log(ghjkShareDir); - }), - ), - deno: new cliffy_cmd.Command() - .description("Access the deno cli used by ghjk.") - .useRawArgs() - .action(async function (_, ...args) { - logger().debug(args); - await $.raw`${Deno.execPath()} ${args}` - .env("DENO_EXEC_PATH", Deno.execPath()); - }), - }; + const subcmds: Record = {}; + + let serializedConfig: object | undefined; + let ghjkDir: string | undefined; + let ghjkfilePath: string | undefined; + // most of the CLI is only avail if there's a + // ghjkfile detected if (args.ghjkfilePath) { - const ghjkfilePath = $.path(args.ghjkfilePath).resolve().normalize() + ghjkfilePath = $.path(args.ghjkfilePath).resolve().normalize() .toString(); - const ghjkDir = $.path(ghjkfilePath).parentOrThrow().join(".ghjk") + ghjkDir = $.path(ghjkfilePath).parentOrThrow().join(".ghjk") .toString(); logger().debug({ ghjkfilePath, ghjkDir }); const gcx = { ghjkShareDir, ghjkfilePath, ghjkDir, blackboard: new Map() }; const hcx = { fileHashMemoStore: new Map() }; - const { subCommands: configCommands, serializedConfig } = await readConfig( + const { + subCommands: configCommands, + serializedConfig: config, + writeLockFile, + } = await readConfig( gcx, hcx, ); - - Object.assign(subcmds, configCommands); - - subcmds.print = subcmds.print - .command( - "ghjk-dir-path", - new cliffy_cmd.Command() - .description("Print the path where ghjk is installed in.") - .action(function () { - console.log(ghjkDir); - }), - ) - .command( - "ghjkfile-path", - new cliffy_cmd.Command() - .description("Print the path of the ghjk.ts used") - .action(function () { - console.log(ghjkfilePath); - }), - ) - .command( - "config", - new cliffy_cmd.Command() - .description( - "Print the extracted ans serialized config from the ghjkfile", - ) - .action(function () { - console.log(Deno.inspect(serializedConfig, { - depth: 10, - colors: isColorfulTty(), - })); - }), - ); + serializedConfig = config; + // lock entries are generated across program usage + // so we defer writing it out until the end + defer.push(writeLockFile); + + for (const [cmdName, [cmd, src]] of Object.entries(configCommands)) { + const conflict = subcmds[cmdName]; + if (conflict) { + throw new Error( + `CLI command conflict under name "${cmdName}" from host and module "${src}"`, + ); + } + subcmds[cmdName] = cmd; + } } - let cmd: cliffy_cmd.Command = new cliffy_cmd.Command() + const root = new cliffy_cmd.Command() .name("ghjk") .version("0.1.1") // FIXME: better way to resolve version .description("Programmable runtime manager.") .action(function () { this.showHelp(); - }); + }) + .command( + "completions", + new cliffy_cmd.CompletionsCommand(), + ) + .command( + "deno", + new cliffy_cmd.Command() + .description("Access the deno cli.") + .useRawArgs() + .action(async function (_, ...args) { + logger().debug(args); + await $.raw`${Deno.execPath()} ${args}` + .env("DENO_EXEC_PATH", Deno.execPath()); + }), + ) + .command( + "print", + new cliffy_cmd.Command() + .description("Emit different discovered and built values to stdout.") + .action(function () { + this.showHelp(); + }) + .command( + "share-dir-path", + new cliffy_cmd.Command() + .description("Print the path where ghjk is installed in.") + .action(function () { + if (!ghjkShareDir) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(ghjkShareDir); + }), + ) + .command( + "ghjkdir-path", + new cliffy_cmd.Command() + .description("Print the path where ghjk is installed in.") + .action(function () { + if (!ghjkDir) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(ghjkDir); + }), + ) + .command( + "ghjkfile-path", + new cliffy_cmd.Command() + .description("Print the path of the ghjk.ts used") + .action(function () { + if (!ghjkfilePath) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(ghjkfilePath); + }), + ) + .command( + "config", + new cliffy_cmd.Command() + .description( + "Print the extracted ans serialized config from the ghjkfile", + ) + .action(function () { + if (!serializedConfig) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(Deno.inspect(serializedConfig, { + depth: 10, + colors: isColorfulTty(), + })); + }), + ), + ); for (const [name, subcmd] of Object.entries(subcmds)) { - cmd = cmd.command(name, subcmd); + root.command(name, subcmd); } - await cmd - .command("completions", new cliffy_cmd.CompletionsCommand()) - .parse(Deno.args); + await root.parse(Deno.args); + await Promise.all(defer.map((fn) => fn())); } async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { @@ -136,15 +189,16 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { const lockFilePath = ghjkDirPath.join("lock.json"); const hashFilePath = ghjkDirPath.join("hash.json"); - const subCommands = {} as Record; + // command name to [cmd, source module id] + const subCommands = {} as Record; const lockEntries = {} as Record; - const curEnvVars = Deno.env.toObject(); - const foundLockObj = await readLockFile(lockFilePath); const foundHashObj = await readHashFile(hashFilePath); - const ghjkfileHash = await fileHashHex(hcx, configPath); + const ghjkfileHash = await fileDigestHex(hcx, configPath); + + const curEnvVars = Deno.env.toObject(); let configExt: SerializedConfigExt | null = null; // TODO: figure out cross platform lockfiles :O @@ -178,7 +232,7 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { const envHashesMatch = async () => { const oldHashes = foundHashObj!.envVarHashes; - const newHashes = await hashEnvVars(curEnvVars, [ + const newHashes = await envVarDigests(curEnvVars, [ ...Object.keys(oldHashes), ]); return deep_eql(oldHashes, newHashes); @@ -187,7 +241,7 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { const cwd = $.path(Deno.cwd()); const fileHashesMatch = async () => { const oldHashes = foundHashObj!.readFileHashes; - const newHashes = await hashFiles(hcx, [ + const newHashes = await fileDigests(hcx, [ ...Object.keys(oldHashes), ], cwd); return deep_eql(oldHashes, newHashes); @@ -204,14 +258,16 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { }; // avoid reserializing the config if // the ghjkfile and environment is _satisfcatorily_ - // similar + // similar. "cache validation" if ( + // NOTE: these are ordered by the amount effort it takes + // to check each foundHashObj && foundHashObj.ghjkfileHash == ghjkfileHash && platformMatch() && - await fileHashesMatch() && + await envHashesMatch() && await fileListingsMatch() && - await envHashesMatch() + await fileHashesMatch() ) { configExt = { config: foundLockObj.config, @@ -222,17 +278,13 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { } } + // configExt will be falsy if no lockfile was found + // or if it failed cache validation if (!configExt) { logger().info("serializing ghjkfile", configPath); configExt = await readAndSerializeConfig(hcx, configPath, curEnvVars); } - const newLockObj: zod.infer = { - version: "0", - platform: serializePlatform(Deno.build), - moduleEntries: {} as Record, - config: configExt.config, - }; const newHashObj: zod.infer = { version: "0", ghjkfileHash, @@ -240,7 +292,7 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { readFileHashes: configExt.readFileHashes, listedFiles: configExt.listedFiles, }; - const instances = []; + const instances = [] as [string, ModuleBase, unknown][]; for (const man of configExt.config.modules) { const mod = std_modules.map[man.id]; if (!mod) { @@ -250,60 +302,80 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { const pMan = await instance.processManifest( gcx, man, - newLockObj.config.blackboard, + configExt.config.blackboard, lockEntries[man.id], ); instances.push([man.id, instance, pMan] as const); - subCommands[man.id] = instance.command(gcx, pMan); - } - // generate the lock entries after *all* the modules - // are done processing their config to allow - // any shared stores to be properly populated - // e.g. the resolution memo store - newLockObj.moduleEntries = Object.fromEntries( - await Array.fromAsync( - instances.map( - async ( - [id, instance, pMan], - ) => [id, await instance.genLockEntry(gcx, pMan)], - ), - ), - ); - // avoid writing lockfile if nothing's changed - if (!foundLockObj || !deep_eql(newLockObj, foundLockObj)) { - await lockFilePath.writeJsonPretty(newLockObj); + for (const [cmdName, cmd] of Object.entries(instance.commands(gcx, pMan))) { + const conflict = subCommands[cmdName]; + if (conflict) { + throw new Error( + `CLI command conflict under name "${cmdName}" from modules "${man.id}" & "${ + conflict[1] + }"`, + ); + } + subCommands[cmdName] = [cmd, man.id]; + } } if (!foundHashObj || !deep_eql(newHashObj, foundHashObj)) { await hashFilePath.writeJsonPretty(newHashObj); } - return { subCommands, serializedConfig: configExt.config }; + + return { + subCommands, + serializedConfig: configExt.config, + async writeLockFile() { + const newLockObj: zod.infer = { + version: "0", + platform: serializePlatform(Deno.build), + moduleEntries: {} as Record, + config: configExt!.config, + }; + + // generate the lock entries after *all* the modules + // are done processing their config to allow + // any shared stores to be properly populated + // e.g. the resolution memo store + newLockObj.moduleEntries = Object.fromEntries( + await Array.fromAsync( + instances.map( + async ( + [id, instance, pMan], + ) => [id, await instance.genLockEntry(gcx, pMan)], + ), + ), + ); + // avoid writing lockfile if nothing's changed + if (!foundLockObj || !deep_eql(newLockObj, foundLockObj)) { + await lockFilePath.writeJsonPretty(newLockObj); + } + }, + }; } -type HashStore = Record; +type DigestsMap = Record; -type SerializedConfigExt = { - config: SerializedConfig; - envVarHashes: HashStore; - readFileHashes: HashStore; - listedFiles: string[]; -}; +type SerializedConfigExt = DePromisify< + ReturnType +>; async function readAndSerializeConfig( hcx: HostCtx, - configPath: PathRef, + configPath: Path, envVars: Record, -): Promise { +) { switch (configPath.extname()) { case "": logger().warn("config file has no extension, assuming deno config"); /* falls through */ case ".ts": { logger().debug("serializing ts config", configPath); - const res = await deno.getSerializedConfig( + const res = await denoFile.getSerializedConfig( configPath.toFileUrl().href, envVars, ); - const envVarHashes = await hashEnvVars(envVars, res.accessedEnvKeys); + const envVarHashes = await envVarDigests(envVars, res.accessedEnvKeys); const cwd = $.path(Deno.cwd()); const cwdStr = cwd.toString(); const listedFiles = res.listedFiles @@ -313,7 +385,7 @@ async function readAndSerializeConfig( // consider reading mtime of files when read by the serializer and comparing // them before hashing to make sure we get the same file // not sure what to do if it has changed though, re-serialize? - const readFileHashes = await hashFiles(hcx, res.readFiles, cwd); + const readFileHashes = await fileDigests(hcx, res.readFiles, cwd); return { config: validateRawConfig(res.config, configPath), @@ -335,15 +407,16 @@ async function readAndSerializeConfig( function validateRawConfig( raw: unknown, - configPath: PathRef, + configPath: Path, ): SerializedConfig { const res = validators.serializedConfig.safeParse(raw); if (!res.success) { - logger().error("zod error", res.error); - logger().error("serializedConf", raw); - throw new Error(`error parsing seralized config from ${configPath}`); + throw new Error( + `error parsing seralized config from ${configPath}: ${ + zod_val_err.fromZodError(res.error).toString() + }`, + ); } - return res.data; } @@ -354,18 +427,31 @@ const lockObjValidator = zod.object({ config: validators.serializedConfig, }); -type LockObject = zod.infer; - -async function readLockFile(lockFilePath: PathRef): Promise { - const raw = await lockFilePath.readMaybeJson(); - if (!raw) return null; - const res = lockObjValidator.safeParse(raw); - if (!res.success) { - throw new Error(`error parsing lockfile from ${lockFilePath}`, { - cause: res.error, - }); +/** + * The lock.json file stores the serialized config and some entries + * from modules. It's primary purpose is as a memo store to avoid + * re-serialization on each CLI invocation. + */ +async function readLockFile(lockFilePath: Path) { + const rawStr = await lockFilePath.readMaybeText(); + if (!rawStr) return; + try { + const rawJson = JSON.parse(rawStr); + const res = lockObjValidator.safeParse(rawJson); + if (!res.success) { + throw zod_val_err.fromZodError(res.error); + } + return res.data; + } catch (err) { + logger().error( + `error parsing lockfile from ${lockFilePath}: ${err.toString()}`, + ); + if (Deno.stderr.isTerminal() && await $.confirm("Discard lockfile?")) { + return; + } else { + throw err; + } } - return res.data; } const hashObjValidator = zod.object({ @@ -377,20 +463,33 @@ const hashObjValidator = zod.object({ // TODO: track listed dirs in case a `walk`ed directory has a new entry }); -async function readHashFile(hashFilePath: PathRef) { - const raw = await hashFilePath.readMaybeJson(); - if (!raw) return; - const res = hashObjValidator.safeParse(raw); - if (!res.success) { - throw new Error(`error parsing hashfile from ${hashObjValidator}`, { - cause: res.error, - }); +/** + * The hash.json file stores the digests of all external accesses + * of a ghjkfile during serialization. The primary purpose is to + * do "cache invalidation" on ghjkfiles, re-serializing them if + * any of the digests change. + */ +async function readHashFile(hashFilePath: Path) { + const rawStr = await hashFilePath.readMaybeText(); + if (!rawStr) return; + try { + const rawJson = JSON.parse(rawStr); + const res = hashObjValidator.safeParse(rawJson); + if (!res.success) { + throw zod_val_err.fromZodError(res.error); + } + return res.data; + } catch (err) { + logger().error( + `error parsing hashfile from ${hashObjValidator}: ${err.toString()}`, + ); + logger().warn("discarding invalid hashfile"); + return; } - return res.data; } -async function hashEnvVars(all: Record, accessed: string[]) { - const hashes = {} as HashStore; +async function envVarDigests(all: Record, accessed: string[]) { + const hashes = {} as DigestsMap; for (const key of accessed) { const val = all[key]; if (!val) { @@ -403,10 +502,10 @@ async function hashEnvVars(all: Record, accessed: string[]) { return hashes; } -async function hashFiles(hcx: HostCtx, readFiles: string[], cwd: PathRef) { +async function fileDigests(hcx: HostCtx, readFiles: string[], cwd: Path) { const cwdStr = cwd.toString(); - const readFileHashes = {} as HashStore; - for (const path of readFiles) { + const readFileHashes = {} as DigestsMap; + await Promise.all(readFiles.map(async (path) => { const pathRef = cwd.resolve(path); const relativePath = pathRef .toString() @@ -415,7 +514,7 @@ async function hashFiles(hcx: HostCtx, readFiles: string[], cwd: PathRef) { const stat = await pathRef.lstat(); if (stat) { const contentHash = (stat.isFile || stat.isSymlink) - ? await fileHashHex(hcx, pathRef) + ? await fileDigestHex(hcx, pathRef) : null; readFileHashes[relativePath] = await objectHashHex({ ...stat, @@ -424,11 +523,15 @@ async function hashFiles(hcx: HostCtx, readFiles: string[], cwd: PathRef) { } else { readFileHashes[relativePath] = null; } - } + })); return readFileHashes; } -function fileHashHex(hcx: HostCtx, path: PathRef) { +/** + * Returns the hash digest of a file. Makes use of a memo + * to dedupe work. + */ +function fileDigestHex(hcx: HostCtx, path: Path) { const absolute = path.resolve().toString(); let promise = hcx.fileHashMemoStore.get(absolute); if (!promise) { diff --git a/install.ts b/install.ts index 16db7830..0efef5b1 100755 --- a/install.ts +++ b/install.ts @@ -3,25 +3,33 @@ //! Install ghjk for the current user import "./setup_logger.ts"; -import { defaultInstallArgs, detectShell, install } from "./install/mod.ts"; +import { defaultInstallArgs, install } from "./install/mod.ts"; +import { detectShell } from "./utils/mod.ts"; if (import.meta.main) { const skipBinInstall = Deno.env.get("GHJK_INSTALL_SKIP_EXE"); const noLockfile = Deno.env.get("GHJK_INSTALL_NO_LOCKFILE"); + let shellsToHook = Deno.env.get("GHJK_INSTALL_HOOK_SHELLS") + ?.split(",") + ?.map((str) => str.trim()) + ?.filter((str) => str.length > 0); + if (!shellsToHook) { + const userShell = await detectShell(); + if (!userShell) { + throw new Error( + "Unable to detect user's shell. Set $GHJK_INSTALL_HOOK_SHELLS to an empty string if no shell hooks are desired.", + ); + } + shellsToHook = [userShell]; + } await install({ ...defaultInstallArgs, ghjkShareDir: Deno.env.get("GHJK_SHARE_DIR") ?? defaultInstallArgs.ghjkShareDir, skipExecInstall: !!skipBinInstall && skipBinInstall != "0" && skipBinInstall != "false", - shellsToHook: Deno.env.get("GHJK_INSTALL_HOOK_SHELLS") - ?.split(",") - ?.map((str) => str.trim()) - ?.filter((str) => str.length > 0) ?? - [ - await detectShell(), - ], + shellsToHook, ghjkExecInstallDir: Deno.env.get("GHJK_INSTALL_EXE_DIR") ?? defaultInstallArgs.ghjkExecInstallDir, ghjkExecDenoExec: Deno.env.get("GHJK_INSTALL_DENO_EXEC") ?? diff --git a/install/ghjk.sh b/install/ghjk.sh index 518f29ba..4ea0977c 100644 --- a/install/ghjk.sh +++ b/install/ghjk.sh @@ -3,6 +3,9 @@ export GHJK_SHARE_DIR="${GHJK_SHARE_DIR:-__GHJK_SHARE_DIR__}" export DENO_DIR="${GHJK_DENO_DIR:-__DENO_CACHE_DIR}" export DENO_NO_UPDATE_CHECK=1 +# NOTE: avoid putting too much in here as the ghjk bin is meant +# to be optional. + # if ghjkfile var is set, set the GHJK_DIR overriding # any set by the user if [ -n "${GHJKFILE+x}" ]; then @@ -36,4 +39,4 @@ fi # we don't want to quote $lock_flag as it's not exactly a single # string param to deno # shellcheck disable=SC2086 -__DENO_EXEC__ run --unstable-kv --unstable-worker-options -A $lock_flag __MAIN_TS_URL__ "$@" +exec __DENO_EXEC__ run --unstable-kv --unstable-worker-options -A $lock_flag __MAIN_TS_URL__ "$@" diff --git a/install/hook.fish b/install/hook.fish index 282e552c..c5672389 100644 --- a/install/hook.fish +++ b/install/hook.fish @@ -1,9 +1,20 @@ -function ghjk_reload --on-variable PWD +function get_ctime_ts + switch (uname -s | tr '[:upper:]' '[:lower:]') + case "linux" + stat -c "%Y" $argv + case "darwin" + stat -f "%Sm" -t "%s" $argv + case "*" + stat -c "%Y" $argv + end +end + +function ghjk_reload --on-variable PWD --on-event ghjk_env_dir_change # --on-variable GHJK_ENV if set --query GHJK_CLEANUP_FISH # restore previous env eval $GHJK_CLEANUP_FISH + set --erase GHJK_CLEANUP_FISH end - set --erase GHJK_CLEANUP_FISH set --local cur_dir set --local local_ghjk_dir $GHJK_DIR @@ -35,26 +46,46 @@ function ghjk_reload --on-variable PWD end if test -n "$local_ghjk_dir" - # locate the default env - set --local default_env $local_ghjk_dir/envs/default - if test -d $default_env + # locate the active env + set --local active_env "$GHJK_ENV" + test -z $active_env; and set --local active_env default + set --local active_env_dir $local_ghjk_dir/envs/$active_env + if test -d $active_env_dir # load the shim - . $default_env/loader.fish + . $active_env_dir/activate.fish + # export variables to assist in change detection + set --global --export GHJK_LAST_ENV_DIR $active_env_dir + set --global --export GHJK_LAST_ENV_DIR_CTIME (get_ctime_ts $active_env_dir/activate.fish) # FIXME: older versions of fish don't recognize -ot # those in debian for example # FIXME: this assumes ghjkfile is of kind ghjk.ts - if test $default_env/loader.fish -ot $cur_dir/ghjk.ts + if test $active_env_dir/activate.fish -ot $cur_dir/ghjk.ts set_color FF4500 - echo "[ghjk] Detected drift from default environment, please sync..." + if test $active_env = "default" + echo "[ghjk] Possible drift from default environment, please sync..." + else + echo "[ghjk] Possible drift from active environment ($active_env), please sync..." + end set_color normal end else set_color FF4500 - echo "[ghjk] No default environment found, please sync..." + if test $active_env = "default" + echo "[ghjk] Default environment not found, please sync..." + else + echo "[ghjk] Active environment ($active_env) not found, please sync..." + end set_color normal end end end +# trigger reload when the env dir loader ctime changes +function ghjk_env_dir_watcher --on-event fish_postexec + if set --query GHJK_LAST_ENV_DIR; and test (get_ctime_ts $GHJK_LAST_ENV_DIR/activate.fish) -gt "$GHJK_LAST_ENV_DIR_CTIME" + emit ghjk_env_dir_change + end +end + ghjk_reload diff --git a/install/hook.sh b/install/hook.sh index f612aa59..544d43d6 100644 --- a/install/hook.sh +++ b/install/hook.sh @@ -1,15 +1,28 @@ # shellcheck disable=SC2148 # keep this posix compatible as it supports bash and zsh +get_ctime_ts () { + case "$(uname -s | tr '[:upper:]' '[:lower:]')" in + "linux") + stat -c "%Y" "$1" + ;; + "darwin") + stat -f "%Sm" -t "%s" "$1" + ;; + "*") + stat -c "%Y" "$1" + ;; + esac +} + ghjk_reload() { if [ -n "${GHJK_CLEANUP_POSIX+x}" ]; then # restore previous env eval "$GHJK_CLEANUP_POSIX" + unset GHJK_CLEANUP_POSIX fi - unset GHJK_CLEANUP_POSIX - local cur_dir - local local_ghjk_dir="${GHJK_DIR:-}" + local_ghjk_dir="${GHJK_DIR:-}" # if $GHJKFILE is set, set the GHJK_DIR overriding # any set by the user if [ -n "${GHJKFILE+x}" ]; then @@ -38,21 +51,36 @@ ghjk_reload() { if [ -n "$local_ghjk_dir" ]; then # export GHJK_DIR - # locate the default env - default_env="$local_ghjk_dir/envs/default" - if [ -d "$default_env" ]; then + # locate the active env + active_env="${GHJK_ENV:-default}"; + active_env_dir="$local_ghjk_dir/envs/$active_env" + if [ -d "$active_env_dir" ]; then # load the shim # shellcheck source=/dev/null - . "$default_env/loader.sh" + . "$active_env_dir/activate.sh" + # export variables to assist in change detection + GHJK_LAST_ENV_DIR="$active_env_dir" + GHJK_LAST_ENV_DIR_CTIME="$(get_ctime_ts "$active_env_dir/activate.sh")" + export GHJK_LAST_ENV_DIR + export GHJK_LAST_ENV_DIR_CTIME # FIXME: -ot not valid in POSIX # FIXME: this assumes ghjkfile is of kind ghjk.ts # shellcheck disable=SC3000-SC4000 - if [ "$default_env/loader.sh" -ot "$cur_dir/ghjk.ts" ]; then - printf "\033[0;33m[ghjk] Detected drift from default environment, please sync...\033[0m\n" + if [ "$active_env_dir/activate.sh" -ot "$cur_dir/ghjk.ts" ]; then + if [ "$active_env" = "default" ]; then + printf "\033[0;33m[ghjk] Possible drift from default environment, please sync...\033[0m\n" + else + printf "\033[0;33m[ghjk] Possible drift from active environment (%s), please sync...\033[0m\n" "$active_env" + fi + fi else - printf "\033[0;31m[ghjk] No default environment found, please sync...\033[0m\n" + if [ "$active_env" = "default" ]; then + printf "\033[0;31m[ghjk] Default environment not set up, please sync...\033[0m\n" + else + printf "\033[0;31m[ghjk] Active environment (%s) not set up, please sync...\033[0m\n" "$active_env" + fi fi fi } @@ -61,9 +89,14 @@ ghjk_reload() { export GHJK_LAST_PWD="$PWD" precmd() { - if [ "$GHJK_LAST_PWD" != "$PWD" ]; then + # trigger reload when either + # - the PWD changes + # - the env dir loader ctime changes + if [ "$GHJK_LAST_PWD" != "$PWD" ] || + [ "$(get_ctime_ts "$GHJK_LAST_ENV_DIR/activate.sh")" -gt "$GHJK_LAST_ENV_DIR_CTIME" ]; then ghjk_reload export GHJK_LAST_PWD="$PWD" + # export GHJK_LAST_ENV="$GHJK_ENV" fi } diff --git a/install/mod.ts b/install/mod.ts index 0cbbf40c..219b497d 100644 --- a/install/mod.ts +++ b/install/mod.ts @@ -33,18 +33,6 @@ const getHooksVfs = async () => ({ ), }); -export async function detectShell(): Promise { - let path = Deno.env.get("SHELL"); - if (!path) { - try { - path = await $`ps -p ${Deno.ppid} -o comm=`.text(); - } catch (err) { - throw new Error(`cannot get parent process name: ${err}`); - } - } - return std_path.basename(path, ".exe").toLowerCase().trim(); -} - async function unpackVFS( vfs: Record, baseDir: string, @@ -101,24 +89,37 @@ interface InstallArgs { homeDir: string; ghjkShareDir: string; shellsToHook: string[]; - /// The mark used when adding the hook to the user's shell rcs - /// Override t + /** The mark used when adding the hook to the user's shell rcs. + * Override to allow multiple hooks in your rc. + */ shellHookMarker: string; - /// The ghjk bin is optional, one can always invoke it - /// using `deno run --flags uri/to/ghjk/main.ts`; + /** + * The ghjk bin is optional, one can always invoke it + * using `deno run --flags uri/to/ghjk/main.ts`; + */ skipExecInstall: boolean; - /// The directory in which to install the ghjk exec - /// Preferrably, one that's in PATH + /** The directory in which to install the ghjk exec + * Preferrably, one that's in PATH + */ ghjkExecInstallDir: string; - /// the deno exec to be used by the ghjk executable - /// by default will be "deno" i.e. whatever the shell resolves that to + /** + * The deno exec to be used by the ghjk executable + * by default will be "deno" i.e. whatever in $PATH that resolves that to. + */ ghjkExecDenoExec: string; - /// The cache dir to use by the ghjk deno installation + /** + * The cache dir to use by the ghjk deno installation. + */ ghjkDenoCacheDir?: string; - // Disable using a lockfile for the ghjk command + /** + * Disable using a lockfile for the ghjk command + */ noLockfile: boolean; } +/** + * @field: + */ export const defaultInstallArgs: InstallArgs = { ghjkShareDir: std_path.resolve(dirs().shareDir, "ghjk"), homeDir: dirs().homeDir, @@ -128,8 +129,10 @@ export const defaultInstallArgs: InstallArgs = { // TODO: respect xdg dirs ghjkExecInstallDir: std_path.resolve(dirs().homeDir, ".local", "bin"), ghjkExecDenoExec: Deno.execPath(), - // the default behvaior kicks in with ghjkDenoCacheDir is falsy - // ghjkDenoCacheDir: undefined, + /** + * the default behvaior kicks in with ghjkDenoCacheDir is falsy + * ghjkDenoCacheDir: undefined, + */ noLockfile: false, }; diff --git a/mod.ts b/mod.ts index fd2c986d..65fd9bff 100644 --- a/mod.ts +++ b/mod.ts @@ -1,549 +1,51 @@ //! This module is intended to be re-exported by `ghjk.ts` config scripts. Please //! avoid importing elsewhere at it has side-effects. -// NOTE: avoid adding sources of randomness -// here to make the resulting config reasonably stable -// across serializaiton. No random identifiers. // TODO: harden most of the items in here import "./setup_logger.ts"; // ports specific imports -import portsValidators from "./modules/ports/types.ts"; import type { - AllowedPortDep, InstallConfigFat, - InstallSet, - InstallSetRefProvision, - PortsModuleConfigHashed, PortsModuleSecureConfig, } from "./modules/ports/types.ts"; import logger from "./utils/logger.ts"; +import { $ } from "./utils/mod.ts"; import { - $, - defaultCommandBuilder, - thinInstallConfig, - unwrapParseRes, -} from "./utils/mod.ts"; -import * as std_ports from "./modules/ports/std.ts"; -import * as cpy from "./ports/cpy_bs.ts"; -import * as node from "./ports/node.ts"; -// host -import type { SerializedConfig } from "./host/types.ts"; -import * as std_modules from "./modules/std.ts"; -// tasks -import { dax, jsonHash, objectHash } from "./deps/common.ts"; + EnvBuilder, + GhjkfileBuilder, + stdDeps, + stdSecureConfig, +} from "./ghjkfiles/mod.ts"; +import type { EnvDefArgs, TaskDefArgs, TaskFn } from "./ghjkfiles/mod.ts"; // WARN: this module has side-effects and only ever import // types from it import type { ExecTaskArgs } from "./modules/tasks/deno.ts"; -import { TasksModuleConfig } from "./modules/tasks/types.ts"; -// envs -import { - EnvRecipe, - EnvsModuleConfig, - WellKnownProvision, -} from "./modules/envs/types.ts"; const DEFAULT_BASE_ENV_NAME = "main"; -export type EnvDefArgs = { - name: string; - installs?: InstallConfigFat[]; - allowedPortDeps?: AllowedPortDep[]; - /* - * If true or not set, will base the task's env on top - * of the default env (usually `main`). If false, will build on - * top of a new env. If given a string, will use the identified env as a base - * for the task env. - */ - envBase?: string | boolean; -}; - -export type TaskFnArgs = { - $: dax.$Type; - argv: string[]; - env: Record; -}; - -export type TaskFn = (args: TaskFnArgs) => Promise | any; - -/* - * Configuration for a task. - */ -export type TaskDefArgs = { - name: string; - fn: TaskFn; - desc?: string; - dependsOn?: string[]; - workingDir?: string | dax.PathRef; - envVars?: Record; - allowedPortDeps?: AllowedPortDep[]; - installs?: InstallConfigFat[]; - envBase?: string | boolean; -}; - -class GhjkfileBuilder { - #installSets = new Map(); - #tasks = {} as Record; - #bb = new Map(); - #seenEnvs: Record = {}; - - addInstall(setId: string, configUnclean: InstallConfigFat) { - const config = unwrapParseRes( - portsValidators.installConfigFat.safeParse(configUnclean), - { - config: configUnclean, - }, - `error parsing InstallConfig`, - ); - - const set = this.#getSet(setId); - set.installs.push(config); - logger().debug("install added", config); - } - - setAllowedPortDeps(setId: string, deps: AllowedPortDep[]) { - const set = this.#getSet(setId); - set.allowedDeps = Object.fromEntries( - deps.map(( - dep, - ) => [dep.manifest.name, dep]), - ); - } - - addTask(args: TaskDefArgs) { - // NOTE: we make sure the env base declared here exists - // this call is necessary to make sure that a `task` can - // be declared before the `env` but still depend on it. - // Order-indepency like this makes the `ghjk.ts` way less - // brittle. - if (typeof args.envBase == "string") { - this.addEnv({ name: args.envBase }); - } - - this.#tasks[args.name] = { - ...args, - name, - }; - return args.name; - } - - addEnv(args: EnvDefArgs) { - let env = this.#seenEnvs[args.name]?.[0]; - if (!env) { - let finalizer: EnvFinalizer; - env = new EnvBuilder(this, (fin) => finalizer = fin, args.name); - this.#seenEnvs[args.name] = [env, finalizer!]; - } - if (args.envBase !== undefined) { - env.base(args.envBase); - } - if (args.installs) { - env.install(...args.installs); - } - if (args.allowedPortDeps) { - env.allowedPortDeps(args.allowedPortDeps); - } - return env; - } - - async execTask( - { name, workingDir, envVars, argv }: ExecTaskArgs, - ) { - const task = this.#tasks[name]; - if (!task) { - throw new Error(`no task defined under "${name}"`); - } - const custom$ = $.build$({ - commandBuilder: defaultCommandBuilder().env(envVars).cwd(workingDir), - }); - await task.fn({ argv, env: envVars, $: custom$ }); - } - - toConfig(secureConfig: PortsModuleSecureConfig | undefined) { - try { - const defaultEnv = secureConfig?.defaultEnv ?? DEFAULT_BASE_ENV_NAME; - const defaultBaseEnv = secureConfig?.defaultBaseEnv ?? - DEFAULT_BASE_ENV_NAME; - const envsConfig = this.#processEnvs( - defaultEnv, - defaultBaseEnv, - ); - const tasksConfig = this.#processTasks(envsConfig, defaultBaseEnv); - const portsConfig = this.#processInstalls( - secureConfig?.masterPortDepAllowList ?? stdDeps(), - ); - - const config: SerializedConfig = { - modules: [{ - id: std_modules.ports, - config: portsConfig, - }, { - id: std_modules.tasks, - config: tasksConfig, - }, { - id: std_modules.envs, - config: envsConfig, - }], - blackboard: Object.fromEntries(this.#bb.entries()), - }; - return config; - } catch (cause) { - throw new Error(`error constructing config for serialization`, { cause }); - } - } - - #getSet(setId: string) { - let set = this.#installSets.get(setId); - if (!set) { - set = { installs: [], allowedDeps: {} }; - this.#installSets.set(setId, set); - } - return set; - } - - #addToBlackboard(inp: unknown) { - // jsonHash.digest is async - const hash = objectHash(jsonHash.canonicalize(inp as jsonHash.Tree)); - - if (!this.#bb.has(hash)) { - this.#bb.set(hash, inp); - } - return hash; - } - - // this processes the defined envs, normalizing dependency (i.e. "envBase") - // relationships to produce the standard EnvsModuleConfig - #processEnvs( - defaultEnv: string, - defaultBaseEnv: string, - ) { - const all = {} as Record< - string, - ReturnType & { envBaseResolved: null | string } - >; - const indie = [] as string[]; - const revDeps = new Map(); - for ( - const [_name, [_builder, finalizer]] of Object.entries(this.#seenEnvs) - ) { - const final = finalizer(); - const { name, envBase } = final; - const envBaseResolved = typeof envBase === "string" - ? envBase - : envBase - ? defaultBaseEnv - : null; - all[name] = { ...final, envBaseResolved }; - if (envBaseResolved) { - let parentRevDeps = revDeps.get(envBaseResolved); - if (!parentRevDeps) { - parentRevDeps = []; - revDeps.set(envBaseResolved, parentRevDeps); - } - parentRevDeps.push(final.name); - } else { - indie.push(name); - } - } - const processed = {} as Record; - const out: EnvsModuleConfig = { envs: {}, defaultEnv }; - const workingSet = [...indie]; - while (workingSet.length > 0) { - const item = workingSet.pop()!; - const final = all[item]; - - const base = final.envBaseResolved - ? processed[final.envBaseResolved] - : null; - - let processedInstallSetId: string | undefined; - { - const installSet = this.#installSets.get(final.installSetId); - if (installSet) { - // if base also has an install set - if (base?.installSetId) { - // merge the parent's installs into this one - const baseSet = this.#installSets.get( - base.installSetId, - )!; - const mergedInstallsSet = new Set([ - ...installSet.installs, - ...baseSet.installs, - ]); - installSet.installs = [...mergedInstallsSet.values()]; - for ( - const [key, val] of Object.entries(baseSet.allowedDeps) - ) { - // prefer the port dep config of the child over any - // similar deps in the parent - if (!installSet.allowedDeps[key]) { - installSet.allowedDeps[key] = val; - } - } - } - processedInstallSetId = final.installSetId; - } // if there's no install set found under the id - else { - // implies that the env has not ports explicitly configured - if (base) { - processedInstallSetId = base.installSetId; - } - } - } - processed[final.name] = { installSetId: processedInstallSetId }; - out.envs[final.name] = { - provides: [ - ...Object.entries(final.vars).map(( - [key, val], - ) => { - const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; - return prov; - }), - ], - }; - if (processedInstallSetId) { - const prov: InstallSetRefProvision = { - ty: "ghjk.ports.InstallSetRef", - setId: processedInstallSetId, - }; - out.envs[final.name].provides.push(prov); - } - - const curRevDeps = revDeps.get(final.name); - if (curRevDeps) { - workingSet.push(...curRevDeps); - revDeps.delete(final.name); - } - } - return out; - } - - #processTasks(envsConfig: EnvsModuleConfig, defaultBaseEnv: string) { - const out: TasksModuleConfig = { - envs: {}, - tasks: {}, - }; - for ( - const [name, args] of Object - .entries( - this.#tasks, - ) - ) { - const { workingDir, desc, dependsOn, envBase } = args; - const envBaseResolved = typeof envBase === "string" - ? envBase - : envBase - ? defaultBaseEnv - : null; - - const envBaseRecipe = envBaseResolved - ? envsConfig.envs[envBaseResolved] - : null; - - const taskEnvRecipe: EnvRecipe = { - provides: [], - }; - - const taskInstallSet: InstallSet = { - installs: args.installs ?? [], - allowedDeps: Object.fromEntries( - (args.allowedPortDeps ?? []).map((dep) => [dep.manifest.name, dep]), - ), - }; - - const mergedEnvVars = args.envVars ?? {}; - if (envBaseRecipe) { - for ( - const prov of envBaseRecipe - .provides as ( - | WellKnownProvision - | InstallSetRefProvision - )[] - ) { - if (prov.ty == "posix.envVar") { - if (!mergedEnvVars[prov.key]) { - mergedEnvVars[prov.key] = prov.val; - } - } else if (prov.ty == "ghjk.ports.InstallSetRef") { - const baseSet = this.#installSets.get(prov.setId)!; - const mergedInstallsSet = new Set([ - ...taskInstallSet.installs, - ...baseSet.installs, - ]); - taskInstallSet.installs = [...mergedInstallsSet.values()]; - for ( - const [key, val] of Object.entries(baseSet.allowedDeps) - ) { - // prefer the port dep config of the child over any - // similar deps in the base - if (!taskInstallSet.allowedDeps[key]) { - taskInstallSet.allowedDeps[key] = val; - } - } - } else { - taskEnvRecipe.provides.push(prov); - } - } - } - if (taskInstallSet.installs.length > 0) { - const setId = `ghjkTaskInstSet___${name}`; - this.#installSets.set(setId, taskInstallSet); - const prov: InstallSetRefProvision = { - ty: "ghjk.ports.InstallSetRef", - setId, - }; - taskEnvRecipe.provides.push(prov); - } - - taskEnvRecipe.provides.push( - ...Object.entries(mergedEnvVars).map(( - [key, val], - ) => { - const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; - return prov; - }), - ); - - const envHash = objectHash( - jsonHash.canonicalize(taskEnvRecipe as jsonHash.Tree), - ); - out.envs[envHash] = taskEnvRecipe; - - out.tasks[name] = { - name, - workingDir: typeof workingDir == "object" - ? workingDir.toString() - : workingDir, - desc, - dependsOn, - envHash, - }; - } - for (const [name, { dependsOn }] of Object.entries(out.tasks)) { - for (const depName of dependsOn ?? []) { - if (!out.tasks[depName]) { - throw new Error( - `task "${name}" depend on non-existent task "${depName}"`, - ); - } - } - } - - return out; - } - - #processInstalls(masterAllowList: AllowedPortDep[]) { - const out: PortsModuleConfigHashed = { - sets: {}, - }; - const masterPortDepAllowList = Object.fromEntries( - masterAllowList.map((dep) => [dep.manifest.name, dep] as const), - ); - for ( - const [setId, set] of this.#installSets.entries() - ) { - for (const [portName, _] of Object.entries(set.allowedDeps)) { - if (!masterPortDepAllowList[portName]) { - throw new Error( - `"${portName}" is in allowedPortDeps list of install set "${setId}" but not in the masterPortDepAllowList`, - ); - } - } - for (const [name, hash] of Object.entries(masterPortDepAllowList)) { - if (!set.allowedDeps[name]) { - set.allowedDeps[name] = hash; - } - } - out.sets[setId] = { - installs: set.installs.map((inst) => this.#addToBlackboard(inst)), - allowedDeps: this.#addToBlackboard(Object.fromEntries( - Object.entries(set.allowedDeps).map( - ([key, dep]) => [key, this.#addToBlackboard(dep)], - ), - )), - }; - } - return out; - } -} - -type EnvFinalizer = () => { - name: string; - installSetId: string; - envBase: string | boolean; - vars: Record; -}; - -// this class will be exposed to users and thus features -// a contrived implementation of the `build`/`finalize` method -// all to avoid exposing the function in the public api -class EnvBuilder { - #installSetId: string; - #file: GhjkfileBuilder; - #base: string | boolean = true; - #vars: Record = {}; - - constructor( - file: GhjkfileBuilder, - setFinalizer: (fin: EnvFinalizer) => void, - public name: string, - ) { - this.#file = file; - this.#installSetId = `ghjkEnvProvInstSet___${name}`; - setFinalizer(() => ({ - name: this.name, - installSetId: this.#installSetId, - envBase: this.#base, - vars: this.#vars, - })); - } - - base(base: string | boolean) { - this.#base = base; - } - - /* - * Provision a port install in the environment. - */ - install(...configs: InstallConfigFat[]) { - for (const config of configs) { - this.#file.addInstall(this.#installSetId, config); - } - return this; - } - - /* - * This is treated as a single set and will replace previously any configured set. - */ - allowedPortDeps(deps: AllowedPortDep[]) { - this.#file.setAllowedPortDeps(this.#installSetId, deps); - } - - var(key: string, val: string) { - this.vars({ [key]: val }); - } - - vars(envVars: Record) { - Object.assign(this.#vars, envVars); - } -} - const file = new GhjkfileBuilder(); const mainEnv = file.addEnv({ name: DEFAULT_BASE_ENV_NAME, - envBase: false, + base: false, allowedPortDeps: stdDeps(), + desc: "the default default environment.", }); -export { $, logger }; +export type { EnvDefArgs, TaskDefArgs, TaskFn } from "./ghjkfiles/mod.ts"; +export { $, logger, stdDeps, stdSecureConfig }; // FIXME: ses.lockdown to freeze primoridials // freeze the object to prevent malicious tampering of the secureConfig export const ghjk = Object.freeze({ getConfig: Object.freeze( - (secureConfig: PortsModuleSecureConfig | undefined) => - file.toConfig(secureConfig), + (secureConfig: PortsModuleSecureConfig | undefined) => { + const defaultEnv = secureConfig?.defaultEnv ?? DEFAULT_BASE_ENV_NAME; + const defaultBaseEnv = secureConfig?.defaultBaseEnv ?? + DEFAULT_BASE_ENV_NAME; + return file.toConfig({ defaultEnv, defaultBaseEnv, secureConfig }); + }, ), execTask: Object.freeze( (args: ExecTaskArgs) => file.execTask(args), @@ -591,39 +93,3 @@ export function env( : { ...argsMaybe, name: nameOrArgs }; return file.addEnv(args); } - -export function stdSecureConfig( - args: { - additionalAllowedPorts?: PortsModuleSecureConfig["masterPortDepAllowList"]; - enableRuntimes?: boolean; - } & Pick, -): PortsModuleSecureConfig { - const { additionalAllowedPorts, enableRuntimes = false } = args; - const out: PortsModuleSecureConfig = { - masterPortDepAllowList: [ - ...stdDeps({ enableRuntimes }), - ...additionalAllowedPorts ?? [], - ], - }; - return out; -} - -export function stdDeps(args = { enableRuntimes: false }) { - const out: AllowedPortDep[] = [ - ...Object.values(std_ports.map), - ]; - if (args.enableRuntimes) { - out.push( - ...[ - node.default(), - cpy.default(), - ].map((fatInst) => { - return portsValidators.allowedPortDep.parse({ - manifest: fatInst.port, - defaultInst: thinInstallConfig(fatInst), - }); - }), - ); - } - return out; -} diff --git a/modules/envs/mod.ts b/modules/envs/mod.ts index 5b785217..0ecc97c8 100644 --- a/modules/envs/mod.ts +++ b/modules/envs/mod.ts @@ -1,24 +1,24 @@ -/* - Design: - - `$ ghjk env activate` to switch to default environment - - `$ ghjk env list` - - `$ ghjk env info` - - By default, all things go to the `main` environment -*/ - export * from "./types.ts"; import { cliffy_cmd, zod } from "../../deps/cli.ts"; -import { $, Json, unwrapParseRes } from "../../utils/mod.ts"; - +import { $, detectShellPath, Json, unwrapParseRes } from "../../utils/mod.ts"; import validators from "./types.ts"; -import type { EnvsModuleConfigX } from "./types.ts"; +import type { + EnvRecipeX, + EnvsModuleConfigX, + WellKnownProvision, +} from "./types.ts"; import type { GhjkCtx, ModuleManifest } from "../types.ts"; import { ModuleBase } from "../mod.ts"; - -import { Blackboard } from "../../host/types.ts"; -import { reduceStrangeProvisions } from "./reducer.ts"; +import type { Blackboard } from "../../host/types.ts"; import { cookPosixEnv } from "./posix.ts"; +import { getInstallSetStore, installGraphToSetMeta } from "../ports/inter.ts"; +import type { + InstallSetProvision, + InstallSetRefProvision, +} from "../ports/types.ts"; +import { isColorfulTty } from "../../utils/logger.ts"; +import { buildInstallGraph, syncCtxFromGhjk } from "../ports/sync.ts"; export type EnvsCtx = { activeEnv: string; @@ -49,7 +49,7 @@ export class EnvsModule extends ModuleBase { validators.envsModuleConfig.safeParse(manifest.config), ); - const activeEnv = config.defaultEnv; + const activeEnv = Deno.env.get("GHJK_ENV") ?? config.defaultEnv; return Promise.resolve({ activeEnv, @@ -57,34 +57,121 @@ export class EnvsModule extends ModuleBase { }); } - command( + commands( gcx: GhjkCtx, ecx: EnvsCtx, ) { - const root: cliffy_cmd.Command = new cliffy_cmd - .Command() - .description("Envs module, the cornerstone") - .alias("e") - .alias("env") - .action(function () { - this.showHelp(); - }) - .command( - "sync", - new cliffy_cmd.Command().description("Syncs the environment.") - .action(async () => { - const envName = ecx.activeEnv; - - const env = ecx.config.envs[envName]; - // TODO: diff env and ask confirmation from user - const reducedEnv = await reduceStrangeProvisions(gcx, env); - const envDir = $.path(gcx.ghjkDir).join("envs", envName).toString(); - - await cookPosixEnv(reducedEnv, envDir, true); - }), - ) - .description("Envs module."); - return root; + return { + envs: new cliffy_cmd + .Command() + .description("Envs module, reproducable posix shells environments.") + .alias("e") + // .alias("env") + .action(function () { + this.showHelp(); + }) + .command( + "ls", + new cliffy_cmd.Command() + .description("List environments defined in the ghjkfile.") + .action(() => { + // deno-lint-ignore no-console + console.log( + Object.entries(ecx.config.envs) + .map(([name, { desc }]) => + `${name}${desc ? ": " + desc : ""}` + ) + .join("\n"), + ); + }), + ) + .command( + "activate", + new cliffy_cmd.Command() + .description(`Activate an environment. + +- If no [envName] is specified and no env is currently active, this activates the configured default env [${ecx.config.defaultEnv}].`) + .arguments("[envName:string]") + .option( + "--shell ", + "The shell to use. Tries to detect the current shell if not provided.", + ) + .action(async function ({ shell: shellMaybe }, envNameMaybe) { + const shell = shellMaybe ?? await detectShellPath(); + if (!shell) { + throw new Error( + "unable to detct shell in use. Use `--shell` flag to explicitly pass shell program.", + ); + } + const envName = envNameMaybe ?? ecx.config.defaultEnv; + // FIXME: the ghjk process will be around and consumer resources + // with approach. Ideally, we'd detach the child and exit but this is blocked by + // https://github.com/denoland/deno/issues/5501 is closed + await $`${shell}` + .env({ GHJK_ENV: envName }); + }), + ) + .command( + "cook", + new cliffy_cmd.Command() + .description(`Cooks the environment to a posix shell. + +- If no [envName] is specified, this will cook the active env [${ecx.activeEnv}]`) + .arguments("[envName:string]") + .action(async function (_void, envNameMaybe) { + const envName = envNameMaybe ?? ecx.activeEnv; + await reduceAndCookEnv(gcx, ecx, envName); + }), + ) + .command( + "show", + new cliffy_cmd.Command() + .description(`Show details about an environment. + +- If no [envName] is specified, this shows details of the active env [${ecx.activeEnv}]. +- If no [envName] is specified and no env is active, this shows details of the default env [${ecx.config.defaultEnv}]. + `) + .arguments("[envName:string]") + .action(async function (_void, envNameMaybe) { + const envName = envNameMaybe ?? ecx.activeEnv; + const env = ecx.config.envs[envName]; + if (!env) { + throw new Error(`No env found under given name "${envName}"`); + } + // deno-lint-ignore no-console + console.log(Deno.inspect( + await showableEnv(gcx, env, envName), + { + depth: 10, + colors: isColorfulTty(), + }, + )); + }), + ), + sync: new cliffy_cmd.Command() + .description(`Cooks and activates an environment. + +- If no [envName] is specified and no env is currently active, this syncs the configured default env [${ecx.config.defaultEnv}]. +- If the environment is already active, this doesn't launch a new shell.`) + .arguments("[envName:string]") + .option( + "--shell ", + "The shell to use. Tries to detect the current shell if not provided.", + ) + .action(async function ({ shell: shellMaybe }, envNameMaybe) { + const shell = shellMaybe ?? await detectShellPath(); + if (!shell) { + throw new Error( + "unable to detct shell in use. Use `--shell` flag to explicitly pass shell program.", + ); + } + const envName = envNameMaybe ?? ecx.activeEnv; + await reduceAndCookEnv(gcx, ecx, envName); + if (ecx.activeEnv != envName) { + await $`${shell}`.env({ GHJK_ENV: envName }); + } + }), + }; } loadLockEntry( @@ -108,3 +195,136 @@ export class EnvsModule extends ModuleBase { }; } } + +async function reduceAndCookEnv( + gcx: GhjkCtx, + ecx: EnvsCtx, + envName: string, +) { + const recipe = ecx.config.envs[envName]; + if (!recipe) { + throw new Error(`No env found under given name "${envName}"`); + } + + // TODO: diff env and ask confirmation from user + const envDir = $.path(gcx.ghjkDir).join("envs", envName); + /* + const recipeShowable = await showableEnv(gcx, recipe, envName); + const oldRecipeShowable = {}; + { + const recipeJsonPath = envDir.join("recipe.json"); + const oldRecipeRaw = await recipeJsonPath.readMaybeJson(); + + if (oldRecipeRaw) { + const oldRecipParsed = validators.envRecipe.safeParse(oldRecipeRaw); + if (oldRecipParsed.success) { + Object.assign( + oldRecipeShowable, + await showableEnv(gcx, oldRecipParsed.data, envName), + ); + } else { + logger.error(`invalid env recipe at ${recipeJsonPath}`); + } + } + } + console.log( + diff_kit.diff( + // TODO: canonicalize objects + JSON.stringify(oldRecipeShowable, undefined, 2), + JSON.stringify(recipeShowable, undefined, 2), + // new diff_kit.DiffTerm(), + ), + ); + if (!await $.confirm("cook env?")) { + return; + } + */ + + await cookPosixEnv({ + gcx, + recipe, + envName, + envDir: envDir.toString(), + createShellLoaders: true, + }); + if (envName == ecx.config.defaultEnv) { + const defaultEnvDir = $.path(gcx.ghjkDir).join("envs", "default"); + await $.removeIfExists(defaultEnvDir); + await defaultEnvDir.createSymlinkTo(envDir, { kind: "relative" }); + } +} + +async function showableEnv( + gcx: GhjkCtx, + recipe: EnvRecipeX, + envName: string, +) { + const printBag = {} as Record; + await using scx = await syncCtxFromGhjk(gcx); + for ( + const prov of recipe + .provides as ( + | WellKnownProvision + | InstallSetRefProvision + | InstallSetProvision + )[] + ) { + switch (prov.ty) { + case "posix.envVar": + printBag.envVars = { + ...printBag.envVars ?? {}, + [prov.key]: prov.val, + }; + break; + case "posix.exec": + printBag.execs = [ + ...printBag.execs ?? [], + prov.absolutePath, + ]; + break; + case "posix.sharedLib": + printBag.sharedLibs = [ + ...printBag.sharedLibs ?? [], + prov.absolutePath, + ]; + break; + case "posix.headerFile": + printBag.headerFiles = [ + ...printBag.headerFiles ?? [], + prov.absolutePath, + ]; + break; + case "ghjk.ports.InstallSet": { + const graph = await buildInstallGraph(scx, prov.set); + const setMeta = installGraphToSetMeta(graph); + printBag.ports = { + ...printBag.ports ?? {}, + [`installSet_${Math.floor(Math.random() * 101)}`]: setMeta, + }; + break; + } + case "ghjk.ports.InstallSetRef": { + const setStore = getInstallSetStore(gcx); + const set = setStore.get(prov.setId); + if (!set) { + throw new Error( + `unable to find install set ref provisioned under id ${prov.setId}`, + ); + } + const graph = await buildInstallGraph(scx, set); + const setMeta = installGraphToSetMeta(graph); + printBag.ports = { + ...printBag.ports ?? {}, + [prov.setId]: setMeta, + }; + break; + } + default: + } + } + return { + ...printBag, + ...(recipe.desc ? { desc: recipe.desc } : {}), + envName, + }; +} diff --git a/modules/envs/posix.ts b/modules/envs/posix.ts index 8a2b2f5a..14d9a250 100644 --- a/modules/envs/posix.ts +++ b/modules/envs/posix.ts @@ -1,18 +1,25 @@ import { std_fs, std_path } from "../../deps/cli.ts"; -import type { WellKnownEnvRecipeX } from "./types.ts"; +import type { EnvRecipeX } from "./types.ts"; import getLogger from "../../utils/logger.ts"; -import { $, PathRef } from "../../utils/mod.ts"; +import { $, Path } from "../../utils/mod.ts"; +import type { GhjkCtx } from "../types.ts"; +import { reduceStrangeProvisions } from "./reducer.ts"; const logger = getLogger(import.meta); export async function cookPosixEnv( - env: WellKnownEnvRecipeX, - envDir: string, - createShellLoaders = false, + { gcx, recipe, envName, envDir, createShellLoaders = false }: { + gcx: GhjkCtx; + recipe: EnvRecipeX; + envName: string; + envDir: string; + createShellLoaders?: boolean; + }, ) { + const reducedRecipe = await reduceStrangeProvisions(gcx, recipe); + await $.removeIfExists(envDir); // create the shims for the user's environment const shimDir = $.path(envDir).join("shims"); - await $.removeIfExists(shimDir); const [binShimDir, libShimDir, includeShimDir] = await Promise.all([ shimDir.join("bin").ensureDir(), @@ -25,11 +32,13 @@ export async function cookPosixEnv( const binPaths = [] as string[]; const libPaths = [] as string[]; const includePaths = [] as string[]; - const vars = {} as Record; + const vars = { + GHJK_ENV: envName, + } as Record; // FIXME: detect shim conflicts // FIXME: better support for multi installs - await Promise.all(env.provides.map((item) => { + await Promise.all(reducedRecipe.provides.map((item) => { switch (item.ty) { case "posix.exec": binPaths.push(item.absolutePath); @@ -48,7 +57,7 @@ export async function cookPosixEnv( }" and "${item.val}"`, ); } - vars[item.key] = vars[item.val]; + vars[item.key] = item.val; break; default: throw Error(`unsupported provision type: ${(item as any).provision}`); @@ -70,6 +79,7 @@ export async function cookPosixEnv( includePaths, includeShimDir, ), + $.path(envDir).join("recipe.json").writeJsonPretty(reducedRecipe), ]); // write loader for the env vars mandated by the installs logger.debug("adding vars to loader", vars); @@ -110,7 +120,7 @@ export async function cookPosixEnv( /// This expands globs found in the targetPaths async function shimLinkPaths( targetPaths: string[], - shimDir: PathRef, + shimDir: Path, ) { // map of filename to shimPath const shims: Record = {}; @@ -156,8 +166,11 @@ async function writeLoader( env: Record, pathVars: Record, ) { - const loader = { + const activate = { posix: [ + `if [ -n "$\{GHJK_CLEANUP_POSIX+x}" ]; then + eval "$GHJK_CLEANUP_POSIX" +fi`, `export GHJK_CLEANUP_POSIX="";`, ...Object.entries(env).map(([k, v]) => // NOTE: single quote the port supplied envs to avoid any embedded expansion/execution @@ -173,7 +186,10 @@ export ${k}="${v}:$${k}"; ), ].join("\n"), fish: [ - `set --erase GHJK_CLEANUP_FISH`, + `if set --query GHJK_CLEANUP_FISH + eval $GHJK_CLEANUP_FISH + set --erase GHJK_CLEANUP_FISH +end`, ...Object.entries(env).map(([k, v]) => `set --global --append GHJK_CLEANUP_FISH "set --global --export ${k} '$${k}';"; set --global --export ${k} '${v}';` @@ -187,7 +203,7 @@ set --global --export --prepend ${k} ${v}; }; const envPathR = await $.path(envDir).ensureDir(); await Promise.all([ - envPathR.join(`loader.fish`).writeText(loader.fish), - envPathR.join(`loader.sh`).writeText(loader.posix), + envPathR.join(`activate.fish`).writeText(activate.fish), + envPathR.join(`activate.sh`).writeText(activate.posix), ]); } diff --git a/modules/envs/reducer.ts b/modules/envs/reducer.ts index 28357be8..64fe228d 100644 --- a/modules/envs/reducer.ts +++ b/modules/envs/reducer.ts @@ -11,6 +11,12 @@ import { wellKnownProvisionTypes } from "./types.ts"; import validators from "./types.ts"; export type ProvisionReducerStore = Map>; + +/** + * In order to provide a means for other modules to define their own + * environment provisions, {@link ProvisionReducer}s can be registered + * here. + */ export function getProvisionReducerStore( gcx: GhjkCtx, ) { diff --git a/modules/envs/types.ts b/modules/envs/types.ts index 00554972..5850483d 100644 --- a/modules/envs/types.ts +++ b/modules/envs/types.ts @@ -32,6 +32,7 @@ const wellKnownProvision = zod.discriminatedUnion( ); const envRecipe = zod.object({ + desc: zod.string().nullish(), provides: zod.array(provision), }); diff --git a/modules/mod.ts b/modules/mod.ts index 2d2241d6..daa35fa1 100644 --- a/modules/mod.ts +++ b/modules/mod.ts @@ -8,22 +8,22 @@ export abstract class ModuleBase { _gcx: GhjkCtx, ): Promise | void {} */ abstract processManifest( - ctx: GhjkCtx, + gcx: GhjkCtx, manifest: ModuleManifest, bb: Blackboard, lockEnt: LockEnt | undefined, ): Promise | Ctx; // returns undefined if previous lock entry is no longer valid abstract loadLockEntry( - ctx: GhjkCtx, + gcx: GhjkCtx, raw: Json, ): Promise | LockEnt | undefined; abstract genLockEntry( - ctx: GhjkCtx, - manifest: Ctx, + gcx: GhjkCtx, + mcx: Ctx, ): Promise | Json; - abstract command( - ctx: GhjkCtx, - pman: Ctx, - ): cliffy_cmd.Command; + abstract commands( + gcx: GhjkCtx, + mcx: Ctx, + ): Record>; } diff --git a/modules/ports/ambient.ts b/modules/ports/ambient.ts index a6852bfb..6b290535 100644 --- a/modules/ports/ambient.ts +++ b/modules/ports/ambient.ts @@ -6,7 +6,7 @@ export class AmbientAccessPort extends PortBase { constructor(public manifest: AmbientAccessPortManifestX) { super(); // dependencies make no sense for ambient ports - if (manifest.deps && manifest.deps.length > 0) { + if (manifest.buildDeps && manifest.buildDeps.length > 0) { throw new Error( `ambient access plugin has deps ${JSON.stringify(manifest)}`, ); diff --git a/modules/ports/inter.ts b/modules/ports/inter.ts new file mode 100644 index 00000000..7e884131 --- /dev/null +++ b/modules/ports/inter.ts @@ -0,0 +1,49 @@ +import type { GhjkCtx } from "../types.ts"; +import type { InstallSetX } from "./types.ts"; +import type { InstallGraph } from "./sync.ts"; // TODO: rename to install.ts + +export type InstallSetStore = Map; + +/** + * {@link InstallSetStore} provides a way for other modules to get + * install sets from the {@link import("./types.ts").PortsModuleConfig} + */ +export function getInstallSetStore( + gcx: GhjkCtx, +) { + const id = "installSetStore"; + let memoStore = gcx.blackboard.get(id) as + | InstallSetStore + | undefined; + if (!memoStore) { + memoStore = new Map(); + gcx.blackboard.set(id, memoStore); + } + return memoStore; +} + +/** + * Get a user friendly description of an {@link InstallGraph}. + */ +export function installGraphToSetMeta(graph: InstallGraph) { + function installMetaFromGraph(id: string) { + const inst = graph.all[id]!; + const { + buildDepConfigs: _bDeps, + resolutionDepConfigs: _rDeps, + ...confWithoutDeps + } = inst.config; + return { + instId: inst.instId, + ...confWithoutDeps, + }; + } + const userInstallIds = new Set(graph.user); + const out = { + userInstalls: graph.user.map(installMetaFromGraph), + buildInstalls: Object.keys(graph.all) + .filter((key) => !userInstallIds.has(key)) + .map(installMetaFromGraph), + }; + return out; +} diff --git a/modules/ports/mod.ts b/modules/ports/mod.ts index 49e60da9..bdeb765f 100644 --- a/modules/ports/mod.ts +++ b/modules/ports/mod.ts @@ -13,20 +13,16 @@ import { ModuleBase } from "../mod.ts"; import { buildInstallGraph, getResolutionMemo, - type InstallGraph, syncCtxFromGhjk, } from "./sync.ts"; // TODO: rename to install.ts import type { Blackboard } from "../../host/types.ts"; import { getProvisionReducerStore } from "../envs/reducer.ts"; import { installSetReducer, installSetRefReducer } from "./reducers.ts"; import type { Provision, ProvisionReducer } from "../envs/types.ts"; +import { getInstallSetStore } from "./inter.ts"; export type PortsCtx = { config: PortsModuleConfigX; - /* - * A map from a setId found in the `PortsModuleConfigX` to the `InstallGraph`. - */ - installGraphs: Map; }; const lockValidator = zod.object({ @@ -39,7 +35,7 @@ const lockValidator = zod.object({ type PortsLockEnt = zod.infer; export class PortsModule extends ModuleBase { - async processManifest( + processManifest( gcx: GhjkCtx, manifest: ModuleManifest, bb: Blackboard, @@ -60,41 +56,35 @@ export class PortsModule extends ModuleBase { config: { sets: {}, }, - installGraphs: new Map(), }; + const setStore = getInstallSetStore(gcx); // pre-process the install sets found in the config - { - // syncCx contains a reference counted db connection - // somewhere deep in there - // so we need to use `using` - await using syncCx = await syncCtxFromGhjk(gcx); - for (const [id, hashedSet] of Object.entries(hashedModConf.sets)) { - // install sets in the config use hash references to dedupe InstallConfigs, - // AllowedDepSets and AllowedDeps - // reify the references from the blackboard before continuing - const installs = hashedSet.installs.map((hash) => - unwrapParseCurry(validators.installConfigFat.safeParse(bb[hash])) - ); - const allowedDepSetHashed = unwrapParseCurry( - validators.allowDepSetHashed.safeParse( - bb[hashedSet.allowedDeps], - ), - ); - const allowedDeps = Object.fromEntries( - Object.entries(allowedDepSetHashed).map(( - [key, hash], - ) => [ - key, - unwrapParseCurry(validators.allowedPortDep.safeParse(bb[hash])), - ]), - ); - const set: InstallSetX = { - installs, - allowedDeps, - }; - pcx.config.sets[id] = set; - pcx.installGraphs.set(id, await buildInstallGraph(syncCx, set)); - } + for (const [id, hashedSet] of Object.entries(hashedModConf.sets)) { + // install sets in the config use hash references to dedupe InstallConfigs, + // AllowedDepSets and AllowedDeps + // reify the references from the blackboard before continuing + const installs = hashedSet.installs.map((hash) => + unwrapParseCurry(validators.installConfigFat.safeParse(bb[hash])) + ); + const allowedDepSetHashed = unwrapParseCurry( + validators.allowDepSetHashed.safeParse( + bb[hashedSet.allowedDeps], + ), + ); + const allowedDeps = Object.fromEntries( + Object.entries(allowedDepSetHashed).map(( + [key, hash], + ) => [ + key, + unwrapParseCurry(validators.allowedPortDep.safeParse(bb[hash])), + ]), + ); + const set: InstallSetX = { + installs, + allowedDeps, + }; + pcx.config.sets[id] = set; + setStore.set(id, set); } // register envrionment reducers for any @@ -111,32 +101,50 @@ export class PortsModule extends ModuleBase { return pcx; } - command( - _gcx: GhjkCtx, - _pcx: PortsCtx, + commands( + gcx: GhjkCtx, + pcx: PortsCtx, ) { - return new cliffy_cmd.Command() - .alias("p") - .action(function () { - this.showHelp(); - }) - .description("Ports module, install programs into your env.") - .command( - "outdated", - new cliffy_cmd.Command() - .description("TODO") - .action(function () { - throw new Error("TODO"); - }), - ) - .command( - "cleanup", - new cliffy_cmd.Command() - .description("TODO") - .action(function () { - throw new Error("TODO"); - }), - ); + return { + ports: new cliffy_cmd.Command() + .alias("p") + .action(function () { + this.showHelp(); + }) + .description("Ports module, install programs into your env.") + .command( + "resolve", + new cliffy_cmd.Command() + .description(`Resolve all installs declared in config. + +- Useful to pre-resolve and add all install configs to the lockfile.`) + .action(async function () { + // scx contains a reference counted db connection + // somewhere deep in there + // so we need to use `using` + await using scx = await syncCtxFromGhjk(gcx); + for (const [_id, set] of Object.entries(pcx.config.sets)) { + void await buildInstallGraph(scx, set); + } + }), + ) + .command( + "outdated", + new cliffy_cmd.Command() + .description("TODO") + .action(function () { + throw new Error("TODO"); + }), + ) + .command( + "cleanup", + new cliffy_cmd.Command() + .description("TODO") + .action(function () { + throw new Error("TODO"); + }), + ), + }; } loadLockEntry( gcx: GhjkCtx, diff --git a/modules/ports/reducers.ts b/modules/ports/reducers.ts index 9c843bbf..9cbf58e2 100644 --- a/modules/ports/reducers.ts +++ b/modules/ports/reducers.ts @@ -40,29 +40,24 @@ export function installSetReducer(gcx: GhjkCtx) { } export function installSetRefReducer(gcx: GhjkCtx, pcx: PortsCtx) { - return async (provisions: InstallSetRefProvision[]) => { - if (provisions.length > 1) { - throw new Error( - 'only one "ghjkPorts" provision per environment is supported', - ); - } - const { setId } = unwrapParseRes( - validators.installSetRefProvision.safeParse(provisions[0]), - {}, - "error parsing env provision", - ); - const installGraph = pcx.installGraphs.get(setId); - if (!installGraph) { - throw new Error( - `provisioned install set under id "${setId}" not found`, - ); - } - await using scx = await syncCtxFromGhjk(gcx); - const installArts = await installFromGraph(scx, installGraph); - - const out = await reduceInstArts(installGraph, installArts); - return out; - }; + const directReducer = installSetReducer(gcx); + return (provisions: InstallSetRefProvision[]) => + directReducer(provisions.map( + (prov) => { + const { setId } = unwrapParseRes( + validators.installSetRefProvision.safeParse(prov), + {}, + "error parsing env provision", + ); + const set = pcx.config.sets[setId]; + if (!set) { + throw new Error( + `provisioned install set under id "${setId}" not found`, + ); + } + return { ty: "ghjk.ports.InstallSet", set }; + }, + )); } async function reduceInstArts( diff --git a/modules/ports/sync.ts b/modules/ports/sync.ts index ea62d373..3a7dda6c 100644 --- a/modules/ports/sync.ts +++ b/modules/ports/sync.ts @@ -33,7 +33,6 @@ import type { GhjkCtx } from "../types.ts"; const logger = getLogger(import.meta); export type ResolutionMemoStore = Map>; -export type SyncCtx = DePromisify>; export function getResolutionMemo( gcx: GhjkCtx, @@ -49,6 +48,8 @@ export function getResolutionMemo( return memoStore; } +export type SyncCtx = DePromisify>; + export async function syncCtxFromGhjk( gcx: GhjkCtx, ) { @@ -94,109 +95,6 @@ export async function syncCtxFromGhjk( }; } -export async function installFromGraphAndShimEnv( - scx: SyncCtx, - envDir: string, - graph: InstallGraph, - createShellLoaders = true, -) { - const installArts = await installFromGraph( - scx, - graph, - ); - // create the shims for the user's environment - const shimDir = $.path(envDir).join("shims"); - await $.removeIfExists(shimDir); - - const [binShimDir, libShimDir, includeShimDir] = await Promise.all([ - shimDir.join("bin").ensureDir(), - shimDir.join("lib").ensureDir(), - shimDir.join("include").ensureDir(), - ]); - - // extract the env vars exported by the user specified - // installs and shim up their exported artifacts - const totalEnv: Record = {}; - // FIXME: detect shim conflicts - // FIXME: better support for multi installs - for (const instId of graph.user) { - const { binPaths, libPaths, includePaths, installPath, env } = installArts - .get( - instId, - )!; - - for (const [key, val] of Object.entries(env)) { - const conflict = totalEnv[key]; - if (conflict) { - throw new Error( - `duplicate env var found ${key} from sources ${instId} & ${ - conflict[1] - }`, - ); - } - totalEnv[key] = [val, instId]; - } - - // bin shims - void await shimLinkPaths( - binPaths, - installPath, - binShimDir.toString(), - ); - // lib shims - void await shimLinkPaths( - libPaths, - installPath, - libShimDir.toString(), - ); - // include shims - void await shimLinkPaths( - includePaths, - installPath, - includeShimDir.toString(), - ); - } - // write loader for the env vars mandated by the installs - logger.debug("adding vars to loader", totalEnv); - // FIXME: prevent malicious env manipulations - let LD_LIBRARY_ENV: string; - switch (Deno.build.os) { - case "darwin": - LD_LIBRARY_ENV = "DYLD_LIBRARY_PATH"; - break; - case "linux": - LD_LIBRARY_ENV = "LD_LIBRARY_PATH"; - break; - default: - throw new Error(`unsupported os ${Deno.build.os}`); - } - const pathVars = { - PATH: `${envDir}/shims/bin`, - LIBRARY_PATH: `${envDir}/shims/lib`, - [LD_LIBRARY_ENV]: `${envDir}/shims/lib`, - C_INCLUDE_PATH: `${envDir}/shims/include`, - CPLUS_INCLUDE_PATH: `${envDir}/shims/include`, - }; - // totalEnv contains info about the origin of the env - // which we don't need anymore - const simplifedTotalEnvs = Object.fromEntries( - Object.entries(totalEnv).map(([key, [val, _]]) => [key, val]), - ); - if (createShellLoaders) { - await writeLoader( - envDir, - simplifedTotalEnvs, - pathVars, - ); - } - return { - env: { - ...simplifedTotalEnvs, - ...pathVars, - }, - }; -} - export async function installFromGraph( scx: SyncCtx, graph: InstallGraph, @@ -481,12 +379,12 @@ export async function buildInstallGraph( graph.all[installId] = inst; - if (!manifest.deps || manifest.deps.length == 0) { + if (!manifest.buildDeps || manifest.buildDeps.length == 0) { graph.indie.push(installId); } else { // this goes into graph.depEdges const deps: [string, string][] = []; - for (const depId of manifest.deps) { + for (const depId of manifest.buildDeps) { const { manifest: depPort } = set.allowedDeps[depId.name]; if (!depPort) { throw new Error( @@ -499,7 +397,7 @@ export async function buildInstallGraph( // the conf is of the resolved kind which means // it's deps are also resolved const depInstall = validators.installConfigResolved.parse( - inst.config.depConfigs![depId.name], + inst.config.buildDepConfigs![depId.name], ); const depInstallId = await getInstallHash(depInstall); @@ -639,8 +537,8 @@ async function resolveConfig( // TODO: port version dependent portDep resolution // e.g. use python-2.7 if foo is resolved to <1.0 or use // python-3.x if foo is resolved to >1.0 - const resolveDepConfigs = {} as Record; - for (const dep of manifest.deps ?? []) { + const buildDepConfigs = {} as Record; + for (const dep of manifest.buildDeps ?? []) { const { manifest: depMan, config: depConf } = getDepConfig( set, manifest, @@ -654,13 +552,13 @@ async function resolveConfig( depMan, depConf, ); - resolveDepConfigs[dep.name] = depInstall; + buildDepConfigs[dep.name] = depInstall; } return validators.installConfigResolved.parse({ ...config, - depConfigs: resolveDepConfigs, version, + buildDepConfigs, }); } } @@ -686,7 +584,7 @@ function getDepConfig( // install configuration of an allowed dep port // can be overriden by dependent ports const res = validators.installConfigLite.safeParse( - (resolutionDep ? config.resolutionDepConfigs : config.depConfigs) + (resolutionDep ? config.resolutionDepConfigs : config.buildDepConfigs) ?.[depId.name] ?? defaultDepInstall, ); if (!res.success) { @@ -704,15 +602,17 @@ function getDepConfig( return { config: res.data, manifest: depPort }; } -/// This is a simpler version of the graph -/// based installer that the rest of this module implements -/// it resolves and installs a single config (and it's deps). -/// This primarily is used to install the manifest.resolutionDeps -/// which are required to do version resolution when building the -/// graph -/// FIXME: the usage of this function implies that resolution -/// will be redone if a config specfied by different resolutionDeps -/// Consider introducing a memoization scheme +/** + * This is a simpler version of the graph based installer that + * the rest of this module implements. + * It resolves and installs a single config (and its deps). + * This primarily is used to install the manifest.resolutionDeps + * which are required to do version resolution when building the + * main graphs. + */ +// FIXME: the usage of this function implies that resolution +// will be redone if a config specfied by different resolutionDeps +// TODO: consider introducing a memoization scheme async function resolveAndInstall( scx: SyncCtx, set: InstallSetX, @@ -737,7 +637,7 @@ async function resolveAndInstall( scx, depShimsRootPath, await Promise.all( - manifest.deps?.map( + manifest.buildDeps?.map( async (dep) => { const depConfig = getDepConfig(set, manifest, config, dep); // we not only resolve but install the dep here @@ -1055,48 +955,3 @@ async function doInstallStage( installVersion, }; } - -// create the loader scripts -// loader scripts are responsible for exporting -// different environment variables from the ports -// and mainpulating the path strings -async function writeLoader( - envDir: string, - env: Record, - pathVars: Record, -) { - const loader = { - posix: [ - `export GHJK_CLEANUP_POSIX="";`, - ...Object.entries(env).map(([k, v]) => - // NOTE: single quote the port supplied envs to avoid any embedded expansion/execution - `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX"export ${k}='$${k}';"; -export ${k}='${v}';` - ), - ...Object.entries(pathVars).map(([k, v]) => - // NOTE: double quote the path vars for expansion - // single quote GHJK_CLEANUP additions to avoid expansion/exec before eval - `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'${k}=$(echo "$${k}" | tr ":" "\\n" | grep -vE "^${envDir}" | tr "\\n" ":");${k}="\${${k}%:}";'; -export ${k}="${v}:$${k}"; -` - ), - ].join("\n"), - fish: [ - `set --erase GHJK_CLEANUP_FISH`, - ...Object.entries(env).map(([k, v]) => - `set --global --append GHJK_CLEANUP_FISH "set --global --export ${k} '$${k}';"; -set --global --export ${k} '${v}';` - ), - ...Object.entries(pathVars).map(([k, v]) => - `set --global --append GHJK_CLEANUP_FISH 'set --global --export --path ${k} (string match --invert --regex "^${envDir}" $${k});'; -set --global --export --prepend ${k} ${v}; -` - ), - ].join("\n"), - }; - const envPathR = await $.path(envDir).ensureDir(); - await Promise.all([ - envPathR.join(`loader.fish`).writeText(loader.fish), - envPathR.join(`loader.sh`).writeText(loader.posix), - ]); -} diff --git a/modules/ports/types.ts b/modules/ports/types.ts index 199ac4f1..69f2c0dc 100644 --- a/modules/ports/types.ts +++ b/modules/ports/types.ts @@ -36,7 +36,7 @@ const portManifestBase = zod.object({ // .nullish() // // default value set after transformation // .default("deferToNewer"), - deps: zod.array(portDep).nullish(), + buildDeps: zod.array(portDep).nullish(), resolutionDeps: zod.array(portDep).nullish(), }).passthrough(); @@ -79,12 +79,12 @@ const portManifest = zod.discriminatedUnion("ty", [ const installConfigSimple = zod.object({ version: zod.string() .nullish(), - // /// A place to put captured env vars + // // A place to put captured env vars // envVars: zod.record(zod.string(), zod.string()).nullish().default({}), }).passthrough(); const installConfigBase = installConfigSimple.merge(zod.object({ - depConfigs: zod.record( + buildDepConfigs: zod.record( portName, // FIXME: figure out cyclically putting `installConfigLite` here zod.unknown(), @@ -117,7 +117,7 @@ const installConfigFat = stdInstallConfigFat; const installConfigResolved = installConfigLite.merge(zod.object({ // NOTE: version is no longer nullish version: zod.string(), - // depConfigs: zod.record( + // buildDepConfigs: zod.record( // portName, // // FIXME: figure out cyclically putting `installConfigResolved` here // zod.object({ version: zod.string() }).passthrough(), @@ -249,7 +249,6 @@ export type AmbientAccessPortManifest = zod.input< typeof validators.ambientAccessPortManifest >; -// Describes the port itself export type PortManifest = zod.input< typeof validators.portManifest >; @@ -261,12 +260,16 @@ export type DenoWorkerPortManifestX = zod.infer< export type AmbientAccessPortManifestX = zod.infer< typeof validators.ambientAccessPortManifest >; -/// This is the transformed version of PortManifest, ready for consumption +/** + * This is the transformed version of PortManifest, ready for consumption + */ export type PortManifestX = zod.infer< typeof validators.portManifest >; -/// PortDeps are used during the port build/install process +/** + * PortDeps are used during the port build/install process + */ export type PortDep = zod.infer; export type PortDepFat = zod.infer; @@ -279,21 +282,39 @@ export type InstallConfigBaseLite = zod.input< export type InstallConfigBaseFat = zod.input< typeof validators.installConfigBaseFat >; -/// Fat install configs include the port manifest within +/** + * Fat install configs include the port manifest within. + */ export type InstallConfigFat = zod.input; -/// Fat install configs include the port manifest within +/** + * Fat install configs include the port manifest within. + */ export type InstallConfigFatX = zod.infer; -/// Lite install configs refer to the port they use by name +/** + * Lite install configs refer to the port they use by name. + */ export type InstallConfigLite = zod.input; -/// Lite install configs refer to the port they use by name +/** + * Lite install configs refer to the port they use by name. + */ export type InstallConfigLiteX = zod.infer; -// Describes a single installation done by a specific plugin. +/** + * Describes a single installation done by a specific plugin. + */ export type InstallConfig = zod.input; -// Describes a single installation done by a specific plugin. +/** + * Describes a single installation done by a specific plugin. + */ export type InstallConfigX = zod.infer; +/** + * {@link InstallConfig} after the {@link InstallConfig.version} has been deternimed. + */ export type InstallConfigResolved = zod.input< typeof validators.installConfigResolved >; +/** + * {@inheritDoc InstallConfigResolved} + */ export type InstallConfigResolvedX = zod.infer< typeof validators.installConfigResolved >; diff --git a/modules/ports/worker.ts b/modules/ports/worker.ts index f3e6441a..5437aa74 100644 --- a/modules/ports/worker.ts +++ b/modules/ports/worker.ts @@ -145,8 +145,9 @@ type WorkerResp = { ty: "install"; }; -/// This creates a new worker for every method -/// invocation +/** + * This creates a new worker for every method invocation. + */ export class DenoWorkerPort extends PortBase { constructor( public manifest: DenoWorkerPortManifestX, @@ -154,7 +155,9 @@ export class DenoWorkerPort extends PortBase { super(); } - /// create new worker and perform "RPC" + /** + * Create new worker and perform "RPC". + */ async call( req: WorkerReq, ): Promise { diff --git a/modules/tasks/exec.ts b/modules/tasks/exec.ts index 4caa3a82..5f7ada62 100644 --- a/modules/tasks/exec.ts +++ b/modules/tasks/exec.ts @@ -9,7 +9,6 @@ import { execTaskDeno } from "./deno.ts"; const logger = getLogger(import.meta); import { cookPosixEnv } from "../envs/posix.ts"; -import { reduceStrangeProvisions } from "../envs/reducer.ts"; export type TaskGraph = DePromisify>; @@ -114,11 +113,14 @@ export async function execTask( const taskEnvDir = await Deno.makeTempDir({ prefix: `ghjkTaskEnv_${taskName}_`, }); - const reducedEnv = await reduceStrangeProvisions( - gcx, - tasksConfig.envs[taskDef.envHash], + const { env: installEnvs } = await cookPosixEnv( + { + gcx, + recipe: tasksConfig.envs[taskDef.envHash], + envName: `taskEnv_${taskName}`, + envDir: taskEnvDir, + }, ); - const { env: installEnvs } = await cookPosixEnv(reducedEnv, taskEnvDir); logger.info("executing", taskName, args); await execTaskDeno( std_path.toFileUrl(gcx.ghjkfilePath).href, diff --git a/modules/tasks/mod.ts b/modules/tasks/mod.ts index e895f148..b34ffdda 100644 --- a/modules/tasks/mod.ts +++ b/modules/tasks/mod.ts @@ -46,13 +46,13 @@ export class TasksModule extends ModuleBase { }; } - command( + commands( gcx: GhjkCtx, tcx: TasksCtx, ) { const commands = Object.entries(tcx.config.tasks).map( ([name, task]) => { - let cliffyCmd = new cliffy_cmd.Command() + const cliffyCmd = new cliffy_cmd.Command() .name(name) .useRawArgs() .action(async (_, ...args) => { @@ -65,22 +65,23 @@ export class TasksModule extends ModuleBase { ); }); if (task.desc) { - cliffyCmd = cliffyCmd.description(task.desc); + cliffyCmd.description(task.desc); } - return cliffyCmd; }, ); - let root: cliffy_cmd.Command = new cliffy_cmd.Command() + const root = new cliffy_cmd.Command() .alias("x") .action(function () { this.showHelp(); }) .description("Tasks module."); for (const cmd of commands) { - root = root.command(cmd.getName(), cmd); + root.command(cmd.getName(), cmd); } - return root; + return { + tasks: root, + }; } loadLockEntry( diff --git a/ports/asdf.ts b/ports/asdf.ts index 55c62755..f32d8daf 100644 --- a/ports/asdf.ts +++ b/ports/asdf.ts @@ -22,7 +22,7 @@ export const manifest = { name: "asdf", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [std_ports.curl_aa, std_ports.git_aa, std_ports.asdf_plugin_git], + buildDeps: [std_ports.curl_aa, std_ports.git_aa, std_ports.asdf_plugin_git], // NOTE: we require the same port set for version resolution as well resolutionDeps: [ std_ports.curl_aa, @@ -49,7 +49,7 @@ export default function conf( const { port: pluginPort, ...liteConf } = asdf_plugin_git({ pluginRepo: config.pluginRepo, }); - const depConfigs = { + const buildDepConfigs = { [std_ports.asdf_plugin_git.name]: { ...liteConf, portRef: getPortRef(pluginPort), @@ -58,8 +58,8 @@ export default function conf( return { ...confValidator.parse(config), port: manifest, - depConfigs, - resolutionDepConfigs: depConfigs, + buildDepConfigs, + resolutionDepConfigs: buildDepConfigs, }; } diff --git a/ports/asdf_plugin_git.ts b/ports/asdf_plugin_git.ts index ab06936f..60bc7669 100644 --- a/ports/asdf_plugin_git.ts +++ b/ports/asdf_plugin_git.ts @@ -22,7 +22,7 @@ export const manifest = { name: "asdf_plugin_git", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [git_aa_id], + buildDeps: [git_aa_id], resolutionDeps: [git_aa_id], platforms: osXarch(["linux", "darwin", "windows"], ["aarch64", "x86_64"]), }; diff --git a/ports/cargobi.ts b/ports/cargobi.ts index fa06bf49..aa25da75 100644 --- a/ports/cargobi.ts +++ b/ports/cargobi.ts @@ -30,7 +30,7 @@ const manifest = { name: "cargobi_cratesio", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [std_ports.cbin_ghrel, std_ports.rust_rustup], + buildDeps: [std_ports.cbin_ghrel, std_ports.rust_rustup], // FIXME: we can't know crate platform support at this point platforms: osXarch([...ALL_OS], [...ALL_ARCH]), }; @@ -56,7 +56,7 @@ export default function conf(config: CargobiInstallConf) { const { rustConfOverride, ...thisConf } = config; const out: InstallConfigFat = { ...confValidator.parse(thisConf), - depConfigs: { + buildDepConfigs: { [std_ports.rust_rustup.name]: thinInstallConfig(rust({ profile: "minimal", ...rustConfOverride, diff --git a/ports/cpy_bs.ts b/ports/cpy_bs.ts index 2fef4cc1..81cdf44d 100644 --- a/ports/cpy_bs.ts +++ b/ports/cpy_bs.ts @@ -36,7 +36,7 @@ export const manifest = { version: "0.1.0", moduleSpecifier: import.meta.url, // python-build-standalone use zstd tarballs - deps: [tar_aa_id, zstd_aa_id], + buildDeps: [tar_aa_id, zstd_aa_id], platforms: osXarch(["linux", "darwin", "windows"], ["x86_64", "aarch64"]), }; diff --git a/ports/dummy.ts b/ports/dummy.ts index 10de2284..8ba1dfcd 100644 --- a/ports/dummy.ts +++ b/ports/dummy.ts @@ -5,18 +5,33 @@ import type { InstallArgs, InstallConfigSimple, } from "../port.ts"; -import { $, ALL_ARCH, ALL_OS, osXarch, PortBase, std_fs } from "../port.ts"; +import { + $, + ALL_ARCH, + ALL_OS, + osXarch, + PortBase, + std_fs, + zod, +} from "../port.ts"; const manifest = { ty: "denoWorker@v1" as const, name: "dummy", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [], platforms: osXarch([...ALL_OS], [...ALL_ARCH]), }; -export default function conf(config: InstallConfigSimple = {}) { +const confValidator = zod.object({ + output: zod.string().nullish(), +}); + +export type DummyInstallConf = + & InstallConfigSimple + & zod.input; + +export default function conf(config: DummyInstallConf = {}) { return { ...config, port: manifest, @@ -35,10 +50,11 @@ export class Port extends PortBase { } async download(args: DownloadArgs) { + const conf = confValidator.parse(args.config); // TODO: windows suport await $.path(args.downloadPath).join("bin", "dummy").writeText( `#!/bin/sh -echo 'dummy hey'`, +echo ${conf.output ?? "dummy hey"}`, { mode: 0o700, }, diff --git a/ports/infisical.ts b/ports/infisical.ts index 25e92274..8667c83d 100644 --- a/ports/infisical.ts +++ b/ports/infisical.ts @@ -18,7 +18,6 @@ const manifest = { name: "infisical_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [], // NOTE: infisical supports more arches than deno platforms: osXarch(["linux", "darwin", "windows", "netbsd", "freebsd"], [ "aarch64", diff --git a/ports/jq_ghrel.ts b/ports/jq_ghrel.ts index c608dfb1..27fa2674 100644 --- a/ports/jq_ghrel.ts +++ b/ports/jq_ghrel.ts @@ -71,7 +71,7 @@ export class Port extends GithubReleasePort { const [{ name: fileName }] = this.downloadUrls(args); const fileDwnPath = $.path(args.downloadPath).resolve(fileName); - await fileDwnPath.copyFile( + await fileDwnPath.copy( (await installPath .join("bin") .ensureDir()) diff --git a/ports/meta_cli_ghrel.ts b/ports/meta_cli_ghrel.ts index 7d1de443..033d6d63 100644 --- a/ports/meta_cli_ghrel.ts +++ b/ports/meta_cli_ghrel.ts @@ -19,7 +19,7 @@ const manifest = { name: "meta_cli_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [ + buildDeps: [ // we have to use tar because their tarballs for darwin use gnu sparse std_ports.tar_aa, ], diff --git a/ports/mold.ts b/ports/mold.ts index 5ebef9a8..cc32e3da 100644 --- a/ports/mold.ts +++ b/ports/mold.ts @@ -18,7 +18,7 @@ const manifest = { name: "mold_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [ + buildDeps: [ // we have to use tar because their tarballs contain symlinks std_ports.tar_aa, ], diff --git a/ports/node.ts b/ports/node.ts index 5e1be2cc..23405ab6 100644 --- a/ports/node.ts +++ b/ports/node.ts @@ -30,7 +30,7 @@ export const manifest = { moduleSpecifier: import.meta.url, // FIXME: tar doens't support windows // TODO: platform disambiguated deps - deps: [tar_aa_id], + buildDeps: [tar_aa_id], // NOTE: node supports more archs than deno but we can't include it here platforms: osXarch(["linux", "darwin", "windows"], ["aarch64", "x86_64"]), }; diff --git a/ports/npmi.ts b/ports/npmi.ts index d86f2152..835f191a 100644 --- a/ports/npmi.ts +++ b/ports/npmi.ts @@ -23,7 +23,7 @@ const manifest = { name: "npmi_npm", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [ + buildDeps: [ std_ports.node_org, ], // NOTE: enable all platforms. Restrictions will apply based diff --git a/ports/pipi.ts b/ports/pipi.ts index 96d54567..1e3c05bc 100644 --- a/ports/pipi.ts +++ b/ports/pipi.ts @@ -23,7 +23,7 @@ const manifest = { name: "pipi_pypi", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [std_ports.cpy_bs_ghrel], + buildDeps: [std_ports.cpy_bs_ghrel], // NOTE: enable all platforms. Restrictions will apply based // cpy_bs support this way platforms: osXarch([...ALL_OS], [...ALL_ARCH]), diff --git a/ports/ruff.ts b/ports/ruff.ts index 10233101..777bd9eb 100644 --- a/ports/ruff.ts +++ b/ports/ruff.ts @@ -19,7 +19,7 @@ const manifest = { name: "ruff_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [ + buildDeps: [ // we have to use tar because their tarballs for darwin use gnu sparse std_ports.tar_aa, ], diff --git a/ports/rust.ts b/ports/rust.ts index 88f84183..bda15bea 100644 --- a/ports/rust.ts +++ b/ports/rust.ts @@ -25,7 +25,7 @@ export const manifest = { name: "rust_rustup", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [rustup_rustlang_id], + buildDeps: [rustup_rustlang_id], // NOTE: indirectly limited by rustup instead platforms: osXarch([...ALL_OS], [...ALL_ARCH]), }; diff --git a/ports/rustup.ts b/ports/rustup.ts index 5c817225..3ab972e7 100644 --- a/ports/rustup.ts +++ b/ports/rustup.ts @@ -26,7 +26,7 @@ export const manifest = { name: "rustup_rustlang", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [git_aa_id], + buildDeps: [git_aa_id], resolutionDeps: [git_aa_id], platforms: [ ...osXarch(["darwin", "linux"], [...ALL_ARCH]), diff --git a/ports/temporal_cli.ts b/ports/temporal_cli.ts index 5a70971e..4bc867a6 100644 --- a/ports/temporal_cli.ts +++ b/ports/temporal_cli.ts @@ -17,7 +17,6 @@ const manifest = { name: "temporal_cli_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [], platforms: osXarch(["linux", "darwin", "windows"], ["aarch64", "x86_64"]), }; diff --git a/ports/terraform.ts b/ports/terraform.ts index bdd11037..f811d5a2 100644 --- a/ports/terraform.ts +++ b/ports/terraform.ts @@ -21,7 +21,6 @@ export const manifest = { name: "terraform_hashicorp", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [], platforms: osXarch([...ALL_OS], ["aarch64", "x86_64"]), }; diff --git a/ports/wasmedge.ts b/ports/wasmedge.ts index 4f9137b8..be3f915b 100644 --- a/ports/wasmedge.ts +++ b/ports/wasmedge.ts @@ -19,7 +19,7 @@ const manifest = { name: "wasmedge_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [ + buildDeps: [ std_ports.tar_aa, ], platforms: osXarch(["linux", "darwin"], ["aarch64", "x86_64"]), diff --git a/tests/ambient.ts b/tests/ambient.ts index 821c0d7f..146b60d3 100644 --- a/tests/ambient.ts +++ b/tests/ambient.ts @@ -7,6 +7,7 @@ import * as tar from "../ports/tar.ts"; import * as git from "../ports/git.ts"; import * as curl from "../ports/curl.ts"; import * as unzip from "../ports/unzip.ts"; +import logger from "../utils/logger.ts"; const manifests = [ tar.manifest, @@ -19,7 +20,7 @@ for (const manUnclean of manifests) { Deno.test(`ambient access ${manifest.name}`, async () => { const plug = new AmbientAccessPort(manifest); const versions = await plug.listAll(); - console.log(versions); + logger(import.meta).info(versions); std_assert.assertEquals(versions.length, 1); }); } diff --git a/tests/envs.ts b/tests/envs.ts new file mode 100644 index 00000000..f4736b34 --- /dev/null +++ b/tests/envs.ts @@ -0,0 +1,204 @@ +import "../setup_logger.ts"; +import { + dockerE2eTest, + E2eTestCase, + type EnvDefArgs, + genTsGhjkFile, + localE2eTest, +} from "./utils.ts"; +import dummy from "../ports/dummy.ts"; + +type CustomE2eTestCase = Omit & { + ePoint: string; + stdin: string; + envs: EnvDefArgs[]; +}; + +const envVarTestEnvs: EnvDefArgs[] = [ + { + name: "main", + vars: { + SONG: "ditto", + }, + }, + { + name: "sss", + vars: { + SING: "Seoul Sonyo Sound", + }, + }, + { + name: "yuki", + base: false, + vars: { + HUMM: "Soul Lady", + }, + }, +]; +const envVarTestsPosix = ` +set -ex +# by default, we should be in main +[ "$SONG" = "ditto" ] || exit 101 + +ghjk envs cook sss +. .ghjk/envs/sss/activate.sh +# by default, envs should be based on main +# so they should inherit it's env vars +[ "$SONG" = "ditto" ] || exit 102 +[ "$SING" = "Seoul Sonyo Sound" ] || exit 103 + +# go back to main and "sss" variables shouldn't be around +. .ghjk/envs/main/activate.sh +[ "$SONG" = "ditto" ] || exit 104 +[ "$SING" = "Seoul Sonyo Sound" ] && exit 105 + +# env base is false for "yuki" and thus no vars from "main" +ghjk envs cook yuki +. .ghjk/envs/yuki/activate.sh +[ "$SONG" = "ditto" ] && exit 102 +[ "$HUMM" = "Soul Lady" ] || exit 103 +`; +const envVarTestsFish = ` +# by default, we should be in main +test "$SONG" = "ditto"; or exit 101; + +ghjk envs cook sss +. .ghjk/envs/sss/activate.fish +# by default, envs should be based on main +# so they should inherit it's env vars +test "$SONG" = "ditto"; or exit 103 +test "$SING" = "Seoul Sonyo Sound"; or exit 104 + +# go back to main and "sss" variables shouldn't be around +. .ghjk/envs/main/activate.fish +test $SONG" = "ditto"; or exit 105 +test $SING" = "Seoul Sonyo Sound"; and exit 106 + +# env base is false for "yuki" and thus no vars from "main" +ghjk envs cook yuki +. .ghjk/envs/yuki/activate.fish +test "$SONG" = "ditto"; and exit 107 +test "$HUMM" = "Soul Lady"; or exit 108 +`; + +const installTestEnvs: EnvDefArgs[] = [ + { + name: "main", + installs: [ + dummy({ output: "main" }), + ], + }, + { + name: "foo", + base: false, + installs: [ + dummy({ output: "foo" }), + ], + }, +]; + +const installTestsPosix = ` +set -eux +# by default, we should be in main +[ "$(dummy)" = "main" ] || exit 101; + +ghjk envs cook foo +. .ghjk/envs/foo/activate.sh +[ "$(dummy)" = "foo" ] || exit 102; + +. .ghjk/envs/main/activate.sh +[ "$(dummy)" = "main" ] || exit 102; +`; + +const installTestsFish = ` +# by default, we should be in main +test (dummy) = "main"; or exit 101; + +ghjk envs cook foo +. .ghjk/envs/foo/activate.fish +test (dummy) = "foo"; or exit 102; + +. .ghjk/envs/main/activate.fish +test (dummy) = "main"; or exit 102; +`; + +const cases: CustomE2eTestCase[] = [ + { + name: "prov_env_vars_bash", + ePoint: `bash -s`, + envs: envVarTestEnvs, + stdin: envVarTestsPosix, + }, + { + name: "prov_env_vars_zsh", + ePoint: `zsh -s`, + envs: envVarTestEnvs, + stdin: envVarTestsPosix, + }, + { + name: "prov_env_vars_fish", + ePoint: `fish`, + envs: envVarTestEnvs, + stdin: envVarTestsFish, + }, + { + name: "prov_port_installs_bash", + ePoint: `bash -l`, + envs: installTestEnvs, + stdin: installTestsPosix, + }, + { + name: "prov_port_installs_zsh", + ePoint: `zsh -l`, + envs: installTestEnvs, + stdin: installTestsPosix, + }, + { + name: "prov_port_installs_fish", + ePoint: `fish`, + envs: installTestEnvs, + stdin: installTestsFish, + }, +]; + +function testMany( + testGroup: string, + cases: CustomE2eTestCase[], + testFn: (inp: E2eTestCase) => Promise, + defaultEnvs: Record = {}, +) { + for (const testCase of cases) { + Deno.test( + `${testGroup} - ${testCase.name}`, + () => + testFn({ + ...testCase, + tsGhjkfileStr: genTsGhjkFile( + { envDefs: testCase.envs }, + ), + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + envVars: { + ...defaultEnvs, + ...testCase.envVars, + }, + }), + ); + } +} + +const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); +if (e2eType == "both") { + testMany("envsDockerE2eTest", cases, dockerE2eTest); + testMany(`envsLocalE2eTest`, cases, localE2eTest); +} else if (e2eType == "local") { + testMany("envsLocalE2eTest", cases, localE2eTest); +} else if ( + e2eType == "docker" || + !e2eType +) { + testMany("envsDockerE2eTest", cases, dockerE2eTest); +} else { + throw new Error( + `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, + ); +} diff --git a/tests/hooks.ts b/tests/hooks.ts index baa8f9a0..120153da 100644 --- a/tests/hooks.ts +++ b/tests/hooks.ts @@ -2,8 +2,8 @@ import "../setup_logger.ts"; import { dockerE2eTest, E2eTestCase, + genTsGhjkFile, localE2eTest, - tsGhjkFileFromInstalls, } from "./utils.ts"; import dummy from "../ports/dummy.ts"; import type { InstallConfigFat } from "../port.ts"; @@ -171,13 +171,13 @@ function testMany( () => testFn({ ...testCase, - tsGhjkfileStr: tsGhjkFileFromInstalls( + tsGhjkfileStr: genTsGhjkFile( { installConf: testCase.installConf ?? dummy(), taskDefs: [] }, ), ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], - envs: { + envVars: { ...defaultEnvs, - ...testCase.envs, + ...testCase.envVars, }, }), ); diff --git a/tests/ports.ts b/tests/ports.ts index e52e9b8e..40cc5b42 100644 --- a/tests/ports.ts +++ b/tests/ports.ts @@ -4,10 +4,11 @@ import { stdSecureConfig } from "../mod.ts"; import { dockerE2eTest, E2eTestCase, + genTsGhjkFile, localE2eTest, - tsGhjkFileFromInstalls, } from "./utils.ts"; import * as ports from "../ports/mod.ts"; +import dummy from "../ports/dummy.ts"; import type { InstallConfigFat, PortsModuleSecureConfig, @@ -21,6 +22,12 @@ type CustomE2eTestCase = Omit & { }; // order tests by download size to make failed runs less expensive const cases: CustomE2eTestCase[] = [ + // 0 megs + { + name: "dummy", + installConf: dummy(), + ePoint: `dummy`, + }, // 2 megs { name: "jq", @@ -214,7 +221,7 @@ function testMany( std_async.deadline( testFn({ ...testCase, - tsGhjkfileStr: tsGhjkFileFromInstalls( + tsGhjkfileStr: genTsGhjkFile( { installConf: testCase.installConf, secureConf: testCase.secureConf, @@ -223,22 +230,27 @@ function testMany( ), ePoints: [ ...["bash -c", "fish -c", "zsh -c"].map((sh) => ({ - cmd: `env ${sh} '${testCase.ePoint}'`, + cmd: [...`env ${sh}`.split(" "), `"${testCase.ePoint}"`], })), - // FIXME: better tests for the `InstallDb` + /* // FIXME: better tests for the `InstallDb` // installs db means this shouldn't take too long // as it's the second sync - { cmd: "env bash -c 'timeout 1 ghjk env sync'" }, + { + cmd: [ + ..."env".split(" "), + "bash -c 'timeout 1 ghjk envs cook'", + ], + }, */ ], - envs: { + envVars: { ...defaultEnvs, - ...testCase.envs, + ...testCase.envVars, }, }), // building the test docker image might taka a while // but we don't want some bug spinlocking the ci for // an hour - 300_000, + 5 * 60 * 1000, ), }, ); diff --git a/tests/tasks.ts b/tests/tasks.ts index 20f6a487..82ec3a91 100644 --- a/tests/tasks.ts +++ b/tests/tasks.ts @@ -2,17 +2,19 @@ import "../setup_logger.ts"; import { dockerE2eTest, E2eTestCase, + genTsGhjkFile, localE2eTest, type TaskDefArgs, - tsGhjkFileFromInstalls, } from "./utils.ts"; import * as ghjk from "../mod.ts"; import * as ports from "../ports/mod.ts"; +import { stdSecureConfig } from "../ghjkfiles/mod.ts"; type CustomE2eTestCase = Omit & { ePoint: string; stdin: string; tasks: TaskDefArgs[]; + enableRuntimesOnMasterPDAL?: boolean; }; const cases: CustomE2eTestCase[] = [ { @@ -70,6 +72,7 @@ ghjk x protoc`, }], ePoint: `fish`, stdin: `ghjk x test`, + enableRuntimesOnMasterPDAL: true, }, { name: "default_port_deps", @@ -129,13 +132,18 @@ function testMany( () => testFn({ ...testCase, - tsGhjkfileStr: tsGhjkFileFromInstalls( - { installConf: [], taskDefs: testCase.tasks }, + tsGhjkfileStr: genTsGhjkFile( + { + taskDefs: testCase.tasks, + secureConf: stdSecureConfig({ + enableRuntimes: testCase.enableRuntimesOnMasterPDAL, + }), + }, ), ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], - envs: { + envVars: { ...defaultEnvs, - ...testCase.envs, + ...testCase.envVars, }, }), ); diff --git a/tests/test-alpine.Dockerfile b/tests/test-alpine.Dockerfile index b1626ce8..e9655187 100644 --- a/tests/test-alpine.Dockerfile +++ b/tests/test-alpine.Dockerfile @@ -46,7 +46,7 @@ RUN ln -s ./main.ts /bin/ghjk WORKDIR /app -ENV GHJK_LOG=debug +ENV GHJK_LOG=info ENV GHJK_INSTALL_EXE_DIR=/usr/bin ENV GHJK_INSTALL_HOOK_SHELLS=fish,bash,zsh # share the module cache of the image @@ -67,7 +67,7 @@ RUN <; - ePoints: { cmd: string; stdin?: string }[]; + envVars?: Record; + ePoints: { cmd: string | string[]; stdin?: string }[]; }; const dockerCmd = (Deno.env.get("DOCKER_CMD") ?? "docker").split(/\s/); @@ -22,7 +23,7 @@ const templateStrings = { }; export async function dockerE2eTest(testCase: E2eTestCase) { - const { name, envs: testEnvs, ePoints, tsGhjkfileStr } = testCase; + const { name, envVars: testEnvs, ePoints, tsGhjkfileStr } = testCase; const tag = `ghjk_e2e_${name}`; const env = { ...testEnvs, @@ -34,18 +35,6 @@ export async function dockerE2eTest(testCase: E2eTestCase) { // repo in the host fs to point to the copy of the // repo in the image .replaceAll(devGhjkPath, "file://$ghjk/") - // .replace(/\\/g, "\\\\") - // - // escape backticks - // .replace(/`/g, "\\`") - // escpape ${VAR} types of vars - // .replace(/\$\{([^\}]*)\}/g, "\\${$1}") - // escpae $VAR types of vars - // double dollar is treated as escpae so place a mark betewen - // .replace(/\$([A-Za-z])/g, "\\$$1") - // .replace(/\$([A-Za-z])/g, "\\$$1") - // remove mark - // .replace(//g, "") .replaceAll("$ghjk", "/ghjk"); const dFile = dbg(dFileTemplate @@ -70,13 +59,17 @@ export async function dockerE2eTest(testCase: E2eTestCase) { ...Object.entries(env).map(([key, val]) => ["-e", `${key}=${val}`]) .flat(), tag, - ePoint.cmd, - ]}` + ]} ${ePoint.cmd}` .env(env); if (ePoint.stdin) { cmd = cmd.stdinText(ePoint.stdin!); } - await cmd; + try { + await cmd; + } catch (err) { + logger(import.meta).error(err); + throw err; + } } await $ .raw`${dockerCmd} rmi '${tag}'` @@ -84,7 +77,7 @@ export async function dockerE2eTest(testCase: E2eTestCase) { } export async function localE2eTest(testCase: E2eTestCase) { - const { envs: testEnvs, ePoints, tsGhjkfileStr } = testCase; + const { envVars: testEnvs, ePoints, tsGhjkfileStr } = testCase; const tmpDir = $.path( await Deno.makeTempDir({ prefix: "ghjk_le2e_", @@ -104,6 +97,9 @@ export async function localE2eTest(testCase: E2eTestCase) { ZDOTDIR: ghjkShareDir.toString(), GHJK_SHARE_DIR: ghjkShareDir.toString(), PATH: `${ghjkShareDir.toString()}:${Deno.env.get("PATH")}`, + // shield tests from external envs + GHJK_ENV: "main", + HOME: tmpDir.toString(), }; // install ghjk await install({ @@ -117,10 +113,11 @@ export async function localE2eTest(testCase: E2eTestCase) { // don't modify system shell configs shellsToHook: [], }); + await $`${ghjkShareDir.join("ghjk").toString()} print config` .cwd(tmpDir.toString()) .env(env); - await $`${ghjkShareDir.join("ghjk").toString()} ports sync` + await $`${ghjkShareDir.join("ghjk").toString()} envs cook` .cwd(tmpDir.toString()) .env(env); /* @@ -142,6 +139,7 @@ export async function localE2eTest(testCase: E2eTestCase) { for (const ePoint of ePoints) { let cmd = $.raw`${ePoint.cmd}` .cwd(tmpDir.toString()) + .clearEnv() .env(env); if (ePoint.stdin) { cmd = cmd.stdinText(ePoint.stdin); @@ -151,16 +149,17 @@ export async function localE2eTest(testCase: E2eTestCase) { await tmpDir.remove({ recursive: true }); } -export function tsGhjkFileFromInstalls( - { installConf, secureConf, taskDefs }: { - installConf: InstallConfigFat | InstallConfigFat[]; +export function genTsGhjkFile( + { installConf, secureConf, taskDefs, envDefs }: { + installConf?: InstallConfigFat | InstallConfigFat[]; secureConf?: PortsModuleSecureConfig; - taskDefs: TaskDefArgs[]; + taskDefs?: TaskDefArgs[]; + envDefs?: EnvDefArgs[]; }, ) { - const installConfArray = Array.isArray(installConf) - ? installConf - : [installConf]; + const installConfArray = installConf + ? Array.isArray(installConf) ? installConf : [installConf] + : []; const serializedPortsInsts = JSON.stringify( installConfArray, @@ -177,21 +176,33 @@ export function tsGhjkFileFromInstalls( secureConf ?? null, (_, val) => typeof val == "string" ? val.replaceAll(/\\/g, "\\\\") : val, ); - const tasks = taskDefs.map( + const tasks = (taskDefs ?? []).map( (def) => { - const { name, ...withoutName } = def; const stringifiedSection = JSON.stringify( - withoutName, + def, (_, val) => typeof val == "string" ? val.replaceAll(/\\/g, "\\\\") : val, ); return $.dedent` - ghjk.task("${name}", { + ghjk.task({ ...JSON.parse(\`${stringifiedSection}\`), fn: ${def.fn.toString()} })`; }, ).join("\n"); + const envs = (envDefs ?? []).map( + (def) => { + const stringifiedSection = JSON.stringify( + def, + (_, val) => + typeof val == "string" ? val.replaceAll(/\\/g, "\\\\") : val, + ); + return $.dedent` + ghjk.env({ + ...JSON.parse(\`${stringifiedSection}\`), + })`; + }, + ).join("\n"); return ` export { ghjk } from "$ghjk/mod.ts"; import * as ghjk from "$ghjk/mod.ts"; @@ -207,5 +218,6 @@ ${serializedSecConf} export const secureConfig = JSON.parse(secConfStr); ${tasks} +${envs} `; } diff --git a/utils/logger.ts b/utils/logger.ts index 839b39d4..c65b0f4b 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -58,14 +58,15 @@ function formatter(lr: std_log.LogRecord) { return msg; } -export class ConsoleErrHandler extends std_log.handlers.BaseHandler { +export class ConsoleErrHandler extends std_log.BaseHandler { constructor( levelName: std_log.LevelName, - options: std_log.HandlerOptions = { formatter }, + options: std_log.BaseHandlerOptions = { formatter }, ) { super(levelName, options); } override log(msg: string): void { + // deno-lint-ignore no-console console.error(msg); } override format(logRecord: std_log.LogRecord): string { @@ -99,7 +100,7 @@ export class TestConsoleErrHandler extends ConsoleErrHandler { constructor( public throwLevel: number, levelName: std_log.LevelName, - options: std_log.HandlerOptions = { formatter }, + options: std_log.BaseHandlerOptions = { formatter }, ) { super(levelName, options); } diff --git a/utils/mod.ts b/utils/mod.ts index d35b9d42..77fdf4bf 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -122,9 +122,11 @@ export function tryDepExecShimPath( return path; } -// Lifted from https://deno.land/x/hextools@v1.0.0 -// MIT License -// Copyright (c) 2020 Santiago Aguilar Hernández +/** + * Lifted from https://deno.land/x/hextools@v1.0.0 + * MIT License + * Copyright (c) 2020 Santiago Aguilar Hernández + */ export function bufferToHex(buffer: ArrayBuffer): string { return Array.prototype.map.call( new Uint8Array(buffer), @@ -139,6 +141,7 @@ export async function bufferHashHex( const hashBuf = await crypto.subtle.digest(algo, buf); return bufferToHex(hashBuf); } + export async function stringHashHex( val: string, algo: AlgorithmIdentifier = "SHA-256", @@ -163,21 +166,21 @@ export function getPortRef(manifest: PortManifest) { export async function getInstallHash(install: InstallConfigResolvedX) { const fullHashHex = await objectHashHex(install as jsonHash.Tree); const hashHex = fullHashHex.slice(0, 8); - return `${install.portRef}+${hashHex}`; + return `${install.portRef}!${hashHex}`; } -export type PathRef = dax.PathRef; +export type Path = dax.Path; export function defaultCommandBuilder() { const builder = new dax.CommandBuilder() .printCommand(true); - builder.setPrintCommandLogger((_, cmd) => { + builder.setPrintCommandLogger((cmd) => { // clean up the already colorized print command logs // TODO: remove when https://github.com/dsherret/dax/pull/203 // is merged return logger().debug( "spawning", - $.stripAnsi(cmd).split(/\s/), + cmd, ); }); return builder; @@ -193,10 +196,10 @@ export const $ = dax.build$( iterableLimit: 500, }); }, - pathToString(path: dax.PathRef) { + pathToString(path: Path) { return path.toString(); }, - async removeIfExists(path: dax.PathRef | string) { + async removeIfExists(path: Path | string) { const pathRef = $.path(path); if (await pathRef.exists()) { await pathRef.remove({ recursive: true }); @@ -340,7 +343,10 @@ export type DownloadFileArgs = { mode?: number; headers?: Record; }; -/// This avoid re-downloading a file if it's already successfully downloaded before. + +/** + * This avoid re-downloading a file if it's already successfully downloaded before. + */ export async function downloadFile( args: DownloadFileArgs, ) { @@ -365,11 +371,11 @@ export async function downloadFile( await $.path(downloadPath).ensureDir(); - await tmpFilePath.copyFile(fileDwnPath); + await tmpFilePath.copy(fileDwnPath); return downloadPath.toString(); } -/* * +/** * This returns a tmp path that's guaranteed to be * on the same file system as targetDir by * checking if $TMPDIR satisfies that constraint @@ -402,8 +408,19 @@ export async function sameFsTmpRoot( // take care of it return $.path(await Deno.makeTempDir({ prefix: "ghjk_sync" })); } + export type Rc = ReturnType>; +/** + * A reference counted box that runs the dispose method when all refernces + * are disposed of.. + * @example Basic usage + * ``` + * using myVar = rc(setTimeout(() => console.log("hola)), clearTimeout) + * spawnOtherThing(myVar.clone()); + * // dispose will only run here as long as `spawnOtherThing` has no references + * ``` + */ export function rc(val: T, onDrop: (val: T) => void) { const rc = { counter: 1, @@ -429,6 +446,10 @@ export function rc(val: T, onDrop: (val: T) => void) { export type AsyncRc = ReturnType>; +/** + * A reference counted box that makse use of `asyncDispose`. + * `async using myVar = asyncRc(setTimeout(() => console.log("hola)), clearTimeout)` + */ export function asyncRc(val: T, onDrop: (val: T) => Promise) { const rc = { counter: 1, @@ -499,6 +520,11 @@ export async function expandGlobsAndAbsolutize(path: string, wd: string) { } return [std_path.resolve(wd, path)]; } + +/** + * Unwrap the result object returned by the `safeParse` method + * on zod schemas. + */ export function unwrapParseRes( res: zod.SafeParseReturnType, cause: object = {}, @@ -514,3 +540,28 @@ export function unwrapParseRes( } return res.data; } + +/** + * Attempts to detect the shell in use by the user. + */ +export async function detectShellPath(): Promise { + let path = Deno.env.get("SHELL"); + if (!path) { + try { + path = await $`ps -p ${Deno.ppid} -o comm=`.text(); + } catch { + return; + } + } + return path; +} + +/** + * {@inheritdoc detectShellPath} + */ +export async function detectShell(): Promise { + const shellPath = await detectShellPath(); + return shellPath + ? std_path.basename(shellPath, ".exe").toLowerCase().trim() + : undefined; +} diff --git a/utils/unarchive.ts b/utils/unarchive.ts index d6a581c0..4ff3b3fb 100644 --- a/utils/unarchive.ts +++ b/utils/unarchive.ts @@ -8,9 +8,11 @@ import { std_untar, } from "../deps/ports.ts"; -/// Uses file extension to determine type -/// Does not support extracting symlinks -/// Does not support tarballs using [GnuSparse](https://www.gnu.org/software/tar/manual/html_node/Sparse-Recovery.html) +/** + * - Uses file extension to determine archive type. + * - Does not support extracting symlinks + * - Does not support tarballs using {@link https://www.gnu.org/software/tar/manual/html_node/Sparse-Recovery.html | GnuSparse} + */ export async function unarchive( path: string, dest = "./", @@ -71,7 +73,9 @@ export async function untar( } } -/// This does not close the reader +/** + * This does not close the reader. + */ export async function untarReader( reader: Deno.Reader, dest = "./",