@purveyors/cli 0.20.1 → 0.23.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/README.md +68 -11
- package/dist/commands/price-index.d.ts +7 -0
- package/dist/commands/price-index.d.ts.map +1 -0
- package/dist/commands/price-index.js +71 -0
- package/dist/commands/price-index.js.map +1 -0
- package/dist/commands/procurement.d.ts +8 -0
- package/dist/commands/procurement.d.ts.map +1 -0
- package/dist/commands/procurement.js +72 -0
- package/dist/commands/procurement.js.map +1 -0
- package/dist/commands/roast.d.ts +27 -1
- package/dist/commands/roast.d.ts.map +1 -1
- package/dist/commands/roast.js +136 -15
- package/dist/commands/roast.js.map +1 -1
- package/dist/lib/interactive/watch.d.ts +22 -2
- package/dist/lib/interactive/watch.d.ts.map +1 -1
- package/dist/lib/interactive/watch.js +5 -3
- package/dist/lib/interactive/watch.js.map +1 -1
- package/dist/lib/manifest.d.ts.map +1 -1
- package/dist/lib/manifest.js +85 -0
- package/dist/lib/manifest.js.map +1 -1
- package/dist/lib/parchment.d.ts +48 -0
- package/dist/lib/parchment.d.ts.map +1 -0
- package/dist/lib/parchment.js +119 -0
- package/dist/lib/parchment.js.map +1 -0
- package/dist/lib/roast.d.ts +0 -10
- package/dist/lib/roast.d.ts.map +1 -1
- package/dist/lib/roast.js +6 -76
- package/dist/lib/roast.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +10 -0
- package/dist/program.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Coffee intelligence from your terminal.
|
|
4
4
|
|
|
5
|
-
`purvey` is the official CLI for [purveyors.io](https://purveyors.io). It gives coffee professionals, developers, and AI agents direct access to the Purveyors platform from the terminal: catalog search, inventory tracking, roast logging, sales records, tasting notes, and Artisan `.alog` import.
|
|
5
|
+
`purvey` is the official CLI for [purveyors.io](https://purveyors.io). It gives coffee professionals, developers, and AI agents direct access to the Purveyors platform from the terminal: catalog search, price-index snapshots, procurement brief reads, inventory tracking, roast logging, sales records, tasting notes, and Artisan `.alog` import.
|
|
6
6
|
|
|
7
7
|
Use `purvey --help` for quick command discovery, `purvey context` for the dense human-readable operator reference, `purvey manifest` for the preferred machine-readable contract, or `@purveyors/cli/manifest` in-process.
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ Use `purvey --help` for quick command discovery, `purvey context` for the dense
|
|
|
13
13
|
- Runtime: Node.js 20+
|
|
14
14
|
- No pre-existing session required: `auth`, `config`, `context`, `manifest`
|
|
15
15
|
- Viewer role required: `catalog` (excluding structured process filters on `catalog search`)
|
|
16
|
-
- Member role required: `inventory`, `roast`, `sales`, `tasting`
|
|
16
|
+
- Member role required: `price-index`, `procurement`, `inventory`, `roast`, `sales`, `tasting`
|
|
17
17
|
- Preferred machine-readable contract: `purvey manifest`
|
|
18
18
|
- Dense human-readable reference: `purvey context`
|
|
19
19
|
- Compatibility JSON alias: `purvey context --json`
|
|
@@ -62,7 +62,7 @@ The CLI is an agent-first product surface. Treat the binary, exported functions,
|
|
|
62
62
|
## Quick Start
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
# 1. Authenticate before using catalog, inventory, roast, sales, or tasting commands
|
|
65
|
+
# 1. Authenticate before using catalog, market intelligence, inventory, roast, sales, or tasting commands
|
|
66
66
|
purvey auth login
|
|
67
67
|
|
|
68
68
|
# For agents, CI, or remote machines, use headless flow:
|
|
@@ -135,7 +135,7 @@ All remote data commands require a valid authenticated session:
|
|
|
135
135
|
|
|
136
136
|
- `catalog` requires the `viewer` role by default
|
|
137
137
|
- `catalog search` structured process filters require the `member` role under the current session-authenticated CLI path
|
|
138
|
-
- `inventory`, `roast`, `sales`, and `tasting` require the `member` role
|
|
138
|
+
- `price-index`, `procurement`, `inventory`, `roast`, `sales`, and `tasting` require the `member` role under the session-authenticated CLI path
|
|
139
139
|
|
|
140
140
|
`purvey` uses Google OAuth through purveyors.io.
|
|
141
141
|
|
|
@@ -174,10 +174,10 @@ Credentials are stored at `~/.config/purvey/credentials.json`.
|
|
|
174
174
|
|
|
175
175
|
### Auth roles
|
|
176
176
|
|
|
177
|
-
| Role | Access
|
|
178
|
-
| -------- |
|
|
179
|
-
| `viewer` | `catalog search`, `catalog get`, `catalog stats`, excluding structured process filters
|
|
180
|
-
| `member` | All viewer commands, `catalog similar`, structured process filters on `catalog search`, plus `inventory`, `roast`, `sales`, `tasting` under the session-authenticated CLI path |
|
|
177
|
+
| Role | Access |
|
|
178
|
+
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
179
|
+
| `viewer` | `catalog search`, `catalog get`, `catalog stats`, excluding structured process filters |
|
|
180
|
+
| `member` | All viewer commands, `catalog similar`, structured process filters on `catalog search`, plus `price-index`, `procurement`, `inventory`, `roast`, `sales`, `tasting` under the session-authenticated CLI path |
|
|
181
181
|
|
|
182
182
|
`auth`, `config`, `context`, and `manifest` remain available without a pre-existing session.
|
|
183
183
|
|
|
@@ -412,6 +412,61 @@ Notes:
|
|
|
412
412
|
- The command preserves `classification_version`, `query_strategy`, score dimensions, proof summaries, pricing metadata, and classification/blocker details supplied by the API.
|
|
413
413
|
- `catalog stats` returns aggregate catalog metrics, not your personal inventory metrics.
|
|
414
414
|
|
|
415
|
+
### price-index
|
|
416
|
+
|
|
417
|
+
- `purvey price-index`
|
|
418
|
+
|
|
419
|
+
`price-index` filters:
|
|
420
|
+
|
|
421
|
+
- `--origin <origin>`
|
|
422
|
+
- `--process <method>`
|
|
423
|
+
- `--grade <grade>`
|
|
424
|
+
- `--from <YYYY-MM-DD>`
|
|
425
|
+
- `--to <YYYY-MM-DD>`
|
|
426
|
+
- `--wholesale <true|false>`
|
|
427
|
+
- `--page <n>`; 1-based page number
|
|
428
|
+
- `--limit <n>`; results per page
|
|
429
|
+
|
|
430
|
+
Examples:
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
purvey price-index --pretty
|
|
434
|
+
purvey price-index --origin "Ethiopia" --from 2026-01-01 --to 2026-06-30 --json
|
|
435
|
+
PARCHMENT_API_KEY="$PURVEYORS_API_KEY" purvey price-index --limit 25 --json
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Notes:
|
|
439
|
+
|
|
440
|
+
- `price-index` is backed by the canonical Parchment API `GET /v1/price-index` through `@purveyors/sdk`.
|
|
441
|
+
- Session-token use requires the local `member` role; API-key use is accepted via `PARCHMENT_API_KEY` or `PURVEYORS_API_KEY` and PPI entitlement is enforced server-side.
|
|
442
|
+
- `PARCHMENT_API_BASE_URL` overrides the canonical API base for this SDK-backed command. `PURVEYORS_BASE_URL` is also accepted for shared environment compatibility.
|
|
443
|
+
|
|
444
|
+
### procurement
|
|
445
|
+
|
|
446
|
+
- `purvey procurement list`
|
|
447
|
+
- `purvey procurement get <id>`
|
|
448
|
+
- `purvey procurement matches <id>`
|
|
449
|
+
|
|
450
|
+
`procurement matches <id>` options:
|
|
451
|
+
|
|
452
|
+
- `--page <n>`; 1-based page number
|
|
453
|
+
- `--limit <n>`; matches per page
|
|
454
|
+
|
|
455
|
+
Examples:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
purvey procurement list --pretty
|
|
459
|
+
purvey procurement get <brief-id> --pretty
|
|
460
|
+
purvey procurement matches <brief-id> --page 2 --limit 50 --json
|
|
461
|
+
PARCHMENT_API_KEY="$PURVEYORS_API_KEY" purvey procurement list --json
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Notes:
|
|
465
|
+
|
|
466
|
+
- Procurement reads are backed by the canonical Parchment API `/v1/procurement/briefs` endpoints through `@purveyors/sdk`.
|
|
467
|
+
- Session-token use requires the local `member` role; API-key use is accepted via `PARCHMENT_API_KEY` or `PURVEYORS_API_KEY` and procurement access is enforced server-side.
|
|
468
|
+
- Brief creation is intentionally not exposed here. It is a write path and belongs to the Phase 2 write build-out.
|
|
469
|
+
|
|
415
470
|
### inventory
|
|
416
471
|
|
|
417
472
|
- `purvey inventory list`
|
|
@@ -769,8 +824,9 @@ Use the right ID for the right command.
|
|
|
769
824
|
- `PURVEYORS_SUPABASE_URL`: override the Supabase project URL
|
|
770
825
|
- `PURVEYORS_SUPABASE_ANON_KEY`: override the Supabase anon key
|
|
771
826
|
- `PURVEYORS_BASE_URL`: override the Purveyors web base URL
|
|
772
|
-
- `PURVEYORS_API_KEY`: API-key token used by API-backed catalog proof reads
|
|
773
|
-
- `PARCHMENT_API_KEY`:
|
|
827
|
+
- `PURVEYORS_API_KEY`: API-key token used by API-backed catalog proof reads, paid-tier similarity reads, and SDK-backed Parchment commands when you want to call canonical API contracts without relying on the local OAuth session
|
|
828
|
+
- `PARCHMENT_API_KEY`: preferred API-key variable for SDK-backed Parchment commands; also accepted for API-backed proof and paid-tier similarity paths
|
|
829
|
+
- `PARCHMENT_API_BASE_URL`: override the SDK-backed Parchment API base URL for `price-index` and `procurement` commands
|
|
774
830
|
- `PURVEY_DEBUG`: enable verbose error output
|
|
775
831
|
|
|
776
832
|
## For AI agents
|
|
@@ -803,9 +859,10 @@ Agent integration rules of thumb:
|
|
|
803
859
|
|
|
804
860
|
- Discover first with `purvey manifest`, then call the narrowest command or package subpath that fits the job.
|
|
805
861
|
- Use `purvey context` when a human-readable operator summary is useful before tool selection.
|
|
806
|
-
- Prefer OAuth session tokens for normal user workflows; use `PURVEYORS_API_KEY` or `PARCHMENT_API_KEY` only when you intentionally need
|
|
862
|
+
- Prefer OAuth session tokens for normal user workflows; use `PURVEYORS_API_KEY` or `PARCHMENT_API_KEY` only when you intentionally need an API-key-backed Parchment path such as catalog proof, paid-tier similarity, price-index, or procurement reads.
|
|
807
863
|
- Treat `--include-proof` as API output, not a local scoring feature. If a filter cannot round-trip through `/v1/catalog?include=proof`, the CLI rejects that invocation instead of returning misleading proof data.
|
|
808
864
|
- Treat `catalog similar` as the canonical `/v1/catalog/{id}/similar` contract. Preserve the distinction between `canonical_candidates` and `similar_recommendations`; do not flatten or re-sort grouped results unless you have a specific downstream reason.
|
|
865
|
+
- Treat `price-index` and `procurement` as SDK-backed canonical API reads. Do not add procurement create/write behavior to this command group until the Phase 2 write contract ships.
|
|
809
866
|
|
|
810
867
|
## Troubleshooting
|
|
811
868
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* `purvey price-index` — Parchment Price Index aggregate snapshots.
|
|
4
|
+
* Requires an authenticated session or API key with price-index entitlement.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildPriceIndexCommand(): Command;
|
|
7
|
+
//# sourceMappingURL=price-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price-index.d.ts","sourceRoot":"","sources":["../../src/commands/price-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAgDhD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { outputData } from '../lib/output.js';
|
|
3
|
+
import { withErrorHandling, PrvrsError } from '../lib/errors.js';
|
|
4
|
+
import { createParchmentClient, unwrapParchment } from '../lib/parchment.js';
|
|
5
|
+
function parsePositiveInt(rawValue, flag) {
|
|
6
|
+
const parsed = Number(rawValue.trim());
|
|
7
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
8
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Invalid ${flag}: "${rawValue}". Must be a positive integer.`);
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function parseWholesale(rawValue) {
|
|
13
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
14
|
+
if (normalized === 'true')
|
|
15
|
+
return 'true';
|
|
16
|
+
if (normalized === 'false')
|
|
17
|
+
return 'false';
|
|
18
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --wholesale: "${rawValue}". Must be "true" or "false".`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `purvey price-index` — Parchment Price Index aggregate snapshots.
|
|
22
|
+
* Requires an authenticated session or API key with price-index entitlement.
|
|
23
|
+
*/
|
|
24
|
+
export function buildPriceIndexCommand() {
|
|
25
|
+
const priceIndex = new Command('price-index')
|
|
26
|
+
.description('Fetch Parchment Price Index aggregate snapshots (requires price-index access)')
|
|
27
|
+
.option('--origin <origin>', 'Filter by origin')
|
|
28
|
+
.option('--process <method>', 'Filter by process method')
|
|
29
|
+
.option('--grade <grade>', 'Filter by grade')
|
|
30
|
+
.option('--from <date>', 'Include snapshots on/after this ISO date (YYYY-MM-DD)')
|
|
31
|
+
.option('--to <date>', 'Include snapshots on/before this ISO date (YYYY-MM-DD)')
|
|
32
|
+
.option('--wholesale <true|false>', 'Filter by wholesale pricing scope')
|
|
33
|
+
.option('--page <n>', '1-based page number')
|
|
34
|
+
.option('--limit <n>', 'Results per page')
|
|
35
|
+
.addHelpText('after', `
|
|
36
|
+
Examples:
|
|
37
|
+
purvey price-index --pretty
|
|
38
|
+
purvey price-index --origin "Ethiopia" --json
|
|
39
|
+
purvey price-index --from 2026-01-01 --to 2026-06-30 --pretty
|
|
40
|
+
purvey price-index --wholesale true --limit 25 --json
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
Requires a Purveyors session or API key with price-index (PPI) access.
|
|
44
|
+
Set PARCHMENT_API_KEY or PURVEYORS_API_KEY to use an API key instead of the logged-in session.`)
|
|
45
|
+
.action(withErrorHandling(async (opts, cmd) => {
|
|
46
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
47
|
+
const query = {};
|
|
48
|
+
if (opts.origin !== undefined)
|
|
49
|
+
query.origin = opts.origin;
|
|
50
|
+
if (opts.process !== undefined)
|
|
51
|
+
query.process = opts.process;
|
|
52
|
+
if (opts.grade !== undefined)
|
|
53
|
+
query.grade = opts.grade;
|
|
54
|
+
if (opts.from !== undefined)
|
|
55
|
+
query.from = opts.from;
|
|
56
|
+
if (opts.to !== undefined)
|
|
57
|
+
query.to = opts.to;
|
|
58
|
+
if (opts.wholesale !== undefined)
|
|
59
|
+
query.wholesale = parseWholesale(opts.wholesale);
|
|
60
|
+
if (opts.page !== undefined)
|
|
61
|
+
query.page = parsePositiveInt(opts.page, '--page');
|
|
62
|
+
if (opts.limit !== undefined)
|
|
63
|
+
query.limit = parsePositiveInt(opts.limit, '--limit');
|
|
64
|
+
const client = await createParchmentClient('member');
|
|
65
|
+
const result = await client.priceIndex.list(query);
|
|
66
|
+
const data = unwrapParchment(result, 'price-index');
|
|
67
|
+
outputData(data, globalOpts);
|
|
68
|
+
}));
|
|
69
|
+
return priceIndex;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=price-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"price-index.js","sourceRoot":"","sources":["../../src/commands/price-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* `purvey procurement` — read saved sourcing briefs from the canonical API.
|
|
4
|
+
* Brief creation is a write and is handled by the Phase 2 write build-out
|
|
5
|
+
* (PADR-0016), not by this read surface.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildProcurementCommand(): Command;
|
|
8
|
+
//# sourceMappingURL=procurement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"procurement.d.ts","sourceRoot":"","sources":["../../src/commands/procurement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CA4EjD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { outputData } from '../lib/output.js';
|
|
3
|
+
import { withErrorHandling, PrvrsError } from '../lib/errors.js';
|
|
4
|
+
import { createParchmentClient, unwrapParchment } from '../lib/parchment.js';
|
|
5
|
+
function parsePositiveInt(rawValue, flag) {
|
|
6
|
+
const parsed = Number(rawValue.trim());
|
|
7
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
8
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Invalid ${flag}: "${rawValue}". Must be a positive integer.`);
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `purvey procurement` — read saved sourcing briefs from the canonical API.
|
|
14
|
+
* Brief creation is a write and is handled by the Phase 2 write build-out
|
|
15
|
+
* (PADR-0016), not by this read surface.
|
|
16
|
+
*/
|
|
17
|
+
export function buildProcurementCommand() {
|
|
18
|
+
const procurement = new Command('procurement').description('Read saved sourcing briefs and their catalog matches');
|
|
19
|
+
// procurement list
|
|
20
|
+
procurement
|
|
21
|
+
.command('list')
|
|
22
|
+
.description('List your saved sourcing briefs')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
Examples:
|
|
25
|
+
purvey procurement list --pretty
|
|
26
|
+
purvey procurement list --json | jq '.data'`)
|
|
27
|
+
.action(withErrorHandling(async (_opts, cmd) => {
|
|
28
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
29
|
+
const client = await createParchmentClient('member');
|
|
30
|
+
const result = await client.procurement.briefs.list();
|
|
31
|
+
const data = unwrapParchment(result, 'procurement list');
|
|
32
|
+
outputData(data, globalOpts);
|
|
33
|
+
}));
|
|
34
|
+
// procurement get <id>
|
|
35
|
+
procurement
|
|
36
|
+
.command('get <id>')
|
|
37
|
+
.description('Get a single saved sourcing brief by id')
|
|
38
|
+
.addHelpText('after', `
|
|
39
|
+
Examples:
|
|
40
|
+
purvey procurement get 3f9a2c10-... --pretty`)
|
|
41
|
+
.action(withErrorHandling(async (id, _opts, cmd) => {
|
|
42
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
43
|
+
const client = await createParchmentClient('member');
|
|
44
|
+
const result = await client.procurement.briefs.get(id);
|
|
45
|
+
const data = unwrapParchment(result, 'procurement get');
|
|
46
|
+
outputData(data, globalOpts);
|
|
47
|
+
}));
|
|
48
|
+
// procurement matches <id>
|
|
49
|
+
procurement
|
|
50
|
+
.command('matches <id>')
|
|
51
|
+
.description('Run a saved brief against the catalog and page through matches')
|
|
52
|
+
.option('--page <n>', '1-based page number (default 1)')
|
|
53
|
+
.option('--limit <n>', 'Matches per page (max 100; default 25)')
|
|
54
|
+
.addHelpText('after', `
|
|
55
|
+
Examples:
|
|
56
|
+
purvey procurement matches 3f9a2c10-... --pretty
|
|
57
|
+
purvey procurement matches 3f9a2c10-... --page 2 --limit 50 --json`)
|
|
58
|
+
.action(withErrorHandling(async (id, opts, cmd) => {
|
|
59
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
60
|
+
const query = {};
|
|
61
|
+
if (opts.page !== undefined)
|
|
62
|
+
query.page = parsePositiveInt(opts.page, '--page');
|
|
63
|
+
if (opts.limit !== undefined)
|
|
64
|
+
query.limit = parsePositiveInt(opts.limit, '--limit');
|
|
65
|
+
const client = await createParchmentClient('member');
|
|
66
|
+
const result = await client.procurement.briefs.matches(id, query);
|
|
67
|
+
const data = unwrapParchment(result, 'procurement matches');
|
|
68
|
+
outputData(data, globalOpts);
|
|
69
|
+
}));
|
|
70
|
+
return procurement;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=procurement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"procurement.js","sourceRoot":"","sources":["../../src/commands/procurement.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/roast.d.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import type { RoastProfile, TemperatureEntry, RoastEventEntry } from '../lib/roast.js';
|
|
2
|
+
import type { RoastProfile, TemperatureEntry, RoastEventEntry, ImportRoastResult } from '../lib/roast.js';
|
|
3
|
+
import type { WatchRoastImporter } from '../lib/interactive/watch.js';
|
|
4
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
5
|
+
import type { components } from '@purveyors/sdk';
|
|
6
|
+
/** Canonical `POST /v1/roasts/imports` response payload. */
|
|
7
|
+
type RoastImportPayload = components['schemas']['RoastImportResponse'];
|
|
3
8
|
export type { RoastProfile, TemperatureEntry, RoastEventEntry };
|
|
9
|
+
/**
|
|
10
|
+
* Map the canonical Parchment `roasts.import` response onto the CLI's existing
|
|
11
|
+
* {@link ImportRoastResult} output shape, so `--pretty` and machine output stay
|
|
12
|
+
* stable now that Artisan parsing and persistence happen server-side.
|
|
13
|
+
*
|
|
14
|
+
* `RoastDetailResource` surfaces only charge / first-crack / drop as top-level
|
|
15
|
+
* time columns, but the full Artisan milestone set (dry-end, second-crack
|
|
16
|
+
* start/end, cool) is persisted as milestone roast events. Recover those from
|
|
17
|
+
* `roast.events` so JSON and `--pretty` output keep the same milestone timing
|
|
18
|
+
* the pre-SDK local importer exposed, without re-parsing the `.alog` locally.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mapSdkImportResult(payload: RoastImportPayload, fallbackCoffeeId: number, fallbackBatchName: string): ImportRoastResult;
|
|
21
|
+
/**
|
|
22
|
+
* Build the roast importer used by `purvey roast watch`. Each import resolves
|
|
23
|
+
* the current member session token at call time (so long-running watch sessions
|
|
24
|
+
* survive token refresh) and forwards the raw `.alog` to the canonical Parchment
|
|
25
|
+
* API via the SDK, which parses and persists the roast server-side. The session
|
|
26
|
+
* JWT is pinned per request so an exported PARCHMENT_API_KEY/PURVEYORS_API_KEY
|
|
27
|
+
* for another account cannot authorize a write against the session user's beans.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createWatchRoastImporter(supabase: SupabaseClient): WatchRoastImporter;
|
|
4
30
|
/**
|
|
5
31
|
* `purvey roast` — Browse and manage your roast profiles.
|
|
6
32
|
* Requires member+ authentication.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roast.d.ts","sourceRoot":"","sources":["../../src/commands/roast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"roast.d.ts","sourceRoot":"","sources":["../../src/commands/roast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAO5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD,4DAA4D;AAC5D,KAAK,kBAAkB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC;AAGvE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAEhE;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,GACxB,iBAAiB,CAyDnB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,GAAG,kBAAkB,CAwBrF;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAy9B3C"}
|
package/dist/commands/roast.js
CHANGED
|
@@ -3,14 +3,110 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import { access, readFile } from 'fs/promises';
|
|
4
4
|
import { basename } from 'path';
|
|
5
5
|
import { outputData, info, success } from '../lib/output.js';
|
|
6
|
-
import { withErrorHandling, PrvrsError } from '../lib/errors.js';
|
|
6
|
+
import { withErrorHandling, PrvrsError, AuthError } from '../lib/errors.js';
|
|
7
7
|
import { requireAuth } from '../lib/auth-guard.js';
|
|
8
8
|
import { confirm, todayIso } from '../lib/prompts.js';
|
|
9
|
-
import { listRoasts, getRoast, createRoast, deleteRoast, updateRoast
|
|
9
|
+
import { listRoasts, getRoast, createRoast, deleteRoast, updateRoast } from '../lib/roast.js';
|
|
10
10
|
import { pickBean, guardCancel } from '../lib/interactive/forms.js';
|
|
11
11
|
import { normalizePathInput } from '../lib/path-input.js';
|
|
12
12
|
import { startWatch, loadWatchSession } from '../lib/interactive/watch.js';
|
|
13
13
|
import { getConfigValue } from '../lib/config.js';
|
|
14
|
+
import { createParchmentClient, resolveParchmentSessionTokenIfAvailable, unwrapParchment, } from '../lib/parchment.js';
|
|
15
|
+
/**
|
|
16
|
+
* Map the canonical Parchment `roasts.import` response onto the CLI's existing
|
|
17
|
+
* {@link ImportRoastResult} output shape, so `--pretty` and machine output stay
|
|
18
|
+
* stable now that Artisan parsing and persistence happen server-side.
|
|
19
|
+
*
|
|
20
|
+
* `RoastDetailResource` surfaces only charge / first-crack / drop as top-level
|
|
21
|
+
* time columns, but the full Artisan milestone set (dry-end, second-crack
|
|
22
|
+
* start/end, cool) is persisted as milestone roast events. Recover those from
|
|
23
|
+
* `roast.events` so JSON and `--pretty` output keep the same milestone timing
|
|
24
|
+
* the pre-SDK local importer exposed, without re-parsing the `.alog` locally.
|
|
25
|
+
*/
|
|
26
|
+
export function mapSdkImportResult(payload, fallbackCoffeeId, fallbackBatchName) {
|
|
27
|
+
const roast = payload.data.roast;
|
|
28
|
+
const summary = payload.data.import;
|
|
29
|
+
const milestones = {};
|
|
30
|
+
if (roast.charge_time != null)
|
|
31
|
+
milestones.charge = roast.charge_time;
|
|
32
|
+
if (roast.fc_start_time != null)
|
|
33
|
+
milestones.fc_start = roast.fc_start_time;
|
|
34
|
+
if (roast.fc_end_time != null)
|
|
35
|
+
milestones.fc_end = roast.fc_end_time;
|
|
36
|
+
if (roast.drop_time != null)
|
|
37
|
+
milestones.drop = roast.drop_time;
|
|
38
|
+
// Fill any remaining markers (notably dry_end / sc_start / sc_end / cool,
|
|
39
|
+
// which have no dedicated column) from the canonical milestone events. The
|
|
40
|
+
// top-level columns above win; events only backfill gaps.
|
|
41
|
+
const MILESTONE_KEYS = [
|
|
42
|
+
'charge',
|
|
43
|
+
'dry_end',
|
|
44
|
+
'fc_start',
|
|
45
|
+
'fc_end',
|
|
46
|
+
'sc_start',
|
|
47
|
+
'sc_end',
|
|
48
|
+
'drop',
|
|
49
|
+
'cool',
|
|
50
|
+
];
|
|
51
|
+
for (const event of roast.events ?? []) {
|
|
52
|
+
const key = event.event_string;
|
|
53
|
+
if (key != null &&
|
|
54
|
+
MILESTONE_KEYS.includes(key) &&
|
|
55
|
+
milestones[key] == null &&
|
|
56
|
+
event.time_seconds != null) {
|
|
57
|
+
milestones[key] = event.time_seconds;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const totalTime = roast.total_roast_time ?? 0;
|
|
61
|
+
const temperatureUnit = roast.temperature_unit === 'C' ? 'C' : 'F';
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
message: `Imported ${summary.temperaturePoints} data points, ${summary.milestoneEvents} milestones, ${summary.controlEvents} control events`,
|
|
65
|
+
milestones,
|
|
66
|
+
phases: {
|
|
67
|
+
drying_percent: roast.dry_percent ?? 0,
|
|
68
|
+
maillard_percent: roast.maillard_percent ?? 0,
|
|
69
|
+
development_percent: roast.development_percent ?? 0,
|
|
70
|
+
total_time_seconds: totalTime,
|
|
71
|
+
},
|
|
72
|
+
total_time: totalTime,
|
|
73
|
+
temperature_unit: temperatureUnit,
|
|
74
|
+
milestone_events: summary.milestoneEvents,
|
|
75
|
+
control_events: summary.controlEvents,
|
|
76
|
+
roast_id: roast.roast_id,
|
|
77
|
+
batch_name: roast.batch_name ?? fallbackBatchName,
|
|
78
|
+
coffee_name: roast.coffee_name ?? '',
|
|
79
|
+
coffee_id: roast.coffee_id ?? fallbackCoffeeId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the roast importer used by `purvey roast watch`. Each import resolves
|
|
84
|
+
* the current member session token at call time (so long-running watch sessions
|
|
85
|
+
* survive token refresh) and forwards the raw `.alog` to the canonical Parchment
|
|
86
|
+
* API via the SDK, which parses and persists the roast server-side. The session
|
|
87
|
+
* JWT is pinned per request so an exported PARCHMENT_API_KEY/PURVEYORS_API_KEY
|
|
88
|
+
* for another account cannot authorize a write against the session user's beans.
|
|
89
|
+
*/
|
|
90
|
+
export function createWatchRoastImporter(supabase) {
|
|
91
|
+
return async (args) => {
|
|
92
|
+
const { data: { session }, } = await supabase.auth.getSession();
|
|
93
|
+
if (!session?.access_token) {
|
|
94
|
+
throw new AuthError('Session expired mid-watch. Run `purvey auth login` and retry.');
|
|
95
|
+
}
|
|
96
|
+
const client = await createParchmentClient('member', session.access_token);
|
|
97
|
+
const payload = unwrapParchment(await client.roasts.import({
|
|
98
|
+
fileContent: args.fileContent,
|
|
99
|
+
fileName: args.fileName,
|
|
100
|
+
coffeeId: args.coffeeId,
|
|
101
|
+
batchName: args.batchName,
|
|
102
|
+
ozIn: args.ozIn,
|
|
103
|
+
roastNotes: args.roastNotes,
|
|
104
|
+
roastTargets: args.roastTargets,
|
|
105
|
+
fileSize: Buffer.byteLength(args.fileContent, 'utf-8'),
|
|
106
|
+
}), 'roast import');
|
|
107
|
+
return mapSdkImportResult(payload, args.coffeeId, args.batchName);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
14
110
|
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
15
111
|
/**
|
|
16
112
|
* `purvey roast` — Browse and manage your roast profiles.
|
|
@@ -433,7 +529,19 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
433
529
|
const targetsStr = String(roastTargetsRaw).trim();
|
|
434
530
|
const spin = p.spinner();
|
|
435
531
|
spin.start('Importing roast data...');
|
|
436
|
-
|
|
532
|
+
// Parse + persist happen server-side via the canonical Parchment
|
|
533
|
+
// API. `bean` was resolved through the authenticated Supabase
|
|
534
|
+
// session above (pickBean), so the import must run under that same
|
|
535
|
+
// session identity. Pin the request to the session access token
|
|
536
|
+
// instead of letting an exported PARCHMENT_API_KEY/PURVEYORS_API_KEY
|
|
537
|
+
// for another account authorize an import against a coffeeId owned
|
|
538
|
+
// by the session user.
|
|
539
|
+
const { data: { session: formSession }, } = await supabase.auth.getSession();
|
|
540
|
+
if (!formSession?.access_token) {
|
|
541
|
+
throw new AuthError('Session expired mid-import. Run `purvey auth login` and retry.');
|
|
542
|
+
}
|
|
543
|
+
const client = await createParchmentClient('member', formSession.access_token);
|
|
544
|
+
const payload = unwrapParchment(await client.roasts.import({
|
|
437
545
|
fileContent,
|
|
438
546
|
fileName,
|
|
439
547
|
coffeeId: bean.id,
|
|
@@ -441,7 +549,9 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
441
549
|
ozIn,
|
|
442
550
|
roastNotes: notesStr !== '' ? notesStr : undefined,
|
|
443
551
|
roastTargets: targetsStr !== '' ? targetsStr : undefined,
|
|
444
|
-
|
|
552
|
+
fileSize: Buffer.byteLength(fileContent, 'utf-8'),
|
|
553
|
+
}), 'roast import');
|
|
554
|
+
const result = mapSdkImportResult(payload, bean.id, batchName);
|
|
445
555
|
spin.stop('Done');
|
|
446
556
|
p.outro(`Roast imported! Profile #${result.roast_id} created.`);
|
|
447
557
|
outputData(result, globalOpts);
|
|
@@ -462,9 +572,7 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
462
572
|
// 2. Read file content
|
|
463
573
|
const fileContent = await readFile(filePath, 'utf-8');
|
|
464
574
|
const fileName = basename(filePath);
|
|
465
|
-
// 3.
|
|
466
|
-
const { supabase, userId } = await requireAuth('member');
|
|
467
|
-
// 4. Parse --coffee-id
|
|
575
|
+
// 3. Parse --coffee-id
|
|
468
576
|
if (!opts.coffeeId) {
|
|
469
577
|
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --coffee-id. Use --form for interactive mode.');
|
|
470
578
|
}
|
|
@@ -472,7 +580,7 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
472
580
|
if (isNaN(coffeeId) || coffeeId <= 0) {
|
|
473
581
|
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --coffee-id: "${opts.coffeeId}".`);
|
|
474
582
|
}
|
|
475
|
-
//
|
|
583
|
+
// 4. Parse --oz-in if provided
|
|
476
584
|
let ozIn;
|
|
477
585
|
if (opts.ozIn !== undefined) {
|
|
478
586
|
ozIn = parseFloat(opts.ozIn);
|
|
@@ -483,8 +591,17 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
483
591
|
const roastTargets = typeof opts.roastTargets === 'string' && opts.roastTargets.trim() !== ''
|
|
484
592
|
? opts.roastTargets.trim()
|
|
485
593
|
: undefined;
|
|
486
|
-
//
|
|
487
|
-
|
|
594
|
+
// 5. Run the import via the canonical Parchment API. The server
|
|
595
|
+
// parses the raw .alog, creates the roast, and persists the curve +
|
|
596
|
+
// events atomically; identity is resolved from the bearer token
|
|
597
|
+
// (session JWT or owner-bound API key), so there is no local write.
|
|
598
|
+
// Prefer an existing logged-in member session for flag mode too:
|
|
599
|
+
// users often copy --coffee-id from session-scoped inventory, and an
|
|
600
|
+
// exported API key for another account must not steal that request.
|
|
601
|
+
// If no valid session exists, the API-key-only path still works.
|
|
602
|
+
const sessionToken = await resolveParchmentSessionTokenIfAvailable('member');
|
|
603
|
+
const client = await createParchmentClient('member', sessionToken);
|
|
604
|
+
const payload = unwrapParchment(await client.roasts.import({
|
|
488
605
|
fileContent,
|
|
489
606
|
fileName,
|
|
490
607
|
coffeeId,
|
|
@@ -492,8 +609,10 @@ Required: <file> path and --coffee-id (unless using --form)
|
|
|
492
609
|
ozIn,
|
|
493
610
|
roastNotes: opts.roastNotes,
|
|
494
611
|
roastTargets,
|
|
495
|
-
|
|
496
|
-
|
|
612
|
+
fileSize: Buffer.byteLength(fileContent, 'utf-8'),
|
|
613
|
+
}), 'roast import');
|
|
614
|
+
const result = mapSdkImportResult(payload, coffeeId, opts.batchName ?? '');
|
|
615
|
+
// 6. Output
|
|
497
616
|
if (globalOpts.pretty) {
|
|
498
617
|
printImportPretty(result, coffeeId);
|
|
499
618
|
}
|
|
@@ -537,6 +656,7 @@ Notes:
|
|
|
537
656
|
`)
|
|
538
657
|
.action(withErrorHandling(async (directory, opts) => {
|
|
539
658
|
const { supabase, userId } = await requireAuth('member');
|
|
659
|
+
const roastImporter = createWatchRoastImporter(supabase);
|
|
540
660
|
const parseCommitMode = (value, fallback = 'batch') => {
|
|
541
661
|
if (value === undefined || value === null || String(value).trim() === '') {
|
|
542
662
|
return fallback;
|
|
@@ -575,7 +695,7 @@ Notes:
|
|
|
575
695
|
ozIn: saved.ozIn,
|
|
576
696
|
roastNotes: saved.roastNotes,
|
|
577
697
|
roastTargets: saved.roastTargets,
|
|
578
|
-
});
|
|
698
|
+
}, { roastImporter });
|
|
579
699
|
return;
|
|
580
700
|
}
|
|
581
701
|
// ── Interactive form mode ────────────────────────────────────────────
|
|
@@ -690,7 +810,7 @@ Notes:
|
|
|
690
810
|
ozIn: parseOptionalPositiveNumber(ozInRaw, 'green weight'),
|
|
691
811
|
roastTargets: String(roastTargetsRaw).trim() !== '' ? String(roastTargetsRaw).trim() : undefined,
|
|
692
812
|
roastNotes: String(roastNotesRaw).trim() !== '' ? String(roastNotesRaw).trim() : undefined,
|
|
693
|
-
});
|
|
813
|
+
}, { roastImporter });
|
|
694
814
|
return;
|
|
695
815
|
}
|
|
696
816
|
// ── Flag-based mode ──────────────────────────────────────────────────
|
|
@@ -751,7 +871,7 @@ Notes:
|
|
|
751
871
|
ozIn,
|
|
752
872
|
roastNotes,
|
|
753
873
|
roastTargets,
|
|
754
|
-
});
|
|
874
|
+
}, { roastImporter });
|
|
755
875
|
}));
|
|
756
876
|
return roast;
|
|
757
877
|
}
|
|
@@ -781,6 +901,7 @@ function printImportPretty(result, coffeeId) {
|
|
|
781
901
|
fc_start: 'FC Start',
|
|
782
902
|
fc_end: 'FC End',
|
|
783
903
|
sc_start: 'SC Start',
|
|
904
|
+
sc_end: 'SC End',
|
|
784
905
|
drop: 'Drop',
|
|
785
906
|
cool: 'Cool',
|
|
786
907
|
};
|