diff --git a/.ghjk/deno.lock b/.ghjk/deno.lock index 885e603d..7d4c9995 100644 --- a/.ghjk/deno.lock +++ b/.ghjk/deno.lock @@ -1,5 +1,95 @@ { "version": "3", + "packages": { + "specifiers": { + "jsr:@david/dax@0.41.0": "jsr:@david/dax@0.41.0", + "jsr:@david/which@^0.4.1": "jsr:@david/which@0.4.1", + "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:@noble/hashes@1.4.0": "npm:@noble/hashes@1.4.0", + "npm:multiformats@13.1.0": "npm:multiformats@13.1.0", + "npm:zod-validation-error@3.3.0": "npm:zod-validation-error@3.3.0_zod@3.23.8", + "npm:zod@3.23.8": "npm:zod@3.23.8" + }, + "jsr": { + "@david/dax@0.41.0": { + "integrity": "9e1ecf66a0415962cc8ad3ba4e3fa93ce0f1a1cc797dd95c36fdfb6977dc7fc8", + "dependencies": [ + "jsr:@david/which@^0.4.1", + "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.4.1": { + "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" + }, + "@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": { + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, + "multiformats@13.1.0": { + "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==", + "dependencies": {} + }, + "zod-validation-error@3.3.0_zod@3.23.8": { + "integrity": "sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==", + "dependencies": { + "zod": "zod@3.23.8" + } + }, + "zod@3.23.8": { + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dependencies": {} + } + } + }, "remote": { "https://deno.land/std@0.116.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", "https://deno.land/std@0.116.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", @@ -13,18 +103,6 @@ "https://deno.land/std@0.116.0/path/posix.ts": "34349174b9cd121625a2810837a82dd8b986bbaaad5ade690d1de75bbb4555b2", "https://deno.land/std@0.116.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", "https://deno.land/std@0.116.0/path/win32.ts": "11549e8c6df8307a8efcfa47ad7b2a75da743eac7d4c89c9723a944661c8bd2e", - "https://deno.land/std@0.120.0/_wasm_crypto/crypto.js": "5c283a80e1059d16589b79fa026be5fb0a28424302a99487cadceef8c17f8afa", - "https://deno.land/std@0.120.0/_wasm_crypto/crypto.wasm.js": "0e6df3c18beb1187b442ec7f0a03df4d18b21212172d6b4a50ee4816404771d7", - "https://deno.land/std@0.120.0/_wasm_crypto/mod.ts": "7d02009ef3ddc953c8f90561d213e02fa0a6f3eaed9b8baf0c241c8cbeec1ed3", - "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/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", @@ -242,137 +320,176 @@ "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.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/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/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/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", "https://deno.land/x/foras@v2.1.4/wasm/pkg/foras.js": "06f8875b456918b9671d52133f64f3047f1c95540feda87fdd4a55ba3d30091d", "https://deno.land/x/foras@v2.1.4/wasm/pkg/foras.wasm.js": "2df8522df7243b0f05b1d188e220629cd5d2c92080a5f1407e15396fc35bebb3", "https://deno.land/x/json_hash@0.2.0/canon.ts": "ce7c07abd871cd7f0eb1280ad9f58f6382f02f84a217898ce977cf35ad315877", - "https://deno.land/x/json_hash@0.2.0/crypto.ts": "8738b601a0cf52c0ff58242707e2d5f7f5ff8f7ca4d51d0282ad3b0bb56548cf", - "https://deno.land/x/json_hash@0.2.0/digest.ts": "95e3d996377eebebb960ad2b6e4fdd70d71543378a651c31de75f1e86b637fc7", - "https://deno.land/x/json_hash@0.2.0/hex.ts": "104154a6408c6b5b36ff35361011aeb3047941bd5a652724f5aebeeb89fcf9a8", - "https://deno.land/x/json_hash@0.2.0/merkle.ts": "cf48004b45fdf0412afd48fea0ba8bb16bf78f717a66a5ff505f6400a88c08cf", - "https://deno.land/x/json_hash@0.2.0/mod.ts": "b0fdd79a540d3fc6aa3e0a9a93fe6735b1a174d9ba2aba103e4a18ee4872acad", "https://deno.land/x/jszip@0.11.0/mod.ts": "5661ddc18e9ac9c07e3c5d2483bc912a7022b6af0d784bb7b05035973e640ba1", - "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", - "https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", - "https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", - "https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1", - "https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", - "https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", - "https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774", - "https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", - "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.8/ZodError.ts": "528da200fbe995157b9ae91498b103c4ef482217a5c086249507ac850bd78f52", + "https://deno.land/x/zod@v3.23.8/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.23.8/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.23.8/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.23.8/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.23.8/helpers/parseUtil.ts": "c14814d167cc286972b6e094df88d7d982572a08424b7cd50f862036b6fcaa77", + "https://deno.land/x/zod@v3.23.8/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.23.8/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.23.8/helpers/util.ts": "30c273131661ca5dc973f2cfb196fa23caf3a43e224cdde7a683b72e101a31fc", + "https://deno.land/x/zod@v3.23.8/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.23.8/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.23.8/mod.ts": "ec6e2b1255c1a350b80188f97bd0a6bac45801bb46fc48f50b9763aa66046039", + "https://deno.land/x/zod@v3.23.8/types.ts": "1b172c90782b1eaa837100ebb6abd726d79d6c1ec336350c8e851e0fd706bf5c", "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/0c5f78/deps/cli.ts": "22fdbfe7f39dc2caa9dd056a57a57051deec6b2a7ba9381e20e2ce7ab6af07e1", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/deps/common.ts": "5d676e006bb1485056935c263a967eee9fcfc1517249d1ca05a7645dca5e2e68", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/deps/ports.ts": "3c60d1f7ab626ffdd81b37f4e83a780910936480da8fe24f4ccceaefa207d339", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/files/deno/mod.ts": "1b8204c3df18b908408b2148b48af788e669d0debbeb8ba119418ab1ddf1ab8f", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/files/deno/worker.ts": "71f3cee9dba3c2bd59c85d2909eac325da556a9917ed6ea01222f6c217638dd9", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/host/mod.ts": "faea10bf051dc22443e0bb3cadb74599d2ef5e4543f065a75777bb57b818c022", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/host/types.ts": "359ceb8a800c5acd9ef4778e40ccfe039fd7724c06205ae3998398641a9b2370", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/install/mod.ts": "f78083efd15e82c8cc302dd801565f39c947497cfaa039fde1023f7e0d5ab368", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/install/utils.ts": "d4634d4fc0e963f540402b4ca7eb5dcba340eaa0d8fceb43af57d722ad267115", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/main.ts": "fb82696926c97ea6749151275cafce049c35c2d500188661ac8b1d205a3b9939", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/envs/mod.ts": "33ddee364795c1f22028e74071063fe85211949682bb94f1ca38396314cbd01e", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/envs/posix.ts": "3193141953de1bfe2d73549e651711f4e1a1d05f5fcc7655ab74160b25be09d0", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/envs/reducer.ts": "853347377f4b265792da2ece78dfde7602c2555341bbd9f8dfd7ac5fd7d989ad", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/envs/types.ts": "a03173fe013a41163471446f41c636bd23acc6e4956ea910e12cb203dc449a9e", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/mod.ts": "fc1cb9176c6557b44ae9c6536fa51c6c4f80ac01fc476d15b0a217e70cb0d176", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/ambient.ts": "823ec8d98702a60e6bfcdbeb64b69dc9f5039e73a1f10e87cd51210c1aaf52d5", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/base.ts": "8ef8a8de372420bddcd63a1b363937f43d898059e99478a58621e8432bcd5891", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/db.ts": "a309d1058f66079a481141c3f1733d928b9af8a37b7ce911b1228f70fd24df0f", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/ghrel.ts": "a1bf0e244080b8b2a62093f536bb7eff0b5a9c596f7eef9f516c11a80aad0be1", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/inter.ts": "62ddc0dede33b059dbd84d18411d0b0acceb145ff96b076401a96c980ae9bfc0", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/mod.ts": "6d4b907ad70a9946299bc5931c976fad1cb1b405466cf4cc8d2acb7d0ba3310c", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/reducers.ts": "eaabbc2cf5d16a55cff5b3f95180f3e1ddb09b1a755776da2931f8817f28a0df", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/sync.ts": "6bbaca38024fd1f6c6ba5811abe65052d2061527539f1893f768ace40016ab5f", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/types.ts": "1adbe5a901f765de106db6513eb770356eed156c435e94d51b7432dce401530e", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/types/platform.ts": "0ecffeda71919293f9ffdb6c564ddea4f23bc85c4e640b08ea78225d34387fdc", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/utils.ts": "6b14b331cce66bd46e7aec51f02424327d819150f16d3f72a6b0aaf7aee43c09", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/ports/worker.ts": "6b76ba1efb2e47a82582fc48bcc6264fe153a166beffccde1a9a3a185024c337", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/std.ts": "419d6b04680f73f7b252257ab287d68c1571cee4347301c53278e2b53df21c4a", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/tasks/deno.ts": "15d5bb6379f3add73cb0d8aa4b578998d87bcfaa939518166c30a7f906ea5750", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/tasks/exec.ts": "cc5db628d85a84b6193f59d7f5d98868f22a59f038716dc3d4fc5ac70494d625", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/tasks/mod.ts": "71a16751895ce8bb687c565602938773ac276ccb62d28a793db0b1715438ee9a", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/tasks/types.ts": "0bf2cf9ac1f5735dc95ac348175866abf602bd90d01c9275c708f767baa976c1", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/types.ts": "53de8906ea0149871e35c937f3e52dee1a615907971fa8ec3f322f4dfe6d40f3", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/modules/utils.ts": "b5866a52cd4e0e1c0dc8ccb56c7281aeff2e2bf5e16866b77eda36e0529e312a", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/port.ts": "c039a010dee7dfd978478cf4c5e2256c643135e10f33c30a09f8db9915e9d89d", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/setup_logger.ts": "f8a206bda0595497d6f4718032d4a959000b32ef3346d4b507777eec6a169458", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/utils/logger.ts": "fcbafb35ae4b812412b9b301ce6d06b8b9798f94ebebe3f92677e25e4b19af3c", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/utils/mod.ts": "d4d0c0198168f63bd084872bf7dfb40925301ecb65fd0501520db942f6c0c961", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/utils/unarchive.ts": "f6d0e9e75f470eeef5aecd0089169f4350fc30ebfdc05466bb7b30042294d6d3", + "https://raw.githubusercontent.com/metatypedev/ghjk/0c5f78/utils/url.ts": "e1ada6fd30fc796b8918c88456ea1b5bbd87a07d0a0538b092b91fd2bb9b7623", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/deps/cli.ts": "aac025f9372ad413b9c2663dc7f61affd597820d9448f010a510d541df3b56ea", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/deps/common.ts": "f775710b66a9099b98651cd3831906466e9b83ef98f2e5c080fd59ee801c28d4", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/deps/ports.ts": "3c60d1f7ab626ffdd81b37f4e83a780910936480da8fe24f4ccceaefa207d339", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/files/deno/mod.ts": "1b8204c3df18b908408b2148b48af788e669d0debbeb8ba119418ab1ddf1ab8f", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/files/deno/worker.ts": "8ded400d70a0bd40e281ceb1ffcdc82578443caf9c481b9eee77166472784282", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/host/mod.ts": "604e2729145c16226af91e6880e3eca30ea060688fb4941ab39d9489109dd62c", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/host/types.ts": "f450d9b9c0eced2650262d02455aa6f794de0edd6b052aade256882148e5697f", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/install/mod.ts": "f78083efd15e82c8cc302dd801565f39c947497cfaa039fde1023f7e0d5ab368", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/install/utils.ts": "d4634d4fc0e963f540402b4ca7eb5dcba340eaa0d8fceb43af57d722ad267115", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/main.ts": "21ea4582db19e163f4dd68ccdb19578c3c48e48dd23c094d8f8f88ab785e34e5", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/envs/inter.ts": "84805fa208754a08f185dca7a5236de3760bbc1d0df96af86ea5fd7778f827a2", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/envs/mod.ts": "b9483be6dbd4c282d1c5b134864b2ff0f53d8bfb25dba6c96e591c84ccf25e01", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/envs/posix.ts": "09e410e3fea9c303a5148ff2a22697474320442b9fea0bd3fc932d6828fe820f", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/envs/reducer.ts": "853347377f4b265792da2ece78dfde7602c2555341bbd9f8dfd7ac5fd7d989ad", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/envs/types.ts": "ab9715cf02e9d73f553ae757db347863be23e1e9daf94d18aab716fc27b3dbc1", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/mod.ts": "fc1cb9176c6557b44ae9c6536fa51c6c4f80ac01fc476d15b0a217e70cb0d176", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/ambient.ts": "823ec8d98702a60e6bfcdbeb64b69dc9f5039e73a1f10e87cd51210c1aaf52d5", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/base.ts": "8ef8a8de372420bddcd63a1b363937f43d898059e99478a58621e8432bcd5891", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/db.ts": "a309d1058f66079a481141c3f1733d928b9af8a37b7ce911b1228f70fd24df0f", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/ghrel.ts": "a1bf0e244080b8b2a62093f536bb7eff0b5a9c596f7eef9f516c11a80aad0be1", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/inter.ts": "b3999e73d73d7f928a8de86e5e2261fe6b1450ceedfb54f24537bf0803532ed0", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/mod.ts": "2b5d4773d64641cdc0aacf09ece6c40d094feb090280647c68f33bbfa8dceee7", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/reducers.ts": "eaabbc2cf5d16a55cff5b3f95180f3e1ddb09b1a755776da2931f8817f28a0df", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/sync.ts": "a7a297f6b098360d56af168692f3cff96f8ceeb5189e5baa249e094f8d9c42ef", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/types.ts": "f4dbd1a3f4b7f539b3a85418617d25adbf710b54144161880d48f6c4ec032eee", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/types/platform.ts": "0ecffeda71919293f9ffdb6c564ddea4f23bc85c4e640b08ea78225d34387fdc", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/utils.ts": "6b14b331cce66bd46e7aec51f02424327d819150f16d3f72a6b0aaf7aee43c09", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/ports/worker.ts": "6b76ba1efb2e47a82582fc48bcc6264fe153a166beffccde1a9a3a185024c337", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/std.ts": "419d6b04680f73f7b252257ab287d68c1571cee4347301c53278e2b53df21c4a", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/tasks/deno.ts": "2b9f33253ac1257eb79a4981cd221509aa9ecf8a3c36d7bd8be1cd6c1150100b", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/tasks/exec.ts": "eaf6b2f9639185fa76f560276e0d28d262a6c78d2bdc0d579e7683e062d7b542", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/tasks/inter.ts": "63e8f2860f7e3b4d95b6f61ca56aeb8567e4f265aa9c22cace6c8075edd6210f", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/tasks/mod.ts": "438f1cbb5e96470f380b6954bb18ad7693ed33bb99314137ff7080d82d026615", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/tasks/types.ts": "072a34bd0749428bad4d612cc86abe463d4d4f74dc56cf0a48a1f41650e2399b", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/modules/types.ts": "c0f212b686a2721d076e9aeb127596c7cbc939758e2cc32fd1d165a8fb320a87", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/port.ts": "c039a010dee7dfd978478cf4c5e2256c643135e10f33c30a09f8db9915e9d89d", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/setup_logger.ts": "f8a206bda0595497d6f4718032d4a959000b32ef3346d4b507777eec6a169458", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/utils/logger.ts": "fcbafb35ae4b812412b9b301ce6d06b8b9798f94ebebe3f92677e25e4b19af3c", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/utils/mod.ts": "fe8b14465fbcbf3a952af48083a17304c294f296591752dff3ca141386c2d46b", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/utils/unarchive.ts": "f6d0e9e75f470eeef5aecd0089169f4350fc30ebfdc05466bb7b30042294d6d3", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/utils/url.ts": "e1ada6fd30fc796b8918c88456ea1b5bbd87a07d0a0538b092b91fd2bb9b7623", + "https://raw.githubusercontent.com/metatypedev/ghjk/5bb0d24/utils/worker.ts": "ac4caf72a36d2e4af4f4e92f2e0a95f9fc2324b568640f24c7c2ff6dc0c11d62" } } diff --git a/.ghjk/lock.json b/.ghjk/lock.json index 09ac08a0..844d0807 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -5,86 +5,89 @@ "ports": { "version": "0", "configResolutions": { - "95dbc2b8c604a5996b88c5b1b4fb0c10b3e0d9cac68f57eb915b012c44288e93": { - "version": "v0.2.60", - "depConfigs": {}, - "portRef": "act_ghrel@0.1.0" + "bciqjlw6cxddajjmznoemlmnu7mgbbm7a3hfmnd2x5oivwajmiqui5ey": { + "version": "v0.2.63", + "buildDepConfigs": {}, + "portRef": "act_ghrel@0.1.0", + "specifiedVersion": false }, - "076a5b8ee3bdc68ebf20a696378458465042bb7dc1e49ac2dc98e5fa0dab3e25": { - "version": "3.6.2", - "depConfigs": { + "bciqao2s3r3r33ruox4qknfrxqrmemuccxn64dze2ylojrzp2bwvt4ji": { + "version": "3.7.1", + "buildDepConfigs": { "cpy_bs_ghrel": { - "version": "3.12.1", - "depConfigs": { + "version": "3.12.3", + "buildDepConfigs": { "tar_aa": { - "version": "1.35", - "depConfigs": {}, - "portRef": "tar_aa@0.1.0" + "version": "1.34", + "buildDepConfigs": {}, + "portRef": "tar_aa@0.1.0", + "specifiedVersion": false }, "zstd_aa": { - "version": "v1.5.5,", - "depConfigs": {}, - "portRef": "zstd_aa@0.1.0" + "version": "v1.4.8,", + "buildDepConfigs": {}, + "portRef": "zstd_aa@0.1.0", + "specifiedVersion": false } }, - "portRef": "cpy_bs_ghrel@0.1.0" + "portRef": "cpy_bs_ghrel@0.1.0", + "specifiedVersion": false } }, "portRef": "pipi_pypi@0.1.0", - "packageName": "pre-commit" + "packageName": "pre-commit", + "specifiedVersion": false }, - "84ecde630296f01e7cb8443c58d1596d668c357a0d9837c0a678b8a541ed0a39": { - "version": "3.12.1", - "depConfigs": { + "bciqij3g6mmbjn4a6ps4eipcy2fmw2zumgv5a3gbxycthroffihwquoi": { + "version": "3.12.3", + "buildDepConfigs": { "tar_aa": { - "version": "1.35", - "depConfigs": {}, - "portRef": "tar_aa@0.1.0" + "version": "1.34", + "buildDepConfigs": {}, + "portRef": "tar_aa@0.1.0", + "specifiedVersion": false }, "zstd_aa": { - "version": "v1.5.5,", - "depConfigs": {}, - "portRef": "zstd_aa@0.1.0" + "version": "v1.4.8,", + "buildDepConfigs": {}, + "portRef": "zstd_aa@0.1.0", + "specifiedVersion": false } }, - "portRef": "cpy_bs_ghrel@0.1.0" + "portRef": "cpy_bs_ghrel@0.1.0", + "specifiedVersion": false }, - "9e3fa7742c431c34ae7ba8d1e907e50c937ccfb631fb4dcfb7a1773742abe267": { - "version": "1.35", - "depConfigs": {}, - "portRef": "tar_aa@0.1.0" + "bciqj4p5hoqweghbuvz52rupja7sqze34z63dd62nz632c5zxikv6ezy": { + "version": "1.34", + "buildDepConfigs": {}, + "portRef": "tar_aa@0.1.0", + "specifiedVersion": false }, - "4f16c72030e922711abf15474d30e3cb232b18144beb73322b297edecfcdb86f": { - "version": "v1.5.5,", - "depConfigs": {}, - "portRef": "zstd_aa@0.1.0" + "bciqe6fwheayositrdk7rkr2ngdr4wizldakex23tgivss7w6z7g3q3y": { + "version": "v1.4.8,", + "buildDepConfigs": {}, + "portRef": "zstd_aa@0.1.0", + "specifiedVersion": false }, - "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" + "bciqkhpxlimubssnvndchh3qoipg6faxas7443pbsguwmd5xcqnbjpyy": { + "version": "v1.44.2", + "buildDepConfigs": {}, + "portRef": "deno_ghrel@0.1.0", + "specifiedVersion": false }, - "a79698808eea53aedd8e83387b2f44e90a1a48d76193c5ccf0fc6efe29bd70f6": { - "version": "v25.3", - "depConfigs": {}, - "portRef": "protoc_ghrel@0.1.0" + "bciqfvlwwndlfuqibybkgee3fgt7cst5ltpztmm3by6hib5veial5spy": { + "version": "v1.44.2", + "buildDepConfigs": {}, + "portRef": "deno_ghrel@0.1.0", + "specifiedVersion": true } } }, "tasks": { "version": "0" + }, + "envs": { + "version": "0" } }, "config": { @@ -92,24 +95,20 @@ { "id": "ports", "config": { - "installs": [ - "c4cf06e095dadfbdd5e26070bc2b7baffc5ff45f", - "9283b97b5499e8da4dcfb7f14c1306c25e8e8a44", - "7d7b0f4b9ec5375688fceab016687f3ac3fbc94c" - ], - "allowedDeps": { - "tar_aa": "e0d1f160d2d7755765f6f01a27a0c33a02ff98d2", - "git_aa": "9d26d0d90f6ecdd69d0705a042b01a344aa626ee", - "curl_aa": "3c447f912abf18883bd05314f946740975ee0dd3", - "unzip_aa": "dfb0f5e74666817e6ab8cbceca0c9da271142bca", - "zstd_aa": "d9122eff1fe3ef56872e53dae725ff3ccb37472e", - "rustup_rustlang": "8f14cde4f25c276d5e54538d91a6ac6d3eec3e8d", - "rust_rustup": "9fc8f32a0f79253defdb8845e2d6a4df69b526b9", - "cargo_binstall_ghrel": "45999e7561d7f6a661191f58ee35e67755d375e0", - "pnpm_ghrel": "b80f4de14adc81c11569bf5f3a2d10b92ad5f1a7", - "asdf_plugin_git": "a36b37f4eda81bf51a50d00362637690c7fea473", - "node_org": "5843605c861f0b7307c0192a1628c3823fe28ed9", - "cpy_bs_ghrel": "7a33163826283c47b52964a23b87a4762662c746" + "sets": { + "ghjkEnvProvInstSet___main": { + "installs": [ + "bciqe72molvtvcuj3tuh47ziue2oqd6t4qetxn3rsoa764ofup6uwjmi", + "bciqe4zlekl4uqqbhxunac7br24mrf6cdpfrfblahqa4vrgaqjujcl4i", + "bciqjyl5um6634zwpw6cewv22chzlrsvhedbjahyghhy2zraqqgyiv2q", + "bciqmgggy7hd5as3zz7pzbx54va7lq657bdxvthntxphhlbsl2434dgq" + ], + "allowedBuildDeps": "bciqjx7llw7t6pfczypzmhbwv7sxaicruj5pdbuac47m4c5qyildiowi" + }, + "ghjkEnvProvInstSet___test": { + "installs": [], + "allowedBuildDeps": "bciqjx7llw7t6pfczypzmhbwv7sxaicruj5pdbuac47m4c5qyildiowi" + } } } }, @@ -117,494 +116,464 @@ "id": "tasks", "config": { "tasks": { - "greet": { - "name": "greet", - "dependsOn": [], - "env": { - "installs": [], - "env": {}, - "allowedPortDeps": [] - } - }, - "ha": { - "name": "ha", - "dependsOn": [], - "env": { - "installs": [ - "f48ddfcfec810fcfcfc155fef7281a8c139c26fa" - ], - "env": { - "STUFF": "stuffier" - }, - "allowedPortDeps": [ - "e0d1f160d2d7755765f6f01a27a0c33a02ff98d2", - "9d26d0d90f6ecdd69d0705a042b01a344aa626ee", - "3c447f912abf18883bd05314f946740975ee0dd3", - "dfb0f5e74666817e6ab8cbceca0c9da271142bca", - "d9122eff1fe3ef56872e53dae725ff3ccb37472e", - "8f14cde4f25c276d5e54538d91a6ac6d3eec3e8d", - "9fc8f32a0f79253defdb8845e2d6a4df69b526b9", - "45999e7561d7f6a661191f58ee35e67755d375e0", - "b80f4de14adc81c11569bf5f3a2d10b92ad5f1a7", - "a36b37f4eda81bf51a50d00362637690c7fea473" - ] - } - }, - "ho": { - "name": "ho", - "dependsOn": [ - "ha" - ], - "env": { - "installs": [], - "env": {}, - "allowedPortDeps": [] - } - }, - "hum": { - "name": "hum", - "dependsOn": [ - "ho" - ], - "env": { - "installs": [], - "env": {}, - "allowedPortDeps": [] - } - }, - "hii": { - "name": "hii", - "dependsOn": [ - "hum" - ], - "env": { - "installs": [], - "env": {}, - "allowedPortDeps": [] - } + "lock-sed": { + "ty": "denoFile@v1", + "key": "lock-sed", + "envKey": "bciqekhy7ndyc6hmkzspdsguxjgvyz5yedr5weigsqsa72kyloity4jy" + } + }, + "tasksNamed": [ + "lock-sed" + ] + } + }, + { + "id": "envs", + "config": { + "envs": { + "bciqekhy7ndyc6hmkzspdsguxjgvyz5yedr5weigsqsa72kyloity4jy": { + "provides": [ + { + "ty": "ghjk.ports.InstallSetRef", + "setId": "ghjkEnvProvInstSet___test" + } + ] }, - "hey": { - "name": "hey", - "dependsOn": [ - "hii", - "ho" - ], - "env": { - "installs": [], - "env": {}, - "allowedPortDeps": [] - } + "bciqfzekhtsrjd72noxifmici3ssck4jgvbjwhxwhhwtirzm7yomhxya": { + "desc": "the default default environment.", + "provides": [ + { + "ty": "ghjk.ports.InstallSetRef", + "setId": "ghjkEnvProvInstSet___main" + } + ] } + }, + "defaultEnv": "main", + "envsNamed": { + "main": "bciqfzekhtsrjd72noxifmici3ssck4jgvbjwhxwhhwtirzm7yomhxya" } } } ], - "globalEnv": { - "installs": { - "f48ddfcfec810fcfcfc155fef7281a8c139c26fa": { - "port": { - "ty": "denoWorker@v1", - "name": "protoc_ghrel", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin" - ], - "version": "0.1.0", - "moduleSpecifier": "file:///data/home/ghjk/ports/protoc.ts" - } - }, - "c4cf06e095dadfbdd5e26070bc2b7baffc5ff45f": { - "port": { - "ty": "denoWorker@v1", - "name": "act_ghrel", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin", - "aarch64-windows", - "x86_64-windows" - ], - "version": "0.1.0", - "moduleSpecifier": "file:///ports/act.ts" - } + "blackboard": { + "bciqe72molvtvcuj3tuh47ziue2oqd6t4qetxn3rsoa764ofup6uwjmi": { + "port": { + "ty": "denoWorker@v1", + "name": "act_ghrel", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "moduleSpecifier": "file:///ports/act.ts" + } + }, + "bciqe4zlekl4uqqbhxunac7br24mrf6cdpfrfblahqa4vrgaqjujcl4i": { + "port": { + "ty": "denoWorker@v1", + "name": "pipi_pypi", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows", + "x86_64-freebsd", + "aarch64-freebsd", + "x86_64-netbsd", + "aarch64-netbsd", + "x86_64-aix", + "aarch64-aix", + "x86_64-solaris", + "aarch64-solaris", + "x86_64-illumos", + "aarch64-illumos", + "x86_64-android", + "aarch64-android" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "cpy_bs_ghrel" + } + ], + "moduleSpecifier": "file:///ports/pipi.ts" }, - "9283b97b5499e8da4dcfb7f14c1306c25e8e8a44": { - "port": { - "ty": "denoWorker@v1", - "name": "pipi_pypi", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows", - "x86_64-freebsd", - "aarch64-freebsd", - "x86_64-netbsd", - "aarch64-netbsd", - "x86_64-aix", - "aarch64-aix", - "x86_64-solaris", - "aarch64-solaris", - "x86_64-illumos", - "aarch64-illumos", - "x86_64-android", - "aarch64-android" - ], - "version": "0.1.0", - "deps": [ - { - "name": "cpy_bs_ghrel" - } - ], - "moduleSpecifier": "file:///ports/pipi.ts" - }, - "packageName": "pre-commit" + "packageName": "pre-commit" + }, + "bciqjyl5um6634zwpw6cewv22chzlrsvhedbjahyghhy2zraqqgyiv2q": { + "port": { + "ty": "denoWorker@v1", + "name": "cpy_bs_ghrel", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "tar_aa" + }, + { + "name": "zstd_aa" + } + ], + "moduleSpecifier": "file:///ports/cpy_bs.ts" + } + }, + "bciqmgggy7hd5as3zz7pzbx54va7lq657bdxvthntxphhlbsl2434dgq": { + "version": "1.44.2", + "port": { + "ty": "denoWorker@v1", + "name": "deno_ghrel", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "moduleSpecifier": "file:///ports/deno_ghrel.ts" + } + }, + "bciqb6ua63xodzwxngnbjq35hfikiwzb3dclbqkc7e6xgjdt5jin4pia": { + "manifest": { + "ty": "ambientAccess@v1", + "name": "tar_aa", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin" + ], + "version": "0.1.0", + "execName": "tar", + "versionExtractFlag": "--version", + "versionExtractRegex": "(\\d+\\.\\d+)", + "versionExtractRegexFlags": "" }, - "7d7b0f4b9ec5375688fceab016687f3ac3fbc94c": { - "port": { - "ty": "denoWorker@v1", - "name": "cpy_bs_ghrel", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows" - ], - "version": "0.1.0", - "deps": [ - { - "name": "tar_aa" - }, - { - "name": "zstd_aa" - } - ], - "moduleSpecifier": "file:///ports/cpy_bs.ts" - }, - "releaseTag": "20231002" + "defaultInst": { + "portRef": "tar_aa@0.1.0" } }, - "allowedPortDeps": { - "e0d1f160d2d7755765f6f01a27a0c33a02ff98d2": { - "manifest": { - "ty": "ambientAccess@v1", - "name": "tar_aa", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin" - ], - "version": "0.1.0", - "execName": "tar", - "versionExtractFlag": "--version", - "versionExtractRegex": "(\\d+\\.\\d+)", - "versionExtractRegexFlags": "" - }, - "defaultInst": { - "portRef": "tar_aa@0.1.0" - } + "bciqfl5s36w335ducrb6f6gwb3vuwup7vzqwwg67pq42xtkngsnxqobi": { + "manifest": { + "ty": "ambientAccess@v1", + "name": "git_aa", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows", + "x86_64-freebsd", + "aarch64-freebsd", + "x86_64-netbsd", + "aarch64-netbsd", + "x86_64-aix", + "aarch64-aix", + "x86_64-solaris", + "aarch64-solaris", + "x86_64-illumos", + "aarch64-illumos", + "x86_64-android", + "aarch64-android" + ], + "version": "0.1.0", + "execName": "git", + "versionExtractFlag": "--version", + "versionExtractRegex": "(\\d+\\.\\d+\\.\\d+)", + "versionExtractRegexFlags": "" }, - "9d26d0d90f6ecdd69d0705a042b01a344aa626ee": { - "manifest": { - "ty": "ambientAccess@v1", - "name": "git_aa", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows", - "x86_64-freebsd", - "aarch64-freebsd", - "x86_64-netbsd", - "aarch64-netbsd", - "x86_64-aix", - "aarch64-aix", - "x86_64-solaris", - "aarch64-solaris", - "x86_64-illumos", - "aarch64-illumos", - "x86_64-android", - "aarch64-android" - ], - "version": "0.1.0", - "execName": "git", - "versionExtractFlag": "--version", - "versionExtractRegex": "(\\d+\\.\\d+\\.\\d+)", - "versionExtractRegexFlags": "" - }, - "defaultInst": { - "portRef": "git_aa@0.1.0" - } + "defaultInst": { + "portRef": "git_aa@0.1.0" + } + }, + "bciqcfe7qyxmokpn6pgtaj35r5qg74jkehuu6cvyrtcsnegvwlm64oqy": { + "manifest": { + "ty": "ambientAccess@v1", + "name": "curl_aa", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows", + "x86_64-freebsd", + "aarch64-freebsd", + "x86_64-netbsd", + "aarch64-netbsd", + "x86_64-aix", + "aarch64-aix", + "x86_64-solaris", + "aarch64-solaris", + "x86_64-illumos", + "aarch64-illumos", + "x86_64-android", + "aarch64-android" + ], + "version": "0.1.0", + "execName": "curl", + "versionExtractFlag": "--version", + "versionExtractRegex": "(\\d+\\.\\d+\\.\\d+)", + "versionExtractRegexFlags": "" }, - "3c447f912abf18883bd05314f946740975ee0dd3": { - "manifest": { - "ty": "ambientAccess@v1", - "name": "curl_aa", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows", - "x86_64-freebsd", - "aarch64-freebsd", - "x86_64-netbsd", - "aarch64-netbsd", - "x86_64-aix", - "aarch64-aix", - "x86_64-solaris", - "aarch64-solaris", - "x86_64-illumos", - "aarch64-illumos", - "x86_64-android", - "aarch64-android" - ], - "version": "0.1.0", - "execName": "curl", - "versionExtractFlag": "--version", - "versionExtractRegex": "(\\d+\\.\\d+\\.\\d+)", - "versionExtractRegexFlags": "" - }, - "defaultInst": { - "portRef": "curl_aa@0.1.0" - } + "defaultInst": { + "portRef": "curl_aa@0.1.0" + } + }, + "bciqgkpwxjmo5phw5se4ugyiz4xua3xrd54quzmk7wdwpq3vghglogjy": { + "manifest": { + "ty": "ambientAccess@v1", + "name": "unzip_aa", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "execName": "unzip", + "versionExtractFlag": "-v", + "versionExtractRegex": "(\\d+\\.\\d+)", + "versionExtractRegexFlags": "" }, - "dfb0f5e74666817e6ab8cbceca0c9da271142bca": { - "manifest": { - "ty": "ambientAccess@v1", - "name": "unzip_aa", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin", - "aarch64-windows", - "x86_64-windows" - ], - "version": "0.1.0", - "execName": "unzip", - "versionExtractFlag": "-v", - "versionExtractRegex": "(\\d+\\.\\d+)", - "versionExtractRegexFlags": "" - }, - "defaultInst": { - "portRef": "unzip_aa@0.1.0" - } + "defaultInst": { + "portRef": "unzip_aa@0.1.0" + } + }, + "bciqmcvyepuficjj3mwshsbfecwdmzch5gwxqo557icnq4zujtdllh4a": { + "manifest": { + "ty": "ambientAccess@v1", + "name": "zstd_aa", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin" + ], + "version": "0.1.0", + "execName": "zstd", + "versionExtractFlag": "--version", + "versionExtractRegex": "v(\\d+\\.\\d+\\.\\d+),", + "versionExtractRegexFlags": "" }, - "d9122eff1fe3ef56872e53dae725ff3ccb37472e": { - "manifest": { - "ty": "ambientAccess@v1", - "name": "zstd_aa", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin" - ], - "version": "0.1.0", - "execName": "zstd", - "versionExtractFlag": "--version", - "versionExtractRegex": "v(\\d+\\.\\d+\\.\\d+),", - "versionExtractRegexFlags": "" - }, - "defaultInst": { - "portRef": "zstd_aa@0.1.0" - } + "defaultInst": { + "portRef": "zstd_aa@0.1.0" + } + }, + "bciqk4ivbyqvpxwcaj5reufmveqldiizo6xmqiqq7njtaczgappydoka": { + "manifest": { + "ty": "denoWorker@v1", + "name": "rustup_rustlang", + "platforms": [ + "x86_64-darwin", + "aarch64-darwin", + "x86_64-linux", + "aarch64-linux", + "x86_64-windows", + "x86_64-illumos", + "x86_64-freebsd", + "x86_64-netbsd" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "git_aa" + } + ], + "resolutionDeps": [ + { + "name": "git_aa" + } + ], + "moduleSpecifier": "file:///ports/rustup.ts" }, - "8f14cde4f25c276d5e54538d91a6ac6d3eec3e8d": { - "manifest": { - "ty": "denoWorker@v1", - "name": "rustup_rustlang", - "platforms": [ - "x86_64-darwin", - "aarch64-darwin", - "x86_64-linux", - "aarch64-linux", - "x86_64-windows", - "x86_64-illumos", - "x86_64-freebsd", - "x86_64-netbsd" - ], - "version": "0.1.0", - "deps": [ - { - "name": "git_aa" - } - ], - "resolutionDeps": [ - { - "name": "git_aa" - } - ], - "moduleSpecifier": "file:///ports/rustup.ts" - }, - "defaultInst": { - "portRef": "rustup_rustlang@0.1.0" - } + "defaultInst": { + "portRef": "rustup_rustlang@0.1.0" + } + }, + "bciqjcmf46h2h6teenwbsda35igg4hea6ro5vh6nfieehk4jkuiqaj2a": { + "manifest": { + "ty": "denoWorker@v1", + "name": "rust_rustup", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows", + "x86_64-freebsd", + "aarch64-freebsd", + "x86_64-netbsd", + "aarch64-netbsd", + "x86_64-aix", + "aarch64-aix", + "x86_64-solaris", + "aarch64-solaris", + "x86_64-illumos", + "aarch64-illumos", + "x86_64-android", + "aarch64-android" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "rustup_rustlang" + } + ], + "moduleSpecifier": "file:///ports/rust.ts" }, - "9fc8f32a0f79253defdb8845e2d6a4df69b526b9": { - "manifest": { - "ty": "denoWorker@v1", - "name": "rust_rustup", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows", - "x86_64-freebsd", - "aarch64-freebsd", - "x86_64-netbsd", - "aarch64-netbsd", - "x86_64-aix", - "aarch64-aix", - "x86_64-solaris", - "aarch64-solaris", - "x86_64-illumos", - "aarch64-illumos", - "x86_64-android", - "aarch64-android" - ], - "version": "0.1.0", - "deps": [ - { - "name": "rustup_rustlang" - } - ], - "moduleSpecifier": "file:///ports/rust.ts" - }, - "defaultInst": { - "portRef": "rust_rustup@0.1.0" - } + "defaultInst": { + "portRef": "rust_rustup@0.1.0" + } + }, + "bciqpgt5wsiw4y7qzovqbt2yrdgq5mvhhjpcg6cxzt4w4taudyen44ca": { + "manifest": { + "ty": "denoWorker@v1", + "name": "cargo_binstall_ghrel", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin" + ], + "version": "0.1.0", + "moduleSpecifier": "file:///ports/cargo-binstall.ts" }, - "45999e7561d7f6a661191f58ee35e67755d375e0": { - "manifest": { - "ty": "denoWorker@v1", - "name": "cargo_binstall_ghrel", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin" - ], - "version": "0.1.0", - "moduleSpecifier": "file:///ports/cargo-binstall.ts" - }, - "defaultInst": { - "portRef": "cargo_binstall_ghrel@0.1.0" - } + "defaultInst": { + "portRef": "cargo_binstall_ghrel@0.1.0" + } + }, + "bciqo7cq7igschrhers3wiibbqpaavdf33fdfdalr4cu7gxr7cblifby": { + "manifest": { + "ty": "denoWorker@v1", + "name": "pnpm_ghrel", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "moduleSpecifier": "file:///ports/pnpm.ts" }, - "b80f4de14adc81c11569bf5f3a2d10b92ad5f1a7": { - "manifest": { - "ty": "denoWorker@v1", - "name": "pnpm_ghrel", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin", - "aarch64-windows", - "x86_64-windows" - ], - "version": "0.1.0", - "moduleSpecifier": "file:///ports/pnpm.ts" - }, - "defaultInst": { - "portRef": "pnpm_ghrel@0.1.0" - } + "defaultInst": { + "portRef": "pnpm_ghrel@0.1.0" + } + }, + "bciqoxx4uhfhw77sux6kzqhy6bvxhxkk4cqigrxdrmggillzkfjgjnli": { + "manifest": { + "ty": "denoWorker@v1", + "name": "asdf_plugin_git", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "git_aa" + } + ], + "resolutionDeps": [ + { + "name": "git_aa" + } + ], + "moduleSpecifier": "file:///ports/asdf_plugin_git.ts" }, - "a36b37f4eda81bf51a50d00362637690c7fea473": { - "manifest": { - "ty": "denoWorker@v1", - "name": "asdf_plugin_git", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin", - "aarch64-windows", - "x86_64-windows" - ], - "version": "0.1.0", - "deps": [ - { - "name": "git_aa" - } - ], - "resolutionDeps": [ - { - "name": "git_aa" - } - ], - "moduleSpecifier": "file:///ports/asdf_plugin_git.ts" - }, - "defaultInst": { - "portRef": "asdf_plugin_git@0.1.0" - } + "defaultInst": { + "portRef": "asdf_plugin_git@0.1.0" + } + }, + "bciqboouqnp54fnumgxvl7uay2k6ho4vhlbibvgoyyt5yt3rkwqaohzi": { + "manifest": { + "ty": "denoWorker@v1", + "name": "node_org", + "platforms": [ + "aarch64-linux", + "x86_64-linux", + "aarch64-darwin", + "x86_64-darwin", + "aarch64-windows", + "x86_64-windows" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "tar_aa" + } + ], + "moduleSpecifier": "file:///ports/node.ts" }, - "5843605c861f0b7307c0192a1628c3823fe28ed9": { - "manifest": { - "ty": "denoWorker@v1", - "name": "node_org", - "platforms": [ - "aarch64-linux", - "x86_64-linux", - "aarch64-darwin", - "x86_64-darwin", - "aarch64-windows", - "x86_64-windows" - ], - "version": "0.1.0", - "deps": [ - { - "name": "tar_aa" - } - ], - "moduleSpecifier": "file:///ports/node.ts" - }, - "defaultInst": { - "portRef": "node_org@0.1.0" - } + "defaultInst": { + "portRef": "node_org@0.1.0" + } + }, + "bciqctvtiscapp6cmlaxuaxnyac664hs3y3xsa5kqh4ctmhbsiehusly": { + "manifest": { + "ty": "denoWorker@v1", + "name": "cpy_bs_ghrel", + "platforms": [ + "x86_64-linux", + "aarch64-linux", + "x86_64-darwin", + "aarch64-darwin", + "x86_64-windows", + "aarch64-windows" + ], + "version": "0.1.0", + "buildDeps": [ + { + "name": "tar_aa" + }, + { + "name": "zstd_aa" + } + ], + "moduleSpecifier": "file:///ports/cpy_bs.ts" }, - "7a33163826283c47b52964a23b87a4762662c746": { - "manifest": { - "ty": "denoWorker@v1", - "name": "cpy_bs_ghrel", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows" - ], - "version": "0.1.0", - "deps": [ - { - "name": "tar_aa" - }, - { - "name": "zstd_aa" - } - ], - "moduleSpecifier": "file:///ports/cpy_bs.ts" - }, - "defaultInst": { - "portRef": "cpy_bs_ghrel@0.1.0" - } + "defaultInst": { + "portRef": "cpy_bs_ghrel@0.1.0" } + }, + "bciqjx7llw7t6pfczypzmhbwv7sxaicruj5pdbuac47m4c5qyildiowi": { + "tar_aa": "bciqb6ua63xodzwxngnbjq35hfikiwzb3dclbqkc7e6xgjdt5jin4pia", + "git_aa": "bciqfl5s36w335ducrb6f6gwb3vuwup7vzqwwg67pq42xtkngsnxqobi", + "curl_aa": "bciqcfe7qyxmokpn6pgtaj35r5qg74jkehuu6cvyrtcsnegvwlm64oqy", + "unzip_aa": "bciqgkpwxjmo5phw5se4ugyiz4xua3xrd54quzmk7wdwpq3vghglogjy", + "zstd_aa": "bciqmcvyepuficjj3mwshsbfecwdmzch5gwxqo557icnq4zujtdllh4a", + "rustup_rustlang": "bciqk4ivbyqvpxwcaj5reufmveqldiizo6xmqiqq7njtaczgappydoka", + "rust_rustup": "bciqjcmf46h2h6teenwbsda35igg4hea6ro5vh6nfieehk4jkuiqaj2a", + "cargo_binstall_ghrel": "bciqpgt5wsiw4y7qzovqbt2yrdgq5mvhhjpcg6cxzt4w4taudyen44ca", + "pnpm_ghrel": "bciqo7cq7igschrhers3wiibbqpaavdf33fdfdalr4cu7gxr7cblifby", + "asdf_plugin_git": "bciqoxx4uhfhw77sux6kzqhy6bvxhxkk4cqigrxdrmggillzkfjgjnli", + "node_org": "bciqboouqnp54fnumgxvl7uay2k6ho4vhlbibvgoyyt5yt3rkwqaohzi", + "cpy_bs_ghrel": "bciqctvtiscapp6cmlaxuaxnyac664hs3y3xsa5kqh4ctmhbsiehusly" } } } diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 35ea4f49..87e0a325 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: env: - DENO_VERSION: "1.42.1" + DENO_VERSION: "1.44.2" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GHJK_LOG_PANIC_LEVEL: error DENO_DIR: .deno-dir @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: metatypedev/setup-ghjk@2e8bbf084060a18828338a7cdd43fde6feb2a3cc + - uses: metatypedev/setup-ghjk@318209a9d215f70716a4ac89dbeb9653a2deb8bc with: installer-url: ./install.ts env: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3eb4f7d0..8e18270f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,11 +9,13 @@ on: - ready_for_review env: - DENO_VERSION: "1.42.1" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DENO_VERSION: "1.44.2" GHJK_LOG: debug GHJK_LOG_PANIC_LEVEL: error DENO_DIR: .deno-dir + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # removing the images after every test is unncessary + DOCKER_NO_RMI: 1 jobs: changes: @@ -30,7 +32,14 @@ jobs: - uses: denoland/setup-deno@v1 with: deno-version: ${{ env.DENO_VERSION }} + # run ghjk once to avoid trigger file changes when + # pre commit runs ghjk. We'll always see changes + # to lock.json since GITHUB_TOKEN is different + # in the CI + - run: deno run --unstable -A main.ts print config - uses: pre-commit/action@v3.0.1 + env: + SKIP: ghjk-resolve test-e2e: runs-on: "${{ matrix.os }}" @@ -40,7 +49,7 @@ jobs: - os: ubuntu-latest platform: linux/x86_64 e2eType: "docker" - - os: custom-macos + - os: custom-arm platform: linux/aarch64 e2eType: "docker" - os: macos-latest @@ -77,12 +86,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: metatypedev/setup-ghjk@2e8bbf084060a18828338a7cdd43fde6feb2a3cc + - uses: metatypedev/setup-ghjk@318209a9d215f70716a4ac89dbeb9653a2deb8bc with: installer-url: ./install.ts env: GHJKFILE: ./examples/protoc/ghjk.ts - run: | - cd examples/protoc + cd examples/tasks . $(ghjk print share-dir-path)/env.sh - protoc --version + ghjk x hey diff --git a/.gitignore b/.gitignore index dc99f6cd..3f14b39f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store play.* +examples/**/.ghjk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 792a1f18..13f702e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,6 +35,16 @@ repos: - commit-msg - repo: local hooks: + - id: lock-sed + name: Sed lock + language: system + entry: bash -c 'deno run --unstable -A main.ts x lock-sed' + pass_filenames: false + - 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 daebd426..53f84081 100644 --- a/README.md +++ b/README.md @@ -11,73 +11,187 @@ ghjk /jk/ is a programmable runtime manager. ## Features -- install and manage tools (e.g. rustup, deno, node, etc.) - - [ ] fuzzy match the version - - support dependencies between tools -- [ ] setup runtime helpers (e.g. pre-commit, linting, ignore, etc.) - - [ ] provide a general regex based lockfile - - enforce custom rules -- [ ] create aliases and shortcuts - - `meta` -> `cargo run -p meta` - - `x meta` -> `cargo run -p meta` (avoid conflicts and provide autocompletion) -- [ ] load environment variables and prompt for missing ones -- [ ] define build tasks with dependencies - - `task("build", {depends_on: [rust], if: Deno.build.os === "Macos" })` - - `task.bash("ls")` -- [x] compatible with continuous integration (e.g. github actions, gitlab) +- Soft-reproducable developer environments. +- Install posix programs from different backend like npm, pypi, crates.io. +- Tasks written in typescript. +- Run tasks when entering/exiting envs. ## Getting started ```bash # stable -curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/install.sh | bash # latest (main) -curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/main/install.sh | GHJK_VERSION=main bash +curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/install.sh | GHJK_VERSION=main bash/fish/zsh ``` -In your project, create a configuration file `ghjk.ts`: +In your project, create a configuration file called `ghjk.ts` that look something like: ```ts -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 node from "https://raw.githubusercontent.com/metatypedev/ghjk/main/ports/node.ts"; +// NOTE: All the calls in your `ghjk.ts` file are ultimately modifying the 'sophon' proxy +// object exported here. +// WARN: always import `hack.ts` file first +export { sophon } from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts"; +import { + install, task, +} from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts"; +import node from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/ports/node.ts"; + +// install programs (ports) into your env +install( + node({ version: "14.17.0" }), +); + +// write simple scripts and execute them using +// `$ ghjk x greet` +task("greet", async ($, { argv: [name] }) => { + await $`echo Hello ${name}!`; +}); +``` + +Use the following command to then access your environment: + +```bash +ghjk sync +``` + +### Environments + +Ghjk is primarily configured through constructs called "environments" or "envs" for short. +They serve as recipes for making (mostly) reproducable posix shells. + +```ts +export { sophon } from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts"; +import * as ghjk from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts"; +import * as ports from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/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 independent. + // or pass name(s) of another env to base on top of + inherit: false, + // envs can specify posix env vars + vars: { CARGO_TARGET_DIR: "my_target" }, + installs: [ + ports.cargobi({ crateName: "cargo-insta" }), + ports.act(), + ], +}) + // use env hooks to run code on activation/deactivation + .onEnter(ghjk.task(($) => $`echo dev activated`)) + .onExit(ghjk.task(($) => $`echo dev de-activated`)); + +ghjk.env({ + name: "docker", + desc: "for Dockerfile usage", + // NOTE: env references are order-independent + inherit: "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", + inherit: "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. +Look in the [kitchen sink](./examples/kitchen/ghjk.ts) for what's currently implemented. -ghjk.install(node({ version: "14.17.0" })); +### Tasks + +TBD: this feature is still in development. +Look in the [tasks example](./examples/tasks/ghjk.ts) for what's currently implemented. + +#### Anonymous tasks + +Tasks that aren't give names cannot be invoked from the CLI. +They can be useful for tasks that are meant to be common dependencies of other tasks. + +### `hack.ts` + +The imports from the `hack.ts` module, while nice and striaght forward to use, hold and modify global state. +Any malicious third-party module your ghjkfile imports will thus be able to access them as well, provided they import the same version of the module. + +```ts +// evil.ts +import { env, task } from "https://.../ghjk/hack.ts"; + +env("main") + // lol + .onEnter(task($ => $`rm -rf --no-preserve-root`); ``` -## How it works - -The only required dependency is `deno`. Everything else is managed automatically -and looks as follows (abstracting away some implementation details): - -- the installer sets up a directory hook in your shell profile - - `.bashrc` - - `.zshrc` - - `.config/fish/config.fish` -- for every visited directory, the hook looks for `$PWD/ghjk.ts` in the - directory or its parents, and - - adds the `$HOME/.local/share/ghjk/envs/$PWD/shims/{bin,lib,include}` to your - paths - - sources environment variables in - `$HOME/.local/share/ghjk/envs/$PWD/loader.{sh,fish}` and clear previously - loaded ones (if any) -- you can then - - sync your runtime with `ghjk ports sync` which - - installs the missing tools at `$HOME/.local/share/ghjk/ports/installs` - - regenerates the shims with symlinks and environment variables - - detects any violation of the enforced rules - - [ ] `ghjk ports list`: list installed tools and versions - - [ ] `ghjk ports outdated`: list outdated tools - - [ ] `ghjk ports cleanup`: remove unused tools and versions - -## Extending `ghjk` +To prevent this scenario, the exports from `hack.ts` inspect the call stack and panic if they detect more than one module using them. +This means if you want to spread your ghjkfile across multiple modules, you'll need to use functions described below. + +> [!CAUTION] +> The panic protections of `hack.ts` described above only work if the module is the first import in your ghjkfile. +> If a malicious script gets imported first, it might be able to modify global primordials and get around them. +> We have more ideas to explore on hardening Ghjk security. +> This _hack_ is only a temporary compromise while Ghjk is in alpha state. + +The `hack.ts` file is only optional though and a more verbose but safe way exists through... ```ts +import { file } from "https://.../ghjk/mod.ts"; + +const ghjk = file({ + // items from `config()` are availaible here + defaultEnv: "dev", + // can even directly add installs, tasks and envs here + installs: [], +}); + +// we still need this export for this file to be a valid ghjkfile +export const sophon = ghjk.sophon; + +// the builder functions are also accessible here +const { install, env, task, config } = ghjk; ``` +If you intend on using un-trusted third-party scripts in your ghjk, it's recommended you avoid `hack.ts`. + ## Development ```bash -cat install.sh | GHJK_INSTALLER_URL=$(pwd)/install.ts bash +$ cat install.sh | GHJK_INSTALLER_URL=$(pwd)/install.ts bash/fish/zsh ``` diff --git a/check.ts b/check.ts index 06a2a67f..0e1f3662 100755 --- a/check.ts +++ b/check.ts @@ -1,5 +1,4 @@ #!/bin/env -S ghjk deno run --allow-env --allow-run --allow-read --allow-write=. -// # FIXME: find a way to resolve !DENO_EXEC_PATH in shebangs import "./setup_logger.ts"; import { $ } from "./utils/mod.ts"; @@ -7,9 +6,12 @@ import { $ } from "./utils/mod.ts"; const files = (await Array.fromAsync( $.path(import.meta.url).parentOrThrow().expandGlob("**/*.ts", { exclude: [ + ".git", "play.ts", ".ghjk/**", ".deno-dir/**", + "vendor/**", + ".git/**", // was throwing an error without this ], }), )).map((ref) => ref.path.toString()); diff --git a/deno.jsonc b/deno.jsonc index 013cad5b..310d5082 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -2,22 +2,36 @@ "tasks": { "test": "deno test --parallel --unstable-worker-options --unstable-kv -A tests/*", "cache": "deno cache deps/*", - "check": "deno run -A check.ts" + "check": "deno run -A ./check.ts" }, "fmt": { "exclude": [ + "*.md", "**/*.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", + "no-external-import", + "no-inferrable-types", + "no-self-compare", + "no-throw-literal" + // "verbatim-module-syntax" + // "no-await-in-loop" + // "ban-untagged-todo" + ], "exclude": [ "no-explicit-any" ] diff --git a/deno.lock b/deno.lock index 8e56fd10..5ab46e01 100644 --- a/deno.lock +++ b/deno.lock @@ -2,12 +2,285 @@ "version": "3", "packages": { "specifiers": { - "npm:@types/node": "npm:@types/node@18.16.19" + "jsr:@david/dax@0.40.1": "jsr:@david/dax@0.40.1", + "jsr:@david/dax@0.41.0": "jsr:@david/dax@0.41.0", + "jsr:@david/which@0.3": "jsr:@david/which@0.3.0", + "jsr:@david/which@^0.4.1": "jsr:@david/which@0.4.1", + "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:@noble/hashes@1.4.0": "npm:@noble/hashes@1.4.0", + "npm:@types/node": "npm:@types/node@18.16.19", + "npm:lodash": "npm:lodash@4.17.21", + "npm:mathjs@11.11.1": "npm:mathjs@11.11.1", + "npm:multiformats@13.1.0": "npm:multiformats@13.1.0", + "npm:pg": "npm:pg@8.12.0", + "npm:validator": "npm:validator@13.12.0", + "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", + "npm:zod-validation-error@3.3.0": "npm:zod-validation-error@3.3.0_zod@3.23.3", + "npm:zod@3.23.8": "npm:zod@3.23.8" + }, + "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/dax@0.41.0": { + "integrity": "9e1ecf66a0415962cc8ad3ba4e3fa93ce0f1a1cc797dd95c36fdfb6977dc7fc8", + "dependencies": [ + "jsr:@david/which@^0.4.1", + "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" + }, + "@david/which@0.4.1": { + "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" + }, + "@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": { + "@babel/runtime@7.24.7": { + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "regenerator-runtime@0.14.1" + } + }, + "@noble/hashes@1.4.0": { + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dependencies": {} + }, "@types/node@18.16.19": { "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==", "dependencies": {} + }, + "complex.js@2.1.1": { + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "dependencies": {} + }, + "decimal.js@10.4.3": { + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dependencies": {} + }, + "escape-latex@1.2.0": { + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "dependencies": {} + }, + "fraction.js@4.3.4": { + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "dependencies": {} + }, + "javascript-natural-sort@0.7.1": { + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dependencies": {} + }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dependencies": {} + }, + "mathjs@11.11.1": { + "integrity": "sha512-uWrwMrhU31TCqHKmm1yFz0C352njGUVr/I1UnpMOxI/VBTTbCktx/mREUXx5Vyg11xrFdg/F3wnMM7Ql/csVsQ==", + "dependencies": { + "@babel/runtime": "@babel/runtime@7.24.7", + "complex.js": "complex.js@2.1.1", + "decimal.js": "decimal.js@10.4.3", + "escape-latex": "escape-latex@1.2.0", + "fraction.js": "fraction.js@4.3.4", + "javascript-natural-sort": "javascript-natural-sort@0.7.1", + "seedrandom": "seedrandom@3.0.5", + "tiny-emitter": "tiny-emitter@2.1.0", + "typed-function": "typed-function@4.2.1" + } + }, + "multiformats@13.1.0": { + "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==", + "dependencies": {} + }, + "pg-cloudflare@1.1.1": { + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dependencies": {} + }, + "pg-connection-string@2.6.4": { + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", + "dependencies": {} + }, + "pg-int8@1.0.1": { + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dependencies": {} + }, + "pg-pool@3.6.2_pg@8.12.0": { + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "dependencies": { + "pg": "pg@8.12.0" + } + }, + "pg-protocol@1.6.1": { + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==", + "dependencies": {} + }, + "pg-types@2.2.0": { + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "pg-int8@1.0.1", + "postgres-array": "postgres-array@2.0.0", + "postgres-bytea": "postgres-bytea@1.0.0", + "postgres-date": "postgres-date@1.0.7", + "postgres-interval": "postgres-interval@1.2.0" + } + }, + "pg@8.12.0": { + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-cloudflare": "pg-cloudflare@1.1.1", + "pg-connection-string": "pg-connection-string@2.6.4", + "pg-pool": "pg-pool@3.6.2_pg@8.12.0", + "pg-protocol": "pg-protocol@1.6.1", + "pg-types": "pg-types@2.2.0", + "pgpass": "pgpass@1.0.5" + } + }, + "pgpass@1.0.5": { + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "split2@4.2.0" + } + }, + "postgres-array@2.0.0": { + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dependencies": {} + }, + "postgres-bytea@1.0.0": { + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dependencies": {} + }, + "postgres-date@1.0.7": { + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dependencies": {} + }, + "postgres-interval@1.2.0": { + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "xtend@4.0.2" + } + }, + "regenerator-runtime@0.14.1": { + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dependencies": {} + }, + "seedrandom@3.0.5": { + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dependencies": {} + }, + "split2@4.2.0": { + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dependencies": {} + }, + "tiny-emitter@2.1.0": { + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dependencies": {} + }, + "typed-function@4.2.1": { + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "dependencies": {} + }, + "validator@13.12.0": { + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dependencies": {} + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "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-validation-error@3.3.0_zod@3.23.3": { + "integrity": "sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==", + "dependencies": { + "zod": "zod@3.23.3" + } + }, + "zod@3.23.3": { + "integrity": "sha512-tPvq1B/2Yu/dh2uAIH2/BhUlUeLIUvAjr6dpL/75I0pCYefHgjhXk1o1Kob3kTU8C7yU1j396jFHlsVWFi9ogg==", + "dependencies": {} + }, + "zod@3.23.8": { + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dependencies": {} } } }, @@ -30,12 +303,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", @@ -292,108 +559,85 @@ "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.219.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.219.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.219.0/cli/mod.ts": "58f75df8ce43fb8266bdd26ec4465f73176b910316d72eb8e090b6a0549391da", + "https://deno.land/std@0.219.0/cli/parse_args.ts": "475b3edc8105c9acea09b83b100afc383d7bddbba9828da3f0c4adced006607a", + "https://deno.land/std@0.219.0/cli/prompt_secret.ts": "831cfb4efa83bfaf9bfd320ddbfd619e03cd87e81260909f93ca199ebe214ec2", + "https://deno.land/std@0.219.0/cli/spinner.ts": "005395c4e00b1086bfa2ae44e8c9413c1231c4741a08a55aa0d3c9ea267cecb5", + "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", @@ -409,8 +653,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", @@ -424,6 +666,32 @@ "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://deno.land/x/zod@v3.23.8/ZodError.ts": "528da200fbe995157b9ae91498b103c4ef482217a5c086249507ac850bd78f52", + "https://deno.land/x/zod@v3.23.8/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.23.8/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.23.8/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.23.8/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.23.8/helpers/parseUtil.ts": "c14814d167cc286972b6e094df88d7d982572a08424b7cd50f862036b6fcaa77", + "https://deno.land/x/zod@v3.23.8/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.23.8/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.23.8/helpers/util.ts": "30c273131661ca5dc973f2cfb196fa23caf3a43e224cdde7a683b72e101a31fc", + "https://deno.land/x/zod@v3.23.8/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.23.8/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.23.8/mod.ts": "ec6e2b1255c1a350b80188f97bd0a6bac45801bb46fc48f50b9763aa66046039", + "https://deno.land/x/zod@v3.23.8/types.ts": "1b172c90782b1eaa837100ebb6abd726d79d6c1ec336350c8e851e0fd706bf5c", "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..9829c228 100644 --- a/deps/cli.ts +++ b/deps/cli.ts @@ -2,4 +2,5 @@ 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"; +export { Table } from "https://deno.land/x/cliffy@v1.0.0-rc.4/table/table.ts"; diff --git a/deps/common.ts b/deps/common.ts index ff55ede8..d81d085f 100644 --- a/deps/common.ts +++ b/deps/common.ts @@ -2,7 +2,7 @@ //! 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 "npm:zod@3.23.8"; 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 +10,20 @@ 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 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 * as zod_val_err from "npm:zod-validation-error@3.3.0"; + +// avoid using the following directly and go through the +// wrappers in ./utils/mod.ts +export * as dax from "jsr:@david/dax@0.41.0"; +// class re-exports are tricky. +export { Path as _DaxPath } from "jsr:@david/dax@0.41.0"; +// export * as dax from "jsr:@ghjk/dax@0.40.2-alpha-ghjk"; + +export { canonicalize as json_canonicalize } from "https://deno.land/x/json_hash@0.2.0/canon.ts"; export { default as deep_eql } from "https://deno.land/x/deep_eql@v5.0.1/index.js"; +// export * as multibase16 from "npm:multiformats@13.1.0/bases/base16"; +export * as multibase32 from "npm:multiformats@13.1.0/bases/base32"; +export * as multibase64 from "npm:multiformats@13.1.0/bases/base64"; +export * as multisha2 from "npm:multiformats@13.1.0/hashes/sha2"; +export * as multihasher from "npm:multiformats@13.1.0/hashes/hasher"; +export { sha256 as syncSha256 } from "npm:@noble/hashes@1.4.0/sha256"; diff --git a/docs/architecture.md b/docs/architecture.md index 8b0fecce..90d607f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,17 +11,21 @@ > [here](https://www.tldraw.com/s/v2_c_MewHuw1lKwZzwv3XG8-Y6?viewport=-3756%2C-1126%2C10279%2C6280&page=page%3Apage). > Be sure to update the [backup](./architecture.tldr) if you update that. -Ghjk is made up of a set of modules that each implement and encapsulate a set of related features. -The program is primarily consumed through the provided CLI. -It takes as an argument a path to a ghjkfile (through `$GHJKFILE`) and if no such argument is provided, it'll look for a file named `ghjk.ts` in the current or any of the parent directories and treat it as the config file. -It then loads the config file in a `WebWorker` to obtain a config object which is expected to contain configuration for any of the modules it's interested in. -The modules then process their configuration and, based on it, outline the cli commands and flags to expose through the CLI. -The modules are also allowed to export entries to the lockfile which is treated as a _memo_ of the processed config file. -As of January 1, 2024 the following modules are implemented/planned: +Ghjk is made up of a set of modules that each implement and encapsulate a set of +related features. The program is primarily consumed through the provided CLI. It +takes as an argument a path to a ghjkfile (through `$GHJKFILE`) and if no such +argument is provided, it'll look for a file named `ghjk.ts` in the current or +any of the parent directories and treat it as the config file. It then loads the +config file in a `WebWorker` to obtain a config object which is expected to +contain configuration for any of the modules it's interested in. The modules +then process their configuration and, based on it, outline the cli commands and +flags to expose through the CLI. The modules are also allowed to export entries +to the lockfile which is treated as a _memo_ of the processed config file. As of +January 1, 2024 the following modules are implemented/planned: - Ports: download and install executables and libraries -- Envs (TBD): make CLI shell environments that have access to specific programs - and variables +- Envs: make CLI shell environments that have access to specific programs and + variables - Tasks: run commands in custom shell environments ## Run down @@ -34,9 +38,14 @@ Ghjk is composed of two distinct spheres: - The host - loads and processes config files -Ghjkfiles are the primary entry point for interacting with `ghjk` and provide the vector of programmability for end users. As of today, only `ghjk.ts` config files are supported but the `ghjk` is designed to support alternatives. -You'll observe that this kind of modularity and extendability is a core motif of the design, providing constraints, guidance and tension that's informed a lot of the current design. -A lot of decisions and abstractions will thus appear YAGNI (you ain't going to need it) at this early stage but programmability is the name of the game in ghjk is programmability so we prefer to err on the side of modularity. +Ghjkfiles are the primary entry point for interacting with `ghjk` and provide +the vector of programmability for end users. As of today, only `ghjk.ts` config +files are supported but the `ghjk` is designed to support alternatives. You'll +observe that this kind of modularity and extendability is a core motif of the +design, providing constraints, guidance and tension that's informed a lot of the +current design. A lot of decisions and abstractions will thus appear YAGNI (you +ain't going to need it) at this early stage but programmability is the name of +the game in ghjk so we prefer to err on the side of modularity. ### Ghjkfiles @@ -52,17 +61,20 @@ A lot of decisions and abstractions will thus appear YAGNI (you ain't going to n - If `ghjk.ts` exposes an item named `secureConfig`, it's passed as the first argument to `getConfig`. - `ghjk/mod.ts` exposes a bunch of helpers for authoring conventional `ghjk.ts` - but as far as the host is concerned, it's only aware of the `getConfig` - interface. + but as far as the host is concerned, it's only aware of the + `getConfig(ghjkfileUrl, secureConfig?): SerializedConfig` interface. ### Ghjkdir - Contains files specific to a certain ghjkfile - Expected and placed in `.ghjk/` within the same dir of the ghjkfile. - Contents: - - `lock.json`: lockfile generated from the ghjkfile. Intended to be version control. - - `deno.lock`: lockfile for any modules used by ghjk when working with that specific lockfile. Intended to be version controlled. - - `hash.json`: serves as a store for hashes used to determine weather re-serialization is necessary. Don't put in version control. + - `lock.json`: lockfile generated from the ghjkfile. Intended to be version + control. + - `deno.lock`: lockfile for any modules used by ghjk when working with that + specific lockfile. Intended to be version controlled. + - `hash.json`: serves as a store for hashes used to determine weather + re-serialization is necessary. Don't put in version control. - `envs`: the shims and loaders of the different environments ### Host @@ -76,14 +88,19 @@ The host is the section of the program expected to: ### Modules -Ghjk is made up of a set of interacting modules implementing specific functionality. -Listed below are the modules that we think will make ghjk a complete runtime manager but note that we don't currently plan on implementing all of them. -Getting each module to become competitive with equivalent tools let alone achieving feature parity is beyond the resources available to the authors today and their design is only considered here to provide a holistic framework for development of ghjk. -It is, after all, a _programmable runtime manager_ and we intend to make the core of ghjk (i.e. the host) modular enough that: +Ghjk is made up of a set of interacting modules implementing specific +functionality. Listed below are the modules that we think will make ghjk a +complete runtime manager but note that we don't currently plan on implementing +all of them. Getting each module to become competitive with equivalent tools let +alone achieving feature parity is beyond the resources available to the authors +today and their design is only considered here to provide a holistic perspective +for the design of ghjk. It is, after all, a _programmable runtime manager_ and +we intend to make the core of ghjk (i.e. the host) modular enough that: - Future implementations shouldn't require large refactors - Easy integration of external tools as modules -- Easy to swap implementation of modules without requiring lot of changes in other dependent modules +- Easy to swap implementation of modules without requiring lot of changes in + other dependent modules #### Ports module @@ -112,14 +129,13 @@ Equivalent tools: program that will handle it's installation. - A `PortManifest` can optionally specify a list of other ports, under `buildDeps`, that the `Port` requires during build time. - - A separate list of dependencies, `resolutionDeps`, is used for - routines used for version resolution like `listAll` and - `latestStable`. + - A separate list of dependencies, `resolutionDeps`, is used for version + resolution like `listAll` and `latestStable`. - Any dependencies used by ports must be declared in the top level `allowedPortDeps` list. - I.e. non standard dependencies will have to be manually declared there by users. - - `InstallConfig` can optionally contain a `version`. + - `InstallConfig` can optionally contain a `version` field. - If found, the `version` is sanity checked against the list of versions returned by `listAll`. - [ ] Fuzzy matching can optionally take place. @@ -135,7 +151,8 @@ Equivalent tools: - A Port is described through the `PortManifest` object. - The implementation and execution of ports depends on the `ty` of the port but - they're all roughly expose the following stages modeled after `asdf` plugins: + they're all, roughly, expected to expose the following stages modeled after + `asdf` plugins: - `listAll`: return a list of all the versions that the port - `latestStable`: the version to install when no version is specified by the user. @@ -174,8 +191,9 @@ Equivalent tools: #### Envs module -Reproducible CLI shell environments that can access specific tools and variables. -Including support to auto-load an environment when a specific shell `cd`'s to the ghjk root. +Reproducible CLI shell environments that can access specific tools and +variables. Including support to auto-load an environment when a specific shell +`cd`'s to the ghjk root. Prior art: @@ -206,7 +224,7 @@ Aspirations: #### Containers module -Create OCI compatible containers from based on the results of the Envs and Build +Create OCI compatible containers from on the outputs of the Envs and Build module. Not planned. Looking at: diff --git a/docs/available-commands.md b/docs/available-commands.md new file mode 100644 index 00000000..79353a4d --- /dev/null +++ b/docs/available-commands.md @@ -0,0 +1,15 @@ + + +| Command | Description | Subcommands/Flags | +|----------------|-------------|-------------------| +| ```ghjk sync``` | Synchronize your shell to what's in your config. | | +| ```ghjk envs ls``` | List environments defined in the ghjkfile. | | +| ```ghjk envs activate ``` | Activate an environment. | | +| ```ghjk ports resolve``` | Resolve all installs declared in config. | | +| ```ghjk ports outdated``` | Show a version table for installs. | `--update-all`: update all installs which their versions is not specified in the config.
`--update-only `: update a selected install | +| ```ghjk print``` | Emit different discovered and built values to stdout. | | +| ```ghjk deno``` | Access the deno cli. | | +| ```ghjk completions``` | Generate shell completions. | | + +You can use the following flag to get help around the CLI. +`ghjk --help` or `ghjk -h` diff --git a/examples/envs/ghjk.ts b/examples/envs/ghjk.ts new file mode 100644 index 00000000..3a2e7c5d --- /dev/null +++ b/examples/envs/ghjk.ts @@ -0,0 +1,31 @@ +export { sophon } from "../../hack.ts"; +import { config, env, install, task } from "../../hack.ts"; +import * as ports from "../../ports/mod.ts"; + +config({ + // we can change which environment + // is activated by default for example + // when we enter the directory + defaultEnv: "main", + // set the env all others envs will by + // default inherit from + defaultBaseEnv: "main", +}); + +env("test", { + installs: [ports.unzip()], +}); + +env("ci") + .install(ports.opentofu_ghrel()); + +// top level `install` calls just +// go to an enviroment called "main" +install(ports.protoc()); + +// we can modify "main" directly +env("main") + // hooks execute when environments are + // activated/deactivated in interactive shells + .onEnter(task(($) => $`echo enter`)) + .onExit(task(($) => $`echo exit`)); diff --git a/examples/kitchen/ghjk.ts b/examples/kitchen/ghjk.ts new file mode 100644 index 00000000..a88897e4 --- /dev/null +++ b/examples/kitchen/ghjk.ts @@ -0,0 +1,122 @@ +import { stdDeps } from "../../files/mod.ts"; +import { file } from "../../mod.ts"; +import * as ports from "../../ports/mod.ts"; + +const ghjk = file({ + // configre an empty env so that no ports are avail by default in our workdir + defaultEnv: "empty", + envs: [{ name: "empty", inherit: false }], + // we wan't all other envs to start from empty unless they opt otherwise + defaultBaseEnv: "empty", + + // we won't use the following for now + // but they pretty much configure the "main" env + allowedBuildDeps: [], + installs: [], + stdDeps: true, + enableRuntimes: true, + // tasks aren't attached to envs + tasks: {}, +}); + +// we need this export for this file to be a valid ghjkfile +// it's the one thing used by the ghjk host implementation to +// interact with your ghjkfile +export const sophon = ghjk.sophon; + +const { install, env, task } = ghjk; + +// we can configure main like this as well +env("main") + // provision env vars to be acccessbile in the env + .var("RUST_LOG", "info,actix=warn") + // provision programs to be avail in the env + .install(ports.jq_ghrel()) + .allowedBuildDeps( + // ports can use the following installs at build time + // very WIP mechanism but this is meant to prevent ports from + // pulling whatever dependency they want at build time unless + // explicitly allowed to do so + ports.node({ version: "1.2.3" }), + ports.rust({ version: "stable" }), + // add the std deps including the runtime ports. + // These includes node and python and this will override + // the node from above since it comes after ordinally + ...stdDeps({ enableRuntimes: true }), + ); + +// these top level installs go to the main env as well +install( + // ports can declare their own config params + ports.rust({ + version: "1.78.0", + profile: "minimal", + components: ["rustfmt"], + }), + // some ports use other programs as backends + ports.pipi({ packageName: "pre-commit" })[0], + ports.cargobi({ crateName: "mise" }), +); + +const ci = env("ci", { + // this inherits from main so it gets jq + inherit: "main", + // extra installs + installs: [ports.protoc(), ports.curl()], + // it has extra allowed deps + allowedBuildDeps: [ports.pnpm()], + // more env vars + vars: { + CI: 1, + }, + desc: "do ci stuff", +}); + +// tasks are invocable from the cli +task("install-app", ($) => $`cargo fetch`); + +task("build-app", { + dependsOn: "install-app", + // the task's env inherits from ci + inherit: ci.name, + // it can add more items to that env + installs: [], + // vars + vars: { + RUST_BACKTRACE: 1, + }, + // allowed build deps + allowedBuildDeps: [ports.zstd()], + desc: "build the app", + fn: async ($) => { + await $`cargo build -p app`; + // we can access zstd here from the ci env + await $`zstd ./target/debug/app -o app.tar.gz`; + }, +}); + +env("python") + // all envs will inherit from `defaultBaseEnv` + // unless set to false which ensures true isolation + .inherit(false) + .install( + ports.cpy_bs({ version: "3.8.18", releaseTag: "20240224" }), + ) + .allowedBuildDeps( + ports.cpy_bs({ version: "3.8.18", releaseTag: "20240224" }), + ports.tar(), + ports.zstd(), + ); + +env("dev") + // we can inherit from many envs + // if conflict on variables or build deps, the one declared + // later overrides + .inherit(["main", "python"]) + // we can set tasks to run on activation/decativation + // which are inheritable + .onEnter(task(($) => $`echo enter`)) + .onEnter(task({ + workingDir: "..", + fn: ($) => $`ls`, + })); diff --git a/examples/many_installs/ghjk.ts b/examples/many_installs/ghjk.ts new file mode 100644 index 00000000..b5a3a154 --- /dev/null +++ b/examples/many_installs/ghjk.ts @@ -0,0 +1,62 @@ +export { sophon } from "../../hack.ts"; +import { config, install } from "../../hack.ts"; +import * as ports from "../../ports/mod.ts"; + +const installs = { + python: ports.cpy_bs({ version: "3.8.18", releaseTag: "20240224" }), + python_latest: ports.cpy_bs({ releaseTag: "20240224" }), + node: ports.node({ version: "20.8.0" }), +}; + +config({ + stdDeps: true, + allowedBuildDeps: [ + installs.python_latest, + installs.node, + ], + enableRuntimes: true, +}); + +install( + //others + ports.act(), + ports.protoc({ version: "v24.1" }), + // cargo crate installs + ports.cargobi({ + crateName: "cargo-insta", + version: "1.33.0", + locked: true, + }), + ports.cargo_binstall({ + crateName: "regex-lite", + }), +); + +install( + // python package installs + installs.python_latest, + ports.pipi({ + packageName: "poetry", + version: "1.7.0", + })[0], + ports.pipi({ + packageName: "requests", + version: "2.18.0", + })[0], + ports.pipi({ + packageName: "pre-commit", + })[0], +); + +install( + // npm packages + installs.node, + ports.pnpm({ version: "v9.0.5" }), + ports.npmi({ + packageName: "yarn", + version: "1.9.1", + })[0], + ports.npmi({ + packageName: "readme", + })[0], +); diff --git a/examples/protoc/ghjk.ts b/examples/protoc/ghjk.ts deleted file mode 100644 index a194ecf7..00000000 --- a/examples/protoc/ghjk.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { ghjk } from "../../mod.ts"; -import { install } from "../../mod.ts"; -import protoc from "../../ports/protoc.ts"; - -install( - protoc(), -); diff --git a/examples/tasks/ghjk.ts b/examples/tasks/ghjk.ts new file mode 100644 index 00000000..61b227f4 --- /dev/null +++ b/examples/tasks/ghjk.ts @@ -0,0 +1,43 @@ +export { sophon } from "../../hack.ts"; +import { logger, task } from "../../hack.ts"; +import * as ports from "../../ports/mod.ts"; + +task("greet", async ($, { argv: [name] }) => { + await $`echo Hello ${name}!`; +}); + +const ha = task({ + name: "ha", + installs: [ports.jq_ghrel()], + vars: { STUFF: "stuffier" }, + async fn($) { + await $`echo $STUFF; + jq --version; + `; + }, +}); + +task("ho", { + dependsOn: [ha], + fn: () => logger().info(`ho`), +}); + +task("hii", { + // task `dependsOn` declaration is order-independent + dependsOn: ["hum"], + fn: () => logger().info(`haii`), +}); + +task("hum", { + dependsOn: ["ho"], + fn: () => logger().info(`hum`), +}); + +// not all tasks need to be named +// but anon tasks can't be accessed from the CLI +const anon = task(() => logger().info("anon")); + +task("hey", { + dependsOn: ["hii", "ho", anon], + fn: () => logger().info(`hey`), +}); diff --git a/host/deno.ts b/files/deno/mod.ts similarity index 97% rename from host/deno.ts rename to files/deno/mod.ts index 5d055528..4b566ceb 100644 --- a/host/deno.ts +++ b/files/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/files/deno/worker.ts b/files/deno/worker.ts new file mode 100644 index 00000000..5a9528f7 --- /dev/null +++ b/files/deno/worker.ts @@ -0,0 +1,49 @@ +//! this loads the ghjk.ts module and provides a program for it + +//// +/// + +// all imports in here should be dynamic imports as we want to +// modify the Deno namespace before anyone touches it + +// NOTE: only import types +import { shimDenoNamespace } from "../../utils/worker.ts"; +import type { DriverRequests, DriverResponse } from "./mod.ts"; + +self.onmessage = onMsg; + +async function onMsg(msg: MessageEvent) { + const req = msg.data; + if (!req.ty) { + throw new Error(`unrecognized event data`, { + cause: req, + }); + } + let res: DriverResponse; + if (req.ty == "serialize") { + res = { + ty: req.ty, + payload: await serializeConfig(req.uri, req.envVars), + }; + } else { + throw new Error(`unrecognized request type: ${req.ty}`, { + cause: req, + }); + } + self.postMessage(res); +} + +async function serializeConfig(uri: string, envVars: Record) { + const shimHandle = shimDenoNamespace(envVars); + const { setup: setupLogger } = await import("../../utils/logger.ts"); + setupLogger(); + const mod = await import(uri); + const rawConfig = await mod.sophon.getConfig(uri, mod.secureConfig); + const config = JSON.parse(JSON.stringify(rawConfig)); + return { + config, + accessedEnvKeys: shimHandle.getAccessedEnvKeys(), + readFiles: shimHandle.getReadFiles(), + listedFiles: shimHandle.getListedFiles(), + }; +} diff --git a/files/mod.ts b/files/mod.ts new file mode 100644 index 00000000..9fd5acf9 --- /dev/null +++ b/files/mod.ts @@ -0,0 +1,1102 @@ +//! This provides the backing implementation of the Ghjkfile frontends. + +// NOTE: avoid adding sources of randomness +// here to make the resulting config reasonably stable +// across repeated serializaitons. No random identifiers. + +import { deep_eql, multibase32, multibase64, zod } from "../deps/common.ts"; + +// ports specific imports +import portsValidators from "../modules/ports/types.ts"; +import type { + AllowedPortDep, + InstallConfigFat, + InstallSetRefProvision, + PortsModuleConfigHashed, +} from "../modules/ports/types.ts"; +import getLogger from "../utils/logger.ts"; +const logger = getLogger(import.meta); +import { + $, + defaultCommandBuilder, + objectHash, + Path, + thinInstallConfig, + unwrapZodRes, +} 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 +// WARN: this module has side-effects and only ever import +// types from it +import type { ExecTaskArgs } from "../modules/tasks/deno.ts"; +import { TaskDefHashed, TasksModuleConfig } from "../modules/tasks/types.ts"; +// envs +import type { + EnvRecipe, + EnvsModuleConfig, + Provision, + WellKnownProvision, +} from "../modules/envs/types.ts"; +import modulesValidators from "../modules/types.ts"; + +const validators = { + envVars: zod.record( + modulesValidators.envVarName, + zod.union([zod.string(), zod.number()]), + ), +}; + +export type EnvParent = string | string[] | boolean | undefined; + +export type EnvDefArgs = { + name: string; + installs?: InstallConfigFat | InstallConfigFat[]; + allowedBuildDeps?: (InstallConfigFat | AllowedPortDep)[]; + /** + * If true or not set, will base the task's env on top + * of the default env (usually `main`). + * Will be a standalone env if false. + * If given a string, will use the identified env as a base + * for the task env. + * If given a set of strings, will inherit from each. + * If conflict is detected during multiple inheritance, the + * item from the env specified at a higher index will override. + */ + inherit?: EnvParent; + desc?: string; + vars?: Record; + /** + * Task to execute when environment is activated. + */ + onEnter?: string | string[]; + /** + * Task to execute when environment is deactivated. + */ + onExit?: string | string[]; +}; + +export type TaskFnArgs = { + $: ReturnType; + argv: string[]; + env: Record; + workingDir: string; +}; + +export type TaskFn = ( + $: ReturnType, + args: TaskFnArgs, +) => Promise | any; + +/** + * Configure a task under the given name or key. + */ +export type TaskDefArgs = { + name?: string; + desc?: string; + dependsOn?: string | string[]; + workingDir?: string | Path; + vars?: Record; + allowedBuildDeps?: (InstallConfigFat | AllowedPortDep)[]; + installs?: InstallConfigFat | InstallConfigFat[]; + inherit?: EnvParent; +}; + +export type DenoTaskDefArgs = TaskDefArgs & { + /** + * The logic to run when the task is invoked. + * + * Note: functions are optional for tasks. If none is set, + * it'll be a no-op. The task it depends on will still be run. + */ + fn?: TaskFn; + /** + * In order to key the right task when ghjk is requesting + * execution of a specific task, we identify each using a hash. + * The {@field fn} is `toString`ed in the hash input. + * If a ghjkfile is produing identical anonymous tasks for + * instance, it can provide a none to disambiguate beteween each + * through hash differences. + * + * NOTE: the nonce must be stable across serialization. + * NOTE: closing over values is generally ill-advised on tasks + * fns. If you want to close over values, make sure they're stable + * across re-serializations. + */ + nonce?: string; +}; + +type TaskDefTyped = DenoTaskDefArgs & { ty: "denoFile@v1" }; + +export class Ghjkfile { + #installSets = new Map< + string, + { installs: Set; allowedBuildDeps: Record } + >(); + #seenInstallConfs = new Map(); + #seenAllowedDepPorts = new Map(); + #tasks = new Map(); + #bb = new Map(); + #seenEnvs: Record = {}; + #finalizedEnvs: Record< + string, + { + finalized: ReturnType; + installSetId?: string; + vars: Record; + envHash: string; + } + > = {}; + + /* dump() { + return { + installSets: Object.fromEntries(this.#installSets), + bb: Object.fromEntries(this.#bb), + seenEnvs: Object.fromEntries( + Object.entries(this.#seenEnvs).map(( + [key, [_builder, finalizer]], + ) => [key, finalizer()]), + ), + tasks: Object.fromEntries( + Object.entries(this.#tasks).map(([key, task]) => [key, { + ...task, + ...(task.ty === "denoFile@v1" + ? { + fn: task.fn.toString(), + } + : {}), + }]), + ), + }; + } */ + + addInstall(setId: string, configUnclean: InstallConfigFat) { + const config = unwrapZodRes( + portsValidators.installConfigFat.safeParse(configUnclean), + { + config: configUnclean, + }, + `error parsing InstallConfig`, + ); + + const hash = objectHashSafe(config); + this.#seenInstallConfs.set(hash, config); + const set = this.#getSet(setId); + set.installs.add(hash); + } + + setAllowedPortDeps( + setId: string, + deps: (InstallConfigFat | AllowedPortDep)[], + ) { + const set = this.#getSet(setId); + set.allowedBuildDeps = Object.fromEntries( + reduceAllowedDeps(deps).map(( + dep, + ) => { + const hash = objectHashSafe(dep); + this.#seenAllowedDepPorts.set(hash, dep); + return [dep.manifest.name, hash]; + }), + ); + } + + addTask(args: TaskDefTyped) { + // FIXME: combine the task env processing + // with normal env processing + // we currrently process task envs at once in the end + // to do env deduplication + if (args.vars) { + args.vars = unwrapZodRes(validators.envVars.safeParse(args.vars), { + vars: args.vars, + }); + } + let key = args.name; + if (!key) { + switch (args.ty) { + case "denoFile@v1": { + const { fn, workingDir, ...argsRest } = args; + key = objectHashSafe({ + ...argsRest, + workingDir: workingDir instanceof Path + ? workingDir.toString() + : workingDir, + ...(fn + ? { + // NOTE: we serialize the function to a string before + // hashing. + fn: fn.toString(), + } + : {}), + }); + key = multibase64.base64urlpad.encode( + multibase32.base32.decode(key), + ); + break; + } + default: + throw new Error(`unexpected task type: ${args.ty}`); + } + } + this.#tasks.set(key, { + ...args, + }); + return key; + } + + addEnv(key: string, args: EnvDefArgsPartial) { + let env = this.#seenEnvs[key]?.[0]; + if (!env) { + let finalizer: EnvFinalizer; + env = new EnvBuilder( + this, + (fin) => { + finalizer = fin; + }, + key, + args.name, + ); + this.#seenEnvs[key] = [env, finalizer!]; + } + if ("inherit" in args) { + env.inherit(args.inherit!); + } + if (args.installs) { + env.install( + ...(Array.isArray(args.installs) ? args.installs : [args.installs]), + ); + } + if (args.allowedBuildDeps) { + env.allowedBuildDeps(...args.allowedBuildDeps); + } + if (args.desc) { + env.desc(args.desc); + } + if (args.vars) { + env.vars(args.vars); + } + if (args.onEnter) { + env.onEnter(...args.onEnter); + } + if (args.onExit) { + env.onEnter(...args.onExit); + } + return env; + } + + async execTask( + { key, workingDir, envVars, argv }: ExecTaskArgs, + ) { + const task = this.#tasks.get(key); + if (!task) { + throw new Error(`no task defined under "${key}"`); + } + if (task.ty != "denoFile@v1") { + throw new Error(`task under "${key}" has unexpected type ${task.ty}`); + } + if (task.fn) { + const custom$ = task$( + argv, + envVars, + workingDir, + ``, + ); + await task.fn(custom$, { + argv, + env: Object.freeze(envVars), + $: custom$, + workingDir, + }); + } + } + + toConfig( + { defaultEnv, defaultBaseEnv }: { + defaultEnv: string; + defaultBaseEnv: string; + ghjkfileUrl: string; + }, + ) { + // make sure referenced envs exist + this.addEnv(defaultEnv, { name: defaultEnv }); + this.addEnv(defaultBaseEnv, { name: defaultBaseEnv }); + + // crearte the envs used by the tasks + const taskToEnvMap = {} as Record; + for ( + const [key, { inherit, vars, installs, allowedBuildDeps }] of this.#tasks + .entries() + ) { + const envKey = `____task_env_${key}`; + this.addEnv(envKey, { + inherit, + vars, + installs, + allowedBuildDeps, + }); + taskToEnvMap[key] = envKey; + } + + try { + const envsConfig = this.#processEnvs( + defaultEnv, + defaultBaseEnv, + taskToEnvMap, + ); + const tasksConfig = this.#processTasks( + envsConfig, + taskToEnvMap, + ); + const portsConfig = this.#processInstalls(); + + const config: SerializedConfig = { + blackboard: Object.fromEntries(this.#bb.entries()), + modules: [{ + id: std_modules.ports, + config: portsConfig, + }, { + id: std_modules.tasks, + config: tasksConfig, + }, { + id: std_modules.envs, + config: envsConfig, + }], + }; + 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: new Set(), allowedBuildDeps: {} }; + this.#installSets.set(setId, set); + } + return set; + } + + #addToBlackboard(inp: unknown) { + // jsonHash.digest is async + const hash = objectHashSafe(inp); + + if (!this.#bb.has(hash)) { + this.#bb.set(hash, inp); + } + return hash; + } + + #mergeEnvs(keys: string[], childName: string) { + const mergedVars = {} as Record; + let mergedInstalls = new Set(); + const mergedOnEnterHooks = []; + const mergedOnExitHooks = []; + const mergedAllowedBuildDeps = {} as Record< + string, + [string, string] | undefined + >; + for (const parentName of keys) { + const { vars, installSetId, finalized } = this.#finalizedEnvs[parentName]; + mergedOnEnterHooks.push(...finalized.onEnterHookTasks); + mergedOnExitHooks.push(...finalized.onExitHookTasks); + for (const [key, val] of Object.entries(vars)) { + const conflict = mergedVars[key]; + // if parents share a parent themselves, they will have + // the same item so it's not exactly a conflict + if (conflict && val !== conflict[0]) { + logger.warn( + "environment variable conflict on multiple env inheritance, parent2 was chosen", + { + child: childName, + parent1: conflict[1], + parent2: parentName, + variable: key, + }, + ); + } + mergedVars[key] = [val, parentName]; + } + if (!installSetId) { + continue; + } + const set = this.#installSets.get(installSetId)!; + mergedInstalls = mergedInstalls.union(set.installs); + for ( + const [key, val] of Object.entries(set.allowedBuildDeps) + ) { + const conflict = mergedAllowedBuildDeps[key]; + if (conflict && !deep_eql(val, conflict[0])) { + logger.warn( + "allowedBuildDeps conflict on multiple env inheritance, parent2 was chosen", + { + child: childName, + parent1: conflict[1], + parent2: parentName, + depPort: key, + }, + ); + } + mergedAllowedBuildDeps[key] = [val, parentName]; + } + } + const outInstallSet = { + installs: mergedInstalls, + allowedBuildDeps: Object.fromEntries( + Object.entries(mergedAllowedBuildDeps).map(( + [key, val], + ) => [key, val![0]]), + ), + }; + const outVars = Object.fromEntries( + Object.entries(mergedVars).map(([key, val]) => [key, val![0]]), + ); + return { + installSet: outInstallSet, + onEnterHookTasks: mergedOnEnterHooks, + onExitHookTasks: mergedOnExitHooks, + vars: outVars, + }; + } + + #resolveEnvBases( + parent: EnvParent, + taskToEnvMap: Record, + defaultBaseEnv: string, + childKey: string, + ) { + if (parent === false) { + return []; + } + if (parent === true || parent === undefined || parent === null) { + return childKey != defaultBaseEnv ? [defaultBaseEnv] : []; + } + const inheritSet = typeof parent == "string" + ? [parent] + : parent + ? [...new Set(parent)] // js sets preserve insert order + : []; + + const swapJobs = [] as [number, string][]; + for (let ii = 0; ii < inheritSet.length; ii++) { + const parentKey = inheritSet[ii]; + // parent env exists + // note: env inheritances take prioritiy over + // tasks of the same name + if (this.#seenEnvs[parentKey]) { + //noop + } else if (this.#tasks.has(parentKey)) { + // while the ghjkfile only refers to the task envs + // by the task name, we must use the private task + // env key for inheritance resolution + // the swap job take cares of that + swapJobs.push([ii, taskToEnvMap[parentKey]] as const); + } else { + throw new Error( + `env "${childKey}" inherits from "${parentKey} but no env or task found under key"`, + ); + } + } + for (const [idx, envKey] of swapJobs) { + inheritSet[idx] = envKey; + } + return inheritSet; + } + + /** this processes the defined envs, resolving inherit + * relationships to produce the standard EnvsModuleConfig + */ + #processEnvs( + defaultEnv: string, + defaultBaseEnv: string, + taskToEnvMap: Record, + ) { + const all = {} as Record< + string, + ReturnType & { envBaseResolved: null | string[] } + >; + const indie = [] as string[]; + const deps = new Map(); + const revDeps = new Map(); + for ( + const [_key, [_builder, finalizer]] of Object.entries(this.#seenEnvs) + ) { + const final = finalizer(); + + const envBaseResolved = this.#resolveEnvBases( + final.inherit, + taskToEnvMap, + defaultBaseEnv, + final.key, + ); + all[final.key] = { ...final, envBaseResolved }; + if (envBaseResolved.length > 0) { + deps.set(final.key, [...envBaseResolved]); + for (const base of envBaseResolved) { + const parentRevDeps = revDeps.get(base); + if (parentRevDeps) { + parentRevDeps.push(final.key); + } else { + revDeps.set(base, [final.key]); + } + } + } else { + indie.push(final.key); + } + } + + const moduleConfig: EnvsModuleConfig = { + envs: {}, + defaultEnv, + envsNamed: {}, + }; + const workingSet = indie; + // console.log({ + // indie, + // deps, + // }); + while (workingSet.length > 0) { + const item = workingSet.pop()!; + const final = all[item]; + + const base = this.#mergeEnvs(final.envBaseResolved ?? [], final.key); + // console.log({ parents: final.envBaseResolved, child: final.key, base }); + + const finalVars = { + ...base.vars, + ...final.vars, + }; + + let finalInstallSetId: string | undefined; + { + const installSet = this.#installSets.get(final.installSetId); + if (installSet) { + installSet.installs = installSet.installs + .union(base.installSet.installs); + for ( + const [key, val] of Object.entries(base.installSet.allowedBuildDeps) + ) { + // prefer the port dep config of the child over any + // similar deps in the base + if (!installSet.allowedBuildDeps[key]) { + installSet.allowedBuildDeps[key] = val; + } + } + finalInstallSetId = final.installSetId; + } // if there's no install set found under the id + else { + // implies that the env has not ports explicitly configured + if (final.envBaseResolved) { + // has a singluar parent + if (final.envBaseResolved.length == 1) { + finalInstallSetId = + this.#finalizedEnvs[final.envBaseResolved[0]].installSetId; + } else { + this.#installSets.set(final.installSetId, base.installSet); + finalInstallSetId = final.installSetId; + } + } + } + } + const hooks = [ + ...base.onEnterHookTasks.map( + (key) => [key, "hook.onEnter.ghjkTask"] as const, + ), + ...final.onEnterHookTasks.map( + (key) => [key, "hook.onEnter.ghjkTask"] as const, + ), + ...base.onExitHookTasks.map( + (key) => [key, "hook.onExit.ghjkTask"] as const, + ), + ...final.onExitHookTasks.map( + (key) => [key, "hook.onExit.ghjkTask"] as const, + ), + ].map(([taskKey, ty]) => { + const task = this.#tasks.get(taskKey); + if (!task) { + throw new Error("unable to find task for onEnterHook", { + cause: { + env: final.name, + taskKey, + }, + }); + } + if (task.ty == "denoFile@v1") { + const prov: InlineTaskHookProvision = { + ty, + taskKey, + }; + return prov; + } + throw new Error( + `unsupported task type "${task.ty}" used for environment hook`, + { + cause: { + taskKey, + task, + }, + }, + ); + }); + + // the actual final final recipe + const recipe: EnvRecipe = { + desc: final.desc, + provides: [ + ...Object.entries(finalVars).map(( + [key, val], + ) => { + const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; + return prov; + }), + // env hooks + ...hooks, + ], + }; + + if (finalInstallSetId) { + const prov: InstallSetRefProvision = { + ty: "ghjk.ports.InstallSetRef", + setId: finalInstallSetId, + }; + recipe.provides.push(prov); + } + + // hashing takes care of deduplication + const envHash = objectHashSafe(recipe); + this.#finalizedEnvs[final.key] = { + installSetId: finalInstallSetId, + vars: finalVars, + finalized: final, + envHash, + }; + // hashing takes care of deduplication + moduleConfig.envs[envHash] = recipe; + + if (final.name) { + moduleConfig.envsNamed[final.name] = envHash; + } + + for (const revDepKey of revDeps.get(final.key) ?? []) { + const revDepDeps = deps.get(revDepKey)!; + // swap remove + const idx = revDepDeps.indexOf(final.key); + const last = revDepDeps.pop()!; + if (revDepDeps.length > idx) { + revDepDeps[idx] = last; + } + + if (revDepDeps.length == 0) { + deps.delete(revDepKey); + workingSet.push(revDepKey); + } + } + } + // sanity checks + if (deps.size > 0) { + throw new Error(`working set empty but pending items found`, { + cause: { + deps, + workingSet, + revDeps, + }, + }); + } + return moduleConfig; + } + + #processTasks( + envsConfig: EnvsModuleConfig, + taskToEnvMap: Record, + ) { + const indie = [] as string[]; + const deps = new Map(); + const revDeps = new Map(); + const nameToKey = Object.fromEntries( + Object.entries(this.#tasks) + .filter(([_, { name }]) => !!name) + .map(([hash, { name }]) => [name, hash] as const), + ); + for (const [key, args] of this.#tasks) { + if (args.dependsOn && args.dependsOn.length > 0) { + const depKeys = + (Array.isArray(args.dependsOn) ? args.dependsOn : [args.dependsOn]) + .map((nameOrKey) => nameToKey[nameOrKey] ?? nameOrKey); + deps.set(key, depKeys); + for (const depKey of depKeys) { + const depRevDeps = revDeps.get(depKey); + if (depRevDeps) { + depRevDeps.push(key); + } else { + revDeps.set(depKey, [key]); + } + } + } else { + indie.push(key); + } + } + const workingSet = indie; + const localToFinalKey = {} as Record; + const moduleConfig: TasksModuleConfig = { + tasks: {}, + tasksNamed: [], + }; + while (workingSet.length > 0) { + const key = workingSet.pop()!; + const args = this.#tasks.get(key)!; + const { workingDir, desc, dependsOn } = args; + + const envKey = taskToEnvMap[key]; + const { envHash } = this.#finalizedEnvs[envKey]; + + const def: TaskDefHashed = { + ty: args.ty, + key, + workingDir: typeof workingDir == "object" + ? workingDir.toString() + : workingDir, + desc, + ...dependsOn + ? { + dependsOn: (Array.isArray(dependsOn) ? dependsOn : [dependsOn]) + ?.map((keyOrHash) => + localToFinalKey[nameToKey[keyOrHash] ?? keyOrHash] + ), + } + : {}, + envKey: envHash, + }; + const taskHash = objectHash(def); + // we prefer the name as a key if present + const finalKey = args.name ?? taskHash; + moduleConfig.tasks[finalKey] = def; + localToFinalKey[key] = finalKey; + + if (args.name) { + moduleConfig.tasksNamed.push(args.name); + } + for (const revDepKey of revDeps.get(key) ?? []) { + const revDepDeps = deps.get(revDepKey)!; + // swap remove + const idx = revDepDeps.indexOf(key); + const last = revDepDeps.pop()!; + if (revDepDeps.length > idx) { + revDepDeps[idx] = last; + } + + if (revDepDeps.length == 0) { + deps.delete(revDepKey); + workingSet.push(revDepKey); + } + } + } + + // do some sanity checks + for (const [key, { dependsOn }] of Object.entries(moduleConfig.tasks)) { + for (const depName of dependsOn ?? []) { + if (!moduleConfig.tasks[depName]) { + throw new Error( + `task "${key}" depend on non-existent task "${depName}"`, + { + cause: { + workingSet, + revDeps, + moduleConfig, + tasks: this.#tasks, + nameToKey, + }, + }, + ); + } + } + } + if (deps.size > 0) { + throw new Error("working set empty but pending items found", { + cause: { + workingSet, + revDeps, + moduleConfig, + tasks: this.#tasks, + }, + }); + } + + // reduce task based env hooks + for (const [_name, env] of Object.entries(envsConfig.envs)) { + env.provides = env.provides.map( + (prov) => { + if ( + prov.ty == "hook.onEnter.ghjkTask" || + prov.ty == "hook.onExit.ghjkTask" + ) { + const inlineProv = prov as InlineTaskHookProvision; + const taskKey = localToFinalKey[inlineProv.taskKey]; + const out: WellKnownProvision = { + ty: /onEnter/.test(prov.ty) + ? "hook.onEnter.posixExec" + : "hook.onExit.posixExec", + program: "ghjk", + arguments: ["x", taskKey], + }; + return out; + } + return prov; + }, + ); + } + + return moduleConfig; + } + + #processInstalls() { + const out: PortsModuleConfigHashed = { + sets: {}, + }; + for ( + const [setId, set] of this.#installSets.entries() + ) { + out.sets[setId] = { + installs: [...set.installs.values()] + .map((instHash) => + this.#addToBlackboard(this.#seenInstallConfs.get(instHash)) + ), + allowedBuildDeps: this.#addToBlackboard(Object.fromEntries( + Object.entries(set.allowedBuildDeps).map( + ( + [key, depHash], + ) => [ + key, + this.#addToBlackboard(this.#seenAllowedDepPorts.get(depHash)), + ], + ), + )), + }; + } + return out; + } +} + +type EnvFinalizer = () => { + key: string; + name?: string; + installSetId: string; + inherit: string | string[] | boolean; + vars: Record; + desc?: string; + onEnterHookTasks: string[]; + onExitHookTasks: string[]; +}; + +export type EnvDefArgsPartial = + & { name?: string } + & Omit; +// +// /** +// * A version of {@link EnvDefArgs} that has all container +// * fields guratneed initialized to non null but possible empty values. +// */ +// export type EnvDefArgsReqiured = +// & Required> +// & Partial>; +// +// export function envDef( +// args: EnvDefArgsPartial, +// ): EnvDefArgsReqiured; +// export function envDef( +// name: string, +// args?: Omit, +// ): EnvDefArgsReqiured; +// export function envDef( +// nameOrArgs: string | EnvDefArgsPartial, +// argsMaybe?: Omit, +// ): EnvDefArgsReqiured { +// const args = typeof nameOrArgs == "object" +// ? nameOrArgs +// : { ...argsMaybe, name: nameOrArgs }; +// return { +// ...args, +// installs: [], +// inherit: args.inherit ?? [], +// vars: args.vars ?? {}, +// onExit: args.onExit ?? [], +// onEnter: args.onEnter ?? [], +// allowedBuildDeps: args.allowedBuildDeps ?? [], +// }; +// } + +/** + 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: Ghjkfile; + #inherit: string | string[] | boolean = true; + #vars: Record = {}; + #desc?: string; + #onEnterHookTasks: string[] = []; + #onExitHookTasks: string[] = []; + + constructor( + file: Ghjkfile, + setFinalizer: (fin: EnvFinalizer) => void, + public readonly key: string, + public name?: string, + ) { + this.#file = file; + this.#installSetId = `ghjkEnvProvInstSet___${key}`; + setFinalizer(() => ({ + key: this.key, + name: this.name, + installSetId: this.#installSetId, + inherit: this.#inherit, + vars: Object.fromEntries( + Object.entries(this.#vars).map(([key, val]) => [key, val.toString()]), + ), + desc: this.#desc, + onExitHookTasks: this.#onExitHookTasks, + onEnterHookTasks: this.#onEnterHookTasks, + })); + } + + inherit(inherit: string | string[] | boolean) { + this.#inherit = inherit; + 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; + } + + /** + * Configure the build time deps allowed to be used by ports. + * This is treated as a single set and will replace previously any configured set. + */ + allowedBuildDeps(...deps: (AllowedPortDep | InstallConfigFat)[]) { + 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, + unwrapZodRes(validators.envVars.safeParse(envVars), { envVars }), + ); + return this; + } + + /** + * Description of the environment. + */ + desc(str: string) { + this.#desc = str; + return this; + } + + /** + * Tasks to execute on enter. + */ + onEnter(...taskKey: string[]) { + this.#onEnterHookTasks.push(...taskKey); + return this; + } + + /** + * Tasks to execute on enter. + */ + onExit(...taskKey: string[]) { + this.#onExitHookTasks.push(...taskKey); + return this; + } +} + +export function stdDeps(args = { enableRuntimes: false }) { + const out: AllowedPortDep[] = [ + ...Object.values(std_ports.map), + ]; + if (args.enableRuntimes) { + out.push( + ...reduceAllowedDeps([ + node.default(), + cpy.default(), + ]), + ); + } + return out; +} + +function task$( + argv: string[], + env: Record, + workingDir: string, + loggerName: string, +) { + const custom$ = Object.assign( + // NOTE: order is important on who assigns to who + // here + $.build$({ + commandBuilder: defaultCommandBuilder().env(env).cwd(workingDir), + }), + { + argv, + env: Object.freeze(env), + workingDir: $.path(workingDir), + logger: getLogger(loggerName), + }, + ); + return custom$; +} + +type InlineTaskHookProvision = Provision & { + ty: "hook.onExit.ghjkTask" | "hook.onEnter.ghjkTask"; + taskKey: string; +}; + +export function reduceAllowedDeps( + deps: (AllowedPortDep | InstallConfigFat)[], +): AllowedPortDep[] { + return deps.map( + (dep: any) => { + { + const res = portsValidators.allowedPortDep.safeParse(dep); + if (res.success) return res.data; + } + const inst = unwrapZodRes( + portsValidators.installConfigFat.safeParse(dep), + dep, + "invalid allowed dep object, provide either InstallConfigFat or AllowedPortDep objects", + ); + const out: AllowedPortDep = { + manifest: inst.port, + defaultInst: thinInstallConfig(inst), + }; + return portsValidators.allowedPortDep.parse(out); + }, + ); +} + +function objectHashSafe(obj: unknown) { + return objectHash(JSON.parse(JSON.stringify(obj))); +} diff --git a/ghjk.ts b/ghjk.ts index 2c71bac2..076a8be7 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -1,69 +1,68 @@ -export { ghjk } from "./mod.ts"; -import * as ghjk from "./mod.ts"; +export { sophon } from "./hack.ts"; +import { config, install, task } from "./hack.ts"; import * as ports from "./ports/mod.ts"; +import { sedLock } from "./std.ts"; -ghjk - .task("greet", { - fn: async ({ $, argv: [name] }) => { - await $`echo Hello ${name}!`; - }, - }); - -const ha = ghjk - .task("ha", { - installs: [ - ports.protoc(), - ], - env: { STUFF: "stuffier" }, - async fn({ $ }) { - await $`echo $STUFF; - protoc --version; - `; - }, - }); - -const ho = ghjk - .task("ho", { - dependsOn: [ha], - async fn({ $ }) { - await $`echo ho`; - }, - }); - -const hum = ghjk - .task("hum", { - dependsOn: [ho], - async fn({ $ }) { - await $`echo hum`; - }, - }); - -const hii = ghjk - .task("hii", { - dependsOn: [hum], - async fn({ $ }) { - await $`echo haii`; - }, - }); - -ghjk - .task("hey", { - dependsOn: [hii, ho], - async fn({ $ }) { - await $`echo hey`; - }, - }); +config({ + defaultBaseEnv: "test", + enableRuntimes: true, +}); // these are just for quick testing -ghjk.install(); +install(); + +const DENO_VERSION = "1.44.2"; // these are used for developing ghjk -ghjk.install( +install( ports.act(), ports.pipi({ packageName: "pre-commit" })[0], - ports.cpy_bs({ releaseTag: "20231002" }), + ports.cpy_bs(), + ports.deno_ghrel({ version: DENO_VERSION }), ); -export const secureConfig = ghjk.secureConfig({ - allowedPortDeps: [...ghjk.stdDeps({ enableRuntimes: true })], -}); +task( + "lock-sed", + async ($) => { + const GHJK_VERSION = "0.2.0"; + await sedLock( + $.path(import.meta.dirname!), + { + lines: { + "./.github/workflows/*.yml": [ + [/(DENO_VERSION: ").*(")/, DENO_VERSION], + ], + "./host/mod.ts": [ + [/(GHJK_VERSION = ").*(")/, GHJK_VERSION], + ], + "./install.sh": [ + [/(GHJK_VERSION="\$\{GHJK_VERSION:-v).*(\}")/, GHJK_VERSION], + [/(DENO_VERSION="\$\{DENO_VERSION:-v).*(\}")/, DENO_VERSION], + ], + "./README.md": [ + [ + /(.*\/metatypedev\/ghjk\/)[^/]*(\/.*)/, + GHJK_VERSION, + ], + ], + }, + ignores: [ + // ignore this file to avoid hits on the regexps + `ghjk.ts`, + `.git`, + // TODO: std function for real ignore handling + ...(await $.path(".gitignore").readText()) + .split("\n") + .map((l) => l.trim()) + .filter((line) => line.length > 0) + .map((l) => `${l}${l.endsWith("*") ? "" : "*"}`), + ...(await $.path(".ghjk/.gitignore").readText()) + .split("\n") + .map((l) => l.trim()) + .filter((line) => line.length > 0) + .map((l) => `.ghjk/${l}${l.endsWith("*") ? "" : "*"}`), + ], + }, + ); + }, +); diff --git a/hack.ts b/hack.ts new file mode 100644 index 00000000..bb093358 --- /dev/null +++ b/hack.ts @@ -0,0 +1,84 @@ +//! This file allows an easy way to start with the typescript ghjkfile +//! but is generally insecure for serious usage. +//! +//! If your ghjkfile imports a malicious module, the module could +//! import the functions defined herin and mess with your ghjkfile. + +export * from "./mod.ts"; +import { file } from "./mod.ts"; +import logger from "./utils/logger.ts"; + +const ghjk = file(); + +export const sophon = Object.freeze(ghjk.sophon); +export const config = Object.freeze(firstCallerCheck(ghjk.config)); +export const env = Object.freeze(firstCallerCheck(ghjk.env)); +export const install = Object.freeze(firstCallerCheck(ghjk.install)); +export const task = Object.freeze(firstCallerCheck(ghjk.task)); + +// capture exit fn to avoid malicous caller from +// changing it on Deno object +// WARN: the following capture only works if the +// hack.ts module is the first import +const exitFn = Deno.exit; +let firstCaller: string | undefined; + +/** + * The following wrapper kills the program if it detects callers to `fn` + * from more than one file. + * + * This is a weak hack to prevent malicous imported scripts from modify the ghjk config + * through the above functions. + */ +function firstCallerCheck any>(fn: F): F { + return ((...args) => { + const caller = getCaller(); + if (!caller) { + logger(import.meta).error( + `unable to detect \`hack.ts\` caller, no stack traces availaible`, + ); + // prefer exit of throw here since malicious user might catch it otherwise + exitFn(1); + } else if (firstCaller === undefined) { + firstCaller = caller; + } else if (caller !== firstCaller) { + logger(import.meta).error( + `new \`hack.ts\` caller detected: ${caller} != ${firstCaller}`, + ); + exitFn(1); + } + return fn(...args); + }) as F; +} + +// lifted from https://github.com/apiel/caller/blob/ead98/caller.ts +// MIT License 2020 Alexander Piel +interface Bind { + cb?: (file: string) => string; +} +function getCaller(this: Bind | any, levelUp = 3) { + const err = new Error(); + const stack = err.stack?.split("\n")[levelUp]; + if (stack) { + return getFile.bind(this)(stack); + } + function getFile(this: Bind | any, stack: string): string { + stack = stack.substring(stack.indexOf("at ") + 3); + if (!stack.startsWith("file://")) { + stack = stack.substring(stack.lastIndexOf("(") + 1); + } + const path = stack.split(":"); + let file; + if (Deno.build.os == "windows") { + file = `${path[0]}:${path[1]}:${path[2]}`; + } else { + file = `${path[0]}:${path[1]}`; + } + + if ((this as Bind)?.cb) { + const cb = (this as Bind).cb as any; + file = cb(file); + } + return file; + } +} diff --git a/host/mod.ts b/host/mod.ts index 3dec14c3..39d4990e 100644 --- a/host/mod.ts +++ b/host/mod.ts @@ -1,72 +1,158 @@ -import { cliffy_cmd, deep_eql, jsonHash, zod } from "../deps/cli.ts"; -import logger, { isColorfulTty } from "../utils/logger.ts"; - +import { cliffy_cmd, deep_eql, zod, zod_val_err } from "../deps/cli.ts"; +import logger from "../utils/logger.ts"; import { $, - bufferHashHex, + bufferHashAsync, Json, - objectHashHex, - PathRef, - stringHashHex, + objectHash, + Path, + stringHash, } 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 "../files/deno/mod.ts"; import type { ModuleBase } from "../modules/mod.ts"; import { GhjkCtx } from "../modules/types.ts"; import { serializePlatform } from "../modules/ports/types/platform.ts"; export interface CliArgs { ghjkShareDir: string; - ghjkfilePath: string; + ghjkfilePath?: string; + ghjkDirPath?: string; + reFlagSet: boolean; + lockedFlagSet: boolean; } type HostCtx = { fileHashMemoStore: Map>; + curEnvVars: Record; + reFlagSet: boolean; + lockedFlagSet: boolean; }; +const GHJK_VERSION = "0.2.0"; + export async function cli(args: CliArgs) { - const ghjkfilePath = $.path(args.ghjkfilePath).resolve().normalize() - .toString(); - const ghjkShareDir = $.path(args.ghjkShareDir).resolve().normalize() - .toString(); - const ghjkDir = $.path(ghjkfilePath).parentOrThrow().join(".ghjk").toString(); + logger().debug(`ghjk CLI`, GHJK_VERSION); + if (args.reFlagSet && args.lockedFlagSet) { + throw new Error("GHJK_LOCKED && GHJK_RE both set"); + } + // items to run at end of function + const defer = [] as (() => Promise)[]; + + const ghjkShareDir = $.path(args.ghjkShareDir).resolve().normalize(); + let serializedConfig: object | undefined; + let gcx: GhjkCtx | undefined; - logger().debug({ ghjkfilePath, ghjkDir }); + if (!args.ghjkDirPath && args.ghjkfilePath) { + args.ghjkDirPath = $.path(args.ghjkfilePath).parentOrThrow().join(".ghjk") + .toString(); + } - const gcx = { ghjkShareDir, ghjkfilePath, ghjkDir, state: new Map() }; - const hcx = { fileHashMemoStore: new Map() }; + const subcmds: Record = {}; + + // most of the CLI is only avail if there's a + // ghjkfile detected + if (args.ghjkDirPath) { + gcx = { + ghjkShareDir, + ghjkDir: $.path(args.ghjkDirPath).resolve().normalize(), + ghjkfilePath: args.ghjkfilePath + ? $.path(args.ghjkfilePath).resolve().normalize() + : undefined, + blackboard: new Map(), + }; + const hcx: HostCtx = { + fileHashMemoStore: new Map(), + curEnvVars: Deno.env.toObject(), + reFlagSet: args.reFlagSet, + lockedFlagSet: args.lockedFlagSet, + }; + logger().debug("context established", { + ghjkDir: gcx?.ghjkDir.toString(), + ghjkfilePath: gcx.ghjkfilePath?.toString(), + }); - const { subCommands, serializedConfig } = await readConfig(gcx, hcx); + if (!await gcx.ghjkDir.join(".gitignore").exists()) { + gcx.ghjkDir.join(".gitignore").writeText($.dedent` + envs + hash.json`); + } + + // this returns nothing if no valid lockifle or ghjkfile + // is found + const commands = await commandsFromConfig(hcx, gcx); + if (commands) { + serializedConfig = commands.config; + // lock entries are also generated across program usage + // so we defer another write out until the end + defer.push(commands.writeLockFile); + + for ( + const [cmdName, [cmd, src]] of Object.entries(commands.subCommands) + ) { + 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 + .version(GHJK_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 discovored and built values to stdout.") + .description("Emit different discovered and built values to stdout.") .action(function () { this.showHelp(); }) .command( - "ghjk-dir-path", + "share-dir-path", new cliffy_cmd.Command() .description("Print the path where ghjk is installed in.") .action(function () { - console.log(ghjkDir); + if (!ghjkShareDir) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(ghjkShareDir.toString()); }), ) .command( - "share-dir-path", + "ghjkdir-path", new cliffy_cmd.Command() .description("Print the path where ghjk is installed in.") .action(function () { - console.log(ghjkShareDir); + if (!gcx) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(gcx.ghjkDir.toString()); }), ) .command( @@ -74,7 +160,11 @@ export async function cli(args: CliArgs) { new cliffy_cmd.Command() .description("Print the path of the ghjk.ts used") .action(function () { - console.log(ghjkfilePath); + if (!gcx?.ghjkfilePath) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log(gcx.ghjkfilePath.toString()); }), ) .command( @@ -83,69 +173,53 @@ export async function cli(args: CliArgs) { .description( "Print the extracted ans serialized config from the ghjkfile", ) - .action(function () { - console.log(Deno.inspect(serializedConfig, { - depth: 10, - colors: isColorfulTty(), - })); + .option( + "--json", + `Use json format when printing config.`, + ) + .action(function ({ json }) { + if (!serializedConfig) { + throw new Error("no ghjkfile found."); + } + // deno-lint-ignore no-console + console.log( + json + ? JSON.stringify(serializedConfig) + : $.inspect(serializedConfig), + ); }), ), - ) - .command( - "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()); - }), ); - - for (const [name, subcmd] of Object.entries(subCommands)) { - cmd = cmd.command(name, subcmd); + for (const [name, subcmd] of Object.entries(subcmds)) { + 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) { - const configPath = $.path(gcx.ghjkfilePath); - const configFileStat = await configPath.stat(); - // FIXME: subset of ghjk commands should be functional - // even if config file not found - if (!configFileStat) { - throw new Error("unable to locate config file", { - cause: gcx, - }); - } - const ghjkDirPath = $.path(gcx.ghjkDir); - if (!await ghjkDirPath.join(".gitignore").exists()) { - ghjkDirPath.join(".gitignore").writeText($.dedent` - envs - hash.json`); - } - const lockFilePath = ghjkDirPath.join("lock.json"); - const hashFilePath = ghjkDirPath.join("hash.json"); - - const subCommands = {} as Record; - const lockEntries = {} as Record; - - const curEnvVars = Deno.env.toObject(); +async function commandsFromConfig(hcx: HostCtx, gcx: GhjkCtx) { + const lockFilePath = gcx.ghjkDir.join("lock.json"); + const hashFilePath = gcx.ghjkDir.join("hash.json"); const foundLockObj = await readLockFile(lockFilePath); const foundHashObj = await readHashFile(hashFilePath); - const ghjkfileHash = await fileHashHex(hcx, configPath); + if (hcx.lockedFlagSet) { + if (!foundLockObj) { + throw new Error("GHJK_LOCKED set but no lockfile found"); + } + if (!foundHashObj) { + throw new Error("GHJK_LOCKED set but no hashfile found"); + } + } - let configExt: SerializedConfigExt | null = null; - // TODO: figure out cross platform lockfiles :O - if ( - foundLockObj && // lockfile found - foundLockObj.version == "0" - ) { + const lockEntries = {} as Record; + + const ghjkfileHash = await gcx.ghjkfilePath?.exists() + ? await fileDigestHex(hcx, gcx.ghjkfilePath!) + : undefined; + + if (!hcx.reFlagSet && foundLockObj) { logger().debug("loading lockfile", lockFilePath); for (const man of foundLockObj.config.modules) { const mod = std_modules.map[man.id]; @@ -166,68 +240,36 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { entry as Json, ); } - - const platformMatch = () => - foundLockObj.platform[0] == Deno.build.os && - foundLockObj.platform[1] == Deno.build.arch; - - const envHashesMatch = async () => { - const oldHashes = foundHashObj!.envVarHashes; - const newHashes = await hashEnvVars(curEnvVars, [ - ...Object.keys(oldHashes), - ]); - return deep_eql(oldHashes, newHashes); - }; - - const cwd = $.path(Deno.cwd()); - const fileHashesMatch = async () => { - const oldHashes = foundHashObj!.readFileHashes; - const newHashes = await hashFiles(hcx, [ - ...Object.keys(oldHashes), - ], cwd); - return deep_eql(oldHashes, newHashes); - }; - - const fileListingsMatch = async () => { - const oldListed = foundHashObj!.listedFiles; - for (const path of oldListed) { - if (!await cwd.resolve(path).exists()) { - return false; - } - } - return true; - }; - // avoid reserializing the config if - // the ghjkfile and environment is _satisfcatorily_ - // similar - if ( - foundHashObj && - foundHashObj.ghjkfileHash == ghjkfileHash && - platformMatch() && - await fileHashesMatch() && - await fileListingsMatch() && - await envHashesMatch() - ) { - configExt = { - config: foundLockObj.config, - envVarHashes: foundHashObj.envVarHashes, - readFileHashes: foundHashObj.readFileHashes, - listedFiles: foundHashObj.listedFiles, - }; - } } - if (!configExt) { - logger().info("serializing ghjkfile", configPath); - configExt = await readAndSerializeConfig(hcx, configPath, curEnvVars); + let configExt: SerializedConfigExt | null = null; + let wasReSerialized = false; + if ( + !hcx.reFlagSet && + foundLockObj && + foundHashObj && + (hcx.lockedFlagSet || + // avoid reserializing the config if + // the ghjkfile and environment is _satisfcatorily_ + // similar. "cache validation" + foundLockObj.version == "0" && + await isHashFileValid(hcx, foundLockObj, foundHashObj, ghjkfileHash)) + ) { + configExt = { + config: foundLockObj.config, + envVarHashes: foundHashObj.envVarHashes, + readFileHashes: foundHashObj.readFileHashes, + listedFiles: foundHashObj.listedFiles, + }; + } else if (gcx.ghjkfilePath) { + logger().info("serializing ghjkfile", gcx.ghjkfilePath); + configExt = await readGhjkfile(hcx, gcx.ghjkfilePath); + wasReSerialized = true; + } else { + // nothing to get the commands from + return; } - const newLockObj: zod.infer = { - version: "0", - platform: serializePlatform(Deno.build), - moduleEntries: {} as Record, - config: configExt.config, - }; const newHashObj: zod.infer = { version: "0", ghjkfileHash, @@ -235,7 +277,10 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { readFileHashes: configExt.readFileHashes, listedFiles: configExt.listedFiles, }; - const instances = []; + // command name to [cmd, source module id] + const subCommands = {} as Record; + const instances = [] as [string, ModuleBase, unknown][]; + for (const man of configExt.config.modules) { const mod = std_modules.map[man.id]; if (!mod) { @@ -245,60 +290,139 @@ async function readConfig(gcx: GhjkCtx, hcx: HostCtx) { const pMan = await instance.processManifest( gcx, man, + configExt.config.blackboard, lockEntries[man.id], - newLockObj.config.globalEnv, ); 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)) { + + if ( + !hcx.lockedFlagSet && wasReSerialized && ( + !foundHashObj || !deep_eql(newHashObj, foundHashObj) + ) + ) { await hashFilePath.writeJsonPretty(newHashObj); } - return { subCommands, serializedConfig: configExt.config }; + + // `writeLockFile` can be invoked multiple times + // so we keep track of the last lockfile wrote + // out to disk + // TODO(#90): file system lock file while ghjk is running + // to avoid multiple instances from clobbering each other + let lastLockObj = { ...foundLockObj }; + return { + subCommands, + config: configExt.config, + async writeLockFile() { + if (hcx.lockedFlagSet) return; + + 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 (!lastLockObj || !deep_eql(newLockObj, lastLockObj)) { + lastLockObj = { ...newLockObj }; + await lockFilePath.writeJsonPretty(newLockObj); + } + }, + }; } -type HashStore = Record; +async function isHashFileValid( + hcx: HostCtx, + foundLockFile: zod.infer, + foundHashFile: zod.infer, + ghjkfileHash?: string, +) { + // TODO: figure out cross platform lockfiles :O + const platformMatch = () => + serializePlatform(Deno.build) == foundLockFile.platform; + + const envHashesMatch = () => { + const oldHashes = foundHashFile!.envVarHashes; + const newHashes = envVarDigests(hcx.curEnvVars, [ + ...Object.keys(oldHashes), + ]); + return deep_eql(oldHashes, newHashes); + }; -type SerializedConfigExt = { - config: SerializedConfig; - envVarHashes: HashStore; - readFileHashes: HashStore; - listedFiles: string[]; -}; + const cwd = $.path(Deno.cwd()); + const fileHashesMatch = async () => { + const oldHashes = foundHashFile!.readFileHashes; + const newHashes = await fileDigests(hcx, [ + ...Object.keys(oldHashes), + ], cwd); + return deep_eql(oldHashes, newHashes); + }; -async function readAndSerializeConfig( + const fileListingsMatch = async () => { + const oldListed = foundHashFile!.listedFiles; + for (const path of oldListed) { + if (!await cwd.resolve(path).exists()) { + return false; + } + } + return true; + }; + // NOTE: these are ordered by the amount effort it takes + // to check each + // we only check file hash of the ghjk file if it's present + return (ghjkfileHash ? foundHashFile.ghjkfileHash == ghjkfileHash : true) && + platformMatch() && + envHashesMatch() && + await fileListingsMatch() && + await fileHashesMatch(); +} + +type DigestsMap = Record; + +type SerializedConfigExt = Awaited< + ReturnType +>; + +async function readGhjkfile( hcx: HostCtx, - configPath: PathRef, - envVars: Record, -): Promise { + configPath: Path, +) { switch (configPath.extname()) { case "": - logger().warning("config file has no extension, assuming deno config"); + 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, + hcx.curEnvVars, ); - const envVarHashes = await hashEnvVars(envVars, res.accessedEnvKeys); + const envVarHashes = envVarDigests(hcx.curEnvVars, res.accessedEnvKeys); const cwd = $.path(Deno.cwd()); const cwdStr = cwd.toString(); const listedFiles = res.listedFiles @@ -308,7 +432,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), @@ -330,16 +454,19 @@ 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}`); + try { + return validators.serializedConfig.parse(raw); + } catch (err) { + const validationError = zod_val_err.fromError(err); + throw new Error( + `error parsing seralized config from ${configPath}: ${validationError.toString()}`, + { + cause: validationError, + }, + ); } - - return res.data; } const lockObjValidator = zod.object({ @@ -349,81 +476,106 @@ 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); + return lockObjValidator.parse(rawJson); + } catch (err) { + const validationError = zod_val_err.fromError(err); + logger().error( + `error parsing lockfile from ${lockFilePath}: ${validationError.toString()}`, + ); + if (Deno.stderr.isTerminal() && await $.confirm("Discard lockfile?")) { + return; + } else { + throw validationError; + } } - return res.data; } const hashObjValidator = zod.object({ version: zod.string(), - ghjkfileHash: zod.string(), + ghjkfileHash: zod.string().nullish(), envVarHashes: zod.record(zod.string(), zod.string().nullish()), readFileHashes: zod.record(zod.string(), zod.string().nullish()), listedFiles: zod.string().array(), // 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); + return hashObjValidator.parse(rawJson); + } catch (err) { + logger().error( + `error parsing hashfile from ${hashObjValidator}: ${ + zod_val_err.fromError(err).toString() + }`, + ); + logger().warn("discarding invalid hashfile"); + return; } - return res.data; } -async function hashEnvVars(all: Record, accessed: string[]) { - const hashes = {} as HashStore; +function envVarDigests(all: Record, accessed: string[]) { + const hashes = {} as DigestsMap; for (const key of accessed) { const val = all[key]; if (!val) { // use null if the serializer accessed hashes[key] = null; } else { - hashes[key] = await stringHashHex(val); + hashes[key] = stringHash(val); } } 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 pathRef = cwd.resolve(path); - const relativePath = pathRef + const readFileHashes = {} as DigestsMap; + await Promise.all(readFiles.map(async (pathStr) => { + const path = cwd.resolve(pathStr); + const relativePath = path .toString() .replace(cwdStr, "."); // FIXME: stream read into hash to improve mem usage - const stat = await pathRef.lstat(); + const stat = await path.lstat(); if (stat) { const contentHash = (stat.isFile || stat.isSymlink) - ? await fileHashHex(hcx, pathRef) + ? await fileDigestHex(hcx, path) : null; - readFileHashes[relativePath] = await objectHashHex({ - ...stat, + readFileHashes[relativePath] = objectHash({ + ...JSON.parse(JSON.stringify(stat)), contentHash, - } as jsonHash.Tree); + }); } 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) { @@ -432,7 +584,7 @@ function fileHashHex(hcx: HostCtx, path: PathRef) { } return promise; async function inner() { - return await bufferHashHex( + return await bufferHashAsync( await path.readBytes(), ); } diff --git a/host/types.ts b/host/types.ts index 094c809b..68283c8d 100644 --- a/host/types.ts +++ b/host/types.ts @@ -1,21 +1,21 @@ import { zod } from "../deps/common.ts"; import moduleValidators from "../modules/types.ts"; -import portsValidator from "../modules/ports/types.ts"; -const globalEnv = zod.object({ - installs: zod.record(zod.string(), portsValidator.installConfigFat), - allowedPortDeps: zod.record(zod.string(), portsValidator.allowedPortDep), -}); +/* const blackboard = zod.object({ + // installs: zod.record(zod.string(), portsValidator.installConfigFat), + // allowedPortDeps: zod.record(zod.string(), portsValidator.allowedPortDep), +}); */ +const blackboard = zod.record(zod.string(), zod.unknown()); const serializedConfig = zod.object( { modules: zod.array(moduleValidators.moduleManifest), - globalEnv, + blackboard, }, ); export type SerializedConfig = zod.infer; -export type GlobalEnv = zod.infer; +export type Blackboard = zod.infer; export default { serializedConfig, diff --git a/install.sh b/install.sh index d10d6aca..5869d4a7 100755 --- a/install.sh +++ b/install.sh @@ -2,10 +2,10 @@ set -e -u -GHJK_VERSION="${GHJK_VERSION:-v0.1.0-alpha}" +GHJK_VERSION="${GHJK_VERSION:-v0.2.0}" GHJK_INSTALLER_URL="${GHJK_INSTALLER_URL:-https://raw.github.com/metatypedev/ghjk/$GHJK_VERSION/install.ts}" GHJK_SHARE_DIR="${GHJK_SHARE_DIR:-$HOME/.local/share/ghjk}" -DENO_VERSION="${DENO_VERSION:-v1.42.1}" +DENO_VERSION="${DENO_VERSION:-v1.44.2}" # make sure the version is prepended with v if [ "${DENO_VERSION#"v"}" = "$DENO_VERSION" ]; then diff --git a/install.ts b/install.ts index 330a2924..c986fd5c 100755 --- a/install.ts +++ b/install.ts @@ -3,25 +3,32 @@ //! 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"; if (import.meta.main) { const skipBinInstall = Deno.env.get("GHJK_INSTALL_SKIP_EXE"); const noLockfile = Deno.env.get("GHJK_INSTALL_NO_LOCKFILE"); + const 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") ?? @@ -34,6 +41,8 @@ if (import.meta.main) { }); } else { throw new Error( - "unexpected ctx: if you want to access the ghjk installer, import `install` from ./install/mod.ts", + `unexpected context: this module is an entrypoint. If you want to programmatically invoke the ghjk installer, import \`install\` from ${ + import.meta.resolve("./install/mod.ts") + }`, ); } diff --git a/install/ghjk.sh b/install/ghjk.sh index 518f29ba..50ac141b 100644 --- a/install/ghjk.sh +++ b/install/ghjk.sh @@ -1,14 +1,25 @@ #!/bin/sh + 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 +GHJK_MAIN_URL="${GHJK_MAIN_URL:-__MAIN_TS_URL__}" + +# NOTE: avoid putting too much in here as this is only one +# method of getting the ghjk bin which is all utlimately optional +# anyways. + +# NOTE: keep this in sync with impls in install/exec.ts # if ghjkfile var is set, set the GHJK_DIR overriding # any set by the user if [ -n "${GHJKFILE+x}" ]; then + GHJK_DIR="$(dirname "$GHJKFILE")/.ghjk" + # if both GHJKFILE and GHJK_DIR are unset elif [ -z "${GHJK_DIR+x}" ]; then + # look for ghjk dirs in parents cur_dir=$PWD while true; do @@ -23,17 +34,22 @@ elif [ -z "${GHJK_DIR+x}" ]; then fi cur_dir="$next_cur_dir" done + fi if [ -n "${GHJK_DIR+x}" ]; then + export GHJK_DIR mkdir -p "$GHJK_DIR" lock_flag="--lock $GHJK_DIR/deno.lock" + else + lock_flag="--no-lock" + 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_FLAGS__ -A $lock_flag $GHJK_MAIN_URL "$@" diff --git a/install/hook.fish b/install/hook.fish index 282e552c..072c19fe 100644 --- a/install/hook.fish +++ b/install/hook.fish @@ -1,21 +1,38 @@ -function ghjk_reload --on-variable PWD +function __ghjk_get_mtime_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 + # precedence is gven to argv over GHJK_ENV + set --local next_env $argv[1] + test -z $next_env; and set next_env "$GHJK_ENV" + # we ignore previously loaded GHJK_ENV when switching + # directories + test "$argv" = "VARIABLE SET PWD"; and set next_env "" + test -z $next_env; and set next_env "default" + 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 # if $GHJKFILE is set, set the GHJK_DIR overriding # any set by the user if set --query GHJKFILE - set cur_dir (dirname $GHJKFILE) - set local_ghjk_dir $cur_dir/.ghjk + set local_ghjk_dir (dirname $GHJKFILE)/.ghjk # if both GHJKFILE and GHJK_DIR are unset else if test -z "$local_ghjk_dir" # look for ghjk dirs in pwd and parents - set cur_dir $PWD + set --local cur_dir $PWD while true if test -d $cur_dir/.ghjk; or test -d $cur_dir/ghjk.ts set local_ghjk_dir $cur_dir/.ghjk @@ -30,31 +47,64 @@ function ghjk_reload --on-variable PWD end set cur_dir $next_cur_dir end - else - set cur_dir (dirname $local_ghjk_dir) 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 + set --global --export GHJK_LAST_GHJK_DIR $local_ghjk_dir + + # locate the next env + set --local next_env_dir $local_ghjk_dir/envs/$next_env + + if test -d $next_env_dir # load the shim - . $default_env/loader.fish + . $next_env_dir/activate.fish + # export variables to assist in change detection + set --global --export GHJK_LAST_ENV_DIR $next_env_dir + set --global --export GHJK_LAST_ENV_DIR_MTIME (__ghjk_get_mtime_ts $next_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 (__ghjk_get_mtime_ts $next_env_dir/activate.fish) -lt (__ghjk_get_mtime_ts $local_ghjk_dir/../ghjk.ts) set_color FF4500 - echo "[ghjk] Detected drift from default environment, please sync..." + if test $next_env = "default" + echo "[ghjk] Possible drift from default environment, please sync..." + else + echo "[ghjk] Possible drift from active environment ($next_env), please sync..." + end set_color normal end else set_color FF4500 - echo "[ghjk] No default environment found, please sync..." + if test $next_env = "default" + echo "[ghjk] Default environment not found, please sync..." + else + echo "[ghjk] Active environment ($next_env) not found, please sync..." + end set_color normal end end end +set --local tmp_dir "$TMPDIR" +test -z $tmp_dir; and set tmp_dir "/tmp" +set --export --global GHJK_NEXTFILE "$tmp_dir/ghjk.nextfile.$fish_pid" + +# trigger reload when the env dir loader mtime changes +function __ghjk_preexec --on-event fish_preexec + + # trigger reload when either + # exists + if set --query GHJK_NEXTFILE; and test -f "$GHJK_NEXTFILE"; + + ghjk_reload (cat $GHJK_NEXTFILE) + rm "$GHJK_NEXTFILE" + + # activate script has reloaded + else if set --query GHJK_LAST_ENV_DIR; + and test (__ghjk_get_mtime_ts $GHJK_LAST_ENV_DIR/activate.fish) -gt $GHJK_LAST_ENV_DIR_MTIME; + ghjk_reload + end +end + ghjk_reload diff --git a/install/hook.sh b/install/hook.sh index f612aa59..181ccbc5 100644 --- a/install/hook.sh +++ b/install/hook.sh @@ -1,23 +1,41 @@ -# shellcheck disable=SC2148 +# shellcheck shell=sh # keep this posix compatible as it supports bash and zsh +__ghjk_get_mtime_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() { + + # precedence is given to argv over GHJK_ENV + # which's usually the current active env + next_env="${1:-${GHJK_ENV:-default}}"; + 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 - cur_dir=$(dirname "$GHJKFILE") - local_ghjk_dir="$cur_dir/.ghjk" + local_ghjk_dir="$(dirname "$GHJKFILE")/.ghjk" # if both GHJKFILE and GHJK_DIR are unset elif [ -z "$local_ghjk_dir" ]; then # look for ghjk dirs in pwd parents + # use do while format to allow detection of .ghjk in root dirs cur_dir=$PWD while true; do if [ -d "$cur_dir/.ghjk" ] || [ -e "$cur_dir/ghjk.ts" ]; then @@ -25,45 +43,76 @@ ghjk_reload() { break fi # recursively look in parent directory - # use do while format to allow detection of .ghjk in root dirs next_cur_dir="$(dirname "$cur_dir")" if [ "$next_cur_dir" = / ] && [ "$cur_dir" = "/" ]; then break fi cur_dir="$next_cur_dir" done - else - cur_dir=$(dirname "$local_ghjk_dir") fi 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 + GHJK_LAST_GHJK_DIR="$local_ghjk_dir" + export GHJK_LAST_GHJK_DIR + + # locate the next env + next_env_dir="$local_ghjk_dir/envs/$next_env" + + if [ -d "$next_env_dir" ]; then # load the shim # shellcheck source=/dev/null - . "$default_env/loader.sh" + . "$next_env_dir/activate.sh" + # export variables to assist in change detection + GHJK_LAST_ENV_DIR="$next_env_dir" + GHJK_LAST_ENV_DIR_MTIME="$(__ghjk_get_mtime_ts "$next_env_dir/activate.sh")" + export GHJK_LAST_ENV_DIR + export GHJK_LAST_ENV_DIR_MTIME - # 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 [ "$(__ghjk_get_mtime_ts "$local_ghjk_dir/../ghjk.ts")" -gt "$(__ghjk_get_mtime_ts "$next_env_dir/activate.sh")" ]; then + if [ "$next_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" "$next_env" + fi + fi else - printf "\033[0;31m[ghjk] No default environment found, please sync...\033[0m\n" + if [ "$next_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" "$next_env" + fi fi fi } # memo to detect directory changes export GHJK_LAST_PWD="$PWD" +export GHJK_NEXTFILE="${TMPDIR:-/tmp}/ghjk.nextfile.$$" precmd() { + # trigger reload when either + # - the PWD changes if [ "$GHJK_LAST_PWD" != "$PWD" ]; then + + # we ignore previously loaded GHJK_ENV when switching + # directories + unset GHJK_ENV ghjk_reload export GHJK_LAST_PWD="$PWD" + + # -nextfile exists + elif [ -f "$GHJK_NEXTFILE" ]; then + + ghjk_reload "$(cat "$GHJK_NEXTFILE")" + rm "$GHJK_NEXTFILE" + + # - the env dir loader mtime changes + elif [ "$(__ghjk_get_mtime_ts "$GHJK_LAST_ENV_DIR/activate.sh")" -gt "$GHJK_LAST_ENV_DIR_MTIME" ]; then + + ghjk_reload + fi } diff --git a/install/mod.ts b/install/mod.ts index 0cbbf40c..d63eca51 100644 --- a/install/mod.ts +++ b/install/mod.ts @@ -1,10 +1,26 @@ //! this installs the different shell ghjk hooks in ~/.local/share/ghjk //! and a `ghjk` bin at ~/.local/share/bin -import logger from "../utils/logger.ts"; -import { std_fs, std_path } from "../deps/cli.ts"; +// TODO: explore installing deno.lock from ghjk repo and +// relying on --frozen-lockfile + +import getLogger from "../utils/logger.ts"; import { $, dirs, importRaw } from "../utils/mod.ts"; +import type { Path } from "../utils/mod.ts"; + +const logger = getLogger(import.meta); +/** + * Deno unstable flags needed for ghjk host. + */ +export const unstableFlags = [ + "--unstable-kv", + "--unstable-worker-options", +]; + +// TODO: calculate and add integrity hashes to these raw imports +// as they won't be covered by deno.lock +// - use pre-commit-hook plus ghjk tasks to do find+replace // null means it should be removed (for cleaning up old versions) const getHooksVfs = async () => ({ "env.sh": ( @@ -33,52 +49,41 @@ 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, + baseDirRaw: Path, replacements: [RegExp, string][], ): Promise { - await $.path(baseDir).ensureDir(); + const baseDir = await $.path(baseDirRaw).ensureDir(); for (const [subpath, content] of Object.entries(vfs)) { - const path = std_path.resolve(baseDir, subpath); + const path = baseDir.join(subpath); if (content === null) { - await $.path(baseDir).remove({ recursive: true }); + await path.remove({ recursive: true }); } else { let text = content.trim(); for (const [re, repl] of replacements) { text = text.replace(re, repl); } - await $.path(std_path.dirname(path)).ensureDir(); - await $.path(path).writeText(text); + await path.parentOrThrow().ensureDir(); + await path.writeText(text); } } } async function filterAddContent( - path: string, + path: Path, marker: RegExp, content: string | null, ) { - const file = await Deno.readTextFile(path).catch(async (err) => { - if (err instanceof Deno.errors.NotFound) { - await Deno.mkdir(std_path.dirname(path), { recursive: true }); - return ""; - } - throw err; - }); + const file = await path.readText() + .catch(async (err) => { + if (err instanceof Deno.errors.NotFound) { + await $.path(path).parentOrThrow().ensureDir(); + return ""; + } + throw err; + }); const lines = file.split("\n"); let i = 0; @@ -94,42 +99,55 @@ async function filterAddContent( lines.push(content); } - await Deno.writeTextFile(path, lines.join("\n")); + await path.writeText(lines.join("\n")); } interface InstallArgs { homeDir: string; ghjkShareDir: string; - shellsToHook: string[]; - /// The mark used when adding the hook to the user's shell rcs - /// Override t + shellsToHook?: string[]; + /** 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; } export const defaultInstallArgs: InstallArgs = { - ghjkShareDir: std_path.resolve(dirs().shareDir, "ghjk"), + ghjkShareDir: $.path(dirs().shareDir).resolve("ghjk").toString(), homeDir: dirs().homeDir, shellsToHook: [], shellHookMarker: "ghjk-hook-default", skipExecInstall: true, // TODO: respect xdg dirs - ghjkExecInstallDir: std_path.resolve(dirs().homeDir, ".local", "bin"), + ghjkExecInstallDir: $.path(dirs().homeDir).resolve(".local", "bin") + .toString(), 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, }; @@ -142,32 +160,34 @@ const shellConfig: Record = { export async function install( args: InstallArgs = defaultInstallArgs, ) { - logger().debug("installing", args); + logger.debug("installing", args); if (Deno.build.os == "windows") { throw new Error("windows is not yet supported, please use wsl"); } - const ghjkShareDir = std_path.resolve( - Deno.cwd(), - std_path.normalize(args.ghjkShareDir), - ); + const ghjkShareDir = $.path(Deno.cwd()) + .resolve(args.ghjkShareDir); - logger().debug("unpacking vfs", { ghjkShareDir }); + logger.info("unpacking vfs", { ghjkShareDir }); await unpackVFS( await getHooksVfs(), ghjkShareDir, - [[/__GHJK_SHARE_DIR__/g, ghjkShareDir]], + [[/__GHJK_SHARE_DIR__/g, ghjkShareDir.toString()]], ); - - for (const shell of args.shellsToHook) { + for (const shell of args.shellsToHook ?? Object.keys(shellConfig)) { const { homeDir } = args; if (!(shell in shellConfig)) { throw new Error(`unsupported shell: ${shell}`); } - const rcPath = std_path.resolve(homeDir, shellConfig[shell]); - logger().debug("installing hook", { + const rcPath = $.path(homeDir).join(shellConfig[shell]); + // if the shell rc file isn't detected and we're hooking + // the default shell set, just skip it + if (!await rcPath.exists() && !args.shellsToHook) { + continue; + } + logger.info("installing hook", { ghjkShareDir, shell, marker: args.shellHookMarker, @@ -187,28 +207,32 @@ export async function install( case "solaris": case "illumos": case "darwin": { - await std_fs.ensureDir(args.ghjkExecInstallDir); - const exePath = std_path.resolve(args.ghjkExecInstallDir, `ghjk`); - logger().debug("installing executable", { exePath }); + const installDir = await $.path(args.ghjkExecInstallDir).ensureDir(); + const exePath = installDir.resolve(`ghjk`); + logger.info("installing executable", { exePath }); // use an isolated cache by default - const denoCacheDir = args.ghjkDenoCacheDir ?? - std_path.resolve(ghjkShareDir, "deno"); - await Deno.writeTextFile( - exePath, + const denoCacheDir = args.ghjkDenoCacheDir + ? $.path(args.ghjkDenoCacheDir) + : ghjkShareDir.resolve("deno"); + await exePath.writeText( (await importRaw(import.meta.resolve("./ghjk.sh"))) .replaceAll( "__GHJK_SHARE_DIR__", - ghjkShareDir, + ghjkShareDir.toString(), ) .replaceAll( "__DENO_CACHE_DIR", - denoCacheDir, + denoCacheDir.toString(), ) .replaceAll( "__DENO_EXEC__", args.ghjkExecDenoExec, ) + .replaceAll( + "__UNSTABLE_FLAGS__", + unstableFlags.join(" "), + ) .replaceAll( "__MAIN_TS_URL__", import.meta.resolve("../main.ts"), @@ -221,5 +245,5 @@ export async function install( throw new Error(`${Deno.build.os} is not yet supported`); } } - logger().info("install success"); + logger.info("install success"); } diff --git a/install/utils.ts b/install/utils.ts new file mode 100644 index 00000000..1f5bb9b5 --- /dev/null +++ b/install/utils.ts @@ -0,0 +1,44 @@ +//! Please keep these in sync with `./ghjk.ts` + +import type { GhjkCtx } from "../modules/types.ts"; +import { unstableFlags } from "./mod.ts"; + +/** + * Returns a simple posix function to invoke the ghjk CLI. + */ +export function ghjk_sh( + gcx: GhjkCtx, + denoDir: string, + functionName = "__ghjk_shim", +) { + return `${functionName} () { + GHJK_SHARE_DIR="${gcx.ghjkShareDir}" \\ + DENO_DIR="${denoDir}" \\ + DENO_NO_UPDATE_CHECK=1 \\ + GHJK_DIR="${gcx.ghjkDir}" \\ + ${Deno.execPath()} run ${ + unstableFlags.join(" ") + } -A --lock ${gcx.ghjkDir}/deno.lock ${import.meta.resolve("../main.ts")} "$@" +}`; +} + +/** + * Returns a simple fish function to invoke the ghjk CLI. + */ +export function ghjk_fish( + gcx: GhjkCtx, + denoDir: string, + functionName = "__ghjk_shim", +) { + return `function ${functionName} + GHJK_SHARE_DIR="${gcx.ghjkShareDir}" \\ + DENO_DIR="${denoDir}" \\ + DENO_NO_UPDATE_CHECK=1 \\ + GHJK_DIR="${gcx.ghjkDir}" \\ + ${Deno.execPath()} run ${ + unstableFlags.join(" ") + } -A --lock ${gcx.ghjkDir}/deno.lock ${ + import.meta.resolve("../main.ts") + } $argv +end`; +} diff --git a/main.ts b/main.ts index 6859e698..35a2f835 100755 --- a/main.ts +++ b/main.ts @@ -4,24 +4,40 @@ import "./setup_logger.ts"; import { cli } from "./host/mod.ts"; import { std_path } from "./deps/common.ts"; import logger from "./utils/logger.ts"; -import { dirs, findConfig } from "./utils/mod.ts"; +import { dirs, findEntryRecursive } from "./utils/mod.ts"; if (import.meta.main) { - const ghjkfile = Deno.env.get("GHJKFILE") ?? - await findConfig(Deno.cwd()); - if (!ghjkfile) { - logger().error( - "ghjk could not find any ghjkfiles, try creating a `ghjk.ts` script.", + // look for ghjkdir + let ghjkdir = Deno.env.get("GHJK_DIR") ?? + await findEntryRecursive(Deno.cwd(), ".ghjk"); + const ghjkfile = ghjkdir + ? await findEntryRecursive(std_path.dirname(ghjkdir), "ghjk.ts") + : await findEntryRecursive(Deno.cwd(), "ghjk.ts"); + if (!ghjkdir && !ghjkfile) { + logger().warn( + "ghjk could not find any ghjkfiles or ghjkdirs, try creating a `ghjk.ts` script.", ); - Deno.exit(2); + // Deno.exit(2); + } + if (ghjkfile && !ghjkdir) { + ghjkdir = std_path.resolve(std_path.dirname(ghjkfile), ".ghjk"); } await cli({ + // FIXME: better + reFlagSet: !!Deno.env.get("GHJK_RE") && + !(["false", "", ""].includes(Deno.env.get("GHJK_RE")!)), + lockedFlagSet: !!Deno.env.get("GHJK_LOCKED") && + !(["false", "", ""].includes(Deno.env.get("GHJK_LOCKED")!)), + ghjkShareDir: Deno.env.get("GHJK_SHARE_DIR") ?? - std_path.resolve(dirs().shareDir, "ghjk"), - ghjkfilePath: std_path.resolve(Deno.cwd(), ghjkfile), + dirs().shareDir.resolve("ghjk").toString(), + ghjkfilePath: ghjkfile ? std_path.resolve(Deno.cwd(), ghjkfile) : undefined, + ghjkDirPath: ghjkdir ? std_path.resolve(Deno.cwd(), ghjkdir) : undefined, }); } else { throw new Error( - "unexpected ctx: if you want to run the ghjk cli, import `main` from ./host/mod.ts", + `unexpected context: this module is an entrypoint. If you want to programmatically invoke the ghjk cli, import \`cli\` from ${ + import.meta.resolve("./host/mod.ts") + }`, ); } diff --git a/mod.ts b/mod.ts index db2f94bb..891c777a 100644 --- a/mod.ts +++ b/mod.ts @@ -1,226 +1,291 @@ -//! This module is intended to be re-exported by `ghjk.ts` config scripts. Please -//! avoid importing elsewhere at it has side-effects. +//! This module is intended to be re-exported by `ghjk.ts` config scripts. // 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, - PortsModuleConfig, - PortsModuleConfigBase, - PortsModuleSecureConfig, } from "./modules/ports/types.ts"; import logger from "./utils/logger.ts"; -import { $, defaultCommandBuilder, thinInstallConfig } 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"; -// hosts -import type { GlobalEnv, SerializedConfig } from "./host/types.ts"; -import * as std_modules from "./modules/std.ts"; -// tasks -import type { - TaskDef, - TaskEnv, - TasksModuleConfig, -} from "./modules/tasks/types.ts"; -import { dax, jsonHash, objectHash } from "./deps/common.ts"; - -const portsConfig: PortsModuleConfigBase = { installs: [] }; - -export type TaskFnArgs = { - $: dax.$Type; - argv: string[]; - env: Record; -}; -export type TaskFn = (args: TaskFnArgs) => Promise; +import { $ } from "./utils/mod.ts"; +import { + EnvBuilder, + Ghjkfile, + reduceAllowedDeps, + stdDeps, +} from "./files/mod.ts"; +import type { DenoTaskDefArgs, EnvDefArgs, TaskFn } from "./files/mod.ts"; +// WARN: this module has side-effects and only ever import +// types from it +import type { ExecTaskArgs } from "./modules/tasks/deno.ts"; + +export type { DenoTaskDefArgs, EnvDefArgs, TaskFn } from "./files/mod.ts"; +export { $, logger, stdDeps }; -export type TaskFnDef = TaskDef & { - fn: TaskFn; - // command: cliffy_cmd.Command; +export type AddEnv = { + (args: EnvDefArgs): EnvBuilder; + (name: string, args?: Omit): EnvBuilder; }; -// TODO tasks config -const tasks = {} as Record; +/** + * Provision a port install in the `main` env. + */ +export type AddInstall = { + (...configs: InstallConfigFat[]): void; +}; -const globalEnv: GlobalEnv = { - installs: {}, - allowedPortDeps: {}, +/** + * Define and register a task. + */ +export type AddTask = { + (args: DenoTaskDefArgs): string; + (name: string, args: Omit): string; + (fn: TaskFn, args?: Omit): string; + ( + name: string, + fn: TaskFn, + args?: Omit, + ): string; }; -// FIXME: ses.lockdown to freeze primoridials -// freeze the object to prevent malicious tampering of the secureConfig -export const ghjk = Object.freeze({ - getConfig: Object.freeze(getConfig), - execTask: Object.freeze(execTask), -}); +export type FileArgs = { + /** + * The env to activate by default. When entering the working + * directory for example. + */ + defaultEnv?: string; + /** + * The default env all envs inherit from. + */ + defaultBaseEnv?: string; + /** + * Additional ports that can be used as build time dependencies. + * + * This applies to the `defaultBaseEnv` env. + */ + allowedBuildDeps?: (InstallConfigFat | AllowedPortDep)[]; + /** + * Wether or not use the default set of allowed build dependencies. + * If set, {@link enableRuntimes} is ignored but {@link allowedBuildDeps} + * is still respected. + * True by default. + * + * This applies to the `defaultBaseEnv` env. + */ + stdDeps?: boolean; + /** + * (unstable) Allow runtimes from std deps to be used as build time dependencies. + * + * This applies to the `defaultBaseEnv` env. + */ + enableRuntimes?: boolean; + /** + * Installs to add to the main env. + */ + installs?: InstallConfigFat[]; + /** + * Tasks to expose to the CLI. + */ + tasks?: Record; + /** + * Different envs availaible to the CLI. + */ + envs?: EnvDefArgs[]; +}; -export { $, logger }; +type SecureConfigArgs = Omit< + FileArgs, + "envs" | "tasks" | "installs" +>; -export function install(...configs: InstallConfigFat[]) { - const cx = portsConfig; - for (const config of configs) { - addInstall(cx, config); - } -} +type DenoFileKnobs = { + sophon: Readonly; + /** + * {@inheritdoc AddInstall} + */ + install: AddInstall; + /** + * {@inheritdoc AddTask} + */ + task: AddTask; + /** + * {@inheritDoc AddEnv} + */ + env: AddEnv; + /** + * Configure global and miscallenous ghjk settings. + */ + config(args: SecureConfigArgs): void; +}; -function registerInstall(config: InstallConfigFat) { - // jsonHash.digest is async - const hash = objectHash(jsonHash.canonicalize(config as jsonHash.Tree)); +export const file = Object.freeze(function file( + args: FileArgs = {}, +): DenoFileKnobs { + const defaultBuildDepsSet: AllowedPortDep[] = []; - if (!globalEnv.installs[hash]) { - globalEnv.installs[hash] = config; - } - return hash; -} + const DEFAULT_BASE_ENV_NAME = "main"; -function registerAllowedPortDep(dep: AllowedPortDep) { - const hash = objectHash(jsonHash.canonicalize(dep as jsonHash.Tree)); - if (!globalEnv.allowedPortDeps[hash]) { - globalEnv.allowedPortDeps[hash] = dep; - } - return hash; -} + const builder = new Ghjkfile(); + const mainEnv = builder.addEnv(DEFAULT_BASE_ENV_NAME, { + name: DEFAULT_BASE_ENV_NAME, + inherit: args.defaultBaseEnv && args.defaultBaseEnv != DEFAULT_BASE_ENV_NAME + ? args.defaultBaseEnv + : false, + desc: "the default default environment.", + }); -/* - * A nicer form of TaskFnDef for better ergonomics in the ghjkfile - */ -export type TaskDefNice = - & Omit - & Partial> - & Partial> - & { allowedPortDeps?: AllowedPortDep[]; installs?: InstallConfigFat[] }; -export function task(name: string, config: TaskDefNice) { - const allowedPortDeps = [ - ...(config.allowedPortDeps ?? (config.installs ? stdDeps() : [])), - ].map(registerAllowedPortDep); - - // TODO validate installs? - const installs = (config.installs ?? []).map(registerInstall); - - tasks[name] = { - name, - fn: config.fn, - desc: config.desc, - dependsOn: config.dependsOn ?? [], - env: { - installs, - env: config.env ?? {}, - allowedPortDeps, - }, - }; - return name; -} - -function addInstall( - cx: PortsModuleConfigBase, - configUnclean: InstallConfigFat, -) { - const res = portsValidators.installConfigFat.safeParse(configUnclean); - if (!res.success) { - throw new Error(`error parsing InstallConfig`, { - cause: { - config: configUnclean, - zodErr: res.error, - }, + if (args.defaultBaseEnv) { + builder.addEnv(args.defaultBaseEnv, { + name: args.defaultBaseEnv, + inherit: false, + installs: args.installs, }); + } else { + if (args.installs) { + mainEnv.install(...args.installs); + } } - const config = res.data; - logger().debug("install added", config); - cx.installs.push(registerInstall(config)); -} - -export function secureConfig( - config: PortsModuleSecureConfig, -) { - return config; -} - -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), - }); - }), + + // this replaces the allowedBuildDeps contents according to the + // args. Written to be called multilple times to allow + // replacement. + const replaceDefaultBuildDeps = (args: SecureConfigArgs) => { + // empty out the array first + defaultBuildDepsSet.length = 0; + defaultBuildDepsSet.push( + ...reduceAllowedDeps(args.allowedBuildDeps ?? []), + ); + const seenPorts = new Set( + defaultBuildDepsSet.map((dep) => dep.manifest.name), ); + // if the user explicitly passes a port config, we let + // it override any ports of the same kind from the std library + for ( + const dep of args.stdDeps !== false // i.e.e true if undefined + ? stdDeps({ enableRuntimes: args.enableRuntimes ?? false }) + : [] + ) { + if (seenPorts.has(dep.manifest.name)) { + continue; + } + defaultBuildDepsSet.push(dep); + } + // we override the allowedBuildDeps of the + // defaultEnvBase each time `file` or `env` are used + if (args.defaultBaseEnv) { + builder.addEnv(args.defaultBaseEnv, { + allowedBuildDeps: defaultBuildDepsSet, + }); + } else { + mainEnv.allowedBuildDeps(...defaultBuildDepsSet); + } + }; + + // populate the bulid deps by the default args first + replaceDefaultBuildDeps(args); + + for (const env of args.envs ?? []) { + builder.addEnv(env.name, env); } - return out; -} - -async function execTask( - name: string, - argv: string[], - envVars: Record, -) { - const task = tasks[name]; - if (!task) { - throw new Error(`no task defined under "${name}"`); + for (const [name, def] of Object.entries(args.tasks ?? {})) { + builder.addTask({ name, ...def, ty: "denoFile@v1" }); } - const custom$ = $.build$({ - commandBuilder: defaultCommandBuilder().env(envVars), + + // FIXME: ses.lockdown to freeze primoridials + // freeze the object to prevent malicious tampering of the secureConfig + const sophon = Object.freeze({ + getConfig: Object.freeze( + ( + ghjkfileUrl: string, + ) => { + return builder.toConfig({ + ghjkfileUrl, + defaultEnv: args.defaultEnv ?? DEFAULT_BASE_ENV_NAME, + defaultBaseEnv: args.defaultBaseEnv ?? + DEFAULT_BASE_ENV_NAME, + }); + }, + ), + execTask: Object.freeze( + // TODO: do we need to source the default base env from + // the secure config here? + (args: ExecTaskArgs) => builder.execTask(args), + ), }); - await task.fn({ argv, env: envVars, $: custom$ }); -} - -async function getConfig(secureConfig: PortsModuleSecureConfig | undefined) { - try { - const allowedDeps = Object.fromEntries([ - ...(secureConfig?.allowedPortDeps ?? stdDeps()) - .map((dep) => - [ - dep.manifest.name, - registerAllowedPortDep(portsValidators.allowedPortDep.parse(dep)), - ] as const - ), - ]); - const fullPortsConfig: PortsModuleConfig = { - installs: portsConfig.installs, - allowedDeps: allowedDeps, - }; - - // const cmdJsons = await Promise.all( - // Object.entries(tasks.comands).map( - // async ([name, cmd]) => [name, await zcli_json.zcliJson(tasksCli, cmd)], - // ), - // ); - const cmdJsons2 = await Promise.all( - Object.entries(tasks).map( - ([name, task]) => [name, { - ...task, - }], - ), - ); - const tasksConfig: TasksModuleConfig = { - tasks: Object.fromEntries( - cmdJsons2, - ), - }; - - const config: SerializedConfig = { - modules: [{ - id: std_modules.ports, - config: fullPortsConfig, - }, { - id: std_modules.tasks, - config: tasksConfig, - }], - globalEnv, - }; - return config; - } catch (cause) { - throw new Error(`error constructing config for serialization`, { cause }); - } -} + + // we return a bunch of functions here + // to ease configuring the main environment + // including overloads + return { + sophon, + + install(...configs: InstallConfigFat[]) { + mainEnv.install(...configs); + }, + + task( + nameOrArgsOrFn: string | DenoTaskDefArgs | TaskFn, + argsOrFn?: Omit | TaskFn, + argsMaybe?: Omit, + ) { + let args: DenoTaskDefArgs; + if (typeof nameOrArgsOrFn == "object") { + args = nameOrArgsOrFn; + } else if (typeof nameOrArgsOrFn == "function") { + args = { + ...(argsOrFn ?? {}), + fn: nameOrArgsOrFn, + }; + } else if (typeof argsOrFn == "object") { + args = { ...argsOrFn, name: nameOrArgsOrFn }; + } else if (argsOrFn) { + args = { + ...(argsMaybe ?? {}), + name: nameOrArgsOrFn, + fn: argsOrFn, + }; + } else { + args = { + name: nameOrArgsOrFn, + }; + } + return builder.addTask({ ...args, ty: "denoFile@v1" }); + }, + + env( + nameOrArgs: string | EnvDefArgs, + argsMaybe?: Omit, + ) { + const args = typeof nameOrArgs == "object" + ? nameOrArgs + : { ...argsMaybe, name: nameOrArgs }; + return builder.addEnv(args.name, args); + }, + + config( + newArgs: SecureConfigArgs, + ) { + if ( + newArgs.defaultBaseEnv !== undefined || + newArgs.enableRuntimes !== undefined || + newArgs.allowedBuildDeps !== undefined || + newArgs.stdDeps !== undefined + ) { + replaceDefaultBuildDeps(newArgs); + } + if ( + newArgs.defaultBaseEnv && + newArgs.defaultBaseEnv != DEFAULT_BASE_ENV_NAME + ) { + mainEnv.inherit(newArgs.defaultBaseEnv); + } + // NOTE:we're deep mutating the first args from above + args = { + ...newArgs, + }; + }, + }; +}); diff --git a/modules/envs/inter.ts b/modules/envs/inter.ts new file mode 100644 index 00000000..90f2597f --- /dev/null +++ b/modules/envs/inter.ts @@ -0,0 +1,26 @@ +import type { GhjkCtx } from "../types.ts"; +import type { EnvsCtx } from "./mod.ts"; + +export function getEnvsCtx( + gcx: GhjkCtx, +): EnvsCtx { + const key = "ctx.envs"; + let ctx = gcx.blackboard.get(key) as + | EnvsCtx + | undefined; + + if (!ctx) { + ctx = { + activeEnv: "", + keyToName: {}, + config: { + defaultEnv: "", + envs: {}, + envsNamed: {}, + }, + }; + gcx.blackboard.set(key, ctx); + } + + return ctx; +} diff --git a/modules/envs/mod.ts b/modules/envs/mod.ts new file mode 100644 index 00000000..6258f72b --- /dev/null +++ b/modules/envs/mod.ts @@ -0,0 +1,405 @@ +export * from "./types.ts"; + +import { cliffy_cmd, zod } from "../../deps/cli.ts"; +import { $, detectShellPath, Json, unwrapZodRes } from "../../utils/mod.ts"; +import validators from "./types.ts"; +import type { + EnvRecipeX, + EnvsModuleConfigX, + WellKnownProvision, +} from "./types.ts"; +import { type GhjkCtx, type ModuleManifest } from "../types.ts"; +import { ModuleBase } from "../mod.ts"; +import type { Blackboard } from "../../host/types.ts"; +import { cookPosixEnv } from "./posix.ts"; +import { getPortsCtx, installGraphToSetMeta } from "../ports/inter.ts"; +import type { + InstallSetProvision, + InstallSetRefProvision, +} from "../ports/types.ts"; +import { buildInstallGraph, syncCtxFromGhjk } from "../ports/sync.ts"; +import { getEnvsCtx } from "./inter.ts"; +import { getTasksCtx } from "../tasks/inter.ts"; + +export type EnvsCtx = { + activeEnv: string; + keyToName: Record; + config: EnvsModuleConfigX; +}; + +const lockValidator = zod.object({ + version: zod.string(), +}); + +type EnvsLockEnt = zod.infer; + +export class EnvsModule extends ModuleBase { + processManifest( + gcx: GhjkCtx, + manifest: ModuleManifest, + _bb: Blackboard, + _lockEnt: EnvsLockEnt | undefined, + ) { + function unwrapParseCurry(res: zod.SafeParseReturnType) { + return unwrapZodRes(res, { + id: manifest.id, + config: manifest.config, + }, "error parsing module config"); + } + const config = unwrapParseCurry( + validators.envsModuleConfig.safeParse(manifest.config), + ); + const setEnv = Deno.env.get("GHJK_ENV"); + const activeEnv = setEnv && setEnv != "" ? setEnv : config.defaultEnv; + + const envsCtx = getEnvsCtx(gcx); + envsCtx.activeEnv = activeEnv; + envsCtx.config = config; + for (const [name, key] of Object.entries(config.envsNamed)) { + envsCtx.keyToName[key] = [name, ...(envsCtx.keyToName[key] ?? [])]; + } + + return Promise.resolve(envsCtx); + } + + commands( + gcx: GhjkCtx, + ecx: EnvsCtx, + ) { + function envKeyArgs( + args: { + taskKeyMaybe?: string; + envKeyMaybe?: string; + }, + ) { + const { envKeyMaybe, taskKeyMaybe } = args; + if (taskKeyMaybe && envKeyMaybe) { + throw new Error( + "--task-env option can not be combined with [envName] argument", + ); + } + if (taskKeyMaybe) { + const tasksCx = getTasksCtx(gcx); + const taskDef = tasksCx.config.tasks[taskKeyMaybe]; + if (!taskDef) { + throw new Error(`no task found under key "${taskKeyMaybe}"`); + } + return { envKey: taskDef.envKey }; + } + const actualKey = ecx.config.envsNamed[envKeyMaybe ?? ecx.activeEnv]; + return actualKey + ? { envKey: actualKey, envName: envKeyMaybe ?? ecx.activeEnv } + : { envKey: envKeyMaybe ?? ecx.activeEnv }; + } + + 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.envsNamed) + // envs that have names which start with underscors + // don't show up in the cli list + .filter(([key]) => !key.startsWith("_")) + .map(([name, hash]) => { + const { desc } = ecx.config.envs[hash]; + return `${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( + "-t, --task-env ", + "Synchronize to the environment used by the named task", + { standalone: true }, + ) + .action(async function ({ taskEnv }, envKeyMaybe) { + const { envKey } = envKeyArgs({ + taskKeyMaybe: taskEnv, + envKeyMaybe, + }); + await activateEnv(envKey); + }), + ) + .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]") + .option( + "-t, --task-env ", + "Synchronize to the environment used by the named task", + { standalone: true }, + ) + .action(async function ({ taskEnv }, envKeyMaybe) { + const { envKey, envName } = envKeyArgs({ + taskKeyMaybe: taskEnv, + envKeyMaybe, + }); + await reduceAndCookEnv(gcx, ecx, envKey, envName ?? envKey); + }), + ) + .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]") + .option( + "-t, --task-env ", + "Synchronize to the environment used by the named task", + { standalone: true }, + ) + .action(async function ({ taskEnv }, envKeyMaybe) { + const { envKey } = envKeyArgs({ + taskKeyMaybe: taskEnv, + envKeyMaybe, + }); + const env = ecx.config.envs[envKey]; + if (!env) { + throw new Error(`no env found under "${envKeyMaybe}"`); + } + // deno-lint-ignore no-console + console.log($.inspect(await showableEnv(gcx, env, envKey))); + }), + ), + sync: new cliffy_cmd.Command() + .description(`Synchronize your shell to what's in your config. + +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( + "-t, --task-env ", + "Synchronize to the environment used by the named task", + { standalone: true }, + ) + .action(async function ({ taskEnv }, envKeyMaybe) { + const { envKey, envName } = envKeyArgs({ + taskKeyMaybe: taskEnv, + envKeyMaybe, + }); + await reduceAndCookEnv( + gcx, + ecx, + envKey, + envName ?? envKey, + ); + await activateEnv(envKey); + }), + }; + } + + loadLockEntry( + _gcx: GhjkCtx, + raw: Json, + ) { + const entry = lockValidator.parse(raw); + + if (entry.version != "0") { + throw new Error(`unexepected version tag deserializing lockEntry`); + } + + return entry; + } + genLockEntry( + _gcx: GhjkCtx, + _tcx: EnvsCtx, + ) { + return { + version: "0", + }; + } +} + +async function reduceAndCookEnv( + gcx: GhjkCtx, + ecx: EnvsCtx, + envKey: string, + envName: string, +) { + const recipe = ecx.config.envs[envKey]; + if (!recipe) { + throw new Error(`No env found under given name "${envKey}"`); + } + + // TODO: diff env and ask confirmation from user + const envDir = $.path(gcx.ghjkDir).join("envs", envKey); + /* + 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, + envKey: envName, + envDir: envDir.toString(), + createShellLoaders: true, + }); + if (envKey == ecx.config.defaultEnv) { + const defaultEnvDir = $.path(gcx.ghjkDir).join("envs", "default"); + await $.removeIfExists(defaultEnvDir); + await defaultEnvDir.symlinkTo(envDir, { kind: "relative" }); + } + await $.co( + Object + .entries(ecx.config.envsNamed) + .map(async ([name, key]) => { + if (key == envKey) { + const namedDir = $.path(gcx.ghjkDir).join("envs", name); + await $.removeIfExists(namedDir); + await namedDir.symlinkTo(envDir, { kind: "relative" }); + } + if (name == ecx.config.defaultEnv || key == ecx.config.defaultEnv) { + const defaultEnvDir = $.path(gcx.ghjkDir).join("envs", "default"); + await $.removeIfExists(defaultEnvDir); + await defaultEnvDir.symlinkTo(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 portsCx = getPortsCtx(gcx); + const set = portsCx.config.sets[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, + }; +} + +async function activateEnv(envKey: string) { + const nextfile = Deno.env.get("GHJK_NEXTFILE"); + if (nextfile) { + await $.path(nextfile).writeText(envKey); + } else { + const shell = await detectShellPath(); + if (!shell) { + throw new Error( + "unable to detct shell in use. Use `--shell` flag to explicitly pass shell program.", + ); + } + // 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: envKey }); + } +} diff --git a/modules/envs/posix.ts b/modules/envs/posix.ts new file mode 100644 index 00000000..848cdad3 --- /dev/null +++ b/modules/envs/posix.ts @@ -0,0 +1,382 @@ +import { std_fs, std_path } from "../../deps/cli.ts"; +import { + type EnvRecipeX, + WellKnownProvision, + wellKnownProvisionTypes, +} from "./types.ts"; +import { $, Path } from "../../utils/mod.ts"; +import type { GhjkCtx } from "../types.ts"; +import { reduceStrangeProvisions } from "./reducer.ts"; +import { ghjk_fish, ghjk_sh } from "../../install/utils.ts"; +import getLogger from "../../utils/logger.ts"; + +const logger = getLogger(import.meta); + +export async function cookPosixEnv( + { gcx, recipe, envKey, envDir, createShellLoaders = false }: { + gcx: GhjkCtx; + recipe: EnvRecipeX; + envKey: string; + envDir: string; + createShellLoaders?: boolean; + }, +) { + logger.debug("cooking env", envKey, { envDir }); + const reducedRecipe = await reduceStrangeProvisions(gcx, recipe); + await $.removeIfExists(envDir); + // create the shims for the user's environment + const shimDir = $.path(envDir).join("shims"); + + 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 binPaths = [] as string[]; + const libPaths = [] as string[]; + const includePaths = [] as string[]; + const vars = { + GHJK_ENV: envKey, + } as Record; + const onEnterHooks = [] as [string, string[]][]; + const onExitHooks = [] as [string, string[]][]; + // FIXME: detect shim conflicts + // FIXME: better support for multi installs + + await Promise.all(reducedRecipe.provides.map((item) => { + if (!wellKnownProvisionTypes.includes(item.ty)) { + return Promise.resolve(); + } + + const wellKnownProv = item as WellKnownProvision; + switch (wellKnownProv.ty) { + case "posix.exec": + binPaths.push(wellKnownProv.absolutePath); + break; + case "posix.sharedLib": + libPaths.push(wellKnownProv.absolutePath); + break; + case "posix.headerFile": + includePaths.push(wellKnownProv.absolutePath); + break; + case "posix.envVar": + if (vars[wellKnownProv.key]) { + throw new Error( + `env var conflict cooking unix env: key "${wellKnownProv.key}" has entries "${ + vars[wellKnownProv.key] + }" and "${wellKnownProv.val}"`, + ); + } + vars[wellKnownProv.key] = wellKnownProv.val; + // installSetIds.push(wellKnownProv.installSetIdProvision!.id); + break; + case "hook.onEnter.posixExec": + onEnterHooks.push([wellKnownProv.program, wellKnownProv.arguments]); + break; + case "hook.onExit.posixExec": + onExitHooks.push([wellKnownProv.program, wellKnownProv.arguments]); + break; + case "ghjk.ports.Install": + // do nothing + break; + default: + throw Error( + `unsupported provision type: ${(wellKnownProv as any).provision}`, + ); + } + })); + void await Promise.all([ + // bin shims + await shimLinkPaths( + binPaths, + binShimDir, + ), + // lib shims + await shimLinkPaths( + libPaths, + libShimDir, + ), + // include shims + await shimLinkPaths( + includePaths, + includeShimDir, + ), + $.path(envDir).join("recipe.json").writeJsonPretty(reducedRecipe), + ]); + // 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`, + }; + if (createShellLoaders) { + // write loader for the env vars mandated by the installs + await writeActivators( + gcx, + envDir, + vars, + pathVars, + onEnterHooks, + onExitHooks, + ); + } + return { + env: { + ...vars, + ...pathVars, + }, + }; +} + +/// This expands globs found in the targetPaths +async function shimLinkPaths( + targetPaths: string[], + shimDir: Path, +) { + // map of filename to shimPath + const shims: Record = {}; + // a work sack to append to incase there are globs expanded + const foundTargetPaths = [...targetPaths]; + while (foundTargetPaths.length > 0) { + const file = foundTargetPaths.pop()!; + if (std_path.isGlob(file)) { + foundTargetPaths.push( + ...(await Array.fromAsync(std_fs.expandGlob(file))) + .map((entry) => entry.path), + ); + continue; + } + const filePath = $.path(file); + const fileName = filePath.basename(); + const shimPath = shimDir.resolve(fileName); + + if (shims[fileName]) { + throw new Error( + `duplicate shim found when adding shim for file: "${fileName}"`, + ); + } + try { + await $.path(shimPath).remove(); + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } + await shimPath.symlinkTo(filePath, { kind: "absolute" }); + shims[fileName] = shimPath.toString(); + } + return shims; +} + +/** + * Create the activate scripts. + * + * Activate scripts are responsible for: + * - exporting different environment variables from the ports + * - mainpulating the path strings + * - running the environment hooks + */ +async function writeActivators( + gcx: GhjkCtx, + envDir: string, + envVars: Record, + pathVars: Record, + onEnterHooks: [string, string[]][], + onExitHooks: [string, string[]][], +) { + const ghjkDirVar = "_ghjk_dir"; + const shareDirVar = "_ghjk_share_dir"; + pathVars = { + ...Object.fromEntries( + Object.entries(pathVars).map(( + [key, val], + ) => [ + key, + val + .replace(gcx.ghjkDir.toString(), "$" + ghjkDirVar) + .replace(gcx.ghjkShareDir.toString(), "$" + shareDirVar), + ]), + ), + }; + + const ghjkShimName = "__ghjk_shim"; + const onEnterHooksEscaped = onEnterHooks.map(([cmd, args]) => + [cmd == "ghjk" ? ghjkShimName : cmd, ...args] + .join(" ") + .replaceAll("'", "'\\''") + ); + const onExitHooksEscaped = onExitHooks.map(([cmd, args]) => + [cmd == "ghjk" ? ghjkShimName : cmd, ...args] + .join(" ").replaceAll("'", "'\\''") + ); + + // ghjk.sh sets the DENO_DIR so we can usually + // assume it's set + const denoDir = Deno.env.get("DENO_DIR") ?? ""; + const scripts = { + // + // posix shell version + posix: [ + `# shellcheck shell=sh`, + `# shellcheck disable=SC2016`, + `# SC2016: disabled because single quoted expressions are used for the cleanup scripts`, + ``, + `if [ -n "$\{GHJK_CLEANUP_POSIX+x}" ]; then`, + ` eval "$GHJK_CLEANUP_POSIX"`, + `fi`, + `export GHJK_CLEANUP_POSIX="";`, + ``, + `# the following variables are used to make the script more human readable`, + `${ghjkDirVar}="${gcx.ghjkDir.toString()}"`, + `${shareDirVar}="${gcx.ghjkShareDir.toString()}"`, + ``, + `# env vars`, + ...Object.entries(envVars).flatMap(([key, val]) => { + const safeVal = val.replaceAll("\\", "\\\\").replaceAll("'", "'\\''"); + // avoid triggering unbound variable if -e is set + // by defaulting to a value that's guranteed to + // be differeint than `key` + // TODO: avoid invalid key values elsewhere + const safeComparisionKey = `$\{${key}:-_${ + val.replace(/['"]/g, "").slice(0, 2) + }}`; + return [ + // we only restore the old $KEY value at cleanup if value of $KEY + // is the one set by the activate script + // we also single quote the supplied values to avoid + // any embedded expansion/execution + // we also single quote the entire test section to avoid + // expansion when creating the cleanup + // string (that's why we "escaped single quote" the value) + // NOTE: the addition sign at the end + `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'[ \"${safeComparisionKey}\" = '\\''${safeVal}'\\'' ] && '` + + // we want to capture the old $key value here so we wrap those + // with double quotes but the rest is in single quotes + // within the value of $key + // i.e. export KEY='OLD $VALUE OF KEY' + // but $VALUE won't be expanded when the cleanup actually runs + // we also unset the key if it wasn't previously set + `$([ -z "$\{${key}+x}" ] && echo 'export ${key}= '\\'"$\{${key}:-unreachable}""';" || echo 'unset ${key};');`, + `export ${key}='${safeVal}';`, + ``, + ]; + }), + ``, + `# path vars`, + ...Object.entries(pathVars).flatMap(([key, val]) => { + const safeVal = val.replaceAll("\\", "\\\\").replaceAll("'", "'\\''"); + return [ + // double quote the path vars for expansion + // single quote GHJK_CLEANUP additions to avoid expansion/exec before eval + `GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'${key}=$(echo "$${key}" | tr ":" "\\n" | grep -vE '\\'"^${safeVal}"\\'' | tr "\\n" ":");${key}="\${${key}%:}";';`, + // FIXME: we're allowing expansion in the value to allow + // readable $ghjkDirVar usage + // (for now safe since all paths are created within ghjk) + `export ${key}="${safeVal}:$${key}";`, + ``, + ]; + }), + ``, + `# hooks that want to invoke ghjk are made to rely`, + `# on this shim to improving latency`, + // the ghjk executable is itself a shell script + // which execs deno, we remove the middleman here + // also, the ghjk executable is optional + ghjk_sh(gcx, denoDir, ghjkShimName), + ``, + `# only run the hooks in interactive mode`, + `case "$-" in`, + ` *i*) # if the shell variables contain "i"`, + ``, + ` # on enter hooks`, + ...onEnterHooksEscaped.map((line) => ` ${line}`), + ``, + ` # on exit hooks`, + ...onExitHooksEscaped.map( + (cmd) => ` GHJK_CLEANUP_POSIX=$GHJK_CLEANUP_POSIX'${cmd};';`, + ), + ` :`, + ` ;;`, + ` *)`, + ` :`, + ` ;;`, + `esac`, + ``, + ], + // + // fish version + fish: [ + `if set --query GHJK_CLEANUP_FISH`, + ` eval $GHJK_CLEANUP_FISH`, + ` set --erase GHJK_CLEANUP_FISH`, + `end`, + ``, + `# the following variables are used to make the script more human readable`, + `set ${ghjkDirVar} "${gcx.ghjkDir.toString()}"`, + `set ${shareDirVar} "${gcx.ghjkShareDir.toString()}"`, + ``, + `# env vars`, + ...Object.entries(envVars).flatMap(([key, val]) => { + const safeVal = val.replaceAll("\\", "\\\\").replaceAll("'", "\\'"); + // read the comments from the posix version of this section + // the fish version is notably simpler since + // - we can escape single quates within single quotes + // - we don't have to deal with 'set -o nounset' + return [ + `set --global --append GHJK_CLEANUP_FISH 'test "$${key}" = \\'${safeVal}\\'; and '` + + `(if set -q ${key}; echo 'set --global --export ${key} \\'' "$${key}" "';"; else; echo 'set -e ${key};'; end;);`, + `set --global --export ${key} '${val}';`, + ``, + ]; + }), + ``, + `# path vars`, + ...Object.entries(pathVars).flatMap(([key, val]) => { + const safeVal = val.replaceAll("\\", "\\\\").replaceAll("'", "\\'"); + return [ + `set --global --append GHJK_CLEANUP_FISH 'set --global --export --path ${key} (string match --invert --regex \\''"^${safeVal}"'\\' $${key});';`, + `set --global --export --prepend ${key} "${safeVal}";`, + ``, + ]; + }), + ``, + `# hooks that want to invoke ghjk are made to rely`, + `# on this shim to improving latency`, + ghjk_fish(gcx, denoDir, ghjkShimName), + ``, + `# only run the hooks in interactive mode`, + `if status is-interactive;`, + ` # on enter hooks`, + ...onEnterHooksEscaped.map((line) => ` ${line}`), + , + ``, + ` # on exit hooks`, + ...onExitHooksEscaped.map((cmd) => + ` set --global --append GHJK_CLEANUP_FISH '${cmd};';` + ), + `end`, + ], + }; + + const envPathR = await $.path(envDir).ensureDir(); + await Promise.all([ + envPathR.join(`activate.fish`).writeText(scripts.fish.join("\n")), + envPathR.join(`activate.sh`).writeText(scripts.posix.join("\n")), + ]); +} diff --git a/modules/envs/reducer.ts b/modules/envs/reducer.ts new file mode 100644 index 00000000..ff402a3d --- /dev/null +++ b/modules/envs/reducer.ts @@ -0,0 +1,87 @@ +import { unwrapZodRes } from "../../port.ts"; +import type { GhjkCtx } from "../types.ts"; +import type { + EnvRecipeX, + Provision, + ProvisionReducer, + WellKnownEnvRecipeX, + WellKnownProvision, +} from "./types.ts"; +import { wellKnownProvisionTypes } from "./types.ts"; +import validators from "./types.ts"; + +export type ProvisionReducerStore = Map< + string, + ProvisionReducer +>; + +/** + * 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, +) { + const id = "provisionReducerStore"; + let store = gcx.blackboard.get(id) as + | ProvisionReducerStore + | undefined; + if (!store) { + store = new Map(); + gcx.blackboard.set(id, store); + } + return store; +} + +/** + * Looks at each provision in the recipe and if it's not a type of + * {@link WellKnownProvision}, looks for reducers in + * {@link ProvisionReducer} to convert it to one. + */ +export async function reduceStrangeProvisions( + gcx: GhjkCtx, + env: EnvRecipeX, +) { + const reducerStore = getProvisionReducerStore(gcx); + // Replace by `Object.groupBy` once the types for it are fixed + const bins = {} as Record; + for (const item of env.provides) { + let bin = bins[item.ty]; + if (!bin) { + bin = []; + bins[item.ty] = bin; + } + bin.push(item); + } + const reducedSet = [] as WellKnownProvision[]; + for (const [ty, items] of Object.entries(bins)) { + if (wellKnownProvisionTypes.includes(ty as any)) { + reducedSet.push( + ...items.map((item) => validators.wellKnownProvision.parse(item)), + ); + continue; + } + const reducer = reducerStore.get(ty); + if (!reducer) { + throw new Error(`no provider reducer found for ty: ${ty}`, { + cause: items, + }); + } + const reduced = await reducer(items); + reducedSet.push( + ...reduced.map((prov) => + unwrapZodRes( + validators.wellKnownProvision.safeParse(prov), + { prov }, + `error parsing reduced provision`, + ) + ), + ); + } + const out: WellKnownEnvRecipeX = { + ...env, + provides: reducedSet, + }; + return out; +} diff --git a/modules/envs/types.ts b/modules/envs/types.ts new file mode 100644 index 00000000..adc0cda7 --- /dev/null +++ b/modules/envs/types.ts @@ -0,0 +1,113 @@ +import { std_path, zod } from "../../deps/common.ts"; +import { installProvisionTy } from "../ports/types.ts"; +import moduleValidators from "../types.ts"; + +const absolutePath = zod.string().refine((path) => std_path.isAbsolute(path)); + +const provision = zod.object({ ty: zod.string() }).passthrough(); + +const posixFileProvisionTypes = [ + "posix.exec", + "posix.sharedLib", + "posix.headerFile", +] as const; + +export const hookProvisionTypes = [ + "hook.onEnter.posixExec", + "hook.onExit.posixExec", +] as const; + +export const installProvisionTypes = [ + installProvisionTy, +] as const; + +// we separate the posix file types in a separate +// array in the interest of type inference +export const wellKnownProvisionTypes = [ + "posix.envVar", + ...posixFileProvisionTypes, + ...hookProvisionTypes, + ...installProvisionTypes, +] as const; + +const wellKnownProvision = zod.discriminatedUnion( + "ty", + [ + zod.object({ + ty: zod.literal(wellKnownProvisionTypes[0]), + key: moduleValidators.envVarName, + val: zod.string(), + }), + ...hookProvisionTypes.map((ty) => + zod.object({ + ty: zod.literal(ty), + program: zod.string(), + arguments: zod.string().array(), + }) + ), + ...posixFileProvisionTypes.map((ty) => + zod.object({ ty: zod.literal(ty), absolutePath }) + ), + ...installProvisionTypes.map( + (ty) => + zod.object( + { + ty: zod.literal(ty), + instId: zod.string(), + }, + ), + ), + ], +); + +const envRecipe = zod.object({ + desc: zod.string().nullish(), + provides: zod.array(provision), +}); + +const wellKnownEnvRecipe = envRecipe.merge(zod.object({ + provides: zod.array(wellKnownProvision), +})); + +const envsModuleConfig = zod.object({ + defaultEnv: zod.string(), + envs: zod.record(zod.string(), envRecipe), + // TODO: regex for env and task names + envsNamed: zod.record(zod.string(), zod.string()), +}).refine((conf) => conf.envsNamed[conf.defaultEnv], { + message: `no env found under the provided "defaultEnv"`, +}); + +const validators = { + provision, + wellKnownProvision, + envRecipe, + envsModuleConfig, + wellKnownEnvRecipe, +}; +export default validators; + +export type EnvsModuleConfig = zod.input; +export type EnvsModuleConfigX = zod.infer; + +export type Provision = zod.input; +export type WellKnownProvision = zod.input< + typeof validators.wellKnownProvision +>; + +export type EnvRecipe = zod.input; +export type EnvRecipeX = zod.infer; + +export type WellKnownEnvRecipe = zod.input< + typeof validators.wellKnownEnvRecipe +>; +export type WellKnownEnvRecipeX = zod.infer< + typeof validators.wellKnownEnvRecipe +>; + +/* + * A function that batch convert strange provisions of a certain kind to well known ones. + */ +export type ProvisionReducer

= ( + provisions: P[], +) => Promise; diff --git a/modules/mod.ts b/modules/mod.ts index 86397f43..daa35fa1 100644 --- a/modules/mod.ts +++ b/modules/mod.ts @@ -1,26 +1,29 @@ import { cliffy_cmd } from "../deps/cli.ts"; -import { GlobalEnv } from "../host/types.ts"; +import { Blackboard } from "../host/types.ts"; import type { Json } from "../utils/mod.ts"; import type { GhjkCtx, ModuleManifest } from "./types.ts"; export abstract class ModuleBase { + /* init( + _gcx: GhjkCtx, + ): Promise | void {} */ abstract processManifest( - ctx: GhjkCtx, + gcx: GhjkCtx, manifest: ModuleManifest, + bb: Blackboard, lockEnt: LockEnt | undefined, - env: GlobalEnv, ): 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/db.ts b/modules/ports/db.ts index d626830e..552400e6 100644 --- a/modules/ports/db.ts +++ b/modules/ports/db.ts @@ -1,21 +1,29 @@ // Deno.Kv api is unstable /// -import type { - DownloadArtifacts, - InstallArtifacts, - InstallConfigLite, - PortManifestX, -} from "./types.ts"; - -export type InstallRow = { - installId: string; - conf: InstallConfigLite; - manifest: PortManifestX; - installArts?: InstallArtifacts; - downloadArts: DownloadArtifacts; - progress: "downloaded" | "installed"; -}; +import { zod } from "../../deps/common.ts"; +// import type { PathRef } from "../../utils/mod.ts"; +// import { $ } from "../../utils/mod.ts"; +import validators from "./types.ts"; +// import getLogger from "../../utils/logger.ts"; + +// const logger = getLogger(import.meta); + +// NOTE: make sure any changes to here are backwards compatible +const installRowValidator = zod.object({ + // version: zod.string(), + installId: zod.string(), + conf: validators.installConfigResolved, + manifest: validators.portManifest, + installArts: validators.installArtifacts.nullish(), + downloadArts: validators.downloadArtifacts, + progress: zod.enum(["downloaded", "installed"]), +}).passthrough(); + +type InstallRowVersioned = zod.infer; +// FIXME: this breaks typescript +// export type InstallRow = Omit; +export type InstallRow = InstallRowVersioned; export abstract class InstallsDb { abstract all(): Promise; @@ -61,3 +69,51 @@ class DenoKvInstallsDb extends InstallsDb { } } } + +/* // TODO: implement me + +class InlineInstallsDb extends InstallsDb { + #map = new Map(); + #dbDir: PathRef; + constructor( + dbDir: string, + ) { + super(); + this.#dbDir = $.path(dbDir); + } + all(): Promise { + throw new Error("Method not implemented."); + } + async get(id: string): Promise { + let row = this.#map.get(id); + if (!row) { + const raw = await this.#dbDir.join(`${id}.meta`).readMaybeText(); + if (raw) { + try { + const rawParsed = installRowValidator.parse(JSON.parse(raw)); + if (rowParsed.version != "0") { + throw new Error(`unexpected version string: ${rowParsed.version}`); + } + row = rowParsed; + this.#map.set(id, row); + } catch (err) { + logger.warn(`error parsing install meta for "${id}"`, err); + } + } + } + return row; + } + set(id: string, row: InstallRow): Promise { + const versioned = { ...row, version: "0" }; + await this.#dbDir.join(`${id}.meta`).writeJsonPretty(versioned); + this.#map.set(id, versioned); + throw new Error("Method not implemented."); + } + delete(id: string): Promise { + this.#map.delete(id); + throw new Error("Method not implemented."); + } + [Symbol.dispose](): void { + throw new Error("Method not implemented."); + } +} */ diff --git a/modules/ports/ghrel.ts b/modules/ports/ghrel.ts index 6ada10a9..a15a6e55 100644 --- a/modules/ports/ghrel.ts +++ b/modules/ports/ghrel.ts @@ -1,9 +1,4 @@ -import { - $, - downloadFile, - DownloadFileArgs, - exponentialBackoff, -} from "../../utils/mod.ts"; +import { $, downloadFile, DownloadFileArgs } from "../../utils/mod.ts"; import { zod } from "../../deps/common.ts"; import { PortBase } from "./base.ts"; import type { DownloadArgs, ListAllArgs } from "./types.ts"; @@ -22,7 +17,7 @@ export function readGhVars() { const out: GithubReleasesInstConf = { ghToken, }; - return out; + return ghToken ? out : {}; } export function ghHeaders(conf: Record) { @@ -73,7 +68,7 @@ export abstract class GithubReleasePort extends PortBase { async latestStable(args: ListAllArgs) { const metadata = await $.withRetries({ count: 10, - delay: exponentialBackoff(1000), + delay: $.exponentialBackoff(1000), action: async () => await $.request( `https://api.github.com/repos/${this.repoOwner}/${this.repoName}/releases/latest`, @@ -88,7 +83,7 @@ export abstract class GithubReleasePort extends PortBase { async listAll(args: ListAllArgs) { const metadata = await $.withRetries({ count: 10, - delay: exponentialBackoff(1000), + delay: $.exponentialBackoff(1000), action: async () => await $.request( `https://api.github.com/repos/${this.repoOwner}/${this.repoName}/releases`, diff --git a/modules/ports/inter.ts b/modules/ports/inter.ts new file mode 100644 index 00000000..6f9c7efb --- /dev/null +++ b/modules/ports/inter.ts @@ -0,0 +1,43 @@ +import type { GhjkCtx } from "../types.ts"; +import type { PortsCtx } from "./mod.ts"; +import type { InstallGraph } from "./sync.ts"; // TODO: rename to install.ts + +export function getPortsCtx( + gcx: GhjkCtx, +) { + const id = "ctx.ports"; + let ctx = gcx.blackboard.get(id) as + | PortsCtx + | undefined; + if (!ctx) { + ctx = { config: { sets: {} } }; + gcx.blackboard.set(id, ctx); + } + return ctx; +} + +/** + * 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 d269f516..e532caaf 100644 --- a/modules/ports/mod.ts +++ b/modules/ports/mod.ts @@ -1,24 +1,43 @@ export * from "./types.ts"; -import { cliffy_cmd, zod } from "../../deps/cli.ts"; -import { $, Json } from "../../utils/mod.ts"; +import { cliffy_cmd, Table, zod } from "../../deps/cli.ts"; +import { $, Json, unwrapZodRes } from "../../utils/mod.ts"; import logger from "../../utils/logger.ts"; -import validators from "./types.ts"; -import type { PortsModuleConfigX } from "./types.ts"; -import type { GhjkCtx, ModuleManifest } from "../types.ts"; +import validators, { + installProvisionTy, + installSetProvisionTy, + installSetRefProvisionTy, +} from "./types.ts"; +import envsValidators from "../envs/types.ts"; +import type { + AllowedPortDep, + InstallConfigResolved, + InstallProvision, + InstallSetRefProvision, + InstallSetX, + PortsModuleConfigX, +} from "./types.ts"; +import { type GhjkCtx, type ModuleManifest } from "../types.ts"; import { ModuleBase } from "../mod.ts"; import { buildInstallGraph, + getDepConfig, + getPortImpl, getResolutionMemo, - installFromGraphAndShimEnv, - type InstallGraph, + getShimmedDepArts, + resolveAndInstall, syncCtxFromGhjk, -} from "./sync.ts"; -import { GlobalEnv } from "../../host/types.ts"; +} 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 { getPortsCtx } from "./inter.ts"; +import { updateInstall } from "./utils.ts"; +import { getEnvsCtx } from "../envs/inter.ts"; -type PortsCtx = { +export type PortsCtx = { config: PortsModuleConfigX; - installGraph: InstallGraph; }; const lockValidator = zod.object({ @@ -31,74 +50,224 @@ const lockValidator = zod.object({ type PortsLockEnt = zod.infer; export class PortsModule extends ModuleBase { - async processManifest( + processManifest( gcx: GhjkCtx, manifest: ModuleManifest, + bb: Blackboard, _lockEnt: PortsLockEnt | undefined, - env: GlobalEnv, ) { - const res = validators.portsModuleConfig.safeParse(manifest.config); - if (!res.success) { - throw new Error("error parsing module config", { - cause: { - config: manifest.config, - zodErr: res.error, - }, - }); + function unwrapParseCurry(res: zod.SafeParseReturnType) { + return unwrapZodRes(res, { + id: manifest.id, + config: manifest.config, + }, "error parsing module config"); } - const config: PortsModuleConfigX = { - installs: res.data.installs.map((hash) => env.installs[hash]), - allowedDeps: Object.fromEntries( - Object.entries(res.data.allowedDeps).map(( - [key, value], - ) => [key, env.allowedPortDeps[value]]), - ), - }; - await using syncCx = await syncCtxFromGhjk(gcx); - const installGraph = await buildInstallGraph(syncCx, config); - return { config, installGraph }; + const hashedModConf = unwrapParseCurry( + validators.portsModuleConfigHashed.safeParse(manifest.config), + ); + + const pcx: PortsCtx = getPortsCtx(gcx); + + // pre-process the install sets found in the config + 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.allowedBuildDeps], + ), + ); + const allowedBuildDeps = Object.fromEntries( + Object.entries(allowedDepSetHashed).map(( + [key, hash], + ) => [ + key, + unwrapParseCurry(validators.allowedPortDep.safeParse(bb[hash])), + ]), + ); + const set: InstallSetX = { + installs, + allowedBuildDeps, + }; + pcx.config.sets[id] = set; + } + + // register envrionment reducers for any + // environemnts making use of install sets + const reducerStore = getProvisionReducerStore(gcx); + reducerStore.set( + installSetRefProvisionTy, + installSetRefReducer(gcx, pcx) as ProvisionReducer, + ); + reducerStore.set( + installSetProvisionTy, + installSetReducer(gcx) as ProvisionReducer, + ); + + return pcx; } - command( + 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( - "sync", - new cliffy_cmd.Command().description("Syncs the environment.") - .action(async () => { - logger().debug("syncing ports"); - await using syncCx = await syncCtxFromGhjk(gcx); - void await installFromGraphAndShimEnv( - syncCx, - $.path(gcx.ghjkDir).join("envs", "default").toString(), - pcx.installGraph, - ); - }), - ) - .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("Show a version table for installs") + .option( + "-u, --update-install ", + "Update specific install", + ) + .option("-n, --update-all", "Update all installs") + .action(async (opts) => { + const envsCtx = getEnvsCtx(gcx); + const envName = envsCtx.activeEnv; + + const installSets = pcx.config.sets; + + let currInstallSetId; + { + const activeEnvName = envsCtx.activeEnv; + const activeEnv = envsCtx.config + .envs[ + envsCtx.config.envsNamed[activeEnvName] ?? activeEnvName + ]; + if (!activeEnv) { + throw new Error( + `No env found under given name "${activeEnvName}"`, + ); + } + + const instSetRef = activeEnv.provides.filter((prov) => + prov.ty === installSetRefProvisionTy + )[0] as InstallSetRefProvision; + + currInstallSetId = instSetRef.setId; + } + const currInstallSet = installSets[currInstallSetId]; + const allowedDeps = currInstallSet.allowedBuildDeps; + + const rows = []; + const { + installedPortsVersions: installed, + latestPortsVersions: latest, + installConfigs, + } = await getOldNewVersionComparison( + gcx, + envName, + allowedDeps, + ); + for (let [installId, installedVersion] of installed.entries()) { + let latestVersion = latest.get(installId); + if (!latestVersion) { + throw new Error( + `Couldn't find the latest version for install id: ${installId}`, + ); + } + + if (latestVersion[0] === "v") { + latestVersion = latestVersion.slice(1); + } + if (installedVersion[0] === "v") { + installedVersion = installedVersion.slice(1); + } + + const config = installConfigs.get(installId); + + if (!config) { + throw new Error( + `Config not found for install id: ${installId}`, + ); + } + + if (config["specifiedVersion"]) { + latestVersion = "=" + latestVersion; + } + + const presentableConfig = { ...config }; + ["buildDepConfigs", "version", "specifiedVersion"].map( + (key) => { + delete presentableConfig[key]; + }, + ); + const row = [ + $.inspect(presentableConfig), + installedVersion, + latestVersion, + ]; + rows.push(row); + } + + if (opts.updateInstall) { + const installName = opts.updateInstall; + // TODO: convert from install name to install id, after port module refactor + let installId!: string; + const newVersion = latest.get(installId); + if (!newVersion) { + logger().info( + `Error while fetching the latest version for: ${installName}`, + ); + return; + } + await updateInstall(gcx, installId, newVersion, allowedDeps); + return; + } + + if (opts.updateAll) { + for (const [installId, newVersion] of latest.entries()) { + await updateInstall(gcx, installId, newVersion, allowedDeps); + } + return; + } + + const _versionTable = new Table() + .header(["Install Config", "Old Version", "New Version"]) + .body(rows) + .border() + .padding(1) + .indent(2) + .maxColWidth(30) + .render(); + }), + ) + .command( + "cleanup", + new cliffy_cmd.Command() + .description("TODO") + .action(function () { + throw new Error("TODO"); + }), + ), + }; } loadLockEntry( gcx: GhjkCtx, @@ -111,7 +280,11 @@ export class PortsModule extends ModuleBase { } const memoStore = getResolutionMemo(gcx); for (const [hash, config] of Object.entries(entry.configResolutions)) { - logger().debug("restoring resolution from lockfile", config); + logger().debug( + "restoring resolution from lockfile", + config.portRef, + config.version, + ); memoStore.set(hash, Promise.resolve(config)); } @@ -134,3 +307,108 @@ export class PortsModule extends ModuleBase { }; } } + +async function getOldNewVersionComparison( + gcx: GhjkCtx, + envName: string, + allowedDeps: Record, +) { + await using scx = await syncCtxFromGhjk(gcx); + + const envDir = $.path(gcx.ghjkDir).join("envs", envName); + const recipePath = envDir.join("recipe.json").toString(); + + // read from `recipe.json` and get installSetIds + const recipeJson = JSON.parse(await Deno.readTextFile(recipePath)); + const reducedRecipe = unwrapZodRes( + envsValidators.envRecipe.safeParse(recipeJson), + { + envName, + recipePath, + }, + "error parsing recipe.json", + ); + + const installProvisions = reducedRecipe.provides.filter((prov) => + prov.ty === installProvisionTy + ) as InstallProvision[]; + + const db = scx.db.val; + + const installedPortsVersions = new Map(); + const latestPortsVersions = new Map(); + const installConfigs = new Map(); + + // get the current/installed version for the ports + for ( + const installProv of installProvisions + ) { + const installId = installProv.instId; + const install = await db.get(installId); + + if (!install) { + throw new Error("InstallId not found in InstallsDb", { + cause: { + installId, + }, + }); + } + + const manifest = install.manifest; + const config = install.conf; + + const resolvedResolutionDeps = [] as [string, string][]; + for (const dep of manifest.resolutionDeps ?? []) { + const { manifest: depManifest, config: depConf } = getDepConfig( + allowedDeps, + manifest, + config, + dep, + ); + + // TODO: avoid reinstall, infact just do a resolve + const depInstId = await resolveAndInstall( + scx, + allowedDeps, + depManifest, + depConf, + ); + resolvedResolutionDeps.push([depInstId.installId, depManifest.name]); + } + + const depShimsRootPath = await Deno.makeTempDir({ + dir: scx.tmpPath, + prefix: `shims_resDeps_${manifest.name}_`, + }); + const resolutionDepArts = await getShimmedDepArts( + scx, + depShimsRootPath, + resolvedResolutionDeps, + ); + + const port = getPortImpl(manifest); + const listAllArgs = { + depArts: resolutionDepArts, + config, + manifest, + }; + + // get the current Version + const currentVersion = config.version; + installedPortsVersions.set(installId, currentVersion); + + // get the latest version of the port + const latestStable = await port.latestStable(listAllArgs); + latestPortsVersions.set(installId, latestStable); + + installConfigs.set(installId, config); + + await $.removeIfExists(depShimsRootPath); + } + + return { + installedPortsVersions: installedPortsVersions, + latestPortsVersions: latestPortsVersions, + installConfigs: installConfigs, + }; +} diff --git a/modules/ports/reducers.ts b/modules/ports/reducers.ts new file mode 100644 index 00000000..ca34bdf4 --- /dev/null +++ b/modules/ports/reducers.ts @@ -0,0 +1,136 @@ +//! Integration between Ports and Envs module + +import { expandGlobsAndAbsolutize, unwrapZodRes } from "../../utils/mod.ts"; +import { Provision } from "../envs/types.ts"; +import { GhjkCtx } from "../types.ts"; +// NOTE: mod.ts must always be a type import +import type { PortsCtx } from "./mod.ts"; +import { + buildInstallGraph, + installFromGraph, + type InstallGraph, + syncCtxFromGhjk, +} from "./sync.ts"; +import type { + InstallArtifacts, + InstallSetProvision, + InstallSetRefProvision, +} from "./types.ts"; +import validators, { installProvisionTy } from "./types.ts"; + +export function installSetReducer(gcx: GhjkCtx) { + return async (provisions: InstallSetProvision[]) => { + if (provisions.length > 1) { + throw new Error( + 'only one "ghjkPorts" provision per environment is supported', + ); + } + const { set } = unwrapZodRes( + validators.installSetProvision.safeParse(provisions[0]), + {}, + "error parsing env provision", + ); + await using scx = await syncCtxFromGhjk(gcx); + const installGraph = await buildInstallGraph(scx, set); + const installArts = await installFromGraph(scx, installGraph); + + const out = await reduceInstArts(installGraph, installArts); + return out; + }; +} + +export function installSetRefReducer(gcx: GhjkCtx, pcx: PortsCtx) { + const directReducer = installSetReducer(gcx); + return (provisions: InstallSetRefProvision[]) => + directReducer(provisions.map( + (prov) => { + const { setId } = unwrapZodRes( + 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( + installGraph: InstallGraph, + installArts: Map, +) { + const out = [] as Provision[]; + + // use this to track seen env vars to report conflicts + const foundEnvVars: Record = {}; + // FIXME: detect shim conflicts + // FIXME: better support for multi installs + await Promise.all(installGraph.user.map(async (instId) => { + const { binPaths, libPaths, includePaths, installPath, env } = installArts + .get( + instId, + )!; + out.push({ ty: installProvisionTy, instId }); + + for (const [key, val] of Object.entries(env)) { + const conflict = foundEnvVars[key]; + if (conflict) { + throw new Error( + `duplicate env var found ${key} from sources ${instId} & ${ + conflict[1] + }`, + { + cause: { + a: [instId, val], + b: conflict, + }, + }, + ); + } + foundEnvVars[key] = [val, instId]; + out.push({ + ty: "posix.envVar", + key, + val, + }); + } + const expandCurry = (path: string) => + expandGlobsAndAbsolutize(path, installPath); + + const [binPathsNorm, libPathsNorm, includePathsNorm] = await Promise + .all( + [ + Promise.all(binPaths.map(expandCurry)), + Promise.all(libPaths.map(expandCurry)), + Promise.all(includePaths.map(expandCurry)), + ], + ); + out.push( + ...binPathsNorm.flatMap((paths) => + paths.map((absolutePath) => ({ + ty: "posix.exec" as const, + absolutePath, + })) + ), + ...libPathsNorm.flatMap((paths) => + paths.map((absolutePath) => ({ + ty: "posix.sharedLib" as const, + absolutePath, + })) + ), + ...includePathsNorm.flatMap((paths) => + paths.map((absolutePath) => ({ + ty: "posix.headerFile" as const, + absolutePath, + })) + ), + ); + })); + + return out; +} diff --git a/modules/ports/sync.ts b/modules/ports/sync.ts index 0fb4937f..c39e6593 100644 --- a/modules/ports/sync.ts +++ b/modules/ports/sync.ts @@ -1,7 +1,8 @@ -import { deep_eql, jsonHash, std_fs, std_path, zod } from "../../deps/cli.ts"; +import { deep_eql, std_fs, std_path, zod } from "../../deps/cli.ts"; import getLogger from "../../utils/logger.ts"; import validators from "./types.ts"; import type { + AllowedPortDep, AmbientAccessPortManifestX, DenoWorkerPortManifestX, DepArts, @@ -9,20 +10,19 @@ import type { InstallArtifacts, InstallConfigLiteX, InstallConfigResolvedX, + InstallSetX, PortArgsBase, PortDep, PortManifestX, - PortsModuleConfigX, } from "./types.ts"; import { DenoWorkerPort } from "./worker.ts"; import { AmbientAccessPort } from "./ambient.ts"; import { $, AVAIL_CONCURRENCY, - DePromisify, getInstallHash, getPortRef, - objectHashHex, + objectHash, type Rc, rc, sameFsTmpRoot, @@ -33,21 +33,23 @@ import type { GhjkCtx } from "../types.ts"; const logger = getLogger(import.meta); export type ResolutionMemoStore = Map>; -export type SyncCtx = DePromisify>; export function getResolutionMemo( gcx: GhjkCtx, ) { - let memoStore = gcx.state.get("resolutionMemoStore") as + const id = "resolutionMemoStore"; + let memoStore = gcx.blackboard.get(id) as | ResolutionMemoStore | undefined; if (!memoStore) { memoStore = new Map(); - gcx.state.set("resolutionMemoStore", memoStore); + gcx.blackboard.set(id, memoStore); } return memoStore; } +export type SyncCtx = Awaited>; + export async function syncCtxFromGhjk( gcx: GhjkCtx, ) { @@ -60,7 +62,7 @@ export async function syncCtxFromGhjk( sameFsTmpRoot(portsPath.toString()), ]) ).map($.pathToString); - let db = gcx.state.get("installsDb") as + let db = gcx.blackboard.get("installsDb") as | Rc | undefined; if (!db) { @@ -72,10 +74,10 @@ export async function syncCtxFromGhjk( ), (db) => { db[Symbol.dispose](); - gcx.state.delete("installsDb"); + gcx.blackboard.delete("installsDb"); }, ); - gcx.state.set("installsDb", db); + gcx.blackboard.set("installsDb", db); } else { db = db.clone(); } @@ -93,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, @@ -220,9 +119,9 @@ export async function installFromGraph( dir: tmpPath, prefix: `shims_${installId}_`, }); - for ( - const [depInstallId, depPortName] of graph.depEdges[installId] ?? [] - ) { + await Promise.all((graph.depEdges[installId] ?? []).map(async ( + [depInstallId, depPortName], + ) => { const depArts = installCtx.artifacts.get(depInstallId); if (!depArts) { throw new Error( @@ -254,7 +153,7 @@ export async function installFromGraph( ), env: depArts.env, }; - } + })); return { totalDepArts, depShimsRootPath }; }, @@ -373,13 +272,13 @@ export async function installFromGraph( return installCtx.artifacts; } -export type InstallGraph = DePromisify>; +export type InstallGraph = Awaited>; // this returns a data structure containing all the info // required for installation including the dependency graph export async function buildInstallGraph( scx: SyncCtx, - portsConfig: PortsModuleConfigX, + set: InstallSetX, ) { type GraphInstConf = { instId: string; @@ -387,9 +286,6 @@ export async function buildInstallGraph( config: InstallConfigResolvedX; }; // this is all referring to port dependencies - // TODO: runtime dependencies - // NOTE: keep this easy to deserialize around as it's put directly - // into the lockfile const graph = { // maps from instHashId all: {} as Record, @@ -429,7 +325,7 @@ export async function buildInstallGraph( const foundInstalls: GraphInstConf[] = []; // collect the user specified insts first - for (const inst of portsConfig.installs) { + for (const inst of set.installs) { const { port: manifest, ...instLiteBase } = inst; const portRef = addPort(manifest); const instLite = validators.installConfigLite.parse({ @@ -438,11 +334,11 @@ export async function buildInstallGraph( }); const resolvedConfig = await resolveConfig( scx, - portsConfig, + set.allowedBuildDeps, manifest, instLite, ); - const instId = await getInstallHash(resolvedConfig); + const instId = getInstallHash(resolvedConfig); // no dupes allowed in user specified insts if (graph.user.includes(instId)) { @@ -480,27 +376,27 @@ 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) { - const { manifest: depPort } = portsConfig.allowedDeps[depId.name]; - if (!depPort) { + for (const depId of manifest.buildDeps) { + const dep = set.allowedBuildDeps[depId.name]; + if (!dep) { throw new Error( `unrecognized dependency "${depId.name}" specified by port "${manifest.name}@${manifest.version}"`, ); } - const portRef = addPort(depPort); + const portRef = addPort(dep.manifest); // get the install config of dependency // 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); + const depInstallId = getInstallHash(depInstall); // only add the install configuration for this dep port // if specific hash hasn't seen before @@ -512,7 +408,7 @@ export async function buildInstallGraph( }); } - deps.push([depInstallId, depPort.name]); + deps.push([depInstallId, dep.manifest.name]); // make sure the dependency knows this install depends on it const reverseDeps = graph.revDepEdges[depInstallId] ?? []; @@ -559,13 +455,13 @@ export async function buildInstallGraph( // This takes user specified InstallConfigs and resolves // their versions to a known, installable version // It also resolves any dependencies that the config specifies -async function resolveConfig( +function resolveConfig( scx: SyncCtx, - portsConfig: PortsModuleConfigX, + allowedDeps: Record, manifest: PortManifestX, config: InstallConfigLiteX, ) { - const hash = await objectHashHex(config as jsonHash.Tree); + const hash = objectHash(JSON.parse(JSON.stringify(config))); let promise = scx.memoStore.get(hash); if (!promise) { promise = inner(); @@ -578,7 +474,7 @@ async function resolveConfig( const resolvedResolutionDeps = [] as [string, string][]; for (const dep of manifest.resolutionDeps ?? []) { const { manifest: depMan, config: depConf } = getDepConfig( - portsConfig, + allowedDeps, manifest, config, dep, @@ -588,7 +484,7 @@ async function resolveConfig( // get the version resolved config of the dependency const depInstId = await resolveAndInstall( scx, - portsConfig, + allowedDeps, depMan, depConf, ); @@ -605,7 +501,7 @@ async function resolveConfig( resolvedResolutionDeps, ); - // finally resolve the versino + // finally resolve the version let version; // TODO: fuzzy matching const port = getPortImpl(manifest); @@ -623,7 +519,7 @@ async function resolveConfig( ); if (!match) { throw new Error(`error resolving verison: not found`, { - cause: { config, manifest }, + cause: { config, manifest, allVersions }, }); } version = match; @@ -638,10 +534,10 @@ 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( - portsConfig, + allowedDeps, manifest, config, dep, @@ -649,17 +545,18 @@ async function resolveConfig( // get the version resolved config of the dependency const depInstall = await resolveConfig( scx, - portsConfig, + allowedDeps, depMan, depConf, ); - resolveDepConfigs[dep.name] = depInstall; + buildDepConfigs[dep.name] = depInstall; } return validators.installConfigResolved.parse({ ...config, - depConfigs: resolveDepConfigs, version, + specifiedVersion: !!config.version, + buildDepConfigs, }); } } @@ -668,24 +565,24 @@ async function resolveConfig( // config.depPorts[depId] or the default InstallConfig specified // for the portsConfig.allowedDeps // No version resolution takes place -function getDepConfig( - portsConfig: PortsModuleConfigX, +export function getDepConfig( + allowedBuildDeps: Record, manifest: PortManifestX, config: InstallConfigLiteX, depId: PortDep, resolutionDep = false, ) { - const { manifest: depPort, defaultInst: defaultDepInstall } = - portsConfig.allowedDeps[depId.name]; - if (!depPort) { + const dep = allowedBuildDeps[depId.name]; + if (!dep) { throw new Error( `unrecognized dependency "${depId.name}" specified by port "${manifest.name}@${manifest.version}"`, ); } + const { manifest: depPort, defaultInst: defaultDepInstall } = dep; // 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) { @@ -703,23 +600,25 @@ 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 -async function resolveAndInstall( +/** + * 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 +export async function resolveAndInstall( scx: SyncCtx, - portsConfig: PortsModuleConfigX, + allowedDeps: Record, manifest: PortManifestX, configLite: InstallConfigLiteX, ) { - const config = await resolveConfig(scx, portsConfig, manifest, configLite); - const installId = await getInstallHash(config); + const config = await resolveConfig(scx, allowedDeps, manifest, configLite); + const installId = getInstallHash(config); const cached = await scx.db.val.get(installId); // we skip it if it's already installed @@ -736,13 +635,13 @@ async function resolveAndInstall( scx, depShimsRootPath, await Promise.all( - manifest.deps?.map( + manifest.buildDeps?.map( async (dep) => { - const depConfig = getDepConfig(portsConfig, manifest, config, dep); + const depConfig = getDepConfig(allowedDeps, manifest, config, dep); // we not only resolve but install the dep here const { installId } = await resolveAndInstall( scx, - portsConfig, + allowedDeps, depConfig.manifest, depConfig.config, ); @@ -810,51 +709,54 @@ async function resolveAndInstall( } // This assumes that the installs are already in the db -async function getShimmedDepArts( +export async function getShimmedDepArts( scx: SyncCtx, shimsRootPath: string, installs: [string, string][], ) { const totalDepArts: DepArts = {}; - for ( - const [installId, portName] of installs - ) { - const installRow = await scx.db.val.get(installId); - if (!installRow || !installRow.installArts) { - throw new Error( - `artifacts not found for "${installId}" not found in db when shimming totalDepArts`, - { - cause: { installs }, + await Promise.all( + installs + .map( + async ([installId, portName]) => { + const installRow = await scx.db.val.get(installId); + if (!installRow || !installRow.installArts) { + throw new Error( + `artifacts not found for "${installId}" not found in db when shimming totalDepArts`, + { + cause: { installs }, + }, + ); + } + const installArts = installRow.installArts; + const shimDir = $.path(shimsRootPath).resolve(installId); + const [binShimDir, libShimDir, includeShimDir] = (await Promise.all([ + shimDir.join("bin").ensureDir(), + shimDir.join("lib").ensureDir(), + shimDir.join("include").ensureDir(), + ])).map($.pathToString); + + totalDepArts[portName] = { + execs: await shimLinkPaths( + installArts.binPaths, + installArts.installPath, + binShimDir, + ), + libs: await shimLinkPaths( + installArts.libPaths, + installArts.installPath, + libShimDir, + ), + includes: await shimLinkPaths( + installArts.includePaths, + installArts.installPath, + includeShimDir, + ), + env: installArts.env, + }; }, - ); - } - const installArts = installRow.installArts; - const shimDir = $.path(shimsRootPath).resolve(installId); - const [binShimDir, libShimDir, includeShimDir] = (await Promise.all([ - shimDir.join("bin").ensureDir(), - shimDir.join("lib").ensureDir(), - shimDir.join("include").ensureDir(), - ])).map($.pathToString); - - totalDepArts[portName] = { - execs: await shimLinkPaths( - installArts.binPaths, - installArts.installPath, - binShimDir, - ), - libs: await shimLinkPaths( - installArts.libPaths, - installArts.installPath, - libShimDir, - ), - includes: await shimLinkPaths( - installArts.includePaths, - installArts.installPath, - includeShimDir, ), - env: installArts.env, - }; - } + ); return totalDepArts; } @@ -896,7 +798,7 @@ async function shimLinkPaths( throw error; } } - await $.path(shimPath).createSymlinkTo(filePath, { type: "file" }); + await $.path(shimPath).symlinkTo(filePath, { type: "file" }); shims[fileName] = shimPath; } return shims; @@ -931,7 +833,7 @@ type DownloadStageArgs = { depArts: DepArts; }; -async function doDownloadStage( +export async function doDownloadStage( { installId, installPath, @@ -980,7 +882,7 @@ async function doDownloadStage( type InstallStageArgs = DownloadStageArgs; -async function doInstallStage( +export async function doInstallStage( { installId, installPath, @@ -1054,48 +956,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 61e42e5e..249c127b 100644 --- a/modules/ports/types.ts +++ b/modules/ports/types.ts @@ -1,6 +1,7 @@ //! NOTE: type FooX is a version of Foo after zod processing/transformation import { semver, zod } from "../../deps/common.ts"; +import moduleValidators from "../types.ts"; import { relativeFileUrl } from "../../utils/url.ts"; import { ALL_ARCH, ALL_OS, archEnum, osEnum } from "./types/platform.ts"; @@ -36,7 +37,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 +80,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 +118,8 @@ const installConfigFat = stdInstallConfigFat; const installConfigResolved = installConfigLite.merge(zod.object({ // NOTE: version is no longer nullish version: zod.string(), - // depConfigs: zod.record( + versionSpecified: zod.boolean().optional(), + // buildDepConfigs: zod.record( // portName, // // FIXME: figure out cyclically putting `installConfigResolved` here // zod.object({ version: zod.string() }).passthrough(), @@ -144,31 +146,60 @@ const allowedPortDep = zod.object({ defaultInst: installConfigLite, }); -const portsModuleSecureConfig = zod.object({ - allowedPortDeps: zod.array(allowedPortDep).nullish(), -}); +const allowDepSet = zod.record(zod.string(), allowedPortDep); + +const allowDepSetHashed = zod.record(zod.string(), zod.string()); -const portsModuleConfigBase = zod.object({ +const installSetHashed = zod.object({ installs: zod.array(zod.string()), + allowedBuildDeps: zod.string(), }); -const portsModuleConfig = portsModuleConfigBase.merge(zod.object({ - allowedDeps: zod.record( - zod.string(), - zod.string(), - ), -})); - -const portsModuleConfigBaseX = zod.object({ +const installSet = zod.object({ installs: zod.array(installConfigFat), + allowedBuildDeps: allowDepSet, }); -const portsModuleConfigX = portsModuleConfigBaseX.merge(zod.object({ - allowedDeps: zod.record( - zod.string(), - allowedPortDep, - ), -})); +const portsModuleConfigHashed = zod.object({ + sets: zod.record(zod.string(), installSetHashed), +}); + +const portsModuleConfig = zod.object({ + sets: zod.record(zod.string(), installSet), +}); + +export const installSetProvisionTy = "ghjk.ports.InstallSet"; +const installSetProvision = zod.object({ + ty: zod.literal(installSetProvisionTy), + set: installSet, +}); + +export const installSetRefProvisionTy = "ghjk.ports.InstallSetRef"; +const installSetRefProvision = zod.object({ + ty: zod.literal(installSetRefProvisionTy), + setId: zod.string(), +}); + +export const installProvisionTy = "ghjk.ports.Install"; +export const installProvision = zod.object({ + ty: zod.literal(installProvisionTy), + instId: zod.string(), +}); + +const downloadArtifacts = zod.object({ + installVersion: zod.string(), + downloadPath: zod.string(), +}); + +const installArtifacts = zod.object({ + env: zod.record(moduleValidators.envVarName, zod.string()), + installVersion: zod.string(), + binPaths: zod.string().array(), + libPaths: zod.string().array(), + includePaths: zod.string().array(), + installPath: zod.string(), + downloadPath: zod.string(), +}); const validators = { osEnum, @@ -190,12 +221,19 @@ const validators = { installConfig, installConfigResolved, portManifest, - portsModuleConfigBase, - portsModuleSecureConfig, portsModuleConfig, - portsModuleConfigX, + portsModuleConfigHashed, allowedPortDep, + allowDepSet, + allowDepSetHashed, + installSetProvision, + installSetRefProvision, + installProvision, + installSet, + installSetHashed, string: zod.string(), + downloadArtifacts, + installArtifacts, stringArray: zod.string().min(1).array(), }; export default validators; @@ -213,7 +251,6 @@ export type AmbientAccessPortManifest = zod.input< typeof validators.ambientAccessPortManifest >; -// Describes the port itself export type PortManifest = zod.input< typeof validators.portManifest >; @@ -225,12 +262,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; @@ -243,61 +284,89 @@ 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 >; -export type PortsModuleConfigBase = zod.infer< - typeof validators.portsModuleConfigBase +/* + * Provisions an [`InstallSet`]. + */ +export type InstallSetProvision = zod.input< + typeof validators.installSetProvision +>; +export type InstallSetProvisionX = zod.infer< + typeof validators.installSetProvision +>; + +export type InstallProvision = zod.infer; + +/* + * Provisions an [`InstallSet`] that's been pre-defined in the [`PortsModuleConfigX`]. + */ +export type InstallSetRefProvision = zod.input< + typeof validators.installSetRefProvision +>; +export type InstallSetRefProvisionX = zod.infer< + typeof validators.installSetRefProvision >; export type AllowedPortDep = zod.input; export type AllowedPortDepX = zod.infer; -/// This is a secure sections of the config intended to be direct exports -/// from the config script instead of the global variable approach the -/// main [`GhjkConfig`] can take. -export type PortsModuleSecureConfig = zod.input< - typeof validators.portsModuleSecureConfig +export type InstallSet = zod.input; +export type InstallSetX = zod.infer< + typeof validators.installSet >; -export type PortsModuleSecureConfigX = zod.input< - typeof validators.portsModuleSecureConfig + +export type InstallSetHashed = zod.input; +export type InstallSetHashedX = zod.infer< + typeof validators.installSetHashed >; export type PortsModuleConfig = zod.input; export type PortsModuleConfigX = zod.infer< - typeof validators.portsModuleConfigX + typeof validators.portsModuleConfig >; -/* -interface ASDF_CONFIG_EXAMPLE { - ASDF_INSTALL_TYPE: "version" | "ref"; - ASDF_INSTALL_VERSION: string; // full version number or Git Ref depending on ASDF_INSTALL_TYPE - ASDF_INSTALL_PATH: string; // the path to where the tool should, or has been installed - ASDF_CONCURRENCY: number; // the number of cores to use when compiling the source code. Useful for setting make -j - ASDF_DOWNLOAD_PATH: string; // the path to where the source code or binary was downloaded to by bin/download - ASDF_PLUGIN_PATH: string; // the path the plugin was installed - ASDF_PLUGIN_SOURCE_URL: string; // the source URL of the plugin - ASDF_PLUGIN_PREV_REF: string; // prevous git-ref of the plugin repo - ASDF_PLUGIN_POST_REF: string; // updated git-ref of the plugin repo - ASDF_CMD_FILE: string; // resolves to the full path of the file being sourced -} -*/ +export type PortsModuleConfigHashed = zod.input< + typeof validators.portsModuleConfigHashed +>; +export type PortsModuleConfigLiteHashedX = zod.infer< + typeof validators.portsModuleConfigHashed +>; export type DepArt = { execs: Record; @@ -344,17 +413,5 @@ export interface InstallArgs extends PortArgsBase { tmpDirPath: string; } -export type DownloadArtifacts = { - installVersion: string; - downloadPath: string; -}; - -export type InstallArtifacts = { - env: Record; - installVersion: string; - binPaths: string[]; - libPaths: string[]; - includePaths: string[]; - installPath: string; - downloadPath: string; -}; +export type DownloadArtifacts = zod.infer; +export type InstallArtifacts = zod.infer; diff --git a/modules/ports/utils.ts b/modules/ports/utils.ts new file mode 100644 index 00000000..73bc452d --- /dev/null +++ b/modules/ports/utils.ts @@ -0,0 +1,140 @@ +import { std_path } from "../../deps/cli.ts"; +import logger from "../../utils/logger.ts"; +import { $ } from "../../utils/mod.ts"; +import { GhjkCtx } from "../types.ts"; +import { AllowedPortDep } from "./mod.ts"; +import { + doDownloadStage, + doInstallStage, + getDepConfig, + getShimmedDepArts, + resolveAndInstall, + SyncCtx, + syncCtxFromGhjk, +} from "./sync.ts"; +import { InstallConfigResolvedX, PortManifestX } from "./types.ts"; + +export async function updateInstall( + gcx: GhjkCtx, + installId: string, + newVersion: string, + allowedDeps: Record, +) { + await using scx = await syncCtxFromGhjk(gcx); + + const db = scx.db.val; + + const install = await db.get(installId); + + if (!install) { + throw new Error("InstallSetId not found in InstallsDb", { + cause: { + installId, + }, + }); + } + + const config = install.conf; + + if (config.version === newVersion) { + logger().info("Skipping update. Install is already uptodate"); + return; + } + + // it's a user specified install, so skip + if (config.specifiedVersion) { + logger().info(`Skipping Version Specified Install: ${installId}`); + return; + } + + config.version = newVersion; + logger().info(`Updating installId ${installId} to version ${newVersion}...`); + await doInstall(installId, scx, install.manifest, allowedDeps, config); + logger().info( + `Successfully updated installId ${installId} to version ${newVersion}`, + ); +} + +async function doInstall( + installId: string, + scx: SyncCtx, + manifest: PortManifestX, + allowedDeps: Record, + config: InstallConfigResolvedX, +) { + const depShimsRootPath = await Deno.makeTempDir({ + dir: scx.tmpPath, + prefix: `shims_${installId}`, + }); + + // readies all the exports of the port's deps including + // shims for their exports + const totalDepArts = await getShimmedDepArts( + scx, + depShimsRootPath, + await Promise.all( + manifest.buildDeps?.map( + async (dep) => { + const depConfig = getDepConfig(allowedDeps, manifest, config, dep); + // we not only resolve but install the dep here + const { installId } = await resolveAndInstall( + scx, + allowedDeps, + depConfig.manifest, + depConfig.config, + ); + return [installId, dep.name]; + }, + ) ?? [], + ), + ); + + const stageArgs = { + installId, + installPath: std_path.resolve(scx.installsPath, installId), + downloadPath: std_path.resolve(scx.downloadsPath, installId), + tmpPath: scx.tmpPath, + config: config, + manifest, + depArts: totalDepArts, + }; + + const dbRow = { + installId, + conf: config, + manifest, + }; + let downloadArts; + + try { + downloadArts = await doDownloadStage({ + ...stageArgs, + }); + } catch (err) { + throw new Error(`error downloading ${installId}`, { cause: err }); + } + await scx.db.val.set(installId, { + ...dbRow, + progress: "downloaded", + downloadArts, + }); + + let installArtifacts; + try { + installArtifacts = await doInstallStage( + { + ...stageArgs, + ...downloadArts, + }, + ); + } catch (err) { + throw new Error(`error installing ${installId}`, { cause: err }); + } + await scx.db.val.set(installId, { + ...dbRow, + progress: "installed", + downloadArts, + installArts: installArtifacts, + }); + await $.removeIfExists(depShimsRootPath); +} 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/std.ts b/modules/std.ts index 917fd610..31a2e73f 100644 --- a/modules/std.ts +++ b/modules/std.ts @@ -1,9 +1,10 @@ +import { EnvsModule } from "./envs/mod.ts"; import { PortsModule } from "./ports/mod.ts"; import { TasksModule } from "./tasks/mod.ts"; export const ports = "ports"; - export const tasks = "tasks"; +export const envs = "envs"; export const map = { [ports as string]: { @@ -12,4 +13,7 @@ export const map = { [tasks as string]: { ctor: TasksModule, }, + [envs as string]: { + ctor: EnvsModule, + }, }; diff --git a/modules/tasks/deno.ts b/modules/tasks/deno.ts index a4d219d1..22e78bdb 100644 --- a/modules/tasks/deno.ts +++ b/modules/tasks/deno.ts @@ -8,6 +8,7 @@ import { std_url } from "../../deps/common.ts"; import { inWorker } from "../../utils/mod.ts"; import logger, { setup as setupLogger } from "../../utils/logger.ts"; +import { shimDenoNamespace } from "../../utils/worker.ts"; if (inWorker()) { initWorker(); @@ -21,10 +22,8 @@ function initWorker() { export type DriverRequests = { ty: "exec"; - name: string; uri: string; - args: string[]; - envVars: Record; + args: ExecTaskArgs; }; export type DriverResponse = { @@ -32,6 +31,13 @@ export type DriverResponse = { payload: boolean; }; +export type ExecTaskArgs = { + key: string; + argv: string[]; + workingDir: string; + envVars: Record; +}; + async function onMsg(msg: MessageEvent) { const req = msg.data; if (!req.ty) { @@ -42,7 +48,7 @@ async function onMsg(msg: MessageEvent) { if (req.ty == "exec") { res = { ty: req.ty, - payload: await importAndExec(req.uri, req.name, req.args, req.envVars), + payload: await importAndExec(req.uri, req.args), }; } else { logger().error(`invalid DriverRequest type`, req); @@ -53,12 +59,11 @@ async function onMsg(msg: MessageEvent) { async function importAndExec( uri: string, - name: string, - args: string[], - envVars: Record, + args: ExecTaskArgs, ) { + const _shimHandle = shimDenoNamespace(args.envVars); const mod = await import(uri); - await mod.ghjk.execTask(name, args, envVars); + await mod.sophon.execTask(args); return true; } @@ -103,17 +108,13 @@ async function rpc(moduleUri: string, req: DriverRequests) { } export async function execTaskDeno( - configUri: string, - name: string, - args: string[], - envVars: Record, + moduleUri: string, + args: ExecTaskArgs, ) { - const resp = await rpc(configUri, { + const resp = await rpc(moduleUri, { ty: "exec", - uri: configUri, - name, + uri: moduleUri, args, - envVars, }); if (resp.ty != "exec") { throw new Error(`invalid response type: ${resp.ty}`); diff --git a/modules/tasks/exec.ts b/modules/tasks/exec.ts index a4bb5cfe..8d72f145 100644 --- a/modules/tasks/exec.ts +++ b/modules/tasks/exec.ts @@ -1,39 +1,20 @@ -import { std_path } from "../../deps/cli.ts"; -import { $, DePromisify } from "../../utils/mod.ts"; +import { $ } from "../../utils/mod.ts"; -import type { TaskDefX, TasksModuleConfigX } from "./types.ts"; +import type { TaskDefHashedX, TasksModuleConfigX } from "./types.ts"; import type { GhjkCtx } from "../types.ts"; -import logger from "../../utils/logger.ts"; +import getLogger from "../../utils/logger.ts"; import { execTaskDeno } from "./deno.ts"; -import { - buildInstallGraph, - installFromGraphAndShimEnv, - syncCtxFromGhjk, -} from "../ports/sync.ts"; -import { GlobalEnv } from "../../host/types.ts"; +const logger = getLogger(import.meta); -export type ExecCtx = DePromisify>; +import { cookPosixEnv } from "../envs/posix.ts"; +import { getEnvsCtx } from "../envs/inter.ts"; -export async function execCtxFromGhjk( - gcx: GhjkCtx, -) { - const syncCx = await syncCtxFromGhjk(gcx); - return { - ghjkCx: gcx, - syncCx, - async [Symbol.asyncDispose]() { - await syncCx![Symbol.asyncDispose](); - }, - }; -} - -export type TaskGraph = DePromisify>; +export type TaskGraph = Awaited>; -export async function buildTaskGraph( - ecx: ExecCtx, - portsConfig: TasksModuleConfigX, - env: GlobalEnv, +export function buildTaskGraph( + _gcx: GhjkCtx, + tasksConfig: TasksModuleConfigX, ) { const graph = { indie: [] as string[], @@ -41,39 +22,28 @@ export async function buildTaskGraph( revDepEdges: {} as Record, // edges from dependent to dependency depEdges: {} as Record, - // the install graphs for the ports declared by the tasks - portInstallGraphs: Object.fromEntries( - await Promise.all( - Object.entries(portsConfig.tasks) - .map(async ([name, task]) => [ - name, - await buildInstallGraph( - ecx.syncCx, - { - installs: task.env.installs.map((hash) => env.installs[hash]), - allowedDeps: Object.fromEntries(task.env.allowedPortDeps.map( - (dep) => [dep.manifest.name, dep], - )), - }, - ), - ]), - ), - ), }; - for (const [name, task] of Object.entries(portsConfig.tasks)) { - if (task.dependsOn.length == 0) { - graph.indie.push(name); + for (const [hash, task] of Object.entries(tasksConfig.tasks)) { + /* + * FIXME: find a way to pre-check if task envs are availaible + if (task.envKey && !envsCx.has(task.envKey)) { + throw new Error( + `unable to find env referenced by task "${hash}" under key "${task.envKey}"`, + ); + } */ + if (!task.dependsOn || task.dependsOn.length == 0) { + graph.indie.push(hash); } else { - for (const depTaskName of task.dependsOn) { + for (const depTaskHash of task.dependsOn) { const testCycle = ( name: string, - depName: string, - ): TaskDefX | undefined => { - const depTask = portsConfig.tasks[depName]; + depHash: string, + ): TaskDefHashedX | undefined => { + const depTask = tasksConfig.tasks[depHash]; if (!depTask) { throw new Error(`specified dependency task doesn't exist`, { cause: { - depTaskName, + depHash, task, }, }); @@ -86,7 +56,7 @@ export async function buildTaskGraph( } }; - const cycleSource = testCycle(name, depTaskName); + const cycleSource = testCycle(hash, depTaskHash); if ( cycleSource ) { @@ -100,87 +70,127 @@ export async function buildTaskGraph( }, ); } - graph.revDepEdges[depTaskName] = [ - ...graph.revDepEdges[depTaskName] ?? [], - name, - ]; + const revDepSet = graph.revDepEdges[depTaskHash]; + if (revDepSet) { + revDepSet.push(hash); + } else { + graph.revDepEdges[depTaskHash] = [hash]; + } } - graph.depEdges[name] = task.dependsOn; + graph.depEdges[hash] = task.dependsOn; } } return graph; } export async function execTask( - ecx: ExecCtx, + gcx: GhjkCtx, tasksConfig: TasksModuleConfigX, taskGraph: TaskGraph, - targetName: string, + targetKey: string, args: string[], // taskEnv: TaskEnvX, // installGraph: InstallGraph, ): Promise { - let workSet = new Set([targetName]); + let workSet = new Set([targetKey]); { - const stack = [targetName]; + const stack = [targetKey]; while (stack.length > 0) { - const taskName = stack.pop()!; - const taskDef = tasksConfig.tasks[taskName]; - stack.push(...taskDef.dependsOn); - workSet = new Set([...workSet.keys(), ...taskDef.dependsOn]); + const taskHash = stack.pop()!; + const taskDef = tasksConfig.tasks[taskHash]; + stack.push(...taskDef.dependsOn ?? []); + workSet = new Set([...workSet.keys(), ...taskDef.dependsOn ?? []]); } } const pendingDepEdges = new Map( Object.entries(taskGraph.depEdges).map(([key, val]) => [key, val!]), ); - const pendingTasks = taskGraph.indie.filter((name) => workSet.has(name)); + const pendingTasks = taskGraph.indie.filter((hash) => workSet.has(hash)); if (pendingTasks.length == 0) { throw new Error("something went wrong, task graph starting set is empty"); } while (pendingTasks.length > 0) { - const taskName = pendingTasks.pop()!; - const taskEnv = tasksConfig.tasks[taskName]; + const taskKey = pendingTasks.pop()!; + const taskDef = tasksConfig.tasks[taskKey]; - const installGraph = taskGraph.portInstallGraphs[taskName]; const taskEnvDir = await Deno.makeTempDir({ - prefix: `ghjkTaskEnv_${taskName}_`, + prefix: `ghjkTaskEnv_${taskKey}_`, }); - const { env: installEnvs } = await installFromGraphAndShimEnv( - ecx.syncCx, - taskEnvDir, - installGraph, + const envsCx = getEnvsCtx(gcx); + const recipe = envsCx.config.envs[taskDef.envKey]; + const { env: installEnvs } = await cookPosixEnv( + { + gcx, + recipe: recipe ?? { provides: [] }, + envKey: taskDef.envKey ?? `taskEnv_${taskKey}`, + envDir: taskEnvDir, + }, ); - logger().info("executing", taskName, args); - await execTaskDeno( - std_path.toFileUrl(ecx.ghjkCx.ghjkfilePath).href, - taskName, + logger.info( + "executing", + taskKey, args, - { - ...Deno.env.toObject(), - ...Object.fromEntries( - Object.entries(installEnvs).map( - ( - [key, val], - ) => [ + ); + + const envVars = { + ...Deno.env.toObject(), + ...Object.fromEntries( + Object.entries(installEnvs).map( + ( + [key, val], + ) => { + if (key.match(/PATH/) && Deno.env.get(key)) { + val = [...new Set([val, ...Deno.env.get(key)!.split(":")]).keys()] + .filter((str) => str.length > 0) + .join(":"); + } + return [ key, - key.match(/PATH/i) ? `${val}:${Deno.env.get(key) ?? ""}` : val, - ], - ), + val, + ]; + }, ), - ...taskEnv.env.env, - }, - ); + ), + }; + if (taskDef.ty == "denoFile@v1") { + if (!gcx.ghjkfilePath) { + throw new Error( + "denoFile task found but no ghjkfile. This occurs when ghjk is working just on a lockfile alone", + ); + } + const workingDir = gcx.ghjkfilePath.parentOrThrow(); + await execTaskDeno( + gcx.ghjkfilePath.toFileUrl().toString(), + { + key: taskDef.key, + argv: args, + envVars, + workingDir: taskDef.workingDir + ? workingDir.resolve(taskDef.workingDir).toString() + : workingDir.toString(), + }, + ); + } else { + throw new Error( + `unsupported task type "${taskDef.ty}"`, + { + cause: { + taskDef, + }, + }, + ); + } $.removeIfExists(taskEnvDir); - workSet.delete(taskName); - const dependentTasks = (taskGraph.revDepEdges[taskName] ?? []) + workSet.delete(taskKey); + const dependentTasks = (taskGraph.revDepEdges[taskKey] ?? []) .filter((name) => workSet.has(name)); const readyTasks = []; for (const parentId of dependentTasks) { const parentDeps = pendingDepEdges.get(parentId)!; // swap remove from parent pending deps list - const idx = parentDeps.indexOf(taskName); + const idx = parentDeps.indexOf(taskKey); const last = parentDeps.pop()!; if (parentDeps.length > idx) { parentDeps[idx] = last; diff --git a/modules/tasks/inter.ts b/modules/tasks/inter.ts new file mode 100644 index 00000000..5c4e3db3 --- /dev/null +++ b/modules/tasks/inter.ts @@ -0,0 +1,25 @@ +import type { GhjkCtx } from "../types.ts"; +import type { TasksCtx } from "./mod.ts"; + +export function getTasksCtx( + gcx: GhjkCtx, +): TasksCtx { + const key = "ctx.tasks"; + let ctx = gcx.blackboard.get(key) as + | TasksCtx + | undefined; + + if (!ctx) { + ctx = { + config: { tasks: {}, tasksNamed: [] }, + taskGraph: { + indie: [], + depEdges: {}, + revDepEdges: {}, + }, + }; + gcx.blackboard.set(key, ctx); + } + + return ctx; +} diff --git a/modules/tasks/mod.ts b/modules/tasks/mod.ts index 608737c3..7c87f7ff 100644 --- a/modules/tasks/mod.ts +++ b/modules/tasks/mod.ts @@ -1,20 +1,16 @@ export * from "./types.ts"; import { cliffy_cmd, zod } from "../../deps/cli.ts"; -import { Json } from "../../utils/mod.ts"; +import { Json, unwrapZodRes } from "../../utils/mod.ts"; import validators from "./types.ts"; import type { TasksModuleConfigX } from "./types.ts"; -import type { GhjkCtx, ModuleManifest } from "../types.ts"; +import { type GhjkCtx, type ModuleManifest } from "../types.ts"; import { ModuleBase } from "../mod.ts"; -import { - buildTaskGraph, - execCtxFromGhjk, - execTask, - type TaskGraph, -} from "./exec.ts"; -import { GlobalEnv } from "../../host/types.ts"; +import { buildTaskGraph, execTask, type TaskGraph } from "./exec.ts"; +import { Blackboard } from "../../host/types.ts"; +import { getTasksCtx } from "./inter.ts"; export type TasksCtx = { config: TasksModuleConfigX; @@ -26,81 +22,76 @@ const lockValidator = zod.object({ type TasksLockEnt = zod.infer; export class TasksModule extends ModuleBase { - async processManifest( - ctx: GhjkCtx, + processManifest( + gcx: GhjkCtx, manifest: ModuleManifest, + bb: Blackboard, _lockEnt: TasksLockEnt | undefined, - env: GlobalEnv, ) { - const res = validators.tasksModuleConfig.safeParse(manifest.config); - if (!res.success) { - throw new Error("error parsing module config", { - cause: { - config: manifest.config, - zodErr: res.error, - }, - }); + function unwrapParseCurry(res: zod.SafeParseReturnType) { + return unwrapZodRes(res, { + id: manifest.id, + config: manifest.config, + bb, + }, "error parsing module config"); } - const config: TasksModuleConfigX = { - tasks: Object.fromEntries( - Object.entries(res.data.tasks).map( - ([name, task]) => [name, { - ...task, - env: { - ...task.env, - allowedPortDeps: task.env.allowedPortDeps.map((hash) => - env.allowedPortDeps[hash] - ), - }, - }], - ), - ), - }; - await using execCx = await execCtxFromGhjk(ctx); - const taskGraph = await buildTaskGraph(execCx, config, env); - return { - config, - taskGraph, - }; + const config = unwrapParseCurry( + validators.tasksModuleConfig.safeParse(manifest.config), + ); + + const taskGraph = buildTaskGraph(gcx, config); + + const tasksCtx = getTasksCtx(gcx); + tasksCtx.config = config; + tasksCtx.taskGraph = taskGraph; + + return tasksCtx; } - command( + commands( gcx: GhjkCtx, tcx: TasksCtx, ) { - const commands = Object.entries(tcx.config.tasks).map( - ([name, task]) => { - let cliffyCmd = new cliffy_cmd.Command() - .name(name) - .useRawArgs() - .action(async (_, ...args) => { - await using execCx = await execCtxFromGhjk(gcx); - await execTask( - execCx, - tcx.config, - tcx.taskGraph, - name, - args, - ); - }); - if (task.desc) { - cliffyCmd = cliffyCmd.description(task.desc); - } - - return cliffyCmd; - }, - ); - let root: cliffy_cmd.Command = new cliffy_cmd.Command() + const namedSet = new Set(tcx.config.tasksNamed); + const commands = Object.keys(tcx.config.tasks) + .sort() + .map( + (key) => { + const def = tcx.config.tasks[key]; + const cmd = new cliffy_cmd.Command() + .name(key) + .useRawArgs() + .action(async (_, ...args) => { + await execTask( + gcx, + tcx.config, + tcx.taskGraph, + key, + args, + ); + }); + if (def.desc) { + cmd.description(def.desc); + } + if (!namedSet.has(key)) { + cmd.hidden(); + } + return cmd; + }, + ); + 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/modules/tasks/types.ts b/modules/tasks/types.ts index 33025838..4472aca1 100644 --- a/modules/tasks/types.ts +++ b/modules/tasks/types.ts @@ -1,50 +1,78 @@ //! NOTE: type FooX is a version of Foo after zod processing/transformation import { zod } from "../../deps/common.ts"; -import portsValidator from "../ports/types.ts"; +import envsValidators from "../envs/types.ts"; const taskName = zod.string().regex(/[^\s]/); -const taskEnvBase = zod.object({ - installs: zod.string().array(), - env: zod.record(zod.string(), zod.string()), +const taskDefBase = zod.object({ + ty: zod.string(), + desc: zod.string().nullish(), + workingDir: zod.string().nullish(), + dependsOn: zod.string().array().nullish(), }); -const taskEnv = taskEnvBase.merge(zod.object({ - allowedPortDeps: zod.string().array(), +const taskDefFullBase = taskDefBase.merge(zod.object({ + env: envsValidators.envRecipe.optional(), })); -const taskEnvX = taskEnvBase.merge(zod.object({ - allowedPortDeps: portsValidator.allowedPortDep.array(), +const taskDefHashedBase = taskDefBase.merge(zod.object({ + envKey: zod.string(), })); -const taskDefBase = zod.object({ - name: zod.string(), - dependsOn: taskName.array(), - desc: zod.string().nullish(), +const denoWorkerTaskDefBase = zod.object({ + ty: zod.literal("denoFile@v1"), + /** + * A single module might host multiple tasks so we need keys to identify + * each with. Names aren't enough since some tasks are anonymous. + */ + // This field primarily exists as an optimization actually. + // The tasksModuleConfig keys the tasks by their hash + // but we use a separate key when asking for exec from the denoFile. + // This is because the denoFile only constructs the hashes for the config + // laziliy but uses separate task keys internally due to different hashing concerns. + // This key will correspond to the internal keys used by the denoFile + // and not the config. + key: zod.string(), }); -const taskDef = taskDefBase.merge(zod.object({ - env: taskEnv, -})); -const taskDefX = taskDefBase.merge(zod.object({ - env: taskEnvX, -})); +const denoWorkerTaskDef = taskDefFullBase.merge(denoWorkerTaskDefBase); +const denoWorkerTaskDefHashed = taskDefHashedBase.merge(denoWorkerTaskDefBase); + +const taskDef = + // zod.discriminatedUnion("ty", [ + denoWorkerTaskDef; +// ]); + +const taskDefHashed = + // zod.discriminatedUnion("ty", [ + denoWorkerTaskDefHashed; +// ]); const tasksModuleConfig = zod.object({ - tasks: zod.record(taskName, taskDef), -}); -const tasksModuleConfigX = zod.object({ - tasks: zod.record(taskName, taskDefX), + /** + * Tasks can be keyed with any old string. The keys + * that also appear in {@field tasksNamed} will shown + * in the CLI. + */ + tasks: zod.record(zod.string(), taskDefHashed), + tasksNamed: taskName.array(), }); -export default { + +const validators = { taskDef, + taskDefHashed, + denoWorkerTaskDefHashed, + denoWorkerTaskDef, tasksModuleConfig, }; +export default validators; + +export type TaskDef = zod.input; +export type TaskDefX = zod.infer; + +export type TaskDefHashed = zod.input; +export type TaskDefHashedX = zod.infer; -export type TaskEnv = zod.input; -export type TaskEnvX = zod.infer; -export type TaskDef = zod.input; -export type TaskDefX = zod.infer; -export type TasksModuleConfig = zod.input; -export type TasksModuleConfigX = zod.infer; +export type TasksModuleConfig = zod.input; +export type TasksModuleConfigX = zod.infer; diff --git a/modules/types.ts b/modules/types.ts index ae7fc499..f1c23ced 100644 --- a/modules/types.ts +++ b/modules/types.ts @@ -1,8 +1,11 @@ import { zod } from "../deps/common.ts"; +import type { Path } from "../utils/mod.ts"; // TODO: better module ident/versioning const moduleId = zod.string().regex(/[^ @]*/); +const envVarName = zod.string().regex(/[a-zA-Z-_]*/); + const moduleManifest = zod.object({ id: moduleId, config: zod.unknown(), @@ -11,13 +14,14 @@ const moduleManifest = zod.object({ export type ModuleId = zod.infer; export type ModuleManifest = zod.infer; export type GhjkCtx = { - ghjkfilePath: string; - ghjkDir: string; - ghjkShareDir: string; - state: Map; + ghjkfilePath?: Path; + ghjkDir: Path; + ghjkShareDir: Path; + blackboard: Map; }; export default { moduleManifest, moduleId, + envVarName, }; diff --git a/ports/act.ts b/ports/act.ts index bb6658ac..019f2362 100644 --- a/ports/act.ts +++ b/ports/act.ts @@ -7,7 +7,6 @@ import { type InstallArgs, type InstallConfigSimple, osXarch, - std_fs, std_path, unarchive, } from "../port.ts"; @@ -80,13 +79,20 @@ export class Port extends GithubReleasePort { await unarchive(fileDwnPath, args.tmpDirPath); + const tmpDir = $.path(args.tmpDirPath); + const binDir = await tmpDir.join("bin").ensureDir(); + for ( + const fileName of ["act"] + ) { + await tmpDir.join( + args.platform.os == "windows" ? fileName + ".exe" : fileName, + ).renameToDir(binDir); + } + const installPath = $.path(args.installPath); if (await installPath.exists()) { await installPath.remove({ recursive: true }); } - await std_fs.copy( - args.tmpDirPath, - installPath.join("bin").toString(), - ); + await tmpDir.rename(installPath); } } diff --git a/ports/asdf.ts b/ports/asdf.ts index d2427db1..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, }; } @@ -150,3 +150,17 @@ export class Port extends PortBase { }); } } +/* +interface ASDF_CONFIG_EXAMPLE { + ASDF_INSTALL_TYPE: "version" | "ref"; + ASDF_INSTALL_VERSION: string; // full version number or Git Ref depending on ASDF_INSTALL_TYPE + ASDF_INSTALL_PATH: string; // the path to where the tool should, or has been installed + ASDF_CONCURRENCY: number; // the number of cores to use when compiling the source code. Useful for setting make -j + ASDF_DOWNLOAD_PATH: string; // the path to where the source code or binary was downloaded to by bin/download + ASDF_PLUGIN_PATH: string; // the path the plugin was installed + ASDF_PLUGIN_SOURCE_URL: string; // the source URL of the plugin + ASDF_PLUGIN_PREV_REF: string; // prevous git-ref of the plugin repo + ASDF_PLUGIN_POST_REF: string; // updated git-ref of the plugin repo + ASDF_CMD_FILE: string; // resolves to the full path of the file being sourced +} +*/ 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/cargo-binstall.ts b/ports/cargo-binstall.ts index 40c8ea6a..9c594dca 100644 --- a/ports/cargo-binstall.ts +++ b/ports/cargo-binstall.ts @@ -7,7 +7,6 @@ import { InstallArgs, InstallConfigSimple, osXarch, - std_fs, std_path, unarchive, } from "../port.ts"; @@ -75,13 +74,18 @@ export class Port extends GithubReleasePort { await unarchive(fileDwnPath, args.tmpDirPath); + const tmpDir = $.path(args.tmpDirPath); + const binDir = await tmpDir.join("bin").ensureDir(); + for ( + const fileName of ["cargo-binstall", "detect-targets", "detect-wasi"] + ) { + await tmpDir.join(fileName).renameToDir(binDir); + } + const installPath = $.path(args.installPath); if (await installPath.exists()) { await installPath.remove({ recursive: true }); } - await std_fs.copy( - args.tmpDirPath, - std_path.resolve(args.installPath, "bin"), - ); + await tmpDir.rename(installPath); } } 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 72cec7bd..3c19603c 100644 --- a/ports/cpy_bs.ts +++ b/ports/cpy_bs.ts @@ -10,7 +10,6 @@ import { depExecShimPath, downloadFile, dwnUrlOut, - exponentialBackoff, osXarch, PortBase, std_fs, @@ -36,7 +35,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"]), }; @@ -79,7 +78,7 @@ export class Port extends PortBase { async latestMeta(headers: Record) { const meta = await $.withRetries({ count: 10, - delay: exponentialBackoff(1000), + delay: $.exponentialBackoff(1000), action: async () => await $.request( `https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/latest-release/latest-release.json`, @@ -113,7 +112,7 @@ export class Port extends PortBase { // on every release const metadata = await $.withRetries({ count: 10, - delay: exponentialBackoff(1000), + delay: $.exponentialBackoff(1000), action: async () => await $.request( `https://api.github.com/repos/${this.repoOwner}/${this.repoName}/releases/tags/${tag}`, diff --git a/ports/deno_ghrel.ts b/ports/deno_ghrel.ts new file mode 100644 index 00000000..9da0d050 --- /dev/null +++ b/ports/deno_ghrel.ts @@ -0,0 +1,76 @@ +import { + $, + DownloadArgs, + dwnUrlOut, + GithubReleasePort, + InstallArgs, + InstallConfigSimple, + osXarch, + std_path, + unarchive, +} from "../port.ts"; +import { GithubReleasesInstConf, readGhVars } from "../modules/ports/ghrel.ts"; + +const manifest = { + ty: "denoWorker@v1" as const, + name: "deno_ghrel", + version: "0.1.0", + moduleSpecifier: import.meta.url, + platforms: osXarch(["linux", "darwin", "windows"], ["aarch64", "x86_64"]), +}; + +export default function conf( + config: + & InstallConfigSimple + & GithubReleasesInstConf = {}, +) { + return { + ...readGhVars(), + ...config, + port: manifest, + }; +} + +export class Port extends GithubReleasePort { + repoOwner = "denoland"; + repoName = "deno"; + + downloadUrls(args: DownloadArgs) { + const { installVersion, platform } = args; + const arch = platform.arch; + let os; + switch (platform.os) { + case "linux": + os = "unknown-linux-gnu"; + break; + case "windows": + os = "windows-msvc"; + break; + case "darwin": + os = "apple-darwin"; + break; + default: + throw new Error(`unsupported: ${platform}`); + } + return [ + this.releaseArtifactUrl( + installVersion, + `deno-${arch}-${os}.zip`, + ), + ].map(dwnUrlOut); + } + + async install(args: InstallArgs) { + const [{ name: fileName }] = this.downloadUrls(args); + + const fileDwnPath = std_path.resolve(args.downloadPath, fileName); + await unarchive(fileDwnPath, args.tmpDirPath); + + const installPath = $.path(args.installPath); + if (await installPath.exists()) { + await installPath.remove({ recursive: true }); + } + await $.path(args.tmpDirPath) + .rename(await installPath.join("bin").ensureDir()); + } +} 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 584b77a1..97e6c696 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/mod.ts b/ports/mod.ts index 25be49d5..c9851c14 100644 --- a/ports/mod.ts +++ b/ports/mod.ts @@ -4,6 +4,7 @@ export { default as cargo_binstall } from "./cargo-binstall.ts"; export { default as cargobi } from "./cargobi.ts"; export { default as cpy_bs } from "./cpy_bs.ts"; export { default as curl } from "./curl.ts"; +export { default as deno_ghrel } from "./deno_ghrel.ts"; export { default as earthly } from "./earthly.ts"; export { default as git } from "./git.ts"; export { default as infisical } from "./infisical.ts"; diff --git a/ports/mold.ts b/ports/mold.ts index 5ebef9a8..3d0f7ce9 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, ], @@ -92,7 +92,7 @@ export class Port extends GithubReleasePort { ); if ((args.config as unknown as MoldInstallConfig).replaceLd) { await installPath.join("bin", "ld") - .createSymlinkTo(installPath.join("bin", "mold").toString(), { + .symlinkTo(installPath.join("bin", "mold").toString(), { kind: "relative", }); } 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..b05ba128 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 @@ -141,7 +141,7 @@ export class Port extends PortBase { await tmpDirPath.join("bin").ensureDir(); for (const [name] of bins) { await tmpDirPath.join("bin", name) - .createSymlinkTo( + .symlinkTo( installPath .join("node_modules", ".bin", name) .toString(), diff --git a/ports/pipi.ts b/ports/pipi.ts index 96d54567..3079aa09 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]), @@ -146,7 +146,7 @@ export class Port extends PortBase { // the cpy_bs port smuggles out the real path of it's python executable const realPyExecPath = args.depArts[std_ports.cpy_bs_ghrel.name].env.REAL_PYTHON_EXEC_PATH; - (await venvPath.join("bin", "python3").remove()).createSymlinkTo( + (await venvPath.join("bin", "python3").remove()).symlinkTo( realPyExecPath, ); 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..132f2d6a 100644 --- a/ports/temporal_cli.ts +++ b/ports/temporal_cli.ts @@ -6,7 +6,6 @@ import { InstallArgs, InstallConfigSimple, osXarch, - std_fs, std_path, unarchive, } from "../port.ts"; @@ -17,7 +16,6 @@ const manifest = { name: "temporal_cli_ghrel", version: "0.1.0", moduleSpecifier: import.meta.url, - deps: [], platforms: osXarch(["linux", "darwin", "windows"], ["aarch64", "x86_64"]), }; @@ -63,13 +61,20 @@ export class Port extends GithubReleasePort { const fileDwnPath = std_path.resolve(args.downloadPath, fileName); await unarchive(fileDwnPath, args.tmpDirPath); + const tmpDir = $.path(args.tmpDirPath); + const binDir = await tmpDir.join("bin").ensureDir(); + for ( + const fileName of ["temporal"] + ) { + await tmpDir.join( + args.platform.os == "windows" ? fileName + ".exe" : fileName, + ).renameToDir(binDir); + } + const installPath = $.path(args.installPath); if (await installPath.exists()) { await installPath.remove({ recursive: true }); } - await std_fs.copy( - args.tmpDirPath, - installPath.join("bin").toString(), - ); + await tmpDir.rename(installPath); } } 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/std.ts b/std.ts new file mode 100644 index 00000000..ff0c8c64 --- /dev/null +++ b/std.ts @@ -0,0 +1,2 @@ +export * from "./std/copyLock.ts"; +export * from "./std/sedLock.ts"; diff --git a/std/copyLock.ts b/std/copyLock.ts new file mode 100644 index 00000000..ad3339f5 --- /dev/null +++ b/std/copyLock.ts @@ -0,0 +1,46 @@ +import { $, expandGlobsAndAbsolutize, Path } from "../utils/mod.ts"; +import { std_fs } from "../deps/common.ts"; + +/** + * Copies the files under the key to the locations in the values. + * + * Supports globs. + */ +export async function copyLock( + wd: Path, + map: Record, + opts?: Omit, +): Promise { + let dirty = false; + await $.co( + Object.entries(map) + .map(async ([file, copies]) => { + const url = wd.resolve(file); + const text = await url.readText(); + + await $.co( + copies.map(async (pathOrGlob) => { + const paths = await expandGlobsAndAbsolutize( + pathOrGlob, + wd.toString(), + opts, + ); + + await $.co(paths.map(async (copy) => { + const copyUrl = $.path(copy); + const copyText = await copyUrl.readText(); + + if (copyText != text) { + copyUrl.writeText(text); + $.logStep(`Updated ${wd.relative(copyUrl)}`); + dirty = true; + } else { + $.logLight(`No change ${wd.relative(copyUrl)}`); + } + })); + }), + ); + }), + ); + return dirty; +} diff --git a/std/sedLock.ts b/std/sedLock.ts new file mode 100644 index 00000000..9219b807 --- /dev/null +++ b/std/sedLock.ts @@ -0,0 +1,111 @@ +import { $, Path, unwrapZodRes } from "../utils/mod.ts"; +import { std_fs, zod } from "../deps/common.ts"; + +export const lockfileValidator = zod.object({ + /** + * A map of paths/globs => regexp strings => replacements. + * + * Rexeps are expected to have two match groups. + * Replacement will be placed between the two groups. + * TODO: use named match groups instead. + */ + lines: zod.record( + zod.string(), + zod.tuple([ + zod.union([zod.string(), zod.instanceof(RegExp)]), + zod.string(), + ]).array(), + ), + ignores: zod.string().array().nullish(), +}); + +export type GrepLockfile = zod.input; + +/** + * Find and replace a set of strings across a directory. + * Useful to keep certain strings consistent across changes. + * + * It will throw an error if not even one hit is found for each pattern. + * + * Avoid globstars over your entire working dir unless you're being careful + * with your ignores. + */ +export async function sedLock( + workingDir: Path, + lockfileIn: GrepLockfile, +): Promise { + const { lines, ignores } = unwrapZodRes( + lockfileValidator.safeParse(lockfileIn), + ); + + let dirty = false; + + await $.co( + Object + .entries(lines) + .map(async ([glob, lookups]) => { + const paths = await Array.fromAsync( + std_fs.expandGlob(glob, { + root: workingDir.toString(), + includeDirs: false, + globstar: true, + exclude: ignores ?? [], + }), + ); + + if (paths.length == 0) { + throw new Error( + `No files found for ${glob}, please check and retry.`, + ); + } + + const matches = Object.fromEntries( + lookups.map(([key]) => [key.toString(), 0]), + ); + + await $.co( + paths.map(async ({ path: pathStr }) => { + const path = $.path(pathStr); + const text = await path.readText(); + const rewrite = [...text.split("\n")]; + + for (const [pattern, replacement] of lookups) { + const regex = typeof pattern == "string" + ? new RegExp(pattern) + : pattern; + + for (let i = 0; i < rewrite.length; i += 1) { + if (regex.test(rewrite[i])) { + matches[pattern.toString()] += 1; + } + + rewrite[i] = rewrite[i].replace( + regex, + `$1${replacement}$2`, + ); + } + } + + const newText = rewrite.join("\n"); + if (text != newText) { + await path.writeText(newText); + $.logStep(`Updated ${workingDir.relative(path)}`); + dirty = true; + } else { + // $.logLight(`No change ${workingDir.relative(path)}`); + } + }), + ); + + for (const [pattern, count] of Object.entries(matches)) { + if (count == 0) { + throw new Error( + `No matches found for ${pattern} in ${glob}, please check and retry.`, + ); + } + } + }), + ); + + return dirty; +} 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/envHooks.ts b/tests/envHooks.ts new file mode 100644 index 00000000..963ea072 --- /dev/null +++ b/tests/envHooks.ts @@ -0,0 +1,93 @@ +import "../setup_logger.ts"; +import { E2eTestCase, harness } from "./utils.ts"; + +const posixInteractiveScript = ` +set -eux +export GHJK_WD=$PWD + +# hook creates a marker file +[ "$GHJK_ENV" = 'main' ] || exit 111 +[ $(cat "$GHJK_WD/marker") = 'remark' ] || exit 101 + +pushd ../ +# marker should be gone by now +[ ! -e "$GHJK_WD/marker" ] || exit 102 + +# cd back in +popd + +# marker should be avail now +[ $(cat $GHJK_WD/marker) = 'remark' ] || exit 103 +`; + +const fishScript = ` +set fish_trace 1 +export GHJK_WD=$PWD + +# hook creates a marker file +test (cat "$GHJK_WD/marker") = 'remark'; or exit 101 + +pushd ../ +# marker should be gone by now +not test -e "$GHJK_WD/marker"; or exit 102 + +# cd back in +popd + +# marker should be avail now +test (cat $GHJK_WD/marker) = 'remark'; or exit 103 +`; + +const fishInteractiveScript = [ + // simulate interactive mode by emitting prexec after each line + // after each line + ...fishScript + .split("\n").flatMap((line) => [ + line, + `emit fish_preexec`, + ]), +] + .join("\n"); + +type CustomE2eTestCase = Omit & { + ePoint: string; + stdin: string; +}; + +const cases: CustomE2eTestCase[] = [ + { + name: "bash_interactive", + // -s: read from stdin + // -l: login mode + // -i: make it interactive + ePoint: Deno.env.get("GHJK_TEST_E2E_TYPE") == "local" + ? `bash --rcfile $BASH_ENV -si` // we don't want to use the system rcfile + : `bash -sil`, + stdin: posixInteractiveScript, + }, + { + name: "zsh_interactive", + ePoint: `zsh -sil`, + stdin: posixInteractiveScript + .split("\n").filter((line) => !/^#/.test(line)).join("\n"), + }, + { + name: "fish_interactive", + ePoint: `fish -il`, + stdin: fishInteractiveScript, + }, +]; + +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: ` +export { sophon } from "$ghjk/hack.ts"; +import { task, env } from "$ghjk/hack.ts"; + +env("main") + .onEnter(task($ => $\`/bin/sh -c 'echo remark > marker'\`)) + .onExit(task($ => $\`/bin/sh -c 'rm marker'\`)) +`, + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + name: `envHooks/${testCase.name}`, +}))); diff --git a/tests/envs.ts b/tests/envs.ts new file mode 100644 index 00000000..7318226a --- /dev/null +++ b/tests/envs.ts @@ -0,0 +1,333 @@ +import "../setup_logger.ts"; +import { + E2eTestCase, + type EnvDefArgs, + genTsGhjkFile, + harness, +} from "./utils.ts"; +import dummy from "../ports/dummy.ts"; +import type { FileArgs } from "../mod.ts"; + +type CustomE2eTestCase = + & Omit + & { + ePoint: string; + stdin: string; + } + & ( + | { + envs: EnvDefArgs[]; + secureConfig?: FileArgs; + } + | { + ghjkTs: string; + } + ); + +const envVarTestEnvs: EnvDefArgs[] = [ + { + name: "main", + vars: { + SONG: "ditto", + }, + }, + { + name: "sss", + vars: { + SING: "Seoul Sonyo Sound", + }, + }, + { + name: "yuki", + inherit: false, + vars: { + HUMM: "Soul Lady", + }, + }, +]; +const envVarTestsPosix = ` +set -ex +# by default, we should be in main +[ "$SONG" = "ditto" ] || exit 101 +[ "$GHJK_ENV" = "main" ] || exit 1011 + +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 +[ "$GHJK_ENV" = "sss" ] || exit 1012 + +# 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 +[ "$GHJK_ENV" = "main" ] || exit 1013 + +# 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 +[ "$GHJK_ENV" = "yuki" ] || exit 1014 +`; +const envVarTestsFish = ` +set fish_trace 1 +# by default, we should be in main +test "$SONG" = "ditto"; or exit 101; +test "$GHJK_ENV" = "main"; or exit 1010; + +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 +test "$GHJK_ENV" = "sss"; or exit 1011; + +# 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 +test "$GHJK_ENV" = "main"; or exit 1012; + +# 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 +test "$GHJK_ENV" = "yuki"; or exit 1013; +`; + +const installTestEnvs: EnvDefArgs[] = [ + { + name: "main", + installs: [ + dummy({ output: "main" }), + ], + }, + { + name: "foo", + inherit: 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 = ` +set fish_trace 1 +# 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, + }, + { + name: "default_env_loader", + ePoint: "fish", + envs: envVarTestEnvs, + secureConfig: { defaultEnv: "yuki" }, + stdin: ` +set fish_trace 1 +# env base is false for "yuki" and thus no vars from "main" +test "$GHJK_ENV" = "yuki"; or exit 106 +test "$SONG" = "ditto"; and exit 107 +test "$HUMM" = "Soul Lady"; or exit 108 +`, + }, + { + name: "env_inherit_from_envs", + ePoint: "fish", + envs: [], + secureConfig: { + defaultEnv: "e1", + envs: [ + { name: "e1", inherit: "e2" }, + { + name: "e2", + vars: { HEY: "hello" }, + }, + ], + }, + stdin: ` +set fish_trace 1 +test "$GHJK_ENV" = "e1"; or exit 101 +test "$HEY" = "hello"; or exit 102 +`, + }, + { + name: "task_inherit_from_envs", + ePoint: "fish", + envs: [], + secureConfig: { + envs: [{ name: "e1", vars: { HEY: "hello" } }], + tasks: { t1: { inherit: "e1", fn: ($) => $`echo $HEY` } }, + }, + stdin: ` +set fish_trace 1 +test (ghjk x t1) = "hello"; or exit 102 +`, + }, + { + name: "env_inherit_from_tasks", + ePoint: "fish", + envs: [], + secureConfig: { + defaultEnv: "e1", + envs: [{ name: "e1", inherit: "t1" }], + tasks: { t1: { vars: { HEY: "hello" } } }, + }, + stdin: ` +set fish_trace 1 +test "$GHJK_ENV" = "e1"; or exit 101 +test "$HEY" = "hello"; or exit 102 +`, + }, + { + name: "task_inherit_from_task", + ePoint: "fish", + envs: [], + secureConfig: { + tasks: { + t1: { vars: { HEY: "hello" }, fn: ($) => $`echo fake` }, + t2: { + inherit: "t1", + fn: ($) => $`echo $HEY`, + }, + }, + }, + stdin: ` +set fish_trace 1 +test (ghjk x t2) = "hello"; or exit 102 +`, + }, + { + name: "hereditary", + ePoint: "fish", + envs: [ + { name: "e1", vars: { E1: "1" }, installs: [dummy({ output: "e1" })] }, + { + name: "e2", + inherit: "e1", + vars: { E2: "2" }, + }, + { + name: "e3", + inherit: "e2", + vars: { E3: "3" }, + }, + ], + stdin: ` +set fish_trace 1 +ghjk envs cook e3 +. .ghjk/envs/e3/activate.fish +test "$E1" = "1"; or exit 101 +test "$E2" = "2"; or exit 102 +test "$E3" = "3"; or exit 103 +test (dummy) = "e1"; or exit 104 +`, // TODO: test inheritance of more props + }, + { + name: "inheritance_diamond", + ePoint: "fish", + envs: [ + { name: "e1", vars: { E1: "1" }, installs: [dummy({ output: "e1" })] }, + { + name: "e2", + inherit: "e1", + vars: { E2: "2" }, + }, + { + name: "e3", + inherit: "e1", + vars: { E3: "3" }, + }, + { + name: "e4", + inherit: ["e2", "e3"], + vars: { E4: "4" }, + }, + ], + stdin: ` +set fish_trace 1 +ghjk envs cook e4 +. .ghjk/envs/e4/activate.fish +test "$E1" = "1"; or exit 101 +test "$E2" = "2"; or exit 102 +test "$E3" = "3"; or exit 103 +test "$E4" = "4"; or exit 104 +test (dummy) = "e1"; or exit 105 +`, // TODO: test inheritance of more props + }, +]; + +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: "ghjkTs" in testCase ? testCase.ghjkTs : genTsGhjkFile( + { + secureConf: { + ...testCase.secureConfig, + envs: [...testCase.envs, ...(testCase.secureConfig?.envs ?? [])], + }, + }, + ), + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + name: `envs/${testCase.name}`, +}))); diff --git a/tests/hooks.ts b/tests/hooks.ts deleted file mode 100644 index baa8f9a0..00000000 --- a/tests/hooks.ts +++ /dev/null @@ -1,202 +0,0 @@ -import "../setup_logger.ts"; -import { - dockerE2eTest, - E2eTestCase, - localE2eTest, - tsGhjkFileFromInstalls, -} from "./utils.ts"; -import dummy from "../ports/dummy.ts"; -import type { InstallConfigFat } from "../port.ts"; - -const posixInteractiveScript = ` -set -eux -[ "$DUMMY_ENV" = "dummy" ] || exit 101 -dummy - -# it should be avail in subshells -sh -c '[ "$DUMMY_ENV" = "dummy" ]' || exit 105 -sh -c "dummy" - -pushd ../ -# it shouldn't be avail here -[ $(set +e; dummy) ] && exit 102 -[ "$DUMMY_ENV" = "dummy" ] && exit 103 - -# cd back in -popd - -# now it should be avail -dummy -[ "$DUMMY_ENV" = "dummy" ] || exit 106 -`; - -const bashInteractiveScript = [ - // simulate interactive mode by evaluating the prompt - // before each line - ` -eval_PROMPT_COMMAND() { - local prompt_command - for prompt_command in "\${PROMPT_COMMAND[@]}"; do - eval "$prompt_command" - done -} -`, - ...posixInteractiveScript - .split("\n").map((line) => - `eval_PROMPT_COMMAND -${line} -` - ), -] - .join("\n"); - -const zshInteractiveScript = [ - // simulate interactive mode by evaluating precmd - // before each line - ...posixInteractiveScript - .split("\n").map((line) => - `precmd -${line} -` - ), -] - .join("\n"); - -const posixNonInteractiveScript = ` -set -eux - -# test that ghjk_reload is avail because BASH_ENV exposed by the suite -ghjk_reload -[ "$DUMMY_ENV" = "dummy" ] || exit 101 -dummy - -# it should be avail in subshells -sh -c '[ "$DUMMY_ENV" = "dummy" ]' || exit 105 -sh -c "dummy" - -pushd ../ -# no reload so it's stil avail -dummy -ghjk_reload - -# it shouldn't be avail now -[ $(set +e; dummy) ] && exit 102 -[ "$DUMMY_ENV" = "dummy" ] && exit 103 - -# cd back in -popd - -# not avail yet -[ $(set +e; dummy) ] && exit 104 -[ "$DUMMY_ENV" = "dummy" ] && exit 105 - -ghjk_reload -# now it should be avail -dummy -[ "$DUMMY_ENV" = "dummy" ] || exit 106 -`; - -const fishScript = ` -dummy; or exit 101 -test $DUMMY_ENV = "dummy"; or exit 102 - -# it should be avail in subshells -sh -c '[ "$DUMMY_ENV" = "dummy" ]'; or exit 105 -sh -c "dummy" - -pushd ../ -# it shouldn't be avail here -which dummy; and exit 103 -test $DUMMY_ENV = "dummy"; and exit 104 - -# cd back in -popd -# now it should be avail -dummy; or exit 123 -test $DUMMY_ENV = "dummy"; or exit 105 -`; - -type CustomE2eTestCase = Omit & { - installConf?: InstallConfigFat | InstallConfigFat[]; - ePoint: string; - stdin: string; -}; -const cases: CustomE2eTestCase[] = [ - { - name: "hook_test_bash_interactive", - // -s: read from stdin - // -l: login/interactive mode - ePoint: `bash -sl`, - stdin: bashInteractiveScript, - }, - { - name: "hook_test_bash_scripting", - ePoint: `bash -s`, - stdin: posixNonInteractiveScript, - }, - { - name: "hook_test_zsh_interactive", - ePoint: `zsh -sl`, - stdin: zshInteractiveScript, - }, - { - name: "hook_test_zsh_scripting", - ePoint: `zsh -s`, - stdin: posixNonInteractiveScript, - }, - { - name: "hook_test_fish_interactive", - ePoint: `fish -l`, - stdin: fishScript, - }, - { - name: "hook_test_fish_scripting", - ePoint: `fish`, - // the fish implementation triggers changes - // on any pwd changes so it's identical to - // interactive usage - stdin: fishScript, - }, -]; - -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: tsGhjkFileFromInstalls( - { installConf: testCase.installConf ?? dummy(), taskDefs: [] }, - ), - ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], - envs: { - ...defaultEnvs, - ...testCase.envs, - }, - }), - ); - } -} - -const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); -if (e2eType == "both") { - testMany("hooksDockerE2eTest", cases, dockerE2eTest); - testMany(`hooksLocalE2eTest`, cases, localE2eTest); -} else if (e2eType == "local") { - testMany("hooksLocalE2eTest", cases, localE2eTest); -} else if ( - e2eType == "docker" || - !e2eType -) { - testMany("hooksDockerE2eTest", cases, dockerE2eTest); -} else { - throw new Error( - `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, - ); -} diff --git a/tests/ports.ts b/tests/ports.ts index 12f7a78a..0a1016f2 100644 --- a/tests/ports.ts +++ b/tests/ports.ts @@ -1,27 +1,24 @@ import "../setup_logger.ts"; -import { std_async } from "../deps/dev.ts"; -import { secureConfig, stdDeps } from "../mod.ts"; -import { - dockerE2eTest, - E2eTestCase, - localE2eTest, - tsGhjkFileFromInstalls, -} from "./utils.ts"; +import { FileArgs } from "../mod.ts"; +import { E2eTestCase, genTsGhjkFile, harness } from "./utils.ts"; import * as ports from "../ports/mod.ts"; -import type { - InstallConfigFat, - PortsModuleSecureConfig, -} from "../modules/ports/types.ts"; +import dummy from "../ports/dummy.ts"; +import type { InstallConfigFat } from "../modules/ports/types.ts"; import { testTargetPlatform } from "./utils.ts"; type CustomE2eTestCase = Omit & { ePoint: string; installConf: InstallConfigFat | InstallConfigFat[]; - secureConf?: PortsModuleSecureConfig; - ignore?: boolean; + secureConf?: FileArgs; }; // 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", @@ -100,18 +97,23 @@ const cases: CustomE2eTestCase[] = [ name: "npmi-node-gyp", installConf: ports.npmi({ packageName: "node-gyp" }), ePoint: `node-gyp --version`, - secureConf: secureConfig({ - allowedPortDeps: stdDeps({ enableRuntimes: true }), - }), + secureConf: { + enableRuntimes: true, + }, }, // node + more megs { name: "npmi-jco", installConf: ports.npmi({ packageName: "@bytecodealliance/jco" }), ePoint: `jco --version`, - secureConf: secureConfig({ - allowedPortDeps: stdDeps({ enableRuntimes: true }), - }), + secureConf: { + enableRuntimes: true, + }, + }, + { + name: "deno", + installConf: ports.deno_ghrel(), + ePoint: `deno --version`, }, // 42 megs { @@ -162,9 +164,9 @@ const cases: CustomE2eTestCase[] = [ name: "pipi-poetry", installConf: ports.pipi({ packageName: "poetry" }), ePoint: `poetry --version`, - secureConf: secureConfig({ - allowedPortDeps: stdDeps({ enableRuntimes: true }), - }), + secureConf: { + enableRuntimes: true, + }, }, // rustup + 600 megs { @@ -201,65 +203,32 @@ const cases: CustomE2eTestCase[] = [ }, ]; -function testMany( - testGroup: string, - cases: CustomE2eTestCase[], - testFn: (inp: E2eTestCase) => Promise, - defaultEnvs: Record = {}, -) { - for (const testCase of cases) { - Deno.test( - { - name: `${testGroup} - ${testCase.name}`, - ignore: testCase.ignore, - fn: () => - std_async.deadline( - testFn({ - ...testCase, - tsGhjkfileStr: tsGhjkFileFromInstalls( - { - installConf: testCase.installConf, - secureConf: testCase.secureConf, - taskDefs: [], - }, - ), - ePoints: [ - ...["bash -c", "fish -c", "zsh -c"].map((sh) => ({ - cmd: `env ${sh} '${testCase.ePoint}'`, - })), - // 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 ports sync'" }, - ], - envs: { - ...defaultEnvs, - ...testCase.envs, - }, - }), - // building the test docker image might taka a while - // but we don't want some bug spinlocking the ci for - // an hour - 300_000, - ), +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: genTsGhjkFile( + { + secureConf: { + ...testCase.secureConf, + installs: Array.isArray(testCase.installConf) + ? testCase.installConf + : [testCase.installConf], }, - ); - } -} - -const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); -if (e2eType == "both") { - testMany("portsDockerE2eTest", cases, dockerE2eTest); - testMany(`portsLocalE2eTest`, cases, localE2eTest); -} else if (e2eType == "local") { - testMany("portsLocalE2eTest", cases, localE2eTest); -} else if ( - e2eType == "docker" || - !e2eType -) { - testMany("portsDockerE2eTest", cases, dockerE2eTest); -} else { - throw new Error( - `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, - ); -} + }, + ), + ePoints: [ + ...["bash -c", "fish -c", "zsh -c"].map((sh) => ({ + cmd: [...`env ${sh}`.split(" "), `"${testCase.ePoint}"`], + })), + /* // FIXME: better tests for the `InstallDb` + // installs db means this shouldn't take too long + // as it's the second sync + { + cmd: [ + ..."env".split(" "), + "bash -c 'timeout 1 ghjk envs cook'", + ], + }, */ + ], + name: `ports/${testCase.name}`, + timeout_ms: 10 * 60 * 1000, +}))); diff --git a/tests/portsOutdated.ts b/tests/portsOutdated.ts new file mode 100644 index 00000000..e079279f --- /dev/null +++ b/tests/portsOutdated.ts @@ -0,0 +1,57 @@ +import "../setup_logger.ts"; +import { E2eTestCase, genTsGhjkFile, harness } from "./utils.ts"; +import * as ports from "../ports/mod.ts"; +import type { InstallConfigFat } from "../modules/ports/types.ts"; +import { FileArgs } from "../mod.ts"; + +type CustomE2eTestCase = Omit & { + ePoint: string; + installConf: InstallConfigFat | InstallConfigFat[]; + secureConf?: FileArgs; +}; + +// FIXME: +const cases: CustomE2eTestCase[] = [ + { + name: "command", + installConf: [ + ports.jq_ghrel({ version: "jq-1.7" }), + ], + ePoint: `ghjk p outdated`, + secureConf: { + enableRuntimes: true, + }, + }, + { + name: "update_all", + installConf: [ + ports.jq_ghrel({ version: "jq-1.7" }), + ports.protoc({ version: "v24.0" }), + ], + ePoint: `ghjk p outdated --update-all`, + secureConf: { + enableRuntimes: true, + }, + }, +]; + +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: genTsGhjkFile( + { + secureConf: { + ...testCase.secureConf, + installs: Array.isArray(testCase.installConf) + ? testCase.installConf + : [testCase.installConf], + }, + }, + ), + ePoints: [ + ...["bash -c", "fish -c", "zsh -c"].map((sh) => ({ + cmd: [...`env ${sh}`.split(" "), `"${testCase.ePoint}"`], + })), + ], + name: `portsOutdated/${testCase.name}`, + timeout_ms: 10 * 60 * 1000, +}))); diff --git a/tests/reloadHooks.ts b/tests/reloadHooks.ts new file mode 100644 index 00000000..8f03679a --- /dev/null +++ b/tests/reloadHooks.ts @@ -0,0 +1,222 @@ +import "../setup_logger.ts"; +import { E2eTestCase, genTsGhjkFile, harness } from "./utils.ts"; +import dummy from "../ports/dummy.ts"; +import type { InstallConfigFat } from "../port.ts"; + +// TODO: test for hook reload when ghjk.ts is touched +// TODO: test for hook reload when nextfile is touched + +const posixInteractiveScript = ` +set -ex +[ "\${DUMMY_ENV:-}" = "dummy" ] || exit 101 +dummy + +# it should be avail in subshells +sh -c '[ "\${DUMMY_ENV:-}" = "dummy" ]' || exit 105 +sh -c "dummy" + +pushd ../ +# it shouldn't be avail here +set +ex +[ $(dummy) ] && exit 102 +[ "\${DUMMY_ENV:-}" = "dummy" ] && exit 103 +set -ex + +# cd back in +popd + +# now it should be avail +dummy +[ "\${DUMMY_ENV:-}" = "dummy" ] || exit 106 + +[ "\${GHJK_ENV:-}" = "main" ] || exit 107 +ghjk e cook test +echo "test" > $GHJK_NEXTFILE +[ "\${GHJK_ENV:-}" = "test" ] || exit 108 +`; + +const posixNonInteractiveScript = ` +set -eux + +# test that ghjk_reload is avail because BASH_ENV exposed by the suite +ghjk_reload +[ "\${DUMMY_ENV:-}" = "dummy" ] || exit 101 +dummy + +# it should be avail in subshells +sh -c '[ "\${DUMMY_ENV:-}" = "dummy" ]' || exit 105 +sh -c "dummy" + +pushd ../ +# no reload so it's stil avail +dummy +ghjk_reload + +# it shouldn't be avail now +[ $(set +e; dummy) ] && exit 102 +[ "\${DUMMY_ENV:-}" = "dummy" ] && exit 103 + +# cd back in +popd + +# not avail yet +[ $(set +e; dummy) ] && exit 104 +[ "\${DUMMY_ENV:-}" = "dummy" ] && exit 105 + +ghjk_reload +# now it should be avail +dummy +[ "\${DUMMY_ENV:-}" = "dummy" ] || exit 106 + +[ "\${GHJK_ENV}" = "main" ] || exit 107 +ghjk e cook test + +ghjk_reload test +[ "\${GHJK_ENV:-}" = "test" ] || exit 110 +ghjk_reload +[ "\${GHJK_ENV:-}" = "test" ] || exit 111 + +GHJK_ENV=test ghjk_reload +[ "\${GHJK_ENV:-}" = "test" ] || exit 112 +`; + +const fishScript = ` +set fish_trace 1 +which dummy; or exit 101 +test $DUMMY_ENV = "dummy"; or exit 102 + +# it should be avail in subshells +sh -c '[ "$DUMMY_ENV" = "dummy" ]'; or exit 105 +sh -c "dummy" + +pushd ../ +# it shouldn't be avail here +which dummy; and exit 103 +test $DUMMY_ENV = "dummy"; and exit 104 + +# cd back in +popd +# now it should be avail +dummy; or exit 123 +test $DUMMY_ENV = "dummy"; or exit 105 +`; + +const fishNoninteractiveScript = ` +set fish_trace 1 +# test that ghjk_reload is avail because config.fish exposed by the suite +ghjk_reload + +${fishScript} + +# must cook test first +ghjk envs cook test + +test $GHJK_ENV = "main"; or exit 107 + +# manually switch to test +ghjk_reload test +test "$GHJK_ENV" = "test"; or exit 108 + +# re-invoking reload won't go back to main +ghjk_reload +test "$GHJK_ENV" = "test"; or exit 109 + +# go back to main +ghjk_reload main +test "$GHJK_ENV" = "main"; or exit 111 + +# changing GHJK_ENV manually gets respected +GHJK_ENV=test ghjk_reload +test "$GHJK_ENV" = "test"; or exit 112`; + +const fishInteractiveScript = [ + fishScript, + // simulate interactive mode by emitting postexec after each line + // after each line + ...` +ghjk e cook test +test $GHJK_ENV = "main"; or exit 107 + +echo "test" > $GHJK_NEXTFILE +test "$GHJK_ENV" = "test"; or exit 108 + +ghjk_reload main +test "$GHJK_ENV" = "main"; or exit 111 + +GHJK_ENV=test ghjk_reload +test "$GHJK_ENV" = "test"; or exit 112 +` + .split("\n").flatMap((line) => [ + line, + `emit fish_preexec`, + ]), +] + .join("\n"); + +type CustomE2eTestCase = Omit & { + installConf?: InstallConfigFat[]; + ePoint: string; + stdin: string; +}; +const cases: CustomE2eTestCase[] = [ + { + name: "bash_interactive", + // -s: read from stdin + // -l: login mode + // -i: interactive mode + ePoint: Deno.env.get("GHJK_TEST_E2E_TYPE") == "local" + ? `bash --rcfile $BASH_ENV -si` // we don't want to use the system rcfile + : `bash -sil`, + stdin: posixInteractiveScript, + }, + { + name: "bash_scripting", + ePoint: `bash -s`, + stdin: posixNonInteractiveScript, + }, + { + name: "zsh_interactive", + ePoint: `zsh -sli`, + stdin: posixInteractiveScript + .split("\n").filter((line) => !/^#/.test(line)).join("\n"), + }, + { + name: "zsh_scripting", + ePoint: `zsh -s`, + stdin: posixNonInteractiveScript, + }, + { + name: "fish_interactive", + ePoint: `fish -il`, + stdin: fishInteractiveScript, + }, + { + name: "fish_scripting", + ePoint: `fish`, + // the fish implementation triggers changes + // on any pwd changes so it's identical to + // interactive usage + stdin: fishNoninteractiveScript, + }, +]; + +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: genTsGhjkFile( + { + secureConf: { + envs: [ + { + name: "main", + installs: testCase.installConf ? testCase.installConf : [dummy()], + }, + { + name: "test", + }, + ], + }, + }, + ), + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + name: `reloadHooks/${testCase.name}`, +}))); diff --git a/tests/tasks.ts b/tests/tasks.ts index d77941c7..5e35475e 100644 --- a/tests/tasks.ts +++ b/tests/tasks.ts @@ -1,55 +1,58 @@ import "../setup_logger.ts"; -import { - dockerE2eTest, - E2eTestCase, - localE2eTest, - type TaskDefTest, - tsGhjkFileFromInstalls, -} from "./utils.ts"; +import { E2eTestCase, genTsGhjkFile, harness, type TaskDef } from "./utils.ts"; import * as ghjk from "../mod.ts"; import * as ports from "../ports/mod.ts"; -type CustomE2eTestCase = Omit & { - ePoint: string; - stdin: string; - tasks: TaskDefTest[]; -}; +type CustomE2eTestCase = + & Omit + & { + ePoint: string; + stdin: string; + enableRuntimesOnMasterPDAL?: boolean; + } + & ( + | { + tasks: TaskDef[]; + } + | { + ghjkTs: string; + } + ); const cases: CustomE2eTestCase[] = [ { name: "base", tasks: [{ name: "greet", - fn: async ({ $, argv: [name] }) => { - await $`echo Hello ${name}!`; + fn: async ($, { argv: [name], workingDir }) => { + await $`echo Hello ${name} from ${workingDir}!`; }, }], ePoint: `fish`, stdin: ` -cat ghjk.ts -test (ghjk x greet world) = 'Hello world!'`, +test (ghjk x greet world) = "Hello world from $PWD!"`, }, { name: "env_vars", tasks: [{ name: "greet", - env: { - NAME: "moon", + vars: { + LUNA: "moon", + SOL: "sun", }, - fn: async ({ $ }) => { - await $`echo Hello $NAME!`; + fn: async ($) => { + await $`echo "Hello $SOL & ${$.env["LUNA"]!}"!`; }, }], ePoint: `fish`, stdin: ` -cat ghjk.ts -test (ghjk x greet world) = 'Hello moon!'`, +test (ghjk x greet world) = 'Hello sun & moon!'`, }, { name: "ports", tasks: [{ name: "protoc", installs: [ports.protoc()], - fn: async ({ $ }) => { + fn: async ($) => { await $`protoc --version`; }, }], @@ -61,15 +64,16 @@ ghjk x protoc`, name: "port_deps", tasks: [{ name: "test", - // node depends on tar_aa + // pipi depends on cpy_bs installs: [...ports.pipi({ packageName: "pre-commit" })], - allowedPortDeps: ghjk.stdDeps({ enableRuntimes: true }), - fn: async ({ $ }) => { + allowedBuildDeps: ghjk.stdDeps({ enableRuntimes: true }), + fn: async ($) => { await $`pre-commit --version`; }, }], ePoint: `fish`, stdin: `ghjk x test`, + enableRuntimesOnMasterPDAL: true, }, { name: "default_port_deps", @@ -77,7 +81,7 @@ ghjk x protoc`, name: "test", // node depends on tar_aa installs: [ports.node()], - fn: async ({ $ }) => { + fn: async ($) => { await $`node --version`; }, }], @@ -90,21 +94,21 @@ ghjk x protoc`, { name: "ed", dependsOn: [], - fn: async ({ $ }) => { + fn: async ($) => { await $`/bin/sh -c 'echo ed > ed'`; }, }, { name: "edd", dependsOn: ["ed"], - fn: async ({ $ }) => { + fn: async ($) => { await $`/bin/sh -c 'echo $(/bin/cat ed) edd > edd'`; }, }, { name: "eddy", dependsOn: ["edd"], - fn: async ({ $ }) => { + fn: async ($) => { await $`/bin/sh -c 'echo $(/bin/cat edd) eddy > eddy'`; }, }, @@ -115,46 +119,45 @@ ghjk x eddy test (cat eddy) = 'ed edd eddy' `, }, -]; + { + name: "anon", + ghjkTs: ` +export { sophon } from "$ghjk/hack.ts"; +import { task } from "$ghjk/hack.ts"; -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: tsGhjkFileFromInstalls( - { installConf: [], taskDefs: testCase.tasks }, - ), - ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], - envs: { - ...defaultEnvs, - ...testCase.envs, - }, - }), - ); - } -} +task({ + dependsOn: [ + task({ + dependsOn: [ + task(($) => $\`/bin/sh -c 'echo ed > ed'\`), + ], + fn: ($) => $\`/bin/sh -c 'echo $(/bin/cat ed) edd > edd'\`, + }), + ], + name: "eddy", + fn: ($) => $\`/bin/sh -c 'echo $(/bin/cat edd) eddy > eddy'\` +}); +`, + ePoint: `fish`, + stdin: ` +ghjk x eddy +test (cat eddy) = 'ed edd eddy' +`, + }, +]; -const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); -if (e2eType == "both") { - testMany("tasksDockerE2eTest", cases, dockerE2eTest); - testMany(`tasksLocalE2eTest`, cases, localE2eTest); -} else if (e2eType == "local") { - testMany("tasksLocalE2eTest", cases, localE2eTest); -} else if ( - e2eType == "docker" || - !e2eType -) { - testMany("tasksDockerE2eTest", cases, dockerE2eTest); -} else { - throw new Error( - `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, - ); -} +harness(cases.map((testCase) => ({ + ...testCase, + tsGhjkfileStr: "ghjkTs" in testCase ? testCase.ghjkTs : genTsGhjkFile( + { + secureConf: { + tasks: Object.fromEntries( + testCase.tasks.map((def) => [def.name!, def]), + ), + enableRuntimes: testCase.enableRuntimesOnMasterPDAL, + }, + }, + ), + ePoints: [{ cmd: testCase.ePoint, stdin: testCase.stdin }], + name: `tasks/${testCase.name}`, +}))); 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 }[]; + timeout_ms?: number; + ignore?: boolean; + only?: boolean; }; export const testTargetPlatform = Deno.env.get("DOCKER_PLATFORM") ?? @@ -38,13 +40,17 @@ const dFileTemplate = await importRaw(import.meta.resolve("./test.Dockerfile")); const templateStrings = { addConfig: `#{{CMD_ADD_CONFIG}}`, }; +const noRmi = Deno.env.get("DOCKER_NO_RMI"); export async function dockerE2eTest(testCase: E2eTestCase) { - const { name, envs: testEnvs, ePoints, tsGhjkfileStr } = testCase; - const tag = `ghjk_e2e_${name}`; + const { name, envVars: testEnvs, ePoints, tsGhjkfileStr } = testCase; + const tag = `ghjk_e2e_${name}`.toLowerCase(); const env = { ...testEnvs, }; + if (Deno.env.get("GITHUB_TOKEN")) { + env.GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN")!; + } const devGhjkPath = import.meta.resolve("../"); const configFile = tsGhjkfileStr @@ -52,18 +58,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 @@ -88,21 +82,27 @@ 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; + } + } + if (!noRmi) { + await $ + .raw`${dockerCmd} rmi '${tag}'` + .env(env); } - await $ - .raw`${dockerCmd} rmi '${tag}'` - .env(env); } 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_", @@ -122,6 +122,7 @@ export async function localE2eTest(testCase: E2eTestCase) { ZDOTDIR: ghjkShareDir.toString(), GHJK_SHARE_DIR: ghjkShareDir.toString(), PATH: `${ghjkShareDir.toString()}:${Deno.env.get("PATH")}`, + HOME: tmpDir.toString(), }; // install ghjk await install({ @@ -135,11 +136,14 @@ export async function localE2eTest(testCase: E2eTestCase) { // don't modify system shell configs shellsToHook: [], }); + await $`${ghjkShareDir.join("ghjk").toString()} print config` .cwd(tmpDir.toString()) + .clearEnv() .env(env); - await $`${ghjkShareDir.join("ghjk").toString()} ports sync` + await $`${ghjkShareDir.join("ghjk").toString()} envs cook` .cwd(tmpDir.toString()) + .clearEnv() .env(env); /* // print the contents of the ghjk dir for debugging purposes @@ -152,7 +156,7 @@ export async function localE2eTest(testCase: E2eTestCase) { { const confHome = await ghjkShareDir.join(".config").ensureDir(); const fishConfDir = await confHome.join("fish").ensureDir(); - await fishConfDir.join("config.fish").createSymlinkTo( + await fishConfDir.join("config.fish").symlinkTo( ghjkShareDir.join("env.fish").toString(), ); env["XDG_CONFIG_HOME"] = confHome.toString(); @@ -160,6 +164,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); @@ -169,62 +174,94 @@ export async function localE2eTest(testCase: E2eTestCase) { await tmpDir.remove({ recursive: true }); } -export type TaskDefTest = TaskDefNice & { name: string }; -export function tsGhjkFileFromInstalls( - { installConf, secureConf, taskDefs }: { - installConf: InstallConfigFat | InstallConfigFat[]; - secureConf?: PortsModuleSecureConfig; - taskDefs: TaskDefTest[]; +export type TaskDef = + & Omit + & Required>; + +export function genTsGhjkFile( + { secureConf }: { + secureConf?: FileArgs; }, ) { - const installConfArray = Array.isArray(installConf) - ? installConf - : [installConf]; - - const serializedPortsInsts = JSON.stringify( - installConfArray, - (_, val) => - typeof val == "string" - // we need to escape a json string embedded in a js string - // 2x - ? val.replaceAll(/\\/g, "\\\\") - : val, - ); const serializedSecConf = JSON.stringify( // undefined is not recognized by JSON.parse // so we stub it with null - secureConf ?? null, + { + ...secureConf, + tasks: [], + }, + // we need to escape a json string embedded in a js string + // 2x (_, val) => typeof val == "string" ? val.replaceAll(/\\/g, "\\\\") : val, + 2, ); - const tasks = taskDefs.map( - (def) => { - const { name, ...withoutName } = def; + + const tasks = Object.entries(secureConf?.tasks ?? {}).map( + ([name, def]) => { const stringifiedSection = JSON.stringify( - withoutName, + { ...def, name }, (_, val) => typeof val == "string" ? val.replaceAll(/\\/g, "\\\\") : val, + 2, ); return $.dedent` - ghjk.task("${name}", { + ghjk.task({ ...JSON.parse(\`${stringifiedSection}\`), - fn: ${def.fn.toString()} + fn: ${def.fn?.toString()} })`; }, ).join("\n"); + return ` -export { ghjk } from "$ghjk/mod.ts"; -import * as ghjk from "$ghjk/mod.ts"; +import { file } from "$ghjk/mod.ts"; + const confStr = \` -${serializedPortsInsts} +${serializedSecConf} \`; const confObj = JSON.parse(confStr); -ghjk.install(...confObj) +const ghjk = file(confObj); -const secConfStr = \` -${serializedSecConf} -\`; -export const secureConfig = JSON.parse(secConfStr); +export const sophon = ghjk.sophon; ${tasks} + `; } + +export function harness( + cases: E2eTestCase[], +) { + const e2eType = Deno.env.get("GHJK_TEST_E2E_TYPE"); + let runners = [[dockerE2eTest, "e2eDocker" as string] as const]; + if (e2eType == "both") { + runners.push([localE2eTest, "e2eLocal"]); + } else if (e2eType == "local") { + runners = [[localE2eTest, "e2eLocal"]]; + } else if ( + e2eType && e2eType != "docker" + ) { + throw new Error( + `unexpected GHJK_TEST_E2E_TYPE: ${e2eType}`, + ); + } + for (const [runner, group] of runners) { + for (const testCase of cases) { + Deno.test( + `${group}/${testCase.name}`, + { + ignore: testCase.ignore, + }, + () => + std_async.deadline( + runner({ + ...testCase, + }), + // building the test docker image might taka a while + // but we don't want some bug spinlocking the ci for + // an hour + testCase.timeout_ms ?? 5 * 60 * 1000, + ), + ); + } + } +} diff --git a/utils/logger.ts b/utils/logger.ts index 839b39d4..e6382011 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); } @@ -163,7 +164,7 @@ Deno.permissions.query({ } }); -export function isColorfulTty(outFile = Deno.stdout) { +export function isColorfulTty(outFile = Deno.stderr) { if (colorEnvFlagSet) { return true; } diff --git a/utils/mod.ts b/utils/mod.ts index 960f17ff..c1482348 100644 --- a/utils/mod.ts +++ b/utils/mod.ts @@ -1,11 +1,19 @@ import { + _DaxPath as Path, dax, - jsonHash, + json_canonicalize, + multibase32, + multihasher, + multisha2, std_fs, std_path, - std_url, + syncSha256, zod, + zod_val_err, } from "../deps/common.ts"; +// class re-exports are tricky. We want al importers +// of path to get it from here so we rename in common.ts +export { _DaxPath as Path } from "../deps/common.ts"; import logger, { isColorfulTty } from "./logger.ts"; // NOTE: only use type imports only when getting stuff from "./modules" import type { @@ -17,7 +25,7 @@ import type { PortManifest, } from "../modules/ports/types.ts"; -export type DePromisify = T extends Promise ? Inner : T; +export type DeArrayify = T extends Array ? Inner : T; const literalSchema = zod.union([ zod.string(), zod.number(), @@ -46,13 +54,13 @@ export function pathsWithDepArts( const includesSet = new Set(); for (const [_, { execs, libs, includes }] of Object.entries(depArts)) { for (const [_, binPath] of Object.entries(execs)) { - pathSet.add(std_path.dirname(binPath)); + pathSet.add($.path(binPath).parentOrThrow()); } for (const [_, libPath] of Object.entries(libs)) { - libSet.add(std_path.dirname(libPath)); + libSet.add($.path(libPath).parentOrThrow()); } for (const [_, incPath] of Object.entries(includes)) { - includesSet.add(std_path.dirname(incPath)); + includesSet.add($.path(incPath).parentOrThrow()); } } @@ -122,85 +130,140 @@ export function tryDepExecShimPath( return path; } -// 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), - (b) => b.toString(16).padStart(2, "0"), - ).join(""); +const syncSha256Hasher = multihasher.from({ + code: multisha2.sha256.code, + name: multisha2.sha256.name, + encode: (input) => syncSha256(input), +}); + +export async function bufferHashAsync( + buf: Uint8Array, +) { + const hashBuf = await multisha2.sha256.digest(buf); + const hashStr = multibase32.base32.encode(hashBuf.bytes); + return hashStr; } -export async function bufferHashHex( - buf: ArrayBuffer, - algo: AlgorithmIdentifier = "SHA-256", +export function bufferHash( + buf: Uint8Array, ) { - const hashBuf = await crypto.subtle.digest(algo, buf); - return bufferToHex(hashBuf); + const hashBuf = syncSha256Hasher.digest(buf); + if (hashBuf instanceof Promise) throw new Error("impossible"); + const hashStr = multibase32.base32.encode(hashBuf.bytes); + return hashStr; } -export async function stringHashHex( + +export function stringHash( val: string, - algo: AlgorithmIdentifier = "SHA-256", ) { const arr = new TextEncoder().encode(val); - return await bufferHashHex(arr, algo); + return bufferHash(arr); } -export async function objectHashHex( - object: jsonHash.Tree, - algo: jsonHash.DigestAlgorithmType = "SHA-256", +export function objectHash( + object: Json, ) { - const hashBuf = await jsonHash.digest(algo, object); - const hashHex = bufferToHex(hashBuf); - return hashHex; + return stringHash(json_canonicalize(object)); } export function getPortRef(manifest: PortManifest) { return `${manifest.name}@${manifest.version}`; } -export async function getInstallHash(install: InstallConfigResolvedX) { - const fullHashHex = await objectHashHex(install as jsonHash.Tree); - const hashHex = fullHashHex.slice(0, 8); - return `${install.portRef}+${hashHex}`; +export function getInstallHash(install: InstallConfigResolvedX) { + const fullHashHex = objectHash(JSON.parse(JSON.stringify(install))); + return `${install.portRef}!${fullHashHex}`; } -export type PathRef = dax.PathRef; - 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/), - ); + return logger().debug("spawning", cmd); }); return builder; } +// type Last = T extends readonly [...any, infer R] ? R +// : DeArrayify; +// +// type Ser[]> = T extends +// readonly [...Promise[], infer R] ? { (...promises: T): R } +// : { +// (...promises: T): DeArrayify; +// }; + export const $ = dax.build$( { commandBuilder: defaultCommandBuilder(), + requestBuilder: new dax.RequestBuilder() + .showProgress(Deno.stderr.isTerminal()), extras: { + mapObject< + O, + V2, + >( + obj: O, + map: (key: keyof O, val: O[keyof O]) => [string, V2], + ): Record { + return Object.fromEntries( + Object.entries(obj as object).map(([key, val]) => + map(key as keyof O, val as O[keyof O]) + ), + ); + }, + exponentialBackoff(initialDelayMs: number) { + let delay = initialDelayMs; + let attempt = 0; + + return { + next() { + if (attempt > 0) { + delay *= 2; + } + attempt += 1; + return delay; + }, + }; + }, inspect(val: unknown) { return Deno.inspect(val, { colors: isColorfulTty(), iterableLimit: 500, + depth: 10, }); }, - pathToString(path: dax.PathRef) { + co( + values: T, + ): Promise<{ -readonly [P in keyof T]: Awaited }> { + return Promise.all(values); + }, + // coIter( + // items: Iterable, + // fn: (item:T) => PromiseLike, + // opts: { + // limit: "cpu" | number; + // } = { + // limit: "cpu" + // }, + // ): Promise[]> { + // const limit = opts.limit == "cpu" ? AVAIL_CONCURRENCY : opts.limit; + // const promises = [] as PromiseLike[]; + // let freeSlots = limit; + // do { + // } while(true); + // return Promise.all(promises); + // } + 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 }); } + return pathRef; }, }, }, @@ -211,23 +274,22 @@ export function inWorker() { self instanceof WorkerGlobalScope; } -export async function findConfig(path: string) { - let current = path; +export async function findEntryRecursive(path: string, name: string) { + let current = $.path(path); while (true) { - const location = `${current}/ghjk.ts`; - if (await std_fs.exists(location)) { + const location = `${current}/${name}`; + if (await $.path(location).exists()) { return location; } - const nextCurrent = std_path.dirname(current); - if (nextCurrent == "/" && current == "/") { + const nextCurrent = $.path(current).parent(); + if (!nextCurrent) { break; } current = nextCurrent; } - return null; } -export function home_dir(): string | null { +export function homeDir() { switch (Deno.build.os) { case "linux": case "darwin": @@ -240,13 +302,13 @@ export function home_dir(): string | null { } export function dirs() { - const home = home_dir(); + const home = homeDir(); if (!home) { throw new Error("cannot find home dir"); } return { homeDir: home, - shareDir: std_path.resolve(home, ".local", "share"), + shareDir: $.path(home).resolve(".local", "share"), }; } @@ -258,40 +320,24 @@ if (Number.isNaN(AVAIL_CONCURRENCY)) { throw new Error(`Value of DENO_JOBS is NAN: ${Deno.env.get("DENO_JOBS")}`); } -export async function importRaw(spec: string) { +export async function importRaw(spec: string, timeout: dax.Delay = "1m") { const url = new URL(spec); if (url.protocol == "file:") { - return await Deno.readTextFile(url.pathname); + return await $.path(url.pathname).readText(); } if (url.protocol.match(/^http/)) { - const resp = await fetch(url); - if (!resp.ok) { - throw new Error( - `error importing raw using fetch from ${spec}: ${resp.status} - ${resp.statusText}`, - ); + let request = $.request(url).timeout(timeout); + const integrity = url.searchParams.get("integrity"); + if (integrity) { + request = request.integrity(integrity); } - return await resp.text(); + return await request.text(); } throw new Error( `error importing raw from ${spec}: unrecognized protocol ${url.protocol}`, ); } -export function exponentialBackoff(initialDelayMs: number) { - let delay = initialDelayMs; - let attempt = 0; - - return { - next() { - if (attempt > 0) { - delay *= 2; - } - attempt += 1; - return delay; - }, - }; -} - export async function shimScript( { shimPath, execPath, os, defArgs, envOverrides, envDefault }: { shimPath: string; @@ -340,12 +386,15 @@ 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, ) { const { name, mode, url, downloadPath, tmpDirPath, headers } = { - name: std_url.basename(args.url), + name: $.path(args.url).basename(), mode: 0o666, headers: {}, ...args, @@ -360,16 +409,15 @@ export async function downloadFile( await $.request(url) .header(headers) - .showProgress() .pipeToPath(tmpFilePath, { create: true, mode }); 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 +450,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 +488,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, @@ -459,3 +522,96 @@ export function thinInstallConfig(fat: InstallConfigFat) { ...lite, }; } + +export type OrRetOf = T extends () => infer Inner ? Inner : T; + +export function switchMap< + K extends string | number | symbol, + All extends { + [Key in K]: All[K]; + }, +>( + val: K, + branches: All, + // def?: D, +): K extends keyof All ? OrRetOf + : /* All[keyof All] | */ undefined { + // return branches[val]; + const branch = branches[val]; + return typeof branch == "function" ? branch() : branch; +} + +switchMap( + "holla" as string, + { + hey: () => 1, + hello: () => 2, + hi: 3, + holla: 4, + } as const, + // () =>5 +); + +export async function expandGlobsAndAbsolutize( + path: string, + wd: string, + opts?: Omit, +) { + if (std_path.isGlob(path)) { + const glob = std_path.isAbsolute(path) + ? path + : std_path.joinGlobs([wd, path], { extended: true }); + return (await Array.fromAsync(std_fs.expandGlob(glob, opts))) + .map((entry) => std_path.resolve(wd, entry.path)); + } + return [std_path.resolve(wd, path)]; +} + +/** + * Unwrap the result object returned by the `safeParse` method + * on zod schemas. + */ +export function unwrapZodRes( + res: zod.SafeParseReturnType, + cause: object = {}, + errMessage = "error parsing object", +) { + if (!res.success) { + const zodErr = zod_val_err.fromZodError(res.error, { + includePath: true, + maxIssuesInMessage: 3, + }); + throw new Error(`${errMessage}: ${zodErr}`, { + cause: { + issues: res.error.issues, + ...cause, + }, + }); + } + 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 = "./", diff --git a/host/worker.ts b/utils/worker.ts similarity index 72% rename from host/worker.ts rename to utils/worker.ts index 3b36fbd2..d8c1dddf 100644 --- a/host/worker.ts +++ b/utils/worker.ts @@ -1,55 +1,4 @@ -//! this loads the ghjk.ts module and provides a program for it - -//// -/// - -// all imports in here should be dynamic imports as we want to -// modify the Deno namespace before anyone touches it - -// NOTE: only import types -import type { DriverRequests, DriverResponse } from "./deno.ts"; - -self.onmessage = onMsg; - -async function onMsg(msg: MessageEvent) { - const req = msg.data; - if (!req.ty) { - throw new Error(`unrecognized event data`, { - cause: req, - }); - } - let res: DriverResponse; - if (req.ty == "serialize") { - res = { - ty: req.ty, - payload: await serializeConfig(req.uri, req.envVars), - }; - } else { - throw new Error(`unrecognized request type: ${req.ty}`, { - cause: req, - }); - } - self.postMessage(res); -} - -async function serializeConfig(uri: string, envVars: Record) { - const shimHandle = shimDenoNamespace(envVars); - const { setup: setupLogger } = await import( - "../utils/logger.ts" - ); - setupLogger(); - const mod = await import(uri); - const rawConfig = await mod.ghjk.getConfig(mod.secureConfig); - const config = JSON.parse(JSON.stringify(rawConfig)); - return { - config, - accessedEnvKeys: shimHandle.getAccessedEnvKeys(), - readFiles: shimHandle.getReadFiles(), - listedFiles: shimHandle.getListedFiles(), - }; -} - -function shimDenoNamespace(envVars: Record) { +export function shimDenoNamespace(envVars: Record) { const { envShim, getAccessedEnvKeys } = denoEnvShim(envVars); Object.defineProperty(Deno, "env", { value: envShim, @@ -72,6 +21,8 @@ function denoFsReadShim() { throw new Error("Deno.watchFs API is disabled"); }] as const, ...[ + // TODO: systemize a way to make sure this + // tracks deno APIs Deno.readFile, Deno.readTextFileSync, Deno.readTextFile,