trackrev 0.1.0 → 0.2.0
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 +874 -42
- package/package.json +14 -2
- package/src/commands/affiliates.js +162 -0
- package/src/commands/analytics.js +128 -0
- package/src/commands/attribution.js +55 -0
- package/src/commands/auth.js +41 -0
- package/src/commands/domains.js +60 -0
- package/src/commands/folders.js +69 -0
- package/src/commands/keys.js +54 -0
- package/src/commands/links.js +203 -0
- package/src/commands/me.js +25 -0
- package/src/commands/money.js +96 -0
- package/src/commands/people.js +94 -0
- package/src/commands/retargeting.js +49 -0
- package/src/commands/revenue.js +86 -0
- package/src/commands/settings.js +64 -0
- package/src/commands/webhooks.js +79 -0
- package/src/index.js +43 -294
- package/src/lib/api.js +87 -0
- package/src/lib/config.js +52 -0
- package/src/lib/output.js +72 -0
- package/src/lib/prompt.js +55 -0
- package/src/registry.d.ts +39 -0
- package/src/registry.js +840 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Link management. Every command here works on the free plan too — the API
|
|
2
|
+
// enforces the same 50-link cap the dashboard does.
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { die, warn, EXIT } from "../lib/api.js";
|
|
5
|
+
import { emit, emitRecord } from "../lib/output.js";
|
|
6
|
+
import { confirm } from "../lib/prompt.js";
|
|
7
|
+
import { intFlag } from "./analytics.js";
|
|
8
|
+
|
|
9
|
+
const MAX_PAGES = 200;
|
|
10
|
+
|
|
11
|
+
const LINK_COLUMNS = [
|
|
12
|
+
{ header: "slug", value: (r) => r.slug },
|
|
13
|
+
{ header: "channel", value: (r) => r.channel },
|
|
14
|
+
{ header: "url", value: (r) => r.url },
|
|
15
|
+
{ header: "destination", value: (r) => r.destination?.url ?? null },
|
|
16
|
+
{ header: "expires", value: (r) => r.expires_at },
|
|
17
|
+
{ header: "password", value: (r) => r.password_protected },
|
|
18
|
+
{ header: "id", value: (r) => r.id },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** Shared flag → API-field mapping for create and update. */
|
|
22
|
+
function settingsFromFlags(flags) {
|
|
23
|
+
const out = {};
|
|
24
|
+
if (flags.slug !== undefined) out.slug = flags.slug;
|
|
25
|
+
if (flags.campaign !== undefined) out.campaign = flags.campaign;
|
|
26
|
+
if (flags.expires !== undefined) out.expires_at = flags.expires;
|
|
27
|
+
if (flags["max-clicks"] !== undefined) {
|
|
28
|
+
out.max_clicks = intFlag("max-clicks", flags["max-clicks"], { max: 1000000000 });
|
|
29
|
+
}
|
|
30
|
+
if (flags["expired-url"] !== undefined) out.expired_redirect_url = flags["expired-url"];
|
|
31
|
+
if (flags.password !== undefined) out.password = flags.password;
|
|
32
|
+
if (flags.retarget !== undefined) {
|
|
33
|
+
if (flags.retarget !== "on" && flags.retarget !== "off") {
|
|
34
|
+
die("--retarget takes on or off.", EXIT.USAGE);
|
|
35
|
+
}
|
|
36
|
+
out.retargeting = flags.retarget === "on";
|
|
37
|
+
}
|
|
38
|
+
if (flags["mobile-url"] !== undefined || flags["desktop-url"] !== undefined) {
|
|
39
|
+
out.targeting = {
|
|
40
|
+
...(flags["mobile-url"] !== undefined && { mobile: flags["mobile-url"] }),
|
|
41
|
+
...(flags["desktop-url"] !== undefined && { desktop: flags["desktop-url"] }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function list({ flags, api }) {
|
|
48
|
+
const pageSize = flags.limit === undefined ? 100 : intFlag("limit", flags.limit, { max: 500 });
|
|
49
|
+
const rows = [];
|
|
50
|
+
let cursor = null;
|
|
51
|
+
let pages = 0;
|
|
52
|
+
|
|
53
|
+
do {
|
|
54
|
+
const params = new URLSearchParams({ view: "records", limit: String(pageSize) });
|
|
55
|
+
if (cursor) params.set("cursor", cursor);
|
|
56
|
+
if (flags.channel) params.set("channel", flags.channel);
|
|
57
|
+
if (flags.q) params.set("q", flags.q);
|
|
58
|
+
|
|
59
|
+
const page = await api.get(`/links?${params}`);
|
|
60
|
+
rows.push(...page.links);
|
|
61
|
+
cursor = page.next_cursor;
|
|
62
|
+
|
|
63
|
+
if (++pages >= MAX_PAGES && cursor) {
|
|
64
|
+
warn(`Stopped after ${MAX_PAGES} pages (${rows.length} links). Narrow it with --channel or --q.`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
} while (flags.all && cursor);
|
|
68
|
+
|
|
69
|
+
emit(LINK_COLUMNS, rows, { links: rows }, {
|
|
70
|
+
json: flags.json,
|
|
71
|
+
empty: "No links yet. Create one with: trackrev links create",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function create({ flags, api }) {
|
|
76
|
+
if (!flags.url || !flags.name) die("--url and --name are required.", EXIT.USAGE);
|
|
77
|
+
if (!flags.smart && !(flags.channel && flags.channel.length)) {
|
|
78
|
+
die("Pass at least one --channel, or --smart for a single Smart Link.", EXIT.USAGE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body = {
|
|
82
|
+
url: flags.url,
|
|
83
|
+
name: flags.name,
|
|
84
|
+
channels: flags.channel ?? [],
|
|
85
|
+
smart: Boolean(flags.smart),
|
|
86
|
+
tags: flags.tag ?? [],
|
|
87
|
+
folder_id: flags.folder ?? null,
|
|
88
|
+
external: Boolean(flags.external),
|
|
89
|
+
...settingsFromFlags(flags),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const result = await api.post("/links", body);
|
|
93
|
+
emit(LINK_COLUMNS, result.links, result, { json: flags.json });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function get({ args, flags, api }) {
|
|
97
|
+
const [key] = args;
|
|
98
|
+
const link = await api.get(`/links/${encodeURIComponent(key)}`);
|
|
99
|
+
emitRecord(
|
|
100
|
+
[
|
|
101
|
+
["id", link.id],
|
|
102
|
+
["slug", link.slug],
|
|
103
|
+
["short_code", link.short_code],
|
|
104
|
+
["url", link.url],
|
|
105
|
+
["channel", link.channel],
|
|
106
|
+
["destination", link.destination?.url],
|
|
107
|
+
["name", link.destination?.name],
|
|
108
|
+
["tags", (link.destination?.tags ?? []).join(", ")],
|
|
109
|
+
["utm_campaign", link.utm_campaign],
|
|
110
|
+
["utm_term", link.utm_term],
|
|
111
|
+
["utm_content", link.utm_content],
|
|
112
|
+
["expires_at", link.expires_at],
|
|
113
|
+
["max_clicks", link.max_clicks],
|
|
114
|
+
["expired_redirect_url", link.expired_redirect_url],
|
|
115
|
+
["password", link.password_protected],
|
|
116
|
+
["retargeting", link.retargeting_enabled],
|
|
117
|
+
["mobile_url", link.targeting?.mobile],
|
|
118
|
+
["desktop_url", link.targeting?.desktop],
|
|
119
|
+
["created_at", link.created_at],
|
|
120
|
+
],
|
|
121
|
+
link,
|
|
122
|
+
{ json: flags.json },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function update({ args, flags, api }) {
|
|
127
|
+
const [id] = args;
|
|
128
|
+
const body = settingsFromFlags(flags);
|
|
129
|
+
if (flags.term !== undefined) body.utm_term = flags.term;
|
|
130
|
+
if (flags.content !== undefined) body.utm_content = flags.content;
|
|
131
|
+
if (flags.campaign !== undefined) {
|
|
132
|
+
// On update the flag means the link's utm_campaign field, not the
|
|
133
|
+
// create-time default for a whole new campaign.
|
|
134
|
+
body.utm_campaign = flags.campaign;
|
|
135
|
+
delete body.campaign;
|
|
136
|
+
}
|
|
137
|
+
if (flags["clear-password"]) body.password = null;
|
|
138
|
+
if (flags["clear-expiry"]) {
|
|
139
|
+
body.expires_at = null;
|
|
140
|
+
body.max_clicks = null;
|
|
141
|
+
body.expired_redirect_url = null;
|
|
142
|
+
}
|
|
143
|
+
if (Object.keys(body).length === 0) {
|
|
144
|
+
die("Nothing to change. Run: trackrev links update --help", EXIT.USAGE);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const link = await api.patch(`/links/${encodeURIComponent(id)}`, body);
|
|
148
|
+
emit(LINK_COLUMNS, [link], link, { json: flags.json });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function remove({ args, flags, api }) {
|
|
152
|
+
const [id] = args;
|
|
153
|
+
// Resolve first so the confirmation names the real link, and so a slug works
|
|
154
|
+
// here even though DELETE takes an id.
|
|
155
|
+
const link = await api.get(`/links/${encodeURIComponent(id)}`);
|
|
156
|
+
const ok = await confirm(
|
|
157
|
+
`Delete ${link.url} (${link.channel})? Clicks and attribution on it are lost.`,
|
|
158
|
+
{ yes: flags.yes },
|
|
159
|
+
);
|
|
160
|
+
if (!ok) die("Cancelled.", EXIT.USAGE);
|
|
161
|
+
|
|
162
|
+
const result = await api.del(`/links/${encodeURIComponent(link.id)}`);
|
|
163
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
164
|
+
else warn(`Deleted ${link.slug}.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function bulk({ flags, api }) {
|
|
168
|
+
if (!flags.file) die("--file is required (use - for stdin).", EXIT.USAGE);
|
|
169
|
+
let csv;
|
|
170
|
+
try {
|
|
171
|
+
csv = readFileSync(flags.file === "-" ? 0 : flags.file, "utf8");
|
|
172
|
+
} catch (err) {
|
|
173
|
+
die(`Could not read ${flags.file}: ${err.message}`, EXIT.USAGE);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = await api.post("/links/bulk", csv);
|
|
177
|
+
emit(
|
|
178
|
+
[
|
|
179
|
+
{ header: "row", value: (r) => r.index + 1, align: "right" },
|
|
180
|
+
{ header: "ok", value: (r) => r.ok },
|
|
181
|
+
{ header: "slug", value: (r) => r.slug ?? null },
|
|
182
|
+
{ header: "error", value: (r) => r.error ?? null },
|
|
183
|
+
],
|
|
184
|
+
result.rows,
|
|
185
|
+
result,
|
|
186
|
+
{ json: flags.json },
|
|
187
|
+
);
|
|
188
|
+
if (!flags.json) warn(`${result.created} created, ${result.errors} failed.`);
|
|
189
|
+
// Nothing landed: make the shell notice.
|
|
190
|
+
if (result.errors > 0 && result.created === 0) process.exitCode = EXIT.FAIL;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function qr({ args, flags, api }) {
|
|
194
|
+
const [key] = args;
|
|
195
|
+
const size = flags.size === undefined ? 512 : intFlag("size", flags.size, { max: 2048 });
|
|
196
|
+
const svg = await api.get(`/links/${encodeURIComponent(key)}/qr?size=${size}`);
|
|
197
|
+
if (flags.out) {
|
|
198
|
+
writeFileSync(flags.out, svg);
|
|
199
|
+
warn(`Wrote ${flags.out}`);
|
|
200
|
+
} else {
|
|
201
|
+
process.stdout.write(svg);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { emitRecord } from "../lib/output.js";
|
|
2
|
+
|
|
3
|
+
const metric = (m) => (m.limit === null ? `${m.used} / unlimited` : `${m.used} / ${m.limit}`);
|
|
4
|
+
|
|
5
|
+
export async function me({ flags, api, auth }) {
|
|
6
|
+
const body = await api.get("/me");
|
|
7
|
+
emitRecord(
|
|
8
|
+
[
|
|
9
|
+
["workspace", `${body.workspace.name} (${body.workspace.slug})`],
|
|
10
|
+
["workspace_id", body.workspace.id],
|
|
11
|
+
["domain", body.workspace.primary_domain],
|
|
12
|
+
["attribution", `${body.workspace.attribution_model} · ${body.workspace.attribution_window}d`],
|
|
13
|
+
["plan", body.plan.covered_by ? `${body.plan.name} (via ${body.plan.covered_by.workspace})` : body.plan.name],
|
|
14
|
+
["status", body.plan.status],
|
|
15
|
+
["links", metric(body.plan.limits.links)],
|
|
16
|
+
["events", metric(body.plan.limits.events)],
|
|
17
|
+
["commission", metric(body.plan.limits.commission)],
|
|
18
|
+
["key", body.key.label ? `${body.key.label} (${body.key.scopes.join(",")})` : body.key.scopes.join(",")],
|
|
19
|
+
["key_source", auth.source],
|
|
20
|
+
["api", auth.apiUrl],
|
|
21
|
+
],
|
|
22
|
+
body,
|
|
23
|
+
{ json: flags.json },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The commission ledger and payout batches.
|
|
2
|
+
import { die, warn, EXIT } from "../lib/api.js";
|
|
3
|
+
import { emit } from "../lib/output.js";
|
|
4
|
+
import { confirm } from "../lib/prompt.js";
|
|
5
|
+
import { intFlag } from "./analytics.js";
|
|
6
|
+
|
|
7
|
+
const COMMISSION_COLUMNS = [
|
|
8
|
+
{ header: "ts", value: (r) => (r.ts ?? "").slice(0, 16).replace("T", " ") },
|
|
9
|
+
{ header: "partner", value: (r) => r.partner_email },
|
|
10
|
+
{ header: "type", value: (r) => r.type },
|
|
11
|
+
{ header: "source", value: (r) => r.source },
|
|
12
|
+
{ header: "level", value: (r) => r.chain_level, align: "right" },
|
|
13
|
+
{ header: "sale", value: (r) => Number(r.amount), align: "right", fixed: 2 },
|
|
14
|
+
{ header: "earned", value: (r) => Number(r.earnings), align: "right", fixed: 2 },
|
|
15
|
+
{ header: "status", value: (r) => r.status },
|
|
16
|
+
{ header: "id", value: (r) => r.id },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const PAYOUT_COLUMNS = [
|
|
20
|
+
{ header: "created", value: (r) => (r.created_at ?? "").slice(0, 10) },
|
|
21
|
+
{ header: "partner", value: (r) => r.partner_email },
|
|
22
|
+
{ header: "amount", value: (r) => Number(r.amount), align: "right", fixed: 2 },
|
|
23
|
+
{ header: "fee", value: (r) => Number(r.fee), align: "right", fixed: 2 },
|
|
24
|
+
{ header: "method", value: (r) => r.method },
|
|
25
|
+
{ header: "status", value: (r) => r.status },
|
|
26
|
+
{ header: "paid", value: (r) => (r.paid_at ?? "").slice(0, 10) || null },
|
|
27
|
+
{ header: "ref", value: (r) => r.manual_reference },
|
|
28
|
+
{ header: "id", value: (r) => r.id },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export async function commissions({ flags, api }) {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
if (flags.limit !== undefined) params.set("limit", String(intFlag("limit", flags.limit, { max: 500 })));
|
|
34
|
+
if (flags.status) params.set("status", flags.status);
|
|
35
|
+
if (flags.partner) params.set("partner_id", flags.partner);
|
|
36
|
+
const body = await api.get(`/commissions?${params}`);
|
|
37
|
+
emit(COMMISSION_COLUMNS, body.commissions, body, {
|
|
38
|
+
json: flags.json,
|
|
39
|
+
empty: "No commissions yet.",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function addCommission({ flags, api }) {
|
|
44
|
+
for (const f of ["program", "partner", "amount", "earnings"]) {
|
|
45
|
+
if (flags[f] === undefined) die(`--${f} is required.`, EXIT.USAGE);
|
|
46
|
+
}
|
|
47
|
+
const amount = Number(flags.amount);
|
|
48
|
+
const earnings = Number(flags.earnings);
|
|
49
|
+
if (!Number.isFinite(amount) || !Number.isFinite(earnings)) {
|
|
50
|
+
die("--amount and --earnings must be numbers.", EXIT.USAGE);
|
|
51
|
+
}
|
|
52
|
+
const c = await api.post("/commissions", {
|
|
53
|
+
program_id: flags.program,
|
|
54
|
+
partner_id: flags.partner,
|
|
55
|
+
amount,
|
|
56
|
+
earnings,
|
|
57
|
+
currency: flags.currency,
|
|
58
|
+
notes: flags.notes,
|
|
59
|
+
});
|
|
60
|
+
emit(COMMISSION_COLUMNS, [c], c, { json: flags.json });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function voidCommission({ args, flags, api }) {
|
|
64
|
+
const [id] = args;
|
|
65
|
+
const status = flags.status ?? "void";
|
|
66
|
+
const ok = await confirm(`Set commission ${id} to "${status}"?`, { yes: flags.yes });
|
|
67
|
+
if (!ok) die("Cancelled.", EXIT.USAGE);
|
|
68
|
+
const c = await api.patch(`/commissions/${encodeURIComponent(id)}`, { status });
|
|
69
|
+
emit(COMMISSION_COLUMNS, [c], c, { json: flags.json });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function payouts({ flags, api }) {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (flags.status) params.set("status", flags.status);
|
|
75
|
+
if (flags.limit !== undefined) params.set("limit", String(intFlag("limit", flags.limit, { max: 500 })));
|
|
76
|
+
const body = await api.get(`/payouts?${params}`);
|
|
77
|
+
emit(PAYOUT_COLUMNS, body.payouts, body, {
|
|
78
|
+
json: flags.json,
|
|
79
|
+
empty: "No payout batches yet. Create one in the dashboard.",
|
|
80
|
+
});
|
|
81
|
+
if (!flags.json && body.payouts.length) {
|
|
82
|
+
warn(`\nopen: ${body.totals.open.toFixed(2)} paid all time: ${body.totals.paid_all_time.toFixed(2)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function markPaid({ args, flags, api }) {
|
|
87
|
+
const [id] = args;
|
|
88
|
+
const ok = await confirm(
|
|
89
|
+
`Mark payout ${id} as paid? This closes out the commissions it covers.`,
|
|
90
|
+
{ yes: flags.yes },
|
|
91
|
+
);
|
|
92
|
+
if (!ok) die("Cancelled.", EXIT.USAGE);
|
|
93
|
+
const p = await api.patch(`/payouts/${encodeURIComponent(id)}`, { reference: flags.reference ?? null });
|
|
94
|
+
emit(PAYOUT_COLUMNS, [p], p, { json: flags.json });
|
|
95
|
+
if (!flags.json) warn("No email was sent — the dashboard sends the 'money is on its way' one.");
|
|
96
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Visitors, orders, and CSV export.
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { warn, EXIT } from "../lib/api.js";
|
|
4
|
+
import { emit, emitRecord } from "../lib/output.js";
|
|
5
|
+
import { intFlag } from "./analytics.js";
|
|
6
|
+
|
|
7
|
+
const MAX_PAGES = 200;
|
|
8
|
+
|
|
9
|
+
/** Walk a cursor-paginated list endpoint, honouring --all. */
|
|
10
|
+
async function paged(api, path, key, flags) {
|
|
11
|
+
const limit = flags.limit === undefined ? 100 : intFlag("limit", flags.limit, { max: 500 });
|
|
12
|
+
const rows = [];
|
|
13
|
+
let cursor = null;
|
|
14
|
+
let pages = 0;
|
|
15
|
+
do {
|
|
16
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
17
|
+
if (cursor) params.set("cursor", cursor);
|
|
18
|
+
if (flags.email) params.set("email", flags.email);
|
|
19
|
+
if (flags.status) params.set("status", flags.status);
|
|
20
|
+
const body = await api.get(`${path}?${params}`);
|
|
21
|
+
rows.push(...body[key]);
|
|
22
|
+
cursor = body.next_cursor;
|
|
23
|
+
if (++pages >= MAX_PAGES && cursor) {
|
|
24
|
+
warn(`Stopped after ${MAX_PAGES} pages (${rows.length} rows).`);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
} while (flags.all && cursor);
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function visitors({ flags, api }) {
|
|
32
|
+
const rows = await paged(api, "/visitors", "visitors", flags);
|
|
33
|
+
emit(
|
|
34
|
+
[
|
|
35
|
+
{ header: "email", value: (r) => r.email },
|
|
36
|
+
{ header: "firstseen", value: (r) => (r.first_seen ?? "").slice(0, 16).replace("T", " ") },
|
|
37
|
+
{ header: "lastseen", value: (r) => (r.last_seen ?? "").slice(0, 16).replace("T", " ") },
|
|
38
|
+
{ header: "id", value: (r) => r.id },
|
|
39
|
+
],
|
|
40
|
+
rows,
|
|
41
|
+
{ visitors: rows },
|
|
42
|
+
{ json: flags.json, empty: "No visitors yet." },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function visitor({ args, flags, api }) {
|
|
47
|
+
const [id] = args;
|
|
48
|
+
const v = await api.get(`/visitors/${encodeURIComponent(id)}`);
|
|
49
|
+
emitRecord(
|
|
50
|
+
[
|
|
51
|
+
["id", v.id],
|
|
52
|
+
["vid", v.vid],
|
|
53
|
+
["email", v.email],
|
|
54
|
+
["first_seen", v.first_seen],
|
|
55
|
+
["last_seen", v.last_seen],
|
|
56
|
+
],
|
|
57
|
+
v,
|
|
58
|
+
{ json: flags.json },
|
|
59
|
+
);
|
|
60
|
+
if (!flags.json) warn(`\nTimeline: trackrev journey ${v.id}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function orders({ flags, api }) {
|
|
64
|
+
const rows = await paged(api, "/orders", "orders", flags);
|
|
65
|
+
emit(
|
|
66
|
+
[
|
|
67
|
+
{ header: "ts", value: (r) => (r.ts ?? "").slice(0, 16).replace("T", " ") },
|
|
68
|
+
{ header: "email", value: (r) => r.email },
|
|
69
|
+
{ header: "amount", value: (r) => (r.amount == null ? null : Number(r.amount)), align: "right", fixed: 2 },
|
|
70
|
+
{ header: "currency", value: (r) => r.currency },
|
|
71
|
+
{ header: "status", value: (r) => r.status },
|
|
72
|
+
{ header: "visitor", value: (r) => r.visitor_id },
|
|
73
|
+
],
|
|
74
|
+
rows,
|
|
75
|
+
{ orders: rows },
|
|
76
|
+
{ json: flags.json, empty: "No orders yet." },
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function exportCsv({ flags, api }) {
|
|
81
|
+
const kind = flags.kind ?? "channels";
|
|
82
|
+
const params = new URLSearchParams({ kind });
|
|
83
|
+
if (flags.from) params.set("from", flags.from);
|
|
84
|
+
if (flags.to) params.set("to", flags.to);
|
|
85
|
+
if (!flags.from && !flags.to) params.set("days", flags.days ?? "30");
|
|
86
|
+
|
|
87
|
+
const csv = await api.get(`/export?${params}`);
|
|
88
|
+
if (flags.out) {
|
|
89
|
+
writeFileSync(flags.out, csv);
|
|
90
|
+
warn(`Wrote ${flags.out}`);
|
|
91
|
+
} else {
|
|
92
|
+
process.stdout.write(csv);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The workspace's ad-pixel library, fired on the consent interstitial.
|
|
2
|
+
import { die, warn, EXIT } from "../lib/api.js";
|
|
3
|
+
import { emit } from "../lib/output.js";
|
|
4
|
+
import { confirm } from "../lib/prompt.js";
|
|
5
|
+
|
|
6
|
+
export async function list({ flags, api }) {
|
|
7
|
+
const body = await api.get("/retargeting");
|
|
8
|
+
if (flags.providers) {
|
|
9
|
+
emit(
|
|
10
|
+
[
|
|
11
|
+
{ header: "provider", value: (r) => r.value },
|
|
12
|
+
{ header: "name", value: (r) => r.label },
|
|
13
|
+
{ header: "id looks like", value: (r) => r.example },
|
|
14
|
+
{ header: "where to find it", value: (r) => r.where },
|
|
15
|
+
],
|
|
16
|
+
body.providers,
|
|
17
|
+
body,
|
|
18
|
+
{ json: flags.json },
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
emit(
|
|
23
|
+
[
|
|
24
|
+
{ header: "provider", value: (r) => r.provider },
|
|
25
|
+
{ header: "pixel_id", value: (r) => r.pixel_id },
|
|
26
|
+
{ header: "added", value: (r) => (r.created_at ?? "").slice(0, 10) },
|
|
27
|
+
],
|
|
28
|
+
body.pixels,
|
|
29
|
+
body,
|
|
30
|
+
{ json: flags.json, empty: "No pixels configured. See: trackrev retargeting list --providers" },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function set({ args, flags, api }) {
|
|
35
|
+
const [provider] = args;
|
|
36
|
+
if (!flags.id) die("--id is required (the pixel/tag id).", EXIT.USAGE);
|
|
37
|
+
const p = await api.put(`/retargeting/${encodeURIComponent(provider)}`, { pixel_id: flags.id });
|
|
38
|
+
if (flags.json) console.log(JSON.stringify(p, null, 2));
|
|
39
|
+
else warn(`Set ${p.provider} → ${p.pixel_id}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function remove({ args, flags, api }) {
|
|
43
|
+
const [provider] = args;
|
|
44
|
+
const ok = await confirm(`Remove the ${provider} pixel?`, { yes: flags.yes });
|
|
45
|
+
if (!ok) die("Cancelled.", EXIT.USAGE);
|
|
46
|
+
const res = await api.del(`/retargeting/${encodeURIComponent(provider)}`);
|
|
47
|
+
if (flags.json) console.log(JSON.stringify(res, null, 2));
|
|
48
|
+
else warn(`Removed ${res.provider}.`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Revenue provider connections and manual sync.
|
|
2
|
+
import { die, warn, EXIT } from "../lib/api.js";
|
|
3
|
+
import { emit } from "../lib/output.js";
|
|
4
|
+
import { confirm } from "../lib/prompt.js";
|
|
5
|
+
|
|
6
|
+
const COLUMNS = [
|
|
7
|
+
{ header: "provider", value: (r) => r.provider },
|
|
8
|
+
{ header: "lastsync", value: (r) => (r.last_sync_at ?? "").slice(0, 16).replace("T", " ") || null },
|
|
9
|
+
{ header: "webhook", value: (r) => r.webhook_configured },
|
|
10
|
+
{ header: "error", value: (r) => r.last_error },
|
|
11
|
+
{ header: "id", value: (r) => r.id },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export async function list({ flags, api }) {
|
|
15
|
+
const body = await api.get("/revenue/connections");
|
|
16
|
+
emit(COLUMNS, body.connections, body, {
|
|
17
|
+
json: flags.json,
|
|
18
|
+
empty: "Nothing connected. See: trackrev revenue providers",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function providers({ flags, api }) {
|
|
23
|
+
const body = await api.get("/revenue/providers");
|
|
24
|
+
emit(
|
|
25
|
+
[
|
|
26
|
+
{ header: "provider", value: (r) => r.value },
|
|
27
|
+
{ header: "name", value: (r) => r.label },
|
|
28
|
+
{ header: "needs", value: (r) => r.fields.map((f) => f.key).join(",") },
|
|
29
|
+
{ header: "sandbox", value: (r) => r.supports_sandbox },
|
|
30
|
+
],
|
|
31
|
+
body.providers,
|
|
32
|
+
body,
|
|
33
|
+
{ json: flags.json },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function connect({ flags, api }) {
|
|
38
|
+
if (!flags.provider) die("--provider is required. See: trackrev revenue providers", EXIT.USAGE);
|
|
39
|
+
if (!flags.field || flags.field.length === 0) {
|
|
40
|
+
die("Pass credentials as --field key=value (repeat per field).", EXIT.USAGE);
|
|
41
|
+
}
|
|
42
|
+
const credentials = {};
|
|
43
|
+
for (const pair of flags.field) {
|
|
44
|
+
const eq = pair.indexOf("=");
|
|
45
|
+
if (eq < 1) die(`--field must be key=value, got "${pair}".`, EXIT.USAGE);
|
|
46
|
+
credentials[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = await api.post("/revenue/connections", {
|
|
50
|
+
provider: flags.provider,
|
|
51
|
+
credentials,
|
|
52
|
+
sandbox: Boolean(flags.sandbox),
|
|
53
|
+
});
|
|
54
|
+
emit(COLUMNS, [body], body, { json: flags.json });
|
|
55
|
+
if (!flags.json) warn("Credentials were verified with a live read before saving.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function sync({ flags, api }) {
|
|
59
|
+
const body = await api.post("/revenue/sync", flags.connection ? { connection_id: flags.connection } : {});
|
|
60
|
+
emit(
|
|
61
|
+
[
|
|
62
|
+
{ header: "provider", value: (r) => r.provider },
|
|
63
|
+
{ header: "imported", value: (r) => r.imported, align: "right" },
|
|
64
|
+
{ header: "attributed", value: (r) => r.attributed, align: "right" },
|
|
65
|
+
{ header: "error", value: (r) => r.error ?? null },
|
|
66
|
+
],
|
|
67
|
+
body.synced,
|
|
68
|
+
body,
|
|
69
|
+
{ json: flags.json },
|
|
70
|
+
);
|
|
71
|
+
if (body.synced.some((s) => s.error)) process.exitCode = EXIT.FAIL;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function disconnect({ args, flags, api }) {
|
|
75
|
+
const [id] = args;
|
|
76
|
+
const conn = await api.get(`/revenue/connections/${encodeURIComponent(id)}`);
|
|
77
|
+
const ok = await confirm(
|
|
78
|
+
`Disconnect ${conn.provider}? New sales stop syncing; orders already imported are kept.`,
|
|
79
|
+
{ yes: flags.yes },
|
|
80
|
+
);
|
|
81
|
+
if (!ok) die("Cancelled.", EXIT.USAGE);
|
|
82
|
+
|
|
83
|
+
const res = await api.del(`/revenue/connections/${encodeURIComponent(conn.id)}`);
|
|
84
|
+
if (flags.json) console.log(JSON.stringify(res, null, 2));
|
|
85
|
+
else warn(`Disconnected ${conn.provider}.`);
|
|
86
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Transactional email switches and white-label branding.
|
|
2
|
+
import { die, warn, EXIT } from "../lib/api.js";
|
|
3
|
+
import { emit, emitRecord } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
export async function notifications({ flags, api }) {
|
|
6
|
+
const body = await api.get("/settings/notifications");
|
|
7
|
+
emit(
|
|
8
|
+
[
|
|
9
|
+
{ header: "key", value: (r) => r.key },
|
|
10
|
+
{ header: "audience", value: (r) => r.audience },
|
|
11
|
+
{ header: "enabled", value: (r) => r.enabled },
|
|
12
|
+
{ header: "default", value: (r) => r.is_default },
|
|
13
|
+
{ header: "fires when", value: (r) => r.description },
|
|
14
|
+
],
|
|
15
|
+
body.notifications,
|
|
16
|
+
body,
|
|
17
|
+
{ json: flags.json },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function setNotification({ args, flags, api }) {
|
|
22
|
+
const [key] = args;
|
|
23
|
+
const on = flags.on === true;
|
|
24
|
+
const off = flags.off === true;
|
|
25
|
+
if (on === off) die("Pass --on or --off.", EXIT.USAGE);
|
|
26
|
+
|
|
27
|
+
const n = await api.patch("/settings/notifications", { key, enabled: on });
|
|
28
|
+
if (flags.json) console.log(JSON.stringify(n, null, 2));
|
|
29
|
+
else warn(`${n.key} is now ${n.enabled ? "on" : "off"}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function branding({ flags, api }) {
|
|
33
|
+
const b = await api.get("/settings/branding");
|
|
34
|
+
emitRecord(
|
|
35
|
+
[
|
|
36
|
+
["brand_logo_url", b.brand_logo_url],
|
|
37
|
+
["brand_color", b.brand_color],
|
|
38
|
+
["affiliate_subdomain", b.affiliate_subdomain],
|
|
39
|
+
],
|
|
40
|
+
b,
|
|
41
|
+
{ json: flags.json },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function setBranding({ flags, api }) {
|
|
46
|
+
const patch = {};
|
|
47
|
+
if (flags.logo !== undefined) patch.logo_url = flags.logo || null;
|
|
48
|
+
if (flags.color !== undefined) patch.color = flags.color || null;
|
|
49
|
+
if (flags["clear-logo"]) patch.logo_url = null;
|
|
50
|
+
if (flags["clear-color"]) patch.color = null;
|
|
51
|
+
if (Object.keys(patch).length === 0) {
|
|
52
|
+
die("Nothing to change. Run: trackrev settings set-branding --help", EXIT.USAGE);
|
|
53
|
+
}
|
|
54
|
+
const b = await api.patch("/settings/branding", patch);
|
|
55
|
+
emitRecord(
|
|
56
|
+
[
|
|
57
|
+
["brand_logo_url", b.brand_logo_url],
|
|
58
|
+
["brand_color", b.brand_color],
|
|
59
|
+
["affiliate_subdomain", b.affiliate_subdomain],
|
|
60
|
+
],
|
|
61
|
+
b,
|
|
62
|
+
{ json: flags.json },
|
|
63
|
+
);
|
|
64
|
+
}
|