diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 738c54d..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "worktree": { - "symlinkDirectories": [".repos"] - } -} diff --git a/.gitignore b/.gitignore index b708aad..49a0bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ node_modules dist coverage .artifacts -/.repos .claude/worktrees/ *.log .DS_Store diff --git a/.worktreeinclude b/.worktreeinclude deleted file mode 100644 index c8bc571..0000000 --- a/.worktreeinclude +++ /dev/null @@ -1 +0,0 @@ -/.repos/effect/ diff --git a/AGENTS.md b/AGENTS.md index eac14c8..ffe54ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,9 @@ Runtime proofs: ## Worktrees -`.worktreeinclude` reuses `.repos`; Claude symlinks it to avoid another copy. -Run `vp install`, `vp config`, then `pnpm exec vp run verify`. +Run `pnpm exec vp install`, `pnpm exec vp config`, then +`pnpm exec vp run verify`. Effect guidance and inspectable source come from the +installed package under `node_modules/effect`. ## Development Guidance @@ -49,6 +50,16 @@ Run `vp install`, `vp config`, then `pnpm exec vp run verify`. - When the public CLI surface or agent-facing setup flow changes, update [`README.md`](README.md) and [`skills/putio-cli/SKILL.md`](skills/putio-cli/SKILL.md) together so the copy-paste prompt and consumer guidance stay aligned. - Keep docs free of volatile metrics. +## Learning more about the Effect + +This repository uses the Effect TypeScript library. + +Before writing any Effect code, first read `node_modules/effect/AGENTS.md` +**completely**, and follow the links in the file when required. + +If you need to learn more about particular Effect APIs and concepts that the +guide doesn't cover, search through the source code in `node_modules/effect/src`. + ## Testing - Prefer in-process tests unless the process boundary is the behavior under test. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d80b42e..521885e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,13 +7,13 @@ Thanks for contributing to `putio-cli`. Use the Node version required by [`package.json`](./package.json), then install dependencies: ```bash -vp install +pnpm exec vp install ``` Then install the stock VitePlus hook wiring for this clone: ```bash -vp config +pnpm exec vp config ``` ## Run Locally @@ -21,13 +21,13 @@ vp config Start the local build watcher: ```bash -vp run dev +pnpm exec vp run dev ``` Build the CLI once and try the shipped entrypoint: ```bash -vp run build +pnpm exec vp run build ./dist/bin.mjs describe ``` @@ -59,8 +59,9 @@ See [Distribution](docs/DISTRIBUTION.md) for release automation, credentials, an - `verify` is the repository delivery gate. - `verify` enforces the production Effect runtime boundary and dead-code checks, exercises the packed CLI through success and failure paths, and writes the smoke report to `.artifacts/smoke-packed-install.json`. -- `vp config` installs the tracked pre-commit and pre-push hooks; pre-push runs the same `verify` gate as CI. -- Prefer `vp install`, `vp test`, and `vp check` for day-to-day local loops. +- `pnpm exec vp config` installs the tracked pre-commit and pre-push hooks; pre-push runs the same `verify` gate as CI. +- Prefer `pnpm exec vp install`, `pnpm exec vp test`, and `pnpm exec vp check` for day-to-day local loops. +- Keep the exact Effect versions, the Effect override, and the pnpm SDK compatibility patch aligned. The build bundles the patched SDK so installed CLIs and the SDK share one Effect runtime. - Keep top-level user docs in `README.md` and contributor workflow here. - Put deeper implementation detail in `docs/` instead of growing the top-level docs. - Keep `AGENTS.md` as repo-development guidance and `skills/*` as consumer-facing agent guidance. diff --git a/docs/DISTRIBUTION.md b/docs/DISTRIBUTION.md index 846858a..85b7c82 100644 --- a/docs/DISTRIBUTION.md +++ b/docs/DISTRIBUTION.md @@ -43,12 +43,16 @@ The npm package includes `dist`, `README.md`, `docs`, `skills`, `AGENTS.md`, library is part of the public package contract so consuming repos and agents can install the same guidance that maintainers use from git. +The build bundles the pinned, compatibility-patched put.io SDK into `dist`. +Effect remains a package dependency so the CLI and its bundled SDK execute on +the same installed Effect runtime. + ## Local Checks Before changing distribution wiring, validate the repo-local guardrails the workflow depends on: ```bash -vp install +pnpm exec vp install pnpm exec vp run verify pnpm exec vp run smoke:pack ``` diff --git a/package.json b/package.json index e604a46..22126b5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "check:dead-code": "knip", "coverage": "vp test run --coverage", "dev": "vp pack --watch", - "prepare": "node ./scripts/prepare-effect.mts", "prepack": "vp pack", "smoke:pack": "node ./scripts/smoke-packed-install.mts", "test": "vp test", @@ -50,14 +49,14 @@ "verify": "node ./scripts/check-architecture.mts && knip && vp check . && vp run smoke:pack && vp test && vp test run --coverage" }, "dependencies": { - "@effect/platform-node": "4.0.0-beta.102", - "@putdotio/sdk": "^10.0.1", + "@effect/platform-node": "4.0.0-beta.107", "cli-table3": "^0.6.5", - "effect": "4.0.0-beta.102", + "effect": "4.0.0-beta.107", "i18next": "^26.3.4" }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.102", + "@effect/vitest": "4.0.0-beta.107", + "@putdotio/sdk": "10.0.1", "@types/node": "^26.1.0", "@vitest/coverage-v8": "catalog:", "esbuild": "^0.28.1", @@ -71,5 +70,8 @@ "engines": { "node": ">=24.18.0" }, - "packageManager": "pnpm@11.2.2" + "packageManager": "pnpm@11.2.2", + "inlinedDependencies": { + "@putdotio/sdk": "10.0.1" + } } diff --git a/patches/@putdotio__sdk@10.0.1.patch b/patches/@putdotio__sdk@10.0.1.patch new file mode 100644 index 0000000..ec4ac59 --- /dev/null +++ b/patches/@putdotio__sdk@10.0.1.patch @@ -0,0 +1,11 @@ +diff --git a/dist/forms-ByF1AEEn.js b/dist/forms-ByF1AEEn.js +index a727d6b998eeb9126d25ab14c85dcab35c44408d..5a0716b2900693419ddd2ca1b7c2e6b64fbdb0dd 100644 +--- a/dist/forms-ByF1AEEn.js ++++ b/dist/forms-ByF1AEEn.js +@@ -45,3 +45,3 @@ +-var PutioApiError = class extends Schema.TaggedErrorClass()("PutioApiError", { ...PutioResponseErrorFields }) {}; +-var PutioAuthError = class extends Schema.TaggedErrorClass()("PutioAuthError", { ...PutioResponseErrorFields }) {}; +-var PutioRateLimitError = class extends Schema.TaggedErrorClass()("PutioRateLimitError", { ++var PutioApiError = class extends Schema.TaggedError()("PutioApiError", { ...PutioResponseErrorFields }) {}; ++var PutioAuthError = class extends Schema.TaggedError()("PutioAuthError", { ...PutioResponseErrorFields }) {}; ++var PutioRateLimitError = class extends Schema.TaggedError()("PutioRateLimitError", { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 103325c..d062e8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,33 +11,36 @@ catalogs: version: 4.1.10 overrides: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 vite: npm:@voidzero-dev/vite-plus-core@0.2.6 vitest: 4.1.10 +patchedDependencies: + '@putdotio/sdk@10.0.1': debbe0ac2733ce35364e5706d8dc5da32973ba300e49c0364ebf8f24d0138c1b + importers: .: dependencies: '@effect/platform-node': - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102)(ioredis@5.10.1) - '@putdotio/sdk': - specifier: ^10.0.1 - version: 10.0.1 + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107)(ioredis@5.10.1) cli-table3: specifier: ^0.6.5 version: 0.6.5 effect: - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102 + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107 i18next: specifier: ^26.3.4 version: 26.3.6(typescript@7.0.2) devDependencies: '@effect/vitest': - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10) + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107)(vitest@4.1.10) + '@putdotio/sdk': + specifier: 10.0.1 + version: 10.0.1(patch_hash=debbe0ac2733ce35364e5706d8dc5da32973ba300e49c0364ebf8f24d0138c1b) '@types/node': specifier: ^26.1.0 version: 26.1.2 @@ -104,23 +107,23 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@effect/platform-node-shared@4.0.0-beta.102': - resolution: {integrity: sha512-gVd793I72MrkX4dXo7eYtRKfNj0RW4eMRfVEKEJI16h2+mBDCzQ+gqMrog2hSTHQnaIbvbYShNQ4TVGuRCYZeQ==} + '@effect/platform-node-shared@4.0.0-beta.107': + resolution: {integrity: sha512-y6BqcRi86BfTJv+tvDrob4ozYVHxxlHYcn/zIQqZjXI9CvKnkgD6ng+38G1o45c4f2ucU+6HRI9POCmFdMoVGA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 - '@effect/platform-node@4.0.0-beta.102': - resolution: {integrity: sha512-wYVAU9jAePT+gouMr/EVz1CaW6yDLjPAgXYeEFLz7wugT5iZhFRag6mqajV/wwN3bzWP2fzZHokLuPmqD/rqeA==} + '@effect/platform-node@4.0.0-beta.107': + resolution: {integrity: sha512-k+6YNbV4Ck0L6YXtlgkvEnuP5tlxWD8EeWOrpn46PDqbGEwt4ONpRltTwm3tn2cyBXD0i+2P11cUH/6sdFagTA==} engines: {node: '>=18.0.0'} peerDependencies: - effect: 4.0.0-beta.102 - ioredis: ^5.7.0 + effect: 4.0.0-beta.107 + ioredis: '>=5.7.0 <6.0.0' - '@effect/vitest@4.0.0-beta.102': - resolution: {integrity: sha512-4dipFAYG6imOzrY3zy3BgzCJkbb9xESyzUef0WSx8bsK0/SpITqbJpdwKeOyDWLZ+rimjua8IbTFMAde865pIQ==} + '@effect/vitest@4.0.0-beta.107': + resolution: {integrity: sha512-n4/qsx4DnT4dEI/wNgMivxyUeJoeiU1TCSz0WnoHWk/dny40Oxjip2P9IXGQDgPb9fsYVnerF0QRA6nPUuExQA==} peerDependencies: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 vitest: 4.1.10 '@emnapi/core@1.11.2': @@ -1417,8 +1420,8 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - effect@4.0.0-beta.102: - resolution: {integrity: sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w==} + effect@4.0.0-beta.107: + resolution: {integrity: sha512-OoBAv8eF+yanc+C6xhgEUnWeXUSHA6ynnscYqpkAY9GSnzZWystsIjBowVqCkLpHGlnRtdIqYT3wHwpOY6JDnQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1454,9 +1457,6 @@ packages: picomatch: optional: true - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} engines: {node: '>=18.3.0'} @@ -1485,10 +1485,6 @@ packages: typescript: optional: true - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - ioredis@5.10.1: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} @@ -1640,9 +1636,6 @@ packages: msgpackr@2.0.4: resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} - multipasta@0.2.8: - resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1814,10 +1807,6 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} - toml@4.1.2: - resolution: {integrity: sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA==} - engines: {node: '>=20'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -1908,18 +1897,6 @@ packages: engines: {node: '>=8'} hasBin: true - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.21.1: resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} @@ -1976,19 +1953,19 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@effect/platform-node-shared@4.0.0-beta.102(effect@4.0.0-beta.102)': + '@effect/platform-node-shared@4.0.0-beta.107(effect@4.0.0-beta.107)': dependencies: '@types/ws': 8.18.1 - effect: 4.0.0-beta.102 - ws: 8.21.0 + effect: 4.0.0-beta.107 + ws: 8.21.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@4.0.0-beta.102(effect@4.0.0-beta.102)(ioredis@5.10.1)': + '@effect/platform-node@4.0.0-beta.107(effect@4.0.0-beta.107)(ioredis@5.10.1)': dependencies: - '@effect/platform-node-shared': 4.0.0-beta.102(effect@4.0.0-beta.102) - effect: 4.0.0-beta.102 + '@effect/platform-node-shared': 4.0.0-beta.107(effect@4.0.0-beta.107) + effect: 4.0.0-beta.107 ioredis: 5.10.1 mime: 4.1.0 undici: 8.7.0 @@ -1996,9 +1973,9 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10)': + '@effect/vitest@4.0.0-beta.107(effect@4.0.0-beta.107)(vitest@4.1.10)': dependencies: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10))(@voidzero-dev/vite-plus-core@0.2.6(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(typescript@7.0.2)(yaml@2.9.0)) '@emnapi/core@1.11.2': @@ -2398,9 +2375,9 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@putdotio/sdk@10.0.1': + '@putdotio/sdk@10.0.1(patch_hash=debbe0ac2733ce35364e5706d8dc5da32973ba300e49c0364ebf8f24d0138c1b)': dependencies: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 '@standard-schema/spec@1.1.0': {} @@ -2755,18 +2732,13 @@ snapshots: dom-accessibility-api@0.5.16: {} - effect@4.0.0-beta.102: + effect@4.0.0-beta.107: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 4.9.0 - find-my-way-ts: 0.1.6 - ini: 7.0.0 kubernetes-types: 1.30.0 msgpackr: 2.0.4 - multipasta: 0.2.8 - toml: 4.1.2 uuid: 14.0.1 - yaml: 2.9.0 emoji-regex@8.0.0: {} @@ -2819,8 +2791,6 @@ snapshots: optionalDependencies: picomatch: 4.0.5 - find-my-way-ts@0.1.6: {} - formatly@0.3.0: dependencies: fd-package-json: 2.0.0 @@ -2840,8 +2810,6 @@ snapshots: optionalDependencies: typescript: 7.0.2 - ini@7.0.0: {} - ioredis@5.10.1: dependencies: '@ioredis/commands': 1.5.1 @@ -2986,8 +2954,6 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.8: {} - nanoid@3.3.12: {} nanoid@3.3.16: {} @@ -3197,8 +3163,6 @@ snapshots: tinyrainbow@3.1.1: {} - toml@4.1.2: {} - totalist@3.0.1: {} tslib@2.8.1: @@ -3329,8 +3293,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - ws@8.21.0: {} - ws@8.21.1: {} yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 418211a..f2e6503 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,5 @@ overrides: - effect: 4.0.0-beta.102 + effect: 4.0.0-beta.107 vite: "catalog:" vitest: "catalog:" @@ -9,10 +9,10 @@ allowBuilds: msgpackr-extract: true minimumReleaseAgeExclude: - - "@effect/platform-node-shared@4.0.0-beta.102" - - "@effect/platform-node@4.0.0-beta.102" - - "@effect/vitest@4.0.0-beta.102" - - effect@4.0.0-beta.102 + - "@effect/platform-node-shared@4.0.0-beta.107" + - "@effect/platform-node@4.0.0-beta.107" + - "@effect/vitest@4.0.0-beta.107" + - effect@4.0.0-beta.107 - knip@6.31.0 catalog: vite: npm:@voidzero-dev/vite-plus-core@0.2.6 @@ -25,3 +25,5 @@ peerDependencyRules: allowedVersions: vite: "*" vitest: "*" +patchedDependencies: + "@putdotio/sdk@10.0.1": patches/@putdotio__sdk@10.0.1.patch diff --git a/scripts/prepare-effect.mts b/scripts/prepare-effect.mts deleted file mode 100644 index 76b40a4..0000000 --- a/scripts/prepare-effect.mts +++ /dev/null @@ -1,105 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync } from "node:fs"; - -if (process.env.CI || process.env.GITHUB_ACTIONS) { - process.exit(0); -} - -const repoDir = ".repos/effect"; -const repoUrl = "https://github.com/Effect-TS/effect.git"; - -const packageJson: unknown = JSON.parse(readFileSync("package.json", "utf8")); - -if ( - typeof packageJson !== "object" || - packageJson === null || - !("dependencies" in packageJson) || - typeof packageJson.dependencies !== "object" || - packageJson.dependencies === null || - !("effect" in packageJson.dependencies) || - typeof packageJson.dependencies.effect !== "string" -) { - throw new Error("Expected package.json dependencies.effect to own the Effect version."); -} - -const effectVersion = packageJson.dependencies.effect; - -if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(effectVersion)) { - throw new Error(`Expected an exact Effect version, received ${effectVersion}.`); -} - -const effectRef = `effect@${effectVersion}`; - -const git = (args: ReadonlyArray) => - execFileSync("git", ["-C", repoDir, ...args], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }).trim(); - -const normalizeRepoUrl = (url: string) => - url - .replace(/^git@github\.com:/, "https://github.com/") - .replace(/\.git$/, "") - .replace(/\/$/, ""); - -if (!existsSync(`${repoDir}/.git`)) { - mkdirSync(".repos", { recursive: true }); - execFileSync( - "git", - [ - "clone", - "--branch", - effectRef, - "--depth", - "1", - "--filter=blob:none", - "--single-branch", - repoUrl, - repoDir, - ], - { stdio: "inherit" }, - ); - process.exit(0); -} - -const actualRepoUrl = git(["remote", "get-url", "origin"]); - -if (normalizeRepoUrl(actualRepoUrl) !== normalizeRepoUrl(repoUrl)) { - throw new Error( - `Expected ${repoDir} origin to be ${repoUrl}, received ${actualRepoUrl}. ` + - `Repair it with: git -C ${repoDir} remote set-url origin ${repoUrl}`, - ); -} - -if (git(["status", "--porcelain"]).length > 0) { - throw new Error( - `Refusing to align ${repoDir} while it has local changes. Commit or stash them first.`, - ); -} - -let resolvedRef: string; - -try { - resolvedRef = git(["rev-list", "-n", "1", effectRef]); -} catch { - execFileSync( - "git", - [ - "-C", - repoDir, - "fetch", - "--depth", - "1", - "origin", - `refs/tags/${effectRef}:refs/tags/${effectRef}`, - ], - { stdio: "inherit" }, - ); - resolvedRef = git(["rev-list", "-n", "1", effectRef]); -} - -if (git(["rev-parse", "HEAD"]) !== resolvedRef) { - execFileSync("git", ["-C", repoDir, "checkout", "--detach", effectRef], { - stdio: "inherit", - }); -} diff --git a/scripts/prepare-effect.sh b/scripts/prepare-effect.sh deleted file mode 100755 index da0e4af..0000000 --- a/scripts/prepare-effect.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env sh - -set -eu - -exec node ./scripts/prepare-effect.mts diff --git a/scripts/smoke-packed-install.mts b/scripts/smoke-packed-install.mts index c8eb58b..24c18bc 100644 --- a/scripts/smoke-packed-install.mts +++ b/scripts/smoke-packed-install.mts @@ -1,4 +1,4 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFileSync, spawn, spawnSync, type ChildProcess } from "node:child_process"; import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -28,12 +28,51 @@ type RemoveResult = { readonly removed: boolean; }; +type TransfersList = { + readonly cursor: string | null; + readonly total: number; + readonly transfers: ReadonlyArray; +}; + +type NpmPackageInventoryEntry = { + readonly version?: string; +}; + const root = process.cwd(); const artifactsDir = join(root, ".artifacts"); const installDir = mkdtempSync(join(tmpdir(), "putio-cli-install-")); const configPath = join(installDir, "putio-config.json"); const commandTimeoutMs = 120_000; +const mockApiSource = ` +import { createServer } from "node:http"; + +const server = createServer((request, response) => { + const isExpectedRequest = + request.method === "GET" && + request.url?.startsWith("/v2/transfers/list?") === true && + request.headers.authorization === "Token packed-smoke-token"; + + response.statusCode = isExpectedRequest ? 200 : 400; + response.setHeader("content-type", "application/json"); + response.end( + JSON.stringify( + isExpectedRequest + ? { cursor: null, status: "OK", total: 0, transfers: [] } + : { error_message: "Unexpected packed-install request", error_type: "BAD_REQUEST" }, + ), + ); +}); + +server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (typeof address !== "object" || address === null) process.exit(1); + process.stdout.write(String(address.port) + "\\n"); +}); + +process.on("SIGTERM", () => server.close(() => process.exit(0))); +`; + const run = (command: string, args: ReadonlyArray, options: object = {}) => execFileSync(command, args, { cwd: root, @@ -68,6 +107,48 @@ const assert = (condition: boolean, message: string) => { } }; +const startMockApi = () => + new Promise<{ readonly baseUrl: string; readonly child: ChildProcess }>((resolve, reject) => { + const child = spawn(process.execPath, ["--input-type=module", "--eval", mockApiSource], { + stdio: ["ignore", "pipe", "pipe"], + }); + let stderr = ""; + let stdout = ""; + const timer = setTimeout(() => { + child.kill(); + reject(new Error(`Timed out starting the packed-install API server. ${stderr}`.trim())); + }, 10_000); + + child.stderr?.on("data", (chunk) => { + stderr += String(chunk); + }); + child.once("error", (error) => { + clearTimeout(timer); + reject(error); + }); + child.once("exit", (code) => { + clearTimeout(timer); + reject(new Error(`Packed-install API server exited with code ${code}. ${stderr}`.trim())); + }); + child.stdout?.on("data", (chunk) => { + stdout += String(chunk); + const newline = stdout.indexOf("\n"); + + if (newline < 0) return; + + const port = Number(stdout.slice(0, newline)); + clearTimeout(timer); + + if (!Number.isInteger(port) || port <= 0) { + child.kill(); + reject(new Error(`Expected the API server to report a valid port. ${stderr}`.trim())); + return; + } + + resolve({ baseUrl: `http://127.0.0.1:${port}`, child }); + }); + }); + const readFailureMessage = (value: unknown) => { if ( typeof value !== "object" || @@ -254,6 +335,8 @@ const smokeAuthProfiles = (binaryPath: string) => { ); }; +let mockApiProcess: ChildProcess | undefined; + try { rmSync(artifactsDir, { force: true, recursive: true }); run("pnpm", ["pack", "--pack-destination", artifactsDir]); @@ -299,6 +382,36 @@ try { JSON.parse(describeOutput); smokeAuthProfiles(binaryPath); + const effectInventory = JSON.parse( + execFileSync("npm", ["query", '[name="effect"]', "--json"], { + cwd: installDir, + encoding: "utf8", + stdio: "pipe", + timeout: commandTimeoutMs, + }), + ) as ReadonlyArray; + const effectVersions = effectInventory.flatMap((entry) => + entry.version === undefined ? [] : [entry.version], + ); + assert( + effectVersions.length === 1 && effectVersions[0] === "4.0.0-beta.107", + `Expected the package to install one Effect 4.0.0-beta.107 runtime, received ${effectVersions.join(", ")}.`, + ); + + const mockApi = await startMockApi(); + mockApiProcess = mockApi.child; + const transfers = runPutioJson( + binaryPath, + ["transfers", "list", "--output", "json"], + { + PUTIO_CLI_API_BASE_URL: mockApi.baseUrl, + PUTIO_CLI_TOKEN: "packed-smoke-token", + }, + ); + assert(transfers.transfers.length === 0, "Expected the SDK-backed transfer list to be empty."); + assert(transfers.cursor === null, "Expected the SDK-backed transfer list cursor to be null."); + assert(transfers.total === 0, "Expected the SDK-backed transfer list total to be zero."); + const missingAuthMessage = runPutioFailure( binaryPath, ["whoami", "--fields", "auth", "--output", "json"], @@ -328,6 +441,8 @@ try { "packaged-install", "version", "describe", + "single-effect-runtime", + "authenticated-sdk-request", "auth-profile-round-trip", "missing-auth-failure", "invalid-config-failure", @@ -340,5 +455,6 @@ try { )}\n`, ); } finally { + mockApiProcess?.kill(); rmSync(installDir, { force: true, recursive: true }); } diff --git a/src/bin.ts b/src/bin.ts index b58d711..0222b16 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,26 +1,17 @@ #!/usr/bin/env node import { NodeRuntime } from "@effect/platform-node"; -import { Cause, Effect } from "effect"; +import { Effect } from "effect"; import { runCli } from "./cli.js"; import { makeCliAppLayer } from "./internal/app-layer.js"; -import { CliOutput, detectOutputModeFromArgv } from "./internal/output-service.js"; +import { handleCliCause } from "./internal/main.js"; import { CliRuntime } from "./internal/runtime.js"; NodeRuntime.runMain( Effect.scoped( Effect.flatMap(CliRuntime, (runtime) => runCli(runtime.argv)).pipe( - Effect.catchCause((cause) => - Effect.gen(function* () { - const cliOutput = yield* CliOutput; - const runtime = yield* CliRuntime; - const outputMode = detectOutputModeFromArgv(runtime.argv, runtime.isInteractiveTerminal); - - yield* cliOutput.error(cliOutput.formatError(Cause.squash(cause), outputMode)); - yield* runtime.setExitCode(1); - }), - ), + Effect.catchCause(handleCliCause), Effect.provide(makeCliAppLayer()), ), ), diff --git a/src/internal/command.test.ts b/src/internal/command.test.ts index e8bccbf..ab3ffa3 100644 --- a/src/internal/command.test.ts +++ b/src/internal/command.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { Effect, Option, Schema } from "effect"; import { + CliCommandInputError, collectAllCursorPages, decodeJsonOption, parseRepeatedIntegers, @@ -337,6 +338,49 @@ describe("collectAllCursorPages", () => { expect(String(exit.cause)).toContain("pagination returned a repeated cursor"); } }); + + it("surfaces pagination validation through the typed error channel", async () => { + const failure = await Effect.runPromise( + Effect.flip( + collectAllCursorPages({ + command: "files list", + continueWithCursor: () => + Effect.succeed({ + cursor: "cursor-1", + files: [{ id: 2 }], + }), + initial: { + cursor: "cursor-1", + files: [{ id: 1 }], + }, + itemKey: "files", + pageAll: true, + }), + ), + ); + + expect(failure).toBeInstanceOf(CliCommandInputError); + }); + + it("preserves continuation failures for their owning error boundary", async () => { + const upstreamError = new Error("upstream unavailable"); + const failure = await Effect.runPromise( + Effect.flip( + collectAllCursorPages({ + command: "files list", + continueWithCursor: () => Effect.fail(upstreamError), + initial: { + cursor: "cursor-1", + files: [{ id: 1 }], + }, + itemKey: "files", + pageAll: true, + }), + ), + ); + + expect(failure).toBe(upstreamError); + }); }); describe("agent-safe string validation", () => { diff --git a/src/internal/command.ts b/src/internal/command.ts index b65d97c..c719c9c 100644 --- a/src/internal/command.ts +++ b/src/internal/command.ts @@ -429,6 +429,12 @@ const mapInputError = (error: unknown, fallbackMessage: string) => message: fallbackMessage, }); +const tryCommandInput = (try_: () => A, fallbackMessage: string) => + Effect.try({ + try: try_, + catch: (error) => mapInputError(error, fallbackMessage), + }); + export const decodeJsonOption = (schema: Schema.Codec, raw: string) => Effect.try({ try: () => JSON.parse(raw) as unknown, @@ -543,39 +549,64 @@ export const collectAllCursorPages = , E, R>(i return input.initial; } - const collectedItems = [...readPageItems(input.initial, input.itemKey, input.command)]; + const collectedItems = [ + ...(yield* tryCommandInput( + () => readPageItems(input.initial, input.itemKey, input.command), + `Unable to validate the first page for \`${input.command}\`.`, + )), + ]; const seenCursors = new Set(); let cursor = readCursor(input.initial); let pageCount = 1; - assertCursorPageBudget({ - command: input.command, - itemCount: collectedItems.length, - pageCount, - }); + yield* tryCommandInput( + () => + assertCursorPageBudget({ + command: input.command, + itemCount: collectedItems.length, + pageCount, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); while (cursor !== null) { - assertCursorNotSeen({ - command: input.command, - cursor, - seenCursors, - }); + const currentCursor = cursor; + yield* tryCommandInput( + () => + assertCursorNotSeen({ + command: input.command, + cursor: currentCursor, + seenCursors, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); - const nextPage = yield* input.continueWithCursor(cursor); + const nextPage = yield* input.continueWithCursor(currentCursor); const nextCursor = readCursor(nextPage); - assertCursorNotRepeated({ - command: input.command, - cursor: nextCursor, - seenCursors, - }); - const pageItems = readPageItems(nextPage, input.itemKey, input.command); + yield* tryCommandInput( + () => + assertCursorNotRepeated({ + command: input.command, + cursor: nextCursor, + seenCursors, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); + const pageItems = yield* tryCommandInput( + () => readPageItems(nextPage, input.itemKey, input.command), + `Unable to validate a page for \`${input.command}\`.`, + ); pageCount += 1; collectedItems.push(...pageItems); - assertCursorPageBudget({ - command: input.command, - itemCount: collectedItems.length, - pageCount, - }); + yield* tryCommandInput( + () => + assertCursorPageBudget({ + command: input.command, + itemCount: collectedItems.length, + pageCount, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); cursor = nextCursor; } @@ -584,11 +615,7 @@ export const collectAllCursorPages = , E, R>(i [input.itemKey]: collectedItems, ...(Object.prototype.hasOwnProperty.call(input.initial, "cursor") ? { cursor: null } : {}), } as A; - }).pipe( - Effect.mapError((error) => - mapInputError(error, `Unable to collect all pages for \`${input.command}\`.`), - ), - ); + }); export const writeReadOutput = >(input: { readonly command: string; @@ -650,12 +677,20 @@ export const writeReadPages = , E, R>(input: { while (true) { if (input.itemKey) { - streamedItemCount += readPageItems(current, input.itemKey, input.command).length; - assertCursorPageBudget({ - command: input.command, - itemCount: streamedItemCount, - pageCount, - }); + const itemKey = input.itemKey; + streamedItemCount += (yield* tryCommandInput( + () => readPageItems(current, itemKey, input.command), + `Unable to validate a page for \`${input.command}\`.`, + )).length; + yield* tryCommandInput( + () => + assertCursorPageBudget({ + command: input.command, + itemCount: streamedItemCount, + pageCount, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); } const selectedValue = yield* selectTopLevelFields({ @@ -676,18 +711,26 @@ export const writeReadPages = , E, R>(input: { return; } - assertCursorNotSeen({ - command: input.command, - cursor, - seenCursors, - }); + yield* tryCommandInput( + () => + assertCursorNotSeen({ + command: input.command, + cursor, + seenCursors, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); const nextPage = yield* input.continueWithCursor(cursor); - assertCursorNotRepeated({ - command: input.command, - cursor: readCursor(nextPage), - seenCursors, - }); + yield* tryCommandInput( + () => + assertCursorNotRepeated({ + command: input.command, + cursor: readCursor(nextPage), + seenCursors, + }), + `Unable to validate pagination for \`${input.command}\`.`, + ); current = nextPage; pageCount += 1; } diff --git a/src/internal/localize-error.test.ts b/src/internal/localize-error.test.ts index c7a1c8c..0e26cb5 100644 --- a/src/internal/localize-error.test.ts +++ b/src/internal/localize-error.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vite-plus/test"; import { isLocalizedError, localizeCliError } from "./localize-error.js"; +import { isPlainRecord } from "./localizers/helpers.js"; const operationError = ( domain: string, @@ -39,6 +40,13 @@ const operationError = ( status, }); +describe("isPlainRecord", () => { + it("accepts records and rejects arrays", () => { + expect(isPlainRecord({ error: "boom" })).toBe(true); + expect(isPlainRecord(["boom"])).toBe(false); + }); +}); + describe("localizeCliError", () => { it("localizes rate limit errors with retry details", () => { const localized = localizeCliError({ diff --git a/src/internal/localizers/helpers.ts b/src/internal/localizers/helpers.ts index 2d3d1e7..0ff08cb 100644 --- a/src/internal/localizers/helpers.ts +++ b/src/internal/localizers/helpers.ts @@ -1,7 +1,8 @@ import type { MatchConditionLocalizer } from "@putdotio/sdk/utilities"; +import { Predicate } from "effect"; export const isPlainRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null; + Predicate.isObject(value); export const parseRetryAfterSeconds = (value: unknown): number | undefined => { if (typeof value === "number" && Number.isFinite(value)) { diff --git a/src/internal/main.test.ts b/src/internal/main.test.ts new file mode 100644 index 0000000..d450b58 --- /dev/null +++ b/src/internal/main.test.ts @@ -0,0 +1,50 @@ +import { Cause, Effect } from "effect"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { handleCliCause } from "./main.js"; +import { CliOutput, type CliOutputService } from "./output-service.js"; +import { CliRuntime, makeCliRuntime } from "./runtime.js"; + +describe("handleCliCause", () => { + it("preserves interrupt-only causes for NodeRuntime signal handling", async () => { + const exit = await Effect.runPromiseExit(handleCliCause(Cause.interrupt())); + + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true); + } + }); + + it("renders ordinary failures and sets the process exit code", async () => { + const failure = new Error("boom"); + let exitCode: number | undefined; + const formatError = vi.fn(() => "formatted failure"); + const writeError = vi.fn(() => Effect.void); + const cliOutput: CliOutputService = { + error: writeError, + formatError, + write: () => Effect.void, + }; + const runtime = { + ...makeCliRuntime({ + argv: ["node", "putio", "whoami", "--output", "json"], + isInteractiveTerminal: false, + }), + setExitCode: (code: number) => + Effect.sync(() => { + exitCode = code; + }), + }; + + await Effect.runPromise( + handleCliCause(Cause.fail(failure)).pipe( + Effect.provideService(CliOutput, cliOutput), + Effect.provideService(CliRuntime, runtime), + ), + ); + + expect(formatError).toHaveBeenCalledWith(failure, "json"); + expect(writeError).toHaveBeenCalledWith("formatted failure"); + expect(exitCode).toBe(1); + }); +}); diff --git a/src/internal/main.ts b/src/internal/main.ts new file mode 100644 index 0000000..13dac6d --- /dev/null +++ b/src/internal/main.ts @@ -0,0 +1,19 @@ +import { Cause, Effect } from "effect"; + +import { CliOutput, detectOutputModeFromArgv } from "./output-service.js"; +import { CliRuntime } from "./runtime.js"; + +export const handleCliCause = (cause: Cause.Cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.failCause(cause); + } + + return Effect.gen(function* () { + const cliOutput = yield* CliOutput; + const runtime = yield* CliRuntime; + const outputMode = detectOutputModeFromArgv(runtime.argv, runtime.isInteractiveTerminal); + + yield* cliOutput.error(cliOutput.formatError(Cause.squash(cause), outputMode)); + yield* runtime.setExitCode(1); + }); +}; diff --git a/src/internal/output-service.ts b/src/internal/output-service.ts index f086285..f8dfc7e 100644 --- a/src/internal/output-service.ts +++ b/src/internal/output-service.ts @@ -1,5 +1,5 @@ import { LocalizedError } from "@putdotio/sdk/utilities"; -import { Console, Context, Effect, Layer } from "effect"; +import { Console, Context, Effect, Layer, Predicate } from "effect"; import { isLocalizedError, localizeCliError } from "./localize-error.js"; import { CliRuntime } from "./runtime.js"; @@ -81,7 +81,7 @@ const TERMINAL_CONTROL_PATTERN = new RegExp( ); const isPlainObject = (value: unknown): value is Record => { - if (typeof value !== "object" || value === null || Array.isArray(value)) { + if (!Predicate.isObject(value)) { return false; } diff --git a/src/internal/runtime.test.ts b/src/internal/runtime.test.ts index a402dfc..43a9803 100644 --- a/src/internal/runtime.test.ts +++ b/src/internal/runtime.test.ts @@ -1,4 +1,5 @@ -import { Effect } from "effect"; +import { Effect, Fiber } from "effect"; +import { EventEmitter } from "node:events"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const { spawnMock } = vi.hoisted(() => ({ @@ -27,7 +28,16 @@ describe("makeCliRuntime", () => { }); it("opens URLs with the right command for each platform", async () => { - spawnMock.mockReturnValue({ unref: vi.fn() }); + spawnMock.mockImplementation(() => { + const child = new EventEmitter() as EventEmitter & { + pid: number; + unref: ReturnType; + }; + child.pid = 123; + child.unref = vi.fn(); + queueMicrotask(() => child.emit("spawn")); + return child; + }); const darwin = makeCliRuntime({ platform: "darwin" }); const linux = makeCliRuntime({ platform: "linux" }); @@ -41,19 +51,19 @@ describe("makeCliRuntime", () => { 1, "open", ["https://app.put.io"], - expect.objectContaining({ detached: true, stdio: "ignore" }), + expect.objectContaining({ detached: true, signal: expect.any(AbortSignal), stdio: "ignore" }), ); expect(spawnMock).toHaveBeenNthCalledWith( 2, "xdg-open", ["https://app.put.io"], - expect.objectContaining({ detached: true, stdio: "ignore" }), + expect.objectContaining({ detached: true, signal: expect.any(AbortSignal), stdio: "ignore" }), ); expect(spawnMock).toHaveBeenNthCalledWith( 3, "cmd", ["/c", "start", "", "https://app.put.io"], - expect.objectContaining({ detached: true, stdio: "ignore" }), + expect.objectContaining({ detached: true, signal: expect.any(AbortSignal), stdio: "ignore" }), ); }); @@ -69,6 +79,56 @@ describe("makeCliRuntime", () => { ); }); + it("returns false when the opener emits an asynchronous spawn error", async () => { + const unref = vi.fn(); + spawnMock.mockImplementation(() => { + const child = new EventEmitter() as EventEmitter & { + pid: undefined; + unref: ReturnType; + }; + child.pid = undefined; + child.unref = unref; + queueMicrotask(() => child.emit("error", new Error("missing opener"))); + return child; + }); + + const runtime = makeCliRuntime({ platform: "linux" }); + + await expect(Effect.runPromise(runtime.openExternal("https://app.put.io"))).resolves.toBe( + false, + ); + expect(unref).not.toHaveBeenCalled(); + }); + + it("aborts opener startup without leaving AbortError unhandled", async () => { + const child = new EventEmitter() as EventEmitter & { + unref: ReturnType; + }; + child.unref = vi.fn(); + let spawnSignal: AbortSignal | undefined; + spawnMock.mockImplementation((_file, _args, options: { signal: AbortSignal }) => { + spawnSignal = options.signal; + options.signal.addEventListener( + "abort", + () => queueMicrotask(() => child.emit("error", new Error("aborted"))), + { once: true }, + ); + return child; + }); + + const runtime = makeCliRuntime({ platform: "linux" }); + const fiber = Effect.runFork(runtime.openExternal("https://app.put.io")); + + await Effect.runPromise(Fiber.interrupt(fiber)); + await Promise.resolve(); + + expect(spawnSignal?.aborted).toBe(true); + expect(child.listenerCount("error")).toBe(0); + expect(child.listenerCount("spawn")).toBe(0); + expect(child.listenerCount("close")).toBe(0); + expect(child.unref).not.toHaveBeenCalled(); + }); + it("starts and stops a spinner and clears the terminal line", async () => { spawnMock.mockReturnValue({ unref: vi.fn() }); const writeSpy = vi.spyOn(process.stdout, "write").mockReturnValue(true); diff --git a/src/internal/runtime.ts b/src/internal/runtime.ts index 9bdce83..0617221 100644 --- a/src/internal/runtime.ts +++ b/src/internal/runtime.ts @@ -34,18 +34,44 @@ const openExternalWithPlatform = (platform: NodeJS.Platform, url: string) => { ? { file: "cmd", args: ["/c", "start", "", url] } : { file: "xdg-open", args: [url] }; - try { - const child = spawn(command.file, command.args, { - detached: true, - stdio: "ignore", + return Effect.callback((resume, signal) => { + let child: ReturnType; + + try { + child = spawn(command.file, command.args, { + detached: true, + signal, + stdio: "ignore", + }); + } catch { + resume(Effect.succeed(false)); + return; + } + + const onError = () => { + child.removeListener("spawn", onSpawn); + child.removeListener("close", onClose); + resume(Effect.succeed(false)); + }; + const onClose = () => { + child.removeListener("error", onError); + child.removeListener("spawn", onSpawn); + }; + const onSpawn = () => { + child.removeListener("error", onError); + child.removeListener("close", onClose); + child.unref(); + resume(Effect.succeed(true)); + }; + + child.once("close", onClose); + child.once("error", onError); + child.once("spawn", onSpawn); + + return Effect.sync(() => { + child.removeListener("spawn", onSpawn); }); - - child.unref(); - - return true; - } catch { - return false; - } + }); }; export const makeCliRuntime = ( @@ -92,7 +118,7 @@ export const makeCliRuntime = ( process.stderr.write(message); }), - openExternal: (url) => Effect.sync(() => openExternalWithPlatform(platform, url)), + openExternal: (url) => openExternalWithPlatform(platform, url), startSpinner: (message) => Effect.sync(() => { let frameIndex = 0; diff --git a/src/sea.ts b/src/sea.ts index ee797b2..937d847 100644 --- a/src/sea.ts +++ b/src/sea.ts @@ -1,24 +1,15 @@ import { NodeRuntime } from "@effect/platform-node"; -import { Cause, Effect } from "effect"; +import { Effect } from "effect"; import { runCli } from "./cli.js"; import { makeCliAppLayer } from "./internal/app-layer.js"; -import { CliOutput, detectOutputModeFromArgv } from "./internal/output-service.js"; +import { handleCliCause } from "./internal/main.js"; import { CliRuntime } from "./internal/runtime.js"; NodeRuntime.runMain( Effect.scoped( Effect.flatMap(CliRuntime, (runtime) => runCli(runtime.argv)).pipe( - Effect.catchCause((cause) => - Effect.gen(function* () { - const cliOutput = yield* CliOutput; - const runtime = yield* CliRuntime; - const outputMode = detectOutputModeFromArgv(runtime.argv, runtime.isInteractiveTerminal); - - yield* cliOutput.error(cliOutput.formatError(Cause.squash(cause), outputMode)); - yield* runtime.setExitCode(1); - }), - ), + Effect.catchCause(handleCliCause), Effect.provide(makeCliAppLayer()), ), ), diff --git a/vite.config.ts b/vite.config.ts index 619a929..0922d30 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -24,6 +24,10 @@ const coverageConfig: CoverageConfig = { export default defineConfig({ pack: { clean: true, + deps: { + alwaysBundle: ["@putdotio/sdk"], + onlyBundle: ["@putdotio/sdk"], + }, dts: false, entry: { index: "src/index.ts", @@ -39,7 +43,7 @@ export default defineConfig({ "*.{js,ts,tsx,mjs,cjs,mts,cts}": "vp check --fix", }, test: { - exclude: ["node_modules/**", ".repos/**", "scripts/**/*.test.ts"], + exclude: ["node_modules/**", "scripts/**/*.test.ts"], coverage: { ...coverageConfig, exclude: [