@scrapecreators/cli 1.0.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 +241 -0
- package/api-config/apis.js +14559 -0
- package/api-config/instagram-apis.js +5753 -0
- package/api-config/tiktok-apis.js +17388 -0
- package/api-config/tiktok-shop-apis.js +1053 -0
- package/bin/check-lockfile-sync.js +42 -0
- package/bin/scrapecreators.js +4 -0
- package/package.json +48 -0
- package/src/api-client.js +71 -0
- package/src/auth.js +27 -0
- package/src/cli.js +78 -0
- package/src/command-registry.js +156 -0
- package/src/commands/agent.js +311 -0
- package/src/commands/api.js +49 -0
- package/src/commands/auth.js +69 -0
- package/src/commands/balance.js +40 -0
- package/src/commands/config.js +65 -0
- package/src/commands/list.js +52 -0
- package/src/config.js +23 -0
- package/src/interactive.js +148 -0
- package/src/output.js +400 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import * as prompts from "@clack/prompts";
|
|
20
|
+
import { resolveApiKey, storeApiKey } from "../auth.js";
|
|
21
|
+
import { callApi } from "../api-client.js";
|
|
22
|
+
|
|
23
|
+
const MCP_URL = "https://api.scrapecreators.com/mcp";
|
|
24
|
+
|
|
25
|
+
const TARGETS = {
|
|
26
|
+
cursor: {
|
|
27
|
+
name: "Cursor",
|
|
28
|
+
configPath: () => resolve(process.cwd(), ".cursor", "mcp.json"),
|
|
29
|
+
basePath: () => process.cwd(),
|
|
30
|
+
buildConfig: (apiKey) => ({
|
|
31
|
+
mcpServers: {
|
|
32
|
+
scrapecreators: {
|
|
33
|
+
url: MCP_URL,
|
|
34
|
+
headers: { "x-api-key": apiKey },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
claude: {
|
|
40
|
+
name: "Claude Desktop",
|
|
41
|
+
configPath: () => {
|
|
42
|
+
if (process.platform === "win32") {
|
|
43
|
+
const appData = process.env.APPDATA;
|
|
44
|
+
if (!appData) return null;
|
|
45
|
+
return resolve(appData, "Claude", "claude_desktop_config.json");
|
|
46
|
+
}
|
|
47
|
+
return resolve(homedir(), ".claude", "claude_desktop_config.json");
|
|
48
|
+
},
|
|
49
|
+
basePath: () => {
|
|
50
|
+
if (process.platform === "win32") return process.env.APPDATA || null;
|
|
51
|
+
return homedir();
|
|
52
|
+
},
|
|
53
|
+
buildConfig: (apiKey) => ({
|
|
54
|
+
mcpServers: {
|
|
55
|
+
scrapecreators: {
|
|
56
|
+
url: MCP_URL,
|
|
57
|
+
headers: { "x-api-key": apiKey },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
codex: {
|
|
63
|
+
name: "Codex",
|
|
64
|
+
configPath: () => resolve(homedir(), ".codex", "mcp.json"),
|
|
65
|
+
basePath: () => homedir(),
|
|
66
|
+
buildConfig: (apiKey) => ({
|
|
67
|
+
mcpServers: {
|
|
68
|
+
scrapecreators: {
|
|
69
|
+
url: MCP_URL,
|
|
70
|
+
headers: { "x-api-key": apiKey },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function isPlainObject(value) {
|
|
78
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isWithinBase(basePath, candidatePath) {
|
|
82
|
+
const rel = relative(basePath, candidatePath);
|
|
83
|
+
return rel === "" || (!isAbsolute(rel) && !rel.startsWith(".."));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveConfigPath(rawPath, rawBasePath, targetName) {
|
|
87
|
+
if (!rawPath || !rawBasePath) {
|
|
88
|
+
console.error(chalk.red(`Could not resolve ${targetName} config path on this system.`));
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const baseReal = realpathSync(rawBasePath);
|
|
94
|
+
const resolvedPath = resolve(rawPath);
|
|
95
|
+
const resolvedParent = dirname(resolvedPath);
|
|
96
|
+
|
|
97
|
+
let existingAncestor = resolvedParent;
|
|
98
|
+
while (!existsSync(existingAncestor) && dirname(existingAncestor) !== existingAncestor) {
|
|
99
|
+
existingAncestor = dirname(existingAncestor);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!existsSync(existingAncestor)) {
|
|
103
|
+
console.error(chalk.red(`Could not validate parent directory for ${targetName} config.`));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ancestorReal = realpathSync(existingAncestor);
|
|
109
|
+
if (!isWithinBase(baseReal, ancestorReal) || !isWithinBase(baseReal, resolvedPath)) {
|
|
110
|
+
console.error(chalk.red(`${targetName} config path resolves outside the expected base directory.`));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (existsSync(resolvedPath) && lstatSync(resolvedPath).isSymbolicLink()) {
|
|
116
|
+
console.error(chalk.red(`${targetName} config path is a symlink. Refusing to write secrets to symlinked files.`));
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return resolvedPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function secureWriteJson(configPath, contents) {
|
|
125
|
+
const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
126
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | noFollow;
|
|
127
|
+
let fd;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
fd = openSync(configPath, flags, 0o600);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const noFollowUnsupported = noFollow && (
|
|
133
|
+
err?.code === "EINVAL" ||
|
|
134
|
+
err?.code === "ENOTSUP" ||
|
|
135
|
+
err?.code === "EOPNOTSUPP"
|
|
136
|
+
);
|
|
137
|
+
if (noFollowUnsupported) {
|
|
138
|
+
const tmpPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
139
|
+
writeFileSync(tmpPath, contents, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
140
|
+
try {
|
|
141
|
+
if (existsSync(configPath) && lstatSync(configPath).isSymbolicLink()) {
|
|
142
|
+
throw new Error("refusing to overwrite symlinked config path");
|
|
143
|
+
}
|
|
144
|
+
renameSync(tmpPath, configPath);
|
|
145
|
+
} catch (fallbackErr) {
|
|
146
|
+
try {
|
|
147
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore cleanup errors
|
|
150
|
+
}
|
|
151
|
+
throw fallbackErr;
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
writeFileSync(fd, contents, "utf-8");
|
|
160
|
+
} finally {
|
|
161
|
+
closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ensureOwnerOnly(pathToFile) {
|
|
166
|
+
try {
|
|
167
|
+
chmodSync(pathToFile, 0o600);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(chalk.yellow(`Warning: could not set 0600 on ${pathToFile}: ${err.message}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function ensureGitignore(configPath) {
|
|
174
|
+
let dir = dirname(configPath);
|
|
175
|
+
let gitRoot = null;
|
|
176
|
+
while (dir !== dirname(dir)) {
|
|
177
|
+
if (existsSync(join(dir, ".git"))) {
|
|
178
|
+
gitRoot = dir;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
dir = dirname(dir);
|
|
182
|
+
}
|
|
183
|
+
if (!gitRoot) return null;
|
|
184
|
+
|
|
185
|
+
const rel = relative(gitRoot, configPath).replace(/\\/g, "/");
|
|
186
|
+
const gitignorePath = join(gitRoot, ".gitignore");
|
|
187
|
+
|
|
188
|
+
if (existsSync(gitignorePath)) {
|
|
189
|
+
let content;
|
|
190
|
+
try {
|
|
191
|
+
content = readFileSync(gitignorePath, "utf-8");
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const lines = content.split(/\r?\n/);
|
|
196
|
+
// already covered by an exact match or parent-dir glob
|
|
197
|
+
const configDir = dirname(rel) + "/";
|
|
198
|
+
if (lines.some((l) => {
|
|
199
|
+
const trimmed = l.trim();
|
|
200
|
+
return trimmed === rel || trimmed === `/${rel}` || trimmed === configDir || trimmed === `/${configDir}`;
|
|
201
|
+
})) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const prefix = existsSync(gitignorePath) && readFileSync(gitignorePath, "utf-8").endsWith("\n") ? "" : "\n";
|
|
208
|
+
appendFileSync(gitignorePath, `${prefix}${rel}\n`, "utf-8");
|
|
209
|
+
return rel;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function agentAddCommand(target, globalOpts) {
|
|
216
|
+
const targetLower = target?.toLowerCase();
|
|
217
|
+
const targetDef = TARGETS[targetLower];
|
|
218
|
+
|
|
219
|
+
if (!targetDef) {
|
|
220
|
+
console.error(chalk.red("Unknown target."));
|
|
221
|
+
console.error(`Available targets: ${Object.keys(TARGETS).join(", ")}`);
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let apiKey = resolveApiKey(globalOpts);
|
|
227
|
+
if (!apiKey) {
|
|
228
|
+
const key = await prompts.text({
|
|
229
|
+
message: "Enter your ScrapeCreators API key",
|
|
230
|
+
placeholder: "paste from https://app.scrapecreators.com",
|
|
231
|
+
validate: (v) => (v.length < 5 ? "Too short" : undefined),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (prompts.isCancel(key)) {
|
|
235
|
+
prompts.cancel("Cancelled.");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await callApi(key, "GET", "/v1/credit-balance");
|
|
240
|
+
if (!result.ok) {
|
|
241
|
+
console.error(chalk.red("Invalid API key."));
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
storeApiKey(key);
|
|
247
|
+
apiKey = key;
|
|
248
|
+
console.log(chalk.green("API key validated and saved."));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const configPath = resolveConfigPath(targetDef.configPath(), targetDef.basePath(), targetDef.name);
|
|
252
|
+
if (!configPath) return;
|
|
253
|
+
const newConfig = targetDef.buildConfig(apiKey);
|
|
254
|
+
|
|
255
|
+
let existing = {};
|
|
256
|
+
if (existsSync(configPath)) {
|
|
257
|
+
let raw;
|
|
258
|
+
try {
|
|
259
|
+
raw = readFileSync(configPath, "utf-8");
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(chalk.red(`Could not read existing config at ${configPath}: ${err.message}`));
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
existing = JSON.parse(raw);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(chalk.red(`Existing config is not valid JSON: ${configPath}`));
|
|
270
|
+
console.error(chalk.yellow("Fix the file or back it up before running this command again."));
|
|
271
|
+
if (err?.message) console.error(chalk.dim(err.message));
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!isPlainObject(existing)) {
|
|
277
|
+
console.error(chalk.red(`Expected a JSON object at root in ${configPath}.`));
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (existing.mcpServers !== undefined && !isPlainObject(existing.mcpServers)) {
|
|
284
|
+
console.error(chalk.red(`Expected "mcpServers" to be an object in ${configPath}.`));
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// merge: preserve other MCP servers
|
|
290
|
+
existing.mcpServers = {
|
|
291
|
+
...(existing.mcpServers || {}),
|
|
292
|
+
...newConfig.mcpServers,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
296
|
+
try {
|
|
297
|
+
secureWriteJson(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error(chalk.red(`Could not write config file ${configPath}: ${err.message}`));
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
ensureOwnerOnly(configPath);
|
|
304
|
+
|
|
305
|
+
const addedEntry = ensureGitignore(configPath);
|
|
306
|
+
console.log(chalk.green(`\n${targetDef.name} MCP config written to ${configPath}`));
|
|
307
|
+
if (addedEntry) {
|
|
308
|
+
console.log(chalk.green(`Added ${addedEntry} to .gitignore to keep your API key out of version control.`));
|
|
309
|
+
}
|
|
310
|
+
console.log(chalk.dim("Make sure the scrapecreators MCP server is enabled in your editor settings."));
|
|
311
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { resolveApiKey } from "../auth.js";
|
|
4
|
+
import { callApi } from "../api-client.js";
|
|
5
|
+
import { printResult } from "../output.js";
|
|
6
|
+
|
|
7
|
+
export async function handleApiCommand(tool, args, globalOpts) {
|
|
8
|
+
const apiKey = resolveApiKey(globalOpts);
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.error(chalk.red("No API key found."));
|
|
11
|
+
console.error(chalk.yellow("Run 'scrapecreators auth login' or set SCRAPECREATORS_API_KEY"));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (tool.credits && tool.credits > 1 && process.stdout.isTTY) {
|
|
17
|
+
console.error(chalk.yellow(`Note: this endpoint costs ${tool.credits} credits per request.`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = {};
|
|
21
|
+
for (const param of tool.params) {
|
|
22
|
+
const cliFlag = param.name.replace(/_/g, "-");
|
|
23
|
+
const value = args[camelCase(cliFlag)];
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
params[param.name] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (globalOpts.trim) params.trim = true;
|
|
30
|
+
if (globalOpts.region) params.region = globalOpts.region;
|
|
31
|
+
|
|
32
|
+
const showSpinner = process.stdout.isTTY && !globalOpts.raw && !globalOpts.json;
|
|
33
|
+
const spinner = showSpinner ? ora({ text: "Fetching...", stream: process.stderr }).start() : null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await callApi(apiKey, tool.method, tool.path, params);
|
|
37
|
+
if (spinner) spinner.stop();
|
|
38
|
+
printResult(result, globalOpts);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (spinner) spinner.stop();
|
|
41
|
+
console.error(chalk.red("Request failed."));
|
|
42
|
+
if (globalOpts.verbose) console.error(chalk.dim(err.message));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function camelCase(str) {
|
|
48
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
49
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as prompts from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { resolveApiKey, storeApiKey, clearApiKey, getStoredApiKey, maskKey } from "../auth.js";
|
|
4
|
+
import { callApi } from "../api-client.js";
|
|
5
|
+
|
|
6
|
+
export async function authLogin() {
|
|
7
|
+
prompts.intro(chalk.bold("ScrapeCreators Authentication"));
|
|
8
|
+
|
|
9
|
+
const apiKey = await prompts.text({
|
|
10
|
+
message: "Enter your API key",
|
|
11
|
+
placeholder: "paste your key from https://app.scrapecreators.com",
|
|
12
|
+
validate: (v) => (v.length < 5 ? "API key seems too short" : undefined),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (prompts.isCancel(apiKey)) {
|
|
16
|
+
prompts.cancel("Cancelled.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const spinner = prompts.spinner();
|
|
21
|
+
spinner.start("Validating API key...");
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await callApi(apiKey, "GET", "/v1/credit-balance");
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
spinner.stop("Invalid API key.");
|
|
27
|
+
console.error(chalk.red("The API key was rejected. Check it and try again."));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
storeApiKey(apiKey);
|
|
33
|
+
const credits = result.data?.creditCount ?? result.data?.credits ?? "unknown";
|
|
34
|
+
spinner.stop(`Authenticated. Balance: ${credits} credits.`);
|
|
35
|
+
prompts.outro(chalk.green("API key saved."));
|
|
36
|
+
} catch {
|
|
37
|
+
spinner.stop("Connection failed.");
|
|
38
|
+
console.error(chalk.red("Could not reach API. Check your network and try again."));
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function authStatus() {
|
|
44
|
+
const key = resolveApiKey();
|
|
45
|
+
if (!key) {
|
|
46
|
+
console.log(chalk.yellow("Not authenticated. Run 'scrapecreators auth login'."));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stored = getStoredApiKey();
|
|
51
|
+
const source = stored ? "stored config" : process.env.SCRAPECREATORS_API_KEY ? "environment variable" : "unknown";
|
|
52
|
+
|
|
53
|
+
console.log(`API key: ${chalk.cyan(maskKey(key))} (from ${source})`);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await callApi(key, "GET", "/v1/credit-balance");
|
|
57
|
+
if (result.ok) {
|
|
58
|
+
const credits = result.data?.creditCount ?? result.data?.credits ?? "unknown";
|
|
59
|
+
console.log(`Credits: ${chalk.green(credits)}`);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// non-critical, just skip
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function authLogout() {
|
|
67
|
+
clearApiKey();
|
|
68
|
+
console.log(chalk.green("API key removed from stored config."));
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { resolveApiKey } from "../auth.js";
|
|
4
|
+
import { callApi } from "../api-client.js";
|
|
5
|
+
import { printResult } from "../output.js";
|
|
6
|
+
|
|
7
|
+
export async function balanceCommand(globalOpts) {
|
|
8
|
+
const apiKey = resolveApiKey(globalOpts);
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.error(chalk.red("No API key found."));
|
|
11
|
+
console.error(chalk.yellow("Run 'scrapecreators auth login' or set SCRAPECREATORS_API_KEY"));
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const showSpinner = process.stdout.isTTY && !globalOpts.raw && !globalOpts.json;
|
|
17
|
+
const spinner = showSpinner ? ora({ text: "Checking balance...", stream: process.stderr }).start() : null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await callApi(apiKey, "GET", "/v1/credit-balance");
|
|
21
|
+
if (spinner) spinner.stop();
|
|
22
|
+
|
|
23
|
+
if (globalOpts.raw || globalOpts.json || !process.stdout.isTTY) {
|
|
24
|
+
printResult(result, globalOpts);
|
|
25
|
+
} else if (result.ok) {
|
|
26
|
+
const credits = result.data?.creditCount ?? result.data?.credits ?? "unknown";
|
|
27
|
+
console.log(`Credits remaining: ${chalk.green(chalk.bold(credits))}`);
|
|
28
|
+
} else {
|
|
29
|
+
printResult(result, globalOpts);
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (spinner) spinner.stop();
|
|
33
|
+
console.error(chalk.red("Request failed."));
|
|
34
|
+
if (globalOpts.verbose) {
|
|
35
|
+
console.error(chalk.dim(err.message));
|
|
36
|
+
if (err.cause) console.error(chalk.dim(`Cause: ${err.cause.message || err.cause}`));
|
|
37
|
+
}
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import config from "../config.js";
|
|
3
|
+
import { maskKey } from "../auth.js";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_KEYS = ["apiKey", "defaultFormat"];
|
|
6
|
+
const VALID_FORMATS = ["auto", "json", "table", "csv", "markdown"];
|
|
7
|
+
|
|
8
|
+
export function configSet(key, value) {
|
|
9
|
+
if (!ALLOWED_KEYS.includes(key)) {
|
|
10
|
+
console.error(chalk.red("Unknown config key."));
|
|
11
|
+
console.error(`Valid keys: ${ALLOWED_KEYS.join(", ")}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (key === "defaultFormat" && !VALID_FORMATS.includes(value)) {
|
|
16
|
+
console.error(chalk.red(`Invalid format: ${value}`));
|
|
17
|
+
console.error(`Valid formats: ${VALID_FORMATS.join(", ")}`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
config.set(key, value);
|
|
22
|
+
const display = key === "apiKey" ? maskKey(value) : value;
|
|
23
|
+
console.log(chalk.green(`${key} = ${display}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function configGet(key) {
|
|
27
|
+
if (!ALLOWED_KEYS.includes(key)) {
|
|
28
|
+
console.error(chalk.red("Unknown config key."));
|
|
29
|
+
console.error(`Valid keys: ${ALLOWED_KEYS.join(", ")}`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key === "apiKey") {
|
|
34
|
+
printApiKeyStatus();
|
|
35
|
+
} else {
|
|
36
|
+
console.log(config.get(key) || chalk.dim("(not set)"));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function configList() {
|
|
41
|
+
console.log(chalk.bold("Current configuration:\n"));
|
|
42
|
+
|
|
43
|
+
process.stdout.write(` ${chalk.cyan("apiKey")}: `);
|
|
44
|
+
printApiKeyStatus();
|
|
45
|
+
|
|
46
|
+
for (const key of ALLOWED_KEYS) {
|
|
47
|
+
if (key === "apiKey") continue;
|
|
48
|
+
console.log(` ${chalk.cyan(key)}: ${config.get(key) || chalk.dim("(not set)")}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.dim(`\nConfig file: ${config.path}`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printApiKeyStatus() {
|
|
55
|
+
const stored = config.get("apiKey");
|
|
56
|
+
const envKey = process.env.SCRAPECREATORS_API_KEY;
|
|
57
|
+
|
|
58
|
+
if (stored) {
|
|
59
|
+
console.log(`${maskKey(stored)} ${chalk.dim("(from stored config)")}`);
|
|
60
|
+
} else if (envKey) {
|
|
61
|
+
console.log(`${maskKey(envKey)} ${chalk.dim("(from SCRAPECREATORS_API_KEY env)")}`);
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.dim("(not set)"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { getPlatformMap } from "../command-registry.js";
|
|
4
|
+
|
|
5
|
+
export function listCommand(platform) {
|
|
6
|
+
const platforms = getPlatformMap();
|
|
7
|
+
|
|
8
|
+
if (platform) {
|
|
9
|
+
const tools = platforms.get(platform);
|
|
10
|
+
if (!tools) {
|
|
11
|
+
console.error(chalk.red(`Unknown platform: ${platform}`));
|
|
12
|
+
console.error(`Available: ${[...platforms.keys()].join(", ")}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(chalk.bold(`\n${platform} endpoints:\n`));
|
|
18
|
+
const table = new Table({
|
|
19
|
+
head: [chalk.cyan("Command"), chalk.cyan("Description"), chalk.cyan("Required Params")],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const tool of tools) {
|
|
23
|
+
const required = tool.params
|
|
24
|
+
.filter((p) => p.required)
|
|
25
|
+
.map((p) => `--${p.name.replace(/_/g, "-")}`)
|
|
26
|
+
.join(", ");
|
|
27
|
+
|
|
28
|
+
table.push([
|
|
29
|
+
`scrapecreators ${platform} ${tool._action}`,
|
|
30
|
+
tool.title,
|
|
31
|
+
required || chalk.dim("none"),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
console.log(table.toString());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// list all platforms (skip credit — handled by `scrapecreators balance`)
|
|
39
|
+
console.log(chalk.bold("\nAvailable platforms:\n"));
|
|
40
|
+
const table = new Table({
|
|
41
|
+
head: [chalk.cyan("Platform"), chalk.cyan("Endpoints")],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const sorted = [...platforms.entries()]
|
|
45
|
+
.filter(([name]) => name !== "credit")
|
|
46
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
47
|
+
for (const [name, tools] of sorted) {
|
|
48
|
+
table.push([name, tools.length]);
|
|
49
|
+
}
|
|
50
|
+
console.log(table.toString());
|
|
51
|
+
console.log(chalk.dim("\nRun 'scrapecreators list <platform>' for endpoint details."));
|
|
52
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
import { chmodSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const config = new Conf({
|
|
5
|
+
projectName: "scrapecreators",
|
|
6
|
+
schema: {
|
|
7
|
+
apiKey: { type: "string", default: "" },
|
|
8
|
+
defaultFormat: {
|
|
9
|
+
type: "string",
|
|
10
|
+
default: "auto",
|
|
11
|
+
enum: ["auto", "json", "table", "csv", "markdown"],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// restrict config file to owner-only (contains API key)
|
|
17
|
+
try {
|
|
18
|
+
chmodSync(config.path, 0o600);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(`Warning: could not set 0600 on ${config.path}: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default config;
|