landingboost 0.1.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 +21 -0
- package/bin/landingboost.mjs +301 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# LandingBoost CLI
|
|
2
|
+
|
|
3
|
+
Run LandingBoost scans from any terminal.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
landingboost login --api-key lpapi_...
|
|
7
|
+
landingboost scan https://example.com
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The API key is saved locally at `~/.landingboost/config.json` with file mode `0600`.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
landingboost login --api-key lpapi_...
|
|
16
|
+
landingboost scan https://example.com
|
|
17
|
+
landingboost scan https://example.com --json
|
|
18
|
+
landingboost status
|
|
19
|
+
landingboost logout
|
|
20
|
+
```
|
|
21
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ENDPOINT = "https://skzaijeszkmghxaoizls.supabase.co/functions/v1/lp-score";
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), ".landingboost");
|
|
9
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
console.log(`LandingBoost CLI
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
landingboost login --api-key lpapi_...
|
|
16
|
+
landingboost scan https://example.com
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
login Save your API key locally.
|
|
20
|
+
scan Run a landing page scan.
|
|
21
|
+
status Show saved CLI configuration.
|
|
22
|
+
logout Remove the saved API key.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--api-key <key> API key. Overrides saved key and LANDINGBOOST_API_KEY.
|
|
26
|
+
--endpoint <url> Override the lp-score endpoint.
|
|
27
|
+
--json Print the compact API response as JSON.
|
|
28
|
+
--help Show this help.
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const args = {
|
|
34
|
+
command: "",
|
|
35
|
+
url: "",
|
|
36
|
+
apiKey: "",
|
|
37
|
+
endpoint: "",
|
|
38
|
+
json: false,
|
|
39
|
+
help: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const first = argv[0] || "";
|
|
43
|
+
if (["login", "scan", "status", "logout", "help"].includes(first)) {
|
|
44
|
+
args.command = first;
|
|
45
|
+
argv = argv.slice(1);
|
|
46
|
+
} else {
|
|
47
|
+
args.command = first === "--help" || first === "-h" ? "help" : "scan";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg === "--help" || arg === "-h") {
|
|
53
|
+
args.help = true;
|
|
54
|
+
} else if (arg === "--json") {
|
|
55
|
+
args.json = true;
|
|
56
|
+
} else if (arg === "--api-key") {
|
|
57
|
+
args.apiKey = argv[i + 1] || "";
|
|
58
|
+
i += 1;
|
|
59
|
+
} else if (arg === "--endpoint") {
|
|
60
|
+
args.endpoint = argv[i + 1] || "";
|
|
61
|
+
i += 1;
|
|
62
|
+
} else if (!arg.startsWith("-") && !args.url) {
|
|
63
|
+
args.url = arg;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeUrl(input) {
|
|
71
|
+
const trimmed = String(input || "").trim();
|
|
72
|
+
if (!trimmed) return "";
|
|
73
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
74
|
+
return `https://${trimmed}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertValidUrl(url, label = "URL") {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = new URL(url);
|
|
80
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("unsupported protocol");
|
|
81
|
+
return parsed.toString();
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`Invalid ${label}: ${url}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveEndpoint(value) {
|
|
88
|
+
return (
|
|
89
|
+
value ||
|
|
90
|
+
process.env.LANDINGBOOST_API_ENDPOINT ||
|
|
91
|
+
(process.env.LANDINGBOOST_SUPABASE_URL
|
|
92
|
+
? `${process.env.LANDINGBOOST_SUPABASE_URL.replace(/\/$/, "")}/functions/v1/lp-score`
|
|
93
|
+
: DEFAULT_ENDPOINT)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function maskKey(key) {
|
|
98
|
+
if (!key) return "";
|
|
99
|
+
return `${key.slice(0, 12)}...${key.slice(-4)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateApiKeyShape(apiKey) {
|
|
103
|
+
if (!apiKey || !apiKey.startsWith("lpapi_")) {
|
|
104
|
+
throw new Error("API key must start with lpapi_.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readConfig() {
|
|
109
|
+
try {
|
|
110
|
+
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
113
|
+
} catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function writeConfig(config) {
|
|
119
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
120
|
+
await writeFile(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function removeConfig() {
|
|
124
|
+
await rm(CONFIG_FILE, { force: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function firstText(...values) {
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scoreLine(label, value) {
|
|
135
|
+
return `${label.padEnd(9)} ${String(value ?? "n/a").padStart(3)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatText(result) {
|
|
139
|
+
const scores = result.scores || {};
|
|
140
|
+
const edit = result.one_edit || {};
|
|
141
|
+
const usage = result.usage || {};
|
|
142
|
+
const refs = result.benchmark_summary?.references || [];
|
|
143
|
+
const scanUrl = result.scanId ? `https://landingboost.app/?scanId=${result.scanId}` : "";
|
|
144
|
+
const title = firstText(edit.title, edit.headline, "Next edit");
|
|
145
|
+
const instruction = firstText(edit.instruction, edit.change_this, edit.summary);
|
|
146
|
+
const where = firstText(edit.where, edit.location);
|
|
147
|
+
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push("LandingBoost scan");
|
|
150
|
+
lines.push(`${result.final_url || result.url || ""}`);
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push(`Score: ${scores.overall_100 ?? "n/a"}/100`);
|
|
153
|
+
if (result.weakest_axis) lines.push(`Bottleneck: ${result.weakest_axis}`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("One Edit");
|
|
156
|
+
lines.push(` ${title}`);
|
|
157
|
+
if (instruction) lines.push(` ${instruction}`);
|
|
158
|
+
if (where) lines.push(` Where: ${where}`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("Scores");
|
|
161
|
+
lines.push(` ${scoreLine("Clarity", scores.clarity_100)}`);
|
|
162
|
+
lines.push(` ${scoreLine("Relevance", scores.relevance_100)}`);
|
|
163
|
+
lines.push(` ${scoreLine("Trust", scores.trust_100)}`);
|
|
164
|
+
lines.push(` ${scoreLine("Action", scores.action_100)}`);
|
|
165
|
+
|
|
166
|
+
const referenceNames = refs.map((ref) => ref.name).filter(Boolean).slice(0, 3);
|
|
167
|
+
if (referenceNames.length) {
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push(`Matched: ${referenceNames.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof usage.api_calls_used === "number" || typeof usage.api_calls_remaining === "number") {
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push(
|
|
175
|
+
`Usage: ${usage.api_calls_used ?? "?"}/${usage.api_calls_limit ?? "?"} API/CLI runs used, ${usage.api_calls_remaining ?? "?"} left this month`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (scanUrl) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push(`Open: ${scanUrl}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function commandLogin(args) {
|
|
188
|
+
const apiKey = args.apiKey || process.env.LANDINGBOOST_API_KEY || "";
|
|
189
|
+
validateApiKeyShape(apiKey);
|
|
190
|
+
const endpoint = assertValidUrl(resolveEndpoint(args.endpoint), "endpoint");
|
|
191
|
+
|
|
192
|
+
await writeConfig({
|
|
193
|
+
apiKey,
|
|
194
|
+
endpoint,
|
|
195
|
+
savedAt: new Date().toISOString(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
console.log(`Saved LandingBoost API key: ${maskKey(apiKey)}`);
|
|
199
|
+
console.log(`Config: ${CONFIG_FILE}`);
|
|
200
|
+
console.log("Run: landingboost scan https://example.com");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function commandStatus() {
|
|
204
|
+
const config = await readConfig();
|
|
205
|
+
if (!config.apiKey) {
|
|
206
|
+
console.log("No saved LandingBoost API key.");
|
|
207
|
+
console.log("Run: landingboost login --api-key lpapi_...");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log("LandingBoost CLI is configured.");
|
|
212
|
+
console.log(`Key: ${maskKey(config.apiKey)}`);
|
|
213
|
+
console.log(`Endpoint: ${config.endpoint || DEFAULT_ENDPOINT}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function commandLogout() {
|
|
217
|
+
await removeConfig();
|
|
218
|
+
console.log("Removed saved LandingBoost API key.");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function commandScan(args) {
|
|
222
|
+
if (!args.url) {
|
|
223
|
+
console.error("Missing URL.");
|
|
224
|
+
console.error("Run: landingboost scan https://example.com");
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const config = await readConfig();
|
|
230
|
+
const apiKey = args.apiKey || process.env.LANDINGBOOST_API_KEY || config.apiKey || "";
|
|
231
|
+
if (!apiKey) {
|
|
232
|
+
console.error("Missing API key.");
|
|
233
|
+
console.error("Run once: landingboost login --api-key lpapi_...");
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
validateApiKeyShape(apiKey);
|
|
238
|
+
|
|
239
|
+
const lpUrl = assertValidUrl(normalizeUrl(args.url));
|
|
240
|
+
const endpoint = assertValidUrl(resolveEndpoint(args.endpoint || config.endpoint), "endpoint");
|
|
241
|
+
|
|
242
|
+
const response = await fetch(endpoint, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
Authorization: `Bearer ${apiKey}`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
lpUrl,
|
|
250
|
+
responseMode: "compact",
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const rawText = await response.text();
|
|
255
|
+
let result;
|
|
256
|
+
try {
|
|
257
|
+
result = JSON.parse(rawText);
|
|
258
|
+
} catch {
|
|
259
|
+
throw new Error(`Non-JSON response from LandingBoost (${response.status}): ${rawText.slice(0, 500)}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!response.ok || result?.ok === false) {
|
|
263
|
+
const message = result?.error || result?.message || `LandingBoost API failed with ${response.status}`;
|
|
264
|
+
if (args.json) {
|
|
265
|
+
console.log(JSON.stringify(result, null, 2));
|
|
266
|
+
} else {
|
|
267
|
+
console.error(message);
|
|
268
|
+
if (result?.code) console.error(`Code: ${result.code}`);
|
|
269
|
+
}
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (args.json) {
|
|
275
|
+
console.log(JSON.stringify(result, null, 2));
|
|
276
|
+
} else {
|
|
277
|
+
console.log(formatText(result));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function main() {
|
|
282
|
+
const args = parseArgs(process.argv.slice(2));
|
|
283
|
+
|
|
284
|
+
if (args.help || args.command === "help") {
|
|
285
|
+
printHelp();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (args.command === "login") return commandLogin(args);
|
|
290
|
+
if (args.command === "status") return commandStatus(args);
|
|
291
|
+
if (args.command === "logout") return commandLogout(args);
|
|
292
|
+
if (args.command === "scan") return commandScan(args);
|
|
293
|
+
|
|
294
|
+
printHelp();
|
|
295
|
+
process.exitCode = 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
main().catch((error) => {
|
|
299
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "landingboost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LandingBoost command line scanner.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"landingboost": "bin/landingboost.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/landingboost.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED"
|
|
17
|
+
}
|