diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1261b319..43de4a29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,7 @@ jobs: - run: bun install --frozen-lockfile --ignore-scripts - run: bun run schema:check - run: bun run build - - run: node dist/index.js --help - - run: node scripts/smoke-node-cli.mjs + - run: bun run package:smoke node-24-smoke: name: Node 24 smoke @@ -58,8 +57,7 @@ jobs: node-version: "24.x" - run: bun install --frozen-lockfile --ignore-scripts - run: bun run build - - run: node dist/index.js --help - - run: node scripts/smoke-node-cli.mjs + - run: bun run package:smoke typecheck: name: Typecheck diff --git a/bun.lock b/bun.lock index 59ec96e4..a65e5f1a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,9 +4,15 @@ "workspaces": { "": { "name": "allagents", - "dependencies": { + "devDependencies": { + "@biomejs/biome": "^2.5.13", "@clack/prompts": "^1.8.1", "@modelcontextprotocol/sdk": "^1.30.0", + "@types/bun": "1.4.2", + "@types/js-yaml": "^4.0.9", + "@types/micromatch": "^4.0.10", + "@types/node": "^22.0.0", + "ajv": "8.20.0", "chalk": "^6.0.0", "cmd-ts": "^0.15.0", "execa": "^8.0.1", @@ -17,17 +23,8 @@ "micromatch": "^4.0.8", "read-cmd-shim": "^4.0.0", "simple-git": "^3.36.0", - "zod": "^3.25.76", - }, - "devDependencies": { - "@biomejs/biome": "^2.5.13", - "@types/bun": "1.4.2", - "@types/js-yaml": "^4.0.9", - "@types/micromatch": "^4.0.10", - "@types/node": "^22.0.0", - "ajv": "8.20.0", - "shx": "^0.4.0", "typescript": "^7.0.2", + "zod": "^3.25.76", "zod-to-json-schema": "3.25.2", }, }, @@ -173,8 +170,6 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -249,14 +244,10 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], - "ip-address": ["ip-address@10.7.2", "", {}, "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], - "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -301,14 +292,10 @@ "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -321,14 +308,10 @@ "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], - "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -337,8 +320,6 @@ "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -349,12 +330,8 @@ "read-cmd-shim": ["read-cmd-shim@4.0.0", "", {}, "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q=="], - "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -365,8 +342,6 @@ "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], - "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -377,10 +352,6 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shelljs": ["shelljs@0.9.2", "", { "dependencies": { "execa": "^1.0.0", "fast-glob": "^3.3.2", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw=="], - - "shx": ["shx@0.4.0", "", { "dependencies": { "minimist": "^1.2.8", "shelljs": "^0.9.2" }, "bin": { "shx": "lib/cli.js" } }, "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA=="], - "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], @@ -403,12 +374,8 @@ "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], - "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], - "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -445,32 +412,10 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "shelljs/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], - "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "bun-types/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - - "shelljs/execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="], - - "shelljs/execa/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], - - "shelljs/execa/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="], - - "shelljs/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], - - "shelljs/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "shelljs/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], - - "shelljs/execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], - - "shelljs/execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - - "shelljs/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], - - "shelljs/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], } } diff --git a/package.json b/package.json index 57a508cf..d961ef61 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "dist" ], "scripts": { - "build": "shx rm -rf dist && bun build ./src/cli/index.ts --outdir ./dist --target node && shx cp -r src/templates dist/", + "build": "bun run scripts/build-package.ts", + "package:check": "bun run scripts/check-package-size.ts", + "package:smoke": "bun run scripts/smoke-packed-package.mjs", "docs:install": "bun install --cwd docs --frozen-lockfile", "docs:build": "bun run docs:install && bun run --cwd docs build", "dev": "bun run src/cli/index.ts", @@ -57,9 +59,15 @@ "url": "git+https://github.com/allagentsdev/allagents.git" }, "homepage": "https://allagents.dev", - "dependencies": { + "devDependencies": { + "@biomejs/biome": "^2.5.13", "@clack/prompts": "^1.8.1", "@modelcontextprotocol/sdk": "^1.30.0", + "@types/bun": "1.4.2", + "@types/js-yaml": "^4.0.9", + "@types/micromatch": "^4.0.10", + "@types/node": "^22.0.0", + "ajv": "8.20.0", "chalk": "^6.0.0", "cmd-ts": "^0.15.0", "execa": "^8.0.1", @@ -70,17 +78,8 @@ "micromatch": "^4.0.8", "read-cmd-shim": "^4.0.0", "simple-git": "^3.36.0", - "zod": "^3.25.76" - }, - "devDependencies": { - "@biomejs/biome": "^2.5.13", - "@types/bun": "1.4.2", - "@types/js-yaml": "^4.0.9", - "@types/micromatch": "^4.0.10", - "@types/node": "^22.0.0", - "ajv": "8.20.0", - "shx": "^0.4.0", "typescript": "^7.0.2", + "zod": "^3.25.76", "zod-to-json-schema": "3.25.2" }, "engines": { diff --git a/scripts/build-package.ts b/scripts/build-package.ts new file mode 100755 index 00000000..1b36672d --- /dev/null +++ b/scripts/build-package.ts @@ -0,0 +1,158 @@ +#!/usr/bin/env bun + +import { + chmod, + cp, + readFile, + readdir, + rm, + writeFile, +} from 'node:fs/promises'; +import { join, resolve } from 'node:path'; + +const LICENSE_FILE_PATTERN = /^(?:licen[cs]e|copying|notice)(?:\.|-|$)/i; + +export type BundledPackage = { + name: string; + packageRoot: string; +}; + +function bundledPackageFromInput( + inputPath: string, +): BundledPackage | undefined { + const normalized = inputPath.replaceAll('\\', '/'); + const marker = 'node_modules/'; + const markerIndex = normalized.lastIndexOf(marker); + if (markerIndex === -1) return undefined; + + const packagePath = normalized.slice(markerIndex + marker.length); + const segments = packagePath.split('/'); + const segmentCount = segments[0]?.startsWith('@') ? 2 : 1; + if (segments.length < segmentCount || !segments[0]) return undefined; + + const name = segments.slice(0, segmentCount).join('/'); + const packageRootEnd = + markerIndex + marker.length + name.length; + return { + name, + packageRoot: normalized.slice(0, packageRootEnd), + }; +} + +export function bundledPackages(metafile: Bun.BuildMetafile): BundledPackage[] { + const packagesByRoot = new Map(); + + for (const output of Object.values(metafile.outputs)) { + for (const [inputPath, contribution] of Object.entries(output.inputs)) { + if (contribution.bytesInOutput <= 0) continue; + const bundledPackage = bundledPackageFromInput(inputPath); + if (bundledPackage) { + packagesByRoot.set(bundledPackage.packageRoot, bundledPackage); + } + } + } + + return [...packagesByRoot.values()].sort( + (left, right) => + left.name.localeCompare(right.name, 'en') || + left.packageRoot.localeCompare(right.packageRoot, 'en'), + ); +} + +function formatDeclaredLicense(license: unknown): string { + if (typeof license === 'string' && license.trim()) return license.trim(); + if (license !== undefined) return JSON.stringify(license); + return 'Not declared'; +} + +export async function createThirdPartyNotices( + metafile: Bun.BuildMetafile, + projectRoot: string, +): Promise { + const sections = await Promise.all( + bundledPackages(metafile).map(async ({ name, packageRoot }) => { + const resolvedPackageRoot = resolve(projectRoot, packageRoot); + const packageJson = JSON.parse( + await readFile(join(resolvedPackageRoot, 'package.json'), 'utf8'), + ) as { name?: string; version?: string; license?: unknown }; + const licenseFiles = (await readdir(resolvedPackageRoot)) + .filter((fileName) => LICENSE_FILE_PATTERN.test(fileName)) + .sort((left, right) => left.localeCompare(right, 'en')); + const licenseTexts = await Promise.all( + licenseFiles.map(async (fileName) => { + const contents = ( + await readFile(join(resolvedPackageRoot, fileName), 'utf8') + ).trim(); + return `--- ${fileName} ---\n${contents}`; + }), + ); + + return [ + '===============================================================================', + `${packageJson.name ?? name}@${packageJson.version ?? 'unknown'}`, + `Bundled from: ${packageRoot}`, + `License: ${formatDeclaredLicense(packageJson.license)}`, + licenseTexts.length > 0 + ? licenseTexts.join('\n\n') + : 'No license or notice file was included in this package.', + ].join('\n'); + }), + ); + + return [ + 'AllAgents Third-Party Notices', + '', + 'This file is generated from the packages embedded in dist/index.js.', + 'Do not edit it by hand.', + '', + ...sections, + '', + ].join('\n'); +} + +async function buildPackage() { + const projectRoot = resolve(import.meta.dir, '..'); + const distPath = join(projectRoot, 'dist'); + await rm(distPath, { recursive: true, force: true }); + + const result = await Bun.build({ + entrypoints: [join(projectRoot, 'src', 'cli', 'index.ts')], + outdir: distPath, + target: 'node', + packages: 'bundle', + minify: true, + metafile: true, + }); + if (!result.success || !result.metafile) { + throw new Error('Production bundle build failed'); + } + + const metafile = result.metafile; + const externalImports = Object.values(metafile.outputs).flatMap((output) => + output.imports.map((entry) => entry.path), + ); + if (externalImports.length > 0) { + throw new Error( + `Production bundle is not self-contained; external imports remain: ${externalImports.join(', ')}`, + ); + } + + await cp(join(projectRoot, 'src', 'templates'), join(distPath, 'templates'), { + recursive: true, + }); + const notices = await createThirdPartyNotices(metafile, projectRoot); + await writeFile(join(distPath, 'THIRD_PARTY_NOTICES.txt'), notices, 'utf8'); + + const entrypoint = join(distPath, 'index.js'); + await chmod(entrypoint, 0o755); + const entrypointOutput = result.outputs.find( + (output) => resolve(output.path) === entrypoint, + ); + console.log( + `Built self-contained production bundle (${entrypointOutput?.size ?? 'unknown'} bytes, ${bundledPackages(metafile).length} third-party package instances).`, + ); +} + +if (import.meta.main) { + await buildPackage(); +} diff --git a/scripts/check-package-size.ts b/scripts/check-package-size.ts new file mode 100755 index 00000000..7df11d32 --- /dev/null +++ b/scripts/check-package-size.ts @@ -0,0 +1,140 @@ +#!/usr/bin/env bun + +import { mkdtemp, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { $ } from 'bun'; + +export type PackageSizes = { + emittedJavaScript: number; + unpackedPackage: number; + compressedTarball: number; +}; + +export type Packument = { + filename: string; + size: number; + unpackedSize: number; + files: Array<{ path: string }>; +}; + +export type CheckedPackageTarball = { + artifact: Packument; + sizes: PackageSizes; + tarballPath: string; + cleanup: () => Promise; +}; + +export const PACKAGE_SIZE_BUDGETS: PackageSizes = { + emittedJavaScript: 1_650_000, + unpackedPackage: 1_800_000, + compressedTarball: 525_000, +}; + +const LABELS: Record = { + emittedJavaScript: 'emitted JavaScript', + unpackedPackage: 'unpacked package', + compressedTarball: 'compressed tarball', +}; + +function bytesLabel(bytes: number): string { + return `${bytes} ${bytes === 1 ? 'byte' : 'bytes'}`; +} + +export function assertPackageSizeBudgets( + sizes: PackageSizes, + budgets: PackageSizes, +): void { + const failures = (Object.keys(LABELS) as Array).flatMap( + (key) => { + const overage = sizes[key] - budgets[key]; + return overage > 0 + ? [ + `${LABELS[key]}: ${bytesLabel(sizes[key])} (budget: ${bytesLabel(budgets[key])}, over by ${bytesLabel(overage)})`, + ] + : []; + }, + ); + + if (failures.length > 0) { + throw new Error(`Package size budget exceeded:\n${failures.join('\n')}`); + } +} + +export function parsePackument(stdout: string): Packument { + // npm 10 can prepend prepare-script output even with --ignore-scripts. + const jsonStart = stdout.lastIndexOf('\n[') + 1; + const [artifact] = JSON.parse(stdout.slice(jsonStart)) as Packument[]; + if (!artifact) throw new Error('npm pack did not report a package artifact'); + return artifact; +} + +async function checkedSizes( + projectRoot: string, + artifact: Packument, +): Promise { + const entrypoint = join(projectRoot, 'dist', 'index.js'); + const entrypointStat = await stat(entrypoint).catch(() => { + throw new Error(`Missing ${entrypoint}; run \`bun run build\` first`); + }); + const sizes: PackageSizes = { + emittedJavaScript: entrypointStat.size, + unpackedPackage: artifact.unpackedSize, + compressedTarball: artifact.size, + }; + + assertPackageSizeBudgets(sizes, PACKAGE_SIZE_BUDGETS); + for (const key of Object.keys(LABELS) as Array) { + console.log( + `āœ“ ${LABELS[key]}: ${bytesLabel(sizes[key])} / ${bytesLabel(PACKAGE_SIZE_BUDGETS[key])}`, + ); + } + return sizes; +} + +export async function createCheckedPackageTarball( + projectRoot = resolve(import.meta.dir, '..'), +): Promise { + const packRoot = await mkdtemp(join(tmpdir(), 'allagents-package-')); + + try { + const packed = await $`npm pack --json --ignore-scripts --pack-destination ${packRoot}` + .cwd(projectRoot) + .quiet(); + const artifact = parsePackument(packed.stdout.toString()); + const tarballPath = resolve(packRoot, artifact.filename); + if (dirname(tarballPath) !== packRoot) { + throw new Error(`npm pack returned an unsafe filename: ${artifact.filename}`); + } + await stat(tarballPath); + const sizes = await checkedSizes(projectRoot, artifact); + let cleaned = false; + + return { + artifact, + sizes, + tarballPath, + cleanup: async () => { + if (cleaned) return; + cleaned = true; + await rm(packRoot, { recursive: true, force: true }); + }, + }; + } catch (error) { + await rm(packRoot, { recursive: true, force: true }); + throw error; + } +} + +export async function checkPackageSize( + projectRoot = resolve(import.meta.dir, '..'), +): Promise { + const packed = await $`npm pack --dry-run --json --ignore-scripts` + .cwd(projectRoot) + .quiet(); + return checkedSizes(projectRoot, parsePackument(packed.stdout.toString())); +} + +if (import.meta.main) { + await checkPackageSize(); +} diff --git a/scripts/publish.ts b/scripts/publish.ts index 42d2d948..dcb3cfae 100644 --- a/scripts/publish.ts +++ b/scripts/publish.ts @@ -14,6 +14,7 @@ import { readFileSync } from "node:fs"; import { $ } from "bun"; +import { createCheckedPackageTarball } from "./check-package-size"; type NpmTag = "next" | "latest"; type DistTags = Record; @@ -110,7 +111,15 @@ async function main() { console.log(` āœ“ ${name}@${version} is already published`); await assertDistTagMatches(name, version, npmTag); } else { - await $`npm publish --tag ${npmTag}`.env({ ...process.env, ALLOW_PUBLISH: "1" }); + const packedPackage = await createCheckedPackageTarball(process.cwd()); + try { + await $`npm publish ${packedPackage.tarballPath} --tag ${npmTag}`.env({ + ...process.env, + ALLOW_PUBLISH: "1", + }); + } finally { + await packedPackage.cleanup(); + } } console.log("\nāœ… Package published."); } diff --git a/scripts/smoke-node-cli.mjs b/scripts/smoke-node-cli.mjs index 923dac51..1a61da20 100755 --- a/scripts/smoke-node-cli.mjs +++ b/scripts/smoke-node-cli.mjs @@ -17,7 +17,7 @@ const root = await mkdtemp(join(tmpdir(), 'allagents-node-smoke-')); const repositoryPath = join(root, 'repository'); const workspacePath = join(root, 'workspace'); const homePath = join(root, 'home'); -const cliPath = resolve('dist/index.js'); +const cliPath = resolve(process.argv[2] ?? 'dist/index.js'); const stdioFixturePath = resolve('tests/helpers/mcp-runtime-stdio-server.mjs'); const stdioStatePath = join(root, 'stdio-state'); const stdioCapturePath = join(stdioStatePath, 'requests.ndjson'); diff --git a/scripts/smoke-packed-package.mjs b/scripts/smoke-packed-package.mjs new file mode 100755 index 00000000..94f92c49 --- /dev/null +++ b/scripts/smoke-packed-package.mjs @@ -0,0 +1,130 @@ +#!/usr/bin/env bun + +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { + mkdir, + mkdtemp, + readFile, + readdir, + rm, + writeFile, +} from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { promisify } from 'node:util'; +import { createCheckedPackageTarball } from './check-package-size.ts'; + +const execFileAsync = promisify(execFile); +const nodeExecutable = 'node'; +const projectRoot = resolve(import.meta.dirname, '..'); +const root = await mkdtemp(join(tmpdir(), 'allagents-packed-smoke-')); +const consumerRoot = join(root, 'consumer'); + +async function runCli(cliPath, args, options = {}) { + return execFileAsync(nodeExecutable, [cliPath, ...args], { + cwd: options.cwd ?? consumerRoot, + env: options.env ?? process.env, + }); +} + +async function exerciseHelpTree(cliPath) { + const visitedPaths = new Set(); + + async function visit(commandPath) { + const pathKey = commandPath.join(' '); + assert.ok( + !visitedPaths.has(pathKey), + `command tree contains a cycle at ${pathKey || ''}`, + ); + visitedPaths.add(pathKey); + + const { stdout } = await runCli(cliPath, [ + ...commandPath, + '--help', + '--json', + ]); + const help = JSON.parse(stdout); + for (const child of help.commands ?? []) { + await visit(child.command.split(/\s+/)); + } + } + + await visit([]); + assert.ok( + visitedPaths.size > 20, + `expected a complete command tree, visited ${visitedPaths.size} nodes`, + ); + return visitedPaths.size; +} + +let packedPackage; + +try { + packedPackage = await createCheckedPackageTarball(projectRoot); + const { artifact } = packedPackage; + assert.ok( + artifact.files.some( + (file) => file.path === 'dist/THIRD_PARTY_NOTICES.txt', + ), + 'packed artifact is missing dist/THIRD_PARTY_NOTICES.txt', + ); + + await writeFile( + join(root, 'package.json'), + JSON.stringify({ private: true, name: 'allagents-packed-smoke' }), + ); + await execFileAsync( + 'npm', + [ + 'install', + '--ignore-scripts', + '--omit=dev', + '--no-audit', + '--no-fund', + packedPackage.tarballPath, + ], + { cwd: root }, + ); + + const installedPackageRoot = join(root, 'node_modules', 'allagents'); + const installedManifest = JSON.parse( + await readFile(join(installedPackageRoot, 'package.json'), 'utf8'), + ); + assert.deepEqual( + installedManifest.dependencies ?? {}, + {}, + 'packed allagents must not install a duplicate runtime dependency graph', + ); + const installedEntries = (await readdir(join(root, 'node_modules'))).filter( + (entry) => !['.bin', '.package-lock.json', 'allagents'].includes(entry), + ); + assert.deepEqual( + installedEntries, + [], + `isolated install added unexpected runtime packages: ${installedEntries.join(', ')}`, + ); + + await mkdir(consumerRoot); + await writeFile( + join(consumerRoot, 'package.json'), + JSON.stringify({ private: true, name: 'allagents-packed-consumer' }), + ); + + const cliPath = join(installedPackageRoot, 'dist', 'index.js'); + const visited = await exerciseHelpTree(cliPath); + await execFileAsync( + nodeExecutable, + ['scripts/smoke-node-cli.mjs', cliPath], + { cwd: projectRoot }, + ); + + console.log( + `Packed npm artifact passed isolated Node smoke (${visited} help nodes; no runtime dependencies installed).`, + ); +} finally { + await Promise.all([ + rm(root, { recursive: true, force: true }), + packedPackage?.cleanup(), + ]); +} diff --git a/tests/unit/core/profile/codex.test.ts b/tests/unit/core/profile/codex.test.ts index d4513043..5d263877 100644 --- a/tests/unit/core/profile/codex.test.ts +++ b/tests/unit/core/profile/codex.test.ts @@ -135,7 +135,7 @@ describe('Codex profile adapter', () => { command: 'npx', args: [ '-y', - 'allagents@1.15.0', + `allagents@${packageJson.version}`, 'mcp', 'proxy', 'https://mcp.example.test', diff --git a/tests/unit/scripts/package-artifact.test.ts b/tests/unit/scripts/package-artifact.test.ts new file mode 100644 index 00000000..29f49d8a --- /dev/null +++ b/tests/unit/scripts/package-artifact.test.ts @@ -0,0 +1,177 @@ +import { describe, expect, test } from 'bun:test'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createThirdPartyNotices } from '../../../scripts/build-package'; +import { + assertPackageSizeBudgets, + parsePackument, +} from '../../../scripts/check-package-size'; + +const metafile = { + inputs: {}, + outputs: { + './index.js': { + bytes: 100, + inputs: { + 'src/index.ts': { bytesInOutput: 20 }, + 'node_modules/z-package/index.js': { bytesInOutput: 40 }, + 'node_modules/@scope/a-package/index.js': { bytesInOutput: 30 }, + 'node_modules/shared-package/index.js': { bytesInOutput: 10 }, + 'node_modules/z-package/node_modules/shared-package/index.js': { + bytesInOutput: 10, + }, + 'node_modules/tree-shaken/index.js': { bytesInOutput: 0 }, + }, + imports: [], + exports: [], + entryPoint: 'src/index.ts', + }, + }, +}; + +describe('packed artifact contracts', () => { + test('derives third-party notices from packages embedded in build output', async () => { + const root = await mkdtemp(join(tmpdir(), 'allagents-notices-')); + + try { + await mkdir(join(root, 'node_modules', 'z-package'), { recursive: true }); + await mkdir(join(root, 'node_modules', '@scope', 'a-package'), { + recursive: true, + }); + await mkdir(join(root, 'node_modules', 'shared-package'), { + recursive: true, + }); + await mkdir( + join( + root, + 'node_modules', + 'z-package', + 'node_modules', + 'shared-package', + ), + { recursive: true }, + ); + await writeFile( + join(root, 'node_modules', 'z-package', 'package.json'), + JSON.stringify({ name: 'z-package', version: '2.0.0', license: 'MIT' }), + ); + await writeFile( + join(root, 'node_modules', 'z-package', 'LICENSE'), + 'Z package license text\n', + ); + await writeFile( + join(root, 'node_modules', '@scope', 'a-package', 'package.json'), + JSON.stringify({ + name: '@scope/a-package', + version: '1.0.0', + license: 'Apache-2.0', + }), + ); + await writeFile( + join(root, 'node_modules', '@scope', 'a-package', 'NOTICE.txt'), + 'A package notice text\n', + ); + await writeFile( + join(root, 'node_modules', 'shared-package', 'package.json'), + JSON.stringify({ + name: 'shared-package', + version: '2.0.0', + license: 'MIT', + }), + ); + await writeFile( + join(root, 'node_modules', 'shared-package', 'LICENSE'), + 'Shared package version 2 license\n', + ); + await writeFile( + join( + root, + 'node_modules', + 'z-package', + 'node_modules', + 'shared-package', + 'package.json', + ), + JSON.stringify({ + name: 'shared-package', + version: '1.0.0', + license: 'ISC', + }), + ); + await writeFile( + join( + root, + 'node_modules', + 'z-package', + 'node_modules', + 'shared-package', + 'LICENSE', + ), + 'Shared package version 1 license\n', + ); + + const notices = await createThirdPartyNotices(metafile, root); + expect(notices).toContain('@scope/a-package@1.0.0'); + expect(notices).toContain('License: Apache-2.0'); + expect(notices).toContain('A package notice text'); + expect(notices).toContain('z-package@2.0.0'); + expect(notices).toContain('Z package license text'); + expect(notices).toContain('shared-package@1.0.0'); + expect(notices).toContain('Shared package version 1 license'); + expect(notices).toContain('shared-package@2.0.0'); + expect(notices).toContain('Shared package version 2 license'); + expect(notices.match(/shared-package@/g)).toHaveLength(2); + expect(notices).toContain( + 'Bundled from: node_modules/z-package/node_modules/shared-package', + ); + expect(notices).not.toContain('tree-shaken'); + expect(notices.indexOf('@scope/a-package')).toBeLessThan( + notices.indexOf('z-package'), + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + test('parses npm pack JSON after prepare-script output', () => { + const artifact = { + filename: 'allagents-1.16.0.tgz', + size: 123, + unpackedSize: 456, + files: [{ path: 'dist/index.js' }], + }; + + expect( + parsePackument( + `Built self-contained production bundle.\n${JSON.stringify([artifact])}`, + ), + ).toEqual(artifact); + }); + + test('reports every exceeded package-size budget with actual and limit bytes', () => { + expect(() => + assertPackageSizeBudgets( + { emittedJavaScript: 101, unpackedPackage: 202, compressedTarball: 303 }, + { emittedJavaScript: 100, unpackedPackage: 200, compressedTarball: 300 }, + ), + ).toThrow( + [ + 'Package size budget exceeded:', + 'emitted JavaScript: 101 bytes (budget: 100 bytes, over by 1 byte)', + 'unpacked package: 202 bytes (budget: 200 bytes, over by 2 bytes)', + 'compressed tarball: 303 bytes (budget: 300 bytes, over by 3 bytes)', + ].join('\n'), + ); + }); + + test('accepts artifacts at their package-size ceilings', () => { + const sizes = { + emittedJavaScript: 100, + unpackedPackage: 200, + compressedTarball: 300, + }; + + expect(() => assertPackageSizeBudgets(sizes, sizes)).not.toThrow(); + }); +}); diff --git a/tests/unit/scripts/publish.test.ts b/tests/unit/scripts/publish.test.ts index f763eedb..c407f0ce 100644 --- a/tests/unit/scripts/publish.test.ts +++ b/tests/unit/scripts/publish.test.ts @@ -10,6 +10,10 @@ type PublishScenario = { version: string; publishedVersion?: string; distTags: Record; + packageSizes?: { + unpackedPackage: number; + compressedTarball: number; + }; }; async function runPublish(scenario: PublishScenario) { @@ -24,12 +28,27 @@ async function runPublish(scenario: PublishScenario) { join(root, 'package.json'), JSON.stringify({ name: 'allagents', version: scenario.version }), ); + await mkdir(join(root, 'dist')); + await writeFile(join(root, 'dist', 'index.js'), '#!/usr/bin/env node\n'); await writeFile( fakeNpmPath, - `import { appendFileSync } from 'node:fs'; + `import { appendFileSync, existsSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; const args = process.argv.slice(2); appendFileSync(process.env.NPM_CALLS!, JSON.stringify(args) + '\\n'); +if (args[0] === 'pack') { + const destination = args[args.indexOf('--pack-destination') + 1]; + const filename = 'allagents-test.tgz'; + writeFileSync(join(destination, filename), 'packed artifact'); + console.log(JSON.stringify([{ + filename, + size: Number(process.env.FAKE_TARBALL_SIZE), + unpackedSize: Number(process.env.FAKE_UNPACKED_SIZE), + files: [], + }])); + process.exit(0); +} if (args[0] === 'view' && args[1]?.includes('@')) { const publishedVersion = process.env.FAKE_PUBLISHED_VERSION; @@ -46,7 +65,13 @@ if (args[0] === 'view' && args[2] === 'dist-tags') { process.exit(0); } -if (args[0] === 'publish') process.exit(0); +if (args[0] === 'publish') { + if (!args[1]?.endsWith('.tgz') || !existsSync(args[1])) { + console.error('publish must receive the retained packed artifact'); + process.exit(93); + } + process.exit(0); +} if (args[0] === 'dist-tag') { console.error('dist-tag mutation must not run'); process.exit(91); @@ -83,6 +108,12 @@ process.exit(92); NPM_CALLS: callsPath, FAKE_PUBLISHED_VERSION: scenario.publishedVersion ?? '', FAKE_DIST_TAGS: JSON.stringify(scenario.distTags), + FAKE_UNPACKED_SIZE: String( + scenario.packageSizes?.unpackedPackage ?? 1, + ), + FAKE_TARBALL_SIZE: String( + scenario.packageSizes?.compressedTarball ?? 1, + ), }, stdout: 'pipe', stderr: 'pipe', @@ -93,12 +124,20 @@ process.exit(92); .split('\n') .filter(Boolean) .map((line) => JSON.parse(line) as string[]); + const publishCall = calls.find(([command]) => command === 'publish'); + const publishedTarballPath = publishCall?.[1]; + const publishedTarballExists = + publishedTarballPath === undefined + ? undefined + : await Bun.file(publishedTarballPath).exists(); return { exitCode: result.exitCode, stdout: result.stdout.toString(), stderr: result.stderr.toString(), calls, + publishedTarballPath, + publishedTarballExists, }; } finally { await rm(root, { recursive: true, force: true }); @@ -122,7 +161,17 @@ describe('npm publishing', () => { const result = await runPublish(scenario); expect(result.exitCode).toBe(0); - expect(result.calls).toContainEqual(['publish', '--tag', scenario.npmTag]); + const packCall = result.calls.find(([command]) => command === 'pack'); + const publishCall = result.calls.find( + ([command]) => command === 'publish', + ); + expect(packCall).toBeDefined(); + expect(publishCall?.[1]).toBe( + join(packCall?.at(-1) ?? '', 'allagents-test.tgz'), + ); + expect(publishCall?.slice(2)).toEqual(['--tag', scenario.npmTag]); + expect(result.publishedTarballPath).toEndWith('.tgz'); + expect(result.publishedTarballExists).toBe(false); expect(result.calls.some(([command]) => command === 'dist-tag')).toBe(false); }); } @@ -140,5 +189,27 @@ describe('npm publishing', () => { 'allagents@1.14.0-next.1 is already published, but next points to 1.13.9-next.1', ); expect(result.calls.some(([command]) => command === 'dist-tag')).toBe(false); + expect(result.calls.some(([command]) => command === 'pack')).toBe(false); + }); + + test('blocks publishing when the packed artifact exceeds its size budget', async () => { + const result = await runPublish({ + npmTag: 'latest', + version: '1.14.0', + distTags: { latest: '1.13.9' }, + packageSizes: { + unpackedPackage: 1, + compressedTarball: 525_001, + }, + }); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Package size budget exceeded'); + expect(result.stderr).toContain( + 'compressed tarball: 525001 bytes (budget: 525000 bytes, over by 1 byte)', + ); + expect(result.calls.some(([command]) => command === 'view')).toBe(true); + expect(result.calls.some(([command]) => command === 'pack')).toBe(true); + expect(result.calls.some(([command]) => command === 'publish')).toBe(false); }); });