@purveyors/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +53 -0
- package/README.md +175 -0
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +193 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/catalog.d.ts +63 -0
- package/dist/commands/catalog.d.ts.map +1 -0
- package/dist/commands/catalog.js +132 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/inventory.d.ts +32 -0
- package/dist/commands/inventory.d.ts.map +1 -0
- package/dist/commands/inventory.js +287 -0
- package/dist/commands/inventory.js.map +1 -0
- package/dist/commands/roast.d.ts +48 -0
- package/dist/commands/roast.d.ts.map +1 -0
- package/dist/commands/roast.js +225 -0
- package/dist/commands/roast.js.map +1 -0
- package/dist/commands/sales.d.ts +17 -0
- package/dist/commands/sales.d.ts.map +1 -0
- package/dist/commands/sales.js +226 -0
- package/dist/commands/sales.js.map +1 -0
- package/dist/commands/tasting.d.ts +46 -0
- package/dist/commands/tasting.d.ts.map +1 -0
- package/dist/commands/tasting.js +174 -0
- package/dist/commands/tasting.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +21 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +45 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +20 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +58 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/output.d.ts +22 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +86 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/prompts.d.ts +11 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +22 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/supabase.d.ts +22 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +87 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/types/database.types.d.ts +13 -0
- package/dist/types/database.types.d.ts.map +1 -0
- package/dist/types/database.types.js +8 -0
- package/dist/types/database.types.js.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +58 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Sustainable Use License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reed Whetstone / purveyors.io
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
By using this software, you agree to all of the terms and conditions below.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
|
|
12
|
+
|
|
13
|
+
## Limitations
|
|
14
|
+
|
|
15
|
+
**You may not** provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
|
|
16
|
+
|
|
17
|
+
**You may not** move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
|
|
18
|
+
|
|
19
|
+
**You may not** alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor's trademarks is subject to applicable law.
|
|
20
|
+
|
|
21
|
+
## Patents
|
|
22
|
+
|
|
23
|
+
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately.
|
|
24
|
+
|
|
25
|
+
## Notices
|
|
26
|
+
|
|
27
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
|
|
28
|
+
|
|
29
|
+
If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
|
|
30
|
+
|
|
31
|
+
## No Other Rights
|
|
32
|
+
|
|
33
|
+
These terms do not imply any licenses other than those expressly granted in these terms.
|
|
34
|
+
|
|
35
|
+
## Termination
|
|
36
|
+
|
|
37
|
+
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
|
|
38
|
+
|
|
39
|
+
## No Liability
|
|
40
|
+
|
|
41
|
+
_As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim._
|
|
42
|
+
|
|
43
|
+
## Definitions
|
|
44
|
+
|
|
45
|
+
The _licensor_ is the entity offering these terms, and the _software_ is the software the licensor makes available under these terms, including any portion of it.
|
|
46
|
+
|
|
47
|
+
_You_ refers both to you as an individual and to any legal entity you represent when using this software.
|
|
48
|
+
|
|
49
|
+
_Your company_ is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. _Control_ means ownership of substantially all the assets of an entity, or the power to direct its management and legal commitments.
|
|
50
|
+
|
|
51
|
+
_Your licenses_ are all the licenses granted to you for the software under these terms.
|
|
52
|
+
|
|
53
|
+
_Use_ means anything you do with the software requiring one of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# prvrs — The Purveyors CLI
|
|
2
|
+
|
|
3
|
+
> Coffee intelligence from your terminal.
|
|
4
|
+
|
|
5
|
+
`prvrs` is the official command-line interface for [purveyors.io](https://purveyors.io). It gives coffee professionals direct terminal access to the Purveyors platform: search green coffee availability, track pricing tiers, monitor inventory, and pipe data into spreadsheets, scripts, or dashboards.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @purveyors/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires **Node.js >= 20**.
|
|
16
|
+
|
|
17
|
+
Verify:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
prvrs --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Authenticate
|
|
29
|
+
prvrs auth login
|
|
30
|
+
|
|
31
|
+
# 2. Confirm your session
|
|
32
|
+
prvrs auth status
|
|
33
|
+
|
|
34
|
+
# 3. Explore commands
|
|
35
|
+
prvrs --help
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
`prvrs` authenticates via Google OAuth, using the same account as your purveyors.io web session.
|
|
43
|
+
|
|
44
|
+
### Login
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
prvrs auth login
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Opens your browser. Complete Google sign-in, then return to the terminal. Credentials are stored at `~/.config/prvrs/credentials.json` (owner-readable only, mode 0600).
|
|
51
|
+
|
|
52
|
+
### Status
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
prvrs auth status
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
✔ Logged in as you@example.com
|
|
60
|
+
ℹ Role: authenticated
|
|
61
|
+
ℹ Token expires: 2026-03-16T08:00:00.000Z
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Logout
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
prvrs auth logout
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Clears stored credentials from disk.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
### `prvrs auth`
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
| ------------------- | --------------------------------------- |
|
|
80
|
+
| `prvrs auth login` | Log in via Google OAuth (opens browser) |
|
|
81
|
+
| `prvrs auth status` | Show current authentication state |
|
|
82
|
+
| `prvrs auth logout` | Clear stored credentials |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Output Formats
|
|
87
|
+
|
|
88
|
+
All `prvrs` commands default to **compact JSON** — one line, no colors, machine-readable. This makes `prvrs` pipeable into `jq`, `csvkit`, or any script.
|
|
89
|
+
|
|
90
|
+
### Default (compact JSON)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
prvrs auth status
|
|
94
|
+
# → {"authenticated":true,"email":"you@example.com","role":"authenticated","tokenExpires":"2026-03-16T08:00:00.000Z"}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Pretty JSON (`--pretty`)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
prvrs auth status --pretty
|
|
101
|
+
# → indented, colorized JSON
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### CSV (`--csv`)
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
prvrs coffee list --csv > coffees.csv
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Piping with jq
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
prvrs auth status | jq .email
|
|
114
|
+
# → "you@example.com"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
User feedback messages (spinners, success/error) go to **stderr**, so they never pollute your stdout pipe.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Environment Variables
|
|
122
|
+
|
|
123
|
+
| Variable | Description |
|
|
124
|
+
| ----------------------------- | ---------------------------------------------------------- |
|
|
125
|
+
| `PURVEYORS_SUPABASE_URL` | Override the Supabase project URL (useful for dev/staging) |
|
|
126
|
+
| `PURVEYORS_SUPABASE_ANON_KEY` | Override the Supabase anon key |
|
|
127
|
+
| `PRVRS_DEBUG` | Set to any value to enable verbose error output |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/reedwhetstone/purveyors-cli
|
|
135
|
+
cd purveyors-cli
|
|
136
|
+
pnpm install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Create a `.env` file:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
PURVEYORS_SUPABASE_URL=https://your-project.supabase.co
|
|
143
|
+
PURVEYORS_SUPABASE_ANON_KEY=your-anon-key
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run locally:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pnpm dev -- auth status
|
|
150
|
+
pnpm dev -- --help
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Build:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pnpm build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Lint + type check + test:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pnpm lint
|
|
163
|
+
pnpm check
|
|
164
|
+
pnpm test
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
See [AGENTS.md](./AGENTS.md) for the full contributor guide including architecture, code conventions, and PR requirements.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
Sustainable Use License. See [LICENSE.md](./LICENSE.md).
|
|
174
|
+
|
|
175
|
+
Copyright © 2026 Reed Whetstone / purveyors.io
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+NpC;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAe1C"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { createAnonClient, validateSession } from '../lib/supabase.js';
|
|
4
|
+
import { writeCredentials, deleteCredentials } from '../lib/config.js';
|
|
5
|
+
import { outputData, success, info, warn } from '../lib/output.js';
|
|
6
|
+
import { withErrorHandling, AuthError } from '../lib/errors.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
const DEFAULT_CALLBACK_HOST = 'localhost';
|
|
10
|
+
const CALLBACK_PATH = '/auth/callback';
|
|
11
|
+
/**
|
|
12
|
+
* Start a one-shot local HTTP server to receive the Supabase OAuth callback.
|
|
13
|
+
*
|
|
14
|
+
* Supabase delivers tokens in the URL fragment (#access_token=...) which is
|
|
15
|
+
* client-side only. We serve a minimal HTML page that extracts the fragment
|
|
16
|
+
* values and POSTs them back to the local server, then resolves the promise.
|
|
17
|
+
*/
|
|
18
|
+
function startCallbackServer() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let tokenResolve;
|
|
21
|
+
let tokenReject;
|
|
22
|
+
const tokenPromise = new Promise((res, rej) => {
|
|
23
|
+
tokenResolve = res;
|
|
24
|
+
tokenReject = rej;
|
|
25
|
+
});
|
|
26
|
+
const server = createServer((req, res) => {
|
|
27
|
+
const url = new URL(req.url ?? '/', `http://${DEFAULT_CALLBACK_HOST}`);
|
|
28
|
+
if (url.pathname === CALLBACK_PATH) {
|
|
29
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
30
|
+
res.end(`<!DOCTYPE html>
|
|
31
|
+
<html>
|
|
32
|
+
<head><title>Authenticating...</title></head>
|
|
33
|
+
<body>
|
|
34
|
+
<p>Completing authentication, please wait...</p>
|
|
35
|
+
<script>
|
|
36
|
+
const hash = window.location.hash.substring(1);
|
|
37
|
+
const params = new URLSearchParams(hash);
|
|
38
|
+
fetch('/auth/token', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
access_token: params.get('access_token'),
|
|
43
|
+
refresh_token: params.get('refresh_token'),
|
|
44
|
+
expires_in: params.get('expires_in'),
|
|
45
|
+
})
|
|
46
|
+
}).then(() => {
|
|
47
|
+
document.body.innerHTML =
|
|
48
|
+
'<h2 style="font-family:sans-serif;color:#2d6a4f;">✔ Authenticated! You can close this tab.</h2>';
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (url.pathname === '/auth/token' && req.method === 'POST') {
|
|
56
|
+
let body = '';
|
|
57
|
+
req.on('data', (chunk) => (body += chunk));
|
|
58
|
+
req.on('end', () => {
|
|
59
|
+
res.writeHead(200);
|
|
60
|
+
res.end('ok');
|
|
61
|
+
server.close();
|
|
62
|
+
try {
|
|
63
|
+
const { access_token, refresh_token, expires_in } = JSON.parse(body);
|
|
64
|
+
if (!access_token || !refresh_token) {
|
|
65
|
+
tokenReject(new AuthError('OAuth callback did not include tokens. Try again.'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
tokenResolve({
|
|
69
|
+
accessToken: access_token,
|
|
70
|
+
refreshToken: refresh_token,
|
|
71
|
+
expiresIn: parseInt(expires_in ?? '3600', 10),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
tokenReject(new AuthError('Failed to parse OAuth callback response.'));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
res.writeHead(404);
|
|
81
|
+
res.end();
|
|
82
|
+
});
|
|
83
|
+
server.listen(0, DEFAULT_CALLBACK_HOST, () => {
|
|
84
|
+
const addr = server.address();
|
|
85
|
+
if (!addr || typeof addr === 'string') {
|
|
86
|
+
reject(new AuthError('Failed to start callback server.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
resolve({ port: addr.port, tokenPromise });
|
|
90
|
+
});
|
|
91
|
+
server.on('error', (err) => reject(err));
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `prvrs auth login`
|
|
96
|
+
* Opens a browser for Google OAuth via Supabase, captures the callback token.
|
|
97
|
+
*/
|
|
98
|
+
const loginAction = withErrorHandling(async () => {
|
|
99
|
+
const { port, tokenPromise } = await startCallbackServer();
|
|
100
|
+
const redirectTo = `http://${DEFAULT_CALLBACK_HOST}:${port}${CALLBACK_PATH}`;
|
|
101
|
+
const supabase = createAnonClient();
|
|
102
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
103
|
+
provider: 'google',
|
|
104
|
+
options: { redirectTo },
|
|
105
|
+
});
|
|
106
|
+
if (error || !data.url) {
|
|
107
|
+
throw new AuthError('Failed to generate OAuth URL.', error);
|
|
108
|
+
}
|
|
109
|
+
console.log(chalk.bold('\n Opening browser for authentication...'));
|
|
110
|
+
console.log(chalk.dim(` If the browser does not open, visit:\n ${data.url}\n`));
|
|
111
|
+
// Open browser cross-platform
|
|
112
|
+
const { spawn } = await import('child_process');
|
|
113
|
+
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
114
|
+
spawn(openCmd, [data.url], { detached: true, stdio: 'ignore' }).unref();
|
|
115
|
+
const spinner = ora('Waiting for authentication...').start();
|
|
116
|
+
const { accessToken, refreshToken, expiresIn } = await tokenPromise;
|
|
117
|
+
spinner.succeed('Authentication received');
|
|
118
|
+
// Fetch user info to confirm identity
|
|
119
|
+
const client = createAnonClient();
|
|
120
|
+
await client.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });
|
|
121
|
+
const { data: { user }, } = await client.auth.getUser();
|
|
122
|
+
const creds = {
|
|
123
|
+
accessToken,
|
|
124
|
+
refreshToken,
|
|
125
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
126
|
+
user: {
|
|
127
|
+
id: user?.id ?? 'unknown',
|
|
128
|
+
email: user?.email ?? 'unknown',
|
|
129
|
+
role: user?.role,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
await writeCredentials(creds);
|
|
133
|
+
success(`Logged in as ${chalk.bold(creds.user.email)}`);
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* `prvrs auth status`
|
|
137
|
+
* Shows current login state and token info.
|
|
138
|
+
*/
|
|
139
|
+
const statusAction = withErrorHandling(async (_, cmd) => {
|
|
140
|
+
const opts = cmd.optsWithGlobals();
|
|
141
|
+
const spinner = ora('Checking authentication status...').start();
|
|
142
|
+
const session = await validateSession();
|
|
143
|
+
spinner.stop();
|
|
144
|
+
if (!session) {
|
|
145
|
+
const result = {
|
|
146
|
+
authenticated: false,
|
|
147
|
+
message: 'Not logged in. Run `prvrs auth login` to authenticate.',
|
|
148
|
+
};
|
|
149
|
+
if (!opts.pretty && !opts.csv) {
|
|
150
|
+
warn(result.message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
outputData(result, opts);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const result = {
|
|
157
|
+
authenticated: true,
|
|
158
|
+
email: session.email,
|
|
159
|
+
role: session.role ?? 'authenticated',
|
|
160
|
+
tokenExpires: new Date(session.expiresAt).toISOString(),
|
|
161
|
+
};
|
|
162
|
+
if (!opts.pretty && !opts.csv) {
|
|
163
|
+
success(`Logged in as ${chalk.bold(session.email)}`);
|
|
164
|
+
info(`Role: ${result.role}`);
|
|
165
|
+
info(`Token expires: ${result.tokenExpires}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
outputData(result, opts);
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* `prvrs auth logout`
|
|
172
|
+
* Clears stored credentials from disk.
|
|
173
|
+
*/
|
|
174
|
+
const logoutAction = withErrorHandling(async () => {
|
|
175
|
+
await deleteCredentials();
|
|
176
|
+
success('Logged out successfully.');
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* Build and return the `auth` command subtree.
|
|
180
|
+
*/
|
|
181
|
+
export function buildAuthCommand() {
|
|
182
|
+
const auth = new Command('auth').description('Manage authentication with purveyors.io');
|
|
183
|
+
auth.command('login').description('Log in via Google OAuth (opens browser)').action(loginAction);
|
|
184
|
+
auth
|
|
185
|
+
.command('status')
|
|
186
|
+
.description('Show current authentication status')
|
|
187
|
+
.option('--pretty', 'Pretty-print JSON output')
|
|
188
|
+
.option('--csv', 'Output as CSV')
|
|
189
|
+
.action(statusAction);
|
|
190
|
+
auth.command('logout').description('Clear stored credentials').action(logoutAction);
|
|
191
|
+
return auth;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Strip PostgREST special characters from user-supplied filter values.
|
|
4
|
+
* Prevents injection into .or() filter strings where values are interpolated directly.
|
|
5
|
+
* Removes: ( ) , . * % that have meaning in PostgREST filter syntax.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeFilterValue(value: string): string;
|
|
8
|
+
export interface CatalogItem {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string | null;
|
|
11
|
+
source: string | null;
|
|
12
|
+
continent: string | null;
|
|
13
|
+
country: string | null;
|
|
14
|
+
region: string | null;
|
|
15
|
+
processing: string | null;
|
|
16
|
+
drying_method: string | null;
|
|
17
|
+
cultivar_detail: string | null;
|
|
18
|
+
grade: string | null;
|
|
19
|
+
appearance: string | null;
|
|
20
|
+
type: string | null;
|
|
21
|
+
description_short: string | null;
|
|
22
|
+
description_long: string | null;
|
|
23
|
+
farm_notes: string | null;
|
|
24
|
+
cupping_notes: string | null;
|
|
25
|
+
ai_description: string | null;
|
|
26
|
+
roast_recs: string | null;
|
|
27
|
+
cost_lb: number | null;
|
|
28
|
+
lot_size: string | null;
|
|
29
|
+
bag_size: string | null;
|
|
30
|
+
score_value: number | null;
|
|
31
|
+
stocked: boolean | null;
|
|
32
|
+
stocked_date: string | null;
|
|
33
|
+
unstocked_date: string | null;
|
|
34
|
+
arrival_date: string | null;
|
|
35
|
+
last_updated: string | null;
|
|
36
|
+
public_coffee: boolean | null;
|
|
37
|
+
wholesale: boolean | null;
|
|
38
|
+
price_tiers: Array<{
|
|
39
|
+
min_lbs: number;
|
|
40
|
+
price: number;
|
|
41
|
+
}> | null;
|
|
42
|
+
}
|
|
43
|
+
export interface CatalogStats {
|
|
44
|
+
total: number;
|
|
45
|
+
stocked: number;
|
|
46
|
+
byOrigin: Record<string, number>;
|
|
47
|
+
avgPricePerLb: number | null;
|
|
48
|
+
priceRange: {
|
|
49
|
+
min: number | null;
|
|
50
|
+
max: number | null;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Aggregate stats from an array of catalog items.
|
|
55
|
+
* Pure function — no I/O, safe to unit test.
|
|
56
|
+
*/
|
|
57
|
+
export declare function computeCatalogStats(items: CatalogItem[]): CatalogStats;
|
|
58
|
+
/**
|
|
59
|
+
* `prvrs catalog` — Browse the public coffee catalog.
|
|
60
|
+
* The coffee_catalog table is publicly readable; no auth required.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildCatalogCommand(): Command;
|
|
63
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/commands/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAID,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACxD;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,YAAY,CAoBtE;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAgH7C"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createAnonClient } from '../lib/supabase.js';
|
|
3
|
+
import { outputData, info } from '../lib/output.js';
|
|
4
|
+
import { withErrorHandling } from '../lib/errors.js';
|
|
5
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Strip PostgREST special characters from user-supplied filter values.
|
|
8
|
+
* Prevents injection into .or() filter strings where values are interpolated directly.
|
|
9
|
+
* Removes: ( ) , . * % that have meaning in PostgREST filter syntax.
|
|
10
|
+
*/
|
|
11
|
+
export function sanitizeFilterValue(value) {
|
|
12
|
+
return value.replace(/[(),.*]/g, '');
|
|
13
|
+
}
|
|
14
|
+
// ─── Pure logic (exported for unit testing) ───────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Aggregate stats from an array of catalog items.
|
|
17
|
+
* Pure function — no I/O, safe to unit test.
|
|
18
|
+
*/
|
|
19
|
+
export function computeCatalogStats(items) {
|
|
20
|
+
const stocked = items.filter((i) => i.stocked === true).length;
|
|
21
|
+
const byOrigin = {};
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const key = item.country ?? item.continent ?? 'Unknown';
|
|
24
|
+
byOrigin[key] = (byOrigin[key] ?? 0) + 1;
|
|
25
|
+
}
|
|
26
|
+
const prices = items.map((i) => i.cost_lb).filter((p) => p !== null);
|
|
27
|
+
const avgPricePerLb = prices.length > 0
|
|
28
|
+
? Math.round((prices.reduce((a, b) => a + b, 0) / prices.length) * 100) / 100
|
|
29
|
+
: null;
|
|
30
|
+
const priceRange = {
|
|
31
|
+
min: prices.length > 0 ? Math.min(...prices) : null,
|
|
32
|
+
max: prices.length > 0 ? Math.max(...prices) : null,
|
|
33
|
+
};
|
|
34
|
+
return { total: items.length, stocked, byOrigin, avgPricePerLb, priceRange };
|
|
35
|
+
}
|
|
36
|
+
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* `prvrs catalog` — Browse the public coffee catalog.
|
|
39
|
+
* The coffee_catalog table is publicly readable; no auth required.
|
|
40
|
+
*/
|
|
41
|
+
export function buildCatalogCommand() {
|
|
42
|
+
const catalog = new Command('catalog').description('Browse the public coffee catalog');
|
|
43
|
+
// ── catalog search ────────────────────────────────────────────────────────
|
|
44
|
+
catalog
|
|
45
|
+
.command('search')
|
|
46
|
+
.description('Search coffees by origin, process, price, or flavor')
|
|
47
|
+
.option('--origin <origin>', 'Filter by origin (country, continent, or region)')
|
|
48
|
+
.option('--process <method>', 'Filter by processing method (e.g. natural, washed)')
|
|
49
|
+
.option('--price-min <n>', 'Minimum price per lb (USD)')
|
|
50
|
+
.option('--price-max <n>', 'Maximum price per lb (USD)')
|
|
51
|
+
.option('--flavor <keywords>', 'Flavor keywords, comma-separated (e.g. "berry,chocolate")')
|
|
52
|
+
.option('--stocked', 'Only show currently stocked coffees')
|
|
53
|
+
.option('--limit <n>', 'Maximum results to return', '10')
|
|
54
|
+
.action(withErrorHandling(async (opts, cmd) => {
|
|
55
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
56
|
+
const supabase = createAnonClient();
|
|
57
|
+
let query = supabase.from('coffee_catalog').select('*');
|
|
58
|
+
if (opts.origin) {
|
|
59
|
+
const o = sanitizeFilterValue(opts.origin);
|
|
60
|
+
query = query.or(`country.ilike.%${o}%,continent.ilike.%${o}%,region.ilike.%${o}%`);
|
|
61
|
+
}
|
|
62
|
+
if (opts.process) {
|
|
63
|
+
query = query.ilike('processing', `%${opts.process}%`);
|
|
64
|
+
}
|
|
65
|
+
if (opts.priceMin !== undefined) {
|
|
66
|
+
query = query.gte('cost_lb', parseFloat(opts.priceMin));
|
|
67
|
+
}
|
|
68
|
+
if (opts.priceMax !== undefined) {
|
|
69
|
+
query = query.lte('cost_lb', parseFloat(opts.priceMax));
|
|
70
|
+
}
|
|
71
|
+
if (opts.flavor) {
|
|
72
|
+
const keywords = opts.flavor
|
|
73
|
+
.split(',')
|
|
74
|
+
.map((k) => sanitizeFilterValue(k.trim()))
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
const flavorFilters = keywords
|
|
77
|
+
.flatMap((kw) => [
|
|
78
|
+
`description_short.ilike.%${kw}%`,
|
|
79
|
+
`description_long.ilike.%${kw}%`,
|
|
80
|
+
`cupping_notes.ilike.%${kw}%`,
|
|
81
|
+
`farm_notes.ilike.%${kw}%`,
|
|
82
|
+
])
|
|
83
|
+
.join(',');
|
|
84
|
+
query = query.or(flavorFilters);
|
|
85
|
+
}
|
|
86
|
+
if (opts.stocked) {
|
|
87
|
+
query = query.eq('stocked', true);
|
|
88
|
+
}
|
|
89
|
+
const limit = Math.max(1, parseInt(opts.limit, 10));
|
|
90
|
+
const { data, error } = await query.limit(limit);
|
|
91
|
+
if (error)
|
|
92
|
+
throw error;
|
|
93
|
+
if (!data || data.length === 0) {
|
|
94
|
+
info('No coffees found matching your criteria.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
outputData(data, globalOpts);
|
|
98
|
+
}));
|
|
99
|
+
// ── catalog get <id> ──────────────────────────────────────────────────────
|
|
100
|
+
catalog
|
|
101
|
+
.command('get <id>')
|
|
102
|
+
.description('Fetch a single coffee by ID')
|
|
103
|
+
.action(withErrorHandling(async (id, _opts, cmd) => {
|
|
104
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
105
|
+
const supabase = createAnonClient();
|
|
106
|
+
const { data, error } = await supabase
|
|
107
|
+
.from('coffee_catalog')
|
|
108
|
+
.select('*')
|
|
109
|
+
.eq('id', parseInt(id, 10))
|
|
110
|
+
.single();
|
|
111
|
+
if (error)
|
|
112
|
+
throw error;
|
|
113
|
+
outputData(data, globalOpts);
|
|
114
|
+
}));
|
|
115
|
+
// ── catalog stats ─────────────────────────────────────────────────────────
|
|
116
|
+
catalog
|
|
117
|
+
.command('stats')
|
|
118
|
+
.description('Aggregate statistics for the coffee catalog')
|
|
119
|
+
.action(withErrorHandling(async (_opts, cmd) => {
|
|
120
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
121
|
+
const supabase = createAnonClient();
|
|
122
|
+
const { data, error } = await supabase
|
|
123
|
+
.from('coffee_catalog')
|
|
124
|
+
.select('id, country, continent, cost_lb, stocked');
|
|
125
|
+
if (error)
|
|
126
|
+
throw error;
|
|
127
|
+
const stats = computeCatalogStats((data ?? []));
|
|
128
|
+
outputData(stats, globalOpts);
|
|
129
|
+
}));
|
|
130
|
+
return catalog;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/commands/catalog.ts"],"names":[],"mappings":"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"}
|