amaprice 1.0.14 → 1.0.16
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 +49 -7
- package/package.json +1 -1
- package/src/background/service.js +72 -31
package/README.md
CHANGED
|
@@ -29,13 +29,13 @@ npx amaprice price "https://www.amazon.de/dp/B0DZ5P7JD6"
|
|
|
29
29
|
## Quickstart
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
# one-shot lookup
|
|
32
|
+
# one-shot lookup (no subscription)
|
|
33
33
|
amaprice price "https://www.amazon.de/dp/B0DZ5P7JD6"
|
|
34
34
|
|
|
35
|
-
# start tracking
|
|
35
|
+
# start tracking + subscribe current user + auto-start background collector
|
|
36
36
|
amaprice track B0DZ5P7JD6 --tier daily
|
|
37
37
|
|
|
38
|
-
# subscribe
|
|
38
|
+
# or subscribe directly to an existing shared catalog product
|
|
39
39
|
amaprice subscribe B0DZ5P7JD6
|
|
40
40
|
|
|
41
41
|
# show history
|
|
@@ -43,6 +43,16 @@ amaprice history B0DZ5P7JD6 --limit 30
|
|
|
43
43
|
|
|
44
44
|
# list tracked products
|
|
45
45
|
amaprice list
|
|
46
|
+
|
|
47
|
+
# list all subscriptions for current user (including paused)
|
|
48
|
+
amaprice subscriptions --all
|
|
49
|
+
|
|
50
|
+
# stop one product subscription for current user
|
|
51
|
+
amaprice unsubscribe B0DZ5P7JD6
|
|
52
|
+
|
|
53
|
+
# stop/start background collector service
|
|
54
|
+
amaprice background off
|
|
55
|
+
amaprice background on
|
|
46
56
|
```
|
|
47
57
|
|
|
48
58
|
## Input Modes
|
|
@@ -59,19 +69,47 @@ Short links from Amazon apps (for example `amzn.eu`, `amzn.to`, `a.co`) are acce
|
|
|
59
69
|
| Command | Description |
|
|
60
70
|
|---|---|
|
|
61
71
|
| `amaprice [url\|asin]` | Shortcut for `amaprice price [url\|asin]` |
|
|
62
|
-
| `amaprice price [url\|asin]` | One-shot lookup and silent history insert |
|
|
63
|
-
| `amaprice track [url\|asin]` | Track product + current
|
|
72
|
+
| `amaprice price [url\|asin]` | One-shot lookup (no subscription) and silent history insert |
|
|
73
|
+
| `amaprice track [url\|asin]` | Track product + subscribe current user + auto-start background (`--tier`, `--manual-tier`, `--auto-tier`, `--inactive`) |
|
|
64
74
|
| `amaprice subscribe [url\|asin]` | Subscribe current user to shared product catalog entry |
|
|
65
75
|
| `amaprice unsubscribe <url\|asin>` | Disable current user subscription |
|
|
66
76
|
| `amaprice subscriptions` | List user subscriptions with latest known prices |
|
|
67
77
|
| `amaprice history <url\|asin>` | Show history (`--limit N`) |
|
|
68
|
-
| `amaprice list` | List
|
|
78
|
+
| `amaprice list` | List current user subscriptions + latest price (default view) |
|
|
79
|
+
| `amaprice list --global` | List global shared catalog tracked products |
|
|
69
80
|
| `amaprice sync --limit <n>` | Run background sync for due products |
|
|
70
81
|
| `amaprice background <on\|off\|status>` | Manage true background collector service |
|
|
71
82
|
| `amaprice tier <url\|asin> <hourly\|daily\|weekly>` | Set tier/status (`--auto`, `--manual`, `--activate`, `--deactivate`) |
|
|
72
83
|
|
|
73
84
|
All commands support `--json`.
|
|
74
85
|
|
|
86
|
+
## Most Common User Flows
|
|
87
|
+
|
|
88
|
+
### One-time Price Check (No Subscription)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
amaprice price B0DZ5P7JD6
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This returns the current price immediately and does not create a user subscription.
|
|
95
|
+
|
|
96
|
+
### Subscribe + Run in Background
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
amaprice track B0DZ5P7JD6
|
|
100
|
+
amaprice background status --json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`track` (and `subscribe`) auto-starts the background collector on macOS (`launchd`).
|
|
104
|
+
You can close your terminal after this; the service keeps running.
|
|
105
|
+
|
|
106
|
+
### Stop Product + Stop Background Service
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
amaprice unsubscribe B0DZ5P7JD6
|
|
110
|
+
amaprice background off
|
|
111
|
+
```
|
|
112
|
+
|
|
75
113
|
## Background Service (Auto)
|
|
76
114
|
|
|
77
115
|
`track` and `subscribe` automatically ensure a true background collector service is running.
|
|
@@ -145,7 +183,11 @@ Run this SQL in Supabase SQL Editor:
|
|
|
145
183
|
|
|
146
184
|
`supabase/migrations/20260220_add_price_history_currency.sql`
|
|
147
185
|
|
|
148
|
-
|
|
186
|
+
`supabase/migrations/20260222_add_hybrid_orchestration.sql`
|
|
187
|
+
|
|
188
|
+
`supabase/migrations/20260223_enforce_collector_first_claiming.sql`
|
|
189
|
+
|
|
190
|
+
These migrations add tier fields, indexes, telemetry, worker health rollups, `price_history.currency`, collector orchestration tables/functions, and strict collector-first claim policy.
|
|
149
191
|
|
|
150
192
|
Note: these files are additive migrations and expect existing `products` + `price_history` tables.
|
|
151
193
|
|
package/package.json
CHANGED
|
@@ -114,15 +114,17 @@ function isLaunchdSupported(platform = process.platform) {
|
|
|
114
114
|
return platform === 'darwin';
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function
|
|
117
|
+
function getLaunchdDomains() {
|
|
118
118
|
if (typeof process.getuid !== 'function') {
|
|
119
119
|
throw new Error('launchd requires a POSIX uid');
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
const uid = process.getuid();
|
|
122
|
+
return [`gui/${uid}`, `user/${uid}`];
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
function buildServiceTarget(label) {
|
|
125
|
-
|
|
125
|
+
function buildServiceTarget(label, domain = null) {
|
|
126
|
+
const safeDomain = domain || getLaunchdDomains()[0];
|
|
127
|
+
return `${safeDomain}/${label}`;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
async function runLaunchctl(args, { allowFailure = false } = {}) {
|
|
@@ -233,24 +235,38 @@ async function getLaunchdServiceStatus({ label }) {
|
|
|
233
235
|
backend: 'launchd',
|
|
234
236
|
label,
|
|
235
237
|
plistPath,
|
|
238
|
+
domain: null,
|
|
236
239
|
installed: false,
|
|
237
240
|
loaded: false,
|
|
238
241
|
running: false,
|
|
239
242
|
};
|
|
240
243
|
}
|
|
241
244
|
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
const domains = getLaunchdDomains();
|
|
246
|
+
for (const domain of domains) {
|
|
247
|
+
const print = await runLaunchctl(['print', buildServiceTarget(label, domain)], { allowFailure: true });
|
|
248
|
+
if (!print.ok) continue;
|
|
249
|
+
const output = `${print.stdout}\n${print.stderr}`;
|
|
250
|
+
const running = /state = running/i.test(output) || /pid = \d+/i.test(output);
|
|
251
|
+
return {
|
|
252
|
+
backend: 'launchd',
|
|
253
|
+
label,
|
|
254
|
+
plistPath,
|
|
255
|
+
domain,
|
|
256
|
+
installed: true,
|
|
257
|
+
loaded: true,
|
|
258
|
+
running,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
246
261
|
|
|
247
262
|
return {
|
|
248
263
|
backend: 'launchd',
|
|
249
264
|
label,
|
|
250
265
|
plistPath,
|
|
266
|
+
domain: domains[0] || null,
|
|
251
267
|
installed: true,
|
|
252
|
-
loaded,
|
|
253
|
-
running,
|
|
268
|
+
loaded: false,
|
|
269
|
+
running: false,
|
|
254
270
|
};
|
|
255
271
|
}
|
|
256
272
|
|
|
@@ -263,7 +279,6 @@ async function enableLaunchdService({
|
|
|
263
279
|
const plistPath = getLaunchdPlistPath(label);
|
|
264
280
|
const logPath = getDaemonLogPath();
|
|
265
281
|
const daemonEntry = getDaemonEntryPath();
|
|
266
|
-
const target = buildServiceTarget(label);
|
|
267
282
|
|
|
268
283
|
await fs.mkdir(path.dirname(plistPath), { recursive: true });
|
|
269
284
|
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
@@ -284,33 +299,53 @@ async function enableLaunchdService({
|
|
|
284
299
|
});
|
|
285
300
|
await fs.writeFile(plistPath, plist, 'utf8');
|
|
286
301
|
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
const domains = getLaunchdDomains();
|
|
303
|
+
const errors = [];
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
await runLaunchctl(['
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!bootstrap.ok && !isAlreadyLoadedError(bootstrap)) {
|
|
298
|
-
throw new Error(`Could not bootstrap launchd service: ${bootstrap.stderr || bootstrap.stdout || 'unknown error'}`);
|
|
299
|
-
}
|
|
305
|
+
for (const domain of domains) {
|
|
306
|
+
const domainTarget = buildServiceTarget(label, domain);
|
|
307
|
+
|
|
308
|
+
// Clean stale state first, then bootstrap fresh.
|
|
309
|
+
await runLaunchctl(['bootout', domainTarget], { allowFailure: true });
|
|
310
|
+
await runLaunchctl(['disable', domainTarget], { allowFailure: true });
|
|
311
|
+
await runLaunchctl(['enable', domainTarget], { allowFailure: true });
|
|
300
312
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
313
|
+
let bootstrap = await runLaunchctl(['bootstrap', domain, plistPath], { allowFailure: true });
|
|
314
|
+
if (!bootstrap.ok && !isAlreadyLoadedError(bootstrap)) {
|
|
315
|
+
await runLaunchctl(['bootout', domainTarget], { allowFailure: true });
|
|
316
|
+
await runLaunchctl(['enable', domainTarget], { allowFailure: true });
|
|
317
|
+
bootstrap = await runLaunchctl(['bootstrap', domain, plistPath], { allowFailure: true });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!bootstrap.ok && !isAlreadyLoadedError(bootstrap)) {
|
|
321
|
+
errors.push(`${domain}: ${bootstrap.stderr || bootstrap.stdout || 'unknown error'}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await runLaunchctl(['enable', domainTarget], { allowFailure: true });
|
|
326
|
+
const kick = await runLaunchctl(['kickstart', '-k', domainTarget], { allowFailure: true });
|
|
327
|
+
if (!kick.ok) {
|
|
328
|
+
await runLaunchctl(['start', label], { allowFailure: true });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const status = await getLaunchdServiceStatus({ label });
|
|
332
|
+
if (status.loaded) {
|
|
333
|
+
return status;
|
|
334
|
+
}
|
|
335
|
+
errors.push(`${domain}: bootstrapped but service not loaded`);
|
|
305
336
|
}
|
|
306
337
|
|
|
307
|
-
|
|
338
|
+
const msg = errors.length > 0 ? errors.join(' | ') : 'unknown error';
|
|
339
|
+
throw new Error(`Could not bootstrap launchd service: ${msg}`);
|
|
308
340
|
}
|
|
309
341
|
|
|
310
342
|
async function disableLaunchdService({ label }) {
|
|
311
343
|
const plistPath = getLaunchdPlistPath(label);
|
|
312
|
-
|
|
313
|
-
|
|
344
|
+
for (const domain of getLaunchdDomains()) {
|
|
345
|
+
const target = buildServiceTarget(label, domain);
|
|
346
|
+
await runLaunchctl(['bootout', target], { allowFailure: true });
|
|
347
|
+
await runLaunchctl(['disable', target], { allowFailure: true });
|
|
348
|
+
}
|
|
314
349
|
try {
|
|
315
350
|
await fs.unlink(plistPath);
|
|
316
351
|
} catch (err) {
|
|
@@ -342,7 +377,7 @@ async function ensureBackgroundOn({
|
|
|
342
377
|
const { collector, statePath } = await ensureCollectorEnabled({
|
|
343
378
|
userId,
|
|
344
379
|
collectorName,
|
|
345
|
-
status: '
|
|
380
|
+
status: 'paused',
|
|
346
381
|
});
|
|
347
382
|
|
|
348
383
|
const service = await enableLaunchdService({
|
|
@@ -352,6 +387,10 @@ async function ensureBackgroundOn({
|
|
|
352
387
|
userId,
|
|
353
388
|
});
|
|
354
389
|
|
|
390
|
+
if (!service.loaded) {
|
|
391
|
+
throw new Error('launchd service did not load');
|
|
392
|
+
}
|
|
393
|
+
|
|
355
394
|
await heartbeatCollector({
|
|
356
395
|
collectorId: collector.id,
|
|
357
396
|
status: 'active',
|
|
@@ -510,6 +549,8 @@ module.exports.__test = {
|
|
|
510
549
|
resolvePollSeconds,
|
|
511
550
|
getLaunchdLabel,
|
|
512
551
|
getLaunchdPlistPath,
|
|
552
|
+
getLaunchdDomains,
|
|
553
|
+
buildServiceTarget,
|
|
513
554
|
renderLaunchdPlist,
|
|
514
555
|
isLaunchdSupported,
|
|
515
556
|
};
|