diff --git a/CLAUDE.md b/CLAUDE.md index 8b3c1ed..a1865e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,40 +53,15 @@ The `drive download` command takes a file ID, not a URL. Extract the ID from Goo ### Core Structure -``` -src/ -├── cli.ts # Entry point; routes commands to handlers -├── commands/ -│ ├── accounts.ts # Accounts command handler -│ ├── cal.ts # Calendar command dispatcher -│ ├── contacts.ts # Contacts command dispatcher -│ ├── drive.ts # Drive command dispatcher (list, get, search, download, upload, delete, mkdir, move, share, stats) -│ ├── mail.ts # Gmail command dispatcher -│ └── registry.ts # Command registry -├── services/ -│ ├── auth-manager.ts # OAuth2 authentication manager -│ ├── base-service.ts # Base service class -│ ├── calendar-service.ts # Google Calendar API wrapper -│ ├── contacts-service.ts # Google Contacts API wrapper -│ ├── drive-service.ts # Google Drive API wrapper -│ ├── error-handler.ts # Centralized error handling -│ ├── mail-service.ts # Gmail API wrapper -│ └── token-store.ts # Multi-account token persistence (SQLite) -├── utils/ -│ ├── sqlite-wrapper.ts # Bun/Node.js SQLite abstraction layer -│ ├── setup-guide.ts # User onboarding for credentials setup -│ └── format.ts # Date/time formatting utilities -└── types/ - └── google-apis.ts # TypeScript types for Google API responses -``` +- `src/cli.ts` — entry point; routes top-level commands. +- `src/commands/{accounts,cal,contacts,drive,mail,sheets,docs,slides}.ts` — command dispatchers; `registry.ts` is the shared registry. +- `src/services/{auth-manager,base-service,calendar-service,contacts-service,drive-service,error-handler,mail-service,token-store}.ts` — Google API wrappers; `token-store.ts` is the SQLite-backed multi-account store. +- `src/utils/{args,sqlite-wrapper,setup-guide,format,logger,output,...}.ts` — argument parsing, SQLite abstraction (Bun/Node), credentials onboarding, formatting. +- `src/types/google-apis.ts` — Google API response types. ### Data Flow -1. **CLI Entry** (`src/cli.ts`): Routes top-level commands (mail/cal) to handlers -2. **Command Handlers** (`src/commands/*.ts`): Parse arguments and call service methods -3. **Services** (`src/services/*.ts`): Google API wrappers; call `initialize()` first (checks credentials, loads/refreshes tokens) -4. **Token Management** (`src/services/token-store.ts`): Singleton that manages SQLite database at `~/.gwork_tokens.db` with support for multiple accounts per service -5. **Setup Flow** (`src/utils/setup-guide.ts`): Friendly onboarding if credentials missing +`cli.ts` → command handler → service `initialize()` (credentials check, token load/refresh) → Google API call. Tokens persist in `~/.gwork_tokens.db` keyed by `(service, account)`; missing credentials trigger the setup guide. ### Token Management & Authentication @@ -144,38 +119,13 @@ bun run build ### npm Authentication (Non-Interactive) -`npm login` and `pnpm login` are interactive and cannot be automated in a non-interactive Claude Code session. Use the npm registry REST API instead: - -```typescript -// Write to /tmp/set-npm-token.ts, then run: bun /tmp/set-npm-token.ts -const otp = process.argv[2]; -const response = await fetch("https://registry.npmjs.org/-/user/org.couchdb.user:mherod", { - method: "PUT", - headers: { "Content-Type": "application/json", "npm-otp": otp }, - body: JSON.stringify({ name: "mherod", password: "", type: "user" }), -}); -const j = await response.json() as any; -if (j.token) { - const npmrcPath = `${process.env.HOME}/.npmrc`; - const existing = await Bun.file(npmrcPath).text().catch(() => ""); - const authLine = `//registry.npmjs.org/:_authToken=${j.token}`; - const filtered = existing.split("\n").filter(l => !l.includes("registry.npmjs.org/:_authToken")).join("\n"); - await Bun.write(npmrcPath, filtered.trim() + "\n" + authLine + "\n"); -} -``` - -Retrieve credentials and OTP from 1Password: -```bash -op item get npmjs.com --fields Username # username -op item get npmjs.com --fields password --reveal # password -op item get npmjs.com --otp # one-time password -``` +`npm login` / `pnpm login` are interactive. Use the registry REST API: write a temp `bun` script that `PUT`s to `https://registry.npmjs.org/-/user/org.couchdb.user:mherod` with `Content-Type: application/json` and `npm-otp: ` headers and body `{ name, password, type: "user" }`, then append `//registry.npmjs.org/:_authToken=` to `~/.npmrc`. Verify with `pnpm whoami`. -After writing the token, verify with `pnpm whoami` before publishing. +Credentials from 1Password: `op item get npmjs.com --fields Username` / `--fields password --reveal` / `--otp`. -**DON'T** use `bun -e '...'` inline for scripts containing `!` — the shell treats `!` as history expansion and the command fails. Write multi-line scripts to a temp `.ts` file and run with `bun /tmp/script.ts`. +**DON'T** use `bun -e '...'` for scripts containing `!` — `!` triggers shell history expansion. Write to a temp `.ts` file and run with `bun /tmp/script.ts`. -**DON'T** use `python3` or `python` in Bash commands — the system Python version is unreliable across environments and is blocked by a pretooluse hook. Use `bun -e 'console.log(require("./package.json").version)'` for quick one-liners, or write a temp `.ts` file and run with `bun /tmp/script.ts` for multi-line scripts. +**DON'T** use `python` / `python3` — unreliable and blocked by a pretooluse hook. Use `bun -e` for one-liners or temp `.ts` files for multi-line. ### Publishing @@ -226,23 +176,7 @@ After adding an override, run `bun install` to regenerate `bun.lock`, then `bun ### Build Timestamp Injection -The build script injects the current UTC timestamp as a compile-time constant via `bun --define`: - -```bash ---define __BUILD_TIME__=$(date -u +'"%Y-%m-%dT%H:%M:%SZ"') -``` - -In source files, declare it at the top before use: - -```typescript -declare const __BUILD_TIME__: string | undefined; -``` - -Then guard with `typeof` before reading (the constant is undefined in dev/test mode): - -```typescript -const buildTime = typeof __BUILD_TIME__ !== "undefined" ? ` (built ${__BUILD_TIME__})` : ""; -``` +Build injects `__BUILD_TIME__` via `bun --define __BUILD_TIME__=$(date -u +'"%Y-%m-%dT%H:%M:%SZ"')`. In source: `declare const __BUILD_TIME__: string | undefined;` then guard with `typeof __BUILD_TIME__ !== "undefined"` — the constant is undefined in dev/test. ### Type Checking diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 768b4d7..d4c9027 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,14 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - minimatch: '>=9.0.6' - qs: '>=6.14.1' - ajv: '>=6.14.0 <7' - glob: '>=13.0.6' - rimraf: '>=6.1.3' - node-domexception: '>=1.0.0 <2' - importers: .: @@ -55,7 +47,7 @@ importers: version: 7.6.13 '@types/bun': specifier: latest - version: 1.3.9 + version: 1.3.14 '@types/nodemailer': specifier: ^7.0.10 version: 7.0.10 @@ -132,11 +124,19 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - '@types/bun@1.3.9': - resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} + '@types/bun@1.3.14': + resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -226,6 +226,10 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} @@ -234,6 +238,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -241,9 +249,8 @@ packages: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} - balanced-match@4.0.3: - resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} - engines: {node: 20 || >=22} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -261,9 +268,11 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@5.0.2: - resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} - engines: {node: 20 || >=22} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -271,8 +280,8 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bun-types@1.3.9: - resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} + bun-types@1.3.14: + resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -316,6 +325,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -367,9 +379,18 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -482,6 +503,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -527,9 +552,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -630,6 +656,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -666,6 +696,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -706,9 +739,8 @@ packages: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} - engines: {node: 20 || >=22} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -722,9 +754,12 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.2.2: - resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} - engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -821,9 +856,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} @@ -870,9 +905,8 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - rimraf@6.1.3: - resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} - engines: {node: 20 || >=22} + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true rrule@2.8.1: @@ -931,6 +965,14 @@ packages: resolution: {integrity: sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==} engines: {node: '>=18'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@8.2.0: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} @@ -938,6 +980,10 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-ansi@7.1.2: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} @@ -1031,6 +1077,14 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -1059,7 +1113,7 @@ snapshots: dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 10.2.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -1080,7 +1134,7 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 10.2.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -1115,13 +1169,25 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 25.3.0 - '@types/bun@1.3.9': + '@types/bun@1.3.14': dependencies: - bun-types: 1.3.9 + bun-types: 1.3.14 '@types/estree@1.0.8': {} @@ -1202,7 +1268,7 @@ snapshots: '@typescript-eslint/types': 8.56.0 '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - minimatch: 10.2.2 + minimatch: 9.0.9 semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -1241,17 +1307,21 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + argparse@2.0.1: {} arrify@2.0.1: {} - balanced-match@4.0.3: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -1272,9 +1342,14 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - brace-expansion@5.0.2: + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: dependencies: - balanced-match: 4.0.3 + balanced-match: 1.0.2 buffer-equal-constant-time@1.0.1: {} @@ -1283,7 +1358,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bun-types@1.3.9: + bun-types@1.3.14: dependencies: '@types/node': 25.3.0 @@ -1324,6 +1399,8 @@ snapshots: color-name@1.1.4: {} + concat-map@0.0.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1363,10 +1440,16 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -1425,7 +1508,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 10.2.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 transitivePeerDependencies: @@ -1486,6 +1569,11 @@ snapshots: flatted@3.3.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -1510,7 +1598,7 @@ snapshots: extend: 3.0.2 https-proxy-agent: 7.0.6 node-fetch: 3.3.2 - rimraf: 6.1.3 + rimraf: 5.0.10 transitivePeerDependencies: - supports-color @@ -1557,11 +1645,14 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@13.0.6: + glob@10.5.0: dependencies: - minimatch: 10.2.2 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 minipass: 7.1.3 - path-scurry: 2.0.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 globals@14.0.0: {} @@ -1667,6 +1758,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -1693,6 +1786,12 @@ snapshots: isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -1738,7 +1837,7 @@ snapshots: is-unicode-supported: 2.1.0 yoctocolors: 2.1.2 - lru-cache@11.2.6: {} + lru-cache@10.4.3: {} math-intrinsics@1.1.0: {} @@ -1746,9 +1845,13 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.2.2: + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 2.1.0 minimist@1.2.8: {} @@ -1842,9 +1945,9 @@ snapshots: path-key@3.1.1: {} - path-scurry@2.0.2: + path-scurry@1.11.1: dependencies: - lru-cache: 11.2.6 + lru-cache: 10.4.3 minipass: 7.1.3 picomatch@4.0.3: {} @@ -1899,10 +2002,9 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - rimraf@6.1.3: + rimraf@5.0.10: dependencies: - glob: 13.0.6 - package-json-from-dist: 1.0.1 + glob: 10.5.0 rrule@2.8.1: dependencies: @@ -1962,6 +2064,18 @@ snapshots: stdin-discarder@0.3.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string-width@8.2.0: dependencies: get-east-asian-width: 1.5.0 @@ -1971,6 +2085,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-ansi@7.1.2: dependencies: ansi-regex: 6.2.2 @@ -2059,6 +2177,18 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} wsl-utils@0.3.1: diff --git a/src/cli.ts b/src/cli.ts index c783ade..d759bc5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,7 +10,7 @@ import { handleSheetsCommand } from "./commands/sheets.ts"; import { handleDocsCommand } from "./commands/docs.ts"; import { handleSlidesCommand } from "./commands/slides.ts"; import { CommandRegistry } from "./commands/registry.ts"; -import { parseAccount } from "./utils/args.ts"; +import { normalizeArgs, parseAccount } from "./utils/args.ts"; import { logServiceError } from "./utils/command-error-handler.ts"; import { logger } from "./utils/logger.ts"; @@ -524,7 +524,7 @@ async function handleContacts(args: string[]) { } async function main() { - const args = process.argv.slice(2); + const args = normalizeArgs(process.argv.slice(2)); // Extract and parse global flags const verbose = args.includes("--verbose"); diff --git a/src/utils/args.ts b/src/utils/args.ts index 1446795..d5926c0 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -7,16 +7,48 @@ export interface ParsedArgs { args: string[]; } +// Splits `--flag=value` / `-x=value` into `["--flag", "value"]` so every +// downstream parser that only matches the literal `--flag` token still finds +// its value. Tokens not starting with `-` and tokens without `=` are left +// untouched. Used at the CLI entry point as a single normalization pass. +export function normalizeArgs(args: string[]): string[] { + const out: string[] = []; + let sawDoubleDash = false; + for (const arg of args) { + if (sawDoubleDash) { + out.push(arg); + continue; + } + if (arg === "--") { + sawDoubleDash = true; + out.push(arg); + continue; + } + if (arg.startsWith("-") && arg.length > 1) { + const eq = arg.indexOf("="); + if (eq > 0) { + out.push(arg.slice(0, eq), arg.slice(eq + 1)); + continue; + } + } + out.push(arg); + } + return out; +} + export function parseAccount(args: string[]): ParsedArgs { let account = "default"; const filteredArgs: string[] = []; for (let i = 0; i < args.length; i++) { - if (args[i] === "--account" && i + 1 < args.length) { + const arg = args[i]!; + if (arg === "--account" && i + 1 < args.length) { account = args[i + 1]!; i++; // Skip next arg since we consumed it + } else if (arg.startsWith("--account=")) { + account = arg.slice("--account=".length); } else { - filteredArgs.push(args[i]!); + filteredArgs.push(arg); } } diff --git a/src/utils/sqlite-wrapper.ts b/src/utils/sqlite-wrapper.ts index 51af574..73730a8 100644 --- a/src/utils/sqlite-wrapper.ts +++ b/src/utils/sqlite-wrapper.ts @@ -234,17 +234,17 @@ export class Database { private wrapBetterSqlite3Statement(stmt: any): Statement { return { run: (params?: Record) => { - const result = stmt.run(params); + const result = params === undefined ? stmt.run() : stmt.run(params); return { changes: result.changes, lastInsertRowid: result.lastInsertRowid, }; }, get: (params?: Record) => { - return stmt.get(params); + return params === undefined ? stmt.get() : stmt.get(params); }, all: (params?: Record) => { - return stmt.all(params); + return params === undefined ? stmt.all() : stmt.all(params); }, finalize: () => { // better-sqlite3 doesn't require explicit finalization diff --git a/tests/unit/utils/args.test.ts b/tests/unit/utils/args.test.ts index 94f5257..6999e10 100644 --- a/tests/unit/utils/args.test.ts +++ b/tests/unit/utils/args.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { parseAccount } from "../../../src/utils/args.ts"; +import { normalizeArgs, parseAccount } from "../../../src/utils/args.ts"; describe("parseAccount", () => { test("extracts account flag from beginning of args", () => { @@ -90,6 +90,40 @@ describe("parseAccount", () => { }); }); + describe("equals-form syntax", () => { + test("extracts --account=email form", () => { + const result = parseAccount(["--account=matthew.herod@gmail.com", "list"]); + expect(result.account).toBe("matthew.herod@gmail.com"); + expect(result.args).toEqual(["list"]); + }); + + test("extracts --account=value from middle of args", () => { + const result = parseAccount([ + "search", + "rail", + "--account=user@gmail.com", + ]); + expect(result.account).toBe("user@gmail.com"); + expect(result.args).toEqual(["search", "rail"]); + }); + + test("treats empty value (--account=) as empty string", () => { + const result = parseAccount(["--account=", "list"]); + expect(result.account).toBe(""); + expect(result.args).toEqual(["list"]); + }); + + test("last form wins when both space- and equals-forms appear", () => { + const result = parseAccount([ + "--account", + "first@example.com", + "--account=second@example.com", + ]); + expect(result.account).toBe("second@example.com"); + expect(result.args).toEqual([]); + }); + }); + describe("edge cases", () => { test("ignores --account without value (no extraction)", () => { const result = parseAccount(["list", "--account"]); @@ -134,6 +168,61 @@ describe("parseAccount", () => { }); }); + describe("normalizeArgs", () => { + test("splits --flag=value into two tokens", () => { + expect(normalizeArgs(["--account=user@gmail.com"])).toEqual([ + "--account", + "user@gmail.com", + ]); + }); + + test("splits short -x=value", () => { + expect(normalizeArgs(["-n=5"])).toEqual(["-n", "5"]); + }); + + test("preserves space-form --flag value", () => { + expect(normalizeArgs(["--account", "user@gmail.com"])).toEqual([ + "--account", + "user@gmail.com", + ]); + }); + + test("preserves positional args containing '='", () => { + expect(normalizeArgs(["search", "foo=bar"])).toEqual([ + "search", + "foo=bar", + ]); + }); + + test("leaves --flag (no value) untouched", () => { + expect(normalizeArgs(["--today"])).toEqual(["--today"]); + }); + + test("preserves -- separator", () => { + expect(normalizeArgs(["--", "--account=x"])).toEqual([ + "--", + "--account=x", + ]); + }); + + test("yields empty value for --flag=", () => { + expect(normalizeArgs(["--account="])).toEqual(["--account", ""]); + }); + + test("splits at first '=' only (value may contain '=')", () => { + expect(normalizeArgs(["--query=key=value"])).toEqual([ + "--query", + "key=value", + ]); + }); + + test("after normalize, literal --flag matcher reads value", () => { + const args = normalizeArgs(["--account=matthew.herod@gmail.com", "list"]); + const result = parseAccount(args); + expect(result.account).toBe("matthew.herod@gmail.com"); + }); + }); + describe("return object structure", () => { test("always returns object with 'account' and 'args' properties", () => { const result = parseAccount(["test"]);