@trawlme/cli 3.6.0 → 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.
@@ -745,10 +745,14 @@ export function attachRunCommand(parent, attachOpts = {}) {
745
745
  // — and, when --watch polled to a terminal state, only when
746
746
  // pollRunProgress didn't flag a genuine run failure/timeout/poll-error
747
747
  // via a non-zero process.exitCode (#107 review F1). See lib/tips.ts
748
- // for the throttle/TTY/json/opt-out gating itself.
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.
749
753
  const runFailed = typeof process.exitCode === 'number' && process.exitCode !== 0;
750
754
  if (!runFailed)
751
- maybeShowReferralTip({ json: opts.json });
755
+ await maybeShowReferralTip({ json: opts.json });
752
756
  });
753
757
  }
754
758
  attachRunCommand(scraps, { hidden: true });
@@ -2,7 +2,11 @@
2
2
  * Pure gate: true when the tip should print. Every external signal (json
3
3
  * flag, TTY-ness, opt-out, persisted timestamp, current time) is a
4
4
  * parameter rather than read internally, so this is trivially testable
5
- * without mocking process.env/stdout/Date/config.
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).
6
10
  */
7
11
  export declare function isReferralTipDue(opts: {
8
12
  json?: boolean;
@@ -15,16 +19,30 @@ export declare function isReferralTipDue(opts: {
15
19
  * Best-effort: print one subtle line inviting the user to refer a builder,
16
20
  * throttled to once per 7 days via a timestamp persisted in the CLI's
17
21
  * existing Conf-backed config store (lib/config.ts — same store as
18
- * apiUrl/token/telemetry/updateNotifier).
22
+ * apiUrl/token/telemetry/updateNotifier), AND gated on the server's
23
+ * `invitations.userFacing` flag (#153) once that throttle window is open.
19
24
  *
20
- * This only gates on throttle/TTY/`--json`/opt-out — it does NOT know
21
- * whether the underlying command actually succeeded. Callers must only
22
- * invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
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
23
28
  * action, which also checks pollRunProgress's exit code under `--watch`
24
- * before calling this). Any state read/write failure (corrupt config file,
25
- * disk error, …) is swallowed silently — a broken tip must never break a
26
- * run.
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.
27
45
  */
28
46
  export declare function maybeShowReferralTip(opts?: {
29
47
  json?: boolean;
30
- }): void;
48
+ }): Promise<void>;
package/dist/lib/tips.js CHANGED
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Throttled post-run referral tip (#151) — after a successful `trawl run`,
3
3
  * occasionally invite the user to refer a builder. Mirrors
4
- * lib/updateNotifier.ts's shape: synchronous, instant, self-guarded so a
5
- * broken tip can never turn a successful command into a failed one.
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).
6
8
  */
7
9
  import chalk from 'chalk';
8
10
  import config from './config.js';
11
+ import { api } from './api.js';
9
12
  /** Max once per 7 days. */
10
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;
11
21
  const REFERRAL_TIP_TEXT = 'Invite a builder — they get 500, you get 1,000 compute → trawl.me → account → Referrals';
12
22
  /**
13
23
  * Opted out via env (presence-based, mirrors NO_COLOR / the update
@@ -23,7 +33,11 @@ function isOptedOut() {
23
33
  * Pure gate: true when the tip should print. Every external signal (json
24
34
  * flag, TTY-ness, opt-out, persisted timestamp, current time) is a
25
35
  * parameter rather than read internally, so this is trivially testable
26
- * without mocking process.env/stdout/Date/config.
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).
27
41
  */
28
42
  export function isReferralTipDue(opts) {
29
43
  if (opts.json)
@@ -34,21 +48,64 @@ export function isReferralTipDue(opts) {
34
48
  return false;
35
49
  return opts.now - opts.lastShownAt >= TIP_THROTTLE_MS;
36
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
+ }
37
80
  /**
38
81
  * Best-effort: print one subtle line inviting the user to refer a builder,
39
82
  * throttled to once per 7 days via a timestamp persisted in the CLI's
40
83
  * existing Conf-backed config store (lib/config.ts — same store as
41
- * apiUrl/token/telemetry/updateNotifier).
84
+ * apiUrl/token/telemetry/updateNotifier), AND gated on the server's
85
+ * `invitations.userFacing` flag (#153) once that throttle window is open.
42
86
  *
43
- * This only gates on throttle/TTY/`--json`/opt-out — it does NOT know
44
- * whether the underlying command actually succeeded. Callers must only
45
- * invoke it on a genuine success (see commands/scraps.ts `run <id>`'s
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
46
90
  * action, which also checks pollRunProgress's exit code under `--watch`
47
- * before calling this). Any state read/write failure (corrupt config file,
48
- * disk error, …) is swallowed silently — a broken tip must never break a
49
- * run.
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.
50
107
  */
51
- export function maybeShowReferralTip(opts = {}) {
108
+ export async function maybeShowReferralTip(opts = {}) {
52
109
  try {
53
110
  const lastShownAt = config.get('referralTipShownAt') || 0;
54
111
  const due = isReferralTipDue({
@@ -60,10 +117,13 @@ export function maybeShowReferralTip(opts = {}) {
60
117
  });
61
118
  if (!due)
62
119
  return;
63
- console.log(chalk.dim(REFERRAL_TIP_TEXT));
120
+ const userFacing = await isReferralProgramUserFacing();
121
+ if (!userFacing)
122
+ return;
64
123
  config.set('referralTipShownAt', Date.now());
124
+ console.log(chalk.dim(REFERRAL_TIP_TEXT));
65
125
  }
66
126
  catch {
67
- // silent — #151: a broken tip must never break a run
127
+ // silent — #151/#153: a broken tip must never break a run
68
128
  }
69
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {