@purveyors/cli 0.9.6 → 0.15.1

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.
Files changed (63) hide show
  1. package/README.md +476 -159
  2. package/dist/commands/auth.d.ts.map +1 -1
  3. package/dist/commands/auth.js +4 -3
  4. package/dist/commands/auth.js.map +1 -1
  5. package/dist/commands/catalog.d.ts.map +1 -1
  6. package/dist/commands/catalog.js +55 -31
  7. package/dist/commands/catalog.js.map +1 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +61 -12
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/context.d.ts.map +1 -1
  12. package/dist/commands/context.js +34 -234
  13. package/dist/commands/context.js.map +1 -1
  14. package/dist/commands/manifest.d.ts +3 -0
  15. package/dist/commands/manifest.d.ts.map +1 -0
  16. package/dist/commands/manifest.js +32 -0
  17. package/dist/commands/manifest.js.map +1 -0
  18. package/dist/commands/roast.d.ts.map +1 -1
  19. package/dist/commands/roast.js +105 -2
  20. package/dist/commands/roast.js.map +1 -1
  21. package/dist/commands/sales.d.ts.map +1 -1
  22. package/dist/commands/sales.js +87 -40
  23. package/dist/commands/sales.js.map +1 -1
  24. package/dist/index.js +6 -105
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/config.d.ts.map +1 -1
  27. package/dist/lib/config.js +8 -2
  28. package/dist/lib/config.js.map +1 -1
  29. package/dist/lib/errors.d.ts +13 -1
  30. package/dist/lib/errors.d.ts.map +1 -1
  31. package/dist/lib/errors.js +153 -10
  32. package/dist/lib/errors.js.map +1 -1
  33. package/dist/lib/index.d.ts +1 -0
  34. package/dist/lib/index.d.ts.map +1 -1
  35. package/dist/lib/index.js +1 -0
  36. package/dist/lib/index.js.map +1 -1
  37. package/dist/lib/interactive/watch.d.ts +37 -3
  38. package/dist/lib/interactive/watch.d.ts.map +1 -1
  39. package/dist/lib/interactive/watch.js +237 -81
  40. package/dist/lib/interactive/watch.js.map +1 -1
  41. package/dist/lib/manifest.d.ts +107 -0
  42. package/dist/lib/manifest.d.ts.map +1 -0
  43. package/dist/lib/manifest.js +977 -0
  44. package/dist/lib/manifest.js.map +1 -0
  45. package/dist/lib/output.d.ts +10 -0
  46. package/dist/lib/output.d.ts.map +1 -1
  47. package/dist/lib/output.js +33 -18
  48. package/dist/lib/output.js.map +1 -1
  49. package/dist/lib/roast.d.ts +2 -0
  50. package/dist/lib/roast.d.ts.map +1 -1
  51. package/dist/lib/roast.js +5 -2
  52. package/dist/lib/roast.js.map +1 -1
  53. package/dist/lib/sales.d.ts +15 -2
  54. package/dist/lib/sales.d.ts.map +1 -1
  55. package/dist/lib/sales.js +77 -15
  56. package/dist/lib/sales.js.map +1 -1
  57. package/dist/program.d.ts +4 -0
  58. package/dist/program.d.ts.map +1 -0
  59. package/dist/program.js +135 -0
  60. package/dist/program.js.map +1 -0
  61. package/dist/types/index.d.ts +4 -0
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/package.json +7 -1
@@ -1,244 +1,44 @@
1
1
  import { Command } from 'commander';
2
- // This text is intentionally dense. An agent should be able to read it once,
3
- // then use the CLI correctly without hunting through help output.
4
- const CONTEXT_TEXT = `
5
- PURVEY CLI - Agent Reference
6
- ============================
7
- Official CLI for purveyors.io. Node.js 20+.
8
- Credentials file: ~/.config/purvey/credentials.json
9
- Config file: ~/.config/purvey/config.json
10
-
11
- ROLES
12
- -----
13
- viewer valid authenticated session; required for catalog commands
14
- member required for inventory, roast, sales, and tasting commands
15
-
16
- AUTH
17
- ----
18
- Interactive login:
19
- purvey auth login
20
-
21
- Headless login:
22
- purvey auth login --headless
23
- 1. CLI prints a Google OAuth URL
24
- 2. User opens it in any browser and signs in
25
- 3. User pastes the full callback URL back into the terminal
26
-
27
- Status:
28
- purvey auth status
29
- purvey auth status --pretty
30
-
31
- Logout:
32
- purvey auth logout
33
-
34
- OUTPUT
35
- ------
36
- Most commands emit compact JSON to stdout by default.
37
- Use --json to request compact JSON explicitly.
38
- Use --pretty for indented JSON.
39
- Use --csv for array-shaped results that support CSV output.
40
- User feedback goes to stderr.
41
-
42
- Important exception:
43
- - auth status prints human-readable output in an interactive TTY unless --json, --pretty, or --csv is passed; otherwise it emits structured output
44
-
45
- ID MAP
46
- ------
47
- catalog_id coffee_catalog row; used by catalog get, inventory add --catalog-id, tasting get, roast list --catalog-id
48
- inventory_id green_coffee_inv row; used by inventory get/update/delete, roast --coffee-id, roast list --coffee-id, tasting rate
49
- roast_id roast_data row; used by roast get/delete, roast list --roast-id, sales --roast-id
50
- sale_id coffee_sales row; used by sales update/delete
51
-
52
- COMMANDS
53
- --------
54
- auth
55
- login [--headless]
56
- status
57
- logout
58
-
59
- catalog
60
- search [options]
61
- --origin <text>
62
- --process <method>
63
- --price-min <n>
64
- --price-max <n>
65
- --flavor <keywords>
66
- --name <text>
67
- --supplier <name>
68
- --ids <n,n,...>
69
- --variety <text>
70
- --drying-method <text>
71
- --stocked-days <n>
72
- --stocked
73
- --sort <price|price-desc|name|origin|newest>
74
- --offset <n>
75
- --limit <n>
76
- get <catalog_id>
77
- stats
78
- similar <catalog_id>
79
- --threshold <0-1> default 0.70
80
- --limit <n> default 10
81
- --stocked-only
82
-
83
- inventory (member)
84
- list
85
- --stocked
86
- --catalog-id <catalog_id>
87
- --purchase-date-start <YYYY-MM-DD>
88
- --purchase-date-end <YYYY-MM-DD>
89
- --origin <country>
90
- --limit <n>
91
- --offset <n>
92
- get <inventory_id>
93
- add
94
- --catalog-id <id> required in flag mode
95
- --qty <lbs> required in flag mode
96
- --cost <dollars>
97
- --tax-ship <dollars>
98
- --notes <text>
99
- --purchase-date <YYYY-MM-DD>
100
- --form
101
- update <inventory_id>
102
- --qty <lbs>
103
- --cost <dollars>
104
- --tax-ship <dollars>
105
- --notes <text>
106
- --stocked <true|false>
107
- delete <inventory_id> [--yes] [--force]
108
- --force cascade delete dependent roast profiles and sales records before deleting the item
109
- without --force, delete fails with DEPENDENCY_CONFLICT if dependents exist
110
-
111
- roast (member)
112
- list
113
- --coffee-id <inventory_id>
114
- --roast-id <roast_id>
115
- --batch-name <text>
116
- --coffee-name <text>
117
- --date-start <YYYY-MM-DD>
118
- --date-end <YYYY-MM-DD>
119
- --stocked
120
- --catalog-id <catalog_id>
121
- --limit <n>
122
- --offset <n>
123
- get <roast_id>
124
- --include-temps
125
- --include-events
126
- create
127
- --coffee-id <inventory_id> required in flag mode
128
- --batch-name <name>
129
- --oz-in <oz>
130
- --oz-out <oz>
131
- --roast-date <YYYY-MM-DD>
132
- --notes <text>
133
- --form
134
- update <roast_id>
135
- --notes <text>
136
- --oz-out <oz>
137
- --batch-name <name>
138
- --targets <text>
139
- delete <roast_id> [--yes]
140
- import [file]
141
- --coffee-id <inventory_id> required in flag mode
142
- --batch-name <name>
143
- --oz-in <oz>
144
- --roast-notes <text>
145
- --form
146
- watch [directory]
147
- --coffee-id <inventory_id> required unless --auto-match
148
- --batch-prefix <name>
149
- --prompt-each
150
- --auto-match mutually exclusive with --coffee-id
151
- --resume
152
- --form
153
-
154
- sales (member)
155
- list
156
- --roast-id <roast_id>
157
- --date-start <YYYY-MM-DD>
158
- --date-end <YYYY-MM-DD>
159
- --buyer <name>
160
- --limit <n>
161
- --offset <n>
162
- record
163
- --roast-id <roast_id> required in flag mode
164
- --oz <amount> required in flag mode
165
- --price <dollars> required in flag mode
166
- --buyer <name>
167
- --sell-date <YYYY-MM-DD>
168
- --form
169
- update <sale_id>
170
- --oz <amount>
171
- --price <dollars>
172
- --buyer <name>
173
- --sell-date <YYYY-MM-DD>
174
- delete <sale_id> [--yes]
175
-
176
- tasting (member)
177
- get <catalog_id>
178
- --filter <user|supplier|both> default both
179
- rate [inventory_id]
180
- --aroma <1-5> required in flag mode
181
- --body <1-5> required in flag mode
182
- --acidity <1-5> required in flag mode
183
- --sweetness <1-5> required in flag mode
184
- --aftertaste <1-5> required in flag mode
185
- --brew-method <method>
186
- --notes <text>
187
- --form
188
-
189
- config
190
- list
191
- get <key>
192
- set <key> <value>
193
- reset
194
-
195
- Current config keys:
196
- form-mode true|false
197
-
198
- WORKFLOWS
199
- ---------
200
- Catalog -> inventory:
201
- purvey catalog search --origin "Ethiopia" --stocked --pretty
202
- purvey inventory add --catalog-id 128 --qty 10 --cost 8.50
203
-
204
- Import a roast from Artisan:
205
- purvey inventory list --stocked --pretty
206
- purvey roast import ~/artisan/ethiopia.alog --coffee-id 7 --pretty
207
-
208
- Watch a folder for new roasts:
209
- purvey roast watch ~/artisan/ --coffee-id 7
210
- purvey roast watch ~/artisan/ --auto-match
211
- purvey roast watch --resume
212
-
213
- Rate coffee and record a sale:
214
- purvey tasting rate 7 --aroma 4 --body 3 --acidity 5 --sweetness 4 --aftertaste 4
215
- purvey sales record --roast-id 123 --oz 12 --price 22.00 --buyer "Jane Smith"
216
-
217
- ERROR PATTERNS
218
- --------------
219
- Not logged in:
220
- run purvey auth login or purvey auth login --headless
221
-
222
- Wrong ID type:
223
- verify whether the command wants catalog_id, inventory_id, roast_id, or sale_id
224
-
225
- Missing required args in write commands:
226
- pass the required flags or use --form
227
-
228
- Mutually exclusive watch flags:
229
- roast watch forbids using --auto-match together with --coffee-id
230
- `.trim();
2
+ import { PrvrsError, withErrorHandling } from '../lib/errors.js';
3
+ import { getCliManifest, renderContextText } from '../lib/manifest.js';
4
+ import { outputData } from '../lib/output.js';
231
5
  export function buildContextCommand() {
232
6
  return new Command('context')
233
- .description('Output full CLI reference for AI agent onboarding')
7
+ .description('Output the dense human-readable operator reference; use --json/--pretty only for manifest parity')
8
+ .option('--json', 'Emit the machine-readable manifest contract as compact JSON')
9
+ .option('--pretty', 'Emit the machine-readable manifest contract as indented JSON')
234
10
  .addHelpText('after', `
11
+ Default output is the dense human-readable operator reference.
12
+ Preferred machine contract: \`purvey manifest\`.
13
+ Use --json or --pretty only when an existing \`purvey context\` caller needs manifest parity.
14
+ Use \`@purveyors/cli/manifest\` for the same contract inside Node.js or agent runtimes.
15
+
235
16
  Examples:
236
17
  purvey context
237
18
  purvey context | head -50
238
- purvey context > cli-reference.txt
19
+ purvey context --json
20
+ purvey context --pretty
21
+ purvey context --json > cli-manifest.json
22
+ purvey manifest
239
23
  `)
240
- .action(() => {
241
- console.log(CONTEXT_TEXT);
242
- });
24
+ .action(withErrorHandling(async (opts, cmd) => {
25
+ const globalOpts = cmd.optsWithGlobals();
26
+ const manifest = getCliManifest();
27
+ const wantsJson = Boolean(opts.json) ||
28
+ Boolean(opts.pretty) ||
29
+ Boolean(globalOpts.json) ||
30
+ Boolean(globalOpts.pretty);
31
+ if (globalOpts.csv) {
32
+ throw new PrvrsError('INVALID_ARGUMENT', 'The context command does not support --csv. Use text, --json, or --pretty.');
33
+ }
34
+ if (wantsJson) {
35
+ outputData(manifest, {
36
+ json: true,
37
+ pretty: Boolean(opts.pretty) || Boolean(globalOpts.pretty),
38
+ });
39
+ return;
40
+ }
41
+ console.log(renderContextText(manifest));
42
+ }));
243
43
  }
244
44
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,6EAA6E;AAC7E,kEAAkE;AAElE,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkOpB,CAAC,IAAI,EAAE,CAAC;AAET,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;SAC1B,WAAW,CAAC,mDAAmD,CAAC;SAChE,WAAW,CACV,OAAO,EACP;;;;;CAKL,CACI;SACA,MAAM,CAAC,GAAG,EAAE;QACX,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACP,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/commands/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;SAC1B,WAAW,CACV,kGAAkG,CACnG;SACA,MAAM,CAAC,QAAQ,EAAE,6DAA6D,CAAC;SAC/E,MAAM,CAAC,UAAU,EAAE,8DAA8D,CAAC;SAClF,WAAW,CACV,OAAO,EACP;;;;;;;;;;;;;CAaL,CACI;SACA,MAAM,CACL,iBAAiB,CAAC,KAAK,EAAE,IAA6B,EAAE,GAAY,EAAE,EAAE;QACtE,MAAM,UAAU,GAAG,GAAG,CAAC,eAAe,EAAmB,CAAC;QAC1D,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;QAClC,MAAM,SAAS,GACb,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;YACpB,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;YACxB,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAE7B,IAAI,UAAU,CAAC,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,UAAU,CAClB,kBAAkB,EAClB,4EAA4E,CAC7E,CAAC;QACJ,CAAC;QAED,IAAI,SAAS,EAAE,CAAC;YACd,UAAU,CAAC,QAAQ,EAAE;gBACnB,IAAI,EAAE,IAAI;gBACV,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;aAC3D,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CACH,CAAC;AACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function buildManifestCommand(): Command;
3
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/commands/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,oBAAoB,IAAI,OAAO,CAsC9C"}
@@ -0,0 +1,32 @@
1
+ import { Command } from 'commander';
2
+ import { PrvrsError, withErrorHandling } from '../lib/errors.js';
3
+ import { getCliManifest } from '../lib/manifest.js';
4
+ import { outputData } from '../lib/output.js';
5
+ export function buildManifestCommand() {
6
+ return new Command('manifest')
7
+ .description('Output the preferred stable machine-readable CLI manifest contract for shells and agents')
8
+ .option('--json', 'Emit the manifest as compact JSON (default)')
9
+ .option('--pretty', 'Emit the manifest as indented JSON')
10
+ .addHelpText('after', `
11
+ Preferred machine-readable entrypoint for shells, scripts, and agents.
12
+ Use \`purvey context --json\` only for compatibility with existing context-based callers.
13
+
14
+ Examples:
15
+ purvey manifest
16
+ purvey manifest --pretty
17
+ purvey manifest > cli-manifest.json
18
+ purvey manifest | jq '.commandGroups[].name'
19
+ node --input-type=module -e "import { getCliManifest } from '@purveyors/cli/manifest'; console.log(JSON.stringify(getCliManifest()))"
20
+ `)
21
+ .action(withErrorHandling(async (opts, cmd) => {
22
+ const globalOpts = cmd.optsWithGlobals();
23
+ if (globalOpts.csv) {
24
+ throw new PrvrsError('INVALID_ARGUMENT', 'The manifest command does not support --csv. Use --json or --pretty.');
25
+ }
26
+ outputData(getCliManifest(), {
27
+ json: true,
28
+ pretty: Boolean(opts.pretty) || Boolean(globalOpts.pretty),
29
+ });
30
+ }));
31
+ }
32
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/commands/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC;SAC3B,WAAW,CACV,0FAA0F,CAC3F;SACA,MAAM,CAAC,QAAQ,EAAE,6CAA6C,CAAC;SAC/D,MAAM,CAAC,UAAU,EAAE,oCAAoC,CAAC;SACxD,WAAW,CACV,OAAO,EACP;;;;;;;;;;CAUL,CACI;SACA,MAAM,CACL,iBAAiB,CAAC,KAAK,EAAE,IAA6B,EAAE,GAAY,EAAE,EAAE;QACtE,MAAM,UAAU,GAAG,GAAG,CAAC,eAAe,EAAmB,CAAC;QAE1D,IAAI,UAAU,CAAC,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,UAAU,CAClB,kBAAkB,EAClB,sEAAsE,CACvE,CAAC;QACJ,CAAC;QAED,UAAU,CAAC,cAAc,EAAE,EAAE;YAC3B,IAAI,EAAE,IAAI;YACV,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;SAC3D,CAAC,CAAC;IACL,CAAC,CAAC,CACH,CAAC;AACN,CAAC"}
@@ -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;AAgBpC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAOzB,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAIhE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAyyB3C"}
1
+ {"version":3,"file":"roast.d.ts","sourceRoot":"","sources":["../../src/commands/roast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EAEhB,MAAM,iBAAiB,CAAC;AAOzB,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAIhE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAk6B3C"}
@@ -342,12 +342,14 @@ Notes:
342
342
  .option('--batch-name <name>', 'Batch name (auto-generated from coffee name + date if omitted)')
343
343
  .option('--oz-in <oz>', 'Green weight in ounces (extracted from .alog if omitted)')
344
344
  .option('--roast-notes <notes>', 'Additional roast notes')
345
+ .option('--roast-targets <targets>', 'Roast targets to store with the import')
345
346
  .option('--form', 'Interactive form mode (browse and select bean)')
346
347
  .addHelpText('after', `
347
348
  Examples:
348
349
  purvey roast import ~/artisan/ethiopia-guji.alog --coffee-id 7 --pretty
349
350
  purvey roast import roast.alog --coffee-id 42 --oz-in 16
350
351
  purvey roast import roast.alog --coffee-id 7 --batch-name "Ethiopia Guji #3" --roast-notes "Faster development"
352
+ purvey roast import roast.alog --coffee-id 7 --roast-targets "Aim for 18% development"
351
353
  purvey roast import --form # interactive wizard (browse files + select bean)
352
354
 
353
355
  Required: <file> path and --coffee-id (unless using --form)
@@ -410,6 +412,11 @@ Required: <file> path and --coffee-id (unless using --form)
410
412
  placeholder: 'optional',
411
413
  });
412
414
  guardCancel(roastNotesRaw);
415
+ const roastTargetsRaw = await p.text({
416
+ message: 'Roast targets',
417
+ placeholder: 'optional',
418
+ });
419
+ guardCancel(roastTargetsRaw);
413
420
  const confirmed = await p.confirm({ message: 'Import this roast?' });
414
421
  guardCancel(confirmed);
415
422
  if (!confirmed) {
@@ -422,6 +429,7 @@ Required: <file> path and --coffee-id (unless using --form)
422
429
  const ozIn = ozInStr !== '' ? parseFloat(ozInStr) : undefined;
423
430
  const batchName = String(batchNameRaw).trim() || defaultBatch;
424
431
  const notesStr = String(roastNotesRaw).trim();
432
+ const targetsStr = String(roastTargetsRaw).trim();
425
433
  const spin = p.spinner();
426
434
  spin.start('Importing roast data...');
427
435
  const result = await importRoastFromFile(supabase, userId, {
@@ -431,6 +439,7 @@ Required: <file> path and --coffee-id (unless using --form)
431
439
  batchName,
432
440
  ozIn,
433
441
  roastNotes: notesStr !== '' ? notesStr : undefined,
442
+ roastTargets: targetsStr !== '' ? targetsStr : undefined,
434
443
  });
435
444
  spin.stop('Done');
436
445
  p.outro(`Roast imported! Profile #${result.roast_id} created.`);
@@ -469,6 +478,9 @@ Required: <file> path and --coffee-id (unless using --form)
469
478
  throw new PrvrsError('INVALID_ARGUMENT', `Invalid --oz-in: "${opts.ozIn}".`);
470
479
  }
471
480
  }
481
+ const roastTargets = typeof opts.roastTargets === 'string' && opts.roastTargets.trim() !== ''
482
+ ? opts.roastTargets.trim()
483
+ : undefined;
472
484
  // 6. Run the import
473
485
  const result = await importRoastFromFile(supabase, userId, {
474
486
  fileContent,
@@ -477,6 +489,7 @@ Required: <file> path and --coffee-id (unless using --form)
477
489
  batchName: opts.batchName,
478
490
  ozIn,
479
491
  roastNotes: opts.roastNotes,
492
+ roastTargets,
480
493
  });
481
494
  // 7. Output
482
495
  if (globalOpts.pretty) {
@@ -490,12 +503,16 @@ Required: <file> path and --coffee-id (unless using --form)
490
503
  // ── roast watch <directory> ───────────────────────────────────────────────
491
504
  roast
492
505
  .command('watch')
493
- .description('Watch a directory for new .alog files and auto-import them')
506
+ .description('Watch a directory for new .alog files and queue or auto-import them')
494
507
  .argument('[directory]', 'Directory to watch for .alog files')
495
508
  .option('--coffee-id <id>', '[REQUIRED unless --auto-match] green_coffee_inv ID for all imports')
496
509
  .option('--batch-prefix <name>', 'Batch name prefix for auto-named batches (defaults to coffee name)')
497
510
  .option('--prompt-each', 'Prompt for bean selection on each new file (instead of using --coffee-id)')
498
511
  .option('--auto-match', 'Use AI to auto-match beans per file (mutually exclusive with --coffee-id)')
512
+ .option('--commit-mode <mode>', 'Commit mode: batch (default) or individual')
513
+ .option('--oz-in <oz>', 'Green weight in ounces for watched imports')
514
+ .option('--roast-notes <notes>', 'Roast notes to apply to watched imports')
515
+ .option('--roast-targets <targets>', 'Roast targets to apply to watched imports')
499
516
  .option('--resume', 'Resume a previous watch session from where it left off')
500
517
  .option('--form', 'Interactive form mode (prompts for directory + bean selection)')
501
518
  .addHelpText('after', `
@@ -503,19 +520,41 @@ Examples:
503
520
  purvey roast watch ~/artisan/ --coffee-id 7
504
521
  purvey roast watch ~/artisan/ --coffee-id 42 --batch-prefix "Colombia Huila"
505
522
  purvey roast watch ~/artisan/ --auto-match
523
+ purvey roast watch ~/artisan/ --coffee-id 7 --commit-mode individual
506
524
  purvey roast watch ~/artisan/ --prompt-each
507
525
  purvey roast watch --resume # continue a previous session
508
526
  purvey roast watch --form # interactive setup wizard
509
527
 
510
528
  Notes:
511
529
  Runs continuously until Ctrl+C. New .alog files detected in the directory
512
- are automatically imported as roast profiles.
530
+ are queued or imported as roast profiles depending on commit mode.
513
531
  --auto-match and --coffee-id are mutually exclusive.
532
+ --commit-mode defaults to batch.
514
533
  Session state is saved for --resume.
515
534
  Requires authentication (member role).
516
535
  `)
517
536
  .action(withErrorHandling(async (directory, opts) => {
518
537
  const { supabase, userId } = await requireAuth('member');
538
+ const parseCommitMode = (value, fallback = 'batch') => {
539
+ if (value === undefined || value === null || String(value).trim() === '') {
540
+ return fallback;
541
+ }
542
+ const normalized = String(value).trim().toLowerCase();
543
+ if (normalized === 'batch' || normalized === 'individual') {
544
+ return normalized;
545
+ }
546
+ throw new PrvrsError('INVALID_ARGUMENT', `Invalid --commit-mode: "${value}". Use "batch" or "individual".`);
547
+ };
548
+ const parseOptionalPositiveNumber = (value, flagName) => {
549
+ if (value === undefined || value === null || String(value).trim() === '') {
550
+ return undefined;
551
+ }
552
+ const parsed = parseFloat(String(value));
553
+ if (isNaN(parsed) || parsed <= 0) {
554
+ throw new PrvrsError('INVALID_ARGUMENT', `Invalid ${flagName}: "${value}".`);
555
+ }
556
+ return parsed;
557
+ };
519
558
  // ── Resume mode ──────────────────────────────────────────────────────
520
559
  if (opts.resume) {
521
560
  const saved = await loadWatchSession();
@@ -526,7 +565,15 @@ Notes:
526
565
  coffeeId: saved.coffeeId,
527
566
  coffeeName: saved.coffeeName,
528
567
  batchPrefix: saved.batchPrefix,
568
+ promptEach: Boolean(saved.promptEach),
569
+ autoMatch: Boolean(saved.autoMatch),
570
+ commitMode: saved.commitMode ?? 'batch',
571
+ startedAt: saved.startedAt,
572
+ resumeImports: saved.imports,
529
573
  startSequence: saved.imports.length,
574
+ ozIn: saved.ozIn,
575
+ roastNotes: saved.roastNotes,
576
+ roastTargets: saved.roastTargets,
530
577
  });
531
578
  return;
532
579
  }
@@ -592,12 +639,56 @@ Notes:
592
639
  });
593
640
  if (typeof promptEachRaw !== 'boolean')
594
641
  guardCancel(promptEachRaw);
642
+ const commitModeRaw = await p.select({
643
+ message: 'Commit mode',
644
+ initialValue: 'batch',
645
+ options: [
646
+ {
647
+ value: 'batch',
648
+ label: 'Batch commit on Ctrl+C',
649
+ hint: 'Default. Queue imports and commit them together when the session ends.',
650
+ },
651
+ {
652
+ value: 'individual',
653
+ label: 'Commit each roast immediately',
654
+ hint: 'Import each new file as soon as it appears.',
655
+ },
656
+ ],
657
+ });
658
+ guardCancel(commitModeRaw);
659
+ const commitMode = parseCommitMode(commitModeRaw);
660
+ const ozInRaw = await p.text({
661
+ message: 'Green weight (oz)',
662
+ placeholder: 'optional',
663
+ validate: (v) => {
664
+ if (!v || v.trim() === '')
665
+ return;
666
+ const n = parseFloat(v);
667
+ if (isNaN(n) || n <= 0)
668
+ return 'Must be a positive number.';
669
+ },
670
+ });
671
+ guardCancel(ozInRaw);
672
+ const roastTargetsRaw = await p.text({
673
+ message: 'Roast targets',
674
+ placeholder: 'optional',
675
+ });
676
+ guardCancel(roastTargetsRaw);
677
+ const roastNotesRaw = await p.text({
678
+ message: 'Roast notes',
679
+ placeholder: 'optional',
680
+ });
681
+ guardCancel(roastNotesRaw);
595
682
  await startWatch(supabase, userId, watchDir, {
596
683
  coffeeId: watchCoffeeId,
597
684
  coffeeName: watchCoffeeName,
598
685
  batchPrefix,
686
+ commitMode,
599
687
  promptEach: useAutoMatch ? false : Boolean(promptEachRaw),
600
688
  autoMatch: useAutoMatch,
689
+ ozIn: parseOptionalPositiveNumber(ozInRaw, 'green weight'),
690
+ roastTargets: String(roastTargetsRaw).trim() !== '' ? String(roastTargetsRaw).trim() : undefined,
691
+ roastNotes: String(roastNotesRaw).trim() !== '' ? String(roastNotesRaw).trim() : undefined,
601
692
  });
602
693
  return;
603
694
  }
@@ -606,6 +697,14 @@ Notes:
606
697
  throw new PrvrsError('INVALID_ARGUMENT', 'Missing directory argument. Use --form for interactive mode or --resume to continue.');
607
698
  }
608
699
  const autoMatch = Boolean(opts.autoMatch);
700
+ const commitMode = parseCommitMode(opts.commitMode);
701
+ const ozIn = parseOptionalPositiveNumber(opts.ozIn, '--oz-in');
702
+ const roastNotes = typeof opts.roastNotes === 'string' && opts.roastNotes.trim() !== ''
703
+ ? opts.roastNotes.trim()
704
+ : undefined;
705
+ const roastTargets = typeof opts.roastTargets === 'string' && opts.roastTargets.trim() !== ''
706
+ ? opts.roastTargets.trim()
707
+ : undefined;
609
708
  // --auto-match and --coffee-id are mutually exclusive (auto-match picks the bean)
610
709
  if (autoMatch && opts.coffeeId) {
611
710
  throw new PrvrsError('INVALID_ARGUMENT', '--auto-match and --coffee-id are mutually exclusive. Use one or the other.');
@@ -644,8 +743,12 @@ Notes:
644
743
  coffeeId,
645
744
  coffeeName,
646
745
  batchPrefix,
746
+ commitMode,
647
747
  promptEach: Boolean(opts.promptEach),
648
748
  autoMatch,
749
+ ozIn,
750
+ roastNotes,
751
+ roastTargets,
649
752
  });
650
753
  }));
651
754
  return roast;