@trawlme/cli 3.5.1 → 3.6.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/scraps.js +15 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/tips.d.ts +48 -0
- package/dist/lib/tips.js +129 -0
- package/package.json +1 -1
package/dist/commands/scraps.js
CHANGED
|
@@ -9,6 +9,7 @@ import { classifyError, reportError, UsageError, RefusalError } from '../lib/err
|
|
|
9
9
|
import { confirmDestructive, isInteractive } from '../lib/confirm.js';
|
|
10
10
|
import { formatDoctor, formatAutofix, fetchRunAndFix, pickRun, pickFix } from './doctor.js';
|
|
11
11
|
import { renderPinch, pinchEnabled } from '../lib/pinch.js';
|
|
12
|
+
import { maybeShowReferralTip } from '../lib/tips.js';
|
|
12
13
|
/**
|
|
13
14
|
* Print a usage/validation error consistently: human text to stderr, or a
|
|
14
15
|
* machine envelope on stdout under --json (never both — reportError is the
|
|
@@ -738,6 +739,20 @@ export function attachRunCommand(parent, attachOpts = {}) {
|
|
|
738
739
|
// of what the watched run actually did.
|
|
739
740
|
await pollRunProgress(id, beforeRun, { json: opts.json });
|
|
740
741
|
}
|
|
742
|
+
// #151 — throttled post-run referral tip. Reached only when the
|
|
743
|
+
// launch call above didn't throw (a hard failure rejects it, unwinding
|
|
744
|
+
// straight to index.ts's central catch before this line ever runs)
|
|
745
|
+
// — and, when --watch polled to a terminal state, only when
|
|
746
|
+
// pollRunProgress didn't flag a genuine run failure/timeout/poll-error
|
|
747
|
+
// via a non-zero process.exitCode (#107 review F1). See lib/tips.ts
|
|
748
|
+
// for the throttle/TTY/json/opt-out/server-flag gating itself. #153 —
|
|
749
|
+
// now async (a due tip fetches the server's userFacing flag before
|
|
750
|
+
// printing), so this must be awaited: otherwise commander's
|
|
751
|
+
// parseAsync-driven action would resolve before the tip's own
|
|
752
|
+
// await settles, racing the process's natural exit.
|
|
753
|
+
const runFailed = typeof process.exitCode === 'number' && process.exitCode !== 0;
|
|
754
|
+
if (!runFailed)
|
|
755
|
+
await maybeShowReferralTip({ json: opts.json });
|
|
741
756
|
});
|
|
742
757
|
}
|
|
743
758
|
attachRunCommand(scraps, { hidden: true });
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ interface TrawlConfig {
|
|
|
9
9
|
* only an explicit `false` disables it. No default entry needed since it's
|
|
10
10
|
* optional. (#129) */
|
|
11
11
|
updateNotifier?: boolean;
|
|
12
|
+
/** Opt-out switch for the throttled post-run referral tip (lib/tips.ts).
|
|
13
|
+
* Same optional/absent-means-enabled shape as `updateNotifier` above. */
|
|
14
|
+
tips?: boolean;
|
|
15
|
+
referralTipShownAt: number;
|
|
12
16
|
}
|
|
13
17
|
declare const config: Conf<TrawlConfig>;
|
|
14
18
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure gate: true when the tip should print. Every external signal (json
|
|
3
|
+
* flag, TTY-ness, opt-out, persisted timestamp, current time) is a
|
|
4
|
+
* parameter rather than read internally, so this is trivially testable
|
|
5
|
+
* without mocking process.env/stdout/Date/config. Does NOT cover the
|
|
6
|
+
* server-side `invitations.userFacing` flag (#153) — that check requires a
|
|
7
|
+
* network round-trip, so it's kept out of this pure/sync gate and only
|
|
8
|
+
* consulted once this gate is already true (see
|
|
9
|
+
* isReferralProgramUserFacing + maybeShowReferralTip below).
|
|
10
|
+
*/
|
|
11
|
+
export declare function isReferralTipDue(opts: {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
isTTY: boolean;
|
|
14
|
+
optedOut: boolean;
|
|
15
|
+
lastShownAt: number;
|
|
16
|
+
now: number;
|
|
17
|
+
}): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort: print one subtle line inviting the user to refer a builder,
|
|
20
|
+
* throttled to once per 7 days via a timestamp persisted in the CLI's
|
|
21
|
+
* existing Conf-backed config store (lib/config.ts — same store as
|
|
22
|
+
* apiUrl/token/telemetry/updateNotifier), AND gated on the server's
|
|
23
|
+
* `invitations.userFacing` flag (#153) once that throttle window is open.
|
|
24
|
+
*
|
|
25
|
+
* This only gates on throttle/TTY/`--json`/opt-out/server-flag — it does
|
|
26
|
+
* NOT know whether the underlying command actually succeeded. Callers must
|
|
27
|
+
* only invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
|
|
28
|
+
* action, which also checks pollRunProgress's exit code under `--watch`
|
|
29
|
+
* before calling this). Any state read/write/network failure (corrupt
|
|
30
|
+
* config file, disk error, unreachable server, …) is swallowed silently —
|
|
31
|
+
* a broken tip must never break a run.
|
|
32
|
+
*
|
|
33
|
+
* #153 — the timestamp is now persisted BEFORE the print (#151 originally
|
|
34
|
+
* had it after). Two overlapping calls that both read `due=true` before
|
|
35
|
+
* either has persisted can still both decide to print (a file-based config
|
|
36
|
+
* store can't fully arbitrate that without a lock this feature doesn't
|
|
37
|
+
* warrant) — but persist-before-print closes the narrower, guaranteed-
|
|
38
|
+
* recurring failure mode: previously, if the print succeeded but the
|
|
39
|
+
* subsequent config.set failed (or simply hadn't landed yet when a second
|
|
40
|
+
* call read the same stale timestamp), the tip could print again on every
|
|
41
|
+
* later run forever, since the "already shown" state never advanced. With
|
|
42
|
+
* persist first, a lost race collapses to "tip skipped this time, timestamp
|
|
43
|
+
* still unset, next due window free to retry" — never a guaranteed
|
|
44
|
+
* repeat print.
|
|
45
|
+
*/
|
|
46
|
+
export declare function maybeShowReferralTip(opts?: {
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}): Promise<void>;
|
package/dist/lib/tips.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttled post-run referral tip (#151) — after a successful `trawl run`,
|
|
3
|
+
* occasionally invite the user to refer a builder. Mirrors
|
|
4
|
+
* lib/updateNotifier.ts's shape: self-guarded so a broken tip can never turn
|
|
5
|
+
* a successful command into a failed one. Async since #153 — printing is
|
|
6
|
+
* additionally gated on a server-side flag fetch (see
|
|
7
|
+
* isReferralProgramUserFacing below).
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import config from './config.js';
|
|
11
|
+
import { api } from './api.js';
|
|
12
|
+
/** Max once per 7 days. */
|
|
13
|
+
const TIP_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
/**
|
|
15
|
+
* #153 — checked only once the 7-day throttle window is already open
|
|
16
|
+
* (isReferralTipDue === true), so a run that isn't due for the tip anyway
|
|
17
|
+
* never pays a network round-trip. ≤2s so a slow/dead server can never
|
|
18
|
+
* perceptibly slow a run down.
|
|
19
|
+
*/
|
|
20
|
+
const FLAG_CHECK_TIMEOUT_MS = 2_000;
|
|
21
|
+
const REFERRAL_TIP_TEXT = 'Invite a builder — they get 500, you get 1,000 compute → trawl.me → account → Referrals';
|
|
22
|
+
/**
|
|
23
|
+
* Opted out via env (presence-based, mirrors NO_COLOR / the update
|
|
24
|
+
* notifier's TRAWL_NO_UPDATE_NOTIFIER — see lib/updateNotifier.ts) or an
|
|
25
|
+
* explicit `false` config value. Absent/undefined config key means enabled.
|
|
26
|
+
*/
|
|
27
|
+
function isOptedOut() {
|
|
28
|
+
if (process.env['TRAWL_NO_TIPS'] !== undefined)
|
|
29
|
+
return true;
|
|
30
|
+
return config.get('tips') === false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pure gate: true when the tip should print. Every external signal (json
|
|
34
|
+
* flag, TTY-ness, opt-out, persisted timestamp, current time) is a
|
|
35
|
+
* parameter rather than read internally, so this is trivially testable
|
|
36
|
+
* without mocking process.env/stdout/Date/config. Does NOT cover the
|
|
37
|
+
* server-side `invitations.userFacing` flag (#153) — that check requires a
|
|
38
|
+
* network round-trip, so it's kept out of this pure/sync gate and only
|
|
39
|
+
* consulted once this gate is already true (see
|
|
40
|
+
* isReferralProgramUserFacing + maybeShowReferralTip below).
|
|
41
|
+
*/
|
|
42
|
+
export function isReferralTipDue(opts) {
|
|
43
|
+
if (opts.json)
|
|
44
|
+
return false;
|
|
45
|
+
if (!opts.isTTY)
|
|
46
|
+
return false;
|
|
47
|
+
if (opts.optedOut)
|
|
48
|
+
return false;
|
|
49
|
+
return opts.now - opts.lastShownAt >= TIP_THROTTLE_MS;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Server-side kill switch (#153) — the web nudge and the digest email
|
|
53
|
+
* footer both gate on the same `invitations.userFacing` flag (trawl_node
|
|
54
|
+
* modules/auth/controllers/auth.controller.js `getConfig`,
|
|
55
|
+
* config/defaults/*.config.js), so if it's ever flipped off for abuse
|
|
56
|
+
* mitigation the CLI must stop promising a referral program that isn't
|
|
57
|
+
* actually reachable. `api.publicGet` resolves the request's base URL the
|
|
58
|
+
* exact same way every other CLI API call does (getApiUrl() in
|
|
59
|
+
* lib/config.ts — TRAWL_API_URL env > stored `trawl login --url` >
|
|
60
|
+
* default) and never requires a token, matching this endpoint's public/
|
|
61
|
+
* unauthenticated contract (mirrors `trawl ping`'s use of the same
|
|
62
|
+
* helper for GET /api/health).
|
|
63
|
+
*
|
|
64
|
+
* ANY failure — network error, timeout (FLAG_CHECK_TIMEOUT_MS), or a
|
|
65
|
+
* malformed/unexpected response shape — is indistinguishable from "flag
|
|
66
|
+
* off" here: resolves to false, never throws, never retries. A slow or
|
|
67
|
+
* broken server must never slow down or break a run.
|
|
68
|
+
*/
|
|
69
|
+
async function isReferralProgramUserFacing() {
|
|
70
|
+
try {
|
|
71
|
+
const data = await api.publicGet('/api/auth/config', {
|
|
72
|
+
timeoutMs: FLAG_CHECK_TIMEOUT_MS,
|
|
73
|
+
});
|
|
74
|
+
return data?.invitations?.userFacing === true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Best-effort: print one subtle line inviting the user to refer a builder,
|
|
82
|
+
* throttled to once per 7 days via a timestamp persisted in the CLI's
|
|
83
|
+
* existing Conf-backed config store (lib/config.ts — same store as
|
|
84
|
+
* apiUrl/token/telemetry/updateNotifier), AND gated on the server's
|
|
85
|
+
* `invitations.userFacing` flag (#153) once that throttle window is open.
|
|
86
|
+
*
|
|
87
|
+
* This only gates on throttle/TTY/`--json`/opt-out/server-flag — it does
|
|
88
|
+
* NOT know whether the underlying command actually succeeded. Callers must
|
|
89
|
+
* only invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
|
|
90
|
+
* action, which also checks pollRunProgress's exit code under `--watch`
|
|
91
|
+
* before calling this). Any state read/write/network failure (corrupt
|
|
92
|
+
* config file, disk error, unreachable server, …) is swallowed silently —
|
|
93
|
+
* a broken tip must never break a run.
|
|
94
|
+
*
|
|
95
|
+
* #153 — the timestamp is now persisted BEFORE the print (#151 originally
|
|
96
|
+
* had it after). Two overlapping calls that both read `due=true` before
|
|
97
|
+
* either has persisted can still both decide to print (a file-based config
|
|
98
|
+
* store can't fully arbitrate that without a lock this feature doesn't
|
|
99
|
+
* warrant) — but persist-before-print closes the narrower, guaranteed-
|
|
100
|
+
* recurring failure mode: previously, if the print succeeded but the
|
|
101
|
+
* subsequent config.set failed (or simply hadn't landed yet when a second
|
|
102
|
+
* call read the same stale timestamp), the tip could print again on every
|
|
103
|
+
* later run forever, since the "already shown" state never advanced. With
|
|
104
|
+
* persist first, a lost race collapses to "tip skipped this time, timestamp
|
|
105
|
+
* still unset, next due window free to retry" — never a guaranteed
|
|
106
|
+
* repeat print.
|
|
107
|
+
*/
|
|
108
|
+
export async function maybeShowReferralTip(opts = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const lastShownAt = config.get('referralTipShownAt') || 0;
|
|
111
|
+
const due = isReferralTipDue({
|
|
112
|
+
json: opts.json,
|
|
113
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
114
|
+
optedOut: isOptedOut(),
|
|
115
|
+
lastShownAt,
|
|
116
|
+
now: Date.now(),
|
|
117
|
+
});
|
|
118
|
+
if (!due)
|
|
119
|
+
return;
|
|
120
|
+
const userFacing = await isReferralProgramUserFacing();
|
|
121
|
+
if (!userFacing)
|
|
122
|
+
return;
|
|
123
|
+
config.set('referralTipShownAt', Date.now());
|
|
124
|
+
console.log(chalk.dim(REFERRAL_TIP_TEXT));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// silent — #151/#153: a broken tip must never break a run
|
|
128
|
+
}
|
|
129
|
+
}
|