amaprice 1.0.9 → 1.0.11

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 CHANGED
@@ -177,6 +177,13 @@ Environment variables used by the npm package:
177
177
  | `SYNC_INTERVAL_MINUTES` | `5` | `src/worker.js` | Worker loop interval |
178
178
  | `SYNC_LIMIT` | `20` | `src/worker.js`, `amaprice sync --limit` | Max due products per run |
179
179
  | `SYNC_RUN_ONCE` | `0` | `src/worker.js` | Set `1` for single run and exit |
180
+ | `VISION_FALLBACK_ENABLED` | `0` | `src/extractors/pipeline.js` | Enable screenshot + vision fallback when HTML/JSON extraction fails |
181
+ | `OPENROUTER_API_KEY` | none | `src/extractors/vision.js` | Preferred vision provider key |
182
+ | `VISION_MODEL` | `google/gemini-3-flash-preview` | `src/extractors/vision.js` | OpenRouter model ID for vision extraction |
183
+ | `VISION_PROVIDER` | auto | `src/extractors/vision.js` | Optional force value: `openrouter` or `openai` |
184
+ | `OPENROUTER_HTTP_REFERER` | none | `src/extractors/vision.js` | Optional OpenRouter attribution header |
185
+ | `OPENROUTER_TITLE` | none | `src/extractors/vision.js` | Optional OpenRouter attribution header |
186
+ | `OPENAI_API_KEY` | none | `src/extractors/vision.js` | Legacy fallback if `OPENROUTER_API_KEY` is unset |
180
187
 
181
188
  For production background workers, prefer the Supabase **service role key**.
182
189
 
@@ -192,6 +199,9 @@ Steps:
192
199
  3. Optional env vars:
193
200
  - `SYNC_INTERVAL_MINUTES=5`
194
201
  - `SYNC_LIMIT=20`
202
+ - `VISION_FALLBACK_ENABLED=1`
203
+ - `OPENROUTER_API_KEY=<your-openrouter-key>`
204
+ - `VISION_MODEL=google/gemini-3-flash-preview`
195
205
  4. Ensure builder is Dockerfile (root `Dockerfile`).
196
206
  5. Deploy.
197
207
  6. Confirm logs show `[worker] processed=...`.
package/bin/cli.js CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  const { program } = require('commander');
4
4
  const pkg = require('../package.json');
5
- const KNOWN_COMMANDS = new Set(['price', 'track', 'history', 'list', 'sync', 'tier', 'help']);
5
+ const KNOWN_COMMANDS = new Set([
6
+ 'price',
7
+ 'track',
8
+ 'history',
9
+ 'list',
10
+ 'sync',
11
+ 'tier',
12
+ 'subscribe',
13
+ 'unsubscribe',
14
+ 'subscriptions',
15
+ 'collector',
16
+ 'help',
17
+ ]);
6
18
 
7
19
  const userArgs = process.argv.slice(2);
8
20
  if (userArgs.length > 0) {
@@ -25,5 +37,9 @@ require('../src/commands/history')(program);
25
37
  require('../src/commands/list')(program);
26
38
  require('../src/commands/sync')(program);
27
39
  require('../src/commands/tier')(program);
40
+ require('../src/commands/subscribe')(program);
41
+ require('../src/commands/unsubscribe')(program);
42
+ require('../src/commands/subscriptions')(program);
43
+ require('../src/commands/collector')(program);
28
44
 
29
45
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amaprice",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "CLI tool to scrape and track Amazon product prices",
5
5
  "main": "src/scraper.js",
6
6
  "type": "commonjs",
@@ -11,6 +11,9 @@
11
11
  "sync": "node bin/cli.js sync",
12
12
  "worker": "node src/worker.js",
13
13
  "worker:once": "SYNC_RUN_ONCE=1 node src/worker.js",
14
+ "collector:start": "node bin/cli.js collector start",
15
+ "collector:once": "node bin/cli.js collector run-once",
16
+ "analyze:scrape": "node scripts/analyze-scrape-attempts.js",
14
17
  "test": "node --test",
15
18
  "postinstall": "npx --yes playwright install chromium"
16
19
  },
@@ -0,0 +1,68 @@
1
+ const { runOrchestratedSync } = require('../orchestrator/runner');
2
+ const { readCollectorState } = require('./state');
3
+ const { heartbeatCollector } = require('../db');
4
+
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ async function ensureCollectorState() {
10
+ const state = await readCollectorState();
11
+ if (!state || !state.collectorId) {
12
+ throw new Error('Collector is not enabled. Run `amaprice collector enable` first.');
13
+ }
14
+ return state;
15
+ }
16
+
17
+ async function runCollectorOnce({ limit = 5 } = {}) {
18
+ const state = await ensureCollectorState();
19
+ await heartbeatCollector({
20
+ collectorId: state.collectorId,
21
+ status: state.status === 'paused' ? 'paused' : 'active',
22
+ capabilities: state.capabilities || { html_json: true, vision: true },
23
+ }).catch(() => {});
24
+
25
+ if (state.status === 'paused') {
26
+ return {
27
+ processed: 0,
28
+ success: 0,
29
+ failed: 0,
30
+ items: [],
31
+ paused: true,
32
+ };
33
+ }
34
+
35
+ return runOrchestratedSync({
36
+ limit,
37
+ collectorId: state.collectorId,
38
+ executor: 'collector',
39
+ routeHint: 'collector_first',
40
+ allowVision: true,
41
+ allowRailwayDomFallback: true,
42
+ });
43
+ }
44
+
45
+ async function runCollectorLoop({ limit = 5, pollSeconds = 20 } = {}) {
46
+ const safePollMs = Math.max(5, Number(pollSeconds) || 20) * 1000;
47
+
48
+ while (true) {
49
+ const started = Date.now();
50
+ const report = await runCollectorOnce({ limit });
51
+ const elapsed = Date.now() - started;
52
+ const waitMs = Math.max(0, safePollMs - elapsed);
53
+
54
+ if (report.paused) {
55
+ await sleep(safePollMs);
56
+ continue;
57
+ }
58
+
59
+ if (waitMs > 0) {
60
+ await sleep(waitMs);
61
+ }
62
+ }
63
+ }
64
+
65
+ module.exports = {
66
+ runCollectorOnce,
67
+ runCollectorLoop,
68
+ };
@@ -0,0 +1,50 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ function getStateDir() {
6
+ return process.env.AMAPRICE_STATE_DIR || path.join(os.homedir(), '.amaprice');
7
+ }
8
+
9
+ function getCollectorStatePath() {
10
+ return path.join(getStateDir(), 'collector.json');
11
+ }
12
+
13
+ async function readCollectorState() {
14
+ const target = getCollectorStatePath();
15
+ try {
16
+ const raw = await fs.readFile(target, 'utf8');
17
+ return JSON.parse(raw);
18
+ } catch (err) {
19
+ if (err && err.code === 'ENOENT') {
20
+ return null;
21
+ }
22
+ throw err;
23
+ }
24
+ }
25
+
26
+ async function writeCollectorState(state) {
27
+ const dir = getStateDir();
28
+ await fs.mkdir(dir, { recursive: true });
29
+ const target = getCollectorStatePath();
30
+ await fs.writeFile(target, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
31
+ return target;
32
+ }
33
+
34
+ async function clearCollectorState() {
35
+ const target = getCollectorStatePath();
36
+ try {
37
+ await fs.unlink(target);
38
+ } catch (err) {
39
+ if (!err || err.code !== 'ENOENT') {
40
+ throw err;
41
+ }
42
+ }
43
+ }
44
+
45
+ module.exports = {
46
+ getCollectorStatePath,
47
+ readCollectorState,
48
+ writeCollectorState,
49
+ clearCollectorState,
50
+ };
@@ -0,0 +1,187 @@
1
+ const os = require('os');
2
+ const { getUserId } = require('../user-context');
3
+ const {
4
+ upsertCollector,
5
+ getCollectorById,
6
+ heartbeatCollector,
7
+ } = require('../db');
8
+ const {
9
+ readCollectorState,
10
+ writeCollectorState,
11
+ clearCollectorState,
12
+ getCollectorStatePath,
13
+ } = require('../collector/state');
14
+ const {
15
+ runCollectorOnce,
16
+ runCollectorLoop,
17
+ } = require('../collector/client');
18
+
19
+ function getDefaultCollectorName() {
20
+ return `${os.hostname()}-collector`;
21
+ }
22
+
23
+ module.exports = function (program) {
24
+ program
25
+ .command('collector <action>')
26
+ .description('Manage local collector daemon (enable|disable|status|pause|resume|run-once|start)')
27
+ .option('--name <name>', 'Collector name override')
28
+ .option('--limit <n>', 'Max jobs per loop/once run', '5')
29
+ .option('--poll-seconds <n>', 'Polling interval for start loop', '20')
30
+ .option('--json', 'Output as JSON')
31
+ .action(async (action, opts) => {
32
+ const normalizedAction = String(action || '').trim().toLowerCase();
33
+ const userId = getUserId();
34
+ const limit = Math.max(1, Number(opts.limit) || 5);
35
+ const pollSeconds = Math.max(5, Number(opts.pollSeconds) || 20);
36
+
37
+ try {
38
+ if (normalizedAction === 'enable') {
39
+ const existing = await readCollectorState();
40
+ const collector = await upsertCollector({
41
+ collectorId: existing?.collectorId || null,
42
+ userId,
43
+ name: opts.name || existing?.name || getDefaultCollectorName(),
44
+ kind: 'cli',
45
+ status: 'active',
46
+ capabilities: {
47
+ html_json: true,
48
+ vision: true,
49
+ railway_dom: true,
50
+ },
51
+ metadata: {
52
+ platform: process.platform,
53
+ node: process.version,
54
+ },
55
+ });
56
+
57
+ const nextState = {
58
+ collectorId: collector.id,
59
+ userId,
60
+ name: collector.name,
61
+ status: 'active',
62
+ capabilities: collector.capabilities,
63
+ enabledAt: new Date().toISOString(),
64
+ };
65
+ const statePath = await writeCollectorState(nextState);
66
+
67
+ if (opts.json) {
68
+ console.log(JSON.stringify({
69
+ enabled: true,
70
+ collectorId: collector.id,
71
+ statePath,
72
+ }));
73
+ return;
74
+ }
75
+
76
+ console.log(`Collector enabled: ${collector.id}`);
77
+ console.log(`State file: ${statePath}`);
78
+ return;
79
+ }
80
+
81
+ if (normalizedAction === 'disable') {
82
+ const existing = await readCollectorState();
83
+ if (existing?.collectorId) {
84
+ await heartbeatCollector({
85
+ collectorId: existing.collectorId,
86
+ status: 'revoked',
87
+ }).catch(() => {});
88
+ }
89
+ await clearCollectorState();
90
+
91
+ if (opts.json) {
92
+ console.log(JSON.stringify({ enabled: false }));
93
+ return;
94
+ }
95
+
96
+ console.log('Collector disabled.');
97
+ return;
98
+ }
99
+
100
+ if (normalizedAction === 'status') {
101
+ const state = await readCollectorState();
102
+ const remote = state?.collectorId
103
+ ? await getCollectorById(state.collectorId).catch(() => null)
104
+ : null;
105
+
106
+ if (opts.json) {
107
+ console.log(JSON.stringify({
108
+ statePath: getCollectorStatePath(),
109
+ local: state,
110
+ remote,
111
+ }));
112
+ return;
113
+ }
114
+
115
+ if (!state) {
116
+ console.log('Collector is not enabled.');
117
+ console.log(`State file: ${getCollectorStatePath()}`);
118
+ return;
119
+ }
120
+
121
+ console.log(`Collector ID: ${state.collectorId}`);
122
+ console.log(`Status: ${state.status || 'active'}`);
123
+ console.log(`State file: ${getCollectorStatePath()}`);
124
+ if (remote) {
125
+ console.log(`Last seen: ${remote.last_seen_at || 'never'}`);
126
+ console.log(`Remote: ${remote.status}`);
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (normalizedAction === 'pause' || normalizedAction === 'resume') {
132
+ const state = await readCollectorState();
133
+ if (!state?.collectorId) {
134
+ throw new Error('Collector is not enabled. Run `amaprice collector enable` first.');
135
+ }
136
+
137
+ const status = normalizedAction === 'pause' ? 'paused' : 'active';
138
+ await heartbeatCollector({ collectorId: state.collectorId, status });
139
+ await writeCollectorState({
140
+ ...state,
141
+ status,
142
+ updatedAt: new Date().toISOString(),
143
+ });
144
+
145
+ if (opts.json) {
146
+ console.log(JSON.stringify({ collectorId: state.collectorId, status }));
147
+ return;
148
+ }
149
+
150
+ console.log(`Collector ${status}.`);
151
+ return;
152
+ }
153
+
154
+ if (normalizedAction === 'run-once') {
155
+ const report = await runCollectorOnce({ limit });
156
+ if (opts.json) {
157
+ console.log(JSON.stringify(report));
158
+ return;
159
+ }
160
+
161
+ if (report.paused) {
162
+ console.log('Collector is paused.');
163
+ return;
164
+ }
165
+
166
+ console.log(`Processed: ${report.processed}`);
167
+ console.log(`Success: ${report.success}`);
168
+ console.log(`Failed: ${report.failed}`);
169
+ return;
170
+ }
171
+
172
+ if (normalizedAction === 'start') {
173
+ if (!opts.json) {
174
+ console.log(`[collector] starting limit=${limit} poll_seconds=${pollSeconds}`);
175
+ }
176
+ await runCollectorLoop({ limit, pollSeconds });
177
+ return;
178
+ }
179
+
180
+ console.error('Unknown collector action. Use: enable, disable, status, pause, resume, run-once, start.');
181
+ process.exit(1);
182
+ } catch (err) {
183
+ console.error(`Error: ${err.message}`);
184
+ process.exit(1);
185
+ }
186
+ });
187
+ };
@@ -1,13 +1,60 @@
1
- const { listProducts } = require('../db');
1
+ const { listProducts, getUserSubscriptions } = require('../db');
2
2
  const { formatPrice } = require('../format');
3
+ const { getUserId } = require('../user-context');
3
4
 
4
5
  module.exports = function (program) {
5
6
  program
6
7
  .command('list')
7
- .description('Show all tracked products with latest price')
8
+ .description('Show tracked products (subscriptions by default)')
9
+ .option('--global', 'Show global tracked products instead of current user subscriptions')
10
+ .option('--all', 'Include inactive subscriptions (subscriptions mode only)')
8
11
  .option('--json', 'Output as JSON')
9
12
  .action(async (opts) => {
10
13
  try {
14
+ if (!opts.global) {
15
+ const userId = getUserId();
16
+ try {
17
+ const subscriptions = await getUserSubscriptions(userId, { activeOnly: !opts.all });
18
+ if (opts.json) {
19
+ console.log(JSON.stringify(subscriptions.map((row) => ({
20
+ asin: row.product?.asin || null,
21
+ title: row.product?.title || null,
22
+ url: row.product?.url || null,
23
+ domain: row.product?.domain || null,
24
+ active: row.is_active,
25
+ tier: row.product?.tier || 'daily',
26
+ tierPref: row.tier_pref || null,
27
+ latestPrice: row.latestPrice ? parseFloat(row.latestPrice.price) : null,
28
+ currency: row.latestPrice?.currency || null,
29
+ lastScraped: row.latestPrice?.scraped_at || null,
30
+ }))));
31
+ return;
32
+ }
33
+
34
+ if (subscriptions.length === 0) {
35
+ console.log('No subscriptions found. Use `amaprice subscribe <url-or-asin>` to start tracking.');
36
+ return;
37
+ }
38
+
39
+ console.log(`Subscriptions for ${userId}:`);
40
+ for (const row of subscriptions) {
41
+ const price = row.latestPrice
42
+ ? formatPrice(parseFloat(row.latestPrice.price), row.latestPrice.currency)
43
+ : 'N/A';
44
+ const tier = row.product?.tier || 'daily';
45
+ const status = row.is_active ? tier : 'paused';
46
+ console.log(` ${row.product?.asin || 'unknown'} ${price} [${status}] ${row.product?.title || ''}`);
47
+ }
48
+ return;
49
+ } catch (err) {
50
+ const msg = String(err.message || '');
51
+ if (!/hybrid orchestration migration/i.test(msg)) {
52
+ throw err;
53
+ }
54
+ // Hybrid schema unavailable; fallback to legacy global listing.
55
+ }
56
+ }
57
+
11
58
  const products = await listProducts();
12
59
 
13
60
  if (opts.json) {
@@ -0,0 +1,126 @@
1
+ const { normalizeAmazonInput } = require('../url');
2
+ const { resolveCliInput } = require('../input');
3
+ const { runCollectionPipeline } = require('../extractors/pipeline');
4
+ const {
5
+ getProductByAsin,
6
+ upsertProduct,
7
+ insertPrice,
8
+ upsertProductLatestPrice,
9
+ updateProductById,
10
+ upsertUserSubscription,
11
+ } = require('../db');
12
+ const { getUserId } = require('../user-context');
13
+ const { normalizeTier, computeNextScrapeAt } = require('../tiering');
14
+
15
+ module.exports = function (program) {
16
+ program
17
+ .command('subscribe [input...]')
18
+ .description('Subscribe the current user to a tracked product (shared product catalog)')
19
+ .option('--tier <tier>', 'Preferred refresh tier for this subscription: hourly|daily|weekly')
20
+ .option('--json', 'Output as JSON')
21
+ .action(async (inputParts, opts) => {
22
+ const input = await resolveCliInput(inputParts);
23
+ const normalized = await normalizeAmazonInput(input);
24
+ if (!normalized) {
25
+ console.error('Error: Input must be an Amazon product URL or a valid ASIN.');
26
+ process.exit(1);
27
+ }
28
+
29
+ const selectedTier = opts.tier ? normalizeTier(opts.tier) : null;
30
+ if (opts.tier && !selectedTier) {
31
+ console.error('Error: Tier must be one of: hourly, daily, weekly.');
32
+ process.exit(1);
33
+ }
34
+
35
+ const userId = getUserId();
36
+
37
+ try {
38
+ let product = await getProductByAsin(normalized.asin);
39
+ let initial = null;
40
+
41
+ if (!product) {
42
+ initial = await runCollectionPipeline({
43
+ url: normalized.url,
44
+ domain: normalized.domain,
45
+ allowVision: true,
46
+ allowRailwayDomFallback: true,
47
+ });
48
+
49
+ product = await upsertProduct({
50
+ asin: normalized.asin,
51
+ title: initial.pageTitle || `ASIN ${normalized.asin}`,
52
+ url: normalized.url,
53
+ domain: normalized.domain,
54
+ tier: selectedTier || 'daily',
55
+ nextScrapeAt: computeNextScrapeAt(selectedTier || 'daily'),
56
+ });
57
+
58
+ if (initial.price) {
59
+ const priceRecord = await insertPrice({
60
+ productId: product.id,
61
+ price: initial.price.numeric,
62
+ currency: initial.price.currency,
63
+ });
64
+
65
+ await upsertProductLatestPrice({
66
+ productId: product.id,
67
+ price: initial.price.numeric,
68
+ currency: initial.price.currency,
69
+ scrapedAt: priceRecord.scraped_at,
70
+ source: initial.method,
71
+ confidence: initial.confidence,
72
+ }).catch(() => {});
73
+
74
+ await updateProductById(product.id, {
75
+ last_price: initial.price.numeric,
76
+ last_scraped_at: priceRecord.scraped_at,
77
+ consecutive_failures: 0,
78
+ last_error: null,
79
+ next_scrape_at: computeNextScrapeAt(selectedTier || product.tier || 'daily'),
80
+ last_price_change_at: priceRecord.scraped_at,
81
+ }).catch(() => {});
82
+ }
83
+ }
84
+
85
+ if (selectedTier && normalizeTier(product.tier) !== selectedTier) {
86
+ await updateProductById(product.id, {
87
+ tier: selectedTier,
88
+ next_scrape_at: computeNextScrapeAt(selectedTier),
89
+ }).catch(() => {});
90
+ }
91
+
92
+ const subscription = await upsertUserSubscription({
93
+ userId,
94
+ productId: product.id,
95
+ tierPref: selectedTier,
96
+ isActive: true,
97
+ });
98
+
99
+ if (opts.json) {
100
+ console.log(JSON.stringify({
101
+ userId,
102
+ subscriptionId: subscription.id,
103
+ active: subscription.is_active,
104
+ tierPref: subscription.tier_pref,
105
+ product: {
106
+ id: product.id,
107
+ asin: product.asin,
108
+ domain: product.domain,
109
+ url: product.url,
110
+ title: product.title,
111
+ },
112
+ initialPrice: initial?.price?.numeric || null,
113
+ initialCurrency: initial?.price?.currency || null,
114
+ }));
115
+ return;
116
+ }
117
+
118
+ console.log(`Subscribed: ${product.asin} (${product.title})`);
119
+ console.log(`User: ${userId}`);
120
+ console.log(`Tier pref: ${subscription.tier_pref || 'default'}`);
121
+ } catch (err) {
122
+ console.error(`Error: ${err.message}`);
123
+ process.exit(1);
124
+ }
125
+ });
126
+ };
@@ -0,0 +1,61 @@
1
+ const { getUserSubscriptions } = require('../db');
2
+ const { getUserId } = require('../user-context');
3
+ const { formatPrice } = require('../format');
4
+
5
+ module.exports = function (program) {
6
+ program
7
+ .command('subscriptions')
8
+ .description('List current user subscriptions and latest known prices')
9
+ .option('--all', 'Include inactive subscriptions')
10
+ .option('--json', 'Output as JSON')
11
+ .action(async (opts) => {
12
+ const userId = getUserId();
13
+
14
+ try {
15
+ const rows = await getUserSubscriptions(userId, {
16
+ activeOnly: !opts.all,
17
+ });
18
+
19
+ if (opts.json) {
20
+ console.log(JSON.stringify(rows.map((row) => ({
21
+ id: row.id,
22
+ userId: row.user_id,
23
+ active: row.is_active,
24
+ tierPref: row.tier_pref,
25
+ createdAt: row.created_at,
26
+ updatedAt: row.updated_at,
27
+ product: {
28
+ id: row.product?.id || null,
29
+ asin: row.product?.asin || null,
30
+ title: row.product?.title || null,
31
+ url: row.product?.url || null,
32
+ domain: row.product?.domain || null,
33
+ tier: row.product?.tier || null,
34
+ },
35
+ latestPrice: row.latestPrice ? Number(row.latestPrice.price) : null,
36
+ latestCurrency: row.latestPrice?.currency || null,
37
+ latestAt: row.latestPrice?.scraped_at || null,
38
+ }))));
39
+ return;
40
+ }
41
+
42
+ if (rows.length === 0) {
43
+ console.log('No subscriptions found. Use `amaprice subscribe <url-or-asin>` to add one.');
44
+ return;
45
+ }
46
+
47
+ console.log(`Subscriptions for ${userId}:`);
48
+ for (const row of rows) {
49
+ const price = row.latestPrice
50
+ ? formatPrice(Number(row.latestPrice.price), row.latestPrice.currency)
51
+ : 'N/A';
52
+ const subStatus = row.is_active ? 'active' : 'inactive';
53
+ const tier = row.product?.tier || 'daily';
54
+ console.log(` ${row.product?.asin || 'unknown'} ${price} [${subStatus}] [tier=${tier}] ${row.product?.title || ''}`);
55
+ }
56
+ } catch (err) {
57
+ console.error(`Error: ${err.message}`);
58
+ process.exit(1);
59
+ }
60
+ });
61
+ };
@@ -5,12 +5,17 @@ module.exports = function (program) {
5
5
  .command('sync')
6
6
  .description('Run background sync for due products (for cron/worker usage)')
7
7
  .option('--limit <n>', 'Max products to process in one run', '20')
8
+ .option('--orchestrator', 'Use orchestrator queue flow')
9
+ .option('--legacy', 'Force legacy due-products flow')
8
10
  .option('--json', 'Output as JSON')
9
11
  .action(async (opts) => {
10
12
  const limit = Math.max(1, parseInt(opts.limit, 10) || 20);
13
+ const useOrchestrator = opts.legacy
14
+ ? false
15
+ : (opts.orchestrator ? true : undefined);
11
16
 
12
17
  try {
13
- const report = await runDueSync({ limit });
18
+ const report = await runDueSync({ limit, useOrchestrator });
14
19
  if (opts.json) {
15
20
  console.log(JSON.stringify(report));
16
21
  } else {