@purveyors/cli 0.3.0 → 0.4.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.
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/inventory.d.ts.map +1 -1
- package/dist/commands/inventory.js +65 -2
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/roast.d.ts.map +1 -1
- package/dist/commands/roast.js +315 -3
- package/dist/commands/roast.js.map +1 -1
- package/dist/commands/sales.d.ts.map +1 -1
- package/dist/commands/sales.js +73 -3
- package/dist/commands/sales.js.map +1 -1
- package/dist/commands/tasting.d.ts.map +1 -1
- package/dist/commands/tasting.js +81 -11
- package/dist/commands/tasting.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/ai.d.ts +36 -0
- package/dist/lib/ai.d.ts.map +1 -0
- package/dist/lib/ai.js +42 -0
- package/dist/lib/ai.js.map +1 -0
- package/dist/lib/config.d.ts +26 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +59 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/interactive/forms.d.ts +33 -0
- package/dist/lib/interactive/forms.d.ts.map +1 -0
- package/dist/lib/interactive/forms.js +139 -0
- package/dist/lib/interactive/forms.js.map +1 -0
- package/dist/lib/interactive/watch.d.ts +66 -0
- package/dist/lib/interactive/watch.d.ts.map +1 -0
- package/dist/lib/interactive/watch.js +494 -0
- package/dist/lib/interactive/watch.js.map +1 -0
- package/dist/lib/supabase.d.ts.map +1 -1
- package/dist/lib/supabase.js +16 -1
- package/dist/lib/supabase.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAoG5C"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { withErrorHandling, PrvrsError } from '../lib/errors.js';
|
|
3
|
+
import { readConfig, writeConfig, getConfigValue, setConfigValue, isValidConfigKey, } from '../lib/config.js';
|
|
4
|
+
import { success, info } from '../lib/output.js';
|
|
5
|
+
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* `purvey config` — Manage purvey CLI settings.
|
|
8
|
+
* Config is stored in ~/.config/purvey/config.json.
|
|
9
|
+
*/
|
|
10
|
+
export function buildConfigCommand() {
|
|
11
|
+
const config = new Command('config').description('Manage purvey CLI settings');
|
|
12
|
+
// ── config list ───────────────────────────────────────────────────────────
|
|
13
|
+
config
|
|
14
|
+
.command('list')
|
|
15
|
+
.description('Show all configuration values')
|
|
16
|
+
.action(withErrorHandling(async () => {
|
|
17
|
+
const cfg = await readConfig();
|
|
18
|
+
const keys = Object.keys(cfg);
|
|
19
|
+
if (keys.length === 0) {
|
|
20
|
+
info('No config values set. Use `purvey config set <key> <value>` to configure.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log('');
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
console.log(` ${key} = ${String(cfg[key])}`);
|
|
26
|
+
}
|
|
27
|
+
console.log('');
|
|
28
|
+
}));
|
|
29
|
+
// ── config get <key> ──────────────────────────────────────────────────────
|
|
30
|
+
config
|
|
31
|
+
.command('get <key>')
|
|
32
|
+
.description('Get a single configuration value')
|
|
33
|
+
.action(withErrorHandling(async (key) => {
|
|
34
|
+
if (!isValidConfigKey(key)) {
|
|
35
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Unknown config key: "${key}". Valid keys: form-mode.`);
|
|
36
|
+
}
|
|
37
|
+
const value = await getConfigValue(key);
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
info(`${key} is not set.`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(value);
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
// ── config set <key> <value> ──────────────────────────────────────────────
|
|
46
|
+
config
|
|
47
|
+
.command('set <key> <value>')
|
|
48
|
+
.description('Set a configuration value')
|
|
49
|
+
.addHelpText('after', `
|
|
50
|
+
Supported keys:
|
|
51
|
+
form-mode true/false — when true, write commands auto-enter form mode
|
|
52
|
+
if required args are missing
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
$ purvey config set form-mode true
|
|
56
|
+
$ purvey config set form-mode false
|
|
57
|
+
$ purvey config get form-mode
|
|
58
|
+
$ purvey config list
|
|
59
|
+
`)
|
|
60
|
+
.action(withErrorHandling(async (key, value) => {
|
|
61
|
+
if (!isValidConfigKey(key)) {
|
|
62
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Unknown config key: "${key}". Valid keys: form-mode.`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await setConfigValue(key, value);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
throw new PrvrsError('INVALID_ARGUMENT', err instanceof Error ? err.message : String(err));
|
|
69
|
+
}
|
|
70
|
+
success(`Config updated: ${key} = ${value}`);
|
|
71
|
+
}));
|
|
72
|
+
// ── config reset ──────────────────────────────────────────────────────────
|
|
73
|
+
config
|
|
74
|
+
.command('reset')
|
|
75
|
+
.description('Reset all configuration to defaults')
|
|
76
|
+
.action(withErrorHandling(async () => {
|
|
77
|
+
await writeConfig({});
|
|
78
|
+
success('Config reset to defaults.');
|
|
79
|
+
}));
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../src/commands/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"inventory.d.ts","sourceRoot":"","sources":["../../src/commands/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD,YAAY,EAAE,aAAa,EAAE,CAAC;AAI9B;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CA4T/C"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
2
3
|
import { createAuthenticatedClient } from '../lib/supabase.js';
|
|
4
|
+
import { getConfigValue } from '../lib/config.js';
|
|
3
5
|
import { outputData, info, success } from '../lib/output.js';
|
|
4
6
|
import { withErrorHandling, AuthError, PrvrsError } from '../lib/errors.js';
|
|
5
7
|
import { confirm, todayIso } from '../lib/prompts.js';
|
|
6
8
|
import { listInventory, getInventory, addInventory, updateInventory, deleteInventory, } from '../lib/inventory.js';
|
|
9
|
+
import { pickCatalogItem, guardCancel } from '../lib/interactive/forms.js';
|
|
7
10
|
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
8
11
|
/**
|
|
9
12
|
* `purvey inventory` — Manage your green coffee inventory.
|
|
@@ -52,12 +55,13 @@ export function buildInventoryCommand() {
|
|
|
52
55
|
inventory
|
|
53
56
|
.command('add')
|
|
54
57
|
.description('Add a new green coffee inventory item')
|
|
55
|
-
.
|
|
56
|
-
.
|
|
58
|
+
.option('--catalog-id <id>', 'Coffee catalog entry ID')
|
|
59
|
+
.option('--qty <lbs>', 'Quantity purchased in pounds')
|
|
57
60
|
.option('--cost <dollars>', 'Bean cost in dollars')
|
|
58
61
|
.option('--tax-ship <dollars>', 'Tax and shipping cost in dollars')
|
|
59
62
|
.option('--notes <text>', 'Notes for this inventory item')
|
|
60
63
|
.option('--purchase-date <YYYY-MM-DD>', 'Purchase date (defaults to today)')
|
|
64
|
+
.option('--form', 'Interactive form mode')
|
|
61
65
|
.action(withErrorHandling(async (opts, cmd) => {
|
|
62
66
|
const globalOpts = cmd.optsWithGlobals();
|
|
63
67
|
const supabase = await createAuthenticatedClient();
|
|
@@ -65,6 +69,65 @@ export function buildInventoryCommand() {
|
|
|
65
69
|
if (!user) {
|
|
66
70
|
throw new AuthError('Not logged in. Run `purvey auth login` first.');
|
|
67
71
|
}
|
|
72
|
+
// ── Interactive form mode ──────────────────────────────────────────
|
|
73
|
+
// Auto-enter form mode if config form-mode is true and required args are missing
|
|
74
|
+
const formMode = opts.form ||
|
|
75
|
+
(!(opts.catalogId && opts.qty) && (await getConfigValue('form-mode')) === 'true');
|
|
76
|
+
if (formMode) {
|
|
77
|
+
p.intro('Add Bean to Inventory');
|
|
78
|
+
const catalogItem = await pickCatalogItem(supabase);
|
|
79
|
+
const qtyRaw = await p.text({
|
|
80
|
+
message: 'Quantity (lbs)',
|
|
81
|
+
placeholder: '5',
|
|
82
|
+
validate: (v) => {
|
|
83
|
+
const n = parseFloat(String(v));
|
|
84
|
+
if (isNaN(n) || n <= 0)
|
|
85
|
+
return 'Must be a positive number.';
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
guardCancel(qtyRaw);
|
|
89
|
+
const costRaw = await p.text({
|
|
90
|
+
message: 'Cost per lb ($)',
|
|
91
|
+
placeholder: 'optional',
|
|
92
|
+
});
|
|
93
|
+
guardCancel(costRaw);
|
|
94
|
+
const notesRaw = await p.text({
|
|
95
|
+
message: 'Notes',
|
|
96
|
+
placeholder: 'optional',
|
|
97
|
+
});
|
|
98
|
+
guardCancel(notesRaw);
|
|
99
|
+
const confirmed = await p.confirm({ message: 'Add this bean?' });
|
|
100
|
+
guardCancel(confirmed);
|
|
101
|
+
if (!confirmed) {
|
|
102
|
+
p.cancel('Aborted.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const costStr = String(costRaw).trim();
|
|
106
|
+
const cost = costStr !== '' ? parseFloat(costStr) : undefined;
|
|
107
|
+
const notesStr = String(notesRaw).trim();
|
|
108
|
+
const notes = notesStr !== '' ? notesStr : undefined;
|
|
109
|
+
const qtyStr = String(qtyRaw);
|
|
110
|
+
const spin = p.spinner();
|
|
111
|
+
spin.start('Adding bean to inventory...');
|
|
112
|
+
const data = await addInventory(supabase, user.id, {
|
|
113
|
+
catalogId: catalogItem.id,
|
|
114
|
+
qty: parseFloat(qtyStr),
|
|
115
|
+
cost,
|
|
116
|
+
notes,
|
|
117
|
+
purchaseDate: todayIso(),
|
|
118
|
+
});
|
|
119
|
+
spin.stop('Done');
|
|
120
|
+
p.outro(`Bean added! Inventory item #${data.id} created.`);
|
|
121
|
+
outputData(data, globalOpts);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// ── Flag-based mode ────────────────────────────────────────────────
|
|
125
|
+
if (!opts.catalogId) {
|
|
126
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --catalog-id. Use --form for interactive mode.');
|
|
127
|
+
}
|
|
128
|
+
if (!opts.qty) {
|
|
129
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --qty. Use --form for interactive mode.');
|
|
130
|
+
}
|
|
68
131
|
const catalogId = parseInt(opts.catalogId, 10);
|
|
69
132
|
if (isNaN(catalogId)) {
|
|
70
133
|
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --catalog-id: "${opts.catalogId}".`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inventory.js","sourceRoot":"","sources":["../../src/commands/inventory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EACL,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"inventory.js","sourceRoot":"","sources":["../../src/commands/inventory.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;AAepC,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,CA2mB3C"}
|
package/dist/commands/roast.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
2
3
|
import { access, readFile } from 'fs/promises';
|
|
3
4
|
import { basename } from 'path';
|
|
4
5
|
import { createAuthenticatedClient } from '../lib/supabase.js';
|
|
@@ -6,6 +7,9 @@ import { outputData, info, success } from '../lib/output.js';
|
|
|
6
7
|
import { withErrorHandling, AuthError, PrvrsError } from '../lib/errors.js';
|
|
7
8
|
import { confirm, todayIso } from '../lib/prompts.js';
|
|
8
9
|
import { listRoasts, getRoast, createRoast, deleteRoast, importRoastFromFile, } from '../lib/roast.js';
|
|
10
|
+
import { pickBean, guardCancel } from '../lib/interactive/forms.js';
|
|
11
|
+
import { startWatch, loadWatchSession } from '../lib/interactive/watch.js';
|
|
12
|
+
import { getConfigValue } from '../lib/config.js';
|
|
9
13
|
// ─── Command builder ──────────────────────────────────────────────────────────
|
|
10
14
|
/**
|
|
11
15
|
* `purvey roast` — Browse and manage your roast profiles.
|
|
@@ -59,12 +63,13 @@ export function buildRoastCommand() {
|
|
|
59
63
|
roast
|
|
60
64
|
.command('create')
|
|
61
65
|
.description('Create a new roast profile')
|
|
62
|
-
.
|
|
66
|
+
.option('--coffee-id <id>', 'green_coffee_inv ID for this roast')
|
|
63
67
|
.option('--batch-name <name>', "Batch name (defaults to coffee name + today's date)")
|
|
64
68
|
.option('--oz-in <oz>', 'Green weight in ounces')
|
|
65
69
|
.option('--oz-out <oz>', 'Roasted weight in ounces')
|
|
66
70
|
.option('--roast-date <YYYY-MM-DD>', 'Roast date (defaults to today)')
|
|
67
71
|
.option('--notes <text>', 'Roast notes')
|
|
72
|
+
.option('--form', 'Interactive form mode')
|
|
68
73
|
.action(withErrorHandling(async (opts, cmd) => {
|
|
69
74
|
const globalOpts = cmd.optsWithGlobals();
|
|
70
75
|
const supabase = await createAuthenticatedClient();
|
|
@@ -72,6 +77,73 @@ export function buildRoastCommand() {
|
|
|
72
77
|
if (!user) {
|
|
73
78
|
throw new AuthError('Not logged in. Run `purvey auth login` first.');
|
|
74
79
|
}
|
|
80
|
+
// ── Interactive form mode ──────────────────────────────────────────
|
|
81
|
+
// Auto-enter form mode if config form-mode is true and required args are missing
|
|
82
|
+
const formMode = opts.form ||
|
|
83
|
+
(!(opts.coffeeId && opts.roastDate) && (await getConfigValue('form-mode')) === 'true');
|
|
84
|
+
if (formMode) {
|
|
85
|
+
p.intro('Create Roast Profile');
|
|
86
|
+
const bean = await pickBean(supabase, user.id);
|
|
87
|
+
const today = todayIso();
|
|
88
|
+
const defaultBatch = `${bean.name} ${today}`;
|
|
89
|
+
const batchNameRaw = await p.text({
|
|
90
|
+
message: 'Batch name',
|
|
91
|
+
placeholder: defaultBatch,
|
|
92
|
+
defaultValue: defaultBatch,
|
|
93
|
+
});
|
|
94
|
+
guardCancel(batchNameRaw);
|
|
95
|
+
const ozInRaw = await p.text({
|
|
96
|
+
message: 'Weight in (oz)',
|
|
97
|
+
placeholder: 'optional',
|
|
98
|
+
validate: (v) => {
|
|
99
|
+
if (!v || v.trim() === '')
|
|
100
|
+
return;
|
|
101
|
+
const n = parseFloat(v);
|
|
102
|
+
if (isNaN(n) || n <= 0)
|
|
103
|
+
return 'Must be a positive number.';
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
guardCancel(ozInRaw);
|
|
107
|
+
const notesRaw = await p.text({
|
|
108
|
+
message: 'Roast notes',
|
|
109
|
+
placeholder: 'optional',
|
|
110
|
+
});
|
|
111
|
+
guardCancel(notesRaw);
|
|
112
|
+
const targetsRaw = await p.text({
|
|
113
|
+
message: 'Roast targets',
|
|
114
|
+
placeholder: 'optional',
|
|
115
|
+
});
|
|
116
|
+
guardCancel(targetsRaw);
|
|
117
|
+
const confirmed = await p.confirm({ message: 'Create this roast?' });
|
|
118
|
+
guardCancel(confirmed);
|
|
119
|
+
if (!confirmed) {
|
|
120
|
+
p.cancel('Aborted.');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const ozInStr = String(ozInRaw).trim();
|
|
124
|
+
const ozIn = ozInStr !== '' ? parseFloat(ozInStr) : undefined;
|
|
125
|
+
const notesStr = String(notesRaw).trim();
|
|
126
|
+
const targetsStr = String(targetsRaw).trim();
|
|
127
|
+
const combinedNotes = [notesStr, targetsStr ? `Targets: ${targetsStr}` : ''].filter(Boolean).join('\n') ||
|
|
128
|
+
undefined;
|
|
129
|
+
const spin = p.spinner();
|
|
130
|
+
spin.start('Creating roast profile...');
|
|
131
|
+
const data = await createRoast(supabase, user.id, {
|
|
132
|
+
coffeeId: bean.id,
|
|
133
|
+
batchName: String(batchNameRaw).trim() || defaultBatch,
|
|
134
|
+
ozIn,
|
|
135
|
+
roastDate: today,
|
|
136
|
+
notes: combinedNotes,
|
|
137
|
+
});
|
|
138
|
+
spin.stop('Done');
|
|
139
|
+
p.outro(`Roast profile created! Roast #${data.roast_id}.`);
|
|
140
|
+
outputData(data, globalOpts);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// ── Flag-based mode ────────────────────────────────────────────────
|
|
144
|
+
if (!opts.coffeeId) {
|
|
145
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --coffee-id. Use --form for interactive mode.');
|
|
146
|
+
}
|
|
75
147
|
const coffeeId = parseInt(opts.coffeeId, 10);
|
|
76
148
|
if (isNaN(coffeeId)) {
|
|
77
149
|
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --coffee-id: "${opts.coffeeId}".`);
|
|
@@ -127,14 +199,102 @@ export function buildRoastCommand() {
|
|
|
127
199
|
}));
|
|
128
200
|
// ── roast import <file> ───────────────────────────────────────────────────
|
|
129
201
|
roast
|
|
130
|
-
.command('import
|
|
202
|
+
.command('import')
|
|
131
203
|
.description('Import an Artisan .alog file and create a new roast profile')
|
|
132
|
-
.
|
|
204
|
+
.argument('[file]', 'Path to .alog file (or use --form for interactive mode)')
|
|
205
|
+
.option('--coffee-id <id>', 'green_coffee_inv ID for this roast')
|
|
133
206
|
.option('--batch-name <name>', 'Batch name (auto-generated from coffee name + date if omitted)')
|
|
134
207
|
.option('--oz-in <oz>', 'Green weight in ounces (extracted from .alog if omitted)')
|
|
135
208
|
.option('--roast-notes <notes>', 'Additional roast notes')
|
|
209
|
+
.option('--form', 'Interactive form mode')
|
|
136
210
|
.action(withErrorHandling(async (file, opts, cmd) => {
|
|
137
211
|
const globalOpts = cmd.optsWithGlobals();
|
|
212
|
+
// ── Interactive form mode ────────────────────────────────────────
|
|
213
|
+
// Auto-enter form mode if config form-mode is true and required args are missing
|
|
214
|
+
const importFormMode = opts.form || (!file && (await getConfigValue('form-mode')) === 'true');
|
|
215
|
+
if (importFormMode) {
|
|
216
|
+
p.intro('Import Artisan Roast');
|
|
217
|
+
const filePathRaw = await p.text({
|
|
218
|
+
message: 'Path to .alog file',
|
|
219
|
+
placeholder: '/path/to/roast.alog',
|
|
220
|
+
validate: (v) => {
|
|
221
|
+
if (!v || String(v).trim() === '')
|
|
222
|
+
return 'Please enter a file path.';
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
guardCancel(filePathRaw);
|
|
226
|
+
const filePath = String(filePathRaw).trim();
|
|
227
|
+
// Validate file exists (async check after prompt)
|
|
228
|
+
try {
|
|
229
|
+
await access(filePath);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
p.cancel(`File not found: "${filePath}"`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
// Authenticate
|
|
236
|
+
const supabase = await createAuthenticatedClient();
|
|
237
|
+
const { data: { user }, } = await supabase.auth.getUser();
|
|
238
|
+
if (!user) {
|
|
239
|
+
throw new AuthError('Not logged in. Run `purvey auth login` first.');
|
|
240
|
+
}
|
|
241
|
+
const bean = await pickBean(supabase, user.id);
|
|
242
|
+
const today = todayIso();
|
|
243
|
+
const defaultBatch = `${bean.name} ${today}`;
|
|
244
|
+
const batchNameRaw = await p.text({
|
|
245
|
+
message: 'Batch name',
|
|
246
|
+
placeholder: defaultBatch,
|
|
247
|
+
defaultValue: defaultBatch,
|
|
248
|
+
});
|
|
249
|
+
guardCancel(batchNameRaw);
|
|
250
|
+
const ozInRaw = await p.text({
|
|
251
|
+
message: 'Weight in (oz)',
|
|
252
|
+
placeholder: 'optional — extracted from .alog if omitted',
|
|
253
|
+
validate: (v) => {
|
|
254
|
+
if (!v || v.trim() === '')
|
|
255
|
+
return;
|
|
256
|
+
const n = parseFloat(v);
|
|
257
|
+
if (isNaN(n) || n <= 0)
|
|
258
|
+
return 'Must be a positive number.';
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
guardCancel(ozInRaw);
|
|
262
|
+
const roastNotesRaw = await p.text({
|
|
263
|
+
message: 'Roast notes',
|
|
264
|
+
placeholder: 'optional',
|
|
265
|
+
});
|
|
266
|
+
guardCancel(roastNotesRaw);
|
|
267
|
+
const confirmed = await p.confirm({ message: 'Import this roast?' });
|
|
268
|
+
guardCancel(confirmed);
|
|
269
|
+
if (!confirmed) {
|
|
270
|
+
p.cancel('Aborted.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
274
|
+
const fileName = basename(filePath);
|
|
275
|
+
const ozInStr = String(ozInRaw).trim();
|
|
276
|
+
const ozIn = ozInStr !== '' ? parseFloat(ozInStr) : undefined;
|
|
277
|
+
const batchName = String(batchNameRaw).trim() || defaultBatch;
|
|
278
|
+
const notesStr = String(roastNotesRaw).trim();
|
|
279
|
+
const spin = p.spinner();
|
|
280
|
+
spin.start('Importing roast data...');
|
|
281
|
+
const result = await importRoastFromFile(supabase, user.id, {
|
|
282
|
+
fileContent,
|
|
283
|
+
fileName,
|
|
284
|
+
coffeeId: bean.id,
|
|
285
|
+
batchName,
|
|
286
|
+
ozIn,
|
|
287
|
+
roastNotes: notesStr !== '' ? notesStr : undefined,
|
|
288
|
+
});
|
|
289
|
+
spin.stop('Done');
|
|
290
|
+
p.outro(`Roast imported! Profile #${result.roast_id} created.`);
|
|
291
|
+
outputData(result, globalOpts);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// ── Flag-based mode ──────────────────────────────────────────────
|
|
295
|
+
if (!file) {
|
|
296
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing file argument. Use --form for interactive mode.');
|
|
297
|
+
}
|
|
138
298
|
// 1. Validate file exists and is readable
|
|
139
299
|
try {
|
|
140
300
|
await access(file);
|
|
@@ -152,6 +312,9 @@ export function buildRoastCommand() {
|
|
|
152
312
|
throw new AuthError('Not logged in. Run `purvey auth login` first.');
|
|
153
313
|
}
|
|
154
314
|
// 4. Parse --coffee-id
|
|
315
|
+
if (!opts.coffeeId) {
|
|
316
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --coffee-id. Use --form for interactive mode.');
|
|
317
|
+
}
|
|
155
318
|
const coffeeId = parseInt(opts.coffeeId, 10);
|
|
156
319
|
if (isNaN(coffeeId) || coffeeId <= 0) {
|
|
157
320
|
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --coffee-id: "${opts.coffeeId}".`);
|
|
@@ -182,6 +345,155 @@ export function buildRoastCommand() {
|
|
|
182
345
|
outputData(result, globalOpts);
|
|
183
346
|
}
|
|
184
347
|
}));
|
|
348
|
+
// ── roast watch <directory> ───────────────────────────────────────────────
|
|
349
|
+
roast
|
|
350
|
+
.command('watch')
|
|
351
|
+
.description('Watch a directory for new .alog files and auto-import them')
|
|
352
|
+
.argument('[directory]', 'Directory to watch for .alog files')
|
|
353
|
+
.option('--coffee-id <id>', 'green_coffee_inv ID for all imports')
|
|
354
|
+
.option('--batch-prefix <name>', 'Batch name prefix (defaults to coffee name)')
|
|
355
|
+
.option('--prompt-each', 'Prompt for bean selection on each new file')
|
|
356
|
+
.option('--auto-match', 'Use AI to auto-match beans (requires member role; no --coffee-id needed)')
|
|
357
|
+
.option('--resume', 'Resume a previous watch session')
|
|
358
|
+
.option('--form', 'Interactive form mode')
|
|
359
|
+
.action(withErrorHandling(async (directory, opts) => {
|
|
360
|
+
const supabase = await createAuthenticatedClient();
|
|
361
|
+
const { data: { user }, } = await supabase.auth.getUser();
|
|
362
|
+
if (!user) {
|
|
363
|
+
throw new AuthError('Not logged in. Run `purvey auth login` first.');
|
|
364
|
+
}
|
|
365
|
+
// ── Resume mode ──────────────────────────────────────────────────────
|
|
366
|
+
if (opts.resume) {
|
|
367
|
+
const saved = await loadWatchSession();
|
|
368
|
+
if (!saved) {
|
|
369
|
+
throw new PrvrsError('NOT_FOUND', 'No watch session to resume. Start a new one with: purvey roast watch <dir> --coffee-id <id>');
|
|
370
|
+
}
|
|
371
|
+
await startWatch(supabase, user.id, saved.directory, {
|
|
372
|
+
coffeeId: saved.coffeeId,
|
|
373
|
+
coffeeName: saved.coffeeName,
|
|
374
|
+
batchPrefix: saved.batchPrefix,
|
|
375
|
+
startSequence: saved.imports.length,
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// ── Interactive form mode ────────────────────────────────────────────
|
|
380
|
+
if (opts.form) {
|
|
381
|
+
p.intro('Watch for Artisan Roasts');
|
|
382
|
+
const dirRaw = await p.text({
|
|
383
|
+
message: 'Directory to watch',
|
|
384
|
+
placeholder: '/path/to/alog/directory',
|
|
385
|
+
validate: (v) => {
|
|
386
|
+
if (!v || String(v).trim() === '')
|
|
387
|
+
return 'Please enter a directory path.';
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
guardCancel(dirRaw);
|
|
391
|
+
const watchDir = String(dirRaw).trim();
|
|
392
|
+
try {
|
|
393
|
+
await access(watchDir);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
p.cancel(`Directory not found: "${watchDir}"`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
// Ask whether to use AI auto-match
|
|
400
|
+
const useAutoMatchRaw = await p.confirm({
|
|
401
|
+
message: 'Use AI to auto-match beans? (requires member role)',
|
|
402
|
+
initialValue: false,
|
|
403
|
+
});
|
|
404
|
+
guardCancel(useAutoMatchRaw);
|
|
405
|
+
const useAutoMatch = Boolean(useAutoMatchRaw);
|
|
406
|
+
let batchPrefix;
|
|
407
|
+
let watchCoffeeId;
|
|
408
|
+
let watchCoffeeName;
|
|
409
|
+
if (useAutoMatch) {
|
|
410
|
+
// In auto-match mode we don't need a bean upfront; use placeholder values
|
|
411
|
+
watchCoffeeId = 0;
|
|
412
|
+
watchCoffeeName = 'auto-match';
|
|
413
|
+
const batchPrefixRaw = await p.text({
|
|
414
|
+
message: 'Batch prefix',
|
|
415
|
+
placeholder: 'Roast',
|
|
416
|
+
defaultValue: 'Roast',
|
|
417
|
+
});
|
|
418
|
+
guardCancel(batchPrefixRaw);
|
|
419
|
+
batchPrefix = String(batchPrefixRaw).trim() || 'Roast';
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const bean = await pickBean(supabase, user.id);
|
|
423
|
+
watchCoffeeId = bean.id;
|
|
424
|
+
watchCoffeeName = bean.name;
|
|
425
|
+
const batchPrefixRaw = await p.text({
|
|
426
|
+
message: 'Batch prefix',
|
|
427
|
+
placeholder: bean.name,
|
|
428
|
+
defaultValue: bean.name,
|
|
429
|
+
});
|
|
430
|
+
guardCancel(batchPrefixRaw);
|
|
431
|
+
batchPrefix = String(batchPrefixRaw).trim() || bean.name;
|
|
432
|
+
}
|
|
433
|
+
const promptEachRaw = useAutoMatch
|
|
434
|
+
? false
|
|
435
|
+
: await p.confirm({
|
|
436
|
+
message: 'Prompt for bean selection on each new file?',
|
|
437
|
+
initialValue: false,
|
|
438
|
+
});
|
|
439
|
+
if (typeof promptEachRaw !== 'boolean')
|
|
440
|
+
guardCancel(promptEachRaw);
|
|
441
|
+
await startWatch(supabase, user.id, watchDir, {
|
|
442
|
+
coffeeId: watchCoffeeId,
|
|
443
|
+
coffeeName: watchCoffeeName,
|
|
444
|
+
batchPrefix,
|
|
445
|
+
promptEach: useAutoMatch ? false : Boolean(promptEachRaw),
|
|
446
|
+
autoMatch: useAutoMatch,
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
// ── Flag-based mode ──────────────────────────────────────────────────
|
|
451
|
+
if (!directory) {
|
|
452
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing directory argument. Use --form for interactive mode or --resume to continue.');
|
|
453
|
+
}
|
|
454
|
+
const autoMatch = Boolean(opts.autoMatch);
|
|
455
|
+
// --auto-match and --coffee-id are mutually exclusive (auto-match picks the bean)
|
|
456
|
+
if (autoMatch && opts.coffeeId) {
|
|
457
|
+
throw new PrvrsError('INVALID_ARGUMENT', '--auto-match and --coffee-id are mutually exclusive. Use one or the other.');
|
|
458
|
+
}
|
|
459
|
+
if (!autoMatch && !opts.coffeeId) {
|
|
460
|
+
throw new PrvrsError('INVALID_ARGUMENT', 'Missing --coffee-id. Use --form for interactive mode or --auto-match to let AI pick the bean.');
|
|
461
|
+
}
|
|
462
|
+
let coffeeId;
|
|
463
|
+
let coffeeName;
|
|
464
|
+
if (autoMatch) {
|
|
465
|
+
// Placeholder values — auto-match resolves per-file at runtime
|
|
466
|
+
coffeeId = 0;
|
|
467
|
+
coffeeName = 'auto-match';
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
coffeeId = parseInt(opts.coffeeId, 10);
|
|
471
|
+
if (isNaN(coffeeId) || coffeeId <= 0) {
|
|
472
|
+
throw new PrvrsError('INVALID_ARGUMENT', `Invalid --coffee-id: "${opts.coffeeId}".`);
|
|
473
|
+
}
|
|
474
|
+
// Look up coffee name from inventory
|
|
475
|
+
const { data: invItem, error: invError } = await supabase
|
|
476
|
+
.from('green_coffee_inv')
|
|
477
|
+
.select('id, coffee_catalog!catalog_id (name)')
|
|
478
|
+
.eq('id', coffeeId)
|
|
479
|
+
.eq('user', user.id)
|
|
480
|
+
.single();
|
|
481
|
+
if (invError || !invItem) {
|
|
482
|
+
throw new AuthError(`Inventory item ${coffeeId} not found or does not belong to you.`);
|
|
483
|
+
}
|
|
484
|
+
const catalogRaw = invItem.coffee_catalog;
|
|
485
|
+
const catalog = Array.isArray(catalogRaw) ? (catalogRaw[0] ?? null) : catalogRaw;
|
|
486
|
+
coffeeName = catalog?.name ?? `Coffee #${coffeeId}`;
|
|
487
|
+
}
|
|
488
|
+
const batchPrefix = opts.batchPrefix ?? coffeeName;
|
|
489
|
+
await startWatch(supabase, user.id, directory, {
|
|
490
|
+
coffeeId,
|
|
491
|
+
coffeeName,
|
|
492
|
+
batchPrefix,
|
|
493
|
+
promptEach: Boolean(opts.promptEach),
|
|
494
|
+
autoMatch,
|
|
495
|
+
});
|
|
496
|
+
}));
|
|
185
497
|
return roast;
|
|
186
498
|
}
|
|
187
499
|
// ─── Pretty-print helper ──────────────────────────────────────────────────────
|