diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6037d3ce --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "ts-sdk"] + path = ts-sdk + url = https://github.com/useautumn/ts-sdk + branch = next diff --git a/atmn/.gitignore b/atmn/.gitignore index a0d218e3..7d9e0450 100644 --- a/atmn/.gitignore +++ b/atmn/.gitignore @@ -1,3 +1,4 @@ node_modules dist -.env \ No newline at end of file +.env +test/.env.test \ No newline at end of file diff --git a/atmn/AGENTS.md b/atmn/AGENTS.md new file mode 100644 index 00000000..baf23c84 --- /dev/null +++ b/atmn/AGENTS.md @@ -0,0 +1,202 @@ +# ATMN CLI - Code Conventions + +## Directory Structure + +The CLI has two source directories: + +- **`source/`** - Legacy code (commands, core logic, compose builders) +- **`src/`** - New code following improved architecture + +New features should be added to `src/`. Existing features in `source/` will be migrated incrementally. + +``` +atmn/ +├── source/ # Legacy code +│ ├── cli.ts # Main entry point +│ ├── constants.ts # BACKEND_URL, FRONTEND_URL, DEFAULT_CONFIG +│ ├── index.ts # Package exports +│ ├── commands/ # CLI commands (pull, push, nuke, init) +│ ├── core/ # Business logic, API requests, utilities +│ └── compose/ # DSL builders for plans/features +│ +├── src/ # New architecture +│ ├── commands/ # Command modules (each command in its own folder) +│ │ └── auth/ # Auth command +│ │ ├── command.ts # Main command export +│ │ ├── oauth.ts # OAuth flow logic +│ │ └── constants.ts # Command-specific constants +│ └── views/ # UI templates +│ └── html/ # HTML templates for browser callbacks +│ └── oauth-callback.ts +│ +├── test/ # Tests +└── dist/ # Build output +``` + +## Architecture Conventions + +### Commands (`src/commands//`) + +Each command should have its own folder with: +- `command.ts` - The main command function (default export) +- `constants.ts` - Command-specific constants +- Additional files for supporting logic (e.g., `oauth.ts` for auth) + +### Views (`src/views/`) + +UI templates organized by type: +- `html/` - HTML templates (for browser callbacks, etc.) +- `react/` - React/Ink components (future) + +### Shared Utilities + +Currently in `source/core/utils.ts`. These will be migrated to `src/utils/` as needed: +- `env.ts` - Environment variable helpers (`readFromEnv`, `storeToEnv`) +- `spinner.ts` - CLI spinner (`initSpinner`) +- `string.ts` - String utilities + +## Import Conventions + +- Use `.js` extensions in imports (required for ESM) +- Import shared constants from `source/constants.js` (until migrated) +- Import utilities from `source/core/utils.js` (until migrated) + +## OAuth Flow + +The auth command uses OAuth 2.1 PKCE: +1. Local HTTP server starts on port `31448` (or next available up to `31452`) +2. Browser opens to backend authorize URL with PKCE challenge +3. User authenticates and selects organization +4. Callback received with authorization code +5. Code exchanged for access token +6. Access token used to create API keys via `/cli/api-keys` endpoint +7. Keys saved to `.env` file + +Key constants: +- `CLI_CLIENT_ID`: `qncNuaPFAEBwzCypjFopNCGPHQDqkchp` +- `OAUTH_PORTS`: `31448-31452` (5 ports, tries each in sequence if previous is in use) + +## React Component Conventions (`src/views/react/`) + +When React files grow large, use folder-based decomposition: + +1. **Create a folder with the component's name** - Replace `Component.tsx` with `Component/` folder +2. **Move parent into folder** - Place main component in `Component/Component.tsx` +3. **Extract children as siblings** - Create separate files for sub-components (e.g., `Component/SubComponent.tsx`) +4. **Apply recursively** - If children grow large, repeat the pattern for them +5. **Extract shared components** - Move frequently reused components to `react/components/` + +**Example:** +``` +react/ +├── InitFlow.tsx # Original file (300+ lines) +│ +# After decomposition: +├── init/ +│ ├── InitFlow.tsx # Main orchestrator +│ └── steps/ +│ ├── AuthStep.tsx # Step 1 +│ ├── StripeStep.tsx # Step 2 +│ └── ConfigStep.tsx # Step 3 +│ +└── components/ # Shared across multiple views + ├── StepHeader.tsx + ├── StatusLine.tsx + └── SelectMenu.tsx +``` + +**Useful Ink Components:** +- `ink-select-input` - Interactive select menus with arrow key navigation +- `ink-confirm-input` - Yes/No confirmation prompts +- `ink-spinner` - Loading spinners +- `ink-text-input` - Text input fields + +## Build + +```bash +npm run build # Build with tsup +npm run dev # Watch mode +npm run test # Run tests +``` + +Entry points are in `source/` but tsup follows imports to include `src/` files. + +## Code Separation Rules + +### .tsx Files (Components) +- **ONLY rendering logic** - No API calls, no business logic, no data transformation +- Import and use custom hooks for all data/state management +- No `useEffect` + `async/await` patterns +- No JSON boilerplate or large data constants + +### .ts Files (Logic) +- All business logic, API calls, data transformation +- Custom hooks (`useX`) for stateful logic +- Utility functions for pure transformations +- Constants and configuration data + +### Data Fetching +- **Always use TanStack Query for queries** - No `useEffect` + `async/await` for data fetching +- Create reusable hooks: `useOrganization`, `usePull`, etc. +- **Use TanStack Query for**: Data that should be cached/refetched (org info, lists, etc.) +- **Use useState + useEffect for**: One-time operations/mutations (pull, push, nuke) +- Hooks return consistent patterns: `{ data, isLoading, error }` or `{ ...data, isLoading, error }` + +### Directory Structure for Logic + +``` +src/ +├── lib/ +│ ├── api/ # API client and endpoints +│ │ ├── client.ts +│ │ └── endpoints/ +│ ├── hooks/ # Custom React hooks +│ │ ├── useOrganization.ts # Org info hook +│ │ ├── usePull.ts # Pull operation hook +│ │ └── index.ts +│ ├── utils/ # Pure utility functions +│ │ └── files.ts +│ └── constants/ # Shared constants, template data +│ └── templates.ts +└── views/react/ # UI components (rendering only) +``` + +### Example: Good vs Bad + +**❌ Bad - Logic in .tsx:** +```tsx +// Component.tsx +export function MyComponent() { + const [data, setData] = useState(null); + + useEffect(() => { + async function fetchData() { + const result = await fetch('/api/data'); + setData(result); + } + fetchData(); + }, []); + + return
{data}
; +} +``` + +**✅ Good - Logic in hook, .tsx only renders:** +```tsx +// hooks/useMyData.ts +export function useMyData() { + return useQuery({ + queryKey: ['myData'], + queryFn: () => fetchMyData(), + }); +} + +// Component.tsx +export function MyComponent() { + const { data, isLoading, error } = useMyData(); + + if (isLoading) return ; + if (error) return ; + return
{data}
; +} +``` diff --git a/atmn/bun.config.ts b/atmn/bun.config.ts index e7048e63..481c6373 100644 --- a/atmn/bun.config.ts +++ b/atmn/bun.config.ts @@ -4,25 +4,29 @@ async function getVersion(): Promise { } async function build() { - console.time(`Building atmn v${await getVersion()}`); + const version = await getVersion(); + + // Build everything with Bun + console.time(`Building atmn v${version}`); await Bun.build({ - entrypoints: ["./source/cli.ts", "./source/index.ts"], + entrypoints: ["./src/cli.tsx", "./source/index.ts"], outdir: "./dist", format: "esm", target: "node", define: { - VERSION: `"${await getVersion()}"`, + VERSION: `"${version}"`, }, - external: ["prettier"] + external: ["prettier"], }); - console.timeEnd(`Building atmn v${await getVersion()}`); + console.timeEnd(`Building atmn v${version}`); - // Generate TypeScript declaration files - console.time(`Generating TypeScript declaration files`); - await Bun.spawn(["tsc", "--emitDeclarationOnly"], { + // Generate TypeScript declarations with tsc + console.time(`Generating type declarations`); + const tsc = Bun.spawn(["tsc", "--project", "tsconfig.build.json"], { stdio: ["inherit", "inherit", "inherit"], }); - console.timeEnd(`Generating TypeScript declaration files`); + await tsc.exited; + console.timeEnd(`Generating type declarations`); } build(); \ No newline at end of file diff --git a/atmn/bun.lock b/atmn/bun.lock new file mode 100644 index 00000000..fc50daa7 --- /dev/null +++ b/atmn/bun.lock @@ -0,0 +1,2079 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "atmn", + "dependencies": { + "@inkjs/ui": "^2.0.0", + "@inquirer/prompts": "^7.6.0", + "@mishieck/ink-titled-box": "^0.3.0", + "@tanstack/react-query": "^5.90.17", + "@types/prettier": "^3.0.0", + "arctic": "^3.7.0", + "axios": "^1.10.0", + "chalk": "^5.2.0", + "clipboardy": "^5.0.2", + "commander": "^14.0.0", + "dotenv": "^17.2.0", + "ink": "^6.6.0", + "ink-big-text": "^2.0.0", + "ink-chart": "^0.1.1", + "ink-confirm-input": "^2.0.0", + "ink-scroll-list": "^0.4.1", + "ink-scroll-view": "^0.3.5", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-table": "^3.1.0", + "ink-text-input": "^6.0.0", + "inquirer": "^12.7.0", + "jiti": "^2.4.2", + "open": "^10.1.2", + "prettier": "^3.6.2", + "react": "^19.2.3", + "yocto-spinner": "^1.0.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@sindresorhus/tsconfig": "^3.0.1", + "@types/bun": "^1.2.21", + "@types/node": "^24.0.10", + "@types/react": "^19.0.0", + "@vdemedes/prettier-config": "^2.0.1", + "ava": "^5.2.0", + "cli-testing-library": "^3.0.1", + "eslint-config-xo-react": "^0.27.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "ink-testing-library": "^3.0.0", + "nodemon": "^3.1.11", + "react-devtools-core": "^6.1.2", + "ts-node": "^10.9.1", + "tsup": "^8.5.0", + "typescript": "^5.0.3", + "xo": "^0.53.1", + }, + }, + }, + "packages": { + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@inkjs/ui": ["@inkjs/ui@2.0.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-spinners": "^3.0.0", "deepmerge": "^4.3.1", "figures": "^6.1.0" }, "peerDependencies": { "ink": ">=5" } }, "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg=="], + + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/checkbox": ["@inquirer/checkbox@4.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/editor": ["@inquirer/editor@4.2.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/external-editor": "^1.0.3", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ=="], + + "@inquirer/expand": ["@inquirer/expand@4.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@4.3.1", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g=="], + + "@inquirer/number": ["@inquirer/number@3.0.23", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg=="], + + "@inquirer/password": ["@inquirer/password@4.0.23", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA=="], + + "@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@4.1.11", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw=="], + + "@inquirer/search": ["@inquirer/search@3.2.2", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA=="], + + "@inquirer/select": ["@inquirer/select@4.4.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], + + "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], + + "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], + + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], + + "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@sindresorhus/tsconfig": ["@sindresorhus/tsconfig@3.0.1", "", {}, "sha512-0/gtPNTY3++0J2BZM5nHHULg0BIMw886gqdn8vWN+Av6bgF5ZU2qIcHubAn+Z9KNvJhO8WFE+9kDOU3n6OcKtA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/eslint": ["@types/eslint@7.29.0", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng=="], + + "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + + "@types/node": ["@types/node@24.10.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ=="], + + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + + "@types/prettier": ["@types/prettier@3.0.0", "", { "dependencies": { "prettier": "*" } }, "sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA=="], + + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vdemedes/prettier-config": ["@vdemedes/prettier-config@2.0.1", "", {}, "sha512-lcHyyLfS2ro282qsXKpxw+canUkOlFIGoanxt3BaNCm5K1NR8k4hGvYbFO54/+QWq12d0y/EYRz68yNQkqWFrw=="], + + "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], + + "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], + + "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], + + "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], + + "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], + + "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], + + "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], + + "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], + + "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], + + "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], + + "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], + + "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], + + "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], + + "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], + + "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], + + "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], + + "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arctic": ["arctic@3.7.0", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw=="], + + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-find-index": ["array-find-index@1.0.2", "", {}, "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "arrgv": ["arrgv@1.0.2", "", {}, "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw=="], + + "arrify": ["arrify@3.0.0", "", {}, "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], + + "ava": ["ava@5.3.1", "", { "dependencies": { "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", "arrgv": "^1.0.2", "arrify": "^3.0.0", "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.2.0", "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.8.0", "ci-parallel-vars": "^1.0.1", "clean-yaml-object": "^0.1.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "common-path-prefix": "^3.0.0", "concordance": "^5.0.4", "currently-unhandled": "^0.4.1", "debug": "^4.3.4", "emittery": "^1.0.1", "figures": "^5.0.0", "globby": "^13.1.4", "ignore-by-default": "^2.1.0", "indent-string": "^5.0.0", "is-error": "^2.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "matcher": "^5.0.0", "mem": "^9.0.2", "ms": "^2.1.3", "p-event": "^5.0.1", "p-map": "^5.5.0", "picomatch": "^2.3.1", "pkg-conf": "^4.0.0", "plur": "^5.1.0", "pretty-ms": "^8.0.0", "resolve-cwd": "^3.0.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.0.1", "supertap": "^3.0.1", "temp-dir": "^3.0.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, "peerDependencies": { "@ava/typescript": "*" }, "optionalPeers": ["@ava/typescript"], "bin": { "ava": "entrypoints/cli.mjs" } }, "sha512-Scv9a4gMOXB6+ni4toLuhAm9KYWEjsgBglJl+kMGI5+IVDt120CCDZyB5HNU9DjmLI2t4I0GbnxGLmmRfGTJGg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "blueimp-md5": ["blueimp-md5@2.19.0", "", {}, "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "builtin-modules": ["builtin-modules@3.3.0", "", {}, "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw=="], + + "builtins": ["builtins@5.1.0", "", { "dependencies": { "semver": "^7.0.0" } }, "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg=="], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@4.2.0", "", {}, "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ=="], + + "camelcase": ["camelcase@7.0.1", "", {}, "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw=="], + + "camelcase-keys": ["camelcase-keys@8.0.2", "", { "dependencies": { "camelcase": "^7.0.0", "map-obj": "^4.3.0", "quick-lru": "^6.1.1", "type-fest": "^2.13.0" } }, "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], + + "cbor": ["cbor@8.1.0", "", { "dependencies": { "nofilter": "^3.1.0" } }, "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg=="], + + "cfonts": ["cfonts@3.3.1", "", { "dependencies": { "supports-color": "^8", "window-size": "^1" }, "bin": { "cfonts": "bin/index.js" } }, "sha512-ZGEmN3W9mViWEDjsuPo4nK4h39sfh6YtoneFYp9WLPI/rw8BaSSrfQC6jkrGW3JMvV3ZnExJB/AEqXc/nHYxkw=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + + "chunkd": ["chunkd@2.0.1", "", {}, "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ=="], + + "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + + "ci-parallel-vars": ["ci-parallel-vars@1.0.1", "", {}, "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg=="], + + "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], + + "clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="], + + "clean-yaml-object": ["clean-yaml-object@0.1.0", "", {}, "sha512-3yONmlN9CSAkzNwnRCiJQ7Q2xK5mWuEfL3PuTZcAUzhObbXsfsnMptJzXwz93nc5zn9V9TwCVMmV7w4xsm43dw=="], + + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + + "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="], + + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "cli-testing-library": ["cli-testing-library@3.0.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "picocolors": "^1.1.1", "redent": "^4.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "strip-final-newline": "^4.0.0", "tree-kill": "^1.2.2" }, "peerDependencies": { "@jest/expect": "^29.0.0", "@jest/globals": "^29.0.0", "vitest": "^3.0.0" }, "optionalPeers": ["@jest/expect", "@jest/globals", "vitest"] }, "sha512-fkQ8D2hQS53RP3s0yuCMHmTfPUMEqtVtJG0rs13MNE2khnkSaY8MsNxN7rSJZAzOOVsSg2I2F2XjEITJwf5dFg=="], + + "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "clipboardy": ["clipboardy@5.0.2", "", { "dependencies": { "execa": "^9.6.0", "is-wayland": "^0.1.0", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-3IG8i8Yfb410yqDlCx9Ve3lYLFN3bD1IkrWcowT1kyTo6y4bwYf2guK9Q8a6zck5vWm7afm6Y61i7BG/Ir3FMA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + + "common-path-prefix": ["common-path-prefix@3.0.0", "", {}, "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "concordance": ["concordance@5.0.4", "", { "dependencies": { "date-time": "^3.1.0", "esutils": "^2.0.3", "fast-diff": "^1.2.0", "js-string-escape": "^1.0.1", "lodash": "^4.17.15", "md5-hex": "^3.0.1", "semver": "^7.3.2", "well-known-symbols": "^2.0.0" } }, "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "confusing-browser-globals": ["confusing-browser-globals@1.0.11", "", {}, "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + + "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "currently-unhandled": ["currently-unhandled@0.4.1", "", { "dependencies": { "array-find-index": "^1.0.1" } }, "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "date-time": ["date-time@3.1.0", "", { "dependencies": { "time-zone": "^1.0.0" } }, "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decamelize": ["decamelize@6.0.1", "", {}, "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ=="], + + "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "define-property": ["define-property@1.0.0", "", { "dependencies": { "is-descriptor": "^1.0.0" } }, "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + + "emittery": ["emittery@1.2.0", "", {}, "sha512-KxdRyyFcS85pH3dnU8Y5yFUm2YJdaHwcBZWrfG8o89ZY9a13/f9itbN+YG3ELbBo9Pg5zvIozstmuV8bX13q6g=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "enhance-visitors": ["enhance-visitors@1.0.0", "", { "dependencies": { "lodash": "^4.13.1" } }, "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA=="], + + "enhanced-resolve": ["enhanced-resolve@0.9.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.2.0", "tapable": "^0.1.8" } }, "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw=="], + + "env-editor": ["env-editor@1.3.0", "", {}, "sha512-EqiD/j01PooUbeWk+etUo2TWoocjoxMfGNYpS9e47glIJ5r8WepycIki+LCbonFbPdwlqY5ETeSTAJVMih4z4w=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "ervy": ["ervy@1.0.7", "", {}, "sha512-LyHLPwIKxCKKtTO/qBMFzwA1BD5IjpM0AA3k6CeK9hrEn4Kbayi93G9eD/Ko4suEIjerSl7YpMmaOoL0g9OCoQ=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "es-toolkit": ["es-toolkit@1.43.0", "", {}, "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-prettier": ["eslint-config-prettier@8.10.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A=="], + + "eslint-config-xo": ["eslint-config-xo@0.43.1", "", { "dependencies": { "confusing-browser-globals": "1.0.11" }, "peerDependencies": { "eslint": ">=8.27.0" } }, "sha512-azv1L2PysRA0NkZOgbndUpN+581L7wPqkgJOgxxw3hxwXAbJgD6Hqb/SjHRiACifXt/AvxCzE/jIKFAlI7XjvQ=="], + + "eslint-config-xo-react": ["eslint-config-xo-react@0.27.0", "", { "peerDependencies": { "eslint": ">=8.6.0", "eslint-plugin-react": ">=7.29.0", "eslint-plugin-react-hooks": ">=4.3.0" } }, "sha512-wiV215xQIn71XZyyVfaOXHaFpR1B14IJttwOjMi/eqUK1s+ojJdHr7eHqTLaGUfh6FKgWha1QNwePlIXx7mBUg=="], + + "eslint-formatter-pretty": ["eslint-formatter-pretty@4.1.0", "", { "dependencies": { "@types/eslint": "^7.2.13", "ansi-escapes": "^4.2.1", "chalk": "^4.1.0", "eslint-rule-docs": "^1.1.5", "log-symbols": "^4.0.0", "plur": "^4.0.0", "string-width": "^4.2.0", "supports-hyperlinks": "^2.0.0" } }, "sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-webpack": ["eslint-import-resolver-webpack@0.13.10", "", { "dependencies": { "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", "hasown": "^2.0.2", "interpret": "^1.4.0", "is-core-module": "^2.15.1", "is-regex": "^1.2.0", "lodash": "^4.17.21", "resolve": "^2.0.0-next.5", "semver": "^5.7.2" }, "peerDependencies": { "eslint-plugin-import": ">=1.4.0", "webpack": ">=1.11.0" } }, "sha512-ciVTEg7sA56wRMR772PyjcBRmyBMLS46xgzQZqt6cWBEKc7cK65ZSSLCTLVRu2gGtKyXUb5stwf4xxLBfERLFA=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-ava": ["eslint-plugin-ava@13.2.0", "", { "dependencies": { "enhance-visitors": "^1.0.0", "eslint-utils": "^3.0.0", "espree": "^9.0.0", "espurify": "^2.1.1", "import-modules": "^2.1.0", "micro-spelling-correcter": "^1.1.1", "pkg-dir": "^5.0.0", "resolve-from": "^5.0.0" }, "peerDependencies": { "eslint": ">=7.22.0" } }, "sha512-i5B5izsEdERKQLruk1nIWzTTE7C26/ju8qQf7JeyRv32XT2lRMW0zMFZNhIrEf5/5VvpSz2rqrV7UcjClGbKsw=="], + + "eslint-plugin-es": ["eslint-plugin-es@4.1.0", "", { "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" }, "peerDependencies": { "eslint": ">=4.19.1" } }, "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ=="], + + "eslint-plugin-eslint-comments": ["eslint-plugin-eslint-comments@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5", "ignore": "^5.0.5" }, "peerDependencies": { "eslint": ">=4.19.1" } }, "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-n": ["eslint-plugin-n@15.7.0", "", { "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", "eslint-utils": "^3.0.0", "ignore": "^5.1.1", "is-core-module": "^2.11.0", "minimatch": "^3.1.2", "resolve": "^1.22.1", "semver": "^7.3.8" }, "peerDependencies": { "eslint": ">=7.0.0" } }, "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q=="], + + "eslint-plugin-no-use-extend-native": ["eslint-plugin-no-use-extend-native@0.5.0", "", { "dependencies": { "is-get-set-prop": "^1.0.0", "is-js-type": "^2.0.0", "is-obj-prop": "^1.0.0", "is-proto-prop": "^2.0.0" } }, "sha512-dBNjs8hor8rJgeXLH4HTut5eD3RGWf9JUsadIfuL7UosVQ/dnvOKwxEcRrXrFxrMZ8llUVWT+hOimxJABsAUzQ=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="], + + "eslint-plugin-unicorn": ["eslint-plugin-unicorn@44.0.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.19.1", "ci-info": "^3.4.0", "clean-regexp": "^1.0.0", "eslint-utils": "^3.0.0", "esquery": "^1.4.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.0", "lodash": "^4.17.21", "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", "regexp-tree": "^0.1.24", "safe-regex": "^2.1.1", "semver": "^7.3.7", "strip-indent": "^3.0.0" }, "peerDependencies": { "eslint": ">=8.23.1" } }, "sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w=="], + + "eslint-rule-docs": ["eslint-rule-docs@1.1.235", "", {}, "sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-utils": ["eslint-utils@3.0.0", "", { "dependencies": { "eslint-visitor-keys": "^2.0.0" }, "peerDependencies": { "eslint": ">=5" } }, "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "esm-utils": ["esm-utils@4.4.2", "", { "dependencies": { "import-meta-resolve": "^4.1.0", "url-or-path": "^2.6.1" } }, "sha512-oG7oQZRniJEUSRYzdeWHOAe3n6mW5lNouDFm2b7pfPounjyuSaJSTVybDuiMnBizALdOBfM1r0QKlDh4psOY9Q=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "espurify": ["espurify@2.1.1", "", {}, "sha512-zttWvnkhcDyGOhSH4vO2qCBILpdCMv/MX8lp4cqgRkQoDRGK2oZxi2GfWhlP2dIXmk7BaKeOTuzbHhyC68o8XQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@5.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "is-unicode-supported": "^1.2.0" } }, "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], + + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-set-props": ["get-set-props@0.1.0", "", {}, "sha512-7oKuKzAGKj0ag+eWZwcGw2fjiZ78tXnXQoBgY0aU7ZOxTu4bB7hSuQSDgtKy978EDH062P5FmD2EWiDpQS9K9Q=="], + + "get-stdin": ["get-stdin@9.0.0", "", {}, "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hosted-git-info": ["hosted-git-info@5.2.1", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "ignore-by-default": ["ignore-by-default@2.1.0", "", {}, "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "import-modules": ["import-modules@2.1.0", "", {}, "sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="], + + "ink-big-text": ["ink-big-text@2.0.0", "", { "dependencies": { "cfonts": "^3.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "ink": ">=4", "react": ">=18" } }, "sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw=="], + + "ink-chart": ["ink-chart@0.1.1", "", { "dependencies": { "ervy": "^1.0.4", "prop-types": "^15.7.2" }, "peerDependencies": { "ink": ">=2.0.0", "react": ">=16.8.0" } }, "sha512-A3WxEO4t5ueuTves/zhaTJe/XcPXnlmf//gcKni8W1Nme6Dj0KhtWPTrsNpp6cZQR6k6HG/diEvZVcQ8DMmYyQ=="], + + "ink-confirm-input": ["ink-confirm-input@2.0.0", "", { "dependencies": { "ink-text-input": "^3.2.1", "prop-types": "^15.5.10", "yn": "^3.1.1" }, "peerDependencies": { "ink": ">=2", "react": ">=16" } }, "sha512-YCd7a9XW0DIIbOhF3XiLo3WF86mOart9qI1qN56wT5IDJxU+j8BanEZh5/QXoazyIPSv1iXlHPIlRB5cbZIMIA=="], + + "ink-scroll-list": ["ink-scroll-list@0.4.1", "", { "dependencies": { "ink-scroll-view": "^0.3.5" }, "peerDependencies": { "ink": ">=6", "react": ">=19" } }, "sha512-2GcjnBXRe0sTSMC03SCKX6IkNAsYYgcpvAN+ggAiaqeg+s+qA9Ls0iVTOmB+Ize/9AVts8xloG4aRcrOy/7Xxg=="], + + "ink-scroll-view": ["ink-scroll-view@0.3.5", "", { "peerDependencies": { "ink": ">=6", "react": ">=19" } }, "sha512-NDCKQz0DDvcLQEboXf25oGQ4g2VpoO3NojMC/eG+eaqEz9PDiGJyg7Y+HTa4QaCjogvME6A+IwGyV+yTLCGdaw=="], + + "ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="], + + "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="], + + "ink-table": ["ink-table@3.1.0", "", { "dependencies": { "object-hash": "^2.0.3" }, "peerDependencies": { "ink": ">=3.0.0", "react": ">=16.8.0" } }, "sha512-qxVb4DIaEaJryvF9uZGydnmP9Hkmas3DCKVpEcBYC0E4eJd3qNgNe+PZKuzgCERFe9LfAS1TNWxCr9+AU4v3YA=="], + + "ink-testing-library": ["ink-testing-library@3.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-ItyyoOmcm6yftb7c5mZI2HU22BWzue8PBbO3DStmY8B9xaqfKr7QJONiWOXcwVsOk/6HuVQ0v7N5xhPaR3jycA=="], + + "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="], + + "inquirer": ["inquirer@12.11.1", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/core": "^10.3.2", "@inquirer/prompts": "^7.10.1", "@inquirer/type": "^3.0.10", "mute-stream": "^2.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], + + "irregular-plurals": ["irregular-plurals@3.5.0", "", {}, "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ=="], + + "is-absolute": ["is-absolute@1.0.0", "", { "dependencies": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" } }, "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA=="], + + "is-accessor-descriptor": ["is-accessor-descriptor@1.0.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-builtin-module": ["is-builtin-module@3.2.1", "", { "dependencies": { "builtin-modules": "^3.3.0" } }, "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-descriptor": ["is-data-descriptor@1.0.1", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-descriptor": ["is-descriptor@1.0.3", "", { "dependencies": { "is-accessor-descriptor": "^1.0.1", "is-data-descriptor": "^1.0.1" } }, "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-error": ["is-error@2.2.2", "", {}, "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-get-set-prop": ["is-get-set-prop@1.0.0", "", { "dependencies": { "get-set-props": "^0.1.0", "lowercase-keys": "^1.0.0" } }, "sha512-DvAYZ1ZgGUz4lzxKMPYlt08qAUqyG9ckSg2pIjfvcQ7+pkVNUHk8yVLXOnCLe5WKXhLop8oorWFBJHpwWQpszQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-js-type": ["is-js-type@2.0.0", "", { "dependencies": { "js-types": "^1.0.0" } }, "sha512-Aj13l47+uyTjlQNHtXBV8Cji3jb037vxwMWCgopRR8h6xocgBGW3qG8qGlIOEmbXQtkKShKuBM9e8AA1OeQ+xw=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negated-glob": ["is-negated-glob@1.0.0", "", {}, "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@3.0.0", "", { "dependencies": { "kind-of": "^3.0.2" } }, "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-obj-prop": ["is-obj-prop@1.0.0", "", { "dependencies": { "lowercase-keys": "^1.0.0", "obj-props": "^1.0.0" } }, "sha512-5Idb61slRlJlsAzi0Wsfwbp+zZY+9LXKUAZpvT/1ySw+NxKLRWfa0Bzj+wXI3fX5O9hiddm5c3DAaRSNP/yl2w=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-proto-prop": ["is-proto-prop@2.0.0", "", { "dependencies": { "lowercase-keys": "^1.0.0", "proto-props": "^2.0.0" } }, "sha512-jl3NbQ/fGLv5Jhan4uX+Ge9ohnemqyblWVVCpAvtTQzNFvV2xhJq+esnkIbYQ9F1nITXoLfDDQLp7LBw/zzncg=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-relative": ["is-relative@1.0.0", "", { "dependencies": { "is-unc-path": "^1.0.0" } }, "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-unc-path": ["is-unc-path@1.0.0", "", { "dependencies": { "unc-path-regex": "^0.1.2" } }, "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ=="], + + "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "is-wayland": ["is-wayland@0.1.0", "", {}, "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-string-escape": ["js-string-escape@1.0.1", "", {}, "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-types": ["js-types@1.0.0", "", {}, "sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw=="], + + "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "line-column-path": ["line-column-path@3.0.0", "", { "dependencies": { "type-fest": "^2.0.0" } }, "sha512-Atocnm7Wr9nuvAn97yEPQa3pcQI5eLQGBz+m6iTb+CVw+IOzYB9MrYK7jI7BfC9ISnT4Fu0eiwhAScV//rp4Hw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + + "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash-es": ["lodash-es@4.17.22", "", {}, "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lowercase-keys": ["lowercase-keys@1.0.1", "", {}, "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="], + + "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="], + + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + + "matcher": ["matcher@5.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "md5-hex": ["md5-hex@3.0.1", "", { "dependencies": { "blueimp-md5": "^2.10.0" } }, "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw=="], + + "mem": ["mem@9.0.2", "", { "dependencies": { "map-age-cleaner": "^0.1.3", "mimic-fn": "^4.0.0" } }, "sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A=="], + + "memory-fs": ["memory-fs@0.2.0", "", {}, "sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng=="], + + "meow": ["meow@11.0.0", "", { "dependencies": { "@types/minimist": "^1.2.2", "camelcase-keys": "^8.0.2", "decamelize": "^6.0.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^4.0.1", "read-pkg-up": "^9.1.0", "redent": "^4.0.0", "trim-newlines": "^4.0.2", "type-fest": "^3.1.0", "yargs-parser": "^21.1.1" } }, "sha512-Cl0yeeIrko6d94KpUo1M+0X1sB14ikoaqlIGuTH1fW4I+E3+YljL54/hb/BWmVfrV9tTV9zU04+xjw08Fh2WkA=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micro-spelling-correcter": ["micro-spelling-correcter@1.1.1", "", {}, "sha512-lkJ3Rj/mtjlRcHk6YyCbvZhyWTOzdBvTHsxMmZSk5jxN1YyVSQ+JETAom55mdzfcyDrY/49Z7UCW760BK30crg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "nodemon": ["nodemon@3.1.11", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g=="], + + "nofilter": ["nofilter@3.1.0", "", {}, "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g=="], + + "normalize-package-data": ["normalize-package-data@4.0.1", "", { "dependencies": { "hosted-git-info": "^5.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "obj-props": ["obj-props@1.4.0", "", {}, "sha512-p7p/7ltzPDiBs6DqxOrIbtRdwxxVRBj5ROukeNb9RgA+fawhrz5n2hpNz8DDmYR//tviJSj7nUnlppGmONkjiQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + + "open-editor": ["open-editor@4.1.1", "", { "dependencies": { "env-editor": "^1.1.0", "execa": "^5.1.1", "line-column-path": "^3.0.0", "open": "^8.4.0" } }, "sha512-SYtGeZ9Zkzj/naoZaEF9LzwDYEGwuqQ4Fx5E3xdVRN98LFJjvMhG/ElByFEOVOiXepGra/Wi1fA4i/E1fXSBsw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-defer": ["p-defer@1.0.0", "", {}, "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw=="], + + "p-event": ["p-event@5.0.1", "", { "dependencies": { "p-timeout": "^5.0.2" } }, "sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "p-map": ["p-map@5.5.0", "", { "dependencies": { "aggregate-error": "^4.0.0" } }, "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg=="], + + "p-timeout": ["p-timeout@5.1.0", "", {}, "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="], + + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-conf": ["pkg-conf@4.0.0", "", { "dependencies": { "find-up": "^6.0.0", "load-json-file": "^7.0.0" } }, "sha512-7dmgi4UY4qk+4mj5Cd8v/GExPo0K+SlY+hulOSdfZ/T6jVH6//y7NtzZo5WrfhDBxuQ0jCa7fLZmNaNh7EWL/w=="], + + "pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "plur": ["plur@5.1.0", "", { "dependencies": { "irregular-plurals": "^3.3.0" } }, "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg=="], + + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], + + "pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "proto-props": ["proto-props@2.0.0", "", {}, "sha512-2yma2tog9VaRZY2mn3Wq51uiSW4NcPYT1cQdBagwyrznrilKSZwIZ0UG3ZPL/mx+axEns0hE35T5ufOYZXEnBQ=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-lru": ["quick-lru@6.1.2", "", {}, "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="], + + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + + "read-pkg": ["read-pkg@5.2.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", "parse-json": "^5.0.0", "type-fest": "^0.6.0" } }, "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg=="], + + "read-pkg-up": ["read-pkg-up@7.0.1", "", { "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" } }, "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "redent": ["redent@4.0.0", "", { "dependencies": { "indent-string": "^5.0.0", "strip-indent": "^4.0.0" } }, "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "regexpp": ["regexpp@3.2.0", "", {}, "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + + "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex": ["safe-regex@2.1.1", "", { "dependencies": { "regexp-tree": "~0.1.1" } }, "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supertap": ["supertap@3.0.1", "", { "dependencies": { "indent-string": "^5.0.0", "js-yaml": "^3.14.1", "serialize-error": "^7.0.1", "strip-ansi": "^7.0.1" } }, "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], + + "tapable": ["tapable@0.1.10", "", {}, "sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ=="], + + "temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="], + + "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], + + "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "time-zone": ["time-zone@1.0.0", "", {}, "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "to-absolute-glob": ["to-absolute-glob@2.0.2", "", { "dependencies": { "is-absolute": "^1.0.0", "is-negated-glob": "^1.0.0" } }, "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="], + + "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "trim-newlines": ["trim-newlines@4.1.1", "", {}, "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.2", "", {}, "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "unc-path-regex": ["unc-path-regex@0.1.2", "", {}, "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg=="], + + "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-or-path": ["url-or-path@2.6.1", "", {}, "sha512-fgFGHE43YhtMpv/1ZAdwKE9q2lFfEa9NfJ4yriBtjXbb1GAZxuVQVhsHXhHQjHd6W5A4FcMWQ25gxWmxVglDTw=="], + + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + + "watchpack": ["watchpack@2.5.0", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA=="], + + "webpack": ["webpack@5.104.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.4", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA=="], + + "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], + + "well-known-symbols": ["well-known-symbols@2.0.0", "", {}, "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], + + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + + "window-size": ["window-size@1.1.1", "", { "dependencies": { "define-property": "^1.0.0", "is-number": "^3.0.0" }, "bin": { "window-size": "cli.js" } }, "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="], + + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + + "xo": ["xo@0.53.1", "", { "dependencies": { "@eslint/eslintrc": "^1.3.3", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "arrify": "^3.0.0", "cosmiconfig": "^7.1.0", "define-lazy-prop": "^3.0.0", "eslint": "^8.27.0", "eslint-config-prettier": "^8.5.0", "eslint-config-xo": "^0.43.1", "eslint-config-xo-typescript": "^0.55.0", "eslint-formatter-pretty": "^4.1.0", "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-ava": "^13.2.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.5.1", "eslint-plugin-no-use-extend-native": "^0.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-unicorn": "^44.0.2", "esm-utils": "^4.1.0", "find-cache-dir": "^4.0.0", "find-up": "^6.3.0", "get-stdin": "^9.0.0", "globby": "^13.1.2", "imurmurhash": "^0.1.4", "json-stable-stringify-without-jsonify": "^1.0.1", "json5": "^2.2.1", "lodash-es": "^4.17.21", "meow": "^11.0.0", "micromatch": "^4.0.5", "open-editor": "^4.0.0", "prettier": "^2.7.1", "semver": "^7.3.8", "slash": "^5.0.0", "to-absolute-glob": "^2.0.2", "typescript": "^4.9.3" }, "bin": { "xo": "cli.js" } }, "sha512-/2R8SPehv1UhiIqJ9uSvrAjslcoygICNsUlEb/Zf2V6rMtr7YCoggc6hlt6b/kbncpR989Roqt6AvEO779dFxw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yocto-spinner": ["yocto-spinner@1.0.0", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-VPX8P/+Z2Fnpx8PC/JELbxp3QRrBxjAekio6yulGtA5gKt9YyRc5ycCb+NHgZCbZ0kx9KxwZp7gC6UlrCcCdSQ=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + + "@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "@inkjs/ui/figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], + + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "builtins/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "camelcase-keys/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "cfonts/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "concordance/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "decamelize-keys/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + + "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "eslint/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "eslint/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "eslint/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "eslint-formatter-pretty/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "eslint-formatter-pretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "eslint-formatter-pretty/plur": ["plur@4.0.0", "", { "dependencies": { "irregular-plurals": "^3.2.0" } }, "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg=="], + + "eslint-formatter-pretty/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-node/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "eslint-import-resolver-webpack/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-webpack/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-es/eslint-utils": ["eslint-utils@2.1.0", "", { "dependencies": { "eslint-visitor-keys": "^1.1.0" } }, "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg=="], + + "eslint-plugin-eslint-comments/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-n/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "eslint-plugin-n/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "eslint-plugin-unicorn/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "eslint-plugin-unicorn/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "eslint-plugin-unicorn/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], + + "execa/figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "execa/pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "execa/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], + + "globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "ink/cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + + "ink/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "ink-confirm-input/ink-text-input": ["ink-text-input@3.3.0", "", { "dependencies": { "chalk": "^3.0.0", "prop-types": "^15.5.10" }, "peerDependencies": { "ink": "^2.0.0", "react": "^16.5.2" } }, "sha512-gO4wrOf2ie3YuEARTIwGlw37lMjFn3Gk6CKIDrMlHb46WFMagZU7DplohjM24zynlqfnXA5UDEIfC2NBcvD8kg=="], + + "ink-select-input/figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "is-number/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "line-column-path/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "meow/read-pkg-up": ["read-pkg-up@9.1.0", "", { "dependencies": { "find-up": "^6.3.0", "read-pkg": "^7.1.0", "type-fest": "^2.5.0" } }, "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg=="], + + "meow/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + + "minimist-options/arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "minimist-options/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "nodemon/ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + + "nodemon/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "normalize-package-data/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "open-editor/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "open-editor/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "parent-module/callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "pkg-conf/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "read-pkg/normalize-package-data": ["normalize-package-data@2.5.0", "", { "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } }, "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA=="], + + "read-pkg/type-fest": ["type-fest@0.6.0", "", {}, "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg=="], + + "read-pkg-up/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="], + + "schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "simple-update-notifier/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "supports-hyperlinks/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "terser-webpack-plugin/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "to-regex-range/is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "tsup/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "webpack/enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "webpack/tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "xo/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "xo/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "xo/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "xo/@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "xo/@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "xo/@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "xo/@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "xo/@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "xo/@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "xo/@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "xo/@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "xo/@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + + "xo/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bundled": true }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + + "xo/@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bundled": true }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + + "xo/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "xo/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.62.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew=="], + + "xo/@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], + + "xo/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + + "xo/@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], + + "xo/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + + "xo/@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "xo/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "xo/acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "xo/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "xo/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "xo/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "xo/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "xo/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "xo/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "xo/callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "xo/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "xo/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "xo/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "xo/concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "xo/cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "xo/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "xo/deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "xo/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "xo/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "xo/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "xo/eslint-config-xo-typescript": ["eslint-config-xo-typescript@0.55.1", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": ">=5.43.0", "@typescript-eslint/parser": ">=5.43.0", "eslint": ">=8.0.0", "typescript": ">=4.4" }, "bundled": true }, "sha512-iXua+7n9fOp7LzGzvXlcZG0w6cdtscjASGTrAHMj0Rn9voayxF2oRoMIK1QS6ZJb4fMVEQZdU2D6gTKmWhcCQQ=="], + + "xo/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "xo/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "xo/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "xo/esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "xo/esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "xo/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "xo/esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "xo/fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "xo/fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "xo/fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "xo/fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "xo/file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "xo/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "xo/flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "xo/flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "xo/fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "xo/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "xo/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "xo/globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "xo/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "xo/graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "xo/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "xo/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "xo/import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "xo/imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "xo/inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "xo/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "xo/is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "xo/is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "xo/is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "xo/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "xo/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "xo/json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "xo/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "xo/json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "xo/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "xo/levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "xo/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "xo/lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "xo/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "xo/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "xo/natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "xo/natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="], + + "xo/once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "xo/optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "xo/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "xo/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "xo/parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "xo/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "xo/path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "xo/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "xo/prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "xo/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "xo/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "xo/queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "xo/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "xo/reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "xo/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "xo/run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "xo/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "xo/shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "xo/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "xo/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "xo/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "xo/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "xo/text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "xo/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "xo/tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + + "xo/type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "xo/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "xo/typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], + + "xo/uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "xo/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "xo/word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "xo/wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xo/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@eslint/eslintrc/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "@inkjs/ui/figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "cfonts/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint-formatter-pretty/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "eslint-formatter-pretty/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint-formatter-pretty/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "eslint-formatter-pretty/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "eslint-formatter-pretty/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "eslint-formatter-pretty/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "eslint-plugin-es/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@1.3.0", "", {}, "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ=="], + + "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "eslint/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "eslint/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "execa/figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "execa/pretty-ms/parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "ink-confirm-input/ink-text-input/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + + "ink-select-input/figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "jest-worker/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "meow/read-pkg-up/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], + + "meow/read-pkg-up/read-pkg": ["read-pkg@7.1.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.1", "normalize-package-data": "^3.0.2", "parse-json": "^5.2.0", "type-fest": "^2.0.0" } }, "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg=="], + + "meow/read-pkg-up/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], + + "open-editor/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "open-editor/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "open-editor/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "open-editor/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "open-editor/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "open-editor/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "open-editor/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "open-editor/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "pkg-conf/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "pkg-conf/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "read-pkg-up/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + + "read-pkg/normalize-package-data/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "read-pkg/normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "tsup/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "xo/eslint/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "xo/esquery/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "xo/esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "eslint-formatter-pretty/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "eslint-formatter-pretty/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "ink-confirm-input/ink-text-input/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ink-confirm-input/ink-text-input/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "meow/read-pkg-up/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "meow/read-pkg-up/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "meow/read-pkg-up/read-pkg/normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "pkg-conf/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "xo/eslint/eslint-scope/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "ink-confirm-input/ink-text-input/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "meow/read-pkg-up/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "meow/read-pkg-up/read-pkg/normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + + "meow/read-pkg-up/read-pkg/normalize-package-data/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "pkg-conf/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "meow/read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "meow/read-pkg-up/read-pkg/normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "pkg-conf/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "find-cache-dir/pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "meow/read-pkg-up/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + } +} diff --git a/atmn/dev.ts b/atmn/dev.ts new file mode 100644 index 00000000..f20488ab --- /dev/null +++ b/atmn/dev.ts @@ -0,0 +1,52 @@ +// Development watcher - rebuilds on file changes +import { watch } from "node:fs"; +import { spawn } from "node:child_process"; + +let building = false; + +async function rebuild() { + if (building) return; + building = true; + + console.log("\n🔄 Rebuilding..."); + const start = Date.now(); + + try { + await Bun.$`bun run bun.config.ts`; + const duration = Date.now() - start; + console.log(`✅ Build complete in ${duration}ms\n`); + } catch (error) { + console.error("❌ Build failed:", error); + } finally { + building = false; + } +} + +// Initial build +await rebuild(); + +// Watch src and source directories +const watcher1 = watch("./src", { recursive: true }, (event, filename) => { + if (filename?.match(/\.(ts|tsx)$/)) { + console.log(`📝 Changed: src/${filename}`); + rebuild(); + } +}); + +const watcher2 = watch("./source", { recursive: true }, (event, filename) => { + if (filename?.match(/\.(ts|tsx)$/)) { + console.log(`📝 Changed: source/${filename}`); + rebuild(); + } +}); + +console.log("👀 Watching for changes in src/ and source/..."); +console.log("Press Ctrl+C to stop\n"); + +// Keep process alive +process.on("SIGINT", () => { + console.log("\n👋 Stopping watcher..."); + watcher1.close(); + watcher2.close(); + process.exit(0); +}); diff --git a/atmn/package.json b/atmn/package.json index c592b8db..3c5090db 100644 --- a/atmn/package.json +++ b/atmn/package.json @@ -1,19 +1,24 @@ { "name": "atmn", - "version": "0.0.30", + "version": "1.0.0-beta.5", "license": "MIT", - "bin": "dist/cli.js", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "bin": { + "atmn": "dist/src/cli.js" + }, + "main": "dist/source/index.js", + "types": "dist/source/index.d.ts", "type": "module", "engines": { "node": ">=16" }, "scripts": { - "build": "tsup", - "dev": "tsup --watch", + "build": "bun run bun.config.ts", + "dev": "nodemon -e ts,tsx --watch src --watch source --ignore dist --exec \"bun run bun.config.ts\"", + "dev:bun": "bun run dev.ts", + "test": "bun test", + "test:watch": "bun test --watch", "test2": "prettier --check . && xo && ava", - "test": "node --trace-deprecation -- ./dist/cli.js" + "test-cli": "node --trace-deprecation -- ./dist/cli.js" }, "files": [ "dist", @@ -21,16 +26,32 @@ "README.md" ], "dependencies": { + "@inkjs/ui": "^2.0.0", "@inquirer/prompts": "^7.6.0", + "@mishieck/ink-titled-box": "^0.3.0", + "@tanstack/react-query": "^5.90.17", "@types/prettier": "^3.0.0", + "arctic": "^3.7.0", "axios": "^1.10.0", "chalk": "^5.2.0", + "clipboardy": "^5.0.2", "commander": "^14.0.0", "dotenv": "^17.2.0", + "ink": "^6.6.0", + "ink-big-text": "^2.0.0", + "ink-chart": "^0.1.1", + "ink-confirm-input": "^2.0.0", + "ink-scroll-list": "^0.4.1", + "ink-scroll-view": "^0.3.5", + "ink-select-input": "^6.2.0", + "ink-spinner": "^5.0.0", + "ink-table": "^3.1.0", + "ink-text-input": "^6.0.0", "inquirer": "^12.7.0", "jiti": "^2.4.2", "open": "^10.1.2", "prettier": "^3.6.2", + "react": "^19.2.3", "yocto-spinner": "^1.0.0", "zod": "^4.0.0" }, @@ -38,13 +59,16 @@ "@sindresorhus/tsconfig": "^3.0.1", "@types/bun": "^1.2.21", "@types/node": "^24.0.10", - "@types/react": "^18.0.32", + "@types/react": "^19.0.0", "@vdemedes/prettier-config": "^2.0.1", "ava": "^5.2.0", + "cli-testing-library": "^3.0.1", "eslint-config-xo-react": "^0.27.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "ink-testing-library": "^3.0.0", + "nodemon": "^3.1.11", + "react-devtools-core": "^6.1.2", "ts-node": "^10.9.1", "tsup": "^8.5.0", "typescript": "^5.0.3", diff --git a/atmn/readme.md b/atmn/readme.md index 8eb49cea..b4ad834a 100644 --- a/atmn/readme.md +++ b/atmn/readme.md @@ -3,9 +3,8 @@ The CLI tool for [Autumn](https://useautumn.com)'s REST API. ## Features -Features -- Create your features in products **in-code**. +- Create your features and plans **in-code**. - Authenticate with the CLI tool. - Easily push and pull changes to and from Autumn. @@ -49,53 +48,57 @@ production key, and comment out your sandbox key. ```typescript autumn.config.ts import { feature, - product, - priceItem, - featureItem, - pricedFeatureItem, -} from 'autumn-js/compose'; + plan, + planFeature, +} from 'atmn'; -const seats = feature({ +export const seats = feature({ id: 'seats', name: 'Seats', type: 'continuous_use', }); -const messages = feature({ +export const messages = feature({ id: 'messages', name: 'Messages', type: 'single_use', }); -const pro = product({ +export const pro = plan({ id: 'pro', name: 'Pro', - items: [ + description: 'Professional plan for growing teams', + add_on: false, + auto_enable: false, + price: { + amount: 50, + interval: 'month', + }, + features: [ // 500 messages per month - featureItem({ + planFeature({ feature_id: messages.id, - included_usage: 500, - interval: 'month', + granted: 500, + reset: { interval: 'month' }, }), // $10 per seat per month - pricedFeatureItem({ + planFeature({ feature_id: seats.id, - price: 10, - interval: 'month', - }), - - // $50 / month - priceItem({ - price: 50, - interval: 'month', + granted: 1, + price: { + amount: 10, + interval: 'month', + billing_method: 'pay_per_use', + billing_units: 1, + }, }), ], }); export default { features: [seats, messages], - products: [pro], + plans: [pro], }; ``` diff --git a/atmn/source/cli.ts b/atmn/source/cli.ts index 8e19dbb8..6b648033 100644 --- a/atmn/source/cli.ts +++ b/atmn/source/cli.ts @@ -2,14 +2,16 @@ import chalk from 'chalk'; import {program} from 'commander'; import open from 'open'; -import AuthCommand from './commands/auth.js'; +import AuthCommand from '../src/commands/auth/command.js'; +import {testTemplateCommand} from '../src/commands/test-template.js'; import Init from './commands/init.js'; import Nuke from './commands/nuke.js'; import Pull from './commands/pull.js'; import Push from './commands/push.js'; import {DEFAULT_CONFIG, FRONTEND_URL} from './constants.js'; import {loadAutumnConfigFile, writeConfig} from './core/config.js'; -import {isSandboxKey, readFromEnv} from './core/utils.js'; +import {getOrgMe} from './core/requests/orgRequests.js'; +import {readFromEnv} from './core/utils.js'; declare const VERSION: string; @@ -24,10 +26,18 @@ program.option('-l, --local', 'Use local autumn environment'); program .command('env') - .description('Check the environment of your API key') + .description('Check the environment and organization info') .action(async () => { - const env = await isSandboxKey(readFromEnv() ?? ''); - console.log(chalk.green(`Environment: ${env ? 'Sandbox' : 'Production'}`)); + // Ensure API key is present + readFromEnv(); + + // Fetch organization info from API + const orgInfo = await getOrgMe(); + + const envDisplay = orgInfo.env === 'sandbox' ? 'Sandbox' : 'Production'; + console.log(chalk.green(`Organization: ${orgInfo.name}`)); + console.log(chalk.green(`Slug: ${orgInfo.slug}`)); + console.log(chalk.green(`Environment: ${envDisplay}`)); }); program @@ -89,6 +99,13 @@ program console.log(computedVersion); }); +program + .command('test-template') + .description('Test template selector UI (prototype)') + .action(() => { + testTemplateCommand(); + }); + /** * This is a hack to silence the DeprecationWarning about url.parse() */ diff --git a/atmn/source/commands/auth.ts b/atmn/source/commands/auth.ts deleted file mode 100644 index a094f37d..00000000 --- a/atmn/source/commands/auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -import open from 'open'; -import chalk from 'chalk'; -import {input, password, confirm} from '@inquirer/prompts'; -import {storeToEnv, readFromEnv} from '../core/utils.js'; - -import {getOTP} from '../core/auth.js'; -import {updateCLIStripeKeys} from '../core/api.js'; -import {FRONTEND_URL} from '../constants.js'; - -const passwordTheme = { - style: { - answer: (text: string) => { - return chalk.magenta('*'.repeat(text.length)); - }, - }, -}; -const inputTheme = { - style: { - answer: (text: string) => { - return chalk.magenta(text); - }, - }, -}; - -export default async function AuthCommand() { - if (readFromEnv({bypass: true})) { - let shouldReauth = await confirm({ - message: - 'You are already authenticated. Would you like to re-authenticate?', - theme: inputTheme, - }); - if (!shouldReauth) return; - } - open(`${FRONTEND_URL}/dev/cli`); - - const otp = await input({ - message: 'Enter OTP:', - theme: inputTheme, - }); - - const keyInfo = await getOTP(otp); - - if (!keyInfo.stripe_connected) { - let connectStripe = await confirm({ - message: - "It seems like your organization doesn't have any Stripe keys connected. Would you like to connect your Stripe test secret key now?", - theme: inputTheme, - }); - if (connectStripe) { - // Ask for stripe Keys - let stripeTestKey = await password({ - message: 'Enter Stripe Test Secret Key:', - mask: '*', - theme: passwordTheme, - }); - await updateCLIStripeKeys({ - stripeSecretKey: stripeTestKey, - autumnSecretKey: keyInfo.sandboxKey, - }); - console.log( - chalk.green( - 'Stripe test secret key has been saved to your .env file. To connect your Stripe live secret key, please visit the Autumn dashboard here: https://app.useautumn.com/dev?tab=stripe', - ), - ); - } else { - console.log( - chalk.yellow( - "Okay, no worries. Go to the Autumn dashboard when you're ready!", - ), - ); - } - } - - await storeToEnv(keyInfo.prodKey, keyInfo.sandboxKey); - - console.log( - chalk.green( - 'Success! Sandbox and production keys have been saved to your .env file.\n`atmn` uses the AUTUMN_SECRET_KEY to authenticate with the Autumn API.', - ), - ); -} diff --git a/atmn/source/commands/init.ts b/atmn/source/commands/init.ts index c9a64581..aa2b92b7 100644 --- a/atmn/source/commands/init.ts +++ b/atmn/source/commands/init.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import { readFromEnv } from "../core/utils.js"; -import AuthCommand from "./auth.js"; +import AuthCommand from "../../src/commands/auth/command.js"; import Pull from "./pull.js"; export default async function Init() { diff --git a/atmn/source/commands/nuke.ts b/atmn/source/commands/nuke.ts index a1dbb7fa..0f684e51 100644 --- a/atmn/source/commands/nuke.ts +++ b/atmn/source/commands/nuke.ts @@ -1,56 +1,57 @@ -import fs from 'node:fs'; -import {confirm} from '@inquirer/prompts'; -import chalk from 'chalk'; -import {nukeCustomers, nukeFeatures, nukeProducts} from '../core/nuke.js'; -import {getAllProducts, getCustomers, getFeatures} from '../core/pull.js'; -import {initSpinner, isSandboxKey, readFromEnv} from '../core/utils.js'; -import {getOrg} from '../core/requests/orgRequests.js'; -import {Feature} from '../compose/models/composeModels.js'; +// @ts-nocheck +import fs from "node:fs"; +import { confirm } from "@inquirer/prompts"; +import chalk from "chalk"; +import type { Feature } from "../compose/models/featureModels.js"; +import { nukeCustomers, nukeFeatures, nukePlans } from "../core/nuke.js"; +import { getAllPlans, getCustomers, getFeatures } from "../core/pull.js"; +import { getOrg } from "../core/requests/orgRequests.js"; +import { initSpinner, isSandboxKey, readFromEnv } from "../core/utils.js"; async function promptAndConfirmNuke(orgName: string): Promise { - console.log('\n' + chalk.bgRed.white.bold(' DANGER: SANDBOX NUKE ')); + console.log("\n" + chalk.bgRed.white.bold(" DANGER: SANDBOX NUKE ")); console.log( chalk.red( `This is irreversible. You are about to permanently delete all data from the organization ` + - chalk.redBright.bold(orgName) + - `\n\n` + - `Items to be deleted:` + - `\n • ` + - chalk.yellowBright('customers') + - `\n • ` + - chalk.yellowBright('features') + - `\n • ` + - chalk.yellowBright('products') + - `\n`, + chalk.redBright.bold(orgName) + + `\n\n` + + `Items to be deleted:` + + `\n • ` + + chalk.yellowBright("customers") + + `\n • ` + + chalk.yellowBright("features") + + `\n • ` + + chalk.yellowBright("plans") + + `\n`, ), ); + const backupConfirm = await confirm({ + message: `Would you like to backup your ${chalk.magentaBright.bold("autumn.config.ts")} file before proceeding? (Recommended)`, + default: true, + }); + const shouldProceed = await confirm({ - message: `Confirm to continue. This will delete ${chalk.redBright.bold('all')} your ${chalk.redBright.bold('products')}, ${chalk.redBright.bold('features')} and ${chalk.redBright.bold('customers')} from your sandbox environment. You will confirm twice.`, + message: `Confirm to continue. This will delete ${chalk.redBright.bold("all")} your ${chalk.redBright.bold("plans")}, ${chalk.redBright.bold("features")} and ${chalk.redBright.bold("customers")} from your sandbox environment. You will confirm twice.`, default: false, }); if (!shouldProceed) { - console.log(chalk.red('Aborting...')); + console.log(chalk.red("Aborting...")); process.exit(1); } const finalConfirm = await confirm({ message: - 'Final confirmation: Are you absolutely sure? This action is irreversible.', + "Final confirmation: Are you absolutely sure? This action is irreversible.", default: false, }); if (!finalConfirm) { - console.log(chalk.red('Aborting...')); + console.log(chalk.red("Aborting...")); process.exit(1); } - const backupConfirm = await confirm({ - message: `Would you like to backup your ${chalk.magentaBright.bold('autumn.config.ts')} file before proceeding? (Recommended)`, - default: true, - }); - return backupConfirm; } @@ -63,25 +64,25 @@ export default async function Nuke() { const backupConfirm = await promptAndConfirmNuke(org.name); if (backupConfirm) { - fs.copyFileSync('autumn.config.ts', 'autumn.config.ts.backup'); - console.log(chalk.green('Backup created successfully!')); + fs.copyFileSync("autumn.config.ts", "autumn.config.ts.backup"); + console.log(chalk.green("Backup created successfully!")); } - console.log(chalk.red('Nuking sandbox...')); + console.log(chalk.red("Nuking sandbox...")); const s = initSpinner( - `Preparing ${chalk.yellowBright('customers')}, ${chalk.yellowBright('features')} and ${chalk.yellowBright('products')} for deletion...`, + `Preparing ${chalk.yellowBright("customers")}, ${chalk.yellowBright("features")} and ${chalk.yellowBright("plans")} for deletion...`, ); - const products = await getAllProducts({archived: true}); + const plans = await getAllPlans({ archived: true }); const features = await getFeatures(); const customers = await getCustomers(); s.success( - `Loaded all ${chalk.yellowBright('customers')}, ${chalk.yellowBright('features')} and ${chalk.yellowBright('products')} for deletion`, + `Loaded all ${chalk.yellowBright("customers")}, ${chalk.yellowBright("features")} and ${chalk.yellowBright("plans")} for deletion`, ); features.sort((a: Feature, b: Feature) => { - if (a.type === 'credit_system') { + if (a.type === "credit_system") { return -1; } return 1; @@ -89,15 +90,15 @@ export default async function Nuke() { try { await nukeCustomers(customers); - await nukeProducts(products.map((product: {id: string}) => product.id)); - await nukeFeatures(features.map((feature: {id: string}) => feature.id)); + await nukePlans(plans.map((plan: { id: string }) => plan.id)); + await nukeFeatures(features.map((feature: { id: string }) => feature.id)); } catch (e: unknown) { - console.error(chalk.red('Failed to nuke sandbox:')); + console.error(chalk.red("Failed to nuke sandbox:")); console.error(e); process.exit(1); } - console.log(chalk.green('Sandbox nuked successfully!')); + console.log(chalk.green("Sandbox nuked successfully!")); } else { console.log(chalk.red`You can't nuke a prod environment!`); process.exit(1); diff --git a/atmn/source/commands/pull.ts b/atmn/source/commands/pull.ts index 6810fedb..ae16ae90 100644 --- a/atmn/source/commands/pull.ts +++ b/atmn/source/commands/pull.ts @@ -1,20 +1,24 @@ -import chalk from 'chalk'; -import prettier from 'prettier'; -import {getAllProducts, getFeatures} from '../core/pull.js'; -import {productBuilder} from '../core/builders/productBuilder.js'; -import {featureBuilder} from '../core/builders/featureBuilder.js'; -import {writeConfig} from '../core/config.js'; -import {importBuilder, exportBuilder} from '../core/builders/productBuilder.js'; -import {snakeCaseToCamelCase} from '../core/utils.js'; -import {Feature, Product} from '../compose/models/composeModels.js'; - -export default async function Pull(options?: {archived?: boolean}) { - console.log(chalk.green('Pulling products and features from Autumn...')); - const products = await getAllProducts({archived: options?.archived ?? false}); - const features = await getFeatures({includeArchived: true}); - - const productSnippets = products.map((product: Product) => - productBuilder({product, features}), +// @ts-nocheck +import chalk from "chalk"; +import prettier from "prettier"; +import type { Feature } from "../compose/models/featureModels.js"; +import type { Plan } from "../compose/models/planModels.js"; +import { featureBuilder } from "../core/builders/featureBuilder.js"; +import { + importBuilder, + planBuilder +} from "../core/builders/planBuilder.js"; +import { writeConfig } from "../core/config.js"; +import { generateSDKTypes } from "../core/generateSDKTypes.js"; +import { getAllPlans, getFeatures } from "../core/pull.js"; + +export default async function Pull(options?: { archived?: boolean }) { + console.log(chalk.green("Pulling plans and features from Autumn...")); + const plans = await getAllPlans({ archived: options?.archived ?? false }); + const features = await getFeatures({ includeArchived: true }); + + const planSnippets = plans.map((plan: Plan) => + planBuilder({ plan, features }), ); const featureSnippets = features @@ -24,18 +28,184 @@ export default async function Pull(options?: {archived?: boolean}) { const autumnConfig = ` ${importBuilder()} -// Features${featureSnippets.join('\n')} +// Features${featureSnippets.join("\n")} -// Products${productSnippets.join('\n')} +// Plans${planSnippets.join("\n")} `; const formattedConfig = await prettier.format(autumnConfig, { - parser: 'typescript', + parser: "typescript", useTabs: true, singleQuote: false, }); writeConfig(formattedConfig); - console.log(chalk.green('Success! Config has been updated.')); + // Fetch products and features from both sandbox and production for comprehensive SDK types + console.log( + chalk.dim( + "Fetching products and features from all environments for SDK types...", + ), + ); + const allEnvironmentFeatures = await fetchFeaturesFromAllEnvironments(); + const allEnvironmentPlans = await fetchPlansFromAllEnvironments(); + + // Generate SDK type narrowing for autocomplete + const sdkTypesPath = generateSDKTypes({ + plans: allEnvironmentPlans, // Use combined plans from both environments + features: allEnvironmentFeatures, // Use combined features from both environments + outputDir: process.cwd(), + }); + + console.log(chalk.green("Success! Config has been updated.")); + console.log(chalk.dim(`Generated SDK types at: ${sdkTypesPath}`)); +} + +/** + * Fetch features from both sandbox and production environments + * This ensures SDK autocomplete includes all possible feature IDs + */ +async function fetchFeaturesFromAllEnvironments(): Promise { + const { readFromEnv, isProdFlag } = await import("../core/utils.js"); + const { getFeatures } = await import("../core/pull.js"); + + const currentEnvIsProd = isProdFlag(); + const allFeatures: Feature[] = []; + const seenIds = new Set(); + + try { + // Fetch from current environment + const currentFeatures = await getFeatures({ includeArchived: true }); + currentFeatures.forEach((f: Feature) => { + if (!seenIds.has(f.id)) { + allFeatures.push(f); + seenIds.add(f.id); + } + }); + + // Try to fetch from other environment if keys exist + const { readFromEnv: readEnvDirect } = await import("fs"); + const envPath = `${process.cwd()}/.env`; + const fs = await import("fs"); + + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, "utf-8"); + const otherKeyName = currentEnvIsProd + ? "AUTUMN_SECRET_KEY" + : "AUTUMN_PROD_SECRET_KEY"; + const otherKeyMatch = envContent.match( + new RegExp(`${otherKeyName}=(.+)`), + ); + + if (otherKeyMatch && otherKeyMatch[1]) { + // Temporarily switch environment + const originalArgs = [...process.argv]; + if (currentEnvIsProd) { + process.argv = process.argv.filter( + (a) => a !== "--prod" && a !== "-p", + ); + } else { + process.argv.push("--prod"); + } + + try { + const otherFeatures = await getFeatures({ includeArchived: true }); + otherFeatures.forEach((f: Feature) => { + if (!seenIds.has(f.id)) { + allFeatures.push(f); + seenIds.add(f.id); + } + }); + } catch (error) { + // Silently fail if other environment is not accessible + console.log( + chalk.dim("Could not fetch from other environment (this is okay)"), + ); + } + + // Restore original args + process.argv = originalArgs; + } + } + } catch (error) { + // Fall back to current environment only + console.log(chalk.dim("Using features from current environment only")); + } + + return allFeatures; +} + +/** + * Fetch plans from both sandbox and production environments + * This ensures SDK autocomplete includes all possible product IDs + */ +async function fetchPlansFromAllEnvironments(): Promise { + const { isProdFlag } = await import("../core/utils.js"); + const { getAllPlans } = await import("../core/pull.js"); + + const currentEnvIsProd = isProdFlag(); + const allPlans: Plan[] = []; + const seenIds = new Set(); + + try { + // Fetch from current environment + const currentPlans = await getAllPlans({ archived: true }); + currentPlans.forEach((plan: Plan) => { + if (!seenIds.has(plan.id)) { + allPlans.push(plan); + seenIds.add(plan.id); + } + }); + + // Try to fetch from other environment if keys exist + const envPath = `${process.cwd()}/.env`; + const fs = await import("fs"); + + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, "utf-8"); + const otherKeyName = currentEnvIsProd + ? "AUTUMN_SECRET_KEY" + : "AUTUMN_PROD_SECRET_KEY"; + const otherKeyMatch = envContent.match( + new RegExp(`${otherKeyName}=(.+)`), + ); + + if (otherKeyMatch && otherKeyMatch[1]) { + // Temporarily switch environment + const originalArgs = [...process.argv]; + if (currentEnvIsProd) { + process.argv = process.argv.filter( + (a) => a !== "--prod" && a !== "-p", + ); + } else { + process.argv.push("--prod"); + } + + try { + const otherPlans = await getAllPlans({ archived: true }); + otherPlans.forEach((plan: Plan) => { + if (!seenIds.has(plan.id)) { + allPlans.push(plan); + seenIds.add(plan.id); + } + }); + } catch (error) { + // Silently fail if other environment is not accessible + console.log( + chalk.dim( + "Could not fetch products from other environment (this is okay)", + ), + ); + } + + // Restore original args + process.argv = originalArgs; + } + } + } catch (error) { + // Fall back to current environment only + console.log(chalk.dim("Using products from current environment only")); + } + + return allPlans; } diff --git a/atmn/source/commands/push.ts b/atmn/source/commands/push.ts index 3f04b165..3c12fe34 100644 --- a/atmn/source/commands/push.ts +++ b/atmn/source/commands/push.ts @@ -1,23 +1,23 @@ +// @ts-nocheck import { confirm } from "@inquirer/prompts"; import chalk from "chalk"; import yoctoSpinner from "yocto-spinner"; -import type { Feature, Product } from "../compose/index.js"; +import type { Feature, Plan } from "../compose/index.js"; import { FRONTEND_URL } from "../constants.js"; -import { deleteFeature, deleteProduct } from "../core/api.js"; -import { getProducts } from "../core/pull.js"; +import { deleteFeature, deletePlan } from "../core/api.js"; import { checkForDeletables, - checkProductForConfirmation, + checkPlanForConfirmation, upsertFeature, - upsertProduct, + upsertPlan, } from "../core/push.js"; import { checkFeatureDeletionData, updateFeature, } from "../core/requests/featureRequests.js"; import { - getProductDeleteInfo, - updateProduct, + getPlanDeleteInfo, + updatePlan, } from "../core/requests/prodRequests.js"; import { initSpinner } from "../core/utils.js"; @@ -30,33 +30,30 @@ const createSpinner = ({ message }: { message?: string }) => { return spinner; }; -const gatherProductDeletionDecisions = async ({ - productsToDelete, +const gatherPlanDeletionDecisions = async ({ + plansToDelete, yes, }: { - productsToDelete: string[]; + plansToDelete: string[]; yes: boolean; }) => { - const productDeletionDecisions = new Map< + const planDeletionDecisions = new Map< string, "delete" | "archive" | "skip" >(); - const batchCheckProducts = []; + const batchCheckPlans = []; - for (const productId of productsToDelete) { - batchCheckProducts.push(getProductDeleteInfo({ productId })); + for (const planId of plansToDelete) { + batchCheckPlans.push(getPlanDeleteInfo({ planId })); } - const checkProductResults = await Promise.all(batchCheckProducts); + const checkPlanResults = await Promise.all(batchCheckPlans); - for (let i = 0; i < productsToDelete.length; i++) { - const productId = productsToDelete[i]; - const result = checkProductResults[i]; + for (let i = 0; i < plansToDelete.length; i++) { + const planId = plansToDelete[i]; + const result = checkPlanResults[i]; - if (!productId) continue; - const product = (await getProducts([productId])).find( - (x) => x.id === productId, - ); + if (!planId) continue; if (result && result.totalCount > 0) { const otherCustomersText = @@ -64,58 +61,51 @@ const gatherProductDeletionDecisions = async ({ ? ` and ${result.totalCount - 1} other customer(s)` : ""; const customerNameText = result.customerName || "Unknown Customer"; - if (product?.archived) { - productDeletionDecisions.set(productId, "skip"); - } else { - const shouldArchive = - yes || - (await confirm({ - message: `Product ${productId} has customer ${customerNameText}${otherCustomersText}. As such, you cannot delete it. Would you like to archive the product instead?`, - })); - productDeletionDecisions.set( - productId, - shouldArchive ? "archive" : "skip", - ); - } + const shouldArchive = + yes || + (await confirm({ + message: `Plan ${planId} has customer ${customerNameText}${otherCustomersText}. As such, you cannot delete it. Would you like to archive the plan instead?`, + })); + planDeletionDecisions.set(planId, shouldArchive ? "archive" : "skip"); } else { - productDeletionDecisions.set(productId, "delete"); + planDeletionDecisions.set(planId, "delete"); } } - return productDeletionDecisions; + return planDeletionDecisions; }; -const handleProductDeletion = async ({ - productsToDelete, +const handlePlanDeletion = async ({ + plansToDelete, yes, }: { - productsToDelete: string[]; + plansToDelete: string[]; yes: boolean; }) => { - const productDeletionDecisions = await gatherProductDeletionDecisions({ - productsToDelete, + const planDeletionDecisions = await gatherPlanDeletionDecisions({ + plansToDelete, yes, }); - for (const productId of productsToDelete) { - const decision = productDeletionDecisions.get(productId); + for (const planId of plansToDelete) { + const decision = planDeletionDecisions.get(planId); if (decision === "delete") { const shouldDelete = yes || (await confirm({ - message: `Delete product [${productId}]?`, + message: `Delete plan [${planId}]?`, })); if (shouldDelete) { - const s = createSpinner({ message: `Deleting product [${productId}]` }); - await deleteProduct({ id: productId }); - s.success(`Product [${productId}] deleted successfully!`); + const s = createSpinner({ message: `Deleting plan [${planId}]` }); + await deletePlan({ id: planId }); + s.success(`Plan [${planId}] deleted successfully!`); } } else if (decision === "archive") { - const s = createSpinner({ message: `Archiving product [${productId}]` }); - await updateProduct({ productId, update: { archived: true } }); - s.success(`Product [${productId}] archived successfully!`); + const s = createSpinner({ message: `Archiving plan [${planId}]` }); + await updatePlan({ planId, update: { archived: true } }); + s.success(`Plan [${planId}] archived successfully!`); } } }; @@ -168,48 +158,48 @@ const pushFeatures = async ({ console.log(); // Empty line for spacing }; -const gatherProductDecisions = async ({ - products, - curProducts, +const gatherPlanDecisions = async ({ + plans, + curPlans, yes, }: { - products: Product[]; - curProducts: any[]; + plans: Plan[]; + curPlans: any[]; yes: boolean; }) => { - const productDecisions = new Map(); - const batchCheckProducts = []; - - for (const product of products) { - batchCheckProducts.push( - checkProductForConfirmation({ - curProducts, - product, + const planDecisions = new Map(); + const batchCheckPlans = []; + + for (const plan of plans) { + batchCheckPlans.push( + checkPlanForConfirmation({ + curPlans, + plan, }), ); } - const checkProductResults = await Promise.all(batchCheckProducts); + const checkPlanResults = await Promise.all(batchCheckPlans); - for (const result of checkProductResults) { + for (const result of checkPlanResults) { if (result.archived) { const shouldUnarchive = yes || (await confirm({ - message: `Product ${result.id} is currently archived. Would you like to un-archive it before pushing?`, + message: `Plan ${result.id} is currently archived. Would you like to un-archive it before pushing?`, })); if (shouldUnarchive) { const s = createSpinner({ - message: `Un-archiving product [${result.id}]`, + message: `Un-archiving plan [${result.id}]`, }); - await updateProduct({ - productId: result.id, + await updatePlan({ + planId: result.id, update: { archived: false }, }); - s.success(`Product [${result.id}] un-archived successfully!`); - productDecisions.set(result.id, true); + s.success(`Plan [${result.id}] un-archived successfully!`); + planDecisions.set(result.id, true); } else { - productDecisions.set(result.id, false); + planDecisions.set(result.id, false); } } @@ -217,42 +207,40 @@ const gatherProductDecisions = async ({ const shouldUpdate = yes || (await confirm({ - message: `Product ${result.id} has customers on it and updating it will create a new version.\nAre you sure you'd like to continue? `, + message: `Plan ${result.id} has customers on it and updating it will create a new version.\nAre you sure you'd like to continue? `, })); - productDecisions.set(result.id, shouldUpdate); + planDecisions.set(result.id, shouldUpdate); } else { - productDecisions.set(result.id, true); + planDecisions.set(result.id, true); } } - return productDecisions; + return planDecisions; }; -const pushProducts = async ({ - products, - curProducts, - productDecisions, +const pushPlans = async ({ + plans, + curPlans, + planDecisions, yes, }: { - products: Product[]; - curProducts: any[]; - productDecisions: Map; + plans: Plan[]; + curPlans: any[]; + planDecisions: Map; yes: boolean; }) => { - const s2 = initSpinner(`Pushing products`); - const batchProducts = []; + const s2 = initSpinner(`Pushing plans`); + const batchPlans = []; - for (const product of products) { - const shouldUpdate = productDecisions.get(product.id); - batchProducts.push( - upsertProduct({ curProducts, product, spinner: s2, shouldUpdate }), - ); + for (const plan of plans) { + const shouldUpdate = planDecisions.get(plan.id); + batchPlans.push(upsertPlan({ curPlans, plan, spinner: s2, shouldUpdate })); } - const prodResults = await Promise.all(batchProducts); - s2.success(`Products pushed successfully!`); - console.log(chalk.dim("\nProducts pushed:")); - prodResults.forEach((result: { id: string; action: string }) => { + const planResults = await Promise.all(batchPlans); + s2.success(`Plans pushed successfully!`); + console.log(chalk.dim("\nPlans pushed:")); + planResults.forEach((result: { id: string; action: string }) => { const action = result.action; console.log( chalk.cyan( @@ -262,7 +250,7 @@ const pushProducts = async ({ }); console.log(); // Empty line for spacing - return prodResults; + return planResults; }; const gatherFeatureDeletionDecisions = async ({ @@ -299,9 +287,9 @@ const gatherFeatureDeletionDecisions = async ({ f.credit_schema?.some((cs) => cs.metered_feature_id === featureId), ); - if (referencingCreditSystems.length > 0) { + if (referencingCreditSystems.length >= 1) { // Feature is referenced by credit system(s) in the current config - must archive - const firstCreditSystem = referencingCreditSystems[0].id; + const firstCreditSystem = referencingCreditSystems[0]?.id; const creditSystemText = referencingCreditSystems.length === 1 ? `the "${firstCreditSystem}" credit system` @@ -411,14 +399,12 @@ const showSuccessMessage = ({ env, prod }: { env: string; prod: boolean }) => { if (prod) { console.log( - chalk.magentaBright( - `You can view the products at ${FRONTEND_URL}/products`, - ), + chalk.magentaBright(`You can view the plans at ${FRONTEND_URL}/products`), ); } else { console.log( chalk.magentaBright( - `You can view the products at ${FRONTEND_URL}/sandbox/products`, + `You can view the plans at ${FRONTEND_URL}/sandbox/products`, ), ); } @@ -431,13 +417,13 @@ export default async function Push({ }: { config: { features: Feature[]; - products: Product[]; + plans: Plan[]; env: string; }; yes: boolean; prod: boolean; }) { - const { features, products, env } = config; + const { features, plans, env } = config; if (env === "prod") { const shouldProceed = @@ -453,18 +439,18 @@ export default async function Push({ } } - const { allFeatures, curProducts, featuresToDelete, productsToDelete } = - await checkForDeletables(features, products); + const { allFeatures, curPlans, featuresToDelete, plansToDelete } = + await checkForDeletables(features, plans); - await handleProductDeletion({ productsToDelete, yes }); + await handlePlanDeletion({ plansToDelete, yes }); await pushFeatures({ features, allFeatures, yes }); - const productDecisions = await gatherProductDecisions({ - products, - curProducts, + const planDecisions = await gatherPlanDecisions({ + plans, + curPlans, yes, }); - await pushProducts({ products, curProducts, productDecisions, yes }); + await pushPlans({ plans, curPlans, planDecisions, yes }); await handleFeatureDeletion({ featuresToDelete, allFeatures, currentFeatures: features, yes }); showSuccessMessage({ env, prod }); diff --git a/atmn/source/compose/builders/builderFunctions.ts b/atmn/source/compose/builders/builderFunctions.ts index 6d6d1ec8..a3928cd1 100644 --- a/atmn/source/compose/builders/builderFunctions.ts +++ b/atmn/source/compose/builders/builderFunctions.ts @@ -1,79 +1,101 @@ -import type { Feature, Product } from "../models/composeModels.js"; +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared schemas +// Run `pnpm gen:atmn` to regenerate -import type { - ProductItem, - ProductItemInterval, - UsageModel, -} from "../models/productItemModels.js"; +import type { Plan, PlanFeature, FreeTrial } from "../models/planModels.js"; +import type { Feature } from "../models/featureModels.js"; -export const product = (p: Product) => p; -export const feature = (f: Feature) => f; +type PlanInput = Omit & Partial>; -export const featureItem = ({ - feature_id, - included_usage, - interval, - reset_usage_when_enabled, - entity_feature_id, -}: { - feature_id: string; - included_usage?: number | 'inf'; - interval?: ProductItemInterval; - reset_usage_when_enabled?: boolean; - entity_feature_id?: string; -}): ProductItem => { - return { - included_usage, - feature_id, - interval, - reset_usage_when_enabled, - entity_feature_id, - }; +/** + * Define a pricing plan in your Autumn configuration + * + * @param p - Plan configuration + * @returns Plan object for use in autumn.config.ts + * + * @example + * export const pro = plan({ + * id: 'pro', + * name: 'Pro Plan', + * description: 'For growing teams', + * features: [ + * planFeature({ feature_id: seats.id, included: 10 }), + * planFeature({ + * feature_id: messages.id, + * included: 1000, + * reset: { interval: 'month' } + * }) + * ], + * price: { amount: 50, interval: 'month' } + * }); + */ +export const plan = (params: PlanInput): Plan => { + return { + ...params, + description: params.description ?? null, + add_on: params.add_on ?? false, + auto_enable: params.auto_enable ?? false, + group: params.group ?? "" + }; }; -export const pricedFeatureItem = ({ - feature_id, - price, - tiers, - interval, - included_usage = undefined, - billing_units = 1, - usage_model = "pay_per_use", - reset_usage_when_enabled, - entity_feature_id, -}: { - feature_id: string; - price?: number; - tiers?: {to: number | 'inf'; amount: number}[]; - interval?: ProductItemInterval; - included_usage?: number; - billing_units?: number; - usage_model?: UsageModel; - reset_usage_when_enabled?: boolean; - entity_feature_id?: string; -}): ProductItem => { - return { - price, - tiers, - interval, - billing_units, - feature_id, - usage_model, - included_usage, - reset_usage_when_enabled, - entity_feature_id, - }; +/** + * Define a feature that can be included in plans + * + * @param f - Feature configuration + * @returns Feature object for use in autumn.config.ts + * + * @example + * // Metered consumable feature (like API calls, tokens) + * export const apiCalls = feature({ + * id: 'api_calls', + * name: 'API Calls', + * type: 'metered', + * consumable: true + * }); + * + * @example + * // Metered non-consumable feature (like seats) + * export const seats = feature({ + * id: 'seats', + * name: 'Team Seats', + * type: 'metered', + * consumable: false + * }); + */ +export const feature = (params: Feature): Feature => { + return params; }; -export const priceItem = ({ - price, - interval, -}: { - price: number; - interval?: ProductItemInterval; -}): ProductItem => { - return { - price, - interval, - }; +/** + * Include a feature in a plan with specific configuration + * + * @param config - Feature configuration for this plan + * @returns PlanFeature for use in plan's features array + * + * @example + * // Simple included usage + * planFeature({ + * feature_id: messages.id, + * included: 1000, + * reset: { interval: 'month' } + * }) + * + * @example + * // Priced feature with tiers + * planFeature({ + * feature_id: seats.id, + * included: 5, + * price: { + * tiers: [ + * { to: 10, amount: 10 }, + * { to: 'inf', amount: 8 } + * ], + * interval: 'month', + * billing_method: 'pay_per_use' + * } + * }) + */ +export const planFeature = (params: PlanFeature): PlanFeature => { + return params; }; diff --git a/atmn/source/compose/index.ts b/atmn/source/compose/index.ts index 01da3097..36a4ab7a 100644 --- a/atmn/source/compose/index.ts +++ b/atmn/source/compose/index.ts @@ -1,21 +1,29 @@ import { feature, - featureItem, - pricedFeatureItem, - priceItem, - product, + plan, + planFeature, } from "./builders/builderFunctions.js"; -import type { Feature, Product } from "./models/composeModels.js"; -import type { ProductItem } from "./models/productItemModels.js"; +import type { Feature } from "./models/featureModels.js"; +import type { + Plan, + PlanFeature, + FreeTrial, +} from "./models/planModels.js"; -export { product, priceItem, feature, featureItem, pricedFeatureItem }; +export { plan, feature, planFeature }; + +export type { + Feature, + Plan, + PlanFeature, + FreeTrial, +}; -export type { Feature, Product, ProductItem }; export type Infinity = "infinity"; -// CLi types +// CLI types export type AutumnConfig = { - products: Product[]; + plans: Plan[]; features: Feature[]; }; diff --git a/atmn/source/compose/models/composeModels.ts b/atmn/source/compose/models/composeModels.ts deleted file mode 100644 index b72939dd..00000000 --- a/atmn/source/compose/models/composeModels.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { z } from "zod/v4"; -import { ProductItemSchema } from "./productItemModels.js"; - -export const FreeTrialSchema = z.object({ - duration: z.enum(["day", "month", "year"], { - message: "Duration must be 'day', 'month', or 'year'", - }), - length: z.number({ - message: "Length must be a valid number", - }), - unique_fingerprint: z.boolean({ - message: "Unique fingerprint must be true or false", - }), - card_required: z.boolean({ - message: "Card required must be true or false", - }), -}); - -export const ProductSchema = z.object({ - id: z.string().min(1, "Product ID is required and cannot be empty"), - name: z.string().min(1, "Product name is required and cannot be empty"), - group: z.string().optional(), - is_add_on: z.boolean().prefault(false).optional(), - is_default: z.boolean().prefault(false).optional(), - items: z.array(ProductItemSchema, { - message: "Items must be an array of product items", - }), - free_trial: FreeTrialSchema.optional(), - archived: z.boolean().optional(), -}); - -export const FeatureSchema = z.object({ - id: z.string().min(1, "Feature ID is required and cannot be empty"), - name: z.string().optional(), - type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"], { - message: - "Type must be 'boolean', 'single_use', 'continuous_use', or 'credit_system'", - }), - credit_schema: z - .array( - z.object({ - metered_feature_id: z.string({ - message: "Metered feature ID must be a string", - }), - credit_cost: z.number({ - message: "Credit cost must be a valid number", - }), - }), - ) - .optional(), - archived: z.boolean().optional(), -}); - -export type Feature = z.infer; -export type Product = z.infer; -export type FreeTrial = z.infer; diff --git a/atmn/source/compose/models/featureModels.ts b/atmn/source/compose/models/featureModels.ts new file mode 100644 index 00000000..b7555e32 --- /dev/null +++ b/atmn/source/compose/models/featureModels.ts @@ -0,0 +1,71 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared schemas +// Run `pnpm gen:atmn` to regenerate + +import { z } from "zod/v4"; + + +export const FeatureSchema = z.object({ + id: z.string(), + name: z.string(), + event_names: z.array(z.string()).optional(), + credit_schema: z + .array( + z.object({ + metered_feature_id: z.string(), + credit_cost: z.number(), + }), + ) + .optional() +}); + + + +// Base fields shared by all feature types +type FeatureBase = { + /** Unique identifier for the feature */ + id: string; + /** Display name for the feature */ + name: string; + /** Event names that trigger this feature */ + event_names?: string[]; + /** Credit schema for credit_system features */ + credit_schema?: Array<{ + metered_feature_id: string; + credit_cost: number; + }>; +}; + +/** Boolean feature - no consumable field allowed */ +export type BooleanFeature = FeatureBase & { + type: "boolean"; + consumable?: never; +}; + +/** Metered feature - requires consumable field */ +export type MeteredFeature = FeatureBase & { + type: "metered"; + /** Whether usage is consumed (true) or accumulated (false) */ + consumable: boolean; +}; + +/** Credit system feature - always consumable */ +export type CreditSystemFeature = FeatureBase & { + type: "credit_system"; + /** Credit systems are always consumable */ + consumable?: true; + /** Required: defines how credits map to metered features */ + credit_schema: Array<{ + metered_feature_id: string; + credit_cost: number; + }>; +}; + +/** + * Feature definition with type-safe constraints: + * - Boolean features cannot have consumable + * - Metered features require consumable (true = single_use style, false = continuous_use style) + * - Credit system features are always consumable and require credit_schema + */ +export type Feature = BooleanFeature | MeteredFeature | CreditSystemFeature; + diff --git a/atmn/source/compose/models/index.ts b/atmn/source/compose/models/index.ts new file mode 100644 index 00000000..f3289775 --- /dev/null +++ b/atmn/source/compose/models/index.ts @@ -0,0 +1,6 @@ +// Auto-generated exports for all camelCase types +// This file is generated by typegen pipeline +// DO NOT EDIT MANUALLY - changes will be overwritten + +export * from './planModels.js'; +export * from './featureModels.js'; diff --git a/atmn/source/compose/models/planModels.ts b/atmn/source/compose/models/planModels.ts new file mode 100644 index 00000000..d558167d --- /dev/null +++ b/atmn/source/compose/models/planModels.ts @@ -0,0 +1,188 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared schemas +// Run `pnpm gen:atmn` to regenerate + +import { z } from "zod/v4"; + +export const UsageTierSchema = z.object({ + to: z.union([z.number(), z.literal("inf")]), + amount: z.number(), +}); + +const idRegex = /^[a-zA-Z0-9_-]+$/; + + +export const PlanFeatureSchema = z.object({ + feature_id: z.string(), + granted_balance: z.number().optional(), + unlimited: z.boolean().optional(), + reset: z + .object({ + interval: z.union([z.literal("one_off"), z.literal("minute"), z.literal("hour"), z.literal("day"), z.literal("week"), z.literal("month"), z.literal("quarter"), z.literal("year")]), + interval_count: z.number().optional(), + reset_when_enabled: z.boolean().optional(), + }) + .optional(), + price: z + .object({ + amount: z.number().optional(), + tiers: z.array(UsageTierSchema).optional(), + + interval: z.union([z.literal("month"), z.literal("quarter"), z.literal("semi_annual"), z.literal("year")]), + interval_count: z.number().default(1).optional(), + + billing_units: z.number().default(1).optional(), + usage_model: z.union([z.literal("prepaid"), z.literal("pay_per_use")]), + max_purchase: z.number().optional(), + }) + .optional(), + proration: z + .object({ + on_increase: z.union([z.literal("prorate"), z.literal("charge_immediately")]), + on_decrease: z.union([z.literal("prorate"), z.literal("refund_immediately"), z.literal("no_action")]), + }) + .optional(), + rollover: z + .object({ + max: z.number(), + expiry_duration_type: z.union([z.literal("one_off"), z.literal("minute"), z.literal("hour"), z.literal("day"), z.literal("week"), z.literal("month"), z.literal("quarter"), z.literal("year")]), + expiry_duration_length: z.number().optional(), + }) + .optional() +}); + +export const FreeTrialSchema = z.object({ + duration_type: z.union([z.literal("day"), z.literal("month"), z.literal("year")]), + duration_length: z.number(), + card_required: z.boolean() +}); + +export const PlanSchema = z.object({ + description: z.string().nullable().default(null), + add_on: z.boolean().default(false), + default: z.boolean().default(false), + price: z + .object({ + amount: z.number(), + interval: z.union([z.literal("month"), z.literal("quarter"), z.literal("semi_annual"), z.literal("year")]), + }) + .optional(), + features: z.array(PlanFeatureSchema).optional(), + free_trial: FreeTrialSchema.nullable().optional(), + /** Unique identifier for the plan */ + id: z.string().nonempty().regex(idRegex), + /** Display name for the plan */ + name: z.string().nonempty(), + /** Group for organizing plans */ + group: z.string().default("") +}); + + +// Type aliases for literal unions +export type ResetInterval = "one_off" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year"; +export type BillingInterval = "month" | "quarter" | "semi_annual" | "year"; +export type BillingMethod = "prepaid" | "pay_per_use"; +export type OnIncrease = "prorate" | "charge_immediately"; +export type OnDecrease = "prorate" | "refund_immediately" | "no_action"; + +// Base type for PlanFeature +type PlanFeatureBase = z.infer; + +/** + * Plan feature configuration with flattened reset fields. Use interval/interval_count at top level. + */ +export type PlanFeature = { + /** Reference to the feature being configured */ + feature_id: string; + + /** Amount of usage included in this plan */ + included?: number; + + /** Whether usage is unlimited */ + unlimited?: boolean; + + /** How often usage resets (e.g., 'month', 'day') */ + interval?: ResetInterval; + + /** Number of intervals between resets (default: 1) */ + interval_count?: number; + + /** Whether to carry over existing usage when feature is enabled (default: true) */ + carry_over_usage?: boolean; + + /** Pricing configuration for usage-based billing */ + price?: { + /** Flat price per unit in cents */ + amount?: number; + + /** Tiered pricing structure based on usage ranges */ + tiers?: Array<{ to: number | "inf"; amount: number }>; + + /** Number of units per billing cycle */ + billing_units: number; + + /** Billing method: 'prepaid' or 'pay_per_use' */ + billing_method: BillingMethod; + + /** Maximum purchasable quantity */ + max_purchase?: number; } + + /** Proration rules for quantity changes */ + proration?: { + /** Behavior when quantity increases */ + on_increase: OnIncrease; + + /** Behavior when quantity decreases */ + on_decrease: OnDecrease; } + + /** Rollover policy for unused usage */ + rollover?: { + /** Maximum amount that can roll over (null for unlimited) */ + max: number | null; + + /** How long rollover lasts before expiring */ + expiry_duration_type: ResetInterval; + + /** Duration length for rollover expiry */ + expiry_duration_length?: number; } +}; + + +// Override Plan type to use PlanFeature discriminated union +type PlanBase = z.infer; +export type FreeTrial = z.infer; + +export type Plan = { + /** Unique identifier for the plan */ + id: string; + + /** Display name for the plan */ + name: string; + + /** Optional description explaining what this plan offers */ + description?: string | null; + + /** Grouping identifier for organizing related plans */ + group?: string; + + /** Whether this plan can be purchased alongside other plans */ + add_on?: boolean; + + /** Whether to automatically enable this plan for new customers */ + auto_enable?: boolean; + + /** Base price for the plan */ + price?: { + /** Price in your currency (e.g., 50 for $50.00) */ + amount: number; + + /** Billing frequency */ + interval: BillingInterval | ResetInterval; } + + /** Features included with usage limits and pricing */ + features?: PlanFeature[]; + + /** Free trial period before billing begins */ + free_trial?: FreeTrial | null; +}; + diff --git a/atmn/source/compose/models/productItemModels.ts b/atmn/source/compose/models/productItemModels.ts deleted file mode 100644 index 6ee2d14f..00000000 --- a/atmn/source/compose/models/productItemModels.ts +++ /dev/null @@ -1,94 +0,0 @@ -import {z} from 'zod/v4'; - -export const ProductItemIntervalEnum = z.enum( - ['minute', 'hour', 'day', 'week', 'month', 'quarter', 'semi_annual', 'year'], - { - message: - "Interval must be 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'semi_annual', or 'year'", - }, -); - -export const UsageModelEnum = z.enum(['prepaid', 'pay_per_use'], { - message: "Usage model must be 'prepaid' or 'pay_per_use'", -}); - -export type ProductItemInterval = z.infer; -export type UsageModel = z.infer; - -export const ProductItemSchema = z.object({ - type: z - .enum(['feature', 'priced_feature'], { - message: "Type must be 'feature' or 'priced_feature'", - }) - .nullish(), - feature_id: z - .string({ - message: 'Feature ID must be a string', - }) - .nullish(), - included_usage: z - .union([z.number(), z.literal('inf')], { - message: 'Included usage must be a number or "inf"', - }) - .nullish(), - interval: ProductItemIntervalEnum.nullish(), - usage_model: UsageModelEnum.nullish(), - price: z - .number({ - message: 'Price must be a valid number', - }) - .nullish(), - tiers: z - .array( - z.object({ - amount: z.number({ - message: 'Tier amount must be a valid number', - }), - to: z.union([z.number(), z.literal('inf')], { - message: 'Tier "to" must be a number or "inf"', - }), - }), - ) - .nullish(), - billing_units: z - .number({ - message: 'Billing units must be a valid number', - }) - .nullish(), - - reset_usage_when_enabled: z - .boolean({ - message: 'Reset usage when enabled must be true or false', - }) - .optional(), - entity_feature_id: z - .string({ - message: 'Entity feature ID must be a string', - }) - .optional(), -}); - -export const FeatureItemSchema = z.object({ - feature_id: z.string({ - message: 'Feature ID is required and must be a string', - }), - included_usage: z - .number({ - message: 'Included usage must be a valid number', - }) - .nullish(), - interval: ProductItemIntervalEnum.nullish(), -}); - -export const PriceItemSchema = z.object({ - price: z - .number({ - message: 'Price must be a valid number', - }) - .gt(0, 'Price must be greater than 0'), - interval: ProductItemIntervalEnum.nullish(), -}); - -export type FeatureItem = z.infer; -export type PriceItem = z.infer; -export type ProductItem = z.infer; diff --git a/atmn/source/constants.ts b/atmn/source/constants.ts index b997348f..48598a1e 100644 --- a/atmn/source/constants.ts +++ b/atmn/source/constants.ts @@ -1,51 +1,11 @@ -// export const FRONTEND_URL = 'http://localhost:3000'; -// export const BACKEND_URL = 'http://localhost:8080'; -export const FRONTEND_URL = 'http://app.useautumn.com'; -export const BACKEND_URL = 'https://api.useautumn.com'; - -export const DEFAULT_CONFIG = `import { - feature, - product, - priceItem, - featureItem, - pricedFeatureItem, -} from 'atmn'; - -export const seats = feature({ - id: 'seats', - name: 'Seats', - type: 'continuous_use', -}); - -export const messages = feature({ - id: 'messages', - name: 'Messages', - type: 'single_use', -}); - -export const pro = product({ - id: 'pro', - name: 'Pro', - items: [ - // 500 messages per month - featureItem({ - feature_id: messages.id, - included_usage: 500, - interval: 'month', - }), - - // $10 per seat per month - pricedFeatureItem({ - feature_id: seats.id, - price: 10, - interval: 'month', - }), - - // $50 / month - priceItem({ - price: 50, - interval: 'month', - }), - ], -}); -`; +/** + * @deprecated This file is deprecated. Import from src/constants.js instead. + * This file re-exports from the new location for backwards compatibility. + */ +export { + FRONTEND_URL, + BACKEND_URL, + LOCAL_FRONTEND_URL, + LOCAL_BACKEND_URL, + DEFAULT_CONFIG, +} from "../src/constants.js"; diff --git a/atmn/source/core/api.ts b/atmn/source/core/api.ts index 890a6c78..9267d211 100644 --- a/atmn/source/core/api.ts +++ b/atmn/source/core/api.ts @@ -49,7 +49,7 @@ export async function request({ params: queryParams, headers: { "Content-Type": "application/json", - "X-API-Version": "1.2.0", + "X-API-Version": "2.0.0", ...headers, Authorization: customAuth || `Bearer ${apiKey}`, }, @@ -148,7 +148,7 @@ export async function deleteFeature({ id }: { id: string }) { path: `/features/${id}`, }); } -export async function deleteProduct({ +export async function deletePlan({ id, allVersions, }: { diff --git a/atmn/source/core/builders/featureBuilder.ts b/atmn/source/core/builders/featureBuilder.ts index 2b28f9d6..4628e562 100644 --- a/atmn/source/core/builders/featureBuilder.ts +++ b/atmn/source/core/builders/featureBuilder.ts @@ -1,8 +1,20 @@ -import {Feature} from '../../compose/index.js'; -import {idToVar} from '../utils.js'; +import {idToVar, notNullish} from '../utils.js'; -const creditSchemaBuilder = (feature: Feature) => { - if (feature.type == 'credit_system') { +// API feature type (what comes from the server) +type ApiFeatureType = 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'static'; + +type ApiFeature = { + id: string; + name?: string | null; + type: ApiFeatureType; + credit_schema?: Array<{ + metered_feature_id: string; + credit_cost: number; + }>; +}; + +const creditSchemaBuilder = (feature: ApiFeature) => { + if (feature.type === 'credit_system' && feature.credit_schema) { let creditSchema = feature.credit_schema || []; return ` credit_schema: [ @@ -14,17 +26,47 @@ const creditSchemaBuilder = (feature: Feature) => { }`, ) .join(',\n ')} - ]`; + ],`; } return ''; }; -export function featureBuilder(feature: Feature) { +/** + * Maps API feature type to SDK type and consumable field + * - API 'boolean' -> SDK type: 'boolean' (no consumable) + * - API 'single_use' -> SDK type: 'metered', consumable: true + * - API 'continuous_use' -> SDK type: 'metered', consumable: false + * - API 'credit_system' -> SDK type: 'credit_system' (no consumable needed) + */ +function getTypeAndConsumable(apiType: ApiFeatureType): {type: string; consumable?: boolean} { + switch (apiType) { + case 'single_use': + return {type: 'metered', consumable: true}; + case 'continuous_use': + return {type: 'metered', consumable: false}; + case 'boolean': + return {type: 'boolean'}; + case 'credit_system': + return {type: 'credit_system'}; + case 'static': + return {type: 'boolean'}; // static maps to boolean in SDK + default: + return {type: apiType}; + } +} + +export function featureBuilder(feature: ApiFeature) { + const nameStr = notNullish(feature.name) ? `\n name: '${feature.name}',` : ''; + const creditSchemaStr = creditSchemaBuilder(feature); + const {type, consumable} = getTypeAndConsumable(feature.type); + + // Build consumable string only for metered features + const consumableStr = consumable !== undefined ? `\n consumable: ${consumable},` : ''; + const snippet = ` export const ${idToVar({id: feature.id, prefix: 'feature'})} = feature({ - id: '${feature.id}', - name: '${feature.name}', - type: '${feature.type}',${creditSchemaBuilder(feature)} + id: '${feature.id}',${nameStr} + type: '${type}',${consumableStr}${creditSchemaStr} })`; return snippet; } diff --git a/atmn/source/core/builders/freeTrialBuilder.ts b/atmn/source/core/builders/freeTrialBuilder.ts index c2bc8737..19a05c5e 100644 --- a/atmn/source/core/builders/freeTrialBuilder.ts +++ b/atmn/source/core/builders/freeTrialBuilder.ts @@ -1,10 +1,9 @@ -import {FreeTrial} from '../../compose/models/composeModels.js'; +import {FreeTrial} from '../../compose/models/planModels.js'; export function freeTrialBuilder({freeTrial}: {freeTrial: FreeTrial}) { return `free_trial: { - duration: '${freeTrial.duration}', - length: ${freeTrial.length}, - unique_fingerprint: ${freeTrial.unique_fingerprint}, + duration_type: '${freeTrial.duration_type}', + duration_length: ${freeTrial.duration_length}, card_required: ${freeTrial.card_required}, },`; } diff --git a/atmn/source/core/builders/planBuilder.ts b/atmn/source/core/builders/planBuilder.ts new file mode 100644 index 00000000..76a157f8 --- /dev/null +++ b/atmn/source/core/builders/planBuilder.ts @@ -0,0 +1,255 @@ +// @ts-nocheck +import type { Feature, Plan } from "../../compose/index.js"; +import { idToVar, notNullish, nullish } from "../utils.js"; + +export function importBuilder() { + return ` +import { + feature, + plan, + planFeature, +} from 'atmn'; + `; +} + +export function exportBuilder(planIds: string[], featureIds: string[]) { + const snippet = ` +const autumnConfig = { + plans: [${planIds.map((id) => `${idToVar({ id, prefix: "plan" })}`).join(", ")}], + features: [${featureIds.map((id) => `${idToVar({ id, prefix: "feature" })}`).join(", ")}] +} + +export default autumnConfig; + `; + return snippet; +} + +export function planBuilder({ + plan, + features, +}: { + plan: Plan; + features: Feature[]; +}) { + // Plan from API has nested reset structure, cast to ApiPlanFeature for builder + const planFeaturesStr = + plan.features + ?.map((pf: any) => + planFeatureBuilder({ planFeature: pf as ApiPlanFeature, features }), + ) + .join(",\n ") || ""; + + const priceStr = plan.price + ? `\n price: {\n amount: ${plan.price.amount},\n interval: '${plan.price.interval}',\n },` + : ""; + + const descriptionStr = + plan.description && plan.description !== null + ? `\n description: '${plan.description.replace(/'/g, "\\'")}',` + : ""; + + const groupStr = + plan.group && plan.group !== "" && plan.group !== null + ? `\n group: '${plan.group}',` + : ""; + + const addOnStr = plan.add_on === true ? `\n add_on: true,` : ""; + + const autoEnableStr = plan.default === true ? `\n auto_enable: true,` : ""; + + const freeTrialStr = + plan.free_trial && plan.free_trial !== null + ? `\n free_trial: {\n duration_type: '${plan.free_trial.duration_type}',\n duration_length: ${plan.free_trial.duration_length},\n card_required: ${plan.free_trial.card_required},\n },` + : ""; + + const snippet = ` +export const ${idToVar({ id: plan.id, prefix: "plan" })} = plan({ + id: '${plan.id}', + name: '${plan.name}',${descriptionStr}${groupStr}${addOnStr}${autoEnableStr}${priceStr} + features: [ + ${planFeaturesStr} + ],${freeTrialStr} +}); +`; + return snippet; +} + +export const getFeatureIdStr = ({ + featureId, + features, +}: { + featureId: string; + features: Feature[]; +}) => { + if (nullish(featureId)) return ""; + + const feature = features.find((f) => f.id === featureId); + + if (feature?.archived) return `"${featureId}"`; + return `${idToVar({ id: featureId, prefix: "feature" })}.id`; +}; + +// API PlanFeature type (what comes from server - has nested reset object) +type ApiPlanFeature = { + feature_id: string; + granted_balance?: number; + unlimited?: boolean; + reset?: { + interval?: string; + interval_count?: number; + reset_when_enabled?: boolean; + }; + price?: { + amount?: number; + tiers?: Array<{ to: number | "inf"; amount: number }>; + interval?: string; + interval_count?: number; + billing_units?: number; + usage_model?: string; + max_purchase?: number; + }; + proration?: { + on_increase?: string; + on_decrease?: string; + }; + rollover?: { + max?: number; + expiry_duration_type?: string; + expiry_duration_length?: number; + }; +}; + +// Plan Feature Builder - transforms API response to SDK format (flattened) +function planFeatureBuilder({ + planFeature, + features, +}: { + planFeature: ApiPlanFeature; + features: Feature[]; +}) { + const featureIdStr = getFeatureIdStr({ + featureId: planFeature.feature_id, + features, + }); + + const parts: string[] = [`feature_id: ${featureIdStr}`]; + + // Included usage (API: granted_balance -> SDK: included) + if ( + notNullish(planFeature.granted_balance) && + planFeature.granted_balance > 0 + ) { + parts.push(`included: ${planFeature.granted_balance}`); + } + + // Unlimited (only if true) + if (planFeature.unlimited === true) { + parts.push(`unlimited: true`); + } + + // Flattened reset fields (API: reset.interval -> SDK: interval at top level) + if (planFeature.reset?.interval) { + parts.push(`interval: '${planFeature.reset.interval}'`); + } + if ( + notNullish(planFeature.reset?.interval_count) && + planFeature.reset!.interval_count !== 1 + ) { + parts.push(`interval_count: ${planFeature.reset!.interval_count}`); + } + // API: reset_when_enabled (true = reset on enable) -> SDK: carry_over_usage (true = keep existing) + // They are inverted: reset_when_enabled=false means carry_over_usage=true (default) + // Only output if explicitly false (meaning carry_over_usage=true, which is non-default behavior) + if (planFeature.reset?.reset_when_enabled === false) { + parts.push(`carry_over_usage: true`); + } else if (planFeature.reset?.reset_when_enabled === true) { + // reset_when_enabled=true is the default, so carry_over_usage=false + // Only output if we want to be explicit + parts.push(`carry_over_usage: false`); + } + + // Price configuration (NO interval/interval_count - they don't exist in SDK price) + if (planFeature.price) { + const priceParts: string[] = []; + + if (notNullish(planFeature.price.amount)) { + priceParts.push(`amount: ${planFeature.price.amount}`); + } + + if (planFeature.price.tiers && planFeature.price.tiers.length > 0) { + const tiersStr = planFeature.price.tiers + .map( + (tier) => + `{ to: ${tier.to === "inf" ? "'inf'" : tier.to}, amount: ${tier.amount} }`, + ) + .join(", "); + priceParts.push(`tiers: [${tiersStr}]`); + } + + // Note: price.interval and price.interval_count are NOT in SDK - they come from top-level interval + + if ( + notNullish(planFeature.price.billing_units) && + planFeature.price.billing_units !== 1 + ) { + priceParts.push(`billing_units: ${planFeature.price.billing_units}`); + } + + // API: usage_model -> SDK: billing_method + if (planFeature.price.usage_model) { + priceParts.push(`billing_method: '${planFeature.price.usage_model}'`); + } + + if (notNullish(planFeature.price.max_purchase)) { + priceParts.push(`max_purchase: ${planFeature.price.max_purchase}`); + } + + if (priceParts.length > 0) { + parts.push(`price: { ${priceParts.join(", ")} }`); + } + } + + // Proration (only if configured) + if (planFeature.proration) { + const prorationParts: string[] = []; + if (planFeature.proration.on_increase) { + prorationParts.push( + `on_increase: '${planFeature.proration.on_increase}'`, + ); + } + if (planFeature.proration.on_decrease) { + prorationParts.push( + `on_decrease: '${planFeature.proration.on_decrease}'`, + ); + } + if (prorationParts.length > 0) { + parts.push(`proration: { ${prorationParts.join(", ")} }`); + } + } + + // Rollover (only if configured) + if (planFeature.rollover) { + const rolloverParts: string[] = []; + if (notNullish(planFeature.rollover.max)) { + rolloverParts.push(`max: ${planFeature.rollover.max}`); + } + if (planFeature.rollover.expiry_duration_type) { + rolloverParts.push( + `expiry_duration_type: '${planFeature.rollover.expiry_duration_type}'`, + ); + } + if ( + notNullish(planFeature.rollover.expiry_duration_length) && + planFeature.rollover.expiry_duration_length !== 1 + ) { + rolloverParts.push( + `expiry_duration_length: ${planFeature.rollover.expiry_duration_length}`, + ); + } + if (rolloverParts.length > 0) { + parts.push(`rollover: { ${rolloverParts.join(", ")} }`); + } + } + + return `planFeature({ ${parts.join(", ")} })`; +} diff --git a/atmn/source/core/builders/productBuilder.ts b/atmn/source/core/builders/productBuilder.ts deleted file mode 100644 index db4d179d..00000000 --- a/atmn/source/core/builders/productBuilder.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { Feature, Product, ProductItem } from "../../compose/index.js"; -import { idToVar, notNullish, nullish } from "../utils.js"; -import { freeTrialBuilder } from "./freeTrialBuilder.js"; - -const ItemBuilders = { - priced_feature: pricedFeatureItemBuilder, - feature: featureItemBuilder, - price: priceItemBuilder, -}; - -export function importBuilder() { - return ` -import { - feature, - product, - featureItem, - pricedFeatureItem, - priceItem, -} from 'atmn'; - `; -} - -export function exportBuilder(productIds: string[], featureIds: string[]) { - const snippet = ` -const autumnConfig = { - products: [${productIds.map((id) => `${idToVar({ id, prefix: "product" })}`).join(", ")}], - features: [${featureIds.map((id) => `${idToVar({ id, prefix: "feature" })}`).join(", ")}] -} - -export default autumnConfig; - `; - return snippet; -} - -export function productBuilder({ - product, - features, -}: { - product: Product; - features: Feature[]; -}) { - const optionalFields = [ - product.group ? `group: '${product.group}'` : "", - product.is_add_on ? `is_add_on: true` : "", - product.is_default ? `is_default: true` : "", - ] - .filter(Boolean) - .join(",\n\t"); - - const snippet = ` -export const ${idToVar({ id: product.id, prefix: "product" })} = product({ - id: '${product.id}', - name: '${product.name}',${optionalFields ? `\n\t${optionalFields},` : ""} - items: [${product.items - .map( - (item: ProductItem) => - `${ItemBuilders[item.type as keyof typeof ItemBuilders]({ - item, - features, - })}`, - ) - .join(" ")} ], - ${product.free_trial ? `${freeTrialBuilder({ freeTrial: product.free_trial })}` : ""} -}) -`; - return snippet; -} - -export const getFeatureIdStr = ({ - featureId, - features, -}: { - featureId: string; - features: Feature[]; -}) => { - if (nullish(featureId)) return ""; - - const feature = features.find((f) => f.id === featureId); - - if (feature?.archived) return `"${featureId}"`; - return `${idToVar({ id: featureId, prefix: "feature" })}.id`; -}; - -// Item Builders - -const getItemFieldPrefix = () => { - return `\n `; -}; -const getResetUsageStr = ({ - item, - features, -}: { - item: ProductItem; - features: Feature[]; -}) => { - if (!item.feature_id) return ""; - const feature = features.find((f) => f.id === item.feature_id); - if (!feature) throw new Error(`FATAL: Feature ${item.feature_id} not found`); - if (feature.type === "boolean" || feature.type === "credit_system") return ""; - - const defaultResetUsage = feature.type === "single_use"; - - if ( - notNullish(item.reset_usage_when_enabled) && - item.reset_usage_when_enabled !== defaultResetUsage - ) { - return `${getItemFieldPrefix()}reset_usage_when_enabled: ${ - item.reset_usage_when_enabled - },`; - } - - return ""; -}; - -const getIntervalStr = ({ item }: { item: ProductItem }) => { - if (item.interval == null) return ``; - return `${getItemFieldPrefix()}interval: '${item.interval}',`; -}; - -const getEntityFeatureIdStr = ({ - item, - features, -}: { - item: ProductItem; - features: Feature[]; -}) => { - if (nullish(item.entity_feature_id)) return ""; - - const featureIdStr = getFeatureIdStr({ - featureId: item.entity_feature_id, - features, - }); - - return `${getItemFieldPrefix()}entity_feature_id: ${featureIdStr},`; -}; - -const getPriceStr = ({ item }: { item: ProductItem }) => { - // 1. If tiers... - if (item.tiers) { - return ` - tiers: [ - ${item.tiers - .map( - (tier) => - `{ to: ${tier.to === "inf" ? "'inf'" : tier.to}, amount: ${ - tier.amount - } }`, - ) - .join(",\n\t\t\t")} - ],`; - } - - if (item.price == null) return ""; - return `price: ${item.price},`; - // if (item.price == null) return ''; - // return `${getItemFieldPrefix()}price: ${item.price},`; -}; - -export function pricedFeatureItemBuilder({ - item, - features, -}: { - item: ProductItem; - features: Feature[]; -}) { - const intervalStr = getIntervalStr({ item }); - const entityFeatureIdStr = getEntityFeatureIdStr({ item, features }); - const resetUsageStr = getResetUsageStr({ item, features }); - const priceStr = getPriceStr({ item }); - const featureIdStr = getFeatureIdStr({ - featureId: item.feature_id!, - features, - }); - - const snippet = ` - pricedFeatureItem({ - feature_id: ${featureIdStr}, - ${priceStr}${intervalStr} - included_usage: ${ - item.included_usage === "inf" ? `"inf"` : item.included_usage - }, - billing_units: ${item.billing_units}, - usage_model: '${ - item.usage_model - }',${resetUsageStr}${entityFeatureIdStr} - }), -`; - return snippet; -} - -export function featureItemBuilder({ - item, - features, -}: { - item: ProductItem; - features: Feature[]; -}) { - const featureIdStr = getFeatureIdStr({ - featureId: item.feature_id!, - features, - }); - const entityFeatureIdStr = getEntityFeatureIdStr({ item, features }); - const intervalStr = getIntervalStr({ item }); - const resetUsageStr = getResetUsageStr({ item, features }); - const snippet = ` - featureItem({ - feature_id: ${featureIdStr}, - included_usage: ${ - item.included_usage === "inf" ? `"inf"` : item.included_usage - },${intervalStr}${resetUsageStr}${entityFeatureIdStr} - }), -`; - return snippet; -} - -export function priceItemBuilder({ item }: { item: ProductItem }) { - const intervalStr = getIntervalStr({ item }); - const snippet = ` - priceItem({ - price: ${item.price},${intervalStr} - }), -`; - return snippet; -} diff --git a/atmn/source/core/cliContext.ts b/atmn/source/core/cliContext.ts new file mode 100644 index 00000000..a7a86b32 --- /dev/null +++ b/atmn/source/core/cliContext.ts @@ -0,0 +1,11 @@ +/** + * @deprecated This file is deprecated. Import from src/lib/env/cliContext.js instead. + * This file re-exports from the new location for backwards compatibility. + */ +export { + type CliContext, + getCliContext, + setCliContext, + isProd, + isLocal, +} from "../../src/lib/env/cliContext.js"; diff --git a/atmn/source/core/config.ts b/atmn/source/core/config.ts index 95d7e450..090664b4 100644 --- a/atmn/source/core/config.ts +++ b/atmn/source/core/config.ts @@ -7,11 +7,14 @@ import {execSync} from 'child_process'; import chalk from 'chalk'; import {confirm, select} from '@inquirer/prompts'; import { - ProductSchema, + PlanSchema, + type Plan, + type FreeTrial, +} from '../compose/models/planModels.js'; +import { FeatureSchema, - type Product, type Feature, -} from '../compose/models/composeModels.js'; +} from '../compose/models/featureModels.js'; import {readFromEnv} from './utils.js'; function checkAtmnInstalled(): boolean { @@ -78,9 +81,9 @@ async function installAtmn(): Promise { } } -function isProduct(value: any): value is Product { +function isPlan(value: any): value is Plan { try { - ProductSchema.parse(value); + PlanSchema.strict().parse(value); return true; } catch (error) { return false; @@ -89,17 +92,17 @@ function isProduct(value: any): value is Product { function isFeature(value: any): value is Feature { try { - FeatureSchema.parse(value); + FeatureSchema.strict().parse(value); return true; } catch { return false; } } -function detectObjectType(value: any): 'product' | 'feature' | 'unknown' { +function detectObjectType(value: any): 'plan' | 'feature' | 'unknown' { if (value && typeof value === 'object') { - if (value.items && Array.isArray(value.items)) { - return 'product'; + if (value.features && Array.isArray(value.features)) { + return 'plan'; } if (value.type) { return 'feature'; @@ -155,15 +158,24 @@ export async function loadAutumnConfigFile() { const jiti = createJiti(import.meta.url); const mod = await jiti.import(fileUrl); - const products: Product[] = []; + const plans: Plan[] = []; const features: Feature[] = []; // Check for old-style default export first const defaultExport = (mod as any).default; - if (defaultExport && defaultExport.products && defaultExport.features) { - // Old format: default export with products and features arrays + if (defaultExport && defaultExport.plans && defaultExport.features) { + // Old format: default export with plans and features arrays + if (Array.isArray(defaultExport.plans)) { + plans.push(...defaultExport.plans); + } + if (Array.isArray(defaultExport.features)) { + features.push(...defaultExport.features); + } + } else if (defaultExport && defaultExport.products && defaultExport.features) { + // Legacy format: default export with products (backwards compatibility) + console.warn(chalk.yellow('⚠️ Using legacy "products" export. Please migrate to "plans" export.')); if (Array.isArray(defaultExport.products)) { - products.push(...defaultExport.products); + plans.push(...defaultExport.products); } if (Array.isArray(defaultExport.features)) { features.push(...defaultExport.features); @@ -173,18 +185,18 @@ export async function loadAutumnConfigFile() { for (const [key, value] of Object.entries(mod as Record)) { if (key === 'default') continue; - if (isProduct(value)) { - products.push(value as Product); + if (isPlan(value)) { + plans.push(value as Plan); } else if (isFeature(value)) { features.push(value as Feature); } else { // Object doesn't match either schema - provide helpful error const detectedType = detectObjectType(value); - if (detectedType === 'product') { - const validationError = getValidationError(ProductSchema, value); - console.error('\n' + chalk.red('❌ Invalid product configuration')); - console.error(chalk.yellow(`Product: "${key}"`)); + if (detectedType === 'plan') { + const validationError = getValidationError(PlanSchema, value); + console.error('\n' + chalk.red('❌ Invalid plan configuration')); + console.error(chalk.yellow(`Plan: "${key}"`)); console.error(chalk.red('Validation errors:') + validationError); process.exit(1); } else if (detectedType === 'feature') { @@ -198,7 +210,7 @@ export async function loadAutumnConfigFile() { console.error(chalk.yellow(`Object: "${key}"`)); console.error( chalk.red('Error:') + - " Object must be either a product (with 'items' field) or feature (with 'type' field)", + " Object must be either a plan (with 'features' field) or feature (with 'type' field)", ); process.exit(1); } @@ -215,7 +227,7 @@ export async function loadAutumnConfigFile() { } return { - products, + plans, features, env: secretKey?.includes('live') ? 'prod' : 'sandbox', }; diff --git a/atmn/source/core/generateSDKTypes.ts b/atmn/source/core/generateSDKTypes.ts new file mode 100644 index 00000000..7d3cf4cb --- /dev/null +++ b/atmn/source/core/generateSDKTypes.ts @@ -0,0 +1,57 @@ +// @ts-nocheck +import fs from "fs"; +import path from "path"; +import type { Feature } from "../compose/models/featureModels.js"; +import type { Plan } from "../compose/models/planModels.js"; + +/** + * Generate @useautumn-sdk.d.ts with type narrowing for product and feature IDs + * + * This creates autocomplete in the IDE for known product/feature IDs + * while still allowing any string value. + */ +export function generateSDKTypes({ + plans, + features, + outputDir, +}: { + plans: Plan[]; + features: Feature[]; + outputDir: string; +}) { + // Generate interface members for each ID + const productIdMembers = plans.map((p) => ` ${p.id}: any;`).join("\n"); + const featureIdMembers = features.map((f) => ` ${f.id}: any;`).join("\n"); + + // Filter for continuous_use features only (for entities) + const entityFeatures = features.filter((f) => f.type === "continuous_use"); + const entityFeatureIdMembers = entityFeatures + .map((f) => ` ${f.id}: any;`) + .join("\n"); + + const content = `// AUTO-GENERATED by atmn pull +// DO NOT EDIT MANUALLY - changes will be overwritten on next pull + +import "@useautumn/sdk"; + +// Add your product and feature IDs to the SDK's interface maps +declare module "@useautumn/sdk/custom/autumn-ids" { + export interface KnownProductIdsMap { +${productIdMembers || " // No products found"} + } + + export interface KnownFeatureIdsMap { +${featureIdMembers || " // No features found"} + } + + export interface KnownEntityFeatureIdsMap { +${entityFeatureIdMembers || " // No continuous_use features found"} + } +} +`; + + const outputPath = path.join(outputDir, "@useautumn-sdk.d.ts"); + fs.writeFileSync(outputPath, content); + + return outputPath; +} diff --git a/atmn/source/core/nuke.ts b/atmn/source/core/nuke.ts index e5530ad2..2d6cda85 100644 --- a/atmn/source/core/nuke.ts +++ b/atmn/source/core/nuke.ts @@ -1,4 +1,4 @@ -import {deleteFeature, deleteProduct, externalRequest} from './api.js'; +import {deleteFeature, deletePlan, externalRequest} from './api.js'; import {initSpinner} from './utils.js'; export async function nukeCustomers( @@ -51,13 +51,13 @@ async function deleteCustomer(id: string) { }); } -export async function nukeProducts(ids: string[]) { - const s = initSpinner('Deleting products'); +export async function nukePlans(ids: string[]) { + const s = initSpinner('Deleting plans'); for (const id of ids) { - s.text = `Deleting product [${id}] ${ids.indexOf(id) + 1} / ${ids.length}`; - await deleteProduct({id, allVersions: true}); + s.text = `Deleting plan [${id}] ${ids.indexOf(id) + 1} / ${ids.length}`; + await deletePlan({id, allVersions: true}); } - s.success('Products deleted successfully!'); + s.success('Plans deleted successfully!'); } export async function nukeFeatures(ids: string[]) { diff --git a/atmn/source/core/pull.ts b/atmn/source/core/pull.ts index 8a05c799..387981be 100644 --- a/atmn/source/core/pull.ts +++ b/atmn/source/core/pull.ts @@ -1,8 +1,8 @@ import chalk from 'chalk'; -import type {Feature, Product} from '../compose/models/composeModels.js'; +import type {Plan} from '../compose/models/planModels.js'; import {externalRequest} from './api.js'; -export async function getProducts(ids: string[]): Promise { +export async function getPlans(ids: string[]): Promise { return await Promise.all( ids.map(id => externalRequest({ @@ -13,56 +13,67 @@ export async function getProducts(ids: string[]): Promise { ); } -export async function getAllProducts(params?: { +export async function getAllPlans(params?: { archived?: boolean; -}): Promise { - const {list: products} = await externalRequest({ +}): Promise { + const {list: plans} = await externalRequest({ method: 'GET', path: `/products`, queryParams: {include_archived: params?.archived ? true : false}, }); - return [...products]; + return [...plans]; } -export async function getAllProductVariants() { +export async function getAllPlanVariants() { const {list} = await externalRequest({ method: 'GET', path: '/products', }); - const allProducts = []; - allProducts.push( - ...list.flatMap((product: {name: string; version: number; id: string}) => { - if (product.version > 1) { - // Get all versions of this product - return Array.from({length: product.version}, (_, i) => ({ - id: product.id, - name: product.name, + const allPlans = []; + allPlans.push( + ...list.flatMap((plan: {name: string; version: number; id: string}) => { + if (plan.version > 1) { + // Get all versions of this plan + return Array.from({length: plan.version}, (_, i) => ({ + id: plan.id, + name: plan.name, version: i + 1, })); } else { return [ { - id: product.id, - name: product.name, - version: product.version, + id: plan.id, + name: plan.name, + version: plan.version, }, ]; } }), ); - return allProducts; + return allPlans; } -export async function getFeatures(params?: {includeArchived?: boolean}) { +// API feature type (what comes from the server) +export type ApiFeature = { + id: string; + name?: string | null; + type: 'boolean' | 'single_use' | 'continuous_use' | 'credit_system' | 'static'; + credit_schema?: Array<{ + metered_feature_id: string; + credit_cost: number; + }>; +}; + +export async function getFeatures(params?: {includeArchived?: boolean}): Promise { const {list} = await externalRequest({ method: 'GET', path: '/features', queryParams: {include_archived: params?.includeArchived ? true : false}, }); - return list.map((feature: Feature) => feature as Feature); + return list as ApiFeature[]; } const MAX_RECURSION_LIMIT = 500; diff --git a/atmn/source/core/push.ts b/atmn/source/core/push.ts index f46d8d85..29ae11b9 100644 --- a/atmn/source/core/push.ts +++ b/atmn/source/core/push.ts @@ -1,13 +1,14 @@ -import type {Spinner} from 'yocto-spinner'; -import type {Feature, Product} from '../compose/index.js'; -import {externalRequest} from './api.js'; -import {getAllProducts, getFeatures} from './pull.js'; +// @ts-nocheck +import type { Spinner } from 'yocto-spinner'; +import type { Feature, Plan } from '../compose/index.js'; +import { externalRequest } from './api.js'; +import { getAllPlans, getFeatures } from './pull.js'; export async function checkForDeletables( currentFeatures: Feature[], - currentProducts: Product[], + currentPlans: Plan[], ) { - const features = await getFeatures({includeArchived: true}); // Get from AUTUMN + const features = await getFeatures({ includeArchived: true }); // Get from AUTUMN const featureIds = features.map((feature: Feature) => feature.id); const currentFeatureIds = currentFeatures.map(feature => feature.id); @@ -19,21 +20,21 @@ export async function checkForDeletables( ), ); - const products = await getAllProducts(); - const productIds = products.map((product: Product) => product.id); - const currentProductIds = currentProducts.map( - (product: Product) => product.id, + const plans = await getAllPlans(); + const planIds = plans.map((plan: Plan) => plan.id); + const currentPlanIds = currentPlans.map( + (plan: Plan) => plan.id, ); - const productsToDelete = productIds.filter( - (productId: string) => !currentProductIds.includes(productId), + const plansToDelete = planIds.filter( + (planId: string) => !currentPlanIds.includes(planId), ); return { allFeatures: features, curFeatures: features.filter((feature: Feature) => !feature.archived), - curProducts: products, + curPlans: plans, featuresToDelete, - productsToDelete, + plansToDelete, }; } @@ -68,8 +69,7 @@ export async function upsertFeature(feature: Feature, s: Spinner) { } console.error( - `\nFailed to push feature ${feature.id}: ${ - error.response?.data?.message || 'Unknown error' + `\nFailed to push feature ${feature.id}: ${error.response?.data?.message || 'Unknown error' }`, ); process.exit(1); @@ -81,105 +81,195 @@ export async function upsertFeature(feature: Feature, s: Spinner) { } } -export async function checkProductForConfirmation({ - curProducts, - product, +export async function checkPlanForConfirmation({ + curPlans, + plan, }: { - curProducts: Product[]; - product: Product; + curPlans: Plan[]; + plan: Plan; }) { - const curProduct = curProducts.find(p => p.id === product.id); - if (!curProduct) { - // return { needsConfirmation: false, shouldUpdate: true }; + const curPlan = curPlans.find(p => p.id === plan.id); + if (!curPlan) { return { - id: product.id, + id: plan.id, will_version: false, }; } const res1 = await externalRequest({ method: 'GET', - path: `/products/${product.id}/has_customers`, - data: product, + path: `/products/${plan.id}/has_customers`, + data: plan, }); return { - id: product.id, + id: plan.id, will_version: res1.will_version, archived: res1.archived, }; +} - // const {will_version} = res1; +/** + * Transform plan data for API submission. + * Maps SDK field names to API field names: + * - 'auto_enable' -> 'default' + * - 'included' -> 'granted_balance' + * - 'billing_method' -> 'usage_model' + * - Flattened interval/interval_count/carry_over_usage -> nested reset object + */ +function transformPlanForApi(plan: Plan): Record { + const transformed = { ...plan } as Record; - // if (will_version) { - // const shouldUpdate = await confirm({ - // message: `Product ${product.id} has customers on it and updating it will create a new version.\nAre you sure you'd like to continue? `, - // }); - // return { needsConfirmation: true, shouldUpdate }; - // } + // 'auto_enable' -> 'default' + if ('auto_enable' in plan) { + transformed.default = plan.auto_enable; + delete transformed.auto_enable; + } + + // Transform features array + if (plan.features && Array.isArray(plan.features)) { + transformed.features = plan.features.map(feature => { + const transformedFeature = { ...feature } as Record; + + // 'included' -> 'granted_balance' + if ('included' in feature && feature.included !== undefined) { + transformedFeature.granted_balance = feature.included; + delete transformedFeature.included; + } + + // Transform flattened reset fields to nested reset object + // SDK: interval, interval_count, carry_over_usage -> API: reset.interval, reset.interval_count, reset.reset_when_enabled + const featureAny = feature as Record; + if ('interval' in featureAny || 'interval_count' in featureAny || 'carry_over_usage' in featureAny) { + const reset: Record = {}; + + if ('interval' in featureAny && featureAny.interval !== undefined) { + reset.interval = featureAny.interval; + delete transformedFeature.interval; + } + + if ('interval_count' in featureAny && featureAny.interval_count !== undefined) { + reset.interval_count = featureAny.interval_count; + delete transformedFeature.interval_count; + } + + // SDK: carry_over_usage (true = keep existing) -> API: reset_when_enabled (true = reset on enable) + // They are inverted + if ('carry_over_usage' in featureAny && featureAny.carry_over_usage !== undefined) { + reset.reset_when_enabled = !featureAny.carry_over_usage; + delete transformedFeature.carry_over_usage; + } + + if (Object.keys(reset).length > 0) { + transformedFeature.reset = reset; + } + } - // return { needsConfirmation: false, shouldUpdate: true }; + // Transform nested price object: 'billing_method' -> 'usage_model' + // Also add interval/interval_count to price from reset if price exists + if ('price' in feature && feature.price && typeof feature.price === 'object') { + const price = feature.price as Record; + const transformedPrice = { ...price }; + + if ('billing_method' in price) { + transformedPrice.usage_model = price.billing_method; + delete transformedPrice.billing_method; + } + + // If we have a reset interval and a price, copy interval to price for API + const resetObj = transformedFeature.reset as Record | undefined; + if (resetObj?.interval) { + transformedPrice.interval = resetObj.interval; + if (resetObj.interval_count) { + transformedPrice.interval_count = resetObj.interval_count; + } + } + + transformedFeature.price = transformedPrice; + } + + return transformedFeature; + }); + } + + return transformed; } -export async function upsertProduct({ - curProducts, - product, +export async function upsertPlan({ + curPlans, + plan, spinner, shouldUpdate = true, }: { - curProducts: Product[]; - product: Product; + curPlans: Plan[]; + plan: Plan; spinner: Spinner; shouldUpdate?: boolean; }) { if (!shouldUpdate) { - spinner.text = `Skipping update to product ${product.id}`; + spinner.text = `Skipping update to plan ${plan.id}`; return { - id: product.id, + id: plan.id, action: 'skipped', }; } - const curProduct = curProducts.find(p => p.id === product.id); - if (!curProduct) { + const curPlan = curPlans.find(p => p.id === plan.id); + + // Transform SDK field names to API field names + const apiPlan = transformPlanForApi(plan); + + if (!curPlan) { await externalRequest({ method: 'POST', path: `/products`, - data: product, + data: apiPlan, }); - spinner.text = `Created product [${product.id}]`; + spinner.text = `Created plan [${plan.id}]`; return { - id: product.id, + id: plan.id, action: 'create', }; } else { // Prepare the update payload - const updatePayload = { ...product }; + const updatePayload = { ...apiPlan }; - // If local product has no group but upstream has one, explicitly unset it - if (!product.group && curProduct.group) { - updatePayload.group = null; + // Handle swapNullish for group field: + // - If local is undefined AND upstream has a group → send null (user wants to unset) + // - If local is null AND upstream has a group → send null (user wants to unset) + // - If local has a value AND upstream is different → send the new value (already in apiPlan) + // - If local is undefined AND upstream is undefined → do nothing + if (plan.group === undefined && curPlan.group !== undefined && curPlan.group !== null) { + updatePayload['group'] = null; + } else if (plan.group === null && curPlan.group !== undefined && curPlan.group !== null) { + updatePayload['group'] = null; } - // If local product has undefined is_add_on but upstream is true, explicitly set to false - if (product.is_add_on === undefined && curProduct.is_add_on === true) { - updatePayload.is_add_on = false; + // Handle swapFalse for add_on field: + // - If local is undefined AND upstream is true → send false (user wants to unset) + // - If local is true or false → send that value (already in apiPlan) + // - If local is undefined AND upstream is false/undefined → do nothing + if (plan.add_on === undefined && curPlan.add_on === true) { + updatePayload['add_on'] = false; } - // If local product has undefined is_default but upstream is true, explicitly set to false - if (product.is_default === undefined && curProduct.is_default === true) { - updatePayload.is_default = false; + // Handle swapFalse for auto_enable (maps to 'default' in API): + // - If local is undefined AND upstream is true → send false (user wants to unset) + // - If local is true or false → send that value (already in apiPlan as 'default') + // - If local is undefined AND upstream is false/undefined → do nothing + if (plan.auto_enable === undefined && curPlan.default === true) { + updatePayload['default'] = false; } await externalRequest({ method: 'POST', - path: `/products/${product.id}`, + path: `/products/${plan.id}`, data: updatePayload, }); - spinner.text = `Updated product [${product.id}]`; + spinner.text = `Updated plan [${plan.id}]`; return { - id: product.id, + id: plan.id, action: 'updated', }; } diff --git a/atmn/source/core/requests/orgRequests.ts b/atmn/source/core/requests/orgRequests.ts index 83f4957a..c05f920d 100644 --- a/atmn/source/core/requests/orgRequests.ts +++ b/atmn/source/core/requests/orgRequests.ts @@ -7,3 +7,11 @@ export const getOrg = async () => { }); return response; }; + +export const getOrgMe = async (): Promise<{ name: string; slug: string; env: string }> => { + const response = await externalRequest({ + method: "GET", + path: "/organization/me", + }); + return response; +}; diff --git a/atmn/source/core/requests/prodRequests.ts b/atmn/source/core/requests/prodRequests.ts index 346489ec..11544288 100644 --- a/atmn/source/core/requests/prodRequests.ts +++ b/atmn/source/core/requests/prodRequests.ts @@ -1,28 +1,28 @@ -import {Product} from '../../compose/index.js'; +import {Plan} from '../../compose/index.js'; import {externalRequest} from '../api.js'; -export const getProductDeleteInfo = async ({ - productId, +export const getPlanDeleteInfo = async ({ + planId, }: { - productId: string; + planId: string; }) => { const response = await externalRequest({ method: 'GET', - path: `/products/${productId}/deletion_info`, + path: `/products/${planId}/deletion_info`, }); return response; }; -export const updateProduct = async ({ - productId, +export const updatePlan = async ({ + planId, update, }: { - productId: string; - update: Partial; + planId: string; + update: Partial; }) => { const response = await externalRequest({ method: 'POST', - path: `/products/${productId}`, + path: `/products/${planId}`, data: update, }); return response; diff --git a/atmn/source/core/utils.ts b/atmn/source/core/utils.ts index d35f6854..79cfd43a 100644 --- a/atmn/source/core/utils.ts +++ b/atmn/source/core/utils.ts @@ -1,185 +1,16 @@ -import { confirm } from "@inquirer/prompts"; -import chalk from "chalk"; -import dotenv from "dotenv"; -import fs from "fs"; -import yoctoSpinner from "yocto-spinner"; - -export const notNullish = (value: unknown) => - value !== null && value !== undefined; -export const nullish = (value: unknown) => - value === null || value === undefined; - -export const isProdFlag = () => { - const prodFlag = - process.argv.includes("--prod") || process.argv.includes("-p"); - return prodFlag; -}; - -export const isLocalFlag = () => { - const localFlag = - process.argv.includes("--local") || process.argv.includes("-l"); - - return localFlag; -}; - -export function snakeCaseToCamelCase(value: string) { - return value.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); -} - -export function idToVar({ - id, - prefix = "product", -}: { - id: string; - prefix?: string; -}): string { - const processed = id - .replace(/[-_](.)/g, (_, letter) => letter.toUpperCase()) - .replace(/[^a-zA-Z0-9_$]/g, ""); // Remove invalid JavaScript identifier characters - - // If the processed string starts with a number, add 'product' prefix - if (/^[0-9]/.test(processed)) { - return `${prefix}${processed}`; - } - - // If it starts with other invalid characters, add 'product' prefix - if (/^[^a-zA-Z_$]/.test(processed)) { - return `${prefix}${processed}`; - } - - return processed; -} - -async function upsertEnvVar( - filePath: string, - varName: string, - newValue: string, -) { - const content = fs.readFileSync(filePath, "utf-8"); - const lines = content.split("\n"); - let found = false; - - for (let i = 0; i < lines.length; i++) { - if (lines[i]?.startsWith(`${varName}=`)) { - const shouldOverwrite = await confirm({ - message: `${varName} already exists in .env. Overwrite?`, - default: false, - }); - if (shouldOverwrite) { - lines[i] = `${varName}=${newValue}`; - found = true; - break; - } - } - } - - // If variable wasn't found, add it to the end - if (!found) { - lines.push(`${varName}=${newValue}`); - } - - // Write the updated content back to the file - fs.writeFileSync(filePath, lines.join("\n")); -} - -export async function storeToEnv(prodKey: string, sandboxKey: string) { - const envPath = `${process.cwd()}/.env`; - const envLocalPath = `${process.cwd()}/.env.local`; - const envVars = `AUTUMN_PROD_SECRET_KEY=${prodKey}\nAUTUMN_SECRET_KEY=${sandboxKey}\n`; - - // Check if .env exists first - if (fs.existsSync(envPath)) { - await upsertEnvVar(envPath, "AUTUMN_PROD_SECRET_KEY", prodKey); - await upsertEnvVar(envPath, "AUTUMN_SECRET_KEY", sandboxKey); - console.log(chalk.green(".env file found. Updated keys.")); - } else if (fs.existsSync(envLocalPath)) { - // If .env doesn't exist but .env.local does, create .env and write keys - fs.writeFileSync(envPath, envVars); - console.log( - chalk.green( - ".env.local found but .env not found. Created new .env file and wrote keys.", - ), - ); - } else { - // Neither .env nor .env.local exists, create .env - fs.writeFileSync(envPath, envVars); - console.log( - chalk.green( - "No .env or .env.local file found. Created new .env file and wrote keys.", - ), - ); - } -} - -function getEnvVar(parsed: { [key: string]: string }, prodFlag: boolean) { - if (prodFlag) return parsed["AUTUMN_PROD_SECRET_KEY"]; - - return parsed["AUTUMN_SECRET_KEY"]; -} - -export function readFromEnv(options?: { bypass?: boolean }) { - const envPath = `${process.cwd()}/.env`; - const envLocalPath = `${process.cwd()}/.env.local`; - const prodFlag = - process.argv.includes("--prod") || process.argv.includes("-p"); - - // biome-ignore lint/complexity/useLiteralKeys: will throw "index signature" error otherwise - if (prodFlag && process.env["AUTUMN_PROD_SECRET_KEY"]) { - return process.env["AUTUMN_PROD_SECRET_KEY"]; - } - - // biome-ignore lint/complexity/useLiteralKeys: will throw "index signature" error otherwise - if (!prodFlag && process.env["AUTUMN_SECRET_KEY"]) { - return process.env["AUTUMN_SECRET_KEY"]; - } - - let secretKey: string | undefined; - - // Check .env second - if (fs.existsSync(envPath) && !secretKey) - secretKey = getEnvVar( - dotenv.parse(fs.readFileSync(envPath, "utf-8")), - prodFlag, - ); - - // If not found in .env, check .env.local - if (fs.existsSync(envLocalPath) && !secretKey) - secretKey = getEnvVar( - dotenv.parse(fs.readFileSync(envLocalPath, "utf-8")), - prodFlag, - ); - - if (!secretKey && !options?.bypass) { - if (prodFlag) { - console.error( - "[Error] atmn uses the AUTUMN_PROD_SECRET_KEY to call the Autumn production API. Please add it to your .env file or run `atmn login` to authenticate.", - ); - process.exit(1); - } else { - console.error( - "[Error] atmn uses the AUTUMN_SECRET_KEY to call the Autumn sandbox API. Please add it to your .env (or .env.local) file or run `atmn login` to authenticate.", - ); - process.exit(1); - } - } - - return secretKey; -} - -export function initSpinner(message: string) { - const spinner = yoctoSpinner({ - text: message, - }); - spinner.start(); - - return spinner; -} - -export async function isSandboxKey(apiKey: string) { - const prefix = apiKey.split("am_sk_")[1]?.split("_")[0]; - - if (prefix === "live") { - return false; - } else if (prefix === "test") return true; - else throw new Error("Invalid API key"); -} +/** + * @deprecated This file is deprecated. Import from src/lib/utils.js instead. + * This file re-exports from the new location for backwards compatibility. + */ +export { + notNullish, + nullish, + isProdFlag, + isLocalFlag, + snakeCaseToCamelCase, + idToVar, + storeToEnv, + readFromEnv, + initSpinner, + isSandboxKey, +} from "../../src/lib/utils.js"; diff --git a/atmn/src/cli.tsx b/atmn/src/cli.tsx new file mode 100644 index 00000000..c88780c1 --- /dev/null +++ b/atmn/src/cli.tsx @@ -0,0 +1,277 @@ +#!/usr/bin/env node +/** biome-ignore-all lint/complexity/useLiteralKeys: necessary */ +import chalk from "chalk"; +import { program } from "commander"; +import { render } from "ink"; +import open from "open"; +import React from "react"; +import Nuke from "../source/commands/nuke.js"; +import { getOrgMe } from "../source/core/requests/orgRequests.js"; +// Import existing commands from source/ (legacy - will migrate incrementally) +import AuthCommand from "./commands/auth/command.js"; +import { pull as newPull } from "./commands/pull/pull.js"; // New pull implementation +import { FRONTEND_URL } from "./constants.js"; +import { isProd, setCliContext } from "./lib/env/cliContext.js"; +import { readFromEnv } from "./lib/utils.js"; +// Import Ink views +import { QueryProvider } from "./views/react/components/providers/QueryProvider.js"; +import { InitFlow } from "./views/react/init/InitFlow.js"; +import { PullView } from "./views/react/pull/Pull.js"; + +declare const VERSION: string; + +// Guard against missing define in watch/incremental rebuilds +const computedVersion = + typeof VERSION !== "undefined" && VERSION ? VERSION : "dev"; + +program.version(computedVersion); + +// Global options - available for all commands +// These are orthogonal: -p controls env (sandbox vs live), -l controls API server (remote vs localhost) +// Combined as -lp: use live environment on localhost API server +program.option( + "-p, --prod", + "Use live/production environment (default: sandbox)", +); +program.option( + "-l, --local", + "Use localhost:8080 API server (default: api.useautumn.com)", +); +program.option("--headless", "Force non-interactive mode (for CI/agents)"); + +// Set CLI context before any command runs +// This allows combined flags like -lp to work correctly +program.hook("preAction", (thisCommand) => { + const opts = thisCommand.opts(); + setCliContext({ + prod: opts["prod"] ?? false, + local: opts["local"] ?? false, + }); + + // Override TTY detection if --headless flag is passed globally + if (opts["headless"]) { + process.stdout.isTTY = false; + } +}); +// === Existing commands (unchanged from source/cli.ts) === + +program + .command("env") + .description("Check the environment and organization info") + .action(async () => { + // Ensure API key is present + readFromEnv(); + + // Fetch organization info from API + const orgInfo = await getOrgMe(); + + const envDisplay = orgInfo.env === "sandbox" ? "Sandbox" : "Production"; + console.log(chalk.green(`Organization: ${orgInfo.name}`)); + console.log(chalk.green(`Slug: ${orgInfo.slug}`)); + console.log(chalk.green(`Environment: ${envDisplay}`)); + }); + +program + .command("nuke") + .description("Permanently nuke your sandbox.") + .action(async () => { + // Nuke is sandbox-only - panic if prod flag is passed + if (isProd()) { + console.error( + chalk.red.bold( + "\n ERROR: nuke command is only available for sandbox!\n", + ), + ); + console.error( + chalk.red( + " The nuke command permanently deletes all data and cannot be used on production.", + ), + ); + console.error( + chalk.red(" Remove the -p/--prod flag to nuke your sandbox.\n"), + ); + process.exit(1); + } + + if (process.stdout.isTTY) { + // Interactive mode - use new beautiful Ink UI + const { NukeView } = await import("./views/react/nuke/NukeView.js"); + render( + + + , + ); + } else { + // Non-TTY mode - use legacy command + await Nuke(); + } + }); + +program + .command("push") + .description("Push changes to Autumn") + .option("-y, --yes", "Confirm all prompts automatically") + .action(async (options) => { + // Import AppEnv here to avoid circular dependencies + const { AppEnv } = await import("./lib/env/index.js"); + const environment = isProd() ? AppEnv.Live : AppEnv.Sandbox; + + if (process.stdout.isTTY) { + // Interactive mode - use new beautiful Ink UI + const { PushView } = await import("./views/react/push/Push.js"); + render( + + { + process.exit(0); + }} + /> + , + ); + } else { + // Non-TTY mode - use headless push with V2 logic + const { headlessPush } = await import("./commands/push/headless.js"); + await headlessPush({ + cwd: process.cwd(), + environment, + yes: options.yes, + }); + } + }); + +program + .command("pull") + .description("Pull changes from Autumn") + .action(async () => { + // Import AppEnv here to avoid circular dependencies + const { AppEnv } = await import("./lib/env/index.js"); + const environment = isProd() ? AppEnv.Live : AppEnv.Sandbox; + + if (process.stdout.isTTY) { + // Interactive mode - use beautiful Ink UI + render( + + { + process.exit(0); + }} + /> + , + ); + } else { + // Non-TTY (CI/agent mode) - use plain text + console.log(`Pulling plans and features from Autumn (${environment})...`); + + try { + const result = await newPull({ + generateSdkTypes: true, + cwd: process.cwd(), + environment, + }); + + console.log( + chalk.green( + `✓ Pulled ${result.features.length} features, ${result.plans.length} plans from ${environment}`, + ), + ); + if (result.sdkTypesPath) { + console.log( + chalk.green(`✓ Generated SDK types at: ${result.sdkTypesPath}`), + ); + } + } catch (error) { + console.error(chalk.red("Error pulling from Autumn:"), error); + process.exit(1); + } + } + }); + +program + .command("init") + .description("Initialize an Autumn project.") + .action(async () => { + if (process.stdout.isTTY) { + // Interactive mode - use new Ink-based init flow + render( + + + , + ); + } else { + // Non-TTY (agent/CI mode) - use headless init flow + const { HeadlessInitFlow } = await import( + "./views/react/init/HeadlessInitFlow.js" + ); + render( + + + , + ); + } + }); + +program + .command("login") + .description("Authenticate with Autumn") + .action(async () => { + if (process.stdout.isTTY) { + // Interactive mode - use new beautiful Ink UI + const { LoginView } = await import("./views/react/login/LoginView.js"); + render( + + { + process.exit(0); + }} + /> + , + ); + } else { + // Non-TTY mode - use legacy command with URL fallback + await AuthCommand(); + } + }); + +program + .command("dashboard") + .description("Open the Autumn dashboard in your browser") + .action(() => { + open(`${FRONTEND_URL}`); + }); + +program + .command("version") + .alias("v") + .description("Show the version of Autumn") + .action(() => { + console.log(computedVersion); + }); + +program + .command("customers") + .description("Browse and inspect customers") + .action(async () => { + const { customersCommand } = await import("./commands/customers/index.js"); + await customersCommand({ prod: isProd() }); + }); + +/** + * This is a hack to silence the DeprecationWarning about url.parse() + */ +// biome-ignore lint/suspicious/noExplicitAny: expected +const originalEmit = process.emitWarning as any; +// biome-ignore lint/suspicious/noExplicitAny: expected +(process as any).emitWarning = (warning: any, ...args: any[]) => { + const msg = typeof warning === "string" ? warning : warning.message; + + if (msg.includes("url.parse()")) { + return; + } + + return originalEmit(warning, ...args); +}; + +program.parse(); diff --git a/atmn/src/commands/auth/command.ts b/atmn/src/commands/auth/command.ts new file mode 100644 index 00000000..6aa11148 --- /dev/null +++ b/atmn/src/commands/auth/command.ts @@ -0,0 +1,76 @@ +import chalk from "chalk"; +import { confirm } from "@inquirer/prompts"; +import { storeToEnv, readFromEnv, initSpinner } from "../../lib/utils.js"; +import { startOAuthFlow, getApiKeysWithToken } from "./oauth.js"; +import { CLI_CLIENT_ID } from "./constants.js"; + +const inputTheme = { + style: { + answer: (text: string) => { + return chalk.magenta(text); + }, + }, +}; + +export default async function AuthCommand() { + if (readFromEnv({ bypass: true })) { + const shouldReauth = await confirm({ + message: + "You are already authenticated. Would you like to re-authenticate?", + theme: inputTheme, + }); + if (!shouldReauth) return; + } + + console.log(chalk.cyan("\nOpening browser for authentication...")); + console.log( + chalk.gray( + "Please sign in and select the organization you want to use.\n", + ), + ); + console.log( + chalk.gray("If your browser doesn't open, visit:"), + ); + console.log( + chalk.cyan("https://app.useautumn.com/cli-auth\n"), + ); + + const spinner = initSpinner("Waiting for authorization..."); + + try { + // Start OAuth flow - opens browser and waits for callback + const { tokens } = await startOAuthFlow(CLI_CLIENT_ID); + + spinner.text = "Creating API keys..."; + + // Use the access token to create API keys + const { sandboxKey, prodKey } = await getApiKeysWithToken( + tokens.access_token, + ); + + spinner.stop(); + + // Store keys to .env + await storeToEnv(prodKey, sandboxKey); + + console.log( + chalk.green( + "\nSuccess! Sandbox and production keys have been saved to your .env file.", + ), + ); + console.log( + chalk.gray( + "`atmn` uses the AUTUMN_SECRET_KEY to authenticate with the Autumn API.\n", + ), + ); + process.exit(0); + } catch (error) { + spinner.stop(); + if (error instanceof Error) { + console.error(chalk.red(`\nAuthentication failed: ${error.message}`)); + } else { + console.error(chalk.red("\nAuthentication failed. Please try again.")); + } + process.exit(1); + } +} diff --git a/atmn/src/commands/auth/constants.ts b/atmn/src/commands/auth/constants.ts new file mode 100644 index 00000000..2fe5feda --- /dev/null +++ b/atmn/src/commands/auth/constants.ts @@ -0,0 +1,22 @@ +// OAuth constants for CLI authentication + +/** The OAuth client ID for the CLI (public client) */ +// export const CLI_CLIENT_ID = "khicXGthBbGMIWmpgodOTDcCCJHJMDpN"; +export const CLI_CLIENT_ID = "NiKwaSyAfaeEEKEvFaUYihTXdTPtIRCk" + +/** Base port for the local OAuth callback server */ +export const OAUTH_PORT_BASE = 31448; + +/** Number of ports to try if the base port is in use */ +export const OAUTH_PORT_RANGE = 5; + +/** All valid OAuth ports (31448-31452) */ +export const OAUTH_PORTS = Array.from( + { length: OAUTH_PORT_RANGE }, + (_, i) => OAUTH_PORT_BASE + i, +); + +/** Get OAuth redirect URI for a specific port */ +export function getOAuthRedirectUri(port: number): string { + return `http://localhost:${port}/`; +} diff --git a/atmn/src/commands/auth/oauth.ts b/atmn/src/commands/auth/oauth.ts new file mode 100644 index 00000000..7f381790 --- /dev/null +++ b/atmn/src/commands/auth/oauth.ts @@ -0,0 +1,219 @@ +import * as http from "node:http"; +import * as arctic from "arctic"; +import open from "open"; +import { BACKEND_URL, LOCAL_BACKEND_URL } from "../../constants.js"; +import { isLocal } from "../../lib/env/cliContext.js"; +import { + getErrorHtml, + getSuccessHtml, +} from "../../views/html/oauth-callback.js"; +import { getOAuthRedirectUri, OAUTH_PORTS } from "./constants.js"; + +/** + * Get the current backend URL based on CLI flags + */ +function getBackendUrl(): string { + return isLocal() ? LOCAL_BACKEND_URL : BACKEND_URL; +} + +const getAuthorizationEndpoint = () => + `${getBackendUrl()}/api/auth/oauth2/authorize`; +const getTokenEndpoint = () => `${getBackendUrl()}/api/auth/oauth2/token`; + +export interface OAuthResult { + tokens: { + access_token: string; + token_type: string; + expires_in?: number; + refresh_token?: string; + }; +} + +export interface StartOAuthOptions { + headless?: boolean; +} + +type CallbackResult = { html: string; result?: OAuthResult; error?: Error }; + +/** Start a one-shot HTTP server, returns null if port in use */ +async function startCallbackServer( + port: number, + onCallback: (url: URL) => Promise, + onListening: () => void, +): Promise { + return new Promise((resolve, reject) => { + const server = http.createServer(async (req, res) => { + const url = new URL(req.url || "/", `http://localhost:${port}`); + if (url.pathname !== "/") { + res.writeHead(404).end("Not found"); + return; + } + + const { html, result, error } = await onCallback(url); + res.writeHead(200, { "Content-Type": "text/html" }).end(html); + clearTimeout(timeoutId); + server.close(); + if (error) { + reject(error); + } else if (result) { + resolve(result); + } else { + reject(new Error("Unexpected callback state: no result or error")); + } + }); + + const timeoutId = setTimeout( + () => { + server.close(); + reject(new Error("Authorization timed out. Please try again.")); + }, + 5 * 60 * 1000, + ); + + server.once("error", (err: NodeJS.ErrnoException) => { + err.code === "EADDRINUSE" ? resolve(null) : reject(err); + }); + + server.listen(port, onListening); + }); +} + +/** Start OAuth flow and wait for callback */ +export async function startOAuthFlow( + clientId: string, + options?: StartOAuthOptions, +): Promise { + const codeVerifier = arctic.generateCodeVerifier(); + const state = arctic.generateState(); + + for (const port of OAUTH_PORTS) { + const redirectUri = getOAuthRedirectUri(port); + const client = new arctic.OAuth2Client(clientId, null, redirectUri); + + const authUrl = client.createAuthorizationURLWithPKCE( + getAuthorizationEndpoint(), + state, + arctic.CodeChallengeMethod.S256, + codeVerifier, + [ + // Dynamically construct scopes using CRUDL for each resource + ...[ + { + resource: "customers", + actions: ["create", "read", "list", "update", "delete"], + }, + { + resource: "features", + actions: ["create", "read", "list", "update", "delete"], + }, + { + resource: "plans", + actions: ["create", "read", "list", "update", "delete"], + }, + { resource: "apiKeys", actions: ["create", "read"] }, + { resource: "organisation", actions: ["read"] }, + ].flatMap(({ resource, actions }) => + actions.map((action) => `${resource}:${action}`), + ), + ], + ); + authUrl.searchParams.set("prompt", "consent"); + + const result = await startCallbackServer( + port, + async (url) => { + const error = url.searchParams.get("error"); + const errorDesc = url.searchParams.get("error_description"); + if (error) { + return { + html: getErrorHtml(errorDesc || error), + error: new Error(errorDesc || error), + }; + } + + if (url.searchParams.get("state") !== state) { + return { + html: getErrorHtml("Invalid state"), + error: new Error("Invalid state - possible CSRF"), + }; + } + + const code = url.searchParams.get("code"); + if (!code) { + return { + html: getErrorHtml("Missing code"), + error: new Error("Missing authorization code"), + }; + } + + try { + const tokens = await client.validateAuthorizationCode( + getTokenEndpoint(), + code, + codeVerifier, + ); + return { + html: getSuccessHtml(), + result: { + tokens: { + access_token: tokens.accessToken(), + token_type: "Bearer", + expires_in: tokens.accessTokenExpiresInSeconds(), + refresh_token: tokens.hasRefreshToken() + ? tokens.refreshToken() + : undefined, + }, + }, + }; + } catch (err) { + const msg = + err instanceof arctic.OAuth2RequestError + ? `OAuth error: ${err.code}` + : "Token exchange failed"; + return { html: getErrorHtml(msg), error: new Error(msg) }; + } + }, + () => { + if (options?.headless) { + console.log( + `\nVisit this URL to authenticate:\n\n ${authUrl.toString()}\n`, + ); + } else { + open(authUrl.toString()); + } + }, // Open browser once server is listening + ); + + if (result) return result; + // Port was in use, try next + } + + throw new Error( + `All OAuth ports (${OAUTH_PORTS[0]}-${OAUTH_PORTS[OAUTH_PORTS.length - 1]}) are in use.`, + ); +} + +/** Get or create API keys using the OAuth access token */ +export async function getApiKeysWithToken( + accessToken: string, +): Promise<{ sandboxKey: string; prodKey: string; orgId: string }> { + const response = await fetch(`${getBackendUrl()}/cli/api-keys`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.error || "Failed to create API keys"); + } + + const data = await response.json(); + return { + sandboxKey: data.sandbox_key, + prodKey: data.prod_key, + orgId: data.org_id, + }; +} diff --git a/atmn/src/commands/customers/command.tsx b/atmn/src/commands/customers/command.tsx new file mode 100644 index 00000000..824ebe29 --- /dev/null +++ b/atmn/src/commands/customers/command.tsx @@ -0,0 +1,52 @@ +import { render } from "ink"; +import React from "react"; +import { QueryProvider } from "../../views/react/components/providers/QueryProvider.js"; +import { CustomersView } from "../../views/react/customers/CustomersView.js"; +import { AppEnv } from "../../lib/env/detect.js"; + +export interface CustomersCommandOptions { + prod?: boolean; +} + +/** + * Customers command entry point + * Renders the interactive customers view + */ +export async function customersCommand( + options: CustomersCommandOptions = {}, +): Promise { + const environment = options.prod ? AppEnv.Live : AppEnv.Sandbox; + + if (process.stdout.isTTY) { + // Interactive mode - render Ink UI + const instance = render( + + { + // Clear the terminal output for a clean exit + instance.clear(); + instance.unmount(); + process.exit(0); + }} + /> + , + ); + + // Handle Ctrl+C - clear terminal before exit + process.on("SIGINT", () => { + instance.clear(); + instance.unmount(); + process.exit(0); + }); + } else { + // Non-TTY mode - plain text fallback + console.log("atmn customers - Autumn CLI"); + console.log( + "This command requires an interactive terminal. Please run in a TTY environment.", + ); + process.exit(1); + } +} + +export default customersCommand; diff --git a/atmn/src/commands/customers/index.ts b/atmn/src/commands/customers/index.ts new file mode 100644 index 00000000..404c8aac --- /dev/null +++ b/atmn/src/commands/customers/index.ts @@ -0,0 +1,2 @@ +export { customersCommand, type CustomersCommandOptions } from "./command.js"; +export { customersCommand as default } from "./command.js"; diff --git a/atmn/src/commands/nuke/backup.ts b/atmn/src/commands/nuke/backup.ts new file mode 100644 index 00000000..7c7e01a8 --- /dev/null +++ b/atmn/src/commands/nuke/backup.ts @@ -0,0 +1,73 @@ +/** + * Backup utilities for nuke command + */ + +import fs from "node:fs"; +import path from "node:path"; + +export interface BackupResult { + created: boolean; + path?: string; + error?: string; +} + +/** + * Create backup of autumn.config.ts + * @param cwd - Working directory (defaults to process.cwd()) + * @returns BackupResult with path or error + */ +export function createConfigBackup(cwd: string = process.cwd()): BackupResult { + const configPath = path.join(cwd, "autumn.config.ts"); + const backupPath = path.join(cwd, "autumn.config.ts.backup"); + + try { + // Check if config exists + if (!fs.existsSync(configPath)) { + return { + created: false, + error: "autumn.config.ts not found", + }; + } + + // Check if backup already exists + if (fs.existsSync(backupPath)) { + // Create timestamped backup instead + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); + const timestampedBackupPath = path.join( + cwd, + `autumn.config.ts.backup.${timestamp}` + ); + + fs.copyFileSync(configPath, timestampedBackupPath); + + return { + created: true, + path: timestampedBackupPath, + }; + } + + // Create backup + fs.copyFileSync(configPath, backupPath); + + return { + created: true, + path: backupPath, + }; + } catch (error) { + return { + created: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} + +/** + * Check if config file exists + */ +export function configExists(cwd: string = process.cwd()): boolean { + const configPath = path.join(cwd, "autumn.config.ts"); + return fs.existsSync(configPath); +} diff --git a/atmn/src/commands/nuke/deletions.ts b/atmn/src/commands/nuke/deletions.ts new file mode 100644 index 00000000..f518c606 --- /dev/null +++ b/atmn/src/commands/nuke/deletions.ts @@ -0,0 +1,106 @@ +/** + * Batch deletion logic for nuke command + */ + +import { DELETE_CONCURRENCY } from "../../constants.js"; +import type { DeletionProgress } from "./types.js"; + +/** + * Delete customers in batches with progress callbacks + */ +export async function deleteCustomersBatch( + customers: { id: string }[], + deleteCustomerFn: (id: string) => Promise, + onProgress?: (progress: DeletionProgress) => void +): Promise { + const concurrency = Math.max( + 1, + Math.min(customers.length, DELETE_CONCURRENCY) + ); + + let completed = 0; + const startTime = Date.now(); + + for (let i = 0; i < customers.length; i += concurrency) { + const batch = customers.slice(i, i + concurrency); + + await Promise.all( + batch.map(async (customer) => { + await deleteCustomerFn(customer.id); + completed++; + + if (onProgress) { + const elapsed = (Date.now() - startTime) / 1000; + const rate = elapsed > 0 ? completed / elapsed : 0; + + onProgress({ + phase: "customers", + current: completed, + total: customers.length, + rate, + }); + } + }) + ); + } +} + +/** + * Delete plans sequentially with progress callbacks + */ +export async function deletePlansSequential( + plans: { id: string }[], + deletePlanFn: (id: string, allVersions: boolean) => Promise, + onProgress?: (progress: DeletionProgress) => void +): Promise { + const startTime = Date.now(); + + for (const [i, plan] of plans.entries()) { + await deletePlanFn(plan.id, true); // allVersions = true + + if (onProgress) { + const elapsed = (Date.now() - startTime) / 1000; + const rate = elapsed > 0 ? (i + 1) / elapsed : 0; + + onProgress({ + phase: "plans", + current: i + 1, + total: plans.length, + rate, + }); + } + } +} + +/** + * Delete features sequentially with progress callbacks + * Sorts credit_system features first (dependencies) + */ +export async function deleteFeaturesSequential( + features: { id: string; type: string }[], + deleteFeatureFn: (id: string) => Promise, + onProgress?: (progress: DeletionProgress) => void +): Promise { + // Sort: credit_system features first (they are dependencies) + const sorted = [...features].sort((a) => + a.type === "credit_system" ? -1 : 1 + ); + + const startTime = Date.now(); + + for (const [i, feature] of sorted.entries()) { + await deleteFeatureFn(feature.id); + + if (onProgress) { + const elapsed = (Date.now() - startTime) / 1000; + const rate = elapsed > 0 ? (i + 1) / elapsed : 0; + + onProgress({ + phase: "features", + current: i + 1, + total: sorted.length, + rate, + }); + } + } +} diff --git a/atmn/src/commands/nuke/types.ts b/atmn/src/commands/nuke/types.ts new file mode 100644 index 00000000..fe6c3a8e --- /dev/null +++ b/atmn/src/commands/nuke/types.ts @@ -0,0 +1,43 @@ +/** + * Types for nuke command + */ + +export interface NukeOptions { + /** Working directory */ + cwd?: string; + /** Skip confirmations (DANGEROUS - for testing only) */ + skipConfirmations?: boolean; + /** Max customers to delete */ + maxCustomers?: number; +} + +export interface NukeStats { + customersDeleted: number; + plansDeleted: number; + featuresDeleted: number; + duration: number; // milliseconds + backupCreated: boolean; + backupPath?: string; +} + +export interface NukeResult { + success: boolean; + stats: NukeStats; + error?: string; +} + +export interface DeletionProgress { + phase: "customers" | "plans" | "features"; + current: number; + total: number; + rate?: number; // items per second +} + +export interface NukePhaseStats { + phase: "customers" | "plans" | "features"; + current: number; + total: number; + rate: number; // items per second + duration?: number; // seconds (when complete) + completed: boolean; +} diff --git a/atmn/src/commands/nuke/validation.ts b/atmn/src/commands/nuke/validation.ts new file mode 100644 index 00000000..1cdefdff --- /dev/null +++ b/atmn/src/commands/nuke/validation.ts @@ -0,0 +1,72 @@ +/** + * Nuke command validation + * Ensures safety checks before deletion + */ + +import { AppEnv, getEnvironmentFromKey } from "../../lib/env/detect.js"; + +export class NukeValidationError extends Error { + constructor(message: string) { + super(message); + this.name = "NukeValidationError"; + } +} + +/** + * Validate that the key is sandbox-only + * Throws error if production key detected + */ +export function validateSandboxOnly(key: string): void { + const env = getEnvironmentFromKey(key); + + if (env !== AppEnv.Sandbox) { + throw new NukeValidationError( + "🚨 NUKE BLOCKED: Cannot nuke production!\n\n" + + "This command only works in sandbox environment.\n" + + `Current environment: ${env}\n\n` + + "To nuke your sandbox:\n" + + "1. Use your sandbox key (AUTUMN_SECRET_KEY)\n" + + "2. Make sure it starts with 'ask_test_'\n\n" + + "Aborting for your safety." + ); + } +} + +/** + * Validate customer count is within limits + */ +export function validateCustomerLimit( + customerCount: number, + maxCustomers: number = 50 +): void { + if (customerCount > maxCustomers) { + throw new NukeValidationError( + `❌ Too Many Customers\n\n` + + `You have ${customerCount} customers in your sandbox.\n` + + `Maximum allowed for nuke: ${maxCustomers}\n\n` + + `Why this limit?\n` + + `• Prevents accidental mass deletion\n` + + `• Ensures reasonable deletion time\n` + + `• Encourages cleanup via dashboard\n\n` + + `Options:\n` + + `1. Delete some customers via dashboard\n` + + `2. Increase limit: ATMN_NUKE_MAX_CUSTOMERS=${customerCount}\n` + + `3. Contact support for bulk deletion\n\n` + + `Aborting.` + ); + } +} + +/** + * Get max customers from environment or use default + */ +export function getMaxCustomers(): number { + const envValue = process.env['ATMN_NUKE_MAX_CUSTOMERS']; + if (envValue) { + const parsed = parseInt(envValue, 10); + if (!isNaN(parsed) && parsed > 0) { + return parsed; + } + } + return 50; // Default limit +} diff --git a/atmn/src/commands/pull/index.ts b/atmn/src/commands/pull/index.ts new file mode 100644 index 00000000..d7932bcb --- /dev/null +++ b/atmn/src/commands/pull/index.ts @@ -0,0 +1,2 @@ +export { pull } from "./pull.js"; +export type { PullOptions, PullResult, EnvironmentData } from "./types.js"; diff --git a/atmn/src/commands/pull/mergeEnvironments.ts b/atmn/src/commands/pull/mergeEnvironments.ts new file mode 100644 index 00000000..0f38ecf1 --- /dev/null +++ b/atmn/src/commands/pull/mergeEnvironments.ts @@ -0,0 +1,46 @@ +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import type { EnvironmentData } from "./types.js"; + +/** + * Merge sandbox and production data for SDK types generation + * Deduplicates by ID, preferring sandbox definitions + */ +export function mergeEnvironments( + sandbox: EnvironmentData, + production: EnvironmentData, +): EnvironmentData { + // Merge features (dedupe by ID) + const featureMap = new Map(); + + // Add sandbox features first + for (const feature of sandbox.features) { + featureMap.set(feature.id, feature); + } + + // Add production features that don't exist in sandbox + for (const feature of production.features) { + if (!featureMap.has(feature.id)) { + featureMap.set(feature.id, feature); + } + } + + // Merge plans (dedupe by ID) + const planMap = new Map(); + + // Add sandbox plans first + for (const plan of sandbox.plans) { + planMap.set(plan.id, plan); + } + + // Add production plans that don't exist in sandbox + for (const plan of production.plans) { + if (!planMap.has(plan.id)) { + planMap.set(plan.id, plan); + } + } + + return { + features: Array.from(featureMap.values()), + plans: Array.from(planMap.values()), + }; +} diff --git a/atmn/src/commands/pull/pull.ts b/atmn/src/commands/pull/pull.ts new file mode 100644 index 00000000..fceea5d2 --- /dev/null +++ b/atmn/src/commands/pull/pull.ts @@ -0,0 +1,93 @@ +import { AppEnv, getKey, hasKey } from "../../lib/env/index.js"; +import { pullFromEnvironment } from "./pullFromEnvironment.js"; +import { mergeEnvironments } from "./mergeEnvironments.js"; +import { writeConfig } from "./writeConfig.js"; +import { generateSdkTypes } from "./sdkTypes.js"; +import type { PullOptions, PullResult } from "./types.js"; + +/** + * Pull command - fetch config from Autumn API and generate local files + * + * Flow: + * 1. Get key for specified environment (defaults to sandbox) + * 2. Fetch & transform from specified environment + * 3. Write autumn.config.ts + * 4. If generateSdkTypes: + * a. Try get live key (optional) + * b. If live key exists, fetch & transform from live + * c. Merge sandbox + live (dedupe by ID) + * d. Generate @useautumn-sdk.d.ts + */ +export async function pull(options: PullOptions = {}): Promise { + const { + generateSdkTypes: shouldGenerateSdkTypes = false, + cwd = process.cwd(), + environment = AppEnv.Sandbox + } = options; + + // 1. Get key for specified environment + const primaryKey = getKey(environment, cwd); + + // 2. Fetch & transform from specified environment + const primaryData = await pullFromEnvironment(primaryKey); + + // 3. Write autumn.config.ts (using primary environment data) + const configPath = await writeConfig( + primaryData.features, + primaryData.plans, + cwd, + ); + + const result: PullResult = { + features: primaryData.features, + plans: primaryData.plans, + configPath, + }; + + // 4. Generate SDK types if requested + if (shouldGenerateSdkTypes) { + let mergedData = primaryData; + + // If pulling from sandbox, try to merge with live for SDK types + if (environment === AppEnv.Sandbox && hasKey(AppEnv.Live, cwd)) { + try { + const liveKey = getKey(AppEnv.Live, cwd); + const liveData = await pullFromEnvironment(liveKey); + + // Merge sandbox and live + mergedData = mergeEnvironments(primaryData, liveData); + } catch (error) { + console.warn( + "Failed to fetch live data, using sandbox only:", + error, + ); + } + } + // If pulling from live, try to merge with sandbox for SDK types + else if (environment === AppEnv.Live && hasKey(AppEnv.Sandbox, cwd)) { + try { + const sandboxKey = getKey(AppEnv.Sandbox, cwd); + const sandboxData = await pullFromEnvironment(sandboxKey); + + // Merge live and sandbox + mergedData = mergeEnvironments(sandboxData, primaryData); + } catch (error) { + console.warn( + "Failed to fetch sandbox data, using live only:", + error, + ); + } + } + + // Generate SDK types + const sdkTypesPath = await generateSdkTypes({ + features: mergedData.features, + plans: mergedData.plans, + outputDir: cwd, + }); + + result.sdkTypesPath = sdkTypesPath; + } + + return result; +} diff --git a/atmn/src/commands/pull/pullFromEnvironment.ts b/atmn/src/commands/pull/pullFromEnvironment.ts new file mode 100644 index 00000000..568cc1e2 --- /dev/null +++ b/atmn/src/commands/pull/pullFromEnvironment.ts @@ -0,0 +1,25 @@ +import { fetchFeatures, fetchPlans } from "../../lib/api/endpoints/index.js"; +import { + transformApiFeature, + transformApiPlan, +} from "../../lib/transforms/index.js"; +import type { EnvironmentData } from "./types.js"; + +/** + * Fetch and transform data from a single environment + */ +export async function pullFromEnvironment( + secretKey: string, +): Promise { + // Fetch features and plans in parallel + const [apiFeatures, apiPlans] = await Promise.all([ + fetchFeatures({ secretKey }), + fetchPlans({ secretKey, includeArchived: true }), + ]); + + // Transform to SDK types + const features = apiFeatures.map(transformApiFeature); + const plans = apiPlans.map(transformApiPlan); + + return { features, plans }; +} diff --git a/atmn/src/commands/pull/sdkTypes.ts b/atmn/src/commands/pull/sdkTypes.ts new file mode 100644 index 00000000..3172b9d3 --- /dev/null +++ b/atmn/src/commands/pull/sdkTypes.ts @@ -0,0 +1,73 @@ +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import prettier from "prettier"; +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import { featureIdToVarName, planIdToVarName } from "../../lib/transforms/index.js"; + +/** + * Generate SDK types file (@useautumn-sdk.d.ts) + */ +export async function generateSdkTypes(options: { + features: Feature[]; + plans: Plan[]; + outputDir: string; +}): Promise { + const { features, plans, outputDir } = options; + const outputPath = resolve(outputDir, "@useautumn-sdk.d.ts"); + + // Generate type definitions + const sections: string[] = []; + + // Header + sections.push(`// AUTO-GENERATED by atmn pull`); + sections.push(`// DO NOT EDIT MANUALLY`); + sections.push(``); + sections.push(`declare module '@useautumn/sdk' {`); + sections.push(``); + + // Feature types + if (features.length > 0) { + sections.push(` // Features`); + for (const feature of features) { + const varName = featureIdToVarName(feature.id); + sections.push(` export const ${varName}: Feature;`); + } + sections.push(``); + } + + // Plan types + if (plans.length > 0) { + sections.push(` // Plans`); + for (const plan of plans) { + const varName = planIdToVarName(plan.id); + sections.push(` export const ${varName}: Plan;`); + } + sections.push(``); + } + + // Base types (reference autumn.config.ts) + sections.push(` // Base types`); + sections.push(` export type Feature = import('./autumn.config').Feature;`); + sections.push(` export type Plan = import('./autumn.config').Plan;`); + sections.push(`}`); + + const code = sections.join("\n"); + + // Format with prettier + let formattedCode: string; + try { + formattedCode = await prettier.format(code, { + parser: "typescript", + useTabs: false, + singleQuote: true, + }); + } catch (error) { + console.warn("Failed to format SDK types file:", error); + formattedCode = code; + } + + // Write file + writeFileSync(outputPath, formattedCode, "utf-8"); + + return outputPath; +} diff --git a/atmn/src/commands/pull/types.ts b/atmn/src/commands/pull/types.ts new file mode 100644 index 00000000..86385f53 --- /dev/null +++ b/atmn/src/commands/pull/types.ts @@ -0,0 +1,36 @@ +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import type { AppEnv } from "../../lib/env/index.js"; + +/** + * Options for pull command + */ +export interface PullOptions { + /** Whether to generate SDK types (.d.ts) */ + generateSdkTypes?: boolean; + /** Working directory (defaults to process.cwd()) */ + cwd?: string; + /** Environment to pull from (defaults to AppEnv.Sandbox) */ + environment?: AppEnv; +} + +/** + * Result of pull operation + */ +export interface PullResult { + /** Features pulled from sandbox */ + features: Feature[]; + /** Plans pulled from sandbox */ + plans: Plan[]; + /** Path to generated config file */ + configPath: string; + /** Path to generated SDK types file (if generateSdkTypes was true) */ + sdkTypesPath?: string; +} + +/** + * Data fetched and transformed from an environment + */ +export interface EnvironmentData { + features: Feature[]; + plans: Plan[]; +} diff --git a/atmn/src/commands/pull/writeConfig.ts b/atmn/src/commands/pull/writeConfig.ts new file mode 100644 index 00000000..d1214af8 --- /dev/null +++ b/atmn/src/commands/pull/writeConfig.ts @@ -0,0 +1,38 @@ +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import prettier from "prettier"; +import { buildConfigFile } from "../../lib/transforms/index.js"; +import type { Feature, Plan } from "../../../source/compose/models/index.js"; + +/** + * Write autumn.config.ts file with formatting + */ +export async function writeConfig( + features: Feature[], + plans: Plan[], + cwd: string = process.cwd(), +): Promise { + const configPath = resolve(cwd, "autumn.config.ts"); + + // Generate code + const code = buildConfigFile(features, plans); + + // Format with prettier + let formattedCode: string; + try { + formattedCode = await prettier.format(code, { + parser: "typescript", + useTabs: true, + singleQuote: true, + }); + } catch (error) { + // If formatting fails, use unformatted code + console.warn("Failed to format config file:", error); + formattedCode = code; + } + + // Write file + writeFileSync(configPath, formattedCode, "utf-8"); + + return configPath; +} diff --git a/atmn/src/commands/push/headless.ts b/atmn/src/commands/push/headless.ts new file mode 100644 index 00000000..db38da40 --- /dev/null +++ b/atmn/src/commands/push/headless.ts @@ -0,0 +1,567 @@ +import chalk from "chalk"; +import fs from "node:fs"; +import path, { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import createJiti from "jiti"; +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import { AppEnv } from "../../lib/env/index.js"; +import { + analyzePush, + archiveFeature, + archivePlan, + deleteFeature, + deletePlan, + fetchRemoteData, + pushFeature, + pushPlan, + unarchiveFeature, + unarchivePlan, +} from "./push.js"; +import type { PushAnalysis } from "./types.js"; +import { + createFeatureArchivedPrompt, + createFeatureDeletePrompt, + createPlanArchivedPrompt, + createPlanDeletePrompt, + createPlanVersioningPrompt, + createProdConfirmationPrompt, + type PushPrompt, +} from "./prompts.js"; +import { validateConfig, formatValidationErrors } from "./validate.js"; + +interface LocalConfig { + features: Feature[]; + plans: Plan[]; +} + +interface HeadlessPushOptions { + cwd?: string; + environment?: AppEnv; + yes?: boolean; +} + +interface HeadlessPushResult { + success: boolean; + featuresCreated: string[]; + featuresUpdated: string[]; + featuresDeleted: string[]; + featuresArchived: string[]; + plansCreated: string[]; + plansUpdated: string[]; + plansDeleted: string[]; + plansArchived: string[]; +} + +/** + * Load local config file using jiti + */ +async function loadLocalConfig(cwd: string): Promise { + const configPath = path.join(cwd, "autumn.config.ts"); + + if (!fs.existsSync(configPath)) { + throw new Error( + `Config file not found at ${configPath}. Run 'atmn pull' first.`, + ); + } + + const absolutePath = resolve(configPath); + const fileUrl = pathToFileURL(absolutePath).href; + + const jiti = createJiti(import.meta.url); + const mod = await jiti.import(fileUrl); + + const plans: Plan[] = []; + const features: Feature[] = []; + + const modRecord = mod as { default?: unknown } & Record; + const defaultExport = modRecord.default as + | { + plans?: Plan[]; + features?: Feature[]; + products?: Plan[]; + } + | undefined; + + if (defaultExport?.plans && defaultExport?.features) { + if (Array.isArray(defaultExport.plans)) { + plans.push(...defaultExport.plans); + } + if (Array.isArray(defaultExport.features)) { + features.push(...defaultExport.features); + } + } else if (defaultExport?.products && defaultExport?.features) { + // Legacy format + if (Array.isArray(defaultExport.products)) { + plans.push(...defaultExport.products); + } + if (Array.isArray(defaultExport.features)) { + features.push(...defaultExport.features); + } + } else { + // New format: individual named exports + for (const [key, value] of Object.entries(modRecord)) { + if (key === "default") continue; + + const obj = value as { features?: unknown; type?: unknown }; + if (obj && typeof obj === "object") { + if (Array.isArray(obj.features)) { + plans.push(obj as unknown as Plan); + } else if ("type" in obj) { + features.push(obj as unknown as Feature); + } + } + } + } + + return { features, plans }; +} + +/** + * Build the list of prompts that would be shown in interactive mode + */ +function buildPromptQueue( + analysis: PushAnalysis, + environment: AppEnv, +): PushPrompt[] { + const prompts: PushPrompt[] = []; + + // Production confirmation + if (environment === AppEnv.Live) { + prompts.push(createProdConfirmationPrompt()); + } + + // Archived features + for (const feature of analysis.archivedFeatures) { + prompts.push(createFeatureArchivedPrompt(feature)); + } + + // Archived plans + for (const plan of analysis.archivedPlans) { + prompts.push(createPlanArchivedPrompt(plan)); + } + + // Plans that will version + for (const planInfo of analysis.plansToUpdate) { + if (planInfo.willVersion) { + prompts.push(createPlanVersioningPrompt(planInfo)); + } + } + + // Feature deletions + for (const info of analysis.featuresToDelete) { + prompts.push(createFeatureDeletePrompt(info)); + } + + // Plan deletions + for (const info of analysis.plansToDelete) { + prompts.push(createPlanDeletePrompt(info)); + } + + return prompts; +} + +/** + * Format a human-readable description of the issues that require confirmation + */ +function formatIssuesSummary(prompts: PushPrompt[]): string { + const issues: string[] = []; + + for (const prompt of prompts) { + switch (prompt.type) { + case "prod_confirmation": + issues.push(" - Pushing to production environment"); + break; + case "plan_versioning": + issues.push( + ` - Plan "${prompt.entityId}" has customers and will create a new version`, + ); + break; + case "plan_delete_has_customers": + issues.push( + ` - Plan "${prompt.entityId}" needs to be removed but has customers`, + ); + break; + case "plan_delete_no_customers": + issues.push(` - Plan "${prompt.entityId}" will be deleted`); + break; + case "plan_archived": + issues.push(` - Plan "${prompt.entityId}" is archived and needs to be un-archived`); + break; + case "feature_delete_credit_system": + issues.push( + ` - Feature "${prompt.entityId}" is used by credit systems and cannot be deleted`, + ); + break; + case "feature_delete_products": + issues.push( + ` - Feature "${prompt.entityId}" is used by products and cannot be deleted`, + ); + break; + case "feature_delete_no_deps": + issues.push(` - Feature "${prompt.entityId}" will be deleted`); + break; + case "feature_archived": + issues.push( + ` - Feature "${prompt.entityId}" is archived and needs to be un-archived`, + ); + break; + } + } + + return issues.join("\n"); +} + +/** + * Execute the push with --yes flag (auto-confirm all prompts with defaults) + */ +async function executePushWithDefaults( + config: LocalConfig, + analysis: PushAnalysis, + prompts: PushPrompt[], +): Promise { + const result: HeadlessPushResult = { + success: true, + featuresCreated: [], + featuresUpdated: [], + featuresDeleted: [], + featuresArchived: [], + plansCreated: [], + plansUpdated: [], + plansDeleted: [], + plansArchived: [], + }; + + // Build response map from defaults + const responses = new Map(); + for (const prompt of prompts) { + if (prompt.type === "prod_confirmation") { + responses.set(prompt.id, "confirm"); + continue; + } + const defaultOption = prompt.options.find((o) => o.isDefault); + responses.set( + prompt.id, + defaultOption?.value || prompt.options[0]?.value || "confirm", + ); + } + + // Get remote plans for pushPlan + const remoteData = await fetchRemoteData(); + const remotePlans = remoteData.plans; + + // Handle archived features - unarchive if default says so + for (const feature of analysis.archivedFeatures) { + const promptId = prompts.find( + (p) => p.type === "feature_archived" && p.entityId === feature.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + if (response === "unarchive") { + console.log(chalk.dim(` Un-archiving feature: ${feature.id}`)); + await unarchiveFeature(feature.id); + } + } + + // Push features + const allFeatures = config.features; + for (const feature of allFeatures) { + const isArchived = analysis.archivedFeatures.some( + (af) => af.id === feature.id, + ); + if (isArchived) { + const promptId = prompts.find( + (p) => p.type === "feature_archived" && p.entityId === feature.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + if (response === "skip") { + continue; + } + } + + const pushResult = await pushFeature(feature); + if (pushResult.action === "created") { + result.featuresCreated.push(feature.id); + } else { + result.featuresUpdated.push(feature.id); + } + } + + // Handle archived plans - unarchive if default says so + for (const plan of analysis.archivedPlans) { + const promptId = prompts.find( + (p) => p.type === "plan_archived" && p.entityId === plan.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + if (response === "unarchive") { + console.log(chalk.dim(` Un-archiving plan: ${plan.id}`)); + await unarchivePlan(plan.id); + } + } + + // Push plans to create + for (const plan of analysis.plansToCreate) { + await pushPlan(plan, remotePlans); + result.plansCreated.push(plan.id); + } + + // Push plans to update + for (const planInfo of analysis.plansToUpdate) { + if (planInfo.willVersion) { + const promptId = prompts.find( + (p) => p.type === "plan_versioning" && p.entityId === planInfo.plan.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + if (response === "skip") { + continue; + } + } + + if (planInfo.isArchived) { + const promptId = prompts.find( + (p) => p.type === "plan_archived" && p.entityId === planInfo.plan.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + if (response === "skip") { + continue; + } + } + + await pushPlan(planInfo.plan, remotePlans); + result.plansUpdated.push(planInfo.plan.id); + } + + // Handle feature deletions + for (const info of analysis.featuresToDelete) { + const promptId = prompts.find( + (p) => p.type.startsWith("feature_delete") && p.entityId === info.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + + if (response === "delete") { + console.log(chalk.dim(` Deleting feature: ${info.id}`)); + await deleteFeature(info.id); + result.featuresDeleted.push(info.id); + } else if (response === "archive") { + console.log(chalk.dim(` Archiving feature: ${info.id}`)); + await archiveFeature(info.id); + result.featuresArchived.push(info.id); + } + // skip = do nothing + } + + // Handle plan deletions + for (const info of analysis.plansToDelete) { + const promptId = prompts.find( + (p) => p.type.startsWith("plan_delete") && p.entityId === info.id, + )?.id; + const response = promptId ? responses.get(promptId) : undefined; + + if (response === "delete") { + console.log(chalk.dim(` Deleting plan: ${info.id}`)); + await deletePlan(info.id); + result.plansDeleted.push(info.id); + } else if (response === "archive") { + console.log(chalk.dim(` Archiving plan: ${info.id}`)); + await archivePlan(info.id); + result.plansArchived.push(info.id); + } + // skip = do nothing + } + + return result; +} + +/** + * Execute a clean push (no edge cases, no prompts needed) + */ +async function executeCleanPush( + config: LocalConfig, + analysis: PushAnalysis, +): Promise { + const result: HeadlessPushResult = { + success: true, + featuresCreated: [], + featuresUpdated: [], + featuresDeleted: [], + featuresArchived: [], + plansCreated: [], + plansUpdated: [], + plansDeleted: [], + plansArchived: [], + }; + + const remoteData = await fetchRemoteData(); + const remotePlans = remoteData.plans; + + // Push all features + for (const feature of config.features) { + const pushResult = await pushFeature(feature); + if (pushResult.action === "created") { + result.featuresCreated.push(feature.id); + } else { + result.featuresUpdated.push(feature.id); + } + } + + // Push plans to create + for (const plan of analysis.plansToCreate) { + await pushPlan(plan, remotePlans); + result.plansCreated.push(plan.id); + } + + // Push plans to update (no versioning issues since prompts.length === 0) + for (const planInfo of analysis.plansToUpdate) { + await pushPlan(planInfo.plan, remotePlans); + result.plansUpdated.push(planInfo.plan.id); + } + + return result; +} + +/** + * Headless push command - uses V2 logic without interactive prompts + * + * If any edge cases require user decisions and --yes is not set, + * exits with a helpful message instructing the user to either: + * - Run in an interactive terminal + * - Use the --yes flag to auto-confirm with defaults + */ +export async function headlessPush( + options: HeadlessPushOptions = {}, +): Promise { + const cwd = options.cwd ?? process.cwd(); + const environment = options.environment ?? AppEnv.Sandbox; + const yes = options.yes ?? false; + + const envLabel = environment === AppEnv.Live ? "production" : "sandbox"; + + // Load config + console.log(chalk.dim(`Loading autumn.config.ts...`)); + const config = await loadLocalConfig(cwd); + console.log( + chalk.dim( + ` Found ${config.features.length} features, ${config.plans.length} plans`, + ), + ); + + // Validate config for missing required fields + console.log(chalk.dim(`Validating config...`)); + const validation = validateConfig(config.features, config.plans); + if (!validation.valid) { + console.log(chalk.red("\nConfig validation failed:\n")); + console.log(chalk.yellow(formatValidationErrors(validation.errors))); + process.exit(1); + } + + // Analyze changes + console.log(chalk.dim(`Analyzing changes against ${envLabel}...`)); + const analysis = await analyzePush(config.features, config.plans); + + // Check if there are any changes + const hasVersioningPlans = analysis.plansToUpdate.some((p) => p.willVersion); + const hasChanges = + analysis.featuresToCreate.length > 0 || + analysis.featuresToDelete.length > 0 || + analysis.plansToCreate.length > 0 || + analysis.plansToDelete.length > 0 || + analysis.archivedFeatures.length > 0 || + analysis.archivedPlans.length > 0 || + hasVersioningPlans; + + if (!hasChanges) { + console.log(chalk.green("\nAlready in sync - no changes to push.")); + return { + success: true, + featuresCreated: [], + featuresUpdated: [], + featuresDeleted: [], + featuresArchived: [], + plansCreated: [], + plansUpdated: [], + plansDeleted: [], + plansArchived: [], + }; + } + + // Build prompt queue to check for edge cases + const prompts = buildPromptQueue(analysis, environment); + + // If there are prompts and --yes is not set, exit with helpful message + if (prompts.length > 0 && !yes) { + console.log(chalk.yellow("\nPush requires confirmation for the following:")); + console.log(formatIssuesSummary(prompts)); + console.log(""); + console.log(chalk.cyan("To proceed, either:")); + console.log( + chalk.white( + " 1. Run this command in an interactive terminal to review and confirm each action", + ), + ); + console.log( + chalk.white( + " 2. Run with --yes to automatically proceed with default actions", + ), + ); + console.log(""); + + // Exit with non-zero to indicate action required + process.exit(1); + } + + // Execute the push + console.log(chalk.dim(`\nPushing to ${envLabel}...`)); + + let result: HeadlessPushResult; + if (prompts.length > 0) { + // --yes was set, execute with defaults + result = await executePushWithDefaults(config, analysis, prompts); + } else { + // No edge cases, clean push + result = await executeCleanPush(config, analysis); + } + + // Print summary + console.log(chalk.green(`\nPush complete!`)); + + if (result.featuresCreated.length > 0) { + console.log( + chalk.dim(` Features created: ${result.featuresCreated.join(", ")}`), + ); + } + if (result.featuresUpdated.length > 0) { + console.log( + chalk.dim(` Features updated: ${result.featuresUpdated.join(", ")}`), + ); + } + if (result.featuresDeleted.length > 0) { + console.log( + chalk.dim(` Features deleted: ${result.featuresDeleted.join(", ")}`), + ); + } + if (result.featuresArchived.length > 0) { + console.log( + chalk.dim(` Features archived: ${result.featuresArchived.join(", ")}`), + ); + } + if (result.plansCreated.length > 0) { + console.log( + chalk.dim(` Plans created: ${result.plansCreated.join(", ")}`), + ); + } + if (result.plansUpdated.length > 0) { + console.log( + chalk.dim(` Plans updated: ${result.plansUpdated.join(", ")}`), + ); + } + if (result.plansDeleted.length > 0) { + console.log( + chalk.dim(` Plans deleted: ${result.plansDeleted.join(", ")}`), + ); + } + if (result.plansArchived.length > 0) { + console.log( + chalk.dim(` Plans archived: ${result.plansArchived.join(", ")}`), + ); + } + + return result; +} diff --git a/atmn/src/commands/push/index.ts b/atmn/src/commands/push/index.ts new file mode 100644 index 00000000..ea49680e --- /dev/null +++ b/atmn/src/commands/push/index.ts @@ -0,0 +1,41 @@ +export { + analyzePush, + archiveFeature, + archivePlan, + deleteFeature, + deletePlan, + fetchRemoteData, + pushFeature, + pushPlan, + unarchiveFeature, + unarchivePlan, +} from "./push.js"; + +export type { + FeatureDeleteInfo, + PlanDeleteInfo, + PlanUpdateInfo, + PushAnalysis, + PushResult, + RemoteData, +} from "./types.js"; + +export { + createFeatureArchivedPrompt, + createFeatureDeletePrompt, + createPlanArchivedPrompt, + createPlanDeletePrompt, + createPlanVersioningPrompt, + createProdConfirmationPrompt, + type PushPrompt, + type PromptType, +} from "./prompts.js"; + +export { headlessPush } from "./headless.js"; + +export { + validateConfig, + formatValidationErrors, + type ValidationError, + type ValidationResult, +} from "./validate.js"; diff --git a/atmn/src/commands/push/prompts.ts b/atmn/src/commands/push/prompts.ts new file mode 100644 index 00000000..73d1e3d6 --- /dev/null +++ b/atmn/src/commands/push/prompts.ts @@ -0,0 +1,277 @@ +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import type { + FeatureDeleteInfo, + PlanDeleteInfo, + PlanUpdateInfo, +} from "./types.js"; + +/** + * Types for push prompts + */ + +export type PromptType = + | "prod_confirmation" + | "plan_versioning" + | "plan_delete_has_customers" + | "plan_delete_no_customers" + | "plan_archived" + | "feature_delete_credit_system" + | "feature_delete_products" + | "feature_delete_no_deps" + | "feature_archived"; + +export interface PromptOption { + label: string; + value: string; + isDefault?: boolean; +} + +export interface PushPrompt { + id: string; + type: PromptType; + entityId: string; + entityName: string; + data: Record; + options: PromptOption[]; +} + +// Counter for unique prompt IDs +let promptCounter = 0; + +function generatePromptId(): string { + return `prompt_${++promptCounter}`; +} + +/** + * Create production confirmation prompt + */ +export function createProdConfirmationPrompt(): PushPrompt { + return { + id: generatePromptId(), + type: "prod_confirmation", + entityId: "production", + entityName: "Production Environment", + data: {}, + options: [ + { label: "Yes, I understand", value: "confirm", isDefault: false }, + { label: "No, cancel", value: "cancel", isDefault: true }, + ], + }; +} + +/** + * Create plan versioning prompt + */ +export function createPlanVersioningPrompt(info: PlanUpdateInfo): PushPrompt { + return { + id: generatePromptId(), + type: "plan_versioning", + entityId: info.plan.id, + entityName: info.plan.name, + data: { + planId: info.plan.id, + planName: info.plan.name, + }, + options: [ + { + label: "Yes, create new version", + value: "version", + isDefault: true, + }, + { label: "No, skip this plan", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create plan delete prompt when plan has customers + */ +export function createPlanDeleteHasCustomersPrompt( + info: PlanDeleteInfo, +): PushPrompt { + return { + id: generatePromptId(), + type: "plan_delete_has_customers", + entityId: info.id, + entityName: info.id, + data: { + planId: info.id, + customerCount: info.customerCount, + firstCustomerName: info.firstCustomerName || "Unknown Customer", + }, + options: [ + { label: "Archive instead", value: "archive", isDefault: true }, + { label: "Skip (keep as is)", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create plan delete prompt when plan has no customers + */ +export function createPlanDeleteNoCustomersPrompt( + info: PlanDeleteInfo, +): PushPrompt { + return { + id: generatePromptId(), + type: "plan_delete_no_customers", + entityId: info.id, + entityName: info.id, + data: { + planId: info.id, + }, + options: [ + { label: "Delete permanently", value: "delete", isDefault: true }, + { label: "Archive instead", value: "archive", isDefault: false }, + { label: "Skip (keep as is)", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create plan archived prompt + */ +export function createPlanArchivedPrompt(plan: Plan): PushPrompt { + return { + id: generatePromptId(), + type: "plan_archived", + entityId: plan.id, + entityName: plan.name, + data: { + planId: plan.id, + planName: plan.name, + }, + options: [ + { label: "Un-archive and push", value: "unarchive", isDefault: true }, + { label: "Skip this plan", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create feature delete prompt when feature is used by credit system + */ +export function createFeatureDeleteCreditSystemPrompt( + info: FeatureDeleteInfo, +): PushPrompt { + const creditSystems = info.referencingCreditSystems || []; + const firstCreditSystem = creditSystems[0] || "Unknown"; + + return { + id: generatePromptId(), + type: "feature_delete_credit_system", + entityId: info.id, + entityName: info.id, + data: { + featureId: info.id, + creditSystems, + firstCreditSystem, + creditSystemCount: creditSystems.length, + }, + options: [ + { label: "Archive instead", value: "archive", isDefault: true }, + { label: "Skip (keep as is)", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create feature delete prompt when feature is used by products + */ +export function createFeatureDeleteProductsPrompt( + info: FeatureDeleteInfo, +): PushPrompt { + const products = info.referencingProducts || { name: "Unknown", count: 1 }; + + return { + id: generatePromptId(), + type: "feature_delete_products", + entityId: info.id, + entityName: info.id, + data: { + featureId: info.id, + productName: products.name, + productCount: products.count, + }, + options: [ + { label: "Archive instead", value: "archive", isDefault: true }, + { label: "Skip (keep as is)", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create feature delete prompt when feature has no dependencies + */ +export function createFeatureDeleteNoDepsPrompt( + info: FeatureDeleteInfo, +): PushPrompt { + return { + id: generatePromptId(), + type: "feature_delete_no_deps", + entityId: info.id, + entityName: info.id, + data: { + featureId: info.id, + }, + options: [ + { label: "Delete permanently", value: "delete", isDefault: true }, + { label: "Archive instead", value: "archive", isDefault: false }, + { label: "Skip (keep as is)", value: "skip", isDefault: false }, + ], + }; +} + +/** + * Create feature archived prompt + */ +export function createFeatureArchivedPrompt(feature: Feature): PushPrompt { + return { + id: generatePromptId(), + type: "feature_archived", + entityId: feature.id, + entityName: feature.name, + data: { + featureId: feature.id, + featureName: feature.name, + }, + options: [ + { label: "Un-archive and push", value: "unarchive", isDefault: true }, + { + label: "Skip this feature", + value: "skip", + isDefault: false, + }, + ], + }; +} + +/** + * Create appropriate delete prompt based on feature delete info + */ +export function createFeatureDeletePrompt(info: FeatureDeleteInfo): PushPrompt { + if (info.reason === "credit_system") { + return createFeatureDeleteCreditSystemPrompt(info); + } + if (info.reason === "products") { + return createFeatureDeleteProductsPrompt(info); + } + return createFeatureDeleteNoDepsPrompt(info); +} + +/** + * Create appropriate delete prompt based on plan delete info + */ +export function createPlanDeletePrompt(info: PlanDeleteInfo): PushPrompt { + if (info.customerCount > 0) { + return createPlanDeleteHasCustomersPrompt(info); + } + return createPlanDeleteNoCustomersPrompt(info); +} + +/** + * Reset prompt counter (useful for testing) + */ +export function resetPromptCounter(): void { + promptCounter = 0; +} diff --git a/atmn/src/commands/push/push.ts b/atmn/src/commands/push/push.ts new file mode 100644 index 00000000..7adecfd9 --- /dev/null +++ b/atmn/src/commands/push/push.ts @@ -0,0 +1,488 @@ +// @ts-nocheck - Using ts-nocheck due to complex Record index signature issues +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import { + fetchFeatures, + fetchPlans, + upsertFeature, + updateFeature, + deleteFeature as deleteFeatureApi, + archiveFeature as archiveFeatureApi, + unarchiveFeature as unarchiveFeatureApi, + getFeatureDeletionInfo, + createPlan, + updatePlan, + deletePlan as deletePlanApi, + archivePlan as archivePlanApi, + unarchivePlan as unarchivePlanApi, + getPlanDeletionInfo, + getPlanHasCustomers, +} from "../../lib/api/endpoints/index.js"; +import { getKey } from "../../lib/env/index.js"; +import { AppEnv } from "../../lib/env/index.js"; +import { isProd } from "../../lib/env/cliContext.js"; +import type { + FeatureDeleteInfo, + PlanDeleteInfo, + PlanUpdateInfo, + PushAnalysis, + RemoteData, +} from "./types.js"; + +/** + * Get the secret key for the current environment + */ +function getSecretKey(): string { + const environment = isProd() ? AppEnv.Live : AppEnv.Sandbox; + return getKey(environment); +} + +/** + * Core push logic - pure functions with no UI/prompts + * All functions throw on error for proper React error handling + */ + +// Fetch all features and plans from remote +export async function fetchRemoteData(): Promise { + const secretKey = getSecretKey(); + + const [features, plans] = await Promise.all([ + fetchFeatures({ secretKey, includeArchived: true }), + fetchPlans({ secretKey, includeArchived: true }), + ]); + + return { + features: features as Feature[], + plans: plans as Plan[], + }; +} + +// Check if a feature can be deleted +async function checkFeatureDeleteInfo( + featureId: string, + localFeatures: Feature[], + remoteFeatures: Feature[], +): Promise { + const secretKey = getSecretKey(); + + // Get the feature type from remote for sorting purposes + const remoteFeature = remoteFeatures.find((f) => f.id === featureId); + const featureType = remoteFeature?.type as + | "boolean" + | "metered" + | "credit_system" + | undefined; + + // Check locally if this feature is referenced by any credit system in the config + const referencingCreditSystems = localFeatures.filter( + (f) => + f.type === "credit_system" && + f.credit_schema?.some((cs) => cs.metered_feature_id === featureId), + ); + + if (referencingCreditSystems.length >= 1) { + return { + id: featureId, + canDelete: false, + reason: "credit_system", + referencingCreditSystems: referencingCreditSystems.map((f) => f.id), + featureType, + }; + } + + // Check API for product references + const response = await getFeatureDeletionInfo({ secretKey, featureId }); + + if (response && response.totalCount > 0) { + return { + id: featureId, + canDelete: false, + reason: "products", + referencingProducts: { + name: response.productName || "Unknown Product", + count: response.totalCount, + }, + featureType, + }; + } + + return { + id: featureId, + canDelete: true, + featureType, + }; +} + +// Check if a plan can be deleted +async function checkPlanDeleteInfo(planId: string): Promise { + const secretKey = getSecretKey(); + const response = await getPlanDeletionInfo({ secretKey, planId }); + + if (response && response.totalCount > 0) { + return { + id: planId, + canDelete: false, + customerCount: response.totalCount, + firstCustomerName: response.customerName, + }; + } + + return { + id: planId, + canDelete: true, + customerCount: 0, + }; +} + +// Check if updating a plan will create a new version +async function checkPlanForVersioning( + plan: Plan, + remotePlans: Plan[], +): Promise { + const secretKey = getSecretKey(); + const remotePlan = remotePlans.find((p) => p.id === plan.id); + + if (!remotePlan) { + return { + plan, + willVersion: false, + isArchived: false, + }; + } + + const response = await getPlanHasCustomers({ secretKey, planId: plan.id }); + + return { + plan, + willVersion: response.will_version || false, + isArchived: response.archived || false, + }; +} + +/** + * Analyze what changes need to be pushed + */ +export async function analyzePush( + localFeatures: Feature[], + localPlans: Plan[], +): Promise { + const remoteData = await fetchRemoteData(); + + const localFeatureIds = new Set(localFeatures.map((f) => f.id)); + const localPlanIds = new Set(localPlans.map((p) => p.id)); + const remoteFeatureIds = new Set(remoteData.features.map((f) => f.id)); + const remotePlanIds = new Set(remoteData.plans.map((p) => p.id)); + + // Find features to create, update, and delete + const featuresToCreate = localFeatures.filter( + (f) => !remoteFeatureIds.has(f.id), + ); + const featuresToUpdate = localFeatures.filter((f) => + remoteFeatureIds.has(f.id), + ); + + // Find features that exist remotely but not locally (potential deletes) + // Exclude already archived features + const featureIdsToDelete = remoteData.features + .filter( + (f) => + !localFeatureIds.has(f.id) && + !(f as Feature & { archived?: boolean }).archived, + ) + .map((f) => f.id); + + // Check deletion info for each feature + const featureDeletePromises = featureIdsToDelete.map((id) => + checkFeatureDeleteInfo(id, localFeatures, remoteData.features), + ); + const featuresToDeleteUnsorted = await Promise.all(featureDeletePromises); + + // Sort features to delete: credit systems first to prevent dependency issues + const featuresToDelete = featuresToDeleteUnsorted.sort((a, b) => { + if (a.featureType === "credit_system" && b.featureType !== "credit_system") { + return -1; + } + if (a.featureType !== "credit_system" && b.featureType === "credit_system") { + return 1; + } + return 0; + }); + + // Find archived features in local config + const archivedFeatures = localFeatures.filter((f) => { + const remote = remoteData.features.find((rf) => rf.id === f.id); + return remote && (remote as Feature & { archived?: boolean }).archived; + }); + + // Find plans to create, update, and delete + const plansToCreate = localPlans.filter((p) => !remotePlanIds.has(p.id)); + const plansToUpdateLocal = localPlans.filter((p) => remotePlanIds.has(p.id)); + + // Check versioning info for each plan to update + const planUpdatePromises = plansToUpdateLocal.map((plan) => + checkPlanForVersioning(plan, remoteData.plans), + ); + const plansToUpdate = await Promise.all(planUpdatePromises); + + // Find plans that exist remotely but not locally (potential deletes) + const planIdsToDelete = remoteData.plans + .filter( + (p) => + !localPlanIds.has(p.id) && + !(p as Plan & { archived?: boolean }).archived, + ) + .map((p) => p.id); + + // Check deletion info for each plan + const planDeletePromises = planIdsToDelete.map((id) => + checkPlanDeleteInfo(id), + ); + const plansToDelete = await Promise.all(planDeletePromises); + + // Find archived plans in local config + const archivedPlans = localPlans.filter((p) => { + const remote = remoteData.plans.find((rp) => rp.id === p.id); + return remote && (remote as Plan & { archived?: boolean }).archived; + }); + + return { + featuresToCreate, + featuresToUpdate, + featuresToDelete, + plansToCreate, + plansToUpdate, + plansToDelete, + archivedFeatures, + archivedPlans, + }; +} + +/** + * Transform plan data for API submission. + * Maps SDK field names to API field names. + */ +function transformPlanForApi(plan: Plan): Record { + const transformed = { ...plan } as Record; + + // 'auto_enable' -> 'default' + if ("auto_enable" in plan) { + transformed.default = ( + plan as Plan & { auto_enable?: boolean } + ).auto_enable; + delete transformed.auto_enable; + } + + // Transform features array + if (plan.features && Array.isArray(plan.features)) { + transformed.features = plan.features.map((feature) => { + const transformedFeature = { ...feature } as Record; + + // 'included' -> 'granted_balance' + if ("included" in feature && feature.included !== undefined) { + transformedFeature.granted_balance = feature.included; + delete transformedFeature.included; + } + + // Transform flattened reset fields to nested reset object + const featureAny = feature as Record; + if ( + "interval" in featureAny || + "interval_count" in featureAny || + "carry_over_usage" in featureAny + ) { + const reset: Record = {}; + + if ("interval" in featureAny && featureAny.interval !== undefined) { + reset.interval = featureAny.interval; + delete transformedFeature.interval; + } + + if ( + "interval_count" in featureAny && + featureAny.interval_count !== undefined + ) { + reset.interval_count = featureAny.interval_count; + delete transformedFeature.interval_count; + } + + // SDK: carry_over_usage (true = keep existing) -> API: reset_when_enabled (true = reset on enable) + if ( + "carry_over_usage" in featureAny && + featureAny.carry_over_usage !== undefined + ) { + reset.reset_when_enabled = !featureAny.carry_over_usage; + delete transformedFeature.carry_over_usage; + } + + if (Object.keys(reset).length > 0) { + transformedFeature.reset = reset; + } + } + + // Transform nested price object: 'billing_method' -> 'usage_model' + if ( + "price" in feature && + feature.price && + typeof feature.price === "object" + ) { + const price = feature.price as Record; + const transformedPrice = { ...price }; + + if ("billing_method" in price) { + transformedPrice.usage_model = price.billing_method; + delete transformedPrice.billing_method; + } + + // Copy interval to price from reset if needed + const resetObj = transformedFeature.reset as + | Record + | undefined; + if (resetObj?.interval) { + transformedPrice.interval = resetObj.interval; + if (resetObj.interval_count) { + transformedPrice.interval_count = resetObj.interval_count; + } + } + + transformedFeature.price = transformedPrice; + } + + return transformedFeature; + }); + } + + return transformed; +} + +/** + * Push a single feature (create or update) + */ +export async function pushFeature( + feature: Feature, +): Promise<{ action: "created" | "updated" }> { + const secretKey = getSecretKey(); + + try { + await upsertFeature({ + secretKey, + feature: feature as Record, + }); + return { action: "created" }; + } catch (error: unknown) { + const apiError = error as { + response?: { code?: string }; + }; + if ( + apiError.response?.code === "duplicate_feature_id" || + apiError.response?.code === "product_already_exists" + ) { + await updateFeature({ + secretKey, + featureId: feature.id, + feature: feature as Record, + }); + return { action: "updated" }; + } + throw error; + } +} + +/** + * Push a single plan (create or update) + */ +export async function pushPlan( + plan: Plan, + remotePlans: Plan[], +): Promise<{ action: "created" | "updated" | "versioned" }> { + const secretKey = getSecretKey(); + const remotePlan = remotePlans.find((p) => p.id === plan.id); + const apiPlan = transformPlanForApi(plan); + + if (!remotePlan) { + await createPlan({ secretKey, plan: apiPlan }); + return { action: "created" }; + } + + // Prepare update payload with swapNullish/swapFalse logic + const updatePayload = { ...apiPlan }; + + // Handle swapNullish for group field + if ( + plan.group === undefined && + remotePlan.group !== undefined && + remotePlan.group !== null + ) { + updatePayload.group = null; + } else if ( + plan.group === null && + remotePlan.group !== undefined && + remotePlan.group !== null + ) { + updatePayload.group = null; + } + + // Handle swapFalse for add_on field + if (plan.add_on === undefined && remotePlan.add_on === true) { + updatePayload.add_on = false; + } + + // Handle swapFalse for auto_enable (maps to 'default' in API) + if ( + (plan as Plan & { auto_enable?: boolean }).auto_enable === undefined && + (remotePlan as Plan & { default?: boolean }).default === true + ) { + updatePayload.default = false; + } + + await updatePlan({ secretKey, planId: plan.id, plan: updatePayload }); + + // We don't know if it actually versioned here, caller should track based on analysis + return { action: "updated" }; +} + +/** + * Delete a feature + */ +export async function deleteFeature(featureId: string): Promise { + const secretKey = getSecretKey(); + await deleteFeatureApi({ secretKey, featureId }); +} + +/** + * Archive a feature + */ +export async function archiveFeature(featureId: string): Promise { + const secretKey = getSecretKey(); + await archiveFeatureApi({ secretKey, featureId }); +} + +/** + * Un-archive a feature + */ +export async function unarchiveFeature(featureId: string): Promise { + const secretKey = getSecretKey(); + await unarchiveFeatureApi({ secretKey, featureId }); +} + +/** + * Delete a plan + */ +export async function deletePlan(planId: string): Promise { + const secretKey = getSecretKey(); + await deletePlanApi({ secretKey, planId, allVersions: true }); +} + +/** + * Archive a plan + */ +export async function archivePlan(planId: string): Promise { + const secretKey = getSecretKey(); + await archivePlanApi({ secretKey, planId }); +} + +/** + * Un-archive a plan + */ +export async function unarchivePlan(planId: string): Promise { + const secretKey = getSecretKey(); + await unarchivePlanApi({ secretKey, planId }); +} diff --git a/atmn/src/commands/push/types.ts b/atmn/src/commands/push/types.ts new file mode 100644 index 00000000..232fdb30 --- /dev/null +++ b/atmn/src/commands/push/types.ts @@ -0,0 +1,63 @@ +import type { Feature, Plan } from "../../../source/compose/models/index.js"; + +/** + * Types for the push command + */ + +// Feature delete info from API +export interface FeatureDeleteInfo { + id: string; + canDelete: boolean; + reason?: "credit_system" | "products"; + referencingCreditSystems?: string[]; // IDs of credit systems using this feature + referencingProducts?: { name: string; count: number }; + featureType?: "boolean" | "metered" | "credit_system"; // For sorting (delete credit systems first) +} + +// Plan delete info from API +export interface PlanDeleteInfo { + id: string; + canDelete: boolean; + customerCount: number; + firstCustomerName?: string; +} + +// Plan update info +export interface PlanUpdateInfo { + plan: Plan; + willVersion: boolean; + isArchived: boolean; +} + +// Analysis result +export interface PushAnalysis { + featuresToCreate: Feature[]; + featuresToUpdate: Feature[]; + featuresToDelete: FeatureDeleteInfo[]; + plansToCreate: Plan[]; + plansToUpdate: PlanUpdateInfo[]; + plansToDelete: PlanDeleteInfo[]; + archivedFeatures: Feature[]; // Features in local config that are archived remotely + archivedPlans: Plan[]; // Plans in local config that are archived remotely +} + +// Push result +export interface PushResult { + featuresCreated: string[]; + featuresUpdated: string[]; + featuresDeleted: string[]; + featuresArchived: string[]; + featuresSkipped: string[]; + plansCreated: string[]; + plansUpdated: string[]; + plansVersioned: string[]; // Plans that created a new version + plansDeleted: string[]; + plansArchived: string[]; + plansSkipped: string[]; +} + +// Remote data fetched during analysis +export interface RemoteData { + features: Feature[]; + plans: Plan[]; +} diff --git a/atmn/src/commands/push/validate.ts b/atmn/src/commands/push/validate.ts new file mode 100644 index 00000000..85574dc0 --- /dev/null +++ b/atmn/src/commands/push/validate.ts @@ -0,0 +1,216 @@ +import type { Feature, Plan, PlanFeature } from "../../../source/compose/models/index.js"; + +/** + * Validation errors with user-friendly messages. + */ +export interface ValidationError { + path: string; + message: string; +} + +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; +} + +/** + * Validate a plan feature has all required fields. + */ +function validatePlanFeature( + feature: PlanFeature, + planId: string, + featureIndex: number, +): ValidationError[] { + const errors: ValidationError[] = []; + const featureId = feature.feature_id || `(no feature_id)`; + const basePath = `plan "${planId}" → features[${featureIndex}] (${featureId})`; + + // If price is defined, validate required price fields + if (feature.price) { + // billing_method is required when price is defined + if (!feature.price.billing_method) { + errors.push({ + path: `${basePath} → price`, + message: `"billing_method" is required when "price" is defined. Must be "prepaid" or "pay_per_use".`, + }); + } + + // If price has amount or tiers, interval must be defined at the planFeature level + if ((feature.price.amount !== undefined || feature.price.tiers) && !feature.interval) { + errors.push({ + path: basePath, + message: `"interval" is required at the planFeature level when pricing is defined (e.g., interval: "month").`, + }); + } + } + + // If interval is defined, it should be a valid value + if (feature.interval) { + const validIntervals = ["one_off", "minute", "hour", "day", "week", "month", "quarter", "year"]; + if (!validIntervals.includes(feature.interval)) { + errors.push({ + path: `${basePath} → interval`, + message: `Invalid interval "${feature.interval}". Must be one of: ${validIntervals.join(", ")}.`, + }); + } + } + + return errors; +} + +/** + * Validate a plan has all required fields. + */ +function validatePlan(plan: Plan): ValidationError[] { + const errors: ValidationError[] = []; + const planId = plan.id || "(no id)"; + + // id is required + if (!plan.id) { + errors.push({ + path: "plan", + message: `"id" is required.`, + }); + } + + // name is required + if (!plan.name) { + errors.push({ + path: `plan "${planId}"`, + message: `"name" is required.`, + }); + } + + // If price is defined, validate it + if (plan.price) { + if (plan.price.amount === undefined) { + errors.push({ + path: `plan "${planId}" → price`, + message: `"amount" is required when "price" is defined.`, + }); + } + if (!plan.price.interval) { + errors.push({ + path: `plan "${planId}" → price`, + message: `"interval" is required when "price" is defined (e.g., interval: "month").`, + }); + } + } + + // Validate each plan feature + if (plan.features && Array.isArray(plan.features)) { + for (let i = 0; i < plan.features.length; i++) { + const feature = plan.features[i]; + if (feature) { + // feature_id is required + if (!feature.feature_id) { + errors.push({ + path: `plan "${planId}" → features[${i}]`, + message: `"feature_id" is required.`, + }); + } + errors.push(...validatePlanFeature(feature, planId, i)); + } + } + } + + return errors; +} + +/** + * Validate a feature has all required fields. + */ +function validateFeature(feature: Feature): ValidationError[] { + const errors: ValidationError[] = []; + const featureId = feature.id || "(no id)"; + + // id is required + if (!feature.id) { + errors.push({ + path: "feature", + message: `"id" is required.`, + }); + } + + // name is required + if (!feature.name) { + errors.push({ + path: `feature "${featureId}"`, + message: `"name" is required.`, + }); + } + + // type is required + if (!feature.type) { + errors.push({ + path: `feature "${featureId}"`, + message: `"type" is required. Must be "boolean", "metered", or "credit_system".`, + }); + } + + // If type is metered, consumable is required + if (feature.type === "metered") { + const meteredFeature = feature as { consumable?: boolean }; + if (meteredFeature.consumable === undefined) { + errors.push({ + path: `feature "${featureId}"`, + message: `"consumable" is required for metered features. Set to true (usage is consumed) or false (usage accumulates).`, + }); + } + } + + // If type is credit_system, credit_schema is required + if (feature.type === "credit_system") { + if (!feature.credit_schema || feature.credit_schema.length === 0) { + errors.push({ + path: `feature "${featureId}"`, + message: `"credit_schema" is required for credit_system features.`, + }); + } + } + + return errors; +} + +/** + * Validate local config before pushing. + * + * This catches missing required fields and provides helpful error messages + * before the API returns confusing Zod validation errors. + */ +export function validateConfig( + features: Feature[], + plans: Plan[], +): ValidationResult { + const errors: ValidationError[] = []; + + // Validate features + for (const feature of features) { + errors.push(...validateFeature(feature)); + } + + // Validate plans + for (const plan of plans) { + errors.push(...validatePlan(plan)); + } + + return { + valid: errors.length === 0, + errors, + }; +} + +/** + * Format validation errors for console output. + */ +export function formatValidationErrors(errors: ValidationError[]): string { + const lines: string[] = []; + + for (const error of errors) { + lines.push(` ${error.path}`); + lines.push(` ${error.message}`); + lines.push(""); + } + + return lines.join("\n"); +} diff --git a/atmn/src/commands/test-template/command.tsx b/atmn/src/commands/test-template/command.tsx new file mode 100644 index 00000000..19e4ce67 --- /dev/null +++ b/atmn/src/commands/test-template/command.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { render } from 'ink'; +import { TemplateSelector } from '../../views/react/template/TemplateSelector.js'; + +export function testTemplateCommand() { + render(); +} diff --git a/atmn/src/constants.ts b/atmn/src/constants.ts new file mode 100644 index 00000000..33dd249b --- /dev/null +++ b/atmn/src/constants.ts @@ -0,0 +1,60 @@ +// Production URLs (default) +export const FRONTEND_URL = "https://dev.useautumn.com"; +export const BACKEND_URL = "https://api-dev.useautumn.com"; + +// Local URLs (used when --local/-l flag is passed) +export const LOCAL_FRONTEND_URL = "http://localhost:3000"; +export const LOCAL_BACKEND_URL = "http://localhost:8080"; + +export const DELETE_CONCURRENCY = 5; + +export const DEFAULT_CONFIG = `import { + feature, + plan, + planFeature, +} from 'atmn'; + +export const seats = feature({ + id: 'seats', + name: 'Seats', + type: 'continuous_use', +}); + +export const messages = feature({ + id: 'messages', + name: 'Messages', + type: 'single_use', +}); + +export const pro = plan({ + id: 'pro', + name: 'Pro', + description: 'Professional plan for growing teams', + add_on: false, + auto_enable: false, + price: { + amount: 50, + interval: 'month', + }, + features: [ + // 500 messages per month + planFeature({ + feature_id: messages.id, + granted: 500, + reset: { interval: 'month' }, + }), + + // $10 per seat per month + planFeature({ + feature_id: seats.id, + granted: 1, + price: { + amount: 10, + interval: 'month', + billing_method: 'pay_per_use', + billing_units: 1, + }, + }), + ], +}); +`; diff --git a/atmn/src/lib/animation/explosion.ts b/atmn/src/lib/animation/explosion.ts new file mode 100644 index 00000000..b64318fb --- /dev/null +++ b/atmn/src/lib/animation/explosion.ts @@ -0,0 +1,102 @@ +/** + * Explosion animation utility + * Reverse-engineered from LazyGit's nuke animation + */ + +export interface ExplodeOptions { + width: number; + height: number; + maxFrames?: number; +} + +const EXPLOSION_CHARS = ['*', '.', '@', '#', '&', '+', '%']; + +/** + * Generate an explosion frame + * @param width - Width of the explosion area + * @param height - Height of the explosion area + * @param frame - Current frame number (0 to max) + * @param maxFrames - Total number of frames + * @returns String representing the explosion frame + */ +export function getExplodeFrame( + width: number, + height: number, + frame: number, + maxFrames: number, +): string { + const lines: string[] = []; + + // Calculate the center of explosion + const centerX = Math.floor(width / 2); + const centerY = Math.floor(height / 2); + + // Calculate the max radius (hypotenuse of the view) + const maxRadius = Math.hypot(centerX, centerY); + + // Calculate frame as a proportion of max, apply square root for non-linear effect + const progress = Math.sqrt(frame / maxFrames); + + // Calculate radius of explosion according to frame + const radius = progress * maxRadius * 2; + + // Calculate inner radius for shockwave effect + const innerRadius = progress > 0.5 ? (progress - 0.5) * 2 * maxRadius : 0; + + // Make probability threshold more aggressive near the end to ensure all chars disappear + // Use exponential curve: progress^4 makes it much harder for chars to appear as we approach 1.0 + // At 90% progress, threshold = 0.656, at 95% = 0.814, at 99% = 0.961 + const threshold = Math.pow(progress, 4); + + for (let y = 0; y < height; y++) { + let line = ''; + for (let x = 0; x < width; x++) { + // Calculate distance from center, scale y by 2 to compensate for character aspect ratio + const distance = Math.hypot(x - centerX, (y - centerY) * 2); + + // If distance is within explosion ring, draw explosion char + if (distance <= radius && distance >= innerRadius) { + // Make placement random and less likely as explosion progresses + // Add extra multiplier for final 15% to guarantee fadeout + const fadeMultiplier = progress > 0.85 ? 1 + (progress - 0.85) * 10 : 1; + const effectiveThreshold = Math.min(threshold * fadeMultiplier, 1.0); + + if (Math.random() > effectiveThreshold) { + // Pick a random explosion char + const char = EXPLOSION_CHARS[Math.floor(Math.random() * EXPLOSION_CHARS.length)]; + line += char; + } else { + line += ' '; + } + } else { + // Empty space + line += ' '; + } + } + lines.push(line); + } + + return lines.join('\n'); +} + +/** + * Get color for current frame (cycles through colors as animation progresses) + */ +export function getExplosionColor(frame: number, maxFrames: number): string { + const colors = ['white', 'yellow', 'red', 'blue', 'blackBright']; + const index = Math.floor((frame * colors.length) / maxFrames) % colors.length; + return colors[index] as string; +} + +/** + * Generate all frames for the explosion animation + */ +export function* generateExplosionFrames(options: ExplodeOptions) { + const { width, height, maxFrames = 25 } = options; + + for (let frame = 0; frame < maxFrames; frame++) { + const image = getExplodeFrame(width, height, frame, maxFrames); + const color = getExplosionColor(frame, maxFrames); + yield { image, color, frame, progress: frame / maxFrames }; + } +} diff --git a/atmn/src/lib/api/client.ts b/atmn/src/lib/api/client.ts new file mode 100644 index 00000000..74107266 --- /dev/null +++ b/atmn/src/lib/api/client.ts @@ -0,0 +1,97 @@ +import { isLocal } from "../env/cliContext.js"; +import { BACKEND_URL, LOCAL_BACKEND_URL } from "../../constants.js"; + +/** + * Get the current backend URL based on CLI flags + */ +function getBackendUrl(): string { + return isLocal() ? LOCAL_BACKEND_URL : BACKEND_URL; +} + +/** + * Low-level API client for making authenticated requests + */ + +export interface RequestOptions { + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + path: string; + secretKey: string; + body?: unknown; + queryParams?: Record; + /** Additional headers to include in the request */ + headers?: Record; +} + +export interface ApiError extends Error { + status?: number; + response?: unknown; +} + +/** + * Make an authenticated API request + */ +export async function request( + options: RequestOptions, +): Promise { + const { method, path, secretKey, body, queryParams, headers: customHeaders } = options; + + // Build URL with query params (respects --local flag) + const url = new URL(path, getBackendUrl()); + if (queryParams) { + for (const [key, value] of Object.entries(queryParams)) { + url.searchParams.set(key, String(value)); + } + } + + // Build request headers + const headers: Record = { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "X-API-Version": "2.0.0", + ...customHeaders, + }; + + const requestInit: RequestInit = { + method, + headers, + }; + + if (body && (method === "POST" || method === "PUT" || method === "PATCH")) { + requestInit.body = JSON.stringify(body); + } + + // Make request + try { + const response = await fetch(url.toString(), requestInit); + + // Parse response + let data: unknown; + const contentType = response.headers.get("content-type"); + if (contentType?.includes("application/json")) { + data = await response.json(); + } else { + data = await response.text(); + } + + // Check for errors + if (!response.ok) { + const error = new Error( + `API request failed: ${response.status} ${response.statusText}`, + ) as ApiError; + error.status = response.status; + error.response = data; + throw error; + } + + return data as T; + } catch (error) { + if (error instanceof Error && "status" in error) { + throw error; // Re-throw API errors + } + // Wrap network errors + const apiError = new Error( + `Network request failed: ${error instanceof Error ? error.message : String(error)}`, + ) as ApiError; + throw apiError; + } +} diff --git a/atmn/src/lib/api/endpoints/customers.ts b/atmn/src/lib/api/endpoints/customers.ts new file mode 100644 index 00000000..5037a880 --- /dev/null +++ b/atmn/src/lib/api/endpoints/customers.ts @@ -0,0 +1,75 @@ +import { request } from "../client.js"; + +/** + * Customer API endpoints + */ + +/** + * Customer response with X-API-Version: 2.0.0 (flat structure) + * Matches BaseApiCustomerSchema from server + */ +export interface ApiCustomer { + autumn_id?: string; + id: string; + name: string | null; + email: string | null; + created_at: number; + fingerprint: string | null; + stripe_id: string | null; + env: string; + metadata: Record; + subscriptions: unknown[]; + scheduled_subscriptions: unknown[]; + balances: Record; +} + +export interface FetchCustomersOptions { + secretKey: string; + limit?: number; + offset?: number; +} + +export interface FetchCustomersResponse { + list: ApiCustomer[]; + total: number; + limit: number; + offset: number; + has_more: boolean; +} + +/** + * Fetch all customers using V1 endpoint (GET with query params) + */ +export async function fetchCustomers( + options: FetchCustomersOptions +): Promise { + const { secretKey, limit = 100, offset = 0 } = options; + + const response = await request({ + method: "GET", + path: "/v1/customers", + secretKey, + queryParams: { + limit, + offset, + }, + }); + + return response.list; +} + +/** + * Delete a single customer + */ +export async function deleteCustomer(options: { + secretKey: string; + customerId: string; +}): Promise { + const { secretKey, customerId } = options; + + await request({ + method: "DELETE", + path: `/v1/customers/${customerId}`, + secretKey, + }); +} diff --git a/atmn/src/lib/api/endpoints/features.ts b/atmn/src/lib/api/endpoints/features.ts new file mode 100644 index 00000000..d5a73dc7 --- /dev/null +++ b/atmn/src/lib/api/endpoints/features.ts @@ -0,0 +1,137 @@ +import { request } from "../client.js"; +import type { ApiFeature } from "../types/index.js"; + +/** + * Fetch features from API + */ +export interface FetchFeaturesOptions { + secretKey: string; + includeArchived?: boolean; +} + +export interface FetchFeaturesResponse { + list: ApiFeature[]; +} + +export async function fetchFeatures( + options: FetchFeaturesOptions, +): Promise { + const { secretKey, includeArchived = true } = options; + + const response = await request({ + method: "GET", + path: "/v1/features", + secretKey, + queryParams: { + include_archived: includeArchived, + }, + }); + + return response.list; +} + +/** + * Create or update a feature + */ +export async function upsertFeature(options: { + secretKey: string; + feature: Record; +}): Promise { + const { secretKey, feature } = options; + + return await request({ + method: "POST", + path: "/v1/features", + secretKey, + body: feature, + }); +} + +/** + * Update an existing feature + */ +export async function updateFeature(options: { + secretKey: string; + featureId: string; + feature: Record; +}): Promise { + const { secretKey, featureId, feature } = options; + + return await request({ + method: "POST", + path: `/v1/features/${featureId}`, + secretKey, + body: feature, + }); +} + +/** + * Delete a single feature + */ +export async function deleteFeature(options: { + secretKey: string; + featureId: string; +}): Promise { + const { secretKey, featureId } = options; + + await request({ + method: "DELETE", + path: `/v1/features/${featureId}`, + secretKey, + }); +} + +/** + * Archive a feature + */ +export async function archiveFeature(options: { + secretKey: string; + featureId: string; +}): Promise { + const { secretKey, featureId } = options; + + await request({ + method: "POST", + path: `/v1/features/${featureId}`, + secretKey, + body: { archived: true }, + }); +} + +/** + * Unarchive a feature + */ +export async function unarchiveFeature(options: { + secretKey: string; + featureId: string; +}): Promise { + const { secretKey, featureId } = options; + + await request({ + method: "POST", + path: `/v1/features/${featureId}`, + secretKey, + body: { archived: false }, + }); +} + +/** + * Get deletion info for a feature (check if it can be deleted) + */ +export interface FeatureDeletionInfo { + totalCount: number; + productName?: string; +} + +export async function getFeatureDeletionInfo(options: { + secretKey: string; + featureId: string; +}): Promise { + const { secretKey, featureId } = options; + + return await request({ + method: "GET", + path: `/v1/features/${featureId}/deletion_info`, + secretKey, + }); +} diff --git a/atmn/src/lib/api/endpoints/index.ts b/atmn/src/lib/api/endpoints/index.ts new file mode 100644 index 00000000..cbd97d39 --- /dev/null +++ b/atmn/src/lib/api/endpoints/index.ts @@ -0,0 +1,30 @@ +export { + fetchPlans, + createPlan, + updatePlan, + deletePlan, + archivePlan, + unarchivePlan, + getPlanDeletionInfo, + getPlanHasCustomers, + type FetchPlansOptions, + type PlanDeletionInfo, + type PlanHasCustomersInfo, +} from "./plans.js"; +export { + fetchFeatures, + upsertFeature, + updateFeature, + deleteFeature, + archiveFeature, + unarchiveFeature, + getFeatureDeletionInfo, + type FetchFeaturesOptions, + type FeatureDeletionInfo, +} from "./features.js"; +export { + fetchOrganization, + fetchOrganizationMe, + type FetchOrganizationOptions, + type OrganizationMeInfo, +} from "./organization.js"; diff --git a/atmn/src/lib/api/endpoints/organization.ts b/atmn/src/lib/api/endpoints/organization.ts new file mode 100644 index 00000000..0998ddd4 --- /dev/null +++ b/atmn/src/lib/api/endpoints/organization.ts @@ -0,0 +1,45 @@ +import { request } from "../client.js"; +import type { ApiOrganization } from "../types/index.js"; + +/** + * Fetch organization details from API + */ +export interface FetchOrganizationOptions { + secretKey: string; +} + +export async function fetchOrganization( + options: FetchOrganizationOptions, +): Promise { + const { secretKey } = options; + + return request({ + method: "GET", + path: "/v1/organization", + secretKey, + }); +} + +/** + * Organization info response from /me endpoint + */ +export interface OrganizationMeInfo { + name: string; + slug: string; + env: string; +} + +/** + * Fetch current organization info (name, slug, env) + */ +export async function fetchOrganizationMe( + options: FetchOrganizationOptions, +): Promise { + const { secretKey } = options; + + return request({ + method: "GET", + path: "/v1/organization/me", + secretKey, + }); +} diff --git a/atmn/src/lib/api/endpoints/plans.ts b/atmn/src/lib/api/endpoints/plans.ts new file mode 100644 index 00000000..dccc31ae --- /dev/null +++ b/atmn/src/lib/api/endpoints/plans.ts @@ -0,0 +1,162 @@ +import { request } from "../client.js"; +import type { ApiPlan } from "../types/index.js"; + +/** + * Fetch plans from API + */ +export interface FetchPlansOptions { + secretKey: string; + includeArchived?: boolean; +} + +export interface FetchPlansResponse { + list: ApiPlan[]; +} + +export async function fetchPlans( + options: FetchPlansOptions, +): Promise { + const { secretKey, includeArchived = true } = options; + + const response = await request({ + method: "GET", + path: "/v1/products", + secretKey, + queryParams: { + include_archived: includeArchived, + }, + }); + + return response.list; +} + +/** + * Create a new plan + */ +export async function createPlan(options: { + secretKey: string; + plan: Record; +}): Promise { + const { secretKey, plan } = options; + + return await request({ + method: "POST", + path: "/v1/products", + secretKey, + body: plan, + }); +} + +/** + * Update an existing plan + */ +export async function updatePlan(options: { + secretKey: string; + planId: string; + plan: Record; +}): Promise { + const { secretKey, planId, plan } = options; + + return await request({ + method: "POST", + path: `/v1/products/${planId}`, + secretKey, + body: plan, + }); +} + +/** + * Delete a single plan + */ +export async function deletePlan(options: { + secretKey: string; + planId: string; + allVersions?: boolean; +}): Promise { + const { secretKey, planId, allVersions = true } = options; + + await request({ + method: "DELETE", + path: `/v1/products/${planId}`, + secretKey, + queryParams: { + all_versions: allVersions, + }, + }); +} + +/** + * Archive a plan + */ +export async function archivePlan(options: { + secretKey: string; + planId: string; +}): Promise { + const { secretKey, planId } = options; + + await request({ + method: "POST", + path: `/v1/products/${planId}`, + secretKey, + body: { archived: true }, + }); +} + +/** + * Unarchive a plan + */ +export async function unarchivePlan(options: { + secretKey: string; + planId: string; +}): Promise { + const { secretKey, planId } = options; + + await request({ + method: "POST", + path: `/v1/products/${planId}`, + secretKey, + body: { archived: false }, + }); +} + +/** + * Get deletion info for a plan (check if it can be deleted) + */ +export interface PlanDeletionInfo { + totalCount: number; + customerName?: string; +} + +export async function getPlanDeletionInfo(options: { + secretKey: string; + planId: string; +}): Promise { + const { secretKey, planId } = options; + + return await request({ + method: "GET", + path: `/v1/products/${planId}/deletion_info`, + secretKey, + }); +} + +/** + * Check if a plan has customers (for versioning check) + */ +export interface PlanHasCustomersInfo { + will_version: boolean; + archived: boolean; +} + +export async function getPlanHasCustomers(options: { + secretKey: string; + planId: string; +}): Promise { + const { secretKey, planId } = options; + + return await request({ + method: "GET", + path: `/v1/products/${planId}/has_customers`, + secretKey, + }); +} diff --git a/atmn/src/lib/api/types/feature.ts b/atmn/src/lib/api/types/feature.ts new file mode 100644 index 00000000..4bcd1d53 --- /dev/null +++ b/atmn/src/lib/api/types/feature.ts @@ -0,0 +1,14 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared API schemas +// Run typegen to regenerate + +import type { ApiFeatureV1 } from "../../../../../../../sirtenzin-autumn/shared/api/features/apiFeatureV1.js"; + +/** + * ApiFeature - Raw API response type + * Source: apiFeatureV1.ts + * + * This type matches the exact structure returned by the Autumn API. + * Use transform functions in src/lib/transforms/apiToSdk to convert to SDK types. + */ +export type ApiFeature = ApiFeatureV1; diff --git a/atmn/src/lib/api/types/index.ts b/atmn/src/lib/api/types/index.ts new file mode 100644 index 00000000..838ae06d --- /dev/null +++ b/atmn/src/lib/api/types/index.ts @@ -0,0 +1,6 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Re-exports for API types + +export type { ApiPlan } from "./plan.js"; +export type { ApiPlanFeature } from "./planFeature.js"; +export type { ApiFeature } from "./feature.js"; diff --git a/atmn/src/lib/api/types/organization.ts b/atmn/src/lib/api/types/organization.ts new file mode 100644 index 00000000..28ecced6 --- /dev/null +++ b/atmn/src/lib/api/types/organization.ts @@ -0,0 +1,12 @@ +// Manual type - not auto-generated + +/** + * Organization API response type + */ +export interface ApiOrganization { + id: string; + name: string; + slug: string; + stripe_connection?: string; + created_at: number; +} diff --git a/atmn/src/lib/api/types/plan.ts b/atmn/src/lib/api/types/plan.ts new file mode 100644 index 00000000..a88ae6ed --- /dev/null +++ b/atmn/src/lib/api/types/plan.ts @@ -0,0 +1,14 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared API schemas +// Run typegen to regenerate + +import type { ApiPlan } from "../../../../../../../sirtenzin-autumn/shared/api/products/apiPlan.js"; + +/** + * ApiPlan - Raw API response type + * Source: apiPlan.ts + * + * This type matches the exact structure returned by the Autumn API. + * Use transform functions in src/lib/transforms/apiToSdk to convert to SDK types. + */ +export type { ApiPlan }; diff --git a/atmn/src/lib/api/types/planFeature.ts b/atmn/src/lib/api/types/planFeature.ts new file mode 100644 index 00000000..2c83a26d --- /dev/null +++ b/atmn/src/lib/api/types/planFeature.ts @@ -0,0 +1,14 @@ +// AUTO-GENERATED - DO NOT EDIT MANUALLY +// Generated from @autumn/shared API schemas +// Run typegen to regenerate + +import type { ApiPlanFeature } from "../../../../../../../sirtenzin-autumn/shared/api/products/planFeature/apiPlanFeature.js"; + +/** + * ApiPlanFeature - Raw API response type + * Source: apiPlanFeature.ts + * + * This type matches the exact structure returned by the Autumn API. + * Use transform functions in src/lib/transforms/apiToSdk to convert to SDK types. + */ +export type { ApiPlanFeature }; diff --git a/atmn/src/lib/constants/templates.ts b/atmn/src/lib/constants/templates.ts new file mode 100644 index 00000000..a83b2df0 --- /dev/null +++ b/atmn/src/lib/constants/templates.ts @@ -0,0 +1,129 @@ +/** + * Template data for the template selector + * Each template has 3 pricing plans with features + */ + +export interface PlanData { + name: string; + features: string[]; + price: string; + badge?: string; +} + +export const templateData: Record = { + Blacktriangle: [ + { + name: "Hobby", + features: [ + "100 GB bandwidth", + "Serverless functions", + "Edge network", + "Community support", + ], + price: "$0/month", + }, + { + name: "Pro", + badge: "most popular", + features: [ + "1 TB bandwidth", + "Advanced analytics", + "Team collaboration", + "Preview deployments", + "Priority support", + "Custom domains", + "DDoS protection", + ], + price: "$20/month", + }, + { + name: "Enterprise", + features: [ + "Unlimited bandwidth", + "SLA guarantee", + "Dedicated support", + "SSO & SAML", + "Custom contracts", + ], + price: "Custom", + }, + ], + RatGPT: [ + { + name: "Free", + features: [ + "50 messages/day", + "Basic AI model", + "Web access", + "Standard speed", + ], + price: "$0/month", + }, + { + name: "Plus", + badge: "most popular", + features: [ + "Unlimited messages", + "Advanced AI models", + "Priority access", + "Plugins & tools", + "Image generation", + "Voice mode", + "File uploads", + ], + price: "$20/month", + }, + { + name: "Team", + features: [ + "All Plus features", + "Admin console", + "Team workspace", + "Usage analytics", + "Priority support", + ], + price: "$25/user/mo", + }, + ], + Aksiom: [ + { + name: "Starter", + features: [ + "1 GB ingest/mo", + "7 day retention", + "Basic dashboards", + "Email alerts", + ], + price: "$0/month", + }, + { + name: "Pro", + badge: "most popular", + features: [ + "50 GB ingest/mo", + "30 day retention", + "Advanced queries", + "Team access", + "Slack alerts", + "API access", + "Custom dashboards", + ], + price: "$25/month", + }, + { + name: "Enterprise", + features: [ + "Unlimited ingest", + "90 day retention", + "SSO & RBAC", + "Dedicated support", + "SLA guarantee", + ], + price: "Custom", + }, + ], +}; + +export const templates = ["Blacktriangle", "RatGPT", "Aksiom"] as const; + +export type TemplateName = (typeof templates)[number]; diff --git a/atmn/src/lib/constants/templates/aksiom.config.ts b/atmn/src/lib/constants/templates/aksiom.config.ts new file mode 100644 index 00000000..783c7840 --- /dev/null +++ b/atmn/src/lib/constants/templates/aksiom.config.ts @@ -0,0 +1,92 @@ +/** + * Aksiom - Axiom-style pricing (observability/logging) + * Starter (free) / Pro ($25/mo) / Enterprise (custom) + */ + +import type { Feature, Plan } from "../../../../source/compose/models/index.js"; + +export const features: Feature[] = [ + { + id: "ingest", + name: "Data Ingest", + type: "metered", + consumable: true, + }, + { + id: "retention_days", + name: "Data Retention", + type: "metered", + consumable: false, + }, + { + id: "dashboards", + name: "Custom Dashboards", + type: "boolean", + }, + { + id: "advanced_queries", + name: "Advanced Queries", + type: "boolean", + }, + { + id: "team_access", + name: "Team Access", + type: "boolean", + }, + { + id: "api_access", + name: "API Access", + type: "boolean", + }, + { + id: "sso_rbac", + name: "SSO & RBAC", + type: "boolean", + }, + { + id: "sla", + name: "SLA Guarantee", + type: "boolean", + }, +]; + +export const plans: Plan[] = [ + { + id: "starter", + name: "Starter", + description: "For small projects and experimentation", + features: [ + { feature_id: "ingest", included: 1 }, // 1 GB/mo + { feature_id: "retention_days", included: 7 }, + ], + }, + { + id: "pro", + name: "Pro", + description: "For growing teams and production workloads", + price: { amount: 25, interval: "month" }, + features: [ + { feature_id: "ingest", included: 50 }, // 50 GB/mo + { feature_id: "retention_days", included: 30 }, + { feature_id: "dashboards" }, + { feature_id: "advanced_queries" }, + { feature_id: "team_access" }, + { feature_id: "api_access" }, + ], + }, + { + id: "enterprise", + name: "Enterprise", + description: "For large organizations with compliance needs", + features: [ + { feature_id: "ingest", unlimited: true }, + { feature_id: "retention_days", included: 90 }, + { feature_id: "dashboards" }, + { feature_id: "advanced_queries" }, + { feature_id: "team_access" }, + { feature_id: "api_access" }, + { feature_id: "sso_rbac" }, + { feature_id: "sla" }, + ], + }, +]; diff --git a/atmn/src/lib/constants/templates/blacktriangle.config.ts b/atmn/src/lib/constants/templates/blacktriangle.config.ts new file mode 100644 index 00000000..7f72f253 --- /dev/null +++ b/atmn/src/lib/constants/templates/blacktriangle.config.ts @@ -0,0 +1,86 @@ +/** + * Blacktriangle - Vercel-style pricing + * Hobby (free) / Pro ($20/mo) / Enterprise (custom) + */ + +import type { Feature, Plan } from "../../../../source/compose/models/index.js"; + +export const features: Feature[] = [ + { + id: "bandwidth", + name: "Bandwidth", + type: "metered", + consumable: true, + }, + { + id: "serverless_functions", + name: "Serverless Functions", + type: "boolean", + }, + { + id: "edge_network", + name: "Edge Network", + type: "boolean", + }, + { + id: "advanced_analytics", + name: "Advanced Analytics", + type: "boolean", + }, + { + id: "team_collaboration", + name: "Team Collaboration", + type: "boolean", + }, + { + id: "priority_support", + name: "Priority Support", + type: "boolean", + }, + { + id: "sso", + name: "SSO & SAML", + type: "boolean", + }, +]; + +export const plans: Plan[] = [ + { + id: "hobby", + name: "Hobby", + description: "For personal projects and experimentation", + features: [ + { feature_id: "bandwidth", included: 100 }, // 100 GB + { feature_id: "serverless_functions" }, + { feature_id: "edge_network" }, + ], + }, + { + id: "pro", + name: "Pro", + description: "For professional developers and small teams", + price: { amount: 20, interval: "month" }, + features: [ + { feature_id: "bandwidth", included: 1000 }, // 1 TB + { feature_id: "serverless_functions" }, + { feature_id: "edge_network" }, + { feature_id: "advanced_analytics" }, + { feature_id: "team_collaboration" }, + { feature_id: "priority_support" }, + ], + }, + { + id: "enterprise", + name: "Enterprise", + description: "For large organizations with custom needs", + features: [ + { feature_id: "bandwidth", unlimited: true }, + { feature_id: "serverless_functions" }, + { feature_id: "edge_network" }, + { feature_id: "advanced_analytics" }, + { feature_id: "team_collaboration" }, + { feature_id: "priority_support" }, + { feature_id: "sso" }, + ], + }, +]; diff --git a/atmn/src/lib/constants/templates/index.ts b/atmn/src/lib/constants/templates/index.ts new file mode 100644 index 00000000..e0314413 --- /dev/null +++ b/atmn/src/lib/constants/templates/index.ts @@ -0,0 +1,28 @@ +/** + * Template configurations index + */ + +import type { Feature, Plan } from "../../../../source/compose/models/index.js"; +import * as blacktriangle from "./blacktriangle.config.js"; +import * as ratgpt from "./ratgpt.config.js"; +import * as aksiom from "./aksiom.config.js"; + +export interface TemplateConfig { + features: Feature[]; + plans: Plan[]; +} + +export const templateConfigs: Record = { + Blacktriangle: { + features: blacktriangle.features, + plans: blacktriangle.plans, + }, + RatGPT: { + features: ratgpt.features, + plans: ratgpt.plans, + }, + Aksiom: { + features: aksiom.features, + plans: aksiom.plans, + }, +}; diff --git a/atmn/src/lib/constants/templates/ratgpt.config.ts b/atmn/src/lib/constants/templates/ratgpt.config.ts new file mode 100644 index 00000000..d8705763 --- /dev/null +++ b/atmn/src/lib/constants/templates/ratgpt.config.ts @@ -0,0 +1,89 @@ +/** + * RatGPT - ChatGPT-style pricing + * Free / Plus ($20/mo) / Team ($25/user/mo) + */ + +import type { Feature, Plan } from "../../../../source/compose/models/index.js"; + +export const features: Feature[] = [ + { + id: "messages", + name: "Messages", + type: "metered", + consumable: true, + }, + { + id: "advanced_models", + name: "Advanced AI Models", + type: "boolean", + }, + { + id: "image_generation", + name: "Image Generation", + type: "boolean", + }, + { + id: "voice_mode", + name: "Voice Mode", + type: "boolean", + }, + { + id: "file_uploads", + name: "File Uploads", + type: "boolean", + }, + { + id: "team_seats", + name: "Team Seats", + type: "metered", + consumable: false, // Seats are not consumed, they accumulate + }, + { + id: "admin_console", + name: "Admin Console", + type: "boolean", + }, +]; + +export const plans: Plan[] = [ + { + id: "free", + name: "Free", + description: "Get started with basic AI capabilities", + features: [ + { feature_id: "messages", included: 50, interval: "day" }, + ], + }, + { + id: "plus", + name: "Plus", + description: "Enhanced AI with unlimited access", + price: { amount: 20, interval: "month" }, + features: [ + { feature_id: "messages", unlimited: true }, + { feature_id: "advanced_models" }, + { feature_id: "image_generation" }, + { feature_id: "voice_mode" }, + { feature_id: "file_uploads" }, + ], + }, + { + id: "team", + name: "Team", + description: "Collaborate with your entire team", + price: { amount: 25, interval: "month" }, + features: [ + { feature_id: "messages", unlimited: true }, + { feature_id: "advanced_models" }, + { feature_id: "image_generation" }, + { feature_id: "voice_mode" }, + { feature_id: "file_uploads" }, + { + feature_id: "team_seats", + interval: "month", + price: { amount: 25, billing_method: "pay_per_use", billing_units: 1 }, + }, + { feature_id: "admin_console" }, + ], + }, +]; diff --git a/atmn/src/lib/env/cliContext.ts b/atmn/src/lib/env/cliContext.ts new file mode 100644 index 00000000..7abb0c43 --- /dev/null +++ b/atmn/src/lib/env/cliContext.ts @@ -0,0 +1,46 @@ +/** + * Global CLI context for storing parsed options + * This avoids relying on process.argv parsing which doesn't handle combined flags like -lp + */ + +export interface CliContext { + prod: boolean; + local: boolean; +} + +let context: CliContext = { + prod: false, + local: false, +}; + +/** + * Set the CLI context from parsed commander options + * Should be called once at CLI startup before any commands run + */ +export function setCliContext(options: Partial): void { + context = { + prod: options.prod ?? false, + local: options.local ?? false, + }; +} + +/** + * Get the current CLI context + */ +export function getCliContext(): CliContext { + return context; +} + +/** + * Check if production mode is enabled + */ +export function isProd(): boolean { + return context.prod; +} + +/** + * Check if local mode is enabled + */ +export function isLocal(): boolean { + return context.local; +} diff --git a/atmn/src/lib/env/detect.ts b/atmn/src/lib/env/detect.ts new file mode 100644 index 00000000..fecd5f9e --- /dev/null +++ b/atmn/src/lib/env/detect.ts @@ -0,0 +1,53 @@ +/** + * Environment detection and key validation utilities + */ + +/** + * Application environment enum + * Matches the canonical AppEnv enum from shared/models + */ +export enum AppEnv { + Sandbox = "sandbox", + Live = "live", +} + +/** + * Detect environment from API key format + * Sandbox keys contain '_test_', live keys contain '_live_' + */ +export function getEnvironmentFromKey(key: string): AppEnv { + if (key.includes("_test_")) { + return AppEnv.Sandbox; + } + if (key.includes("_live_")) { + return AppEnv.Live; + } + throw new Error( + `Invalid API key format: must contain '_test_' (sandbox) or '_live_' (live)`, + ); +} + +/** + * Check if key is a sandbox key + */ +export function isSandboxKey(key: string): boolean { + return key.includes("_test_"); +} + +/** + * Check if key is a live/production key + */ +export function isLiveKey(key: string): boolean { + return key.includes("_live_"); +} + +/** + * Validate key format (basic check) + */ +export function isValidKey(key: string): boolean { + return ( + typeof key === "string" && + key.length > 0 && + (isSandboxKey(key) || isLiveKey(key)) + ); +} diff --git a/atmn/src/lib/env/dotenv.ts b/atmn/src/lib/env/dotenv.ts new file mode 100644 index 00000000..698c5a53 --- /dev/null +++ b/atmn/src/lib/env/dotenv.ts @@ -0,0 +1,103 @@ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; + +/** + * Low-level .env file reading/writing utilities + */ + +export interface DotenvEntry { + key: string; + value: string; +} + +/** + * Parse .env file content into key-value pairs + */ +export function parseDotenv(content: string): Map { + const entries = new Map(); + + for (const line of content.split("\n")) { + const trimmed = line.trim(); + + // Skip comments and empty lines + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + + // Parse KEY=VALUE + const match = trimmed.match(/^([^=]+)=(.*)$/); + if (match) { + const [, key, value] = match; + if (key && value !== undefined) { + // Remove quotes if present + const cleanValue = value.replace(/^["']|["']$/g, ""); + entries.set(key.trim(), cleanValue); + } + } + } + + return entries; +} + +/** + * Read a .env file and return parsed entries + */ +export function readDotenvFile(filePath: string): Map { + if (!existsSync(filePath)) { + return new Map(); + } + + try { + const content = readFileSync(filePath, "utf-8"); + return parseDotenv(content); + } catch (_error) { + return new Map(); + } +} + +/** + * Write entries to a .env file + */ +export function writeDotenvFile( + filePath: string, + entries: Map, +): void { + const lines = Array.from(entries.entries()) + .map(([key, value]) => { + // Quote values that contain spaces + const quotedValue = value.includes(" ") ? `"${value}"` : value; + return `${key}=${quotedValue}`; + }) + .join("\n"); + + writeFileSync(filePath, lines + "\n", "utf-8"); +} + +/** + * Get value from .env file (checks .env.local first, then .env) + */ +export function getDotenvValue(key: string, cwd = process.cwd()): string | undefined { + const localPath = resolve(cwd, ".env.local"); + const localEntries = readDotenvFile(localPath); + if (localEntries.has(key)) { + return localEntries.get(key); + } + + const envPath = resolve(cwd, ".env"); + const envEntries = readDotenvFile(envPath); + return envEntries.get(key); +} + +/** + * Set value in .env file (always writes to .env, not .env.local) + */ +export function setDotenvValue( + key: string, + value: string, + cwd = process.cwd(), +): void { + const envPath = resolve(cwd, ".env"); + const entries = readDotenvFile(envPath); + entries.set(key, value); + writeDotenvFile(envPath, entries); +} diff --git a/atmn/src/lib/env/index.ts b/atmn/src/lib/env/index.ts new file mode 100644 index 00000000..3482070d --- /dev/null +++ b/atmn/src/lib/env/index.ts @@ -0,0 +1,23 @@ +export { readApiKeys, getKey, hasKey, getAnyKey } from "./keys.js"; +export { + AppEnv, + getEnvironmentFromKey, + isSandboxKey, + isLiveKey, + isValidKey, +} from "./detect.js"; +export { + parseDotenv, + readDotenvFile, + writeDotenvFile, + getDotenvValue, + setDotenvValue, + type DotenvEntry, +} from "./dotenv.js"; +export { + type CliContext, + getCliContext, + setCliContext, + isProd, + isLocal, +} from "./cliContext.js"; diff --git a/atmn/src/lib/env/keys.ts b/atmn/src/lib/env/keys.ts new file mode 100644 index 00000000..558bbddc --- /dev/null +++ b/atmn/src/lib/env/keys.ts @@ -0,0 +1,72 @@ +import { AppEnv, isValidKey } from "./detect.js"; +import { getDotenvValue } from "./dotenv.js"; + +/** + * API key management utilities + */ + +export interface ApiKeys { + [AppEnv.Sandbox]?: string; + [AppEnv.Live]?: string; +} + +/** + * Read API keys from .env files + * Returns keys organized by environment + * + * Standard naming convention: + * - AUTUMN_SECRET_KEY: sandbox key (am_sk_test_* prefix) + * - AUTUMN_PROD_SECRET_KEY: production/live key (am_sk_live_* prefix) + */ +export function readApiKeys(cwd?: string): ApiKeys { + const keys: ApiKeys = {}; + + // Read standard environment variables + const autumnSecretKey = getDotenvValue("AUTUMN_SECRET_KEY", cwd); + const autumnProdSecretKey = getDotenvValue("AUTUMN_PROD_SECRET_KEY", cwd); + + // AUTUMN_SECRET_KEY is always sandbox (ask_* prefix) + if (autumnSecretKey && isValidKey(autumnSecretKey)) { + keys[AppEnv.Sandbox] = autumnSecretKey; + } + + // AUTUMN_PROD_SECRET_KEY is always production/live (apks_* prefix) + if (autumnProdSecretKey && isValidKey(autumnProdSecretKey)) { + keys[AppEnv.Live] = autumnProdSecretKey; + } + + return keys; +} + +/** + * Get API key for specific environment + * Throws if key is not found + */ +export function getKey(env: AppEnv, cwd?: string): string { + const keys = readApiKeys(cwd); + const key = keys[env]; + + if (!key) { + throw new Error( + `No ${env} API key found. Run 'atmn auth' to authenticate.`, + ); + } + + return key; +} + +/** + * Check if API key exists for environment + */ +export function hasKey(env: AppEnv, cwd?: string): boolean { + const keys = readApiKeys(cwd); + return !!keys[env]; +} + +/** + * Get any available key (prefers sandbox) + */ +export function getAnyKey(cwd?: string): string | undefined { + const keys = readApiKeys(cwd); + return keys[AppEnv.Sandbox] || keys[AppEnv.Live]; +} diff --git a/atmn/src/lib/hooks/index.ts b/atmn/src/lib/hooks/index.ts new file mode 100644 index 00000000..81b5eb76 --- /dev/null +++ b/atmn/src/lib/hooks/index.ts @@ -0,0 +1,43 @@ +export { useOrganization, type OrganizationInfo } from "./useOrganization.js"; +export { usePull, type GeneratedFile } from "./usePull.js"; +export { useConfigCounts } from "./useConfigCounts.js"; +export { useCreateGuides } from "./useCreateGuides.js"; +export { + useHeadlessAuth, + type HeadlessAuthState, + type OrgInfo, + type UseHeadlessAuthOptions, + type UseHeadlessAuthReturn, +} from "./useHeadlessAuth.js"; +export { + usePush, + type PushPhase, + type FeatureStatus, + type PlanStatus, + type UsePushOptions, +} from "./usePush.js"; +export { useWriteTemplateConfig } from "./useWriteTemplateConfig.js"; +export { + useAgentSetup, + type AgentIdentifier, + type FileOption, + type InstallMcpResult, + type CreateAgentFilesResult, + type UseAgentSetupOptions, +} from "./useAgentSetup.js"; +export { + useCustomers, + type ListCustomersResponse, + type UseCustomersOptions, +} from "./useCustomers.js"; +export { + useCustomerNavigation, + type FocusTarget, + type NavigationState, + type NavigationAction, +} from "./useCustomerNavigation.js"; +export { + useClipboard, + type UseClipboardOptions, + type UseClipboardReturn, +} from "./useClipboard.js"; diff --git a/atmn/src/lib/hooks/useAgentSetup.ts b/atmn/src/lib/hooks/useAgentSetup.ts new file mode 100644 index 00000000..bcae2bbc --- /dev/null +++ b/atmn/src/lib/hooks/useAgentSetup.ts @@ -0,0 +1,163 @@ +import clipboard from "clipboardy"; +import { exec } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { useMutation } from "@tanstack/react-query"; + +const execAsync = promisify(exec); + +const MCP_URL = "https://docs.useautumn.com/mcp"; + +// Markdown content for CLAUDE.md and AGENTS.md +const markdownContent = ` + +# Autumn Billing Integration + +## Overview + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Usage + +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +## Features + +- Lorem ipsum dolor sit amet +- Consectetur adipiscing elit +- Sed do eiusmod tempor incididunt + +## Resources + +- [Autumn Documentation](https://docs.useautumn.com) +- [API Reference](https://docs.useautumn.com/api) +`; + +// Content for .cursorrules +const cursorRulesContent = ` + +# Autumn Billing Rules + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +`; + +export type AgentIdentifier = "claude-code" | "other"; +export type FileOption = "claude-md" | "agents-md" | "cursor-rules"; + +export interface InstallMcpResult { + installedAgents: AgentIdentifier[]; + copiedToClipboard: boolean; +} + +export interface CreateAgentFilesResult { + createdFiles: string[]; +} + +/** + * Install MCP for the specified agents + */ +async function installMcpForAgents( + agents: AgentIdentifier[], +): Promise { + const installedAgents: AgentIdentifier[] = []; + let copiedToClipboard = false; + + for (const agent of agents) { + if (agent === "claude-code") { + try { + await execAsync(`claude mcp add --transport http autumn ${MCP_URL}`); + installedAgents.push("claude-code"); + } catch (err) { + // If it fails, it might already exist - check the error message + const errorMessage = err instanceof Error ? err.message : String(err); + if (!errorMessage.includes("already exists")) { + // If it's not an "already exists" error, throw it + throw err; + } + // Otherwise, silently continue (already installed is fine) + installedAgents.push("claude-code"); + } + } else if (agent === "other") { + // Copy URL to clipboard + await clipboard.write(MCP_URL); + copiedToClipboard = true; + } + } + + return { installedAgents, copiedToClipboard }; +} + +/** + * Helper function to append to existing file or create new file + */ +async function appendOrCreate(filePath: string, content: string): Promise { + try { + // Try to read existing file + await fs.access(filePath); + // File exists, append content + await fs.appendFile(filePath, content, "utf-8"); + } catch { + // File doesn't exist, create it + await fs.writeFile(filePath, content.trimStart(), "utf-8"); + } +} + +/** + * Create agent configuration files based on selected options + */ +async function createAgentFilesForOptions( + options: FileOption[], + cwd?: string, +): Promise { + const effectiveCwd = cwd ?? process.cwd(); + const createdFiles: string[] = []; + + for (const option of options) { + if (option === "claude-md") { + await appendOrCreate(path.join(effectiveCwd, "CLAUDE.md"), markdownContent); + createdFiles.push("CLAUDE.md"); + } else if (option === "agents-md") { + await appendOrCreate(path.join(effectiveCwd, "AGENTS.md"), markdownContent); + createdFiles.push("AGENTS.md"); + } else if (option === "cursor-rules") { + await appendOrCreate( + path.join(effectiveCwd, ".cursorrules"), + cursorRulesContent, + ); + createdFiles.push(".cursorrules"); + } + } + + return { createdFiles }; +} + +export interface UseAgentSetupOptions { + cwd?: string; +} + +/** + * Hook for managing agent setup operations including MCP installation + * and agent configuration file creation. + */ +export function useAgentSetup(options?: UseAgentSetupOptions) { + const cwd = options?.cwd; + + const installMcp = useMutation({ + mutationFn: async (agents: AgentIdentifier[]) => { + return await installMcpForAgents(agents); + }, + }); + + const createAgentFiles = useMutation({ + mutationFn: async (fileOptions: FileOption[]) => { + return await createAgentFilesForOptions(fileOptions, cwd); + }, + }); + + return { + installMcp, + createAgentFiles, + }; +} diff --git a/atmn/src/lib/hooks/useClipboard.ts b/atmn/src/lib/hooks/useClipboard.ts new file mode 100644 index 00000000..5fd601e5 --- /dev/null +++ b/atmn/src/lib/hooks/useClipboard.ts @@ -0,0 +1,56 @@ +import clipboard from "clipboardy"; +import { useCallback, useState } from "react"; + +export interface UseClipboardOptions { + /** Duration in ms to show feedback (default: 1500) */ + feedbackDuration?: number; +} + +export interface UseClipboardReturn { + /** Copy text to clipboard */ + copy: (text: string) => Promise; + /** Whether feedback is currently showing */ + showingFeedback: boolean; + /** Last error if copy failed */ + error: Error | null; +} + +/** + * Cross-platform clipboard hook with feedback state + */ +export function useClipboard( + options: UseClipboardOptions = {}, +): UseClipboardReturn { + const { feedbackDuration = 1500 } = options; + const [showingFeedback, setShowingFeedback] = useState(false); + const [error, setError] = useState(null); + + const copy = useCallback( + async (text: string): Promise => { + try { + await clipboard.write(text); + setError(null); + setShowingFeedback(true); + + // Clear feedback after duration + setTimeout(() => { + setShowingFeedback(false); + }, feedbackDuration); + + return true; + } catch (err) { + const copyError = + err instanceof Error ? err : new Error("Failed to copy to clipboard"); + setError(copyError); + return false; + } + }, + [feedbackDuration], + ); + + return { + copy, + showingFeedback, + error, + }; +} diff --git a/atmn/src/lib/hooks/useConfigCounts.ts b/atmn/src/lib/hooks/useConfigCounts.ts new file mode 100644 index 00000000..da0860bf --- /dev/null +++ b/atmn/src/lib/hooks/useConfigCounts.ts @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query"; +import { + fetchFeatures, + fetchPlans, +} from "../api/endpoints/index.js"; +import { AppEnv, getKey } from "../env/index.js"; + +interface ConfigCounts { + plansCount: number; + featuresCount: number; +} + +/** + * Hook to fetch configuration counts (plans and features) + * Uses TanStack Query for caching and state management + */ +export function useConfigCounts() { + return useQuery({ + queryKey: ["configCounts"], + queryFn: async () => { + const sandboxKey = getKey(AppEnv.Sandbox); + const [features, plans] = await Promise.all([ + fetchFeatures({ secretKey: sandboxKey }), + fetchPlans({ secretKey: sandboxKey }), + ]); + + return { + featuresCount: features.length, + plansCount: plans.length, + }; + }, + }); +} diff --git a/atmn/src/lib/hooks/useCreateGuides.ts b/atmn/src/lib/hooks/useCreateGuides.ts new file mode 100644 index 00000000..c6e15d0f --- /dev/null +++ b/atmn/src/lib/hooks/useCreateGuides.ts @@ -0,0 +1,68 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { useState } from "react"; +import { customerPrompt } from "../../prompts/customer.js"; +import { paymentsPrompt } from "../../prompts/payments.js"; +import { pricingPrompt } from "../../prompts/pricing.js"; +import { usagePrompt } from "../../prompts/usage.js"; + +const GUIDES_DIR = "autumn-guides"; + +type CreateGuidesState = "idle" | "creating" | "done" | "error"; + +export function useCreateGuides() { + const [state, setState] = useState("idle"); + const [filesCreated, setFilesCreated] = useState([]); + const [error, setError] = useState(null); + + const create = async (hasPricing: boolean, options?: { saveAll?: boolean }) => { + setState("creating"); + try { + const cwd = process.cwd(); + const guidesPath = path.join(cwd, GUIDES_DIR); + await fs.mkdir(guidesPath, { recursive: true }); + + const created: string[] = []; + + // Always write customer, payments, usage guides + await fs.writeFile( + path.join(guidesPath, "1_Customer_Creation.md"), + customerPrompt, + "utf-8", + ); + created.push("1_Customer_Creation.md"); + + await fs.writeFile( + path.join(guidesPath, "2_Accepting_Payments.md"), + paymentsPrompt, + "utf-8", + ); + created.push("2_Accepting_Payments.md"); + + await fs.writeFile( + path.join(guidesPath, "3_Tracking_Usage.md"), + usagePrompt, + "utf-8", + ); + created.push("3_Tracking_Usage.md"); + + // Only write pricing guide if user doesn't have pricing yet (or saveAll is true) + if (options?.saveAll || !hasPricing) { + await fs.writeFile( + path.join(guidesPath, "0_Designing_Pricing.md"), + pricingPrompt, + "utf-8", + ); + created.unshift("0_Designing_Pricing.md"); + } + + setFilesCreated(created); + setState("done"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create guides"); + setState("error"); + } + }; + + return { create, state, filesCreated, error, guidesDir: GUIDES_DIR }; +} diff --git a/atmn/src/lib/hooks/useCustomerExpanded.ts b/atmn/src/lib/hooks/useCustomerExpanded.ts new file mode 100644 index 00000000..04c5eceb --- /dev/null +++ b/atmn/src/lib/hooks/useCustomerExpanded.ts @@ -0,0 +1,91 @@ +import { useQuery } from "@tanstack/react-query"; +import { useState, useEffect } from "react"; +import { request } from "../api/client.js"; +import { getKey } from "../env/keys.js"; +import { AppEnv } from "../env/detect.js"; +import type { ApiCustomerExpanded } from "../../views/react/customers/types.js"; + +/** + * All expand params for full customer data (comma-separated for query string) + */ +const EXPAND_PARAMS = [ + "invoices", + "rewards", + "entities", + "referrals", + "subscriptions.plan", + "scheduled_subscriptions.plan", + "balances.feature", +].join(","); + +export interface UseCustomerExpandedOptions { + /** Customer ID to fetch */ + customerId: string | null; + /** Environment (sandbox/live) */ + environment?: AppEnv; + /** Whether to enable the query */ + enabled?: boolean; + /** Debounce delay in ms (default: 150ms) */ + debounceMs?: number; +} + +/** + * Custom hook to debounce a value + */ +function useDebouncedValue(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} + +/** + * TanStack Query hook for fetching a single customer with all expand params. + * Use this to lazily load full customer details when the sheet opens. + * + * Includes debouncing to prevent excessive API calls during rapid navigation. + */ +export function useCustomerExpanded({ + customerId, + environment = AppEnv.Sandbox, + enabled = true, + debounceMs = 150, +}: UseCustomerExpandedOptions) { + // Debounce the customer ID to prevent rapid API calls while scrolling + const debouncedCustomerId = useDebouncedValue(customerId, debounceMs); + + return useQuery({ + queryKey: ["customer", debouncedCustomerId, "expanded", environment], + queryFn: async () => { + if (!debouncedCustomerId) { + throw new Error("Customer ID is required"); + } + + const secretKey = getKey(environment); + + const response = await request({ + method: "GET", + path: `/v1/customers/${encodeURIComponent(debouncedCustomerId)}`, + secretKey, + queryParams: { + expand: EXPAND_PARAMS, + }, + }); + + return response; + }, + enabled: enabled && !!debouncedCustomerId, + staleTime: 30_000, + // Don't refetch on window focus for detail views + refetchOnWindowFocus: false, + }); +} diff --git a/atmn/src/lib/hooks/useCustomerNavigation.ts b/atmn/src/lib/hooks/useCustomerNavigation.ts new file mode 100644 index 00000000..e4416afc --- /dev/null +++ b/atmn/src/lib/hooks/useCustomerNavigation.ts @@ -0,0 +1,215 @@ +import { useReducer, useCallback } from "react"; +import type { ApiCustomer } from "../api/endpoints/customers.js"; + +export type FocusTarget = "table" | "sheet" | "search"; + +export interface NavigationState { + page: number; + selectedIndex: number; + sheetOpen: boolean; + searchOpen: boolean; + searchQuery: string; + focusTarget: FocusTarget; + selectedCustomer: ApiCustomer | null; + copiedFeedback: boolean; +} + +export type NavigationAction = + | { type: "MOVE_UP" } + | { type: "MOVE_DOWN"; maxIndex: number } + | { type: "NEXT_PAGE"; canNavigate: boolean } + | { type: "PREV_PAGE" } + | { type: "OPEN_SHEET"; customer: ApiCustomer } + | { type: "CLOSE_SHEET" } + | { type: "TOGGLE_FOCUS" } + | { type: "COPY_ID" } + | { type: "CLEAR_COPIED_FEEDBACK" } + | { type: "SELECT_CUSTOMER"; customer: ApiCustomer; index: number } + | { type: "OPEN_SEARCH" } + | { type: "CLOSE_SEARCH" } + | { type: "SET_SEARCH_QUERY"; query: string }; + +const initialState: NavigationState = { + page: 1, + selectedIndex: 0, + sheetOpen: false, + searchOpen: false, + searchQuery: "", + focusTarget: "table", + selectedCustomer: null, + copiedFeedback: false, +}; + +function navigationReducer( + state: NavigationState, + action: NavigationAction, +): NavigationState { + switch (action.type) { + case "MOVE_UP": + if (state.selectedIndex > 0) { + return { ...state, selectedIndex: state.selectedIndex - 1 }; + } + return state; + + case "MOVE_DOWN": + if (state.selectedIndex < action.maxIndex) { + return { ...state, selectedIndex: state.selectedIndex + 1 }; + } + return state; + + case "NEXT_PAGE": + if (action.canNavigate) { + return { ...state, page: state.page + 1, selectedIndex: 0 }; + } + return state; + + case "PREV_PAGE": + if (state.page > 1) { + return { ...state, page: state.page - 1, selectedIndex: 0 }; + } + return state; + + case "OPEN_SHEET": + return { + ...state, + sheetOpen: true, + focusTarget: "sheet", + selectedCustomer: action.customer, + }; + + case "CLOSE_SHEET": + return { + ...state, + sheetOpen: false, + focusTarget: "table", + }; + + case "TOGGLE_FOCUS": + if (state.sheetOpen) { + return { + ...state, + focusTarget: state.focusTarget === "table" ? "sheet" : "table", + }; + } + return state; + + case "COPY_ID": + return { ...state, copiedFeedback: true }; + + case "CLEAR_COPIED_FEEDBACK": + return { ...state, copiedFeedback: false }; + + case "SELECT_CUSTOMER": + return { + ...state, + selectedIndex: action.index, + selectedCustomer: action.customer, + }; + + case "OPEN_SEARCH": + return { + ...state, + searchOpen: true, + focusTarget: "search", + }; + + case "CLOSE_SEARCH": + return { + ...state, + searchOpen: false, + focusTarget: "table", + }; + + case "SET_SEARCH_QUERY": + return { + ...state, + searchQuery: action.query, + searchOpen: false, + focusTarget: "table", + page: 1, + selectedIndex: 0, + }; + + default: + return state; + } +} + +export function useCustomerNavigation() { + const [state, dispatch] = useReducer(navigationReducer, initialState); + + const moveUp = useCallback(() => { + dispatch({ type: "MOVE_UP" }); + }, []); + + const moveDown = useCallback((maxIndex: number) => { + dispatch({ type: "MOVE_DOWN", maxIndex }); + }, []); + + const nextPage = useCallback((canNavigate: boolean) => { + dispatch({ type: "NEXT_PAGE", canNavigate }); + }, []); + + const prevPage = useCallback(() => { + dispatch({ type: "PREV_PAGE" }); + }, []); + + const openSheet = useCallback((customer: ApiCustomer) => { + dispatch({ type: "OPEN_SHEET", customer }); + }, []); + + const closeSheet = useCallback(() => { + dispatch({ type: "CLOSE_SHEET" }); + }, []); + + const toggleFocus = useCallback(() => { + dispatch({ type: "TOGGLE_FOCUS" }); + }, []); + + const copyId = useCallback(() => { + dispatch({ type: "COPY_ID" }); + }, []); + + const clearCopiedFeedback = useCallback(() => { + dispatch({ type: "CLEAR_COPIED_FEEDBACK" }); + }, []); + + const selectCustomer = useCallback((customer: ApiCustomer, index: number) => { + dispatch({ type: "SELECT_CUSTOMER", customer, index }); + }, []); + + const openSearch = useCallback(() => { + dispatch({ type: "OPEN_SEARCH" }); + }, []); + + const closeSearch = useCallback(() => { + dispatch({ type: "CLOSE_SEARCH" }); + }, []); + + const setSearchQuery = useCallback((query: string) => { + dispatch({ type: "SET_SEARCH_QUERY", query }); + }, []); + + const clearSearch = useCallback(() => { + dispatch({ type: "SET_SEARCH_QUERY", query: "" }); + }, []); + + return { + state, + dispatch, + moveUp, + moveDown, + nextPage, + prevPage, + openSheet, + closeSheet, + toggleFocus, + copyId, + clearCopiedFeedback, + selectCustomer, + openSearch, + closeSearch, + setSearchQuery, + clearSearch, + }; +} diff --git a/atmn/src/lib/hooks/useCustomers.ts b/atmn/src/lib/hooks/useCustomers.ts new file mode 100644 index 00000000..8bf3955d --- /dev/null +++ b/atmn/src/lib/hooks/useCustomers.ts @@ -0,0 +1,61 @@ +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { request } from "../api/client.js"; +import type { ApiCustomer } from "../api/endpoints/customers.js"; +import { AppEnv } from "../env/detect.js"; +import { getKey } from "../env/keys.js"; + +/** + * Response from POST /v1/customers/list + */ +export interface ListCustomersResponse { + list: ApiCustomer[]; + has_more: boolean; + offset: number; + limit: number; + /** Number of items in CURRENT PAGE only, NOT total in DB */ + total: number; +} + +export interface UseCustomersOptions { + page: number; + pageSize?: number; + environment?: AppEnv; + /** Search query to filter customers by id, name, or email */ + search?: string; +} + +/** + * TanStack Query hook for fetching paginated customers + */ +export function useCustomers({ + page, + pageSize = 50, + environment = AppEnv.Sandbox, + search, +}: UseCustomersOptions) { + const offset = (page - 1) * pageSize; + + return useQuery({ + queryKey: ["customers", { offset, limit: pageSize, environment, search }], + queryFn: async () => { + const secretKey = getKey(environment); + + const body: Record = { + limit: pageSize, + offset, + search: search?.trim() ?? undefined, + }; + + const response = await request({ + method: "POST", + path: "/v1/customers/list", + secretKey, + body, + }); + + return response; + }, + placeholderData: keepPreviousData, + staleTime: 30_000, + }); +} diff --git a/atmn/src/lib/hooks/useEnvironmentStore.ts b/atmn/src/lib/hooks/useEnvironmentStore.ts new file mode 100644 index 00000000..cab5e171 --- /dev/null +++ b/atmn/src/lib/hooks/useEnvironmentStore.ts @@ -0,0 +1,80 @@ +import fs from "node:fs"; +import path from "node:path"; + +export interface StoreEnvResult { + envPath: string; + created: boolean; + keysWritten: string[]; +} + +export interface StoreEnvOptions { + /** Force overwrite existing keys without prompting */ + forceOverwrite?: boolean; + /** Working directory (defaults to process.cwd()) */ + cwd?: string; +} + +/** + * Store API keys to .env file + * Non-interactive version for use in React/Ink components + */ +export async function storeEnvKeys( + keys: { prodKey: string; sandboxKey: string }, + options?: StoreEnvOptions, +): Promise { + const cwd = options?.cwd ?? process.cwd(); + const forceOverwrite = options?.forceOverwrite ?? true; + + const envPath = path.join(cwd, ".env"); + const envLocalPath = path.join(cwd, ".env.local"); + + const keysWritten: string[] = []; + let created = false; + + const keyMap = { + AUTUMN_PROD_SECRET_KEY: keys.prodKey, + AUTUMN_SECRET_KEY: keys.sandboxKey, + }; + + // Check if .env exists + if (fs.existsSync(envPath)) { + // Read and update existing file + const content = fs.readFileSync(envPath, "utf-8"); + const lines = content.split("\n"); + + for (const [varName, value] of Object.entries(keyMap)) { + const existingIndex = lines.findIndex((line) => + line.startsWith(`${varName}=`), + ); + + if (existingIndex !== -1) { + if (forceOverwrite) { + lines[existingIndex] = `${varName}=${value}`; + keysWritten.push(varName); + } + // If not forceOverwrite, skip this key + } else { + // Key doesn't exist, add it + lines.push(`${varName}=${value}`); + keysWritten.push(varName); + } + } + + fs.writeFileSync(envPath, lines.join("\n")); + } else { + // Create new .env file + created = true; + const envContent = Object.entries(keyMap) + .map(([key, value]) => `${key}=${value}`) + .join("\n"); + + fs.writeFileSync(envPath, envContent + "\n"); + keysWritten.push(...Object.keys(keyMap)); + } + + return { + envPath, + created, + keysWritten, + }; +} diff --git a/atmn/src/lib/hooks/useHeadlessAuth.ts b/atmn/src/lib/hooks/useHeadlessAuth.ts new file mode 100644 index 00000000..51bcdb4b --- /dev/null +++ b/atmn/src/lib/hooks/useHeadlessAuth.ts @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { getOrgMe } from "../../../source/core/requests/orgRequests.js"; +import { readFromEnv } from "../utils.js"; +import { CLI_CLIENT_ID } from "../../commands/auth/constants.js"; +import { + getApiKeysWithToken, + startOAuthFlow, +} from "../../commands/auth/oauth.js"; +import { storeEnvKeys } from "./useEnvironmentStore.js"; + +export interface OrgInfo { + name: string; + slug: string; +} + +export type HeadlessAuthState = + | "checking" + | "not_authenticated" + | "authenticating" + | "authenticated" + | "error"; + +export interface UseHeadlessAuthOptions { + onComplete?: (orgInfo: OrgInfo) => void; + headless?: boolean; +} + +export interface UseHeadlessAuthReturn { + authState: HeadlessAuthState; + orgInfo: OrgInfo | null; + error: string | null; +} + +export function useHeadlessAuth( + options?: UseHeadlessAuthOptions, +): UseHeadlessAuthReturn { + const { onComplete, headless = false } = options ?? {}; + + const [authState, setAuthState] = useState("checking"); + const [orgInfo, setOrgInfo] = useState(null); + const [error, setError] = useState(null); + const hasStarted = useRef(false); + const isCompleted = useRef(false); + + const performAuth = useCallback(async () => { + setAuthState("authenticating"); + + try { + const { tokens } = await startOAuthFlow(CLI_CLIENT_ID, { headless }); + const { sandboxKey, prodKey } = await getApiKeysWithToken( + tokens.access_token, + ); + + // Use storeEnvKeys (non-interactive) instead of storeToEnv (interactive) + await storeEnvKeys({ prodKey, sandboxKey }, { forceOverwrite: true }); + + // Fetch org info with new key + const info = await getOrgMe(); + setOrgInfo(info); + setAuthState("authenticated"); + + if (!isCompleted.current && onComplete) { + isCompleted.current = true; + onComplete(info); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Authentication failed"); + setAuthState("error"); + } + }, [headless, onComplete]); + + useEffect(() => { + if (hasStarted.current) return; + hasStarted.current = true; + + const checkAuth = async () => { + try { + const apiKey = readFromEnv({ bypass: true }); + + if (!apiKey) { + setAuthState("not_authenticated"); + await performAuth(); + return; + } + + // Verify the key works by fetching org info + const info = await getOrgMe(); + setOrgInfo(info); + setAuthState("authenticated"); + + if (!isCompleted.current && onComplete) { + isCompleted.current = true; + onComplete(info); + } + } catch { + // Key exists but is invalid + setAuthState("not_authenticated"); + await performAuth(); + } + }; + + checkAuth(); + }, [performAuth, onComplete]); + + return { authState, orgInfo, error }; +} diff --git a/atmn/src/lib/hooks/useLogin.ts b/atmn/src/lib/hooks/useLogin.ts new file mode 100644 index 00000000..5a4a9d09 --- /dev/null +++ b/atmn/src/lib/hooks/useLogin.ts @@ -0,0 +1,179 @@ +import { useMutation } from "@tanstack/react-query"; +import { useCallback, useEffect, useState, useRef } from "react"; +import { readFromEnv } from "../utils.js"; +import { + startOAuthFlow, + getApiKeysWithToken, +} from "../../commands/auth/oauth.js"; +import { CLI_CLIENT_ID } from "../../commands/auth/constants.js"; +import { getOrgMe } from "../../../source/core/requests/orgRequests.js"; +import { storeEnvKeys } from "./useEnvironmentStore.js"; + +export type LoginPhase = + | "checking" + | "confirm_reauth" + | "opening_browser" + | "waiting_auth" + | "creating_keys" + | "saving_keys" + | "complete" + | "error"; + +export interface OrgInfo { + name: string; + slug: string; +} + +export interface UseLoginOptions { + onComplete?: () => void; +} + +export interface UseLoginReturn { + phase: LoginPhase; + error: string | null; + orgInfo: OrgInfo | null; + authUrl: string | null; + isAlreadyAuthenticated: boolean; + confirmReauth: () => void; + cancelReauth: () => void; + startTime: number; +} + +export function useLogin(options?: UseLoginOptions): UseLoginReturn { + const onComplete = options?.onComplete; + + const [startTime] = useState(Date.now()); + const [phase, setPhase] = useState("checking"); + const [error, setError] = useState(null); + const [orgInfo, setOrgInfo] = useState(null); + const [authUrl, setAuthUrl] = useState(null); + const [isAlreadyAuthenticated, setIsAlreadyAuthenticated] = useState(false); + + // Track if mutations have been started + const checkStarted = useRef(false); + const oauthStarted = useRef(false); + + // Check if already authenticated + const checkAuthMutation = useMutation({ + mutationFn: async () => { + const existingKey = readFromEnv({ bypass: true }); + return !!existingKey; + }, + onSuccess: (hasExistingKey) => { + setIsAlreadyAuthenticated(hasExistingKey); + if (hasExistingKey) { + setPhase("confirm_reauth"); + } else { + setPhase("opening_browser"); + } + }, + onError: () => { + // No key found, proceed with login + setPhase("opening_browser"); + }, + }); + + // OAuth flow mutation + const oauthMutation = useMutation({ + mutationFn: async () => { + setPhase("waiting_auth"); + const { tokens } = await startOAuthFlow(CLI_CLIENT_ID); + return tokens; + }, + onSuccess: (tokens) => { + keysMutation.mutate(tokens.access_token); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Authentication failed"); + setPhase("error"); + }, + }); + + // Create API keys mutation + const keysMutation = useMutation({ + mutationFn: async (accessToken: string) => { + setPhase("creating_keys"); + return await getApiKeysWithToken(accessToken); + }, + onSuccess: (data) => { + saveMutation.mutate(data); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to create API keys"); + setPhase("error"); + }, + }); + + // Save keys mutation + const saveMutation = useMutation({ + mutationFn: async (data: { + sandboxKey: string; + prodKey: string; + orgId: string; + }) => { + setPhase("saving_keys"); + await storeEnvKeys( + { prodKey: data.prodKey, sandboxKey: data.sandboxKey }, + { forceOverwrite: true }, + ); + // Fetch org info + const org = await getOrgMe(); + return org; + }, + onSuccess: (org) => { + setOrgInfo({ + name: org.name, + slug: org.slug, + }); + setPhase("complete"); + if (onComplete) { + setTimeout(onComplete, 1500); + } + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to save keys"); + setPhase("error"); + }, + }); + + // Confirm re-authentication + const confirmReauth = useCallback(() => { + oauthStarted.current = false; // Reset so OAuth can start + setPhase("opening_browser"); + }, []); + + // Cancel re-authentication + const cancelReauth = useCallback(() => { + setPhase("complete"); + if (onComplete) { + setTimeout(onComplete, 500); + } + }, [onComplete]); + + // Auto-start checking on mount + useEffect(() => { + if (phase === "checking" && !checkStarted.current) { + checkStarted.current = true; + checkAuthMutation.mutate(); + } + }, [phase, checkAuthMutation]); + + // Start OAuth when entering opening_browser phase + useEffect(() => { + if (phase === "opening_browser" && !oauthStarted.current) { + oauthStarted.current = true; + oauthMutation.mutate(); + } + }, [phase, oauthMutation]); + + return { + phase, + error, + orgInfo, + authUrl, + isAlreadyAuthenticated, + confirmReauth, + cancelReauth, + startTime, + }; +} diff --git a/atmn/src/lib/hooks/useNuke.ts b/atmn/src/lib/hooks/useNuke.ts new file mode 100644 index 00000000..83188670 --- /dev/null +++ b/atmn/src/lib/hooks/useNuke.ts @@ -0,0 +1,187 @@ +/** + * useNuke hook - Manages nuke operation with TanStack Query + */ + +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { + deleteCustomersBatch, + deleteFeaturesSequential, + deletePlansSequential, +} from "../../commands/nuke/deletions.js"; +import type { + DeletionProgress, + NukePhaseStats, +} from "../../commands/nuke/types.js"; +import { getKey } from "../env/index.js"; +import { AppEnv } from "../env/detect.js"; +import { + deleteCustomer, + fetchCustomers, + type ApiCustomer, +} from "../api/endpoints/customers.js"; +import { deletePlan, fetchPlans } from "../api/endpoints/plans.js"; +import { deleteFeature, fetchFeatures } from "../api/endpoints/features.js"; + +export interface UseNukeOptions { + onComplete?: () => void; + onError?: (error: Error) => void; +} + +export interface UseNukeReturn { + /** Start the nuke operation */ + nuke: () => void; + /** Current phase statistics */ + phases: NukePhaseStats[]; + /** Currently active phase */ + activePhase: "customers" | "plans" | "features" | null; + /** Total elapsed time in seconds */ + totalElapsed: number; + /** Whether nuke is currently running */ + isNuking: boolean; + /** Error if any */ + error: Error | null; +} + +/** + * Hook to manage nuke operation with progress tracking + */ +export function useNuke(options?: UseNukeOptions): UseNukeReturn { + const [phases, setPhases] = useState([ + { phase: "customers", current: 0, total: 0, rate: 0, completed: false }, + { phase: "plans", current: 0, total: 0, rate: 0, completed: false }, + { phase: "features", current: 0, total: 0, rate: 0, completed: false }, + ]); + const [activePhase, setActivePhase] = useState< + "customers" | "plans" | "features" | null + >(null); + const [startTime, setStartTime] = useState(0); + const [currentElapsed, setCurrentElapsed] = useState(0); + + // Update elapsed time periodically + const updateElapsed = () => { + if (startTime > 0) { + setCurrentElapsed((Date.now() - startTime) / 1000); + } + }; + + const updatePhase = (progress: DeletionProgress) => { + setPhases((prev) => + prev.map((p) => + p.phase === progress.phase + ? { + ...p, + current: progress.current, + total: progress.total, + rate: progress.rate || 0, + } + : p + ) + ); + updateElapsed(); + }; + + const completePhase = ( + phase: "customers" | "plans" | "features", + duration: number + ) => { + setPhases((prev) => + prev.map((p) => (p.phase === phase ? { ...p, completed: true, duration } : p)) + ); + updateElapsed(); + }; + + const nukeMutation = useMutation({ + mutationFn: async () => { + const secretKey = getKey(AppEnv.Sandbox); + setStartTime(Date.now()); + + // Phase 1: Customers + const customersPhaseStart = Date.now(); + setActivePhase("customers"); + + const customers = await fetchCustomers({ secretKey }); + setPhases((prev) => + prev.map((p) => + p.phase === "customers" ? { ...p, total: customers.length } : p + ) + ); + + await deleteCustomersBatch( + customers.map((c: ApiCustomer) => ({ id: c.id })), + async (id: string) => { + await deleteCustomer({ secretKey, customerId: id }); + }, + updatePhase + ); + + completePhase("customers", (Date.now() - customersPhaseStart) / 1000); + + // Phase 2: Plans + const plansPhaseStart = Date.now(); + setActivePhase("plans"); + + const plans = await fetchPlans({ secretKey, includeArchived: true }); + setPhases((prev) => + prev.map((p) => (p.phase === "plans" ? { ...p, total: plans.length } : p)) + ); + + await deletePlansSequential( + plans.map((p) => ({ id: p.id })), + async (id: string, allVersions: boolean) => { + await deletePlan({ secretKey, planId: id, allVersions }); + }, + updatePhase + ); + + completePhase("plans", (Date.now() - plansPhaseStart) / 1000); + + // Wait a bit for DB to propagate plan deletions + // This prevents race conditions where features are still referenced + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Phase 3: Features + const featuresPhaseStart = Date.now(); + setActivePhase("features"); + + const features = await fetchFeatures({ secretKey }); + setPhases((prev) => + prev.map((p) => + p.phase === "features" ? { ...p, total: features.length } : p + ) + ); + + await deleteFeaturesSequential( + features.map((f) => ({ id: f.id, type: f.type })), + async (id: string) => { + await deleteFeature({ secretKey, featureId: id }); + }, + updatePhase + ); + + completePhase("features", (Date.now() - featuresPhaseStart) / 1000); + + // Done! + setActivePhase(null); + }, + onSuccess: () => { + if (options?.onComplete) { + options.onComplete(); + } + }, + onError: (error: Error) => { + if (options?.onError) { + options.onError(error); + } + }, + }); + + return { + nuke: () => nukeMutation.mutate(), + phases, + activePhase, + totalElapsed: currentElapsed, + isNuking: nukeMutation.isPending, + error: nukeMutation.error, + }; +} diff --git a/atmn/src/lib/hooks/useNukeData.ts b/atmn/src/lib/hooks/useNukeData.ts new file mode 100644 index 00000000..61ed5a5a --- /dev/null +++ b/atmn/src/lib/hooks/useNukeData.ts @@ -0,0 +1,49 @@ +/** + * useNukeData hook - Fetch data needed for nuke UI + */ + +import { useQuery } from "@tanstack/react-query"; +import { getKey } from "../env/index.js"; +import { AppEnv } from "../env/detect.js"; +import { fetchCustomers } from "../api/endpoints/customers.js"; +import { fetchPlans } from "../api/endpoints/plans.js"; +import { fetchFeatures } from "../api/endpoints/features.js"; +import { getOrgMe } from "../../../source/core/requests/orgRequests.js"; + +export interface NukeData { + orgName: string; + orgSlug: string; + customersCount: number; + plansCount: number; + featuresCount: number; +} + +/** + * Fetch organization info and counts for nuke UI + */ +export function useNukeData() { + return useQuery({ + queryKey: ["nuke-data"], + queryFn: async (): Promise => { + const secretKey = getKey(AppEnv.Sandbox); + + // Fetch all data in parallel + const [org, customers, plans, features] = await Promise.all([ + getOrgMe(), + fetchCustomers({ secretKey }), + fetchPlans({ secretKey, includeArchived: true }), + fetchFeatures({ secretKey }), + ]); + + return { + orgName: org.name, + orgSlug: org.slug, + customersCount: customers.length, + plansCount: plans.length, + featuresCount: features.length, + }; + }, + staleTime: 0, // Always fetch fresh data + retry: 1, + }); +} diff --git a/atmn/src/lib/hooks/useOrganization.ts b/atmn/src/lib/hooks/useOrganization.ts new file mode 100644 index 00000000..02459a9f --- /dev/null +++ b/atmn/src/lib/hooks/useOrganization.ts @@ -0,0 +1,25 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchOrganizationMe } from "../api/endpoints/index.js"; +import { AppEnv, getKey } from "../env/index.js"; + +export interface OrganizationInfo { + name: string; + slug: string; + environment: "Sandbox" | "Live"; +} + +export function useOrganization(cwd?: string) { + return useQuery({ + queryKey: ["organization", cwd], + queryFn: async (): Promise => { + const sandboxKey = getKey(AppEnv.Sandbox, cwd); + const orgData = await fetchOrganizationMe({ secretKey: sandboxKey }); + + return { + name: orgData.name, + slug: orgData.slug, + environment: orgData.env === AppEnv.Sandbox ? "Sandbox" : "Live", + }; + }, + }); +} diff --git a/atmn/src/lib/hooks/usePull.ts b/atmn/src/lib/hooks/usePull.ts new file mode 100644 index 00000000..9fbda137 --- /dev/null +++ b/atmn/src/lib/hooks/usePull.ts @@ -0,0 +1,115 @@ +import { readFileSync } from "node:fs"; +import { useMutation } from "@tanstack/react-query"; +import { useEffect } from "react"; +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import { AppEnv } from "../env/index.js"; +import { pull } from "../../commands/pull/pull.js"; +import { useOrganization } from "./useOrganization.js"; + +export interface GeneratedFile { + name: string; + path: string; + lines: number; +} + +interface PullParams { + cwd: string; + generateSdkTypes?: boolean; + environment?: AppEnv; +} + +interface PullData { + features: Feature[]; + plans: Plan[]; + files: GeneratedFile[]; +} + +function countLines(filePath: string): number { + try { + const content = readFileSync(filePath, "utf-8"); + return content.split("\n").length; + } catch { + return 0; + } +} + +export function usePull(options?: { + cwd?: string; + environment?: AppEnv; + onComplete?: () => void; +}) { + const effectiveCwd = options?.cwd ?? process.cwd(); + const environment = options?.environment ?? AppEnv.Sandbox; + const onComplete = options?.onComplete; + + // Get org info using TanStack Query (this IS a query) + const orgQuery = useOrganization(effectiveCwd); + + // Use mutation for the pull operation + const pullMutation = useMutation({ + mutationFn: async (params: PullParams): Promise => { + const result = await pull({ + generateSdkTypes: params.generateSdkTypes ?? true, + cwd: params.cwd, + environment: params.environment ?? AppEnv.Sandbox, + }); + + const files: GeneratedFile[] = []; + + if (result.configPath) { + files.push({ + name: "autumn.config.ts", + path: result.configPath, + lines: countLines(result.configPath), + }); + } + + if (result.sdkTypesPath) { + files.push({ + name: "@useautumn-sdk.d.ts", + path: result.sdkTypesPath, + lines: countLines(result.sdkTypesPath), + }); + } + + return { + features: result.features, + plans: result.plans, + files, + }; + }, + onSuccess: () => { + // Call onComplete callback after successful pull + if (onComplete) { + // Add a small delay to let the UI show the success state + setTimeout(() => { + onComplete(); + }, 1000); + } + }, + }); + + // Auto-trigger pull when org info is ready + useEffect(() => { + if (orgQuery.isSuccess && !pullMutation.isPending && !pullMutation.isSuccess) { + pullMutation.mutate({ + cwd: effectiveCwd, + generateSdkTypes: true, + environment, + }); + } + }, [orgQuery.isSuccess, pullMutation, effectiveCwd, environment]); + + const error = orgQuery.error || pullMutation.error; + + return { + orgInfo: orgQuery.data, + features: pullMutation.data?.features ?? [], + plans: pullMutation.data?.plans ?? [], + files: pullMutation.data?.files ?? [], + isOrgLoading: orgQuery.isLoading, + isPullLoading: pullMutation.isPending, + isSuccess: pullMutation.isSuccess, + error: error ? (error instanceof Error ? error.message : String(error)) : null, + }; +} diff --git a/atmn/src/lib/hooks/usePush.ts b/atmn/src/lib/hooks/usePush.ts new file mode 100644 index 00000000..4dbb52c0 --- /dev/null +++ b/atmn/src/lib/hooks/usePush.ts @@ -0,0 +1,703 @@ +import fs from "node:fs"; +import path, { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { useMutation } from "@tanstack/react-query"; +import createJiti from "jiti"; +import { useCallback, useEffect, useState } from "react"; +import type { Feature, Plan } from "../../../source/compose/models/index.js"; +import { + analyzePush, + archiveFeature as archiveFeatureApi, + archivePlan as archivePlanApi, + deleteFeature as deleteFeatureApi, + deletePlan as deletePlanApi, + fetchRemoteData, + type PushAnalysis, + type PushResult, + pushFeature, + pushPlan, + unarchiveFeature as unarchiveFeatureApi, + unarchivePlan as unarchivePlanApi, + createFeatureArchivedPrompt, + createFeatureDeletePrompt, + createPlanArchivedPrompt, + createPlanDeletePrompt, + createPlanVersioningPrompt, + createProdConfirmationPrompt, + type PushPrompt, +} from "../../commands/push/index.js"; +import { AppEnv } from "../env/index.js"; +import { type OrganizationInfo, useOrganization } from "./useOrganization.js"; + +export type PushPhase = + | "loading_config" + | "loading_org" + | "analyzing" + | "no_changes" + | "confirming" + | "pushing_features" + | "pushing_plans" + | "deleting" + | "complete" + | "error"; + +export type FeatureStatus = + | "pending" + | "pushing" + | "created" + | "updated" + | "deleted" + | "archived" + | "skipped"; + +export type PlanStatus = + | "pending" + | "pushing" + | "created" + | "updated" + | "versioned" + | "deleted" + | "archived" + | "skipped"; + +export interface UsePushOptions { + cwd?: string; + environment?: AppEnv; + yes?: boolean; + onComplete?: () => void; +} + +interface LocalConfig { + features: Feature[]; + plans: Plan[]; +} + +// Load local config file +async function loadLocalConfig(cwd: string): Promise { + const configPath = path.join(cwd, "autumn.config.ts"); + + if (!fs.existsSync(configPath)) { + throw new Error( + `Config file not found at ${configPath}. Run 'atmn pull' first.`, + ); + } + + const absolutePath = resolve(configPath); + const fileUrl = pathToFileURL(absolutePath).href; + + const jiti = createJiti(import.meta.url); + const mod = await jiti.import(fileUrl); + + const plans: Plan[] = []; + const features: Feature[] = []; + + // Check for old-style default export first + const modRecord = mod as { default?: unknown } & Record; + const defaultExport = modRecord.default as + | { + plans?: Plan[]; + features?: Feature[]; + products?: Plan[]; + } + | undefined; + + if (defaultExport?.plans && defaultExport?.features) { + if (Array.isArray(defaultExport.plans)) { + plans.push(...defaultExport.plans); + } + if (Array.isArray(defaultExport.features)) { + features.push(...defaultExport.features); + } + } else if (defaultExport?.products && defaultExport?.features) { + // Legacy format + if (Array.isArray(defaultExport.products)) { + plans.push(...defaultExport.products); + } + if (Array.isArray(defaultExport.features)) { + features.push(...defaultExport.features); + } + } else { + // New format: individual named exports + for (const [key, value] of Object.entries(modRecord)) { + if (key === "default") continue; + + const obj = value as { features?: unknown; type?: unknown }; + // Detect if it's a plan (has features array) or feature (has type) + if (obj && typeof obj === "object") { + if (Array.isArray(obj.features)) { + plans.push(obj as unknown as Plan); + } else if ("type" in obj) { + features.push(obj as unknown as Feature); + } + } + } + } + + return { features, plans }; +} + +export function usePush(options?: UsePushOptions) { + const effectiveCwd = options?.cwd ?? process.cwd(); + const environment = options?.environment ?? AppEnv.Sandbox; + const yes = options?.yes ?? false; + const onComplete = options?.onComplete; + + const [startTime] = useState(Date.now()); + const [phase, setPhase] = useState("loading_config"); + const [localConfig, setLocalConfig] = useState(null); + const [analysis, setAnalysis] = useState(null); + const [error, setError] = useState(null); + + // Prompt queue management + const [promptQueue, setPromptQueue] = useState([]); + const [currentPromptIndex, setCurrentPromptIndex] = useState(0); + const [promptResponses, setPromptResponses] = useState>( + new Map(), + ); + + // Progress tracking + const [featureProgress, setFeatureProgress] = useState< + Map + >(new Map()); + const [planProgress, setPlanProgress] = useState>( + new Map(), + ); + + // Results + const [result, setResult] = useState(null); + + // Remote plans for push operations + const [remotePlans, setRemotePlans] = useState([]); + + // Get org info + const orgQuery = useOrganization(effectiveCwd); + + // Current prompt + const currentPrompt = + currentPromptIndex < promptQueue.length + ? promptQueue[currentPromptIndex] + : null; + + // Load local config + const loadConfigMutation = useMutation({ + mutationFn: async () => { + const config = await loadLocalConfig(effectiveCwd); + + // Validate config for missing required fields + const { validateConfig, formatValidationErrors } = await import( + "../../commands/push/validate.js" + ); + const validation = validateConfig(config.features, config.plans); + if (!validation.valid) { + throw new Error( + `Config validation failed:\n\n${formatValidationErrors(validation.errors)}` + ); + } + + return config; + }, + onSuccess: (config) => { + setLocalConfig(config); + setPhase("loading_org"); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to load config"); + setPhase("error"); + }, + }); + + // Analyze push + const analyzeMutation = useMutation({ + mutationFn: async (config: LocalConfig) => { + const remoteData = await fetchRemoteData(); + setRemotePlans(remoteData.plans); + return await analyzePush(config.features, config.plans); + }, + onSuccess: (analysisResult) => { + setAnalysis(analysisResult); + + // Check if there are any meaningful changes to push + // Note: featuresToUpdate/plansToUpdate always contain items when features/plans exist + // They're no-ops if nothing actually changed, so we only count them if: + // - There are creates/deletes (actual changes) + // - There are archived items to handle + // - Any plan will create a new version (actual change) + const hasVersioningPlans = analysisResult.plansToUpdate.some( + (p) => p.willVersion, + ); + const hasChanges = + analysisResult.featuresToCreate.length > 0 || + analysisResult.featuresToDelete.length > 0 || + analysisResult.plansToCreate.length > 0 || + analysisResult.plansToDelete.length > 0 || + analysisResult.archivedFeatures.length > 0 || + analysisResult.archivedPlans.length > 0 || + hasVersioningPlans; + + if (!hasChanges) { + // No changes to push - show "already in sync" state + setPhase("no_changes"); + if (onComplete) { + setTimeout(onComplete, 1000); + } + return; + } + + // Build prompt queue + const prompts: PushPrompt[] = []; + + // Production confirmation + if (environment === AppEnv.Live) { + prompts.push(createProdConfirmationPrompt()); + } + + // Archived features + for (const feature of analysisResult.archivedFeatures) { + prompts.push(createFeatureArchivedPrompt(feature)); + } + + // Archived plans + for (const plan of analysisResult.archivedPlans) { + prompts.push(createPlanArchivedPrompt(plan)); + } + + // Plans that will version + for (const planInfo of analysisResult.plansToUpdate) { + if (planInfo.willVersion) { + prompts.push(createPlanVersioningPrompt(planInfo)); + } + } + + // Feature deletions + for (const info of analysisResult.featuresToDelete) { + prompts.push(createFeatureDeletePrompt(info)); + } + + // Plan deletions + for (const info of analysisResult.plansToDelete) { + prompts.push(createPlanDeletePrompt(info)); + } + + setPromptQueue(prompts); + + // If yes flag or no prompts, proceed directly + if (yes || prompts.length === 0) { + // Auto-respond to all prompts with appropriate defaults + if (yes) { + const responses = new Map(); + for (const prompt of prompts) { + // Special case: prod confirmation should auto-confirm with --yes + if (prompt.type === "prod_confirmation") { + responses.set(prompt.id, "confirm"); + continue; + } + // For all other prompts, use the default option + const defaultOption = prompt.options.find((o) => o.isDefault); + responses.set( + prompt.id, + defaultOption?.value || prompt.options[0]?.value || "confirm", + ); + } + setPromptResponses(responses); + } + setCurrentPromptIndex(prompts.length); + setPhase("pushing_features"); + } else { + setPhase("confirming"); + } + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to analyze push"); + setPhase("error"); + }, + }); + + // Push features mutation + const pushFeaturesMutation = useMutation({ + mutationFn: async (config: LocalConfig) => { + const created: string[] = []; + const updated: string[] = []; + const skipped: string[] = []; + + // Check which archived features should be unarchived + for (const feature of analysis?.archivedFeatures || []) { + const response = promptResponses.get( + promptQueue.find( + (p) => p.type === "feature_archived" && p.entityId === feature.id, + )?.id || "", + ); + if (response === "unarchive") { + setFeatureProgress((prev) => + new Map(prev).set(feature.id, "pushing"), + ); + await unarchiveFeatureApi(feature.id); + } + } + + // Push all features + const allFeatures = [ + ...config.features.filter( + (f) => !analysis?.archivedFeatures.some((af) => af.id === f.id), + ), + ...config.features.filter((f) => + analysis?.archivedFeatures.some((af) => af.id === f.id), + ), + ]; + + for (const feature of allFeatures) { + // Check if this archived feature was skipped + const isArchived = analysis?.archivedFeatures.some( + (af) => af.id === feature.id, + ); + if (isArchived) { + const response = promptResponses.get( + promptQueue.find( + (p) => p.type === "feature_archived" && p.entityId === feature.id, + )?.id || "", + ); + if (response === "skip") { + skipped.push(feature.id); + setFeatureProgress((prev) => + new Map(prev).set(feature.id, "skipped"), + ); + continue; + } + } + + setFeatureProgress((prev) => new Map(prev).set(feature.id, "pushing")); + const result = await pushFeature(feature); + if (result.action === "created") { + created.push(feature.id); + setFeatureProgress((prev) => + new Map(prev).set(feature.id, "created"), + ); + } else { + updated.push(feature.id); + setFeatureProgress((prev) => + new Map(prev).set(feature.id, "updated"), + ); + } + } + + return { created, updated, skipped }; + }, + onSuccess: () => { + setPhase("pushing_plans"); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to push features"); + setPhase("error"); + }, + }); + + // Push plans mutation + const pushPlansMutation = useMutation({ + mutationFn: async (_config: LocalConfig) => { + const created: string[] = []; + const updated: string[] = []; + const versioned: string[] = []; + const skipped: string[] = []; + + // Check which archived plans should be unarchived + for (const plan of analysis?.archivedPlans || []) { + const response = promptResponses.get( + promptQueue.find( + (p) => p.type === "plan_archived" && p.entityId === plan.id, + )?.id || "", + ); + if (response === "unarchive") { + setPlanProgress((prev) => new Map(prev).set(plan.id, "pushing")); + await unarchivePlanApi(plan.id); + } + } + + // Push plans to create + for (const plan of analysis?.plansToCreate || []) { + setPlanProgress((prev) => new Map(prev).set(plan.id, "pushing")); + await pushPlan(plan, remotePlans); + created.push(plan.id); + setPlanProgress((prev) => new Map(prev).set(plan.id, "created")); + } + + // Push plans to update + for (const planInfo of analysis?.plansToUpdate || []) { + // Check if this was skipped via prompt + if (planInfo.willVersion) { + const response = promptResponses.get( + promptQueue.find( + (p) => + p.type === "plan_versioning" && p.entityId === planInfo.plan.id, + )?.id || "", + ); + if (response === "skip") { + skipped.push(planInfo.plan.id); + setPlanProgress((prev) => + new Map(prev).set(planInfo.plan.id, "skipped"), + ); + continue; + } + } + + // Check if archived plan was skipped + if (planInfo.isArchived) { + const response = promptResponses.get( + promptQueue.find( + (p) => + p.type === "plan_archived" && p.entityId === planInfo.plan.id, + )?.id || "", + ); + if (response === "skip") { + skipped.push(planInfo.plan.id); + setPlanProgress((prev) => + new Map(prev).set(planInfo.plan.id, "skipped"), + ); + continue; + } + } + + setPlanProgress((prev) => + new Map(prev).set(planInfo.plan.id, "pushing"), + ); + await pushPlan(planInfo.plan, remotePlans); + + if (planInfo.willVersion) { + versioned.push(planInfo.plan.id); + setPlanProgress((prev) => + new Map(prev).set(planInfo.plan.id, "versioned"), + ); + } else { + updated.push(planInfo.plan.id); + setPlanProgress((prev) => + new Map(prev).set(planInfo.plan.id, "updated"), + ); + } + } + + return { created, updated, versioned, skipped }; + }, + onSuccess: () => { + setPhase("deleting"); + }, + onError: (err) => { + setError(err instanceof Error ? err.message : "Failed to push plans"); + setPhase("error"); + }, + }); + + // Handle deletions mutation + const deletionsMutation = useMutation({ + mutationFn: async () => { + const featuresDeleted: string[] = []; + const featuresArchived: string[] = []; + const featuresSkipped: string[] = []; + const plansDeleted: string[] = []; + const plansArchived: string[] = []; + const plansSkipped: string[] = []; + + // Handle feature deletions based on prompt responses + for (const info of analysis?.featuresToDelete || []) { + const promptId = promptQueue.find( + (p) => p.type.startsWith("feature_delete") && p.entityId === info.id, + )?.id; + const response = promptId ? promptResponses.get(promptId) : undefined; + + // Use response from prompt (auto-set to default in --yes mode) + const action = response; + + if (action === "delete") { + setFeatureProgress((prev) => new Map(prev).set(info.id, "pushing")); + await deleteFeatureApi(info.id); + featuresDeleted.push(info.id); + setFeatureProgress((prev) => new Map(prev).set(info.id, "deleted")); + } else if (action === "archive") { + setFeatureProgress((prev) => new Map(prev).set(info.id, "pushing")); + await archiveFeatureApi(info.id); + featuresArchived.push(info.id); + setFeatureProgress((prev) => new Map(prev).set(info.id, "archived")); + } else { + // skip or no response + featuresSkipped.push(info.id); + setFeatureProgress((prev) => new Map(prev).set(info.id, "skipped")); + } + } + + // Handle plan deletions based on prompt responses + for (const info of analysis?.plansToDelete || []) { + const promptId = promptQueue.find( + (p) => p.type.startsWith("plan_delete") && p.entityId === info.id, + )?.id; + const response = promptId ? promptResponses.get(promptId) : undefined; + + // Use response from prompt (auto-set to default in --yes mode) + const action = response; + + if (action === "delete") { + setPlanProgress((prev) => new Map(prev).set(info.id, "pushing")); + await deletePlanApi(info.id); + plansDeleted.push(info.id); + setPlanProgress((prev) => new Map(prev).set(info.id, "deleted")); + } else if (action === "archive") { + setPlanProgress((prev) => new Map(prev).set(info.id, "pushing")); + await archivePlanApi(info.id); + plansArchived.push(info.id); + setPlanProgress((prev) => new Map(prev).set(info.id, "archived")); + } else { + // skip or no response + plansSkipped.push(info.id); + setPlanProgress((prev) => new Map(prev).set(info.id, "skipped")); + } + } + + return { + featuresDeleted, + featuresArchived, + featuresSkipped, + plansDeleted, + plansArchived, + plansSkipped, + }; + }, + onSuccess: (deletionResult) => { + // Combine all results + const finalResult: PushResult = { + featuresCreated: pushFeaturesMutation.data?.created || [], + featuresUpdated: pushFeaturesMutation.data?.updated || [], + featuresDeleted: deletionResult.featuresDeleted, + featuresArchived: deletionResult.featuresArchived, + featuresSkipped: [ + ...(pushFeaturesMutation.data?.skipped || []), + ...deletionResult.featuresSkipped, + ], + plansCreated: pushPlansMutation.data?.created || [], + plansUpdated: pushPlansMutation.data?.updated || [], + plansVersioned: pushPlansMutation.data?.versioned || [], + plansDeleted: deletionResult.plansDeleted, + plansArchived: deletionResult.plansArchived, + plansSkipped: [ + ...(pushPlansMutation.data?.skipped || []), + ...deletionResult.plansSkipped, + ], + }; + + setResult(finalResult); + setPhase("complete"); + + // Call onComplete after a delay + if (onComplete) { + setTimeout(onComplete, 1000); + } + }, + onError: (err) => { + setError( + err instanceof Error ? err.message : "Failed to process deletions", + ); + setPhase("error"); + }, + }); + + // Respond to prompt + const respondToPrompt = useCallback( + (value: string) => { + if (!currentPrompt) return; + + // Check for cancel on prod confirmation + if (currentPrompt.type === "prod_confirmation" && value === "cancel") { + setError("Push cancelled by user"); + setPhase("error"); + return; + } + + setPromptResponses((prev) => { + const next = new Map(prev); + next.set(currentPrompt.id, value); + return next; + }); + + // Move to next prompt or next phase + if (currentPromptIndex + 1 >= promptQueue.length) { + setCurrentPromptIndex(currentPromptIndex + 1); + setPhase("pushing_features"); + } else { + setCurrentPromptIndex(currentPromptIndex + 1); + } + }, + [currentPrompt, currentPromptIndex, promptQueue.length], + ); + + // Auto-start config loading + useEffect(() => { + if (phase === "loading_config" && !loadConfigMutation.isPending) { + loadConfigMutation.mutate(); + } + }, [phase, loadConfigMutation]); + + // Start analysis when org is ready + useEffect(() => { + if ( + phase === "loading_org" && + orgQuery.isSuccess && + localConfig && + !analyzeMutation.isPending + ) { + setPhase("analyzing"); + analyzeMutation.mutate(localConfig); + } + }, [phase, orgQuery.isSuccess, localConfig, analyzeMutation]); + + // Start pushing features + useEffect(() => { + if ( + phase === "pushing_features" && + localConfig && + !pushFeaturesMutation.isPending && + !pushFeaturesMutation.isSuccess + ) { + pushFeaturesMutation.mutate(localConfig); + } + }, [phase, localConfig, pushFeaturesMutation]); + + // Start pushing plans + useEffect(() => { + if ( + phase === "pushing_plans" && + localConfig && + !pushPlansMutation.isPending && + !pushPlansMutation.isSuccess + ) { + pushPlansMutation.mutate(localConfig); + } + }, [phase, localConfig, pushPlansMutation]); + + // Handle deletions + useEffect(() => { + if ( + phase === "deleting" && + !deletionsMutation.isPending && + !deletionsMutation.isSuccess + ) { + deletionsMutation.mutate(); + } + }, [phase, deletionsMutation]); + + // Combine errors + const combinedError = + error || orgQuery.error + ? error || + (orgQuery.error instanceof Error + ? orgQuery.error.message + : String(orgQuery.error)) + : null; + + return { + orgInfo: orgQuery.data as OrganizationInfo | null, + analysis, + phase, + currentPrompt, + respondToPrompt, + featureProgress, + planProgress, + result, + error: combinedError, + startTime, + }; +} diff --git a/atmn/src/lib/hooks/useWriteTemplateConfig.ts b/atmn/src/lib/hooks/useWriteTemplateConfig.ts new file mode 100644 index 00000000..e37a1dd1 --- /dev/null +++ b/atmn/src/lib/hooks/useWriteTemplateConfig.ts @@ -0,0 +1,46 @@ +import fs from "node:fs"; +import path from "node:path"; +import { useMutation } from "@tanstack/react-query"; +import { templateConfigs } from "../constants/templates/index.js"; +import { buildConfigFile } from "../transforms/sdkToCode/configFile.js"; + +interface WriteTemplateConfigParams { + template: string; + cwd?: string; +} + +interface WriteTemplateConfigResult { + configPath: string; + template: string; +} + +/** + * Hook to write a template config file to autumn.config.ts + * Uses useMutation for the one-time write operation + */ +export function useWriteTemplateConfig() { + return useMutation({ + mutationFn: async ( + params: WriteTemplateConfigParams, + ): Promise => { + const { template, cwd = process.cwd() } = params; + + const config = templateConfigs[template]; + if (!config) { + throw new Error(`Unknown template: ${template}`); + } + + // Generate the config file content + const configContent = buildConfigFile(config.features, config.plans); + + // Write to autumn.config.ts + const configPath = path.join(cwd, "autumn.config.ts"); + fs.writeFileSync(configPath, configContent, "utf-8"); + + return { + configPath, + template, + }; + }, + }); +} diff --git a/atmn/src/lib/transforms/apiToSdk/README.md b/atmn/src/lib/transforms/apiToSdk/README.md new file mode 100644 index 00000000..3aa54a05 --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/README.md @@ -0,0 +1,162 @@ +# API → SDK Transform Layer + +This directory contains transforms that convert Autumn API responses to SDK configuration types. + +## Declarative Transformer System + +Instead of writing repetitive imperative transform functions, we use a **declarative configuration system** that dramatically reduces code and improves maintainability. + +### Quick Example + +**Instead of this (79 lines):** +```typescript +export function transformApiFeature(apiFeature: any): Feature { + const base = { + id: apiFeature.id, + name: apiFeature.name, + event_names: apiFeature.event_names, + credit_schema: apiFeature.credit_schema, + }; + + if (apiFeature.type === "boolean") { + return { ...base, type: "boolean" as const }; + } + + if (apiFeature.type === "single_use") { + return { + ...base, + type: "metered" as const, + consumable: true, + }; + } + // ... 50+ more lines +} +``` + +**We write this (40 lines):** +```typescript +export const featureTransformer = createTransformer({ + discriminator: 'type', + cases: { + 'boolean': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { type: () => 'boolean' as const }, + }, + 'single_use': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'metered' as const, + consumable: () => true, + }, + }, + // ... more cases + }, +}); + +export function transformApiFeature(apiFeature: any): Feature { + return featureTransformer.transform(apiFeature); +} +``` + +## Transformer API + +```typescript +createTransformer({ + // Copy fields as-is + copy: ['id', 'name', 'description'], + + // Rename fields + rename: { + 'default': 'auto_enable', + 'granted_balance': 'included', + }, + + // Flatten nested fields + flatten: { + 'reset.interval': 'interval', + 'reset.interval_count': 'interval_count', + }, + + // Compute new values + compute: { + carry_over_usage: (api) => !api.reset?.reset_when_enabled, + type: () => 'metered' as const, + }, + + // Default values for missing fields + defaults: { + credit_schema: [], + enabled: true, + }, + + // Transform nested arrays + transformArrays: { + features: { + from: 'features', + transform: planFeatureTransformer.config + } + }, +}) +``` + +### Discriminated Unions + +For types that switch behavior based on a field (like `type`): + +```typescript +createTransformer({ + discriminator: 'type', // Field to switch on + cases: { + 'boolean': { /* config for boolean */ }, + 'metered': { /* config for metered */ }, + 'credit_system': { /* config for credit */ }, + }, + default: { /* fallback config */ }, +}) +``` + +## Benefits + +1. **Self-documenting** - Config clearly shows what transforms happen +2. **DRY** - No repetitive field copying boilerplate +3. **Type-safe** - Generic input/output types +4. **Testable** - Test engine once, not each transform +5. **Composable** - Reuse configs across transforms +6. **58% less code** - 213 lines → 90 lines + reusable engine + +## Files + +- `Transformer.ts` - Core declarative transformer engine +- `feature.ts` - Feature transforms (API → SDK) +- `plan.ts` - Plan transforms (API → SDK) +- `planFeature.ts` - Plan feature transforms (API → SDK) +- `helpers.ts` - Helper functions (invert, map enums, etc.) +- `Transformer.test.ts` - Comprehensive test suite + +## Transform Mappings + +### Feature +- `type: "single_use"` → `type: "metered", consumable: true` +- `type: "continuous_use"` → `type: "metered", consumable: false` +- `type: "credit_system"` → `type: "credit_system", consumable: true` +- `type: "boolean"` → `type: "boolean"` (no consumable field) + +### Plan +- `default` → `auto_enable` +- Copy: `id`, `name`, `description`, `group`, `add_on`, `free_trial` +- Transform: `price`, `features[]` + +### Plan Feature +- `granted_balance` → `included` +- `reset.interval` → `interval` (flatten) +- `reset.interval_count` → `interval_count` (flatten) +- `reset.reset_when_enabled` → `carry_over_usage` (flatten + invert) +- `price.usage_model` → `price.billing_method` (rename) + +## Testing + +```bash +bun test src/lib/transforms/apiToSdk/Transformer.test.ts +``` + +All transforms are thoroughly tested to ensure correct behavior. diff --git a/atmn/src/lib/transforms/apiToSdk/Transformer.test.ts b/atmn/src/lib/transforms/apiToSdk/Transformer.test.ts new file mode 100644 index 00000000..996fa8be --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/Transformer.test.ts @@ -0,0 +1,198 @@ +import { describe, test, expect } from "bun:test"; +import { createTransformer } from "./Transformer.js"; +import { transformApiFeature } from "./feature.js"; +import { transformApiPlan } from "./plan.js"; + +describe("Transformer", () => { + describe("Feature transforms", () => { + test("boolean feature", () => { + const apiFeature = { + id: "enabled", + name: "Feature Enabled", + type: "boolean", + event_names: [], + }; + + const result = transformApiFeature(apiFeature); + + expect(result.type).toBe("boolean"); + expect(result.id).toBe("enabled"); + expect(result.name).toBe("Feature Enabled"); + }); + + test("single_use → metered with consumable=true", () => { + const apiFeature = { + id: "api_calls", + name: "API Calls", + type: "single_use", + event_names: ["api.call"], + }; + + const result = transformApiFeature(apiFeature); + + expect(result.type).toBe("metered"); + expect(result.consumable).toBe(true); + expect(result.id).toBe("api_calls"); + }); + + test("continuous_use → metered with consumable=false", () => { + const apiFeature = { + id: "seats", + name: "Seats", + type: "continuous_use", + event_names: [], + }; + + const result = transformApiFeature(apiFeature); + + expect(result.type).toBe("metered"); + expect(result.consumable).toBe(false); + }); + + test("credit_system", () => { + const apiFeature = { + id: "credits", + name: "Credits", + type: "credit_system", + credit_schema: [ + { metered_feature_id: "api_calls", credit_cost: 10 } + ], + }; + + const result = transformApiFeature(apiFeature); + + expect(result.type).toBe("credit_system"); + expect(result.consumable).toBe(true); + expect(result.credit_schema).toHaveLength(1); + }); + }); + + describe("Plan transforms", () => { + test("basic plan with default → auto_enable rename", () => { + const apiPlan: any = { + id: "pro", + name: "Pro Plan", + description: "Professional tier", + default: true, + features: [], + }; + + const result = transformApiPlan(apiPlan); + + expect(result.auto_enable).toBe(true); + expect('default' in result).toBe(false); + expect(result.id).toBe("pro"); + }); + + test("plan with price", () => { + const apiPlan: any = { + id: "premium", + name: "Premium", + price: { + amount: 9900, + interval: "month" as const, + }, + features: [], + }; + + const result = transformApiPlan(apiPlan); + + expect(result.price).toEqual({ + amount: 9900, + interval: "month", + }); + }); + }); + + describe("Transformer core", () => { + test("copy fields", () => { + const transformer = createTransformer({ + copy: ['id', 'name'], + }); + + const result = transformer.transform({ + id: "test", + name: "Test", + extra: "ignored", + }); + + expect(result).toEqual({ id: "test", name: "Test" }); + }); + + test("rename fields", () => { + const transformer = createTransformer({ + rename: { old_name: 'new_name' }, + }); + + const result = transformer.transform({ old_name: "value" }); + + expect(result).toEqual({ new_name: "value" }); + }); + + test("flatten nested fields", () => { + const transformer = createTransformer({ + flatten: { + 'parent.child': 'flat', + 'deeply.nested.value': 'value', + }, + }); + + const result = transformer.transform({ + parent: { child: "test" }, + deeply: { nested: { value: 42 } }, + }); + + expect(result).toEqual({ + flat: "test", + value: 42, + }); + }); + + test("compute fields", () => { + const transformer = createTransformer({ + compute: { + doubled: (api: any) => api.value * 2, + inverted: (api: any) => !api.flag, + }, + }); + + const result = transformer.transform({ value: 5, flag: true }); + + expect(result).toEqual({ + doubled: 10, + inverted: false, + }); + }); + + test("discriminated union", () => { + const transformer = createTransformer({ + discriminator: 'type', + cases: { + 'A': { copy: ['id'], compute: { value: () => 'A' } }, + 'B': { copy: ['id'], compute: { value: () => 'B' } }, + }, + }); + + const resultA = transformer.transform({ id: "1", type: "A" }); + const resultB = transformer.transform({ id: "2", type: "B" }); + + expect(resultA).toEqual({ id: "1", value: "A" }); + expect(resultB).toEqual({ id: "2", value: "B" }); + }); + + test("defaults", () => { + const transformer = createTransformer({ + copy: ['name'], + defaults: { count: 0, enabled: true }, + }); + + const result = transformer.transform({ name: "test" }); + + expect(result).toEqual({ + name: "test", + count: 0, + enabled: true, + }); + }); + }); +}); diff --git a/atmn/src/lib/transforms/apiToSdk/Transformer.ts b/atmn/src/lib/transforms/apiToSdk/Transformer.ts new file mode 100644 index 00000000..97100e13 --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/Transformer.ts @@ -0,0 +1,210 @@ +/** + * Declarative transformer system for API → SDK transformations + * + * Instead of writing repetitive transform functions, define transformations as config: + * - copy: fields that pass through unchanged + * - rename: { apiField: 'sdkField' } + * - flatten: { 'nested.field': 'flatField' } + * - compute: { sdkField: (api) => api.field * 2 } + * - discriminate: conditional transforms based on a field value + */ + +export interface FieldMapping { + /** Fields to copy as-is from API to SDK */ + copy?: string[]; + + /** Rename fields: { apiFieldName: 'sdkFieldName' } */ + rename?: Record; + + /** Flatten nested fields: { 'parent.child': 'flatName' } */ + flatten?: Record; + + /** Remove fields from output */ + remove?: string[]; + + /** Computed fields: { sdkField: (api) => computed value } */ + compute?: Record unknown>; + + /** Default values for undefined/null fields: { sdkField: defaultValue } */ + defaults?: Record; + + /** Transform nested arrays: { sdkField: { from: 'apiField', transform: TransformConfig } } */ + transformArrays?: Record }>; + + /** Fields that swap null to undefined when coming from API: ['field1', 'field2'] */ + swapNullish?: string[]; + + /** Fields that swap false to undefined when coming from API (only true or undefined): ['field1', 'field2'] */ + swapFalse?: string[]; +} + +export interface DiscriminatedTransform { + /** Field to discriminate on (e.g., 'type') */ + discriminator: string; + + /** Map of discriminator value → transform config */ + cases: Record>; + + /** Fallback transform if no case matches */ + default?: FieldMapping; +} + +/** + * Generic transformer that applies field mappings declaratively + */ +export class Transformer { + constructor( + private config: FieldMapping | DiscriminatedTransform, + ) {} + + transform(input: TInput): TOutput { + // Handle discriminated union + if ('discriminator' in this.config) { + return this.transformDiscriminated(input); + } + + // Handle simple field mapping + return this.transformFields(input, this.config); + } + + private transformDiscriminated(input: TInput): TOutput { + const config = this.config as DiscriminatedTransform; + const discriminatorValue = this.getNestedValue(input, config.discriminator) as string; + + const caseConfig = config.cases[discriminatorValue] || config.default; + if (!caseConfig) { + throw new Error( + `No transform case found for ${config.discriminator}="${discriminatorValue}"`, + ); + } + + return this.transformFields(input, caseConfig); + } + + private transformFields(input: TInput, mapping: FieldMapping): TOutput { + const output: Record = {}; + + // Track which fields should swap null to undefined + const swapNullishSet = new Set(mapping.swapNullish || []); + // Track which fields should swap false to undefined + const swapFalseSet = new Set(mapping.swapFalse || []); + + // 1. Copy fields + if (mapping.copy) { + for (const field of mapping.copy) { + const value = this.getNestedValue(input, field); + if (value !== undefined) { + // If field is in swapNullish and value is null, convert to undefined + if (swapNullishSet.has(field) && value === null) { + // Don't set the field (undefined) + continue; + } + // If field is in swapFalse and value is false, convert to undefined + if (swapFalseSet.has(field) && value === false) { + // Don't set the field (undefined) + continue; + } + output[field] = value; + } + } + } + + // 2. Rename fields + if (mapping.rename) { + for (const [apiField, sdkField] of Object.entries(mapping.rename)) { + const value = this.getNestedValue(input, apiField); + if (value !== undefined) { + // If apiField is in swapNullish and value is null, convert to undefined + if (swapNullishSet.has(apiField) && value === null) { + // Don't set the field (undefined) + continue; + } + // If apiField is in swapFalse and value is false, convert to undefined + if (swapFalseSet.has(apiField) && value === false) { + // Don't set the field (undefined) + continue; + } + output[sdkField] = value; + } + } + } + + // 3. Flatten nested fields + if (mapping.flatten) { + for (const [nestedPath, flatName] of Object.entries(mapping.flatten)) { + const value = this.getNestedValue(input, nestedPath); + if (value !== undefined) { + // If nestedPath is in swapNullish and value is null, convert to undefined + if (swapNullishSet.has(nestedPath) && value === null) { + // Don't set the field (undefined) + continue; + } + // If nestedPath is in swapFalse and value is false, convert to undefined + if (swapFalseSet.has(nestedPath) && value === false) { + // Don't set the field (undefined) + continue; + } + output[flatName] = value; + } + } + } + + // 4. Compute fields + if (mapping.compute) { + for (const [sdkField, computeFn] of Object.entries(mapping.compute)) { + const value = computeFn(input); + if (value !== undefined) { + output[sdkField] = value; + } + } + } + + // 5. Transform arrays + if (mapping.transformArrays) { + for (const [sdkField, config] of Object.entries(mapping.transformArrays)) { + const apiArray = this.getNestedValue(input, config.from); + if (Array.isArray(apiArray)) { + const transformer = new Transformer(config.transform); + output[sdkField] = apiArray.map(item => transformer.transform(item)); + } + } + } + + // 6. Apply defaults + if (mapping.defaults) { + for (const [field, defaultValue] of Object.entries(mapping.defaults)) { + if (output[field] === undefined || output[field] === null) { + output[field] = defaultValue; + } + } + } + + return output as TOutput; + } + + /** + * Get nested value using dot notation (e.g., 'reset.interval') + */ + private getNestedValue(obj: unknown, path: string): unknown { + const parts = path.split('.'); + let value: unknown = obj; + + for (const part of parts) { + if (value === null || value === undefined) { + return undefined; + } + value = (value as Record)[part]; + } + + return value; + } +} + +/** + * Helper to create a transformer with type safety + */ +export function createTransformer( + config: FieldMapping | DiscriminatedTransform, +): Transformer { + return new Transformer(config); +} diff --git a/atmn/src/lib/transforms/apiToSdk/feature.ts b/atmn/src/lib/transforms/apiToSdk/feature.ts new file mode 100644 index 00000000..5e21d63d --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/feature.ts @@ -0,0 +1,69 @@ +import type { Feature } from "../../../../source/compose/models/featureModels.js"; +import type { ApiFeature } from "../../api/types/index.js"; +import { createTransformer } from "./Transformer.js"; + +/** + * Declarative feature transformer - replaces 79 lines with 40 lines of config + */ +export const featureTransformer = createTransformer({ + discriminator: 'type', + cases: { + // Boolean features: just copy base fields, no consumable + 'boolean': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'boolean' as const, + }, + }, + + // Credit system features: always consumable + 'credit_system': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'credit_system' as const, + consumable: () => true, + credit_schema: (api) => api.credit_schema || [], + }, + }, + + // Backend bug: API returns "single_use" instead of "metered" with consumable=true + 'single_use': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'metered' as const, + consumable: () => true, + }, + }, + + // Backend bug: API returns "continuous_use" instead of "metered" with consumable=false + 'continuous_use': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'metered' as const, + consumable: () => false, + }, + }, + + // If API ever returns "metered" properly + 'metered': { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'metered' as const, + consumable: (api) => api.consumable ?? true, + }, + }, + }, + + // Fallback for unknown types + default: { + copy: ['id', 'name', 'event_names', 'credit_schema'], + compute: { + type: () => 'metered' as const, + consumable: () => true, + }, + }, +}); + +export function transformApiFeature(apiFeature: any): Feature { + return featureTransformer.transform(apiFeature); +} diff --git a/atmn/src/lib/transforms/apiToSdk/helpers.ts b/atmn/src/lib/transforms/apiToSdk/helpers.ts new file mode 100644 index 00000000..e166d621 --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/helpers.ts @@ -0,0 +1,52 @@ +/** + * Helper functions for API → SDK transformations + */ + +/** + * Map API feature type to SDK type + * API uses "metered" but SDK distinguishes between metered consumable and non-consumable + */ +export function mapFeatureType( + apiType: string, + consumable: boolean, +): "boolean" | "metered" | "credit_system" { + if (apiType === "boolean") { + return "boolean"; + } + if (apiType === "credit_system") { + return "credit_system"; + } + // For metered, the SDK doesn't actually use different type values + // The consumable field is what matters + return "metered"; +} + +/** + * Invert reset_when_enabled to carry_over_usage + * API: reset_when_enabled = true means reset usage when enabled + * SDK: carry_over_usage = true means DON'T reset (i.e., carry over) + * So they're opposites + */ +export function invertResetWhenEnabled( + resetWhenEnabled: boolean | undefined, +): boolean | undefined { + if (resetWhenEnabled === undefined) { + return undefined; + } + return !resetWhenEnabled; +} + +/** + * Map API usage_model to SDK billing_method + */ +export function mapUsageModel( + usageModel: string, +): "prepaid" | "pay_per_use" | undefined { + if (usageModel === "prepaid") { + return "prepaid"; + } + if (usageModel === "pay_per_use") { + return "pay_per_use"; + } + return undefined; +} diff --git a/atmn/src/lib/transforms/apiToSdk/index.ts b/atmn/src/lib/transforms/apiToSdk/index.ts new file mode 100644 index 00000000..3be97e80 --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/index.ts @@ -0,0 +1,8 @@ +export { transformApiFeature } from "./feature.js"; +export { transformApiPlan } from "./plan.js"; +export { transformApiPlanFeature } from "./planFeature.js"; +export { + mapFeatureType, + invertResetWhenEnabled, + mapUsageModel, +} from "./helpers.js"; diff --git a/atmn/src/lib/transforms/apiToSdk/plan.ts b/atmn/src/lib/transforms/apiToSdk/plan.ts new file mode 100644 index 00000000..dbcc30aa --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/plan.ts @@ -0,0 +1,50 @@ +import type { Plan } from "../../../../source/compose/models/planModels.js"; +import type { ApiPlan } from "../../api/types/index.js"; +import { createTransformer } from "./Transformer.js"; +import { transformApiPlanFeature } from "./planFeature.js"; + +/** + * Declarative plan transformer - replaces 57 lines with ~20 lines of config + */ +export const planTransformer = createTransformer({ + copy: [ + 'id', + 'name', + 'description', + 'group', + 'add_on', + 'free_trial', + ], + + // Rename: default → auto_enable + rename: { + 'default': 'auto_enable', + }, + + // Swap null to undefined for these fields (API → SDK direction) + // When pulling from API: null becomes undefined (cleaner, won't show in generated code) + swapNullish: ['group'], + + // Swap false to undefined for these fields (API → SDK direction) + // When pulling from API: false becomes undefined (only true or undefined in SDK) + // API 'default' field maps to SDK 'auto_enable', API 'add_on' stays as 'add_on' + swapFalse: ['default', 'add_on'], + + // Copy nested price object as-is + compute: { + price: (api) => api.price ? { + amount: api.price.amount, + interval: api.price.interval, + } : undefined, + + // Transform features array (only if non-empty) + features: (api) => + api.features && api.features.length > 0 + ? api.features.map(transformApiPlanFeature) + : undefined, + }, +}); + +export function transformApiPlan(apiPlan: ApiPlan): Plan { + return planTransformer.transform(apiPlan); +} diff --git a/atmn/src/lib/transforms/apiToSdk/planFeature.ts b/atmn/src/lib/transforms/apiToSdk/planFeature.ts new file mode 100644 index 00000000..4942aba7 --- /dev/null +++ b/atmn/src/lib/transforms/apiToSdk/planFeature.ts @@ -0,0 +1,61 @@ +import type { PlanFeature } from "../../../../source/compose/models/planModels.js"; +import type { ApiPlanFeature } from "../../api/types/index.js"; +import { invertResetWhenEnabled, mapUsageModel } from "./helpers.js"; +import { createTransformer } from "./Transformer.js"; + +/** + * Declarative plan feature transformer - replaces 77 lines with ~30 lines of config + */ +export const planFeatureTransformer = createTransformer< + ApiPlanFeature, + PlanFeature +>({ + copy: ["feature_id", "unlimited", "proration"], + + // Rename: granted_balance → included + rename: { + granted_balance: "included", + }, + + // Flatten: reset.* → top-level fields + flatten: { + "reset.interval": "interval", + "reset.interval_count": "interval_count", + }, + + // Computed fields + compute: { + // Invert: reset.reset_when_enabled → carry_over_usage + carry_over_usage: (api) => + api.reset?.reset_when_enabled !== undefined + ? invertResetWhenEnabled(api.reset.reset_when_enabled) + : undefined, + + // Transform price object + price: (api) => + api.price + ? { + ...api.price, + max_purchase: api.price.max_purchase ?? undefined, + billing_method: api.price.usage_model + ? mapUsageModel(api.price.usage_model) + : undefined, + } + : undefined, + + // Transform rollover object + rollover: (api) => + api.rollover + ? { + ...api.rollover, + max: api.rollover.max ?? 0, + } + : undefined, + }, +}); + +export function transformApiPlanFeature( + apiPlanFeature: ApiPlanFeature, +): PlanFeature { + return planFeatureTransformer.transform(apiPlanFeature); +} diff --git a/atmn/src/lib/transforms/index.ts b/atmn/src/lib/transforms/index.ts new file mode 100644 index 00000000..5defbac3 --- /dev/null +++ b/atmn/src/lib/transforms/index.ts @@ -0,0 +1,2 @@ +export * from "./apiToSdk/index.js"; +export * from "./sdkToCode/index.js"; diff --git a/atmn/src/lib/transforms/sdkToCode/configFile.ts b/atmn/src/lib/transforms/sdkToCode/configFile.ts new file mode 100644 index 00000000..027f9c95 --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/configFile.ts @@ -0,0 +1,38 @@ +import type { + Feature, + Plan, +} from "../../../../source/compose/models/index.js"; +import { buildImports } from "./imports.js"; +import { buildFeatureCode } from "./feature.js"; +import { buildPlanCode } from "./plan.js"; + +/** + * Generate complete autumn.config.ts file content + */ +export function buildConfigFile(features: Feature[], plans: Plan[]): string { + const sections: string[] = []; + + // Add imports + sections.push(buildImports()); + sections.push(""); + + // Add features + if (features.length > 0) { + sections.push("// Features"); + for (const feature of features) { + sections.push(buildFeatureCode(feature)); + sections.push(""); + } + } + + // Add plans + if (plans.length > 0) { + sections.push("// Plans"); + for (const plan of plans) { + sections.push(buildPlanCode(plan, features)); + sections.push(""); + } + } + + return sections.join("\n"); +} diff --git a/atmn/src/lib/transforms/sdkToCode/feature.ts b/atmn/src/lib/transforms/sdkToCode/feature.ts new file mode 100644 index 00000000..35d6c51d --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/feature.ts @@ -0,0 +1,41 @@ +import type { Feature } from "../../../../source/compose/models/featureModels.js"; +import { featureIdToVarName, formatValue } from "./helpers.js"; + +/** + * Generate TypeScript code for a feature definition + * + * Rules: + * - Boolean features: No consumable field + * - Metered features: MUST output consumable: true or false explicitly + * - Credit system features: Don't output consumable (implied true) + */ +export function buildFeatureCode(feature: Feature): string { + const varName = featureIdToVarName(feature.id); + const lines: string[] = []; + + lines.push(`export const ${varName} = feature({`); + lines.push(`\tid: '${feature.id}',`); + lines.push(`\tname: '${feature.name}',`); + lines.push(`\ttype: '${feature.type}',`); + + // Metered features MUST have explicit consumable field + // consumable: true = single_use (usage is consumed) + // consumable: false = continuous_use (usage accumulates, like seats) + if (feature.type === "metered") { + lines.push(`\tconsumable: ${feature.consumable},`); + } + + // Add event_names if present + if (feature.event_names && feature.event_names.length > 0) { + lines.push(`\tevent_names: ${formatValue(feature.event_names)},`); + } + + // Add credit_schema for credit_system features + if (feature.type === "credit_system" && feature.credit_schema) { + lines.push(`\tcredit_schema: ${formatValue(feature.credit_schema)},`); + } + + lines.push(`});`); + + return lines.join("\n"); +} diff --git a/atmn/src/lib/transforms/sdkToCode/helpers.ts b/atmn/src/lib/transforms/sdkToCode/helpers.ts new file mode 100644 index 00000000..fdf939c0 --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/helpers.ts @@ -0,0 +1,101 @@ +/** + * Helper functions for SDK → Code generation + */ + +/** + * Convert ID to valid variable name + * Examples: "pro-plan" → "pro_plan", "api_calls" → "api_calls", "123" → "123" + */ +function sanitizeId(id: string): string { + return id + .replace(/[^a-zA-Z0-9_]/g, "_") // Replace invalid chars with underscore + .replace(/_+/g, "_") // Collapse multiple underscores + .replace(/^_/, "") // Remove leading underscore + .replace(/_$/, ""); // Remove trailing underscore +} + +/** + * Convert ID to valid variable name with context-specific prefix + * Generic version - kept for backwards compatibility + */ +export function idToVarName(id: string, prefix = "item_"): string { + const sanitized = sanitizeId(id); + + // JavaScript identifiers can't start with a number + if (/^[0-9]/.test(sanitized)) { + return prefix + sanitized; + } + + return sanitized; +} + +/** + * Convert plan ID to valid variable name + * Examples: "pro-plan" → "pro_plan", "123" → "plan_123" + */ +export function planIdToVarName(id: string): string { + return idToVarName(id, "plan_"); +} + +/** + * Convert feature ID to valid variable name + * Examples: "api-calls" → "api_calls", "123" → "feature_123" + */ +export function featureIdToVarName(id: string): string { + return idToVarName(id, "feature_"); +} + +/** + * Escape string for TypeScript string literal + */ +export function escapeString(str: string): string { + return str + .replace(/\\/g, "\\\\") + .replace(/'/g, "\\'") + .replace(/"/g, '\\"') + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t"); +} + +/** + * Indent code by given number of tabs + */ +export function indentCode(code: string, tabs: number): string { + const indent = "\t".repeat(tabs); + return code + .split("\n") + .map((line) => (line.trim() ? indent + line : line)) + .join("\n"); +} + +/** + * Format a value for TypeScript code + */ +export function formatValue(value: unknown): string { + if (value === null) { + return "null"; + } + if (value === undefined) { + return "undefined"; + } + if (typeof value === "string") { + return `'${escapeString(value)}'`; + } + if (typeof value === "number") { + return String(value); + } + if (typeof value === "boolean") { + return String(value); + } + if (Array.isArray(value)) { + return `[${value.map(formatValue).join(", ")}]`; + } + if (typeof value === "object") { + const entries = Object.entries(value) + .map(([k, v]) => `${k}: ${formatValue(v)}`) + .join(", "); + return `{ ${entries} }`; + } + return String(value); +} diff --git a/atmn/src/lib/transforms/sdkToCode/imports.ts b/atmn/src/lib/transforms/sdkToCode/imports.ts new file mode 100644 index 00000000..ee3a8e8a --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/imports.ts @@ -0,0 +1,6 @@ +/** + * Generate imports for config file + */ +export function buildImports(): string { + return `import { feature, plan, planFeature } from 'atmn';`; +} diff --git a/atmn/src/lib/transforms/sdkToCode/index.ts b/atmn/src/lib/transforms/sdkToCode/index.ts new file mode 100644 index 00000000..00f51ed3 --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/index.ts @@ -0,0 +1,13 @@ +export { buildFeatureCode } from "./feature.js"; +export { buildPlanCode } from "./plan.js"; +export { buildPlanFeatureCode } from "./planFeature.js"; +export { buildImports } from "./imports.js"; +export { buildConfigFile } from "./configFile.js"; +export { + idToVarName, + planIdToVarName, + featureIdToVarName, + escapeString, + indentCode, + formatValue +} from "./helpers.js"; diff --git a/atmn/src/lib/transforms/sdkToCode/plan.ts b/atmn/src/lib/transforms/sdkToCode/plan.ts new file mode 100644 index 00000000..3f9cbcf0 --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/plan.ts @@ -0,0 +1,66 @@ +import type { + Feature, + Plan, +} from "../../../../source/compose/models/index.js"; +import { planIdToVarName, formatValue } from "./helpers.js"; +import { buildPlanFeatureCode } from "./planFeature.js"; + +/** + * Generate TypeScript code for a plan definition + */ +export function buildPlanCode(plan: Plan, features: Feature[]): string { + const varName = planIdToVarName(plan.id); + const lines: string[] = []; + + lines.push(`export const ${varName} = plan({`); + lines.push(`\tid: '${plan.id}',`); + lines.push(`\tname: '${plan.name}',`); + + // Add description + if (plan.description !== undefined && plan.description !== null) { + lines.push(`\tdescription: '${plan.description}',`); + } + + // Add group (only if it has a non-empty string value) + // undefined and null both mean "no group" and should be omitted from generated code + if (plan.group !== undefined && plan.group !== null && plan.group !== "") { + lines.push(`\tgroup: '${plan.group}',`); + } + + // Add add_on (only if true - false becomes undefined via swapFalse) + if (plan.add_on !== undefined) { + lines.push(`\tadd_on: ${plan.add_on},`); + } + + // Add auto_enable (only if true - false becomes undefined via swapFalse) + if (plan.auto_enable !== undefined) { + lines.push(`\tauto_enable: ${plan.auto_enable},`); + } + + // Add price + if (plan.price) { + lines.push(`\tprice: {`); + lines.push(`\t\tamount: ${plan.price.amount},`); + lines.push(`\t\tinterval: '${plan.price.interval}',`); + lines.push(`\t},`); + } + + // Add features + if (plan.features && plan.features.length > 0) { + lines.push(`\tfeatures: [`); + for (const planFeature of plan.features) { + const featureCode = buildPlanFeatureCode(planFeature, features); + lines.push(featureCode); + } + lines.push(`\t],`); + } + + // Add free_trial + if (plan.free_trial) { + lines.push(`\tfree_trial: ${formatValue(plan.free_trial)},`); + } + + lines.push(`});`); + + return lines.join("\n"); +} diff --git a/atmn/src/lib/transforms/sdkToCode/planFeature.ts b/atmn/src/lib/transforms/sdkToCode/planFeature.ts new file mode 100644 index 00000000..57858218 --- /dev/null +++ b/atmn/src/lib/transforms/sdkToCode/planFeature.ts @@ -0,0 +1,89 @@ +import type { + Feature, + PlanFeature, +} from "../../../../source/compose/models/index.js"; +import { idToVarName, formatValue } from "./helpers.js"; + +/** + * Generate TypeScript code for a plan feature configuration + */ +export function buildPlanFeatureCode( + planFeature: PlanFeature, + features: Feature[], +): string { + // Find the feature to get its variable name + const feature = features.find((f) => f.id === planFeature.feature_id); + const featureVarName = feature ? idToVarName(feature.id) : null; + const featureIdCode = featureVarName + ? `${featureVarName}.id` + : `'${planFeature.feature_id}'`; + + const lines: string[] = []; + lines.push(`\t\tplanFeature({`); + lines.push(`\t\t\tfeature_id: ${featureIdCode},`); + + // Add included (granted_balance) + if (planFeature.included !== undefined) { + lines.push(`\t\t\tincluded: ${planFeature.included},`); + } + + // Add unlimited + if (planFeature.unlimited !== undefined) { + lines.push(`\t\t\tunlimited: ${planFeature.unlimited},`); + } + + // Add reset fields (flattened) + if (planFeature.interval) { + lines.push(`\t\t\tinterval: '${planFeature.interval}',`); + } + if (planFeature.interval_count !== undefined) { + lines.push(`\t\t\tinterval_count: ${planFeature.interval_count},`); + } + if (planFeature.carry_over_usage !== undefined) { + lines.push(`\t\t\tcarry_over_usage: ${planFeature.carry_over_usage},`); + } + + // Add price + if (planFeature.price) { + lines.push(`\t\t\tprice: {`); + + if (planFeature.price.amount !== undefined) { + lines.push(`\t\t\t\tamount: ${planFeature.price.amount},`); + } + + if (planFeature.price.tiers) { + const tiersCode = formatValue(planFeature.price.tiers); + lines.push(`\t\t\t\ttiers: ${tiersCode},`); + } + + if (planFeature.price.billing_units !== undefined) { + lines.push(`\t\t\t\tbilling_units: ${planFeature.price.billing_units},`); + } + + if (planFeature.price.billing_method) { + lines.push( + `\t\t\t\tbilling_method: '${planFeature.price.billing_method}',`, + ); + } + + if (planFeature.price.max_purchase !== undefined) { + lines.push(`\t\t\t\tmax_purchase: ${planFeature.price.max_purchase},`); + } + + lines.push(`\t\t\t},`); + } + + // Add proration + if (planFeature.proration) { + lines.push(`\t\t\tproration: ${formatValue(planFeature.proration)},`); + } + + // Add rollover + if (planFeature.rollover) { + lines.push(`\t\t\trollover: ${formatValue(planFeature.rollover)},`); + } + + lines.push(`\t\t}),`); + + return lines.join("\n"); +} diff --git a/atmn/src/lib/utils.ts b/atmn/src/lib/utils.ts new file mode 100644 index 00000000..dc5d3254 --- /dev/null +++ b/atmn/src/lib/utils.ts @@ -0,0 +1,188 @@ +import { confirm } from "@inquirer/prompts"; +import chalk from "chalk"; +import dotenv from "dotenv"; +import fs from "fs"; +import yoctoSpinner from "yocto-spinner"; +import { isLocal, isProd } from "./env/cliContext.js"; + +export const notNullish = (value: unknown) => + value !== null && value !== undefined; +export const nullish = (value: unknown) => + value === null || value === undefined; + +/** + * @deprecated Use isProd() from cliContext.ts instead + */ +export const isProdFlag = () => { + return isProd(); +}; + +/** + * @deprecated Use isLocal() from cliContext.ts instead + */ +export const isLocalFlag = () => { + return isLocal(); +}; + +export function snakeCaseToCamelCase(value: string) { + return value.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()); +} + +export function idToVar({ + id, + prefix = "product", +}: { + id: string; + prefix?: string; +}): string { + const processed = id + .replace(/[-_](.)/g, (_, letter) => letter.toUpperCase()) + .replace(/[^a-zA-Z0-9_$]/g, ""); // Remove invalid JavaScript identifier characters + + // If the processed string starts with a number, add 'product' prefix + if (/^[0-9]/.test(processed)) { + return `${prefix}${processed}`; + } + + // If it starts with other invalid characters, add 'product' prefix + if (/^[^a-zA-Z_$]/.test(processed)) { + return `${prefix}${processed}`; + } + + return processed; +} + +async function upsertEnvVar( + filePath: string, + varName: string, + newValue: string, +) { + const content = fs.readFileSync(filePath, "utf-8"); + const lines = content.split("\n"); + let foundIndex = -1; + + // Find the first occurrence of the variable + for (let i = 0; i < lines.length; i++) { + if (lines[i]?.startsWith(`${varName}=`)) { + foundIndex = i; + break; + } + } + + if (foundIndex !== -1) { + const shouldOverwrite = await confirm({ + message: `${varName} already exists in .env. Overwrite?`, + default: false, + }); + if (shouldOverwrite) { + lines[foundIndex] = `${varName}=${newValue}`; + } + } else { + // Variable wasn't found, add it to the end + lines.push(`${varName}=${newValue}`); + } + + // Write the updated content back to the file + fs.writeFileSync(filePath, lines.join("\n")); +} + +export async function storeToEnv(prodKey: string, sandboxKey: string) { + const envPath = `${process.cwd()}/.env`; + const envLocalPath = `${process.cwd()}/.env.local`; + const envVars = `AUTUMN_PROD_SECRET_KEY=${prodKey}\nAUTUMN_SECRET_KEY=${sandboxKey}\n`; + + // Check if .env exists first + if (fs.existsSync(envPath)) { + await upsertEnvVar(envPath, "AUTUMN_PROD_SECRET_KEY", prodKey); + await upsertEnvVar(envPath, "AUTUMN_SECRET_KEY", sandboxKey); + console.log(chalk.green(".env file found. Updated keys.")); + } else if (fs.existsSync(envLocalPath)) { + // If .env doesn't exist but .env.local does, create .env and write keys + fs.writeFileSync(envPath, envVars); + console.log( + chalk.green( + ".env.local found but .env not found. Created new .env file and wrote keys.", + ), + ); + } else { + // Neither .env nor .env.local exists, create .env + fs.writeFileSync(envPath, envVars); + console.log( + chalk.green( + "No .env or .env.local file found. Created new .env file and wrote keys.", + ), + ); + } +} + +function getEnvVar(parsed: { [key: string]: string }, prodFlag: boolean) { + if (prodFlag) return parsed["AUTUMN_PROD_SECRET_KEY"]; + + return parsed["AUTUMN_SECRET_KEY"]; +} + +export function readFromEnv(options?: { bypass?: boolean }) { + const envPath = `${process.cwd()}/.env`; + const envLocalPath = `${process.cwd()}/.env.local`; + const prodFlag = isProd(); + + // biome-ignore lint/complexity/useLiteralKeys: will throw "index signature" error otherwise + if (prodFlag && process.env["AUTUMN_PROD_SECRET_KEY"]) { + return process.env["AUTUMN_PROD_SECRET_KEY"]; + } + + // biome-ignore lint/complexity/useLiteralKeys: will throw "index signature" error otherwise + if (!prodFlag && process.env["AUTUMN_SECRET_KEY"]) { + return process.env["AUTUMN_SECRET_KEY"]; + } + + let secretKey: string | undefined; + + // Check .env second + if (fs.existsSync(envPath) && !secretKey) + secretKey = getEnvVar( + dotenv.parse(fs.readFileSync(envPath, "utf-8")), + prodFlag, + ); + + // If not found in .env, check .env.local + if (fs.existsSync(envLocalPath) && !secretKey) + secretKey = getEnvVar( + dotenv.parse(fs.readFileSync(envLocalPath, "utf-8")), + prodFlag, + ); + + if (!secretKey && !options?.bypass) { + if (prodFlag) { + console.error( + "[Error] atmn uses the AUTUMN_PROD_SECRET_KEY to call the Autumn production API. Please add it to your .env file or run `atmn login` to authenticate.", + ); + process.exit(1); + } else { + console.error( + "[Error] atmn uses the AUTUMN_SECRET_KEY to call the Autumn sandbox API. Please add it to your .env (or .env.local) file or run `atmn login` to authenticate.", + ); + process.exit(1); + } + } + + return secretKey; +} + +export function initSpinner(message: string) { + const spinner = yoctoSpinner({ + text: message, + }); + spinner.start(); + + return spinner; +} + +export async function isSandboxKey(apiKey: string) { + const prefix = apiKey.split("am_sk_")[1]?.split("_")[0]; + + if (prefix === "live") { + return false; + } else if (prefix === "test") return true; + else throw new Error("Invalid API key"); +} diff --git a/atmn/src/lib/writeEmptyConfig.ts b/atmn/src/lib/writeEmptyConfig.ts new file mode 100644 index 00000000..3d617d37 --- /dev/null +++ b/atmn/src/lib/writeEmptyConfig.ts @@ -0,0 +1,15 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** + * Writes an empty/skeleton autumn.config.ts file to the current working directory + */ +export function writeEmptyConfig(): void { + const content = `import { plan, feature, planFeature } from 'atmn' +// export const message = feature({ "id": "msg", "name": "Messages", "type": "metered", "consumable": true }) +// export const free = plan({ "id": "free", "name": "Free Tier", features: [...] }) +`; + + const configPath = path.join(process.cwd(), "autumn.config.ts"); + fs.writeFileSync(configPath, content, "utf-8"); +} diff --git a/atmn/src/prompts/creditSystemDocs.ts b/atmn/src/prompts/creditSystemDocs.ts new file mode 100644 index 00000000..ba6a24aa --- /dev/null +++ b/atmn/src/prompts/creditSystemDocs.ts @@ -0,0 +1,662 @@ +export const creditSystemDocs = ` +# Monetary credits + +> Grant your users a currency-based balance of credits, that various features can draw from + +When you have multiple features that cost different amounts, you can use a credit system to deduct usage from a single balance. This can be great to simplify billing and usage tracking, especially when you have lots of features. + +## Example case + +We have a AI chatbot product with 2 different models, and each model costs a different amount to use. + +* Basic message: $1 per 100 messages +* Premium message: $10 per 100 messages + +And we have the following plans: + +* Free tier: $5 credits per month for free +* Pro tier: $10 credits per month, at $10 per month + +Users should also be able to top up their balance with more credits. + +## Configure Pricing + + + + #### Create Features + + Create a \`metered\` \`consumable\` feature for each message type, so that we can track the usage of each: + + + + + + + + + + #### Create Credit System + + Now, we'll create a credit system, where we'll define the cost of each message type. We'll define the cost per message in USD: + + | Feature | Cost per message (USD) | Credit cost per message (USD) | + | --------------- | ---------------------- | ----------------------------- | + | Basic message | $1 per 100 messages | 0.01 | + | Premium message | $10 per 100 messages | 0.10 | + + + + + + + + + + #### Create Free, Pro and Top-up Plans + + Let's create our free and pro plans, and add the credits amounts to each. + + + Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers. + + + + + + + + +
+ + + + + + + + Then, we'll create our top-up plan. We'll add a price to our credit feature, where each credit is worth $1. These top up credits will be \`one-off\` \`prepaid\` purchases that never expire. + + + + + + +
+
+ +## Implementation + + + + #### Create an Autumn Customer + + When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them $5 credits per month. + + + \`\`\`jsx React theme={null} + import { useCustomer } from "autumn-js/react"; + + const App = () => { + const { customer } = useCustomer(); + + console.log("Autumn customer:", customer); + + return

Welcome, {customer?.name || "user"}!

; + }; + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data, error } = await autumn.customers.create({ + id: "user_or_org_id_from_auth", + name: "John Yeo", + email: "john@example.com", + }); + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn('am_sk_42424242') + + async def main(): + customer = await autumn.customers.create( + id="user_or_org_id_from_auth", + name="John Yeo", + email="john@example.com", + ) + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl --request POST \ + --url https://api.useautumn.com/customers \ + --header 'Authorization: Bearer am_sk_42424242' \ + --header 'Content-Type: application/json' \ + --data '{ + "id": "user_or_org_id_from_auth", + "name": "John Yeo", + "email": "john@example.com" + }' + \`\`\` +
+
+ + + #### Checking for access + + Every time our user sends a message to the chatbot, we'll first check if they have enough credits remaining to send the message. + + The \`required_balance\` parameter will convert the number of messages to credits. Eg, if you pass \`required_balance: 5\` for basic messages, then check will return \`allowed: true\` if the user has at least 0.05 USD credits remaining. + + + Note how we're interacting with the underlying features (\`basic_messages\`, + \`premium_messages\`) here--not the credit system. + + + + \`\`\`jsx React wrap theme={null} + import { useCustomer } from "autumn-js/react"; + + export function CheckBasicMessage() { + const { check, refetch } = useCustomer(); + + const handleCheckAccess = async () => { + const { data } = await check({ featureId: "basic_messages", requiredBalance: 1 }); + + if (!data?.allowed) { + alert("You've run out of basic message credits"); + } else { + // proceed with sending message + await refetch(); + } + }; + } + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data } = await autumn.check({ + customer_id: "user_or_org_id_from_auth", + feature_id: "basic_messages", + required_balance: 1, + }); + + if (!data.allowed) { + console.log("User has run out of basic message credits"); + return; + } + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + response = await autumn.check( + customer_id="user_or_org_id_from_auth", + feature_id="basic_messages", + required_balance=1, + ) + + if not response.allowed: + print("User has run out of basic message credits") + return + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/check" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "basic_messages", + "required_balance": 1 + }' + \`\`\` + + + + The credit system ID will be returned in the \`balances\` field. + + \`\`\`json {8} theme={null} + { + "allowed": true, + "code": "feature_found", + "customer_id": "ayush", + "feature_id": "usd_credits", + "required_balance": 0.01, + "interval": "month", + "interval_count": 1, + "unlimited": false, + "balance": 5, + "usage": 0, + "included_usage": 5, + "next_reset_at": 1769110978704, + "overage_allowed": false, + "credit_schema": [ + { + "feature_id": "basic_messages", + "credit_amount": 0.01 + }, + { + "feature_id": "premium_messages", + "credit_amount": 0.1 + } + ] + } + \`\`\` + + + + + #### Tracking messages and using credits + + Now let's implement our usage tracking and use up our credits. In this example, we're using 2 basic messages, which will cost us 0.02 USD credits. + + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + await autumn.track({ + customer_id: "user_or_org_id_from_auth", + feature_id: "basic_messages", + value: 2, + }); + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + await autumn.track( + customer_id="user_or_org_id_from_auth", + feature_id="basic_messages", + value=2, + ) + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/track" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "basic_messages", + "value": 2 + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "code": "event_received", + "customer_id": "user_or_org_id_from_auth", + "feature_id": "basic_messages" + } + \`\`\` + + + + + #### Upgrading to Pro + + We can prompt the user to upgrade. When they click our "upgrade" button, we can use the \`checkout\` route to get a Stripe Checkout URL for them to make a payment. + + + \`\`\`jsx React theme={null} + import { useCustomer, CheckoutDialog } from "autumn-js/react"; + + export default function UpgradeButton() { + const { checkout } = useCustomer(); + + return ( + + ); + } + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data } = await autumn.checkout({ + customer_id: "user_or_org_id_from_auth", + product_id: "pro", + }); + + if (data.url) { + // Redirect user to Stripe checkout URL + } else { + // Show upgrade preview to user + } + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + response = await autumn.checkout( + customer_id="user_or_org_id_from_auth", + product_id="pro" + ) + + if response.url: + # Redirect user to Stripe checkout URL + pass + else: + # Show upgrade preview to user + pass + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/checkout" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "product_id": "pro" + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "customer_id": "user_or_org_id_from_auth", + "lines": [ + { + "description": "Pro - $10 / month", + "amount": 10, + "item": { + "type": "price", + "feature_id": null, + "feature": null, + "interval": "month", + "interval_count": 1, + "price": 10, + "display": { + "primary_text": "$10", + "secondary_text": "per month" + } + } + } + ], + "product": { + "id": "pro", + "name": "Pro", + "group": null, + "env": "sandbox", + "is_add_on": false, + "is_default": false, + "archived": false, + "version": 1, + "created_at": 1766428038264, + "items": [ + { + "type": "price", + "feature_id": null, + "feature": null, + "interval": "month", + "interval_count": 1, + "price": 10, + "display": { + "primary_text": "$10", + "secondary_text": "per month" + } + }, + { + "type": "feature", + "feature_id": "usd_credits", + "feature_type": "single_use", + "feature": { + "id": "usd_credits", + "name": "USD credits", + "type": "credit_system", + "display": { + "singular": "USD credits", + "plural": "USD credits" + }, + "credit_schema": [ + { + "metered_feature_id": "basic_messages", + "credit_cost": 0.01 + }, + { + "metered_feature_id": "premium_messages", + "credit_cost": 0.1 + } + ] + }, + "included_usage": 10, + "interval": "month", + "interval_count": 1, + "reset_usage_when_enabled": true, + "entity_feature_id": null, + "display": { + "primary_text": "10 USD credits" + } + } + ], + "free_trial": null, + "base_variant_id": null, + "scenario": "upgrade", + "properties": { + "is_free": false, + "is_one_off": false, + "interval_group": "month", + "has_trial": false, + "updateable": false + } + }, + "current_product": { + "id": "free", + "name": "Free", + "group": null, + "env": "sandbox", + "is_add_on": false, + "is_default": true, + "archived": false, + "version": 1, + "created_at": 1766427877578, + "items": [ + { + "type": "feature", + "feature_id": "usd_credits", + "feature_type": "single_use", + "feature": { + "id": "usd_credits", + "name": "USD credits", + "type": "credit_system", + "display": { + "singular": "USD credits", + "plural": "USD credits" + }, + "credit_schema": [ + { + "metered_feature_id": "basic_messages", + "credit_cost": 0.01 + }, + { + "metered_feature_id": "premium_messages", + "credit_cost": 0.1 + } + ] + }, + "included_usage": 5, + "interval": "month", + "interval_count": 1, + "reset_usage_when_enabled": true, + "entity_feature_id": null, + "display": { + "primary_text": "5 USD credits", + "secondary_text": "per month" + } + } + ], + "free_trial": null, + "base_variant_id": null, + "scenario": "new", + "properties": { + "is_free": true, + "is_one_off": false, + "has_trial": false, + "updateable": false + } + }, + "options": [], + "total": 10, + "currency": "usd", + "url": "https://checkout.stripe.com/c/pay/.......", + "has_prorations": false + } + \`\`\` + + + + + #### Purchasing Top-ups + + When users run low on credits, they can purchase additional credits using our top-up plan. In this example, the user is purchasing 20 USD credits, which will cost them $20. + + + \`\`\`jsx React theme={null} + import { useCustomer, CheckoutDialog } from "autumn-js/react"; + + export default function TopUpButton() { + const { checkout } = useCustomer(); + + return ( + + ); + } + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data } = await autumn.attach({ + customer_id: "user_or_org_id_from_auth", + product_id: "top_up", + options: [{ + feature_id: "usd_credits", + quantity: 20, + }], + }); + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + response = await autumn.attach( + customer_id="user_or_org_id_from_auth", + product_id="top_up", + options=[{ + "feature_id": "usd_credits", + "quantity": 20, + }], + ) + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/attach" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "product_id": "top_up", + "options": [{ + "feature_id": "usd_credits", + "quantity": 20, + }] + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "success": true, + "customer_id": "user_or_org_id_from_auth", + "product_ids": [ + "top_up" + ], + "code": "one_off_product_attached", + "message": "Successfully purchased product(s) Top up and attached to customer John" + } + \`\`\` + + +
+ + +--- + +> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt +`; diff --git a/atmn/src/prompts/customer.ts b/atmn/src/prompts/customer.ts new file mode 100644 index 00000000..6591a257 --- /dev/null +++ b/atmn/src/prompts/customer.ts @@ -0,0 +1,296 @@ +export const customerPrompt = `## Set up Autumn billing integration + +Autumn is a billing and entitlements layer over Stripe, which we are adding into this codebase. Your task is to create an Autumn customer by following these steps, and add it to a place in this app where it will be automatically created. + +### Step 1: Analyze my codebase + +Before making changes, detect: +- Language (TypeScript/JavaScript, Python, or other) +- If TS/JS: Framework (Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other) +- If TS/JS: Is there a React frontend? (Check for React in package.json) + +Also ask me: + +**1. Should Autumn customers be individual users, or organizations?** +- Users (B2C): Each user has their own plan and limits +- Organizations (B2B): Plans and limits are shared across an org + +**2. Have you created an AUTUMN_SECRET_KEY and added it to .env?** +Please prompt them to create one here: https://app.useautumn.com/dev?tab=api_keys and add it to .env as AUTUMN_SECRET_KEY + + + +Tell me what you detected, which path you'll follow and what you'll be adding autumn to. + +--- + +## Path A: React + Node.js (fullstack TypeScript) + +Use this path if there's a React frontend with a Node.js backend. + +### A1. Install the SDK + +**Use the package manager already installed** -- eg user may be using bun, or pnpm. +\`\`\`bash +npm install autumn-js +\`\`\` + +### A2. Mount the handler (server-side) + +This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn. + +**Next.js (App Router):** +\`\`\`typescript +// app/api/autumn/[...all]/route.ts +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async (request) => { + // Get user/org from your auth provider + const session = await auth.api.getSession({ headers: request.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +\`\`\` + +**React Router:** +\`\`\`typescript +// app/routes/api.autumn.tsx +import { autumnHandler } from "autumn-js/react-router"; + +export const { loader, action } = autumnHandler({ + identify: async (args) => { + const session = await auth.api.getSession({ headers: args.request.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, +}); + +// routes.ts - add this route +route("api/autumn/*", "routes/api.autumn.tsx") +\`\`\` + +**Tanstack Start:** +\`\`\`typescript +// routes/api/autumn.$.ts +import { autumnHandler } from "autumn-js/tanstack"; + +const handler = autumnHandler({ + identify: async ({ request }) => { + const session = await auth.api.getSession({ headers: request.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, +}); + +export const Route = createFileRoute("/api/autumn/$")({ + server: { handlers: handler }, +}); +\`\`\` + +**Hono:** +\`\`\`typescript +import { autumnHandler } from "autumn-js/hono"; + +app.use("/api/autumn/*", autumnHandler({ + identify: async (c) => { + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, +})); +\`\`\` + +**Express:** +\`\`\`typescript +import { autumnHandler } from "autumn-js/express"; + +app.use(express.json()); // Must be before autumnHandler +app.use("/api/autumn", autumnHandler({ + identify: async (req) => { + const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, +})); +\`\`\` + +**Fastify:** +\`\`\`typescript +import { autumnHandler } from "autumn-js/fastify"; + +fastify.route({ + method: ["GET", "POST"], + url: "/api/autumn/*", + handler: autumnHandler({ + identify: async (request) => { + const session = await auth.api.getSession({ headers: request.headers as any }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, + }), +}); +\`\`\` + +**Other frameworks (generic handler):** +\`\`\`typescript +import { autumnHandler } from "autumn-js/backend"; + +// Mount this handler onto the /api/autumn/* path in your backend +const handleRequest = async (request) => { + // Your authentication logic here + const customerId = "user_or_org_id_from_auth"; + + let body = null; + if (request.method !== "GET") { + body = await request.json(); + } + + const { statusCode, response } = await autumnHandler({ + customerId, + customerData: { name: "", email: "" }, + request: { + url: request.url, + method: request.method, + body: body, + }, + }); + + return new Response(JSON.stringify(response), { + status: statusCode, + headers: { "Content-Type": "application/json" }, + }); +}; +\`\`\` + +### A3. Add the provider (client-side) + +Wrap your app with AutumnProvider: +\`\`\`tsx +import { AutumnProvider } from "autumn-js/react"; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} +\`\`\` + +If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`: +\`\`\`tsx + +\`\`\` + +### A4. Create a test customer + +Add this hook to any component to verify the integration: +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { customer } = useCustomer(); +console.log("Autumn customer:", customer); +\`\`\` + +This automatically creates an Autumn customer for new users/orgs. + +--- + +## Path B: Backend only (Node.js, Python, or other) + +Use this path if there's no React frontend, or you prefer server-side only. + +### B1. Install the SDK +\`\`\`bash +# Node.js +npm install autumn-js + +# Python +pip install autumn-py +\`\`\` + +### B2. Initialize the client + +**TypeScript/JavaScript:** +\`\`\`typescript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ + secretKey: process.env.AUTUMN_SECRET_KEY, +}); +\`\`\` + +**Python:** +\`\`\`python +from autumn import Autumn + +autumn = Autumn('am_sk_test_xxx') +\`\`\` + +### B3. Create a test customer + +This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider. +The response returns customer state, used to display billing information client-side. Please console.log the Autumn customer client-side. + +**TypeScript:** +\`\`\`typescript +const { data, error } = await autumn.customers.create({ + id: "user_or_org_id_from_auth", + name: "Test User", + email: "test@example.com", +}); +\`\`\` + +**Python:** +\`\`\`python +customer = await autumn.customers.create( + id="user_or_org_id_from_auth", + name="Test User", + email="test@example.com", +) +\`\`\` + +**cURL:** +\`\`\`bash +curl -X POST https://api.useautumn.com/customers \\ + -H "Authorization: Bearer am_sk_test_xxx" \\ + -H "Content-Type: application/json" \\ + -d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}' +\`\`\` + +When calling these functions from the client, the SDK exports types for all response objects. Use these for type-safe code. + +\`\`\`tsx +import type { Customer } from "autumn-js"; +\`\`\` + +--- + +## Verify + +After setup, tell me: +1. What stack you detected +2. Which path you followed +3. What files you created/modified +4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard + +**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. + +Docs: https://docs.useautumn.com/llms.txt`; diff --git a/atmn/src/prompts/payments.ts b/atmn/src/prompts/payments.ts new file mode 100644 index 00000000..31300670 --- /dev/null +++ b/atmn/src/prompts/payments.ts @@ -0,0 +1,254 @@ +import { prepaidDocs } from "./prepaidDocs.js"; + +export const paymentsPrompt = `## Add Autumn payment flow + +Autumn handles Stripe checkout and plan changes. Your task is to add the payment flow to this codebase for ALL plans in the Autumn configuration. + +### Step 1: Detect my integration type + +Check if this codebase already has Autumn set up: +- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **Path A: React** +- If there's just an \`Autumn\` client initialized → **Path B: Backend SDK** + +Before implementing: +1. Tell me which path you'll follow before proceeding. +2. Tell me that I will be building pricing cards to handle billing flows, and ask for any guidance or any input + +--- + +## Path A: React + +### Checkout Flow + +Use \`checkout\` from \`useCustomer\`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file). + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { checkout } = useCustomer(); + +const data = await checkout({ productId: "pro" }); + +if (!data.url) { + // Returning customer → show confirmation dialog with result data + // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } +} +\`\`\` + +After user confirms in your dialog, call \`attach\` to enable plan (and charge card as needed) + +\`\`\`tsx +const { attach } = useCustomer(); + +await attach({ productId: "pro" }); +\`\`\` + +### Getting Billing State + +Use \`usePricingTable\` to get products with their billing scenario and display state. + +\`\`\`tsx +import { usePricingTable } from "autumn-js/react"; + +function PricingPage() { + const { products } = usePricingTable(); + // Each product has: scenario, properties + // scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" +} +\`\`\` + +### Canceling +Only use this if there is no free plan in the user's Autumn config. If there is a free plan, then you can cancel by attaching the free plan. + +\`\`\`tsx +const { cancel } = useCustomer(); +await cancel({ productId: "pro" }); +\`\`\` + +--- + +## Path B: Backend SDK + +### Checkout Flow + +Payments are a 2-step process: +1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer) +2. **attach** - Confirms purchase when no URL was returned + +**TypeScript:** +\`\`\`typescript +import { Autumn } from "autumn-js"; +import type { CheckoutResult, AttachResult } from "autumn-js"; + +const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY }); + +// Step 1: Get checkout info +const { data } = await autumn.checkout({ + customer_id: "user_or_org_id_from_auth", + product_id: "pro", +}) as { data: CheckoutResult }; + +if (data.url) { + // New customer → redirect to Stripe + return redirect(data.url); +} else { + // Returning customer → return preview data for confirmation UI + // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } + return data; +} + +// Step 2: After user confirms (only if no URL) +const { data: attachData } = await autumn.attach({ + customer_id: "user_or_org_id_from_auth", + product_id: "pro", +}) as { data: AttachResult }; +\`\`\` + +**Python:** +\`\`\`python +from autumn import Autumn + +autumn = Autumn('am_sk_test_xxx') + +# Step 1: Get checkout info +response = await autumn.checkout( + customer_id="user_or_org_id_from_auth", + product_id="pro", +) + +if response.url: + # New customer → redirect to Stripe + return redirect(response.url) +else: + # Returning customer → return preview data for confirmation UI + return response + +# Step 2: After user confirms +attach_response = await autumn.attach( + customer_id="user_or_org_id_from_auth", + product_id="pro", +) +\`\`\` + +For prepaid pricing options, see the end of this message. + +### Getting Billing State + +Use \`products.list\` with a \`customer_id\` to get products with their billing scenario. **Don't build custom billing state logic.** + +**TypeScript:** +\`\`\`typescript +const { data } = await autumn.products.list({ + customer_id: "user_or_org_id_from_auth", +}); + +data.list.forEach((product) => { + const { scenario } = product; + // "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" +}); +\`\`\` + +**Python:** +\`\`\`python +response = await autumn.products.list(customer_id="user_or_org_id_from_auth") + +for product in response.list: + scenario = product.scenario +\`\`\` + +**curl:** +\`\`\`bash +curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \\ + -H "Authorization: Bearer $AUTUMN_SECRET_KEY" +\`\`\` + +### Canceling + +\`\`\`typescript +await autumn.cancel({ customer_id: "...", product_id: "pro" }); +\`\`\` + +Or attach a free product ID to downgrade. + +--- + +## Common Patterns + +### Pricing Button Text + +\`\`\`typescript +const SCENARIO_TEXT: Record = { + scheduled: "Plan Scheduled", + active: "Current Plan", + renew: "Renew", + upgrade: "Upgrade", + new: "Enable", + downgrade: "Downgrade", + cancel: "Cancel Plan", +}; + +export const getPricingButtonText = (product: Product): string => { + const { scenario, properties } = product; + const { is_one_off, updateable, has_trial } = properties ?? {}; + + if (has_trial) return "Start Trial"; + if (scenario === "active" && updateable) return "Update"; + if (scenario === "new" && is_one_off) return "Purchase"; + + return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan"; +}; +\`\`\` + +### Confirmation Dialog Text + +\`\`\`typescript +import type { CheckoutResult, Product } from "autumn-js"; + +export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => { + const { product, current_product, next_cycle } = result; + const scenario = product.scenario; + const productName = product.name; + const currentProductName = current_product?.name; + const nextCycleDate = next_cycle?.starts_at + ? new Date(next_cycle.starts_at).toLocaleDateString() + : undefined; + + const isRecurring = !product.properties?.is_one_off; + + const CONFIRMATION_TEXT: Record = { + scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." }, + active: { title: "Already Active", message: "You are already subscribed to this product." }, + renew: { title: "Renew", message: \`Renew your subscription to \${productName}.\` }, + upgrade: { title: \`Upgrade to \${productName}\`, message: \`Upgrade to \${productName}. Your card will be charged immediately.\` }, + downgrade: { title: \`Downgrade to \${productName}\`, message: \`\${currentProductName} will be cancelled. \${productName} begins \${nextCycleDate}.\` }, + cancel: { title: "Cancel", message: \`Your subscription to \${currentProductName} will end \${nextCycleDate}.\` }, + }; + + if (scenario === "new") { + return isRecurring + ? { title: \`Subscribe to \${productName}\`, message: \`Subscribe to \${productName}. Charged immediately.\` } + : { title: \`Purchase \${productName}\`, message: \`Purchase \${productName}. Charged immediately.\` }; + } + + return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." }; +}; +\`\`\` + +--- + +## Notes + +- **NB: the result is \`data.url\`, NOT \`data.checkout_url\`** +- This handles all upgrades, downgrades, renewals, uncancellations automatically +- Product IDs come from the Autumn configuration +- Pass \`successUrl\` to \`checkout\` to redirect users after payment +- For prepaid pricing examples, see the end of this message. + +**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. + +Docs: https://docs.useautumn.com/llms.txt + + +${prepaidDocs} + +`; diff --git a/atmn/src/prompts/prepaidDocs.ts b/atmn/src/prompts/prepaidDocs.ts new file mode 100644 index 00000000..45c8f6d1 --- /dev/null +++ b/atmn/src/prompts/prepaidDocs.ts @@ -0,0 +1,463 @@ +export const prepaidDocs = ` + +# Prepaid top-ups + +> Let customers purchase prepaid packages and top-ups. + +If a user hits a usage limit you granted them, they may be willing to purchase a top-up. + +These are typically one-time purchases (or less commonly, recurring add-ons) that grant a fixed usage of a feature. + +This gives users full spend control and allows your business to be paid upfront. For these reasons, it tends to be a more popular alternative to usage-based pricing -- eg, OpenAI uses this model for their API. + +## Example case + +In this example, we have an AI chatbot that offers: + +* 10 premium messages for free +* An option for customers to top-up premium messages in packages of $10 per 100 messages. + +## Configure Pricing + + + + #### Create Features + + Create a \`metered\` \`consumable\` feature for our premium messages, so we can track its balance. + + + + + + + + + + #### Create Free and Top-up Plans + + Create our free plan, and assign 10 premium messages to it. These are "one-off" credits, that will not reset periodically. + + + Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers. + + + + + + + + + Now we'll create our top-up plan. We'll add a price to our premium messages feature, at $10 per 100 messages. These are "one-off" purchases, with a \`prepaid\` billing method. + + \`prepaid\` features require a \`quantity\` to be sent in when a customer attaches this product, so the customer can specify how many premium messages they want to top up with. + + + + + + + + + +## Implementation + + + + #### Create an Autumn Customer + + When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them 10 premium messages. + + + \`\`\`jsx React + import { useCustomer } from "autumn-js/react"; + + const App = () => { + const { customer } = useCustomer(); + + console.log("Autumn customer:", customer); + + return

Welcome, {customer?.name || "user"}!

; + }; + \`\`\` + + \`\`\`typescript Node.js + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data, error } = await autumn.customers.create({ + id: "user_or_org_id_from_auth", + name: "John Yeo", + email: "john@example.com", + }); + \`\`\` + + \`\`\`python Python + import asyncio + from autumn import Autumn + + autumn = Autumn('am_sk_42424242') + + async def main(): + customer = await autumn.customers.create( + id="user_or_org_id_from_auth", + name="John Yeo", + email="john@example.com", + ) + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL + curl --request POST \ + --url https://api.useautumn.com/customers \ + --header 'Authorization: Bearer am_sk_42424242' \ + --header 'Content-Type: application/json' \ + --data '{ + "id": "user_or_org_id_from_auth", + "name": "John Yeo", + "email": "john@example.com" + }' + \`\`\` +
+
+ + + #### Checking for access + + Every time our user wants to send a premium message, we'll first check if they have enough premium messages remaining. + + + \`\`\`jsx React wrap + import { useCustomer } from "autumn-js/react"; + + export function CheckPremiumMessage() { + const { check, refetch } = useCustomer(); + + const handleCheckAccess = async () => { + const { data } = await check({ featureId: "premium-messages" }); + + if (!data?.allowed) { + alert("You've run out of premium messages"); + } else { + // proceed with sending message + await refetch(); + } + }; + } + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data } = await autumn.check({ + customer_id: "user_or_org_id_from_auth", + feature_id: "premium_messages", + }); + + if (!data.allowed) { + console.log("User has run out of premium messages"); + return; + } + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_1234567890") + + async def main(): + response = await autumn.check( + customer_id="user_or_org_id_from_auth", + feature_id="premium_messages", + ) + + if not response.allowed: + print("User has run out of premium messages") + return + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/check" \ + -H "Authorization: Bearer am_sk_1234567890" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "premium_messages" + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "customer_id": "user_or_org_id_from_auth", + "feature_id": "premium_messages", + "code": "feature_found", + "allowed": true, + "balance": 10, + "usage": 0, + "included_usage": 10, + "unlimited": false, + "interval": null, + "interval_count": 1, + "next_reset_at": null, + "overage_allowed": false + } + \`\`\` + + + + + #### Tracking premium messages + + Now let's implement our usage tracking and use up our premium messages. In this example, we're using 5 premium messages. + + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + await autumn.track({ + customer_id: "user_or_org_id_from_auth", + feature_id: "premium_messages", + value: 5, + }); + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + await autumn.track( + customer_id="user_or_org_id_from_auth", + feature_id="premium_messages", + value=5, + ) + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/track" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "premium_messages", + "value": 5 + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "code": "event_received", + "customer_id": "user_or_org_id_from_auth", + "feature_id": "premium_messages" + } + \`\`\` + + + + + #### Purchasing top-ups + + When users run out of premium messages, they can purchase additional messages using our top-up plan. In this example, the user is purchasing 200 premium messages, which will cost them $20. + + + \`\`\`jsx React theme={null} + import { useCustomer, CheckoutDialog } from "autumn-js/react"; + + export default function TopUpButton() { + const { checkout } = useCustomer(); + + return ( + + ); + } + \`\`\` + + \`\`\`typescript Node.js theme={null} + import { Autumn } from "autumn-js"; + + const autumn = new Autumn({ + secretKey: 'am_sk_42424242', + }); + + const { data } = await autumn.checkout({ + customer_id: "user_or_org_id_from_auth", + product_id: "top_up", + options: [{ + feature_id: "premium_messages", + quantity: 200, + }], + }); + + if (data.url) { + // Redirect user to Stripe checkout URL + } else { + // Show purchase preview to user + } + \`\`\` + + \`\`\`python Python theme={null} + import asyncio + from autumn import Autumn + + autumn = Autumn("am_sk_42424242") + + async def main(): + response = await autumn.checkout( + customer_id="user_or_org_id_from_auth", + product_id="top-up", + options=[{ + "feature_id": "premium-messages", + "quantity": 200, + }], + ) + + if response.url: + # Redirect user to Stripe checkout URL + pass + else: + # Show purchase preview to user + pass + + asyncio.run(main()) + \`\`\` + + \`\`\`bash cURL theme={null} + curl -X POST "https://api.useautumn.com/v1/checkout" \ + -H "Authorization: Bearer am_sk_42424242" \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "product_id": "top-up", + "options": [{ + "feature_id": "premium-messages", + "quantity": 200 + }] + }' + \`\`\` + + + + \`\`\`json theme={null} + { + "customer_id": "user_or_org_id_from_auth", + "lines": [ + { + "description": "Top-up - 200 premium messages", + "amount": 20, + "item": { + "type": "feature", + "feature_id": "premium-messages", + "feature_type": "prepaid", + "feature": { + "id": "premium-messages", + "name": "Premium messages", + "type": "metered", + "display": { + "singular": "premium message", + "plural": "premium messages" + } + }, + "quantity": 200, + "price": 10, + "price_per": 100, + "display": { + "primary_text": "200 premium messages", + "secondary_text": "$10 per 100 messages" + } + } + } + ], + "product": { + "id": "top-up", + "name": "Top-up", + "group": null, + "env": "sandbox", + "is_add_on": false, + "is_default": false, + "archived": false, + "version": 1, + "created_at": 1766428038264, + "items": [ + { + "type": "feature", + "feature_id": "premium-messages", + "feature_type": "prepaid", + "feature": { + "id": "premium-messages", + "name": "Premium messages", + "type": "metered", + "display": { + "singular": "premium message", + "plural": "premium messages" + } + }, + "price": 10, + "price_per": 100, + "display": { + "primary_text": "$10 per 100 messages" + } + } + ], + "free_trial": null, + "base_variant_id": null, + "scenario": "attach", + "properties": { + "is_free": false, + "is_one_off": true, + "has_trial": false, + "updateable": false + } + }, + "total": 20, + "currency": "usd", + "url": "https://checkout.stripe.com/c/pay/.......", + "has_prorations": false + } + \`\`\` + + + Once the customer completes the payment, they will have an additional 200 premium messages available to use. You can display to the user by getting balances from the \`customer\` method. + +
+ + +--- + +> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt + +`; diff --git a/atmn/src/prompts/pricing.ts b/atmn/src/prompts/pricing.ts new file mode 100644 index 00000000..55d70d21 --- /dev/null +++ b/atmn/src/prompts/pricing.ts @@ -0,0 +1,137 @@ +export const pricingPrompt = `## Design your Autumn pricing model + +This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and products (plans). + +### Step 1: Understand your pricing needs + +Before building, consider: +1. What features do you want to offer? (API calls, seats, storage, etc.) +2. What plans do you want? (Free, Pro, Enterprise tiers?) +3. How should usage be measured and limited? + +--- + +## Feature Types + +Autumn supports these feature types: + +- **single_use**: Consumable resources (API calls, tokens, messages, credits, generations) +- **continuous_use**: Non-consumable resources (seats, workspaces, projects, team members) +- **boolean**: On/off features (advanced analytics, priority support, SSO) +- **credit_system**: A unified credit pool that maps to multiple single_use features + +--- + +## Item Types + +Products contain an array of items. There are distinct item patterns: + +### 1. Flat Fee (standalone price, no feature) +\`\`\`typescript +{ feature_id: null, price: 13, interval: "month" } +\`\`\` +Customer pays $13/month as a base subscription fee. + +### 2. Free Feature Allocation (feature grant, no price) +\`\`\`typescript +{ feature_id: "credits", included_usage: 10000 } +\`\`\` +Customer gets 10,000 credits included. + +### 3. Metered/Usage-Based Pricing +\`\`\`typescript +{ feature_id: "credits", included_usage: 10000, price: 0.01, usage_model: "pay_per_use", interval: "month" } +\`\`\` +Customer can use 10,000 credits per month, then pays $0.01 per credit after that. + +### 4. Prepaid Credit Purchase (one-time purchase of usage) +\`\`\`typescript +{ feature_id: "credits", price: 10, usage_model: "prepaid", billing_units: 10000 } +\`\`\` +Customer pays $10 once to receive 10,000 credits. + +### 5. Per-Unit Pricing Structure +For any "per-X" pricing (like "$Y per seat", "$Y per project", "$Y per website"), use this pattern: +\`\`\`typescript +// Base subscription fee +{ feature_id: null, price: 10, interval: "month" } +// Unit allocation +{ feature_id: "seats", included_usage: 1, price: 10, usage_model: "pay_per_use", billing_units: 1 } +\`\`\` +This creates: $10/month base price that includes 1 unit, then $10 per additional unit purchased. + +**Always** use this two-item pattern for any per-unit pricing - never use pure per-unit without a base fee. + +--- + +## Guidelines + +### Naming Conventions +- Product and Feature IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`) + +### Default Plans +- **Never** set \`is_default: true\` for plans with prices. Default plans must be free. + +### Enterprise Plans +- Ignore "Enterprise" plans with custom pricing in the config. Custom plans can be created per-customer in the Autumn dashboard. + +### Annual Plans +- For annual variants, create a separate plan with annual price interval. Name it \` - Annual\`. + +### Currency +- Currency can be changed in the Autumn dashboard under Developer > Stripe. + +--- + +## Example Configuration + +\`\`\`typescript +import { feature, plan, planFeature } from "atmn"; + +// Features +export const messages = feature({ + id: "messages", + name: "Messages", + type: "single_use", +}); + +export const seats = feature({ + id: "seats", + name: "Team Seats", + type: "continuous_use", +}); + +// Plans +export const free = plan({ + id: "free", + name: "Free", + is_default: true, + items: [ + { feature_id: "messages", included_usage: 100 }, + { feature_id: "seats", included_usage: 1 }, + ], +}); + +export const pro = plan({ + id: "pro", + name: "Pro", + items: [ + { feature_id: null, price: 29, interval: "month" }, + { feature_id: "messages", included_usage: 10000, price: 0.01, usage_model: "pay_per_use" }, + { feature_id: "seats", included_usage: 5, price: 10, usage_model: "pay_per_use", billing_units: 1 }, + ], +}); +\`\`\` + +--- + +## Next Steps + +Once you've designed your pricing: +1. Update \`autumn.config.ts\` with your features and plans +2. Run \`atmn push\` to sync your configuration to Autumn +3. Test in sandbox mode before going live + +For more help: https://discord.gg/atmn (we're very responsive) + +Docs: https://docs.useautumn.com/llms.txt`; diff --git a/atmn/src/prompts/usage.ts b/atmn/src/prompts/usage.ts new file mode 100644 index 00000000..b648dd10 --- /dev/null +++ b/atmn/src/prompts/usage.ts @@ -0,0 +1,131 @@ +import { creditSystemDocs } from "./creditSystemDocs.js"; + +export const usagePrompt = `## Add Autumn gating and usage tracking + +Autumn tracks feature usage and enforces limits. Add usage tracking to this codebase. + +### Step 1: Detect my integration type + +Check if this codebase already has Autumn set up: +- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **React hooks available** (can use for UX) +- Backend SDK should **always** be used to enforce limits server-side + +Tell me what you detected before proceeding. + +--- + +## Frontend checks (React hooks) + +Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security. + +### Check feature access +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export function SendChatMessage() { + const { check, refetch } = useCustomer(); + + const handleSendMessage = async () => { + const { data } = check({ featureId: "messages" }); + + if (!data?.allowed) { + alert("You're out of messages"); + } else { + //send chatbot message + //then, refresh customer usage data + await refetch(); + } + }; +} +\`\`\` + +--- + +## Backend checks (required for security) + +**Always check on the backend** before executing any protected action. Frontend checks can be bypassed. + +### TypeScript +\`\`\`typescript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ + secretKey: process.env.AUTUMN_SECRET_KEY, +}); + +// Check before executing the action +const { data } = await autumn.check({ + customer_id: "user_or_org_id_from_auth", + feature_id: "api_calls", +}); + +if (!data.allowed) { + return { error: "Usage limit reached" }; +} + +// Safe to proceed - do the actual work here +const result = await doTheActualWork(); + +// Track usage after success +await autumn.track({ + customer_id: "user_or_org_id_from_auth", + feature_id: "api_calls", + value: 1, +}); + +return result; +\`\`\` + +### Python +\`\`\`python +from autumn import Autumn + +autumn = Autumn('am_sk_test_xxx') + +# Check before executing the action +response = await autumn.check( + customer_id="user_or_org_id_from_auth", + feature_id="api_calls" +) + +if not response.allowed: + raise HTTPException(status_code=403, detail="Usage limit reached") + +# Safe to proceed - do the actual work here +result = await do_the_actual_work() + +# Track usage after success +await autumn.track( + customer_id="user_or_org_id_from_auth", + feature_id="api_calls", + value=1 +) + +return result +\`\`\` + +--- + +## Notes + +- **Frontend checks** = UX (show/hide UI, display limits) - can be bypassed by users +- **Backend checks** = Security (enforce limits) - required before any protected action +- Pattern: check → do work → track (only track after successful completion) +- Feature IDs come from the Autumn configuration +- Current usage and total limit can be taken from from Customer object and displayed -- see the Customer types from the Autumn SDK +\`\`\`tsx +import type { Customer } from "autumn-js"; + +//Balance is: customer.features..balance +\`\`\` + +For credit systems, see the end of this message. + +**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. + +Docs: https://docs.useautumn.com/llms.txt + + +${creditSystemDocs} + +`; \ No newline at end of file diff --git a/atmn/src/views/App.tsx b/atmn/src/views/App.tsx new file mode 100644 index 00000000..a27f1396 --- /dev/null +++ b/atmn/src/views/App.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Box, Text } from "ink"; + +export default function App() { + return ( + + + atmn + + Autumn CLI - Interactive Mode + + ); +} diff --git a/atmn/src/views/html/oauth-callback.ts b/atmn/src/views/html/oauth-callback.ts new file mode 100644 index 00000000..d67e9ff7 --- /dev/null +++ b/atmn/src/views/html/oauth-callback.ts @@ -0,0 +1,251 @@ +// HTML templates for OAuth callback pages + +/** Shared styles matching Autumn's theme with dark mode support */ +export const BASE_STYLES = ` + @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); + + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #fafaf9; + color: #121212; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + @media (prefers-color-scheme: dark) { + body { + background: #161616; + color: #ddd; + } + } + + .container { + text-align: center; + padding: 3rem 2rem; + max-width: 400px; + } + + .icon-container { + width: 64px; + height: 64px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.5rem; + } + + .icon-success { + background: linear-gradient(135deg, #f3e8ff 0%, #ede1ff 100%); + border: 2px solid #c4b5fd; + box-shadow: 0 2px 4px rgba(136, 56, 255, 0.15); + } + + @media (prefers-color-scheme: dark) { + .icon-success { + background: linear-gradient(135deg, #2d1f4e 0%, #3d2a5e 100%); + border: 2px solid #6b46c1; + } + } + + .icon-error { + background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%); + border: 2px solid #fca5a5; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.15); + } + + @media (prefers-color-scheme: dark) { + .icon-error { + background: linear-gradient(135deg, #4a1a1a 0%, #5c2020 100%); + border: 2px solid #dc2626; + } + } + + .icon-success svg { + color: #8838ff; + } + + @media (prefers-color-scheme: dark) { + .icon-success svg { + color: #a855f7; + } + } + + .icon-error svg { + color: #dc2626; + } + + @media (prefers-color-scheme: dark) { + .icon-error svg { + color: #f87171; + } + } + + h1 { + font-size: 20px; + font-weight: 600; + margin-bottom: 0.5rem; + letter-spacing: -0.02em; + } + + .success h1 { + color: #121212; + } + + @media (prefers-color-scheme: dark) { + .success h1 { + color: #ddd; + } + } + + .error h1 { + color: #dc2626; + } + + @media (prefers-color-scheme: dark) { + .error h1 { + color: #f87171; + } + } + + .description { + font-size: 14px; + font-weight: 450; + color: #666; + line-height: 1.5; + margin-bottom: 1.5rem; + } + + @media (prefers-color-scheme: dark) { + .description { + color: #999; + } + } + + .hint { + font-size: 13px; + font-weight: 450; + color: #888; + padding: 0.75rem 1rem; + background: #f5f5f4; + border-radius: 8px; + border: 1px solid #e5e5e5; + } + + @media (prefers-color-scheme: dark) { + .hint { + color: #888; + background: #1d1d1d; + border: 1px solid #2c2c2c; + } + } + + .footer { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + } + + .footer-card { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 500; + color: #888; + padding: 0.5rem 0.75rem; + background: #f5f5f4; + border-radius: 6px; + border: 1px solid #e5e5e5; + } + + @media (prefers-color-scheme: dark) { + .footer-card { + color: #777; + background: #1d1d1d; + border: 1px solid #2c2c2c; + } + } +`; + +/** Generate success HTML page for OAuth callback */ +export function getSuccessHtml(): string { + return ` + + + + + Authorization Successful - Autumn + + + +
+
+ + + +
+

Authorization Successful

+

Your CLI has been authenticated successfully.

+

You can close this window and return to your terminal.

+
+ + +`; +} + +/** Generate error HTML page for OAuth callback */ +export function getErrorHtml(errorMessage: string): string { + // Escape HTML to prevent XSS + const safeMessage = errorMessage + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + return ` + + + + + Authorization Failed - Autumn + + + +
+
+ + + + +
+

Authorization Failed

+

${safeMessage}

+

Please close this window and try again in your terminal.

+
+ + +`; +} diff --git a/atmn/src/views/react/components/Card.tsx b/atmn/src/views/react/components/Card.tsx new file mode 100644 index 00000000..964070bc --- /dev/null +++ b/atmn/src/views/react/components/Card.tsx @@ -0,0 +1,133 @@ +import { Box, Text } from "ink"; +import React, { type ReactNode, useEffect, useId, useMemo } from "react"; +import { useCardWidth } from "./providers/CardWidthContext.js"; + +const DEFAULT_WIDTH = 65; +// Account for border (2 chars) + padding (2 chars on each side) +const BORDER_PADDING_OVERHEAD = 6; + +interface CardProps { + title: string; + children?: ReactNode; +} + +/** + * Calculate the display width of a string (accounting for ANSI codes, emojis, etc.) + */ +function getStringWidth(str: string): number { + // Strip ANSI codes + const stripped = str.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ); + + let width = 0; + for (const char of stripped) { + const code = char.codePointAt(0) ?? 0; + // Emoji and wide characters (rough heuristic) + if (code > 0x1f600 || (code >= 0x2600 && code <= 0x27bf)) { + width += 2; + } else if (code >= 0x4e00 && code <= 0x9fff) { + // CJK characters + width += 2; + } else { + width += 1; + } + } + return width; +} + +/** + * Recursively extract text content from React children to measure width + */ +function extractTextContent(children: ReactNode): string[] { + const lines: string[] = []; + + const extract = (node: ReactNode): void => { + if (node === null || node === undefined || typeof node === "boolean") { + return; + } + + if (typeof node === "string" || typeof node === "number") { + lines.push(String(node)); + return; + } + + if (Array.isArray(node)) { + for (const child of node) { + extract(child); + } + return; + } + + if (React.isValidElement(node)) { + const props = node.props as { children?: ReactNode }; + if (props.children) { + extract(props.children); + } + } + }; + + extract(children); + return lines; +} + +/** + * Reusable card component with rounded border. + * Coordinates width with other Cards via CardWidthContext. + */ +export function Card({ title, children }: CardProps) { + const id = useId(); + const cardWidth = useCardWidth(); + + // Calculate the content width needed for this card + const contentWidth = useMemo(() => { + const titleWidth = getStringWidth(title); + + let maxLineWidth = titleWidth; + if (children) { + const textLines = extractTextContent(children); + for (const line of textLines) { + const lineWidth = getStringWidth(line); + if (lineWidth > maxLineWidth) { + maxLineWidth = lineWidth; + } + } + } + + // Add overhead for border and padding + return maxLineWidth + BORDER_PADDING_OVERHEAD; + }, [title, children]); + + // Register our width with the context + useEffect(() => { + if (cardWidth) { + cardWidth.registerWidth(id, contentWidth); + return () => cardWidth.unregisterWidth(id); + } + }, [id, contentWidth, cardWidth]); + + // Use shared width from context, or fallback to default + const width = cardWidth?.width ?? Math.max(DEFAULT_WIDTH, contentWidth); + + return ( + + + {title} + + {children && ( + + {children} + + )} + + ); +} diff --git a/atmn/src/views/react/components/KeyValue.tsx b/atmn/src/views/react/components/KeyValue.tsx new file mode 100644 index 00000000..2fd918c7 --- /dev/null +++ b/atmn/src/views/react/components/KeyValue.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Text } from "ink"; + +interface KeyValueProps { + label: string; + value: string; +} + +/** + * Displays a key-value pair with styled label + */ +export function KeyValue({ label, value }: KeyValueProps) { + return ( + + {label}: {value} + + ); +} diff --git a/atmn/src/views/react/components/LoadingText.tsx b/atmn/src/views/react/components/LoadingText.tsx new file mode 100644 index 00000000..91f1702f --- /dev/null +++ b/atmn/src/views/react/components/LoadingText.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Text } from "ink"; +import Spinner from "ink-spinner"; + +interface LoadingTextProps { + text: string; +} + +/** + * Shows a spinner with text + */ +export function LoadingText({ text }: LoadingTextProps) { + return ( + + {text} + + ); +} diff --git a/atmn/src/views/react/components/PromptCard.tsx b/atmn/src/views/react/components/PromptCard.tsx new file mode 100644 index 00000000..26642b29 --- /dev/null +++ b/atmn/src/views/react/components/PromptCard.tsx @@ -0,0 +1,165 @@ +import { Box, Text } from "ink"; +import SelectInput from "ink-select-input"; +import React, { type ReactNode, useEffect, useId, useMemo } from "react"; +import { useCardWidth } from "./providers/CardWidthContext.js"; + +const DEFAULT_WIDTH = 65; +// Account for border (2 chars) + padding (2 chars on each side) +const BORDER_PADDING_OVERHEAD = 6; + +interface PromptOption { + label: string; + value: string; +} + +interface PromptCardProps { + title: string; + icon?: string; + children: ReactNode; + options: PromptOption[]; + onSelect: (value: string) => void; +} + +/** + * Calculate the display width of a string (accounting for ANSI codes, emojis, etc.) + */ +function getStringWidth(str: string): number { + // Strip ANSI codes + const stripped = str.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ); + + let width = 0; + for (const char of stripped) { + const code = char.codePointAt(0) ?? 0; + // Emoji and wide characters (rough heuristic) + if (code > 0x1f600 || (code >= 0x2600 && code <= 0x27bf)) { + width += 2; + } else if (code >= 0x4e00 && code <= 0x9fff) { + // CJK characters + width += 2; + } else { + width += 1; + } + } + return width; +} + +/** + * Recursively extract text content from React children to measure width + */ +function extractTextContent(children: ReactNode): string[] { + const lines: string[] = []; + + const extract = (node: ReactNode): void => { + if (node === null || node === undefined || typeof node === "boolean") { + return; + } + + if (typeof node === "string" || typeof node === "number") { + lines.push(String(node)); + return; + } + + if (Array.isArray(node)) { + for (const child of node) { + extract(child); + } + return; + } + + if (React.isValidElement(node)) { + const props = node.props as { children?: ReactNode }; + if (props.children) { + extract(props.children); + } + } + }; + + extract(children); + return lines; +} + +/** + * Card component with a select menu for prompts + * Coordinates width with other Cards via CardWidthContext + */ +export function PromptCard({ + title, + icon, + children, + options, + onSelect, +}: PromptCardProps) { + const id = useId(); + const cardWidth = useCardWidth(); + + const handleSelect = (item: { value: string }) => { + onSelect(item.value); + }; + + // Calculate the content width needed for this card + const contentWidth = useMemo(() => { + const fullTitle = icon ? `${icon} ${title}` : title; + const titleWidth = getStringWidth(fullTitle); + + let maxLineWidth = titleWidth; + + // Check children content + if (children) { + const textLines = extractTextContent(children); + for (const line of textLines) { + const lineWidth = getStringWidth(line); + if (lineWidth > maxLineWidth) { + maxLineWidth = lineWidth; + } + } + } + + // Check option labels + for (const opt of options) { + // Add 4 for the select indicator "❯ " or " " + const optWidth = getStringWidth(opt.label) + 4; + if (optWidth > maxLineWidth) { + maxLineWidth = optWidth; + } + } + + // Add overhead for border and padding + return maxLineWidth + BORDER_PADDING_OVERHEAD; + }, [title, icon, children, options]); + + // Register our width with the context + useEffect(() => { + if (cardWidth) { + cardWidth.registerWidth(id, contentWidth); + return () => cardWidth.unregisterWidth(id); + } + }, [id, contentWidth, cardWidth]); + + // Use shared width from context, or fallback to default + const width = cardWidth?.width ?? Math.max(DEFAULT_WIDTH, contentWidth); + + return ( + + + {icon ? `${icon} ${title}` : title} + + + {children} + + + + + + ); +} diff --git a/atmn/src/views/react/components/SelectMenu.tsx b/atmn/src/views/react/components/SelectMenu.tsx new file mode 100644 index 00000000..24a361ac --- /dev/null +++ b/atmn/src/views/react/components/SelectMenu.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import SelectInput from "ink-select-input"; + +/** + * Wrapper around ink-select-input for consistent styling + */ + +export interface SelectMenuItem { + label: string; + value: V; + disabled?: boolean; +} + +export interface SelectMenuProps { + items: SelectMenuItem[]; + onSelect: (item: SelectMenuItem) => void; +} + +export function SelectMenu({ + items, + onSelect, +}: SelectMenuProps) { + return ; +} diff --git a/atmn/src/views/react/components/StatusLine.tsx b/atmn/src/views/react/components/StatusLine.tsx new file mode 100644 index 00000000..d337e799 --- /dev/null +++ b/atmn/src/views/react/components/StatusLine.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Text } from "ink"; +import Spinner from "ink-spinner"; + +interface StatusLineProps { + status: "pending" | "loading" | "success" | "error"; + message: string; + detail?: string; +} + +export function StatusLine({ status, message, detail }: StatusLineProps) { + const icon = { + pending: , + loading: ( + + + + ), + success: , + error: , + }[status]; + + return ( + + {icon} {message} + {detail && ({detail})} + + ); +} diff --git a/atmn/src/views/react/components/StatusRow.tsx b/atmn/src/views/react/components/StatusRow.tsx new file mode 100644 index 00000000..0661957a --- /dev/null +++ b/atmn/src/views/react/components/StatusRow.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { Text } from "ink"; +import Spinner from "ink-spinner"; + +export type StatusRowStatus = + | "pending" + | "loading" + | "success" + | "warning" + | "error" + | "skipped"; + +export type StatusRowAction = + | "created" + | "updated" + | "deleted" + | "archived" + | "skipped" + | "unchanged" + | "versioned"; + +interface StatusRowProps { + status: StatusRowStatus; + label: string; + detail?: string; + action?: StatusRowAction; +} + +/** + * Generic status row component with icon indicator + */ +export function StatusRow({ status, label, detail, action }: StatusRowProps) { + const renderIcon = () => { + switch (status) { + case "pending": + return ; + case "loading": + return ( + + + + ); + case "success": + return ; + case "warning": + return ; + case "error": + return ; + case "skipped": + return ; + default: + return ; + } + }; + + const renderAction = () => { + if (!action) return null; + + const actionColor = + action === "skipped" || action === "unchanged" ? "gray" : "green"; + + return ( + + {" "} + ({action}) + + ); + }; + + return ( + + {renderIcon()} {label} + {detail && {detail}} + {renderAction()} + + ); +} diff --git a/atmn/src/views/react/components/StepHeader.tsx b/atmn/src/views/react/components/StepHeader.tsx new file mode 100644 index 00000000..08c92cfe --- /dev/null +++ b/atmn/src/views/react/components/StepHeader.tsx @@ -0,0 +1,24 @@ +import { Box, Text } from "ink"; +import React from "react"; + +interface StepHeaderProps { + step: number; + totalSteps: number; + title: string; +} + +export function StepHeader({ step, totalSteps, title }: StepHeaderProps) { + return ( + + + + Step {step}/{totalSteps}: + {" "} + {title} + + + {"─".repeat(`Step ${step}/${totalSteps}:`.length)} + + + ); +} diff --git a/atmn/src/views/react/components/index.ts b/atmn/src/views/react/components/index.ts new file mode 100644 index 00000000..1b010ff0 --- /dev/null +++ b/atmn/src/views/react/components/index.ts @@ -0,0 +1,17 @@ +export { Card } from "./Card.js"; +export { KeyValue } from "./KeyValue.js"; +export { LoadingText } from "./LoadingText.js"; +export { PromptCard } from "./PromptCard.js"; +export { CardWidthProvider } from "./providers/CardWidthContext.js"; +export { + SelectMenu, + type SelectMenuItem, + type SelectMenuProps, +} from "./SelectMenu.js"; +export { StatusLine } from "./StatusLine.js"; +export { + StatusRow, + type StatusRowAction, + type StatusRowStatus, +} from "./StatusRow.js"; +export { StepHeader } from "./StepHeader.js"; diff --git a/atmn/src/views/react/components/providers/CardWidthContext.tsx b/atmn/src/views/react/components/providers/CardWidthContext.tsx new file mode 100644 index 00000000..104bd94b --- /dev/null +++ b/atmn/src/views/react/components/providers/CardWidthContext.tsx @@ -0,0 +1,79 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useMemo, + type ReactNode, +} from "react"; + +const DEFAULT_MIN_WIDTH = 65; + +interface CardWidthContextValue { + /** The current shared width for all cards */ + width: number; + /** Register a content width - cards call this to report their needed width */ + registerWidth: (id: string, contentWidth: number) => void; + /** Unregister when card unmounts */ + unregisterWidth: (id: string) => void; +} + +const CardWidthContext = createContext(null); + +interface CardWidthProviderProps { + children: ReactNode; + /** Minimum width for cards (default: 65) */ + minWidth?: number; +} + +/** + * Provider that coordinates card widths across all Card components. + * Cards register their content widths, and all cards use the maximum. + */ +export function CardWidthProvider({ + children, + minWidth = DEFAULT_MIN_WIDTH, +}: CardWidthProviderProps) { + const [widthMap, setWidthMap] = useState>(new Map()); + + const registerWidth = useCallback((id: string, contentWidth: number) => { + setWidthMap((prev) => { + const next = new Map(prev); + next.set(id, contentWidth); + return next; + }); + }, []); + + const unregisterWidth = useCallback((id: string) => { + setWidthMap((prev) => { + const next = new Map(prev); + next.delete(id); + return next; + }); + }, []); + + // Calculate the shared width: max of all registered widths, but at least minWidth + const width = useMemo(() => { + const maxContent = Math.max(0, ...Array.from(widthMap.values())); + return Math.max(minWidth, maxContent); + }, [widthMap, minWidth]); + + const value = useMemo( + () => ({ width, registerWidth, unregisterWidth }), + [width, registerWidth, unregisterWidth], + ); + + return ( + + {children} + + ); +} + +/** + * Hook to access the shared card width context. + * Returns null if not inside a CardWidthProvider (cards will use default width). + */ +export function useCardWidth(): CardWidthContextValue | null { + return useContext(CardWidthContext); +} diff --git a/atmn/src/views/react/components/providers/QueryProvider.tsx b/atmn/src/views/react/components/providers/QueryProvider.tsx new file mode 100644 index 00000000..e0773cd0 --- /dev/null +++ b/atmn/src/views/react/components/providers/QueryProvider.tsx @@ -0,0 +1,24 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { ReactNode } from "react"; +import React from "react"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + +interface QueryProviderProps { + children: ReactNode; +} + +export function QueryProvider({ children }: QueryProviderProps) { + return ( + + {children} + + ); +} diff --git a/atmn/src/views/react/components/providers/index.ts b/atmn/src/views/react/components/providers/index.ts new file mode 100644 index 00000000..e105c7df --- /dev/null +++ b/atmn/src/views/react/components/providers/index.ts @@ -0,0 +1 @@ +export { QueryProvider } from "./QueryProvider.js"; diff --git a/atmn/src/views/react/customers/CustomersView.tsx b/atmn/src/views/react/customers/CustomersView.tsx new file mode 100644 index 00000000..042a6f21 --- /dev/null +++ b/atmn/src/views/react/customers/CustomersView.tsx @@ -0,0 +1,364 @@ +import { Box, Text, useApp, useInput } from "ink"; +import open from "open"; +import React, { useCallback, useEffect, useMemo } from "react"; +import { AppEnv } from "../../../lib/env/detect.js"; +import { useClipboard } from "../../../lib/hooks/useClipboard.js"; +import { useCustomerExpanded } from "../../../lib/hooks/useCustomerExpanded.js"; +import { useCustomerNavigation } from "../../../lib/hooks/useCustomerNavigation.js"; +import { useCustomers } from "../../../lib/hooks/useCustomers.js"; +import { + CustomerSheet, + CustomersTable, + EmptyState, + ErrorState, + KeybindHints, + LoadingState, + SearchInput, + TitleBar, +} from "./components/index.js"; +import { calculateColumnWidths, getPaginationDisplay } from "./types.js"; + +const AUTUMN_DASHBOARD_URL = "https://app.useautumn.com"; + +const PAGE_SIZE = 50; + +export interface CustomersViewProps { + environment?: AppEnv; + /** Called when user exits (q or ctrl+c) - use to clear terminal */ + onExit?: () => void; +} + +/** + * Main customers view orchestrator + */ +export function CustomersView({ + environment = AppEnv.Sandbox, + onExit, +}: CustomersViewProps) { + const { exit } = useApp(); + const { copy, showingFeedback } = useClipboard(); + + const { + state, + moveUp, + moveDown, + nextPage, + prevPage, + openSheet, + closeSheet, + toggleFocus, + selectCustomer, + openSearch, + closeSearch, + setSearchQuery, + clearSearch, + } = useCustomerNavigation(); + + const { data, isLoading, isError, error, refetch, isFetching } = useCustomers( + { + page: state.page, + pageSize: PAGE_SIZE, + environment, + search: state.searchQuery, + }, + ); + + // Lazy load expanded customer data when sheet is open + const { + data: expandedCustomer, + isLoading: isLoadingExpanded, + error: expandedError, + } = useCustomerExpanded({ + customerId: state.selectedCustomer?.id ?? null, + environment, + enabled: state.sheetOpen && !!state.selectedCustomer?.id, + }); + + const customers = data?.list ?? []; + const hasMore = data?.has_more ?? false; + const pagination = getPaginationDisplay( + state.page, + customers.length, + PAGE_SIZE, + hasMore, + ); + + // Handle keyboard input + useInput( + useCallback( + (input, key) => { + // Don't handle input when search dialog is open (it handles its own input) + if (state.focusTarget === "search") { + return; + } + + // Quit + if (input === "q") { + if (onExit) { + onExit(); + } else { + exit(); + } + return; + } + + // Refresh + if (input === "r") { + refetch(); + return; + } + + // Open search (/ or s) + if (input === "/" || input === "s") { + openSearch(); + return; + } + + // Clear search (x when search is active) + if (input === "x" && state.searchQuery) { + clearSearch(); + return; + } + + // Sheet-specific controls + if (state.focusTarget === "sheet" && state.sheetOpen) { + // Copy ID + if (input === "c" && state.selectedCustomer) { + copy(state.selectedCustomer.id); + return; + } + + // Open in Autumn dashboard + if (input === "o" && state.selectedCustomer) { + const env = state.selectedCustomer.env === "live" ? "" : "/sandbox"; + open(`${AUTUMN_DASHBOARD_URL}${env}/customers/${state.selectedCustomer.id}`); + return; + } + + // Close sheet + if (key.escape) { + closeSheet(); + return; + } + + // Toggle focus to table + if (key.tab) { + toggleFocus(); + return; + } + + return; + } + + // Table-specific controls + if (state.focusTarget === "table") { + // Navigate up + if (key.upArrow || input === "k") { + moveUp(); + return; + } + + // Navigate down + if (key.downArrow || input === "j") { + moveDown(customers.length - 1); + return; + } + + // Previous page + if (key.leftArrow && pagination.canGoPrev) { + prevPage(); + return; + } + + // Next page + if (key.rightArrow && pagination.canGoNext) { + nextPage(pagination.canGoNext); + return; + } + + // Open sheet + if (key.return && customers[state.selectedIndex]) { + openSheet(customers[state.selectedIndex]); + return; + } + + // Toggle focus to sheet (if open) + if (key.tab && state.sheetOpen) { + toggleFocus(); + return; + } + } + }, + [ + state, + customers, + pagination, + exit, + onExit, + refetch, + copy, + closeSheet, + toggleFocus, + moveUp, + moveDown, + prevPage, + nextPage, + openSheet, + openSearch, + clearSearch, + ], + ), + ); + + // Calculate column widths based on actual customer data and terminal width + const columnWidths = useMemo( + () => + calculateColumnWidths(customers, process.stdout.columns, state.sheetOpen), + [customers, state.sheetOpen], + ); + + // Sync selected customer when customers load + useEffect(() => { + if (customers.length > 0 && state.selectedIndex < customers.length) { + selectCustomer(customers[state.selectedIndex], state.selectedIndex); + } + }, [customers, state.selectedIndex, selectCustomer]); + + // Loading state + if (isLoading && !data) { + return ( + + + + ); + } + + // Error state + if (isError && error) { + return ( + + + + ); + } + + // Empty state + if (!customers.length && !isFetching) { + return ( + + + {state.searchOpen && ( + + + + )} + + + + + + + + ); + } + + // Main view with table and optional sheet + return ( + + {/* Title bar */} + + + {/* Inline search input */} + {state.searchOpen && ( + + + + )} + + {/* Main content: Table + Sheet side by side */} + + {/* Table container - takes remaining space */} + + + {isFetching && ( + + Loading... + + )} + + + {/* Sheet (when open) - fixed width, doesn't shrink */} + {state.sheetOpen && state.selectedCustomer && ( + + { + if (state.selectedCustomer) { + copy(state.selectedCustomer.id); + } + }} + onOpenInBrowser={() => { + if (state.selectedCustomer) { + const env = state.selectedCustomer.env === "live" ? "" : "/sandbox"; + open(`${AUTUMN_DASHBOARD_URL}${env}/customers/${state.selectedCustomer.id}`); + } + }} + expandedCustomer={expandedCustomer} + isLoadingExpanded={isLoadingExpanded} + expandedError={expandedError as Error | null} + /> + + )} + + + {/* Keybind hints */} + + + + + ); +} diff --git a/atmn/src/views/react/customers/components/CustomerRow.tsx b/atmn/src/views/react/customers/components/CustomerRow.tsx new file mode 100644 index 00000000..7a4f7394 --- /dev/null +++ b/atmn/src/views/react/customers/components/CustomerRow.tsx @@ -0,0 +1,82 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { CustomerRowProps, ColumnWidths } from "../types.js"; +import { formatDate, truncate } from "../types.js"; + +/** + * Single customer row in the table + */ +export function CustomerRow({ + customer, + isSelected, + isFocused, + columnWidths, +}: CustomerRowProps) { + const marker = isSelected ? "▸ " : " "; + const markerColor = isSelected && isFocused ? "magenta" : "gray"; + + const { colId, colName, colEmail, colCreated, shouldTruncate } = columnWidths; + + return ( + + {marker} + + + {shouldTruncate ? truncate(customer.id, colId - 1) : (customer.id || "-")} + + + + + {shouldTruncate ? truncate(customer.name, colName - 1) : (customer.name || "-")} + + + + + {shouldTruncate ? truncate(customer.email, colEmail - 1) : (customer.email || "-")} + + + + + {formatDate(customer.created_at)} + + + + ); +} + +export interface CustomerTableHeaderProps { + columnWidths: ColumnWidths; +} + +/** + * Table header row + */ +export function CustomerTableHeader({ columnWidths }: CustomerTableHeaderProps) { + const { colId, colName, colEmail, colCreated } = columnWidths; + + return ( + + {" "} + + + ID + + + + + Name + + + + + Email + + + + + Created + + + + ); +} diff --git a/atmn/src/views/react/customers/components/CustomerSheet.tsx b/atmn/src/views/react/customers/components/CustomerSheet.tsx new file mode 100644 index 00000000..67768cea --- /dev/null +++ b/atmn/src/views/react/customers/components/CustomerSheet.tsx @@ -0,0 +1,201 @@ +import { Box, Text } from "ink"; +import { Spinner } from "@inkjs/ui"; +import React from "react"; +import type { CustomerSheetProps, ApiBalance } from "../types.js"; +import { formatDate } from "../types.js"; +import { + BalancesSection, + EntitiesSection, + SubscriptionsSection, + InvoicesSection, + RewardsSection, + ReferralsSection, +} from "./sections/index.js"; + +/** + * Customer detail sheet (right panel). + * Shows basic info immediately, then lazy-loads expanded data. + */ +export function CustomerSheet({ + customer, + isFocused, + copiedFeedback, + onCopy: _onCopy, + onOpenInBrowser: _onOpenInBrowser, + expandedCustomer, + isLoadingExpanded, + expandedError, +}: CustomerSheetProps) { + const borderColor = isFocused ? "magenta" : "gray"; + + // Use expanded data if available, otherwise fall back to basic + const displayCustomer = expandedCustomer ?? customer; + + // Title: Name > ID > Email + const title = displayCustomer.name || displayCustomer.id || displayCustomer.email || "Unknown"; + + // Get balances (converted to proper type if expanded) + const balances = expandedCustomer?.balances ?? {}; + + return ( + + {/* Customer Title */} + + {title} + + + {/* Basic Customer Info */} + + + ID: + {displayCustomer.id} + + + Name: + {displayCustomer.name ?? "-"} + + + Email: + {displayCustomer.email ?? "-"} + + + Created: + {formatDate(displayCustomer.created_at)} + + + Env: + + {displayCustomer.env} + + + {displayCustomer.stripe_id && ( + + Stripe: + {displayCustomer.stripe_id} + + )} + + + {/* Loading state for expanded data */} + {isLoadingExpanded && ( + + + + )} + + {/* Error state for expanded data */} + {expandedError && ( + + Failed to load details + + )} + + {/* Expanded sections (only show when data is loaded) */} + {expandedCustomer && ( + + {/* Subscriptions - always at top */} + + + {/* Scheduled Subscriptions */} + {expandedCustomer.scheduled_subscriptions.length > 0 && ( + + + + )} + + {/* Feature Balances */} + + } /> + + + {/* Entities */} + {expandedCustomer.entities && expandedCustomer.entities.length > 0 && ( + + + + )} + + {/* Invoices */} + {expandedCustomer.invoices && expandedCustomer.invoices.length > 0 && ( + + + + )} + + {/* Rewards */} + {expandedCustomer.rewards && expandedCustomer.rewards.discounts.length > 0 && ( + + + + )} + + {/* Referrals */} + {expandedCustomer.referrals && expandedCustomer.referrals.length > 0 && ( + + + + )} + + )} + + {/* Basic subscriptions (fallback when expanded not loaded) */} + {!expandedCustomer && !isLoadingExpanded && ( + + + Subscriptions + + {(customer.subscriptions as unknown[]).length > 0 ? ( + (customer.subscriptions as Array<{ plan_id?: string; status?: string }>) + .slice(0, 5) + .map((sub, i) => ( + + - + {sub.plan_id || "Unknown"} + {sub.status && ( + + {" "} + ({sub.status}) + + )} + + )) + ) : ( + No subscriptions + )} + + )} + + {/* Spacer to push actions to bottom */} + + + {/* Actions - pinned to bottom */} + + {copiedFeedback ? ( + Copied! + ) : ( + + [c] + Copy ID + + )} + + [o] + Open in Autumn + + + + ); +} diff --git a/atmn/src/views/react/customers/components/CustomersTable.tsx b/atmn/src/views/react/customers/components/CustomersTable.tsx new file mode 100644 index 00000000..2b1b51e8 --- /dev/null +++ b/atmn/src/views/react/customers/components/CustomersTable.tsx @@ -0,0 +1,60 @@ +import { Box } from "ink"; +import { ScrollList, type ScrollListRef } from "ink-scroll-list"; +import React, { useRef, useEffect } from "react"; +import type { CustomersTableProps, ColumnWidths } from "../types.js"; +import { CustomerRow, CustomerTableHeader } from "./CustomerRow.js"; + +export interface CustomersTableComponentProps extends CustomersTableProps { + columnWidths: ColumnWidths; +} + +/** + * Scrollable customer table using ink-scroll-list + */ +export function CustomersTable({ + customers, + selectedIndex, + onSelect, + isFocused, + columnWidths, +}: CustomersTableComponentProps) { + const listRef = useRef(null); + + // Handle terminal resize + useEffect(() => { + const handleResize = () => { + listRef.current?.remeasure(); + }; + + process.stdout.on("resize", handleResize); + return () => { + process.stdout.off("resize", handleResize); + }; + }, []); + + // Update selected customer when index changes + useEffect(() => { + if (customers[selectedIndex]) { + onSelect(customers[selectedIndex], selectedIndex); + } + }, [selectedIndex, customers, onSelect]); + + return ( + + + + + {customers.map((customer, index) => ( + + ))} + + + + ); +} diff --git a/atmn/src/views/react/customers/components/EmptyState.tsx b/atmn/src/views/react/customers/components/EmptyState.tsx new file mode 100644 index 00000000..e623f00a --- /dev/null +++ b/atmn/src/views/react/customers/components/EmptyState.tsx @@ -0,0 +1,67 @@ +import { Box, Text } from "ink"; +import React from "react"; +import { AppEnv } from "../../../../lib/env/detect.js"; +import type { EmptyStateProps } from "../types.js"; + +/** + * Empty state when no customers exist + */ +export function EmptyState({ environment, searchQuery }: EmptyStateProps) { + const envLabel = environment === AppEnv.Sandbox ? "sandbox" : "live"; + + // Different message when search has no results + if (searchQuery) { + return ( + + 🔍 + + No results for "{searchQuery}" + + + + Try a different search term or press x to clear the search. + + + + ); + } + + return ( + + 📭 + + No customers found + + + + There are no customers in your {envLabel} environment yet. + + + + + Create customers via the API or dashboard to see them here. + + + + ); +} diff --git a/atmn/src/views/react/customers/components/ErrorState.tsx b/atmn/src/views/react/customers/components/ErrorState.tsx new file mode 100644 index 00000000..a5114eb6 --- /dev/null +++ b/atmn/src/views/react/customers/components/ErrorState.tsx @@ -0,0 +1,33 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ErrorStateProps } from "../types.js"; + +/** + * Error state with retry option + */ +export function ErrorState({ error, onRetry: _onRetry }: ErrorStateProps) { + return ( + + + + ✗ Error loading customers + + + + {error.message} + + + + Press r to retry or{" "} + q to quit + + + + ); +} diff --git a/atmn/src/views/react/customers/components/KeybindHints.tsx b/atmn/src/views/react/customers/components/KeybindHints.tsx new file mode 100644 index 00000000..8f549fed --- /dev/null +++ b/atmn/src/views/react/customers/components/KeybindHints.tsx @@ -0,0 +1,92 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { KeybindHintsProps } from "../types.js"; + +/** + * Context-aware keyboard shortcut hints + */ +export function KeybindHints({ + focusTarget, + sheetOpen, + canGoPrev, + canGoNext, +}: KeybindHintsProps) { + if (focusTarget === "sheet" && sheetOpen) { + return ( + + + Tab + focus table + + + Esc + close + + + c + copy ID + + + o + open + + + q + quit + + + ); + } + + // Table focused hints + return ( + + + ↑↓ + navigate + + {canGoPrev && ( + + + prev page + + )} + {canGoNext && ( + + + next page + + )} + + Enter + inspect + + + / + search + + + r + refresh + + + q + quit + + + ); +} diff --git a/atmn/src/views/react/customers/components/LoadingState.tsx b/atmn/src/views/react/customers/components/LoadingState.tsx new file mode 100644 index 00000000..40a44000 --- /dev/null +++ b/atmn/src/views/react/customers/components/LoadingState.tsx @@ -0,0 +1,29 @@ +import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; +import React from "react"; +import { AppEnv } from "../../../../lib/env/detect.js"; +import type { LoadingStateProps } from "../types.js"; + +/** + * Loading state with spinner + */ +export function LoadingState({ environment }: LoadingStateProps) { + const envLabel = environment === AppEnv.Sandbox ? "sandbox" : "live"; + + return ( + + + + + + Loading customers from {envLabel}... + + + ); +} diff --git a/atmn/src/views/react/customers/components/SearchInput.tsx b/atmn/src/views/react/customers/components/SearchInput.tsx new file mode 100644 index 00000000..b1b80ce0 --- /dev/null +++ b/atmn/src/views/react/customers/components/SearchInput.tsx @@ -0,0 +1,51 @@ +import { Box, Text, useInput } from "ink"; +import TextInput from "ink-text-input"; +import React, { useState } from "react"; + +export interface SearchInputProps { + /** Current search value */ + initialValue: string; + /** Called when search is submitted */ + onSubmit: (query: string) => void; + /** Called when search is cancelled */ + onCancel: () => void; +} + +/** + * Inline search input that appears below the title bar + */ +export function SearchInput({ + initialValue, + onSubmit, + onCancel, +}: SearchInputProps) { + const [value, setValue] = useState(initialValue); + + useInput((input, key) => { + if (key.escape) { + onCancel(); + return; + } + if (key.return) { + onSubmit(value); + return; + } + }); + + return ( + + Search: + + (Enter to search, Esc to cancel) + + ); +} diff --git a/atmn/src/views/react/customers/components/TitleBar.tsx b/atmn/src/views/react/customers/components/TitleBar.tsx new file mode 100644 index 00000000..b8b82125 --- /dev/null +++ b/atmn/src/views/react/customers/components/TitleBar.tsx @@ -0,0 +1,35 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { TitleBarProps } from "../types.js"; + +// Get version from package.json (injected at build time or fallback) +const VERSION = "1.0.0-beta.2"; + +/** + * Title bar showing version, command name, pagination info, and search query + */ +export function TitleBar({ environment, pagination, searchQuery }: TitleBarProps) { + return ( + + v{VERSION} + + atmn customers + + {pagination.display} + {searchQuery && ( + <> + + search: + {searchQuery} + (x to clear) + + )} + + ); +} diff --git a/atmn/src/views/react/customers/components/index.ts b/atmn/src/views/react/customers/components/index.ts new file mode 100644 index 00000000..239bcc69 --- /dev/null +++ b/atmn/src/views/react/customers/components/index.ts @@ -0,0 +1,9 @@ +export { CustomerRow, CustomerTableHeader } from "./CustomerRow.js"; +export { CustomerSheet } from "./CustomerSheet.js"; +export { CustomersTable } from "./CustomersTable.js"; +export { EmptyState } from "./EmptyState.js"; +export { ErrorState } from "./ErrorState.js"; +export { KeybindHints } from "./KeybindHints.js"; +export { LoadingState } from "./LoadingState.js"; +export { SearchInput } from "./SearchInput.js"; +export { TitleBar } from "./TitleBar.js"; diff --git a/atmn/src/views/react/customers/components/sections/BalancesSection.tsx b/atmn/src/views/react/customers/components/sections/BalancesSection.tsx new file mode 100644 index 00000000..cce3da7f --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/BalancesSection.tsx @@ -0,0 +1,145 @@ +import { Box, Text } from "ink"; +import { ProgressBar } from "@inkjs/ui"; +import React from "react"; +import type { ApiBalance } from "../../types.js"; + +export interface BalancesSectionProps { + balances: Record; +} + +/** + * Renders customer feature balances. + * - Boolean features: ON/OFF toggle + * - Metered features with unlimited: "Unlimited" badge + * - Metered features with limits: Progress bar showing usage + */ +export function BalancesSection({ balances }: BalancesSectionProps) { + const balanceList = Object.values(balances); + + if (balanceList.length === 0) { + return null; // Don't show section if no balances + } + + // Separate boolean and metered features + const booleanBalances = balanceList.filter( + (b) => b.feature?.type === "boolean" + ); + const meteredBalances = balanceList.filter( + (b) => b.feature?.type !== "boolean" + ); + + return ( + + {/* Boolean Features */} + {booleanBalances.length > 0 && ( + + + Features + + {booleanBalances.map((balance) => ( + + ))} + + )} + + {/* Metered Features */} + {meteredBalances.length > 0 && ( + 0 ? 1 : 0}> + + Usage + + {meteredBalances.map((balance) => ( + + ))} + + )} + + ); +} + +/** + * Boolean feature display (ON/OFF) + */ +function BooleanFeatureRow({ balance }: { balance: ApiBalance }) { + const featureName = balance.feature?.name ?? balance.feature_id; + + return ( + + ON + {featureName} + + ); +} + +/** + * Metered feature display with progress bar + */ +function MeteredFeatureRow({ balance }: { balance: ApiBalance }) { + const featureName = balance.feature?.name ?? balance.feature_id; + const displayName = + balance.feature?.display?.plural ?? + balance.feature?.display?.singular ?? + featureName; + + // Unlimited feature + if (balance.unlimited) { + return ( + + {displayName}: + + Unlimited + + + ); + } + + // Calculate usage percentage + const total = balance.granted_balance + balance.purchased_balance; + const used = balance.usage; + const remaining = balance.current_balance; + + // Handle edge cases + if (total <= 0) { + return ( + + {displayName}: + No allocation + + ); + } + + const percentage = Math.min(100, Math.round((used / total) * 100)); + const isOverage = remaining < 0; + + return ( + + + {displayName}: + + {remaining.toLocaleString()} / {total.toLocaleString()} + + {isOverage && ( + (overage) + )} + + + + + {balance.reset?.resets_at && ( + + Resets: {formatResetDate(balance.reset.resets_at)} + + )} + + ); +} + +/** + * Format reset timestamp + */ +function formatResetDate(timestamp: number): string { + const ms = timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp; + const date = new Date(ms); + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; +} diff --git a/atmn/src/views/react/customers/components/sections/EntitiesSection.tsx b/atmn/src/views/react/customers/components/sections/EntitiesSection.tsx new file mode 100644 index 00000000..0297baca --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/EntitiesSection.tsx @@ -0,0 +1,53 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ApiEntity } from "../../types.js"; + +export interface EntitiesSectionProps { + entities: ApiEntity[]; +} + +/** + * Renders customer entities + */ +export function EntitiesSection({ entities }: EntitiesSectionProps) { + if (entities.length === 0) { + return ( + + + Entities + + No entities + + ); + } + + return ( + + + Entities ({entities.length}) + + + {entities.slice(0, 10).map((entity, index) => ( + + ))} + {entities.length > 10 && ( + ...and {entities.length - 10} more + )} + + + ); +} + +function EntityRow({ entity }: { entity: ApiEntity }) { + const displayName = entity.name ?? entity.id ?? "Unknown"; + + return ( + + - + {displayName} + {entity.feature_id && ( + ({entity.feature_id}) + )} + + ); +} diff --git a/atmn/src/views/react/customers/components/sections/InvoicesSection.tsx b/atmn/src/views/react/customers/components/sections/InvoicesSection.tsx new file mode 100644 index 00000000..eec2bcd3 --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/InvoicesSection.tsx @@ -0,0 +1,92 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ApiInvoice } from "../../types.js"; +import { formatDate } from "../../types.js"; + +export interface InvoicesSectionProps { + invoices: ApiInvoice[]; +} + +/** + * Renders customer invoices + */ +export function InvoicesSection({ invoices }: InvoicesSectionProps) { + if (invoices.length === 0) { + return ( + + + Invoices + + No invoices + + ); + } + + return ( + + + Invoices ({invoices.length}) + + + {invoices.slice(0, 5).map((invoice) => ( + + ))} + {invoices.length > 5 && ( + ...and {invoices.length - 5} more + )} + + + ); +} + +function InvoiceRow({ invoice }: { invoice: ApiInvoice }) { + const statusColor = getInvoiceStatusColor(invoice.status); + const amount = formatCurrency(invoice.total, invoice.currency); + + return ( + + {getStatusIcon(invoice.status)} + {amount} + - {formatDate(invoice.created_at)} + ({invoice.status}) + + ); +} + +function formatCurrency(dollars: number, currency: string): string { + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency.toUpperCase(), + }); + return formatter.format(dollars); +} + +function getInvoiceStatusColor(status: string): string { + switch (status) { + case "paid": + return "green"; + case "open": + case "draft": + return "yellow"; + case "void": + case "uncollectible": + return "gray"; + default: + return "white"; + } +} + +function getStatusIcon(status: string): string { + switch (status) { + case "paid": + return "✓"; + case "open": + case "draft": + return "○"; + case "void": + case "uncollectible": + return "✗"; + default: + return "○"; + } +} diff --git a/atmn/src/views/react/customers/components/sections/ReferralsSection.tsx b/atmn/src/views/react/customers/components/sections/ReferralsSection.tsx new file mode 100644 index 00000000..45b3a569 --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/ReferralsSection.tsx @@ -0,0 +1,61 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ApiReferral } from "../../types.js"; +import { formatDate } from "../../types.js"; + +export interface ReferralsSectionProps { + referrals: ApiReferral[]; +} + +/** + * Renders customer referrals + */ +export function ReferralsSection({ referrals }: ReferralsSectionProps) { + if (referrals.length === 0) { + return ( + + + Referrals + + No referrals + + ); + } + + return ( + + + Referrals ({referrals.length}) + + + {referrals.slice(0, 5).map((referral) => ( + + ))} + {referrals.length > 5 && ( + ...and {referrals.length - 5} more + )} + + + ); +} + +function ReferralRow({ referral }: { referral: ApiReferral }) { + const customerDisplay = + referral.customer.name ?? referral.customer.email ?? referral.customer.id; + + return ( + + + {referral.reward_applied ? "✓ " : "○ "} + + {customerDisplay} + - {formatDate(referral.created_at)} + {!referral.reward_applied && ( + (pending) + )} + + ); +} diff --git a/atmn/src/views/react/customers/components/sections/RewardsSection.tsx b/atmn/src/views/react/customers/components/sections/RewardsSection.tsx new file mode 100644 index 00000000..1166b6df --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/RewardsSection.tsx @@ -0,0 +1,71 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ApiRewards, ApiDiscount } from "../../types.js"; + +export interface RewardsSectionProps { + rewards: ApiRewards | null | undefined; +} + +/** + * Renders customer rewards/discounts + */ +export function RewardsSection({ rewards }: RewardsSectionProps) { + const discounts = rewards?.discounts ?? []; + + if (discounts.length === 0) { + return ( + + + Rewards + + No active rewards + + ); + } + + return ( + + + Rewards ({discounts.length}) + + + {discounts.map((discount) => ( + + ))} + + + ); +} + +function DiscountRow({ discount }: { discount: ApiDiscount }) { + const valueDisplay = formatDiscountValue(discount); + + return ( + + + {discount.name} + - + {valueDisplay} + {discount.duration_type !== "forever" && ( + ({discount.duration_type}) + )} + + ); +} + +function formatDiscountValue(discount: ApiDiscount): string { + switch (discount.type) { + case "percentage_discount": + return `${discount.discount_value}% off`; + case "fixed_discount": { + const amount = discount.discount_value / 100; + return `$${amount.toFixed(2)} off`; + } + case "free_product": + return "Free product"; + case "invoice_credits": + return `$${(discount.discount_value / 100).toFixed(2)} credits`; + default: + return `${discount.discount_value}`; + } +} diff --git a/atmn/src/views/react/customers/components/sections/SubscriptionsSection.tsx b/atmn/src/views/react/customers/components/sections/SubscriptionsSection.tsx new file mode 100644 index 00000000..f58f477c --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/SubscriptionsSection.tsx @@ -0,0 +1,114 @@ +import { Box, Text } from "ink"; +import React from "react"; +import type { ApiSubscription } from "../../types.js"; +import { formatDate } from "../../types.js"; + +export interface SubscriptionsSectionProps { + subscriptions: ApiSubscription[]; + title?: string; +} + +/** + * Renders customer subscriptions with optional plan info + */ +export function SubscriptionsSection({ + subscriptions, + title = "Subscriptions", +}: SubscriptionsSectionProps) { + if (subscriptions.length === 0) { + return ( + + + {title} + + No subscriptions + + ); + } + + return ( + + + {title} ({subscriptions.length}) + + + {subscriptions.map((sub) => ( + + ))} + + + ); +} + +function SubscriptionRow({ subscription }: { subscription: ApiSubscription }) { + const planName = subscription.plan?.name ?? subscription.plan_id; + const statusColor = getStatusColor(subscription.status, subscription.past_due); + + return ( + + + {getStatusIcon(subscription.status)} + {planName} + {subscription.add_on && ( + (add-on) + )} + {subscription.default && ( + (default) + )} + + + + Status: {subscription.status} + {subscription.past_due && (past due)} + + {subscription.trial_ends_at && ( + + Trial ends: {formatDate(subscription.trial_ends_at)} + + )} + {subscription.current_period_end && ( + + Period ends: {formatDate(subscription.current_period_end)} + + )} + {subscription.expires_at && ( + + Expires: {formatDate(subscription.expires_at)} + + )} + {subscription.canceled_at && ( + + Canceled: {formatDate(subscription.canceled_at)} + + )} + + + ); +} + +function getStatusColor(status: string, pastDue: boolean): string { + if (pastDue) return "red"; + switch (status) { + case "active": + return "green"; + case "scheduled": + return "cyan"; + case "expired": + return "gray"; + default: + return "white"; + } +} + +function getStatusIcon(status: string): string { + switch (status) { + case "active": + return "●"; + case "scheduled": + return "○"; + case "expired": + return "○"; + default: + return "○"; + } +} diff --git a/atmn/src/views/react/customers/components/sections/index.ts b/atmn/src/views/react/customers/components/sections/index.ts new file mode 100644 index 00000000..99590d3e --- /dev/null +++ b/atmn/src/views/react/customers/components/sections/index.ts @@ -0,0 +1,6 @@ +export { BalancesSection } from "./BalancesSection.js"; +export { EntitiesSection } from "./EntitiesSection.js"; +export { SubscriptionsSection } from "./SubscriptionsSection.js"; +export { InvoicesSection } from "./InvoicesSection.js"; +export { RewardsSection } from "./RewardsSection.js"; +export { ReferralsSection } from "./ReferralsSection.js"; diff --git a/atmn/src/views/react/customers/types.ts b/atmn/src/views/react/customers/types.ts new file mode 100644 index 00000000..8fe30757 --- /dev/null +++ b/atmn/src/views/react/customers/types.ts @@ -0,0 +1,425 @@ +import type { ApiCustomer } from "../../../lib/api/endpoints/customers.js"; +import type { FocusTarget } from "../../../lib/hooks/useCustomerNavigation.js"; +import type { AppEnv } from "../../../lib/env/detect.js"; + +// ======================================== +// Expanded Customer Types +// ======================================== + +/** + * Feature types + */ +export type FeatureType = "boolean" | "metered" | "credit_system"; + +/** + * Feature display configuration + */ +export interface FeatureDisplay { + singular?: string | null; + plural?: string | null; +} + +/** + * Feature definition (expanded from balance) + */ +export interface ApiFeature { + id: string; + name: string; + type: FeatureType; + consumable: boolean; + event_names?: string[]; + display?: FeatureDisplay; + archived: boolean; +} + +/** + * Balance reset configuration + */ +export interface ApiBalanceReset { + interval: string; + interval_count?: number; + resets_at: number | null; +} + +/** + * Customer balance for a feature + */ +export interface ApiBalance { + feature_id: string; + feature?: ApiFeature; + unlimited: boolean; + granted_balance: number; + purchased_balance: number; + current_balance: number; + usage: number; + overage_allowed: boolean; + max_purchase: number | null; + reset: ApiBalanceReset | null; + plan_id: string | null; +} + +/** + * Plan definition (expanded from subscription) + */ +export interface ApiPlan { + id: string; + name: string; + description: string | null; + group: string | null; + version: number; + add_on: boolean; + default: boolean; + created_at: number; + env: string; + archived: boolean; +} + +/** + * Customer subscription + */ +export interface ApiSubscription { + plan?: ApiPlan; + plan_id: string; + default: boolean; + add_on: boolean; + status: "active" | "scheduled" | "expired"; + past_due: boolean; + canceled_at: number | null; + expires_at: number | null; + trial_ends_at: number | null; + started_at: number; + current_period_start: number | null; + current_period_end: number | null; + quantity: number; +} + +/** + * Customer entity + */ +export interface ApiEntity { + autumn_id?: string; + id: string | null; + name: string | null; + customer_id?: string | null; + feature_id?: string | null; + created_at: number; + env: string; +} + +/** + * Invoice + */ +export interface ApiInvoice { + plan_ids: string[]; + stripe_id: string; + status: string; + total: number; + currency: string; + created_at: number; + hosted_invoice_url?: string | null; +} + +/** + * Referral + */ +export interface ApiReferral { + program_id: string; + customer: { + id: string; + name?: string | null; + email?: string | null; + }; + reward_applied: boolean; + created_at: number; +} + +/** + * Discount/Reward + */ +export interface ApiDiscount { + id: string; + name: string; + type: "percentage_discount" | "fixed_discount" | "free_product" | "invoice_credits"; + discount_value: number; + duration_type: "one_off" | "months" | "forever"; + duration_value?: number | null; + currency?: string | null; + start?: number | null; + end?: number | null; +} + +/** + * Rewards container + */ +export interface ApiRewards { + discounts: ApiDiscount[]; +} + +/** + * Full expanded customer with all optional expand fields + */ +export interface ApiCustomerExpanded extends Omit { + subscriptions: ApiSubscription[]; + scheduled_subscriptions: ApiSubscription[]; + balances: Record; + // Expanded fields + invoices?: ApiInvoice[]; + entities?: ApiEntity[]; + rewards?: ApiRewards | null; + referrals?: ApiReferral[]; +} + +/** + * Column widths for the customer table + */ +export interface ColumnWidths { + colId: number; + colName: number; + colEmail: number; + colCreated: number; + /** Whether truncation is needed based on terminal width */ + shouldTruncate: boolean; +} + +/** + * Pagination display information + */ +export interface PaginationInfo { + /** Current page number (1-indexed) */ + page: number; + /** Display text for the page indicator */ + display: string; + /** Can navigate to previous page */ + canGoPrev: boolean; + /** Can navigate to next page (based on API response) */ + canGoNext: boolean; +} + +/** + * Props for the TitleBar component + */ +export interface TitleBarProps { + environment: AppEnv; + pagination: PaginationInfo; + searchQuery?: string; +} + +/** + * Props for the CustomersTable component + */ +export interface CustomersTableProps { + customers: ApiCustomer[]; + selectedIndex: number; + onSelect: (customer: ApiCustomer, index: number) => void; + isFocused: boolean; +} + +/** + * Props for the CustomerRow component + */ +export interface CustomerRowProps { + customer: ApiCustomer; + isSelected: boolean; + isFocused: boolean; + columnWidths: ColumnWidths; +} + +/** + * Props for the CustomerSheet component + */ +export interface CustomerSheetProps { + customer: ApiCustomer; + isFocused: boolean; + copiedFeedback: boolean; + onCopy: () => void; + onOpenInBrowser: () => void; + /** Expanded customer data (lazily loaded) */ + expandedCustomer?: ApiCustomerExpanded; + /** Whether expanded data is loading */ + isLoadingExpanded?: boolean; + /** Error loading expanded data */ + expandedError?: Error | null; +} + +/** + * Props for the KeybindHints component + */ +export interface KeybindHintsProps { + focusTarget: FocusTarget; + sheetOpen: boolean; + canGoPrev: boolean; + canGoNext: boolean; +} + +/** + * Props for the EmptyState component + */ +export interface EmptyStateProps { + environment: AppEnv; + searchQuery?: string; +} + +/** + * Props for the ErrorState component + */ +export interface ErrorStateProps { + error: Error; + onRetry: () => void; +} + +/** + * Props for the LoadingState component + */ +export interface LoadingStateProps { + environment: AppEnv; +} + +/** + * Utility function to format customer dates + * Handles both Unix timestamps (seconds) and JS timestamps (milliseconds) + */ +export function formatDate(timestamp: number): string { + // If timestamp is less than ~10 billion, it's in seconds (Unix), convert to ms + // Otherwise it's already in milliseconds + const ms = timestamp < 10_000_000_000 ? timestamp * 1000 : timestamp; + const date = new Date(ms); + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; +} + +/** + * Truncate a string to a maximum length with ellipsis + */ +export function truncate(str: string | null, maxLength: number): string { + if (!str) return "-"; + if (str.length <= maxLength) return str; + return `${str.slice(0, maxLength - 3)}...`; +} + +/** + * Calculate pagination display text based on API response + */ +export function getPaginationDisplay( + page: number, + itemCount: number, + pageSize: number, + hasMore: boolean, +): PaginationInfo { + let display: string; + let canGoNext: boolean; + + if (page === 1) { + if (itemCount < pageSize) { + // Certain: no more pages + display = "Page 1 (only)"; + canGoNext = false; + } else { + // Uncertain: might be more + display = "Page 1"; + canGoNext = true; // Allow attempting next page + } + } else { + if (hasMore) { + // Certain: more exists + display = `Page ${page} of many`; + canGoNext = true; + } else { + // Certain: end reached + display = `Page ${page} (last)`; + canGoNext = false; + } + } + + return { + page, + display, + canGoPrev: page > 1, + canGoNext, + }; +} + +// Fixed column width for created date (always the same format: "Jan 1, 2025") +const COL_CREATED = 14; + +// Row overhead: marker (2) + 3 column margins (3) = 5 +// Table overhead: border left (1) + border right (1) + paddingX (2) = 4 +// Sheet when open: minWidth (44) + margin (1) = 45 +const ROW_OVERHEAD = 5; +const TABLE_OVERHEAD = 4; +const SHEET_WIDTH = 45; + +/** + * Calculate column widths based on ACTUAL customer data. + * Shows full content by default, only truncates if total row width exceeds available space. + * + * @param customers - Array of customers to measure + * @param terminalColumns - process.stdout.columns + * @param sheetOpen - whether the detail sheet is open + */ +export function calculateColumnWidths( + customers: ApiCustomer[], + terminalColumns: number, + sheetOpen: boolean = false, +): ColumnWidths { + // Calculate available width for table content + const sheetReserved = sheetOpen ? SHEET_WIDTH : 0; + const availableWidth = terminalColumns - TABLE_OVERHEAD - sheetReserved - ROW_OVERHEAD; + + // Find maximum actual content widths from data + let maxIdLen = 2; // minimum "ID" header + let maxNameLen = 4; // minimum "Name" header + let maxEmailLen = 5; // minimum "Email" header + + for (const customer of customers) { + if (customer.id) maxIdLen = Math.max(maxIdLen, customer.id.length); + if (customer.name) maxNameLen = Math.max(maxNameLen, customer.name.length); + if (customer.email) maxEmailLen = Math.max(maxEmailLen, customer.email.length); + } + + // Total width needed to show all content + const totalContentWidth = maxIdLen + maxNameLen + maxEmailLen + COL_CREATED; + + // If everything fits, use actual content widths (no truncation) + if (totalContentWidth <= availableWidth) { + return { + colId: maxIdLen, + colName: maxNameLen, + colEmail: maxEmailLen, + colCreated: COL_CREATED, + shouldTruncate: false, + }; + } + + // Need to truncate - distribute available space proportionally + // Created column is fixed, distribute rest among id/name/email + const spaceForVariableColumns = availableWidth - COL_CREATED; + const totalVariableContent = maxIdLen + maxNameLen + maxEmailLen; + + // Calculate proportional widths (with minimum of 8 chars each) + const ratio = spaceForVariableColumns / totalVariableContent; + const colId = Math.max(8, Math.floor(maxIdLen * ratio)); + const colName = Math.max(8, Math.floor(maxNameLen * ratio)); + // Email gets remaining space + const colEmail = Math.max(8, spaceForVariableColumns - colId - colName); + + return { + colId, + colName, + colEmail, + colCreated: COL_CREATED, + shouldTruncate: true, + }; +} diff --git a/atmn/src/views/react/init/HeadlessInitFlow.tsx b/atmn/src/views/react/init/HeadlessInitFlow.tsx new file mode 100644 index 00000000..5e7dd560 --- /dev/null +++ b/atmn/src/views/react/init/HeadlessInitFlow.tsx @@ -0,0 +1,257 @@ +import { Box, Text, useApp } from "ink"; +import React, { useEffect, useRef, useState } from "react"; +import { pull } from "../../../commands/pull/pull.js"; +import { AppEnv } from "../../../lib/env/index.js"; +import { + useConfigCounts, + useCreateGuides, + useHeadlessAuth, +} from "../../../lib/hooks/index.js"; +import { writeEmptyConfig } from "../../../lib/writeEmptyConfig.js"; + +type Step = "auth" | "detect" | "sync" | "guides" | "complete" | "error"; + +interface SyncResult { + features: number; + plans: number; + typesPath?: string; +} + +export function HeadlessInitFlow() { + const { exit } = useApp(); + const [step, setStep] = useState("auth"); + const [syncResult, setSyncResult] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + // Hooks + const { authState, orgInfo, error: authError } = useHeadlessAuth(); + const { + data: configCounts, + isLoading: isLoadingConfig, + error: configError, + } = useConfigCounts(); + const { + create: createGuides, + state: guidesState, + filesCreated, + guidesDir, + } = useCreateGuides(); + + // Refs to prevent double-execution + const hasStartedSync = useRef(false); + const hasStartedGuides = useRef(false); + + // Step 1: Auth -> Detect + useEffect(() => { + if (step !== "auth") return; + + if (authState === "authenticated") { + setStep("detect"); + } else if (authState === "error") { + setErrorMessage(authError || "Authentication failed"); + setStep("error"); + } + }, [step, authState, authError]); + + // Step 2: Detect -> Sync + useEffect(() => { + if (step !== "detect") return; + + if (configError) { + setErrorMessage( + configError instanceof Error + ? configError.message + : "Failed to check configuration", + ); + setStep("error"); + return; + } + + if (!isLoadingConfig && configCounts) { + setStep("sync"); + } + }, [step, isLoadingConfig, configCounts, configError]); + + // Step 3: Sync step + useEffect(() => { + if (step !== "sync" || hasStartedSync.current || !configCounts) return; + hasStartedSync.current = true; + + const doSync = async () => { + try { + const hasConfig = + configCounts.plansCount > 0 || configCounts.featuresCount > 0; + + if (hasConfig) { + // Pull existing config + const result = await pull({ + generateSdkTypes: true, + cwd: process.cwd(), + environment: AppEnv.Sandbox, + }); + setSyncResult({ + features: result.features.length, + plans: result.plans.length, + typesPath: result.sdkTypesPath, + }); + } else { + // Write empty config + writeEmptyConfig(); + setSyncResult({ features: 0, plans: 0 }); + } + setStep("guides"); + } catch (err) { + setErrorMessage( + err instanceof Error ? err.message : "Failed to sync configuration", + ); + setStep("error"); + } + }; + + doSync(); + }, [step, configCounts]); + + // Step 4: Guides step + useEffect(() => { + if (step !== "guides" || hasStartedGuides.current) return; + hasStartedGuides.current = true; + + createGuides(true); + }, [step, createGuides]); + + // Step 5: Guides -> Complete + useEffect(() => { + if (step !== "guides") return; + + if (guidesState === "done") { + setStep("complete"); + } else if (guidesState === "error") { + setErrorMessage("Failed to create integration guides"); + setStep("error"); + } + }, [step, guidesState]); + + // Exit on complete or error + useEffect(() => { + if (step !== "complete" && step !== "error") { + return; + } + + const timer = setTimeout(() => { + exit( + step === "error" + ? new Error(errorMessage || "Unknown error") + : undefined, + ); + }, 100); + return () => clearTimeout(timer); + }, [step, exit, errorMessage]); + + return ( + + {/* Step 1: Auth - always show */} + Checking authentication... + {authState === "waiting" && ( + Waiting for authentication... (timeout: 5 minutes) + )} + {authState === "authenticated" && orgInfo && ( + + {"✓"} Logged in as {orgInfo.name} ({orgInfo.slug}) + + )} + + {/* Step 2: Detect - show after auth complete */} + {step !== "auth" && ( + <> + {"\n"}Checking your sandbox... + {isLoadingConfig && Loading...} + {configCounts && ( + + {"✓"}{" "} + {configCounts.plansCount > 0 || configCounts.featuresCount > 0 + ? `Found ${configCounts.plansCount} plans, ${configCounts.featuresCount} features` + : "Sandbox is empty"} + + )} + + )} + + {/* Step 3: Sync - show after detect */} + {(step === "sync" || step === "guides" || step === "complete") && + configCounts && ( + <> + + {"\n"} + {configCounts.plansCount > 0 || configCounts.featuresCount > 0 + ? "Pulling configuration..." + : "Creating empty config..."} + + {syncResult && ( + <> + + {"✓"}{" "} + {configCounts.plansCount > 0 || configCounts.featuresCount > 0 + ? `Pulled ${syncResult.features} features, ${syncResult.plans} plans` + : "Created autumn.config.ts"} + + {syncResult.typesPath && ( + + {"✓"} Generated SDK types at: {syncResult.typesPath} + + )} + + )} + + )} + + {/* Step 4: Guides - show after sync */} + {(step === "guides" || step === "complete") && ( + <> + {"\n"}Creating integration guides... + {guidesState === "done" && ( + <> + + {"✓"} Created {guidesDir}/ + + {filesCreated.map((file, i) => ( + + {" "} + {i === filesCreated.length - 1 ? "└──" : "├──"} {file} + + ))} + + )} + + )} + + {/* Step 5: Complete */} + {step === "complete" && ( + + Setup complete! + {"\n"}Next steps: + + 1. Review the guides in {guidesDir}/ for integration instructions + + + 2. Each guide walks through one part of the Autumn integration + + + 3. Run `atmn push` when ready to deploy changes to your sandbox + + {"\n"} + Documentation: https://docs.useautumn.com + Discord: https://discord.gg/atmn + + )} + + {/* Error state */} + {step === "error" && ( + + + {"✗"} {errorMessage} + + + )} + + ); +} diff --git a/atmn/src/views/react/init/InitFlow.tsx b/atmn/src/views/react/init/InitFlow.tsx new file mode 100644 index 00000000..cfc1e50c --- /dev/null +++ b/atmn/src/views/react/init/InitFlow.tsx @@ -0,0 +1,76 @@ +import { Box, Text } from "ink"; +import React, { useState } from "react"; + +import { AuthStep } from "./steps/AuthStep.js"; +import { ConfigStep } from "./steps/ConfigStep.js"; +import { HandoffStep } from "./steps/HandoffStep.js"; + +const TOTAL_STEPS = 3; + +type Step = "auth" | "config" | "handoff"; + +interface OrgInfo { + name: string; + slug: string; +} + +export function InitFlow() { + const [currentStep, setCurrentStep] = useState("auth"); + const [orgInfo, setOrgInfo] = useState(null); + const [hasPricing, setHasPricing] = useState(false); + + const handleAuthComplete = (info: OrgInfo) => { + setOrgInfo(info); + setCurrentStep("config"); + }; + + const handleConfigComplete = (configHasPricing: boolean) => { + setHasPricing(configHasPricing); + setCurrentStep("handoff"); + }; + + const handleHandoffComplete = () => { + // HandoffStep handles exit via useApp() + }; + + return ( + + {/* Welcome message */} + + + Welcome to{" "} + + Autumn + + ! Let's set up your billing. + + + + {/* Step 1: Authentication */} + + + {/* Step 2: Configuration (only show after auth) */} + {(currentStep === "config" || currentStep === "handoff") && ( + + )} + + {/* Step 3: Handoff */} + {currentStep === "handoff" && ( + + )} + + ); +} diff --git a/atmn/src/views/react/init/steps/AgentStep.tsx b/atmn/src/views/react/init/steps/AgentStep.tsx new file mode 100644 index 00000000..733fde8a --- /dev/null +++ b/atmn/src/views/react/init/steps/AgentStep.tsx @@ -0,0 +1,277 @@ +import { MultiSelect } from "@inkjs/ui"; +import { Box, Text } from "ink"; +import React, { useEffect, useState } from "react"; +import { + useAgentSetup, + type AgentIdentifier, + type FileOption, +} from "../../../../lib/hooks/index.js"; +import { StatusLine, StepHeader } from "../../components/index.js"; + +interface AgentStepProps { + step: number; + totalSteps: number; + onComplete: () => void; +} + +type AgentState = + | "selecting" + | "mcp-agents" + | "installing" + | "creating" + | "complete" + | "error"; + +/** + * Agent setup step - allows user to configure agent files + */ +export function AgentStep({ step, totalSteps, onComplete }: AgentStepProps) { + const [state, setState] = useState("selecting"); + const [selectedOptions, setSelectedOptions] = useState([]); + const [selectedAgents, setSelectedAgents] = useState([]); + const [error, setError] = useState(null); + + const { installMcp, createAgentFiles } = useAgentSetup(); + + const options = [ + { + label: "MCP Server Config (for Claude Code, OpenCode, etc.)", + value: "mcp", + }, + { + label: "CLAUDE.md", + value: "claude-md", + }, + { + label: "AGENTS.md", + value: "agents-md", + }, + { + label: ".cursorrules", + value: "cursor-rules", + }, + ]; + + const agentOptions = [ + { + label: "Claude Code (auto-install via command)", + value: "claude-code", + }, + { + label: "OpenCode, Codex and others (copy URL to clipboard)", + value: "other", + }, + ]; + + const handleSubmit = (values: string[]) => { + setSelectedOptions(values); + + // If MCP is selected, go to agent selection + if (values.includes("mcp")) { + setState("mcp-agents"); + } else { + // Otherwise, create the other files + setState("creating"); + const fileOptions = values.filter( + (v) => v !== "mcp", + ) as FileOption[]; + createAgentFiles.mutate(fileOptions); + } + }; + + const handleAgentSubmit = (agents: string[]) => { + setSelectedAgents(agents); + setState("installing"); + + // Install MCP for selected agents + installMcp.mutate(agents as AgentIdentifier[]); + }; + + // Handle MCP installation completion + useEffect(() => { + if (installMcp.isSuccess && state === "installing") { + // Now create the other selected files + const nonMcpOptions = selectedOptions.filter( + (opt) => opt !== "mcp", + ) as FileOption[]; + if (nonMcpOptions.length > 0) { + setState("creating"); + createAgentFiles.mutate(nonMcpOptions); + } else { + setState("complete"); + setTimeout(() => { + onComplete(); + }, 1000); + } + } + }, [installMcp.isSuccess, state, selectedOptions, onComplete, createAgentFiles]); + + // Handle MCP installation error + useEffect(() => { + if (installMcp.isError) { + setError( + installMcp.error instanceof Error + ? installMcp.error.message + : "Failed to install MCP", + ); + setState("error"); + } + }, [installMcp.isError, installMcp.error]); + + // Handle file creation completion + useEffect(() => { + if (createAgentFiles.isSuccess && state === "creating") { + setState("complete"); + setTimeout(() => { + onComplete(); + }, 1000); + } + }, [createAgentFiles.isSuccess, state, onComplete]); + + // Handle file creation error + useEffect(() => { + if (createAgentFiles.isError) { + setError( + createAgentFiles.error instanceof Error + ? createAgentFiles.error.message + : "Failed to create files", + ); + setState("error"); + } + }, [createAgentFiles.isError, createAgentFiles.error]); + + const handleChange = (values: string[]) => { + setSelectedOptions(values); + }; + + const handleAgentChange = (values: string[]) => { + setSelectedAgents(values); + }; + + if (state === "selecting") { + return ( + + + + Select agent configuration files to create/update: + (Space to select, Enter to confirm) + + + + + + ); + } + + if (state === "mcp-agents") { + return ( + + + + Which agent(s) are you using? + (Space to select, Enter to confirm) + + + + + + ); + } + + if (state === "installing") { + const installMessages: string[] = []; + + if (selectedAgents.includes("claude-code")) { + installMessages.push("Installing MCP for Claude Code..."); + } + + if ( + selectedAgents.includes("opencode") || + selectedAgents.includes("other") + ) { + installMessages.push("Copied MCP URL to clipboard!"); + } + + return ( + + + + {installMessages.map((msg) => ( + + ))} + + + ); + } + + if (state === "creating") { + return ( + + + o !== "mcp").length} file${selectedOptions.filter((o) => o !== "mcp").length !== 1 ? "s" : ""}...`} + /> + + ); + } + + if (state === "complete") { + const createdFiles: string[] = []; + + if (selectedAgents.length > 0) { + createdFiles.push("MCP server config"); + } + if (selectedOptions.includes("claude-md")) { + createdFiles.push("CLAUDE.md"); + } + if (selectedOptions.includes("agents-md")) { + createdFiles.push("AGENTS.md"); + } + if (selectedOptions.includes("cursor-rules")) { + createdFiles.push(".cursorrules"); + } + + return ( + + + 0 + ? `Created ${createdFiles.join(", ")}` + : "Setup complete" + } + /> + + ); + } + + if (state === "error") { + return ( + + + + + ); + } + + return null; +} diff --git a/atmn/src/views/react/init/steps/AuthStep.tsx b/atmn/src/views/react/init/steps/AuthStep.tsx new file mode 100644 index 00000000..fdac736e --- /dev/null +++ b/atmn/src/views/react/init/steps/AuthStep.tsx @@ -0,0 +1,45 @@ +import { Box } from "ink"; +import React from "react"; +import { + useHeadlessAuth, + type OrgInfo, +} from "../../../../lib/hooks/useHeadlessAuth.js"; +import { StatusLine, StepHeader } from "../../components/index.js"; + +interface AuthStepProps { + step: number; + totalSteps: number; + onComplete: (orgInfo: OrgInfo) => void; +} + +export function AuthStep({ step, totalSteps, onComplete }: AuthStepProps) { + const { authState, orgInfo, error } = useHeadlessAuth({ onComplete }); + + return ( + + + {authState === "checking" && ( + + )} + {authState === "not_authenticated" && ( + + )} + {authState === "authenticating" && ( + + )} + {authState === "authenticated" && orgInfo && ( + + )} + {authState === "error" && ( + + )} + + ); +} diff --git a/atmn/src/views/react/init/steps/ConfigStep.tsx b/atmn/src/views/react/init/steps/ConfigStep.tsx new file mode 100644 index 00000000..ed2b45ef --- /dev/null +++ b/atmn/src/views/react/init/steps/ConfigStep.tsx @@ -0,0 +1,290 @@ +import { Box, Text } from "ink"; +import React, { useCallback, useState } from "react"; +import { + useConfigCounts, + useWriteTemplateConfig, +} from "../../../../lib/hooks/index.js"; +import { writeEmptyConfig } from "../../../../lib/writeEmptyConfig.js"; +import { + SelectMenu, + type SelectMenuItem, + StatusLine, + StepHeader, +} from "../../components/index.js"; +import { InlineNukeFlow } from "../../nuke/InlineNukeFlow.js"; +import { PullView } from "../../pull/Pull.js"; +import { TemplateSelector } from "../../template/TemplateSelector.js"; + +type ConfigState = + | "choosing" + | "pulling" + | "nuking" + | "post_nuke_choice" + | "template" + | "complete" + | "error"; + +interface ConfigStepProps { + step: number; + totalSteps: number; + onComplete: (hasPricing: boolean) => void; +} + +export function ConfigStep({ step, totalSteps, onComplete }: ConfigStepProps) { + const [configState, setConfigState] = useState("choosing"); + const [error, setError] = useState(null); + const [completionAction, setCompletionAction] = useState< + "pull" | "nuke" | "template" | "blank" | null + >(null); + + // Fetch configuration counts using TanStack Query + const { + data: configCounts, + isLoading, + error: fetchError, + } = useConfigCounts(); + + // Mutation for writing template config + const writeTemplateConfig = useWriteTemplateConfig(); + + const plansCount = configCounts?.plansCount ?? 0; + const featuresCount = configCounts?.featuresCount ?? 0; + const hasExistingConfig = plansCount > 0 || featuresCount > 0; + + const handlePullExisting = useCallback(() => { + setConfigState("pulling"); + }, []); + + const handleNuke = useCallback(() => { + setConfigState("nuking"); + }, []); + + const handleTemplate = useCallback(() => { + setConfigState("template"); + }, []); + + const handleBlank = useCallback(() => { + try { + writeEmptyConfig(); + setCompletionAction("blank"); + setConfigState("complete"); + setTimeout(() => { + onComplete(false); // No pricing + }, 1000); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to write config"); + setConfigState("error"); + } + }, [onComplete]); + + const handleTemplateSelect = useCallback( + (template: string) => { + writeTemplateConfig.mutate( + { template }, + { + onSuccess: () => { + setCompletionAction("template"); + setConfigState("complete"); + setTimeout(() => { + onComplete(true); // Has pricing from template + }, 1000); + }, + onError: (err) => { + setError( + err instanceof Error ? err.message : "Failed to write config", + ); + setConfigState("error"); + }, + }, + ); + }, + [onComplete, writeTemplateConfig], + ); + + const handleTemplateCancel = useCallback(() => { + // If we came from post-nuke choice, go back there; otherwise go back to main choosing + if (completionAction === "nuke") { + setConfigState("post_nuke_choice"); + } else { + setConfigState("choosing"); + } + }, [completionAction]); + + const handleSelectOption = useCallback( + (item: SelectMenuItem) => { + if (item.value === "pull") { + handlePullExisting(); + } else if (item.value === "nuke") { + handleNuke(); + } else if (item.value === "template") { + handleTemplate(); + } else if (item.value === "blank") { + handleBlank(); + } + }, + [handlePullExisting, handleNuke, handleTemplate, handleBlank], + ); + + const handlePostNukeSelect = useCallback( + (item: SelectMenuItem) => { + if (item.value === "template") { + setCompletionAction("nuke"); // Track that we came from nuke + handleTemplate(); + } else if (item.value === "blank") { + handleBlank(); + } + }, + [handleTemplate, handleBlank], + ); + + // Menu items based on whether config exists + const menuItems: SelectMenuItem[] = hasExistingConfig + ? [ + { + label: `Pull existing (${plansCount} plan${plansCount !== 1 ? "s" : ""}, ${featuresCount} feature${featuresCount !== 1 ? "s" : ""})`, + value: "pull", + }, + { + label: "Nuke and start fresh", + value: "nuke", + }, + ] + : [ + { + label: "Use a template", + value: "template", + }, + { + label: "Start from scratch", + value: "blank", + }, + ]; + + // Post-nuke menu items + const postNukeMenuItems: SelectMenuItem[] = [ + { + label: "Use a template", + value: "template", + }, + { + label: "Start from scratch", + value: "blank", + }, + ]; + + // Handle loading state from query + if (isLoading) { + return ( + + + + + ); + } + + // Handle error state from query + if (fetchError) { + return ( + + + + + ); + } + + return ( + + + {configState === "choosing" && ( + + + {hasExistingConfig + ? "Found existing pricing in your sandbox:" + : "Your sandbox is empty. How do you want to start?"} + + + + + + )} + {configState === "pulling" && ( + { + setCompletionAction("pull"); + setConfigState("complete"); + setTimeout(() => { + onComplete(true); // Has pricing from pull + }, 1000); + }} + /> + )} + {configState === "nuking" && ( + { + setCompletionAction("nuke"); + setConfigState("post_nuke_choice"); + }} + onCancel={() => { + setConfigState("choosing"); + }} + /> + )} + {configState === "post_nuke_choice" && ( + + + + Now, how do you want to set up your pricing? + + + + + + )} + {configState === "template" && ( + { + handleTemplateSelect(template); + }} + onCancel={handleTemplateCancel} + /> + )} + {configState === "complete" && ( + + + + )} + {configState === "error" && ( + + )} + + ); +} diff --git a/atmn/src/views/react/init/steps/HandoffStep.tsx b/atmn/src/views/react/init/steps/HandoffStep.tsx new file mode 100644 index 00000000..eed81c95 --- /dev/null +++ b/atmn/src/views/react/init/steps/HandoffStep.tsx @@ -0,0 +1,153 @@ +import { Select } from "@inkjs/ui"; +import { Box, Text, useApp } from "ink"; +import React, { useState } from "react"; +import { useCreateGuides } from "../../../../lib/hooks/useCreateGuides.js"; +import { StatusLine, StepHeader } from "../../components/index.js"; + +interface HandoffStepProps { + step: number; + totalSteps: number; + hasPricing: boolean; + onComplete: () => void; +} + +type HandoffState = "ai_choice" | "creating" | "complete" | "manual_exit"; + +export function HandoffStep({ + step, + totalSteps, + hasPricing, + onComplete, +}: HandoffStepProps) { + const { exit } = useApp(); + const [state, setState] = useState("ai_choice"); + const { + create, + state: guidesState, + filesCreated, + error, + guidesDir, + } = useCreateGuides(); + + const aiChoiceOptions = [ + { label: "Yes, set me up with AI guides", value: "yes" }, + { label: "No thanks, I'll figure it out", value: "no" }, + ]; + + const handleAiChoice = async (value: string) => { + if (value === "no") { + setState("manual_exit"); + setTimeout(() => { + exit(); + }, 100); + return; + } + + // AI-assisted path + setState("creating"); + await create(hasPricing, { saveAll: true }); + setState("complete"); + setTimeout(() => { + exit(); + }, 100); + }; + + if (state === "ai_choice") { + return ( + + + + Want us to generate guides for your AI coding assistant? + +