apitweet-cli 0.1.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/LICENSE +21 -0
- package/README.md +349 -0
- package/bin/apitweet.js +11 -0
- package/package.json +37 -0
- package/src/commands.js +747 -0
- package/src/config.js +301 -0
- package/src/constants.js +34 -0
- package/src/help.js +86 -0
- package/src/index.js +15 -0
- package/src/parser.js +55 -0
- package/src/request.js +95 -0
- package/src/utils.js +164 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { CONFIG_FILE_NAME, DEFAULT_BASE_URL, DEFAULT_CONFIG } from "./constants.js";
|
|
5
|
+
import { exitWithError, findOption, hasFlag, maskSecret, printJson, sanitizeForOutput } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
export function configFilePath(configDir) {
|
|
8
|
+
return path.join(configDir, CONFIG_FILE_NAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function loadConfig(configDir) {
|
|
12
|
+
try {
|
|
13
|
+
const contents = await fs.readFile(configFilePath(configDir), "utf8");
|
|
14
|
+
const parsed = JSON.parse(contents);
|
|
15
|
+
return {
|
|
16
|
+
...structuredClone(DEFAULT_CONFIG),
|
|
17
|
+
...parsed,
|
|
18
|
+
apps: parsed.apps || {},
|
|
19
|
+
profiles: parsed.profiles || {},
|
|
20
|
+
};
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
23
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
24
|
+
}
|
|
25
|
+
exitWithError("Failed to load config.", error.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function saveConfig(configDir, config) {
|
|
30
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
31
|
+
await fs.writeFile(configFilePath(configDir), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function printConfigSummary(configDir, config) {
|
|
35
|
+
const display = {
|
|
36
|
+
configDir,
|
|
37
|
+
currentApp: config.currentApp || null,
|
|
38
|
+
currentProfile: config.currentProfile || null,
|
|
39
|
+
apps: Object.fromEntries(
|
|
40
|
+
Object.entries(config.apps).map(([name, app]) => [
|
|
41
|
+
name,
|
|
42
|
+
{
|
|
43
|
+
baseUrl: app.baseUrl,
|
|
44
|
+
apiKey: maskSecret(app.apiKey),
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
),
|
|
48
|
+
profiles: Object.fromEntries(
|
|
49
|
+
Object.entries(config.profiles).map(([name, profile]) => [
|
|
50
|
+
name,
|
|
51
|
+
{
|
|
52
|
+
cookie: profile.cookie ? maskSecret(profile.cookie) : "",
|
|
53
|
+
authToken: profile.authToken ? maskSecret(profile.authToken) : "",
|
|
54
|
+
ct0: profile.ct0 ? maskSecret(profile.ct0) : "",
|
|
55
|
+
},
|
|
56
|
+
]),
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
printJson(display);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveAppState(state, config) {
|
|
64
|
+
const configuredApp = state.appName
|
|
65
|
+
? config.apps[state.appName]
|
|
66
|
+
: config.currentApp
|
|
67
|
+
? config.apps[config.currentApp]
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
if (state.appName && !configuredApp) {
|
|
71
|
+
exitWithError(`Unknown app profile: ${state.appName}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
appName: state.appName || config.currentApp || "",
|
|
76
|
+
apiKey: state.apiKey || process.env.APITWEET_KEY || configuredApp?.apiKey || "",
|
|
77
|
+
baseUrl: (
|
|
78
|
+
state.baseUrl ||
|
|
79
|
+
process.env.APITWEET_BASE_URL ||
|
|
80
|
+
configuredApp?.baseUrl ||
|
|
81
|
+
DEFAULT_BASE_URL
|
|
82
|
+
).replace(/\/+$/, ""),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveProfile(config, preferredName) {
|
|
87
|
+
const profileName = preferredName || config.currentProfile || "";
|
|
88
|
+
if (!profileName) {
|
|
89
|
+
return { profileName: "", profile: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const profile = config.profiles[profileName];
|
|
93
|
+
if (!profile) {
|
|
94
|
+
exitWithError(`Unknown profile: ${profileName}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { profileName, profile };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function profileCredential(profile) {
|
|
101
|
+
if (!profile) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
if (profile.cookie) {
|
|
105
|
+
return profile.cookie;
|
|
106
|
+
}
|
|
107
|
+
if (profile.authToken && profile.ct0) {
|
|
108
|
+
return `ct0=${profile.ct0}; auth_token=${profile.authToken}`;
|
|
109
|
+
}
|
|
110
|
+
if (profile.authToken) {
|
|
111
|
+
return profile.authToken;
|
|
112
|
+
}
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveCookieArg(args, state, config, required = true) {
|
|
117
|
+
const explicitCookie = findOption(args, "--cookie");
|
|
118
|
+
if (explicitCookie) {
|
|
119
|
+
return explicitCookie;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const explicitProfile = findOption(args, "--profile") || state.profileName;
|
|
123
|
+
const { profile } = resolveProfile(config, explicitProfile);
|
|
124
|
+
const savedValue = profileCredential(profile);
|
|
125
|
+
if (savedValue) {
|
|
126
|
+
return savedValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (required && !state.dryRun) {
|
|
130
|
+
exitWithError("Missing cookie/auth token.", "Pass --cookie or configure a saved profile.");
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function handleConfigCommand(state, config, args) {
|
|
136
|
+
const action = args[1] || "show";
|
|
137
|
+
if (action === "show") {
|
|
138
|
+
printConfigSummary(state.configDir, config);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (action === "path") {
|
|
143
|
+
console.log(configFilePath(state.configDir));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
exitWithError("Unknown config command.", "Use: config show | config path");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function handleAuthAppsCommand(state, config, args) {
|
|
151
|
+
const action = args[2];
|
|
152
|
+
|
|
153
|
+
if (action === "add") {
|
|
154
|
+
const name = findOption(args, "--name");
|
|
155
|
+
const apiKey = findOption(args, "--api-key");
|
|
156
|
+
if (!name || !apiKey) {
|
|
157
|
+
exitWithError("Usage: apitweet auth apps add --name <name> --api-key <key> [--base-url <url>]");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const baseUrl = (findOption(args, "--base-url") || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
161
|
+
config.apps[name] = { apiKey, baseUrl };
|
|
162
|
+
if (!config.currentApp || hasFlag(args, "--use")) {
|
|
163
|
+
config.currentApp = name;
|
|
164
|
+
}
|
|
165
|
+
await saveConfig(state.configDir, config);
|
|
166
|
+
|
|
167
|
+
printJson({
|
|
168
|
+
saved: name,
|
|
169
|
+
currentApp: config.currentApp,
|
|
170
|
+
app: {
|
|
171
|
+
baseUrl,
|
|
172
|
+
apiKey: maskSecret(apiKey),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (action === "list") {
|
|
179
|
+
printJson({
|
|
180
|
+
currentApp: config.currentApp || null,
|
|
181
|
+
apps: Object.entries(config.apps).map(([name, app]) => ({
|
|
182
|
+
name,
|
|
183
|
+
isCurrent: name === config.currentApp,
|
|
184
|
+
baseUrl: app.baseUrl,
|
|
185
|
+
apiKey: maskSecret(app.apiKey),
|
|
186
|
+
})),
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (action === "use") {
|
|
192
|
+
const name = args[3];
|
|
193
|
+
if (!name) {
|
|
194
|
+
exitWithError("Usage: apitweet auth apps use <name>");
|
|
195
|
+
}
|
|
196
|
+
if (!config.apps[name]) {
|
|
197
|
+
exitWithError(`Unknown app profile: ${name}`);
|
|
198
|
+
}
|
|
199
|
+
config.currentApp = name;
|
|
200
|
+
await saveConfig(state.configDir, config);
|
|
201
|
+
printJson({ currentApp: name });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (action === "remove") {
|
|
206
|
+
const name = args[3];
|
|
207
|
+
if (!name) {
|
|
208
|
+
exitWithError("Usage: apitweet auth apps remove <name>");
|
|
209
|
+
}
|
|
210
|
+
if (!config.apps[name]) {
|
|
211
|
+
exitWithError(`Unknown app profile: ${name}`);
|
|
212
|
+
}
|
|
213
|
+
delete config.apps[name];
|
|
214
|
+
if (config.currentApp === name) {
|
|
215
|
+
config.currentApp = "";
|
|
216
|
+
}
|
|
217
|
+
await saveConfig(state.configDir, config);
|
|
218
|
+
printJson({ removed: name, currentApp: config.currentApp || null });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
exitWithError("Unknown auth apps command.", "Use: add | list | use | remove");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function handleAuthProfilesCommand(state, config, args) {
|
|
226
|
+
const action = args[2];
|
|
227
|
+
|
|
228
|
+
if (action === "add") {
|
|
229
|
+
const name = findOption(args, "--name");
|
|
230
|
+
const cookie = findOption(args, "--cookie");
|
|
231
|
+
const authToken = findOption(args, "--auth-token");
|
|
232
|
+
const ct0 = findOption(args, "--ct0");
|
|
233
|
+
if (!name || (!cookie && !authToken)) {
|
|
234
|
+
exitWithError("Usage: apitweet auth profiles add --name <name> [--cookie <value> | --auth-token <value>] [--ct0 <value>]");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
config.profiles[name] = {
|
|
238
|
+
...(cookie ? { cookie } : {}),
|
|
239
|
+
...(authToken ? { authToken } : {}),
|
|
240
|
+
...(ct0 ? { ct0 } : {}),
|
|
241
|
+
};
|
|
242
|
+
if (!config.currentProfile || hasFlag(args, "--use")) {
|
|
243
|
+
config.currentProfile = name;
|
|
244
|
+
}
|
|
245
|
+
await saveConfig(state.configDir, config);
|
|
246
|
+
|
|
247
|
+
printJson({
|
|
248
|
+
saved: name,
|
|
249
|
+
currentProfile: config.currentProfile,
|
|
250
|
+
profile: sanitizeForOutput(config.profiles[name]),
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (action === "list") {
|
|
256
|
+
printJson({
|
|
257
|
+
currentProfile: config.currentProfile || null,
|
|
258
|
+
profiles: Object.entries(config.profiles).map(([name, profile]) => ({
|
|
259
|
+
name,
|
|
260
|
+
isCurrent: name === config.currentProfile,
|
|
261
|
+
hasCookie: Boolean(profile.cookie),
|
|
262
|
+
hasAuthToken: Boolean(profile.authToken),
|
|
263
|
+
hasCt0: Boolean(profile.ct0),
|
|
264
|
+
})),
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (action === "use") {
|
|
270
|
+
const name = args[3];
|
|
271
|
+
if (!name) {
|
|
272
|
+
exitWithError("Usage: apitweet auth profiles use <name>");
|
|
273
|
+
}
|
|
274
|
+
if (!config.profiles[name]) {
|
|
275
|
+
exitWithError(`Unknown profile: ${name}`);
|
|
276
|
+
}
|
|
277
|
+
config.currentProfile = name;
|
|
278
|
+
await saveConfig(state.configDir, config);
|
|
279
|
+
printJson({ currentProfile: name });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (action === "remove") {
|
|
284
|
+
const name = args[3];
|
|
285
|
+
if (!name) {
|
|
286
|
+
exitWithError("Usage: apitweet auth profiles remove <name>");
|
|
287
|
+
}
|
|
288
|
+
if (!config.profiles[name]) {
|
|
289
|
+
exitWithError(`Unknown profile: ${name}`);
|
|
290
|
+
}
|
|
291
|
+
delete config.profiles[name];
|
|
292
|
+
if (config.currentProfile === name) {
|
|
293
|
+
config.currentProfile = "";
|
|
294
|
+
}
|
|
295
|
+
await saveConfig(state.configDir, config);
|
|
296
|
+
printJson({ removed: name, currentProfile: config.currentProfile || null });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
exitWithError("Unknown auth profiles command.", "Use: add | list | use | remove");
|
|
301
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_BASE_URL = "https://apitweet.com";
|
|
5
|
+
export const API_PATH_PREFIX = "/api";
|
|
6
|
+
export const CONFIG_FILE_NAME = "config.json";
|
|
7
|
+
export const DEFAULT_CONFIG_DIR =
|
|
8
|
+
process.env.APITWEET_CONFIG_DIR ||
|
|
9
|
+
path.join(os.homedir(), ".apitweet");
|
|
10
|
+
export const DEFAULT_CONFIG = {
|
|
11
|
+
version: 1,
|
|
12
|
+
currentApp: "",
|
|
13
|
+
currentProfile: "",
|
|
14
|
+
apps: {},
|
|
15
|
+
profiles: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const LEADING_GLOBAL_OPTIONS = {
|
|
19
|
+
"-X": { takesValue: true, key: "method" },
|
|
20
|
+
"--method": { takesValue: true, key: "method" },
|
|
21
|
+
"-d": { takesValue: true, key: "data" },
|
|
22
|
+
"--data": { takesValue: true, key: "data" },
|
|
23
|
+
"-H": { takesValue: true, key: "header" },
|
|
24
|
+
"--header": { takesValue: true, key: "header" },
|
|
25
|
+
"--app": { takesValue: true, key: "appName" },
|
|
26
|
+
"--profile": { takesValue: true, key: "profileName" },
|
|
27
|
+
"--api-key": { takesValue: true, key: "apiKey" },
|
|
28
|
+
"--base-url": { takesValue: true, key: "baseUrl" },
|
|
29
|
+
"--config-dir": { takesValue: true, key: "configDir" },
|
|
30
|
+
"--dry-run": { takesValue: false, key: "dryRun" },
|
|
31
|
+
"--raw": { takesValue: false, key: "raw" },
|
|
32
|
+
"-h": { takesValue: false, key: "help" },
|
|
33
|
+
"--help": { takesValue: false, key: "help" },
|
|
34
|
+
};
|
package/src/help.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function printHelp() {
|
|
2
|
+
console.log(`apitweet - command-line client for Twitter/X apitweet APIs
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
apitweet [global options] <path> # Raw API request
|
|
6
|
+
|
|
7
|
+
# Twitter/X User and Search
|
|
8
|
+
apitweet [global options] users <username...> # Multi-user lookup
|
|
9
|
+
apitweet [global options] about <screen_name> # User profile details
|
|
10
|
+
apitweet [global options] search tweets <term...> [--count <n>] # Search Twitter/X tweets
|
|
11
|
+
apitweet [global options] search users <keyword> [--count <n>] # Search Twitter/X users
|
|
12
|
+
apitweet [global options] followers <screen_name> [--count <n>] # List user followers
|
|
13
|
+
apitweet [global options] following <screen_name> [--count <n>] # List users followed
|
|
14
|
+
|
|
15
|
+
# Twitter/X Lists
|
|
16
|
+
apitweet [global options] list search --query <text> [--count <n>] # Search for lists
|
|
17
|
+
apitweet [global options] list create --name <n> --desc <t> [--private] # Create a new list
|
|
18
|
+
apitweet [global options] list members <list_id> [--count <n>] # List members in a list
|
|
19
|
+
apitweet [global options] list subscribers <list_id> [--count <n>] # List list subscribers
|
|
20
|
+
|
|
21
|
+
# Twitter/X Articles and DMs
|
|
22
|
+
apitweet [global options] article markdown <tweet_id> # Fetch article as Markdown
|
|
23
|
+
apitweet [global options] article lookup <tweet_id...> # Batch article details
|
|
24
|
+
apitweet [global options] article publish-md <file.md> --title <title> # Publish Markdown as an X article
|
|
25
|
+
apitweet [global options] dm history <username> [--max-id <id>] # Fetch DM history
|
|
26
|
+
apitweet [global options] dm send <username> --text <content> # Send a Direct Message
|
|
27
|
+
|
|
28
|
+
# Twitter/X Profile and Timeline
|
|
29
|
+
apitweet [global options] profile update [--name <n>] [--desc <t>] ... # Update your profile info
|
|
30
|
+
apitweet [global options] timeline user <screen_name> [--cursor <c>] # Fetch user timeline page
|
|
31
|
+
|
|
32
|
+
# Twitter/X Global Trending
|
|
33
|
+
apitweet [global options] trending tweets --country <country> # Fetch global trending tweets
|
|
34
|
+
|
|
35
|
+
# Twitter/X Tweets
|
|
36
|
+
apitweet [global options] tweet create --text <c> [--media-url <u>] # Create a new tweet
|
|
37
|
+
apitweet [global options] tweet quote --text <c> --quote-url <u> # Quote a tweet
|
|
38
|
+
apitweet [global options] tweet lookup <tweet_id...> [--summary] # Batch tweet lookup
|
|
39
|
+
apitweet [global options] tweet replies <tweet_id> [--count <n>] # Fetch tweet replies
|
|
40
|
+
apitweet [global options] tweet like <tweet_id> # Like a tweet
|
|
41
|
+
apitweet [global options] tweet bookmark <tweet_id> # Bookmark a tweet
|
|
42
|
+
apitweet [global options] tweet retweet <tweet_id> # Retweet a tweet
|
|
43
|
+
|
|
44
|
+
# Twitter/X Actions
|
|
45
|
+
apitweet [global options] user follow <username> # Follow a user
|
|
46
|
+
apitweet [global options] user unfollow <username> # Unfollow a user
|
|
47
|
+
|
|
48
|
+
# Config and Auth
|
|
49
|
+
apitweet [global options] auth apps add --name <n> --api-key <k> # Add an app config
|
|
50
|
+
apitweet [global options] auth profiles add --name <n> [--cookie <v>] # Add an auth profile
|
|
51
|
+
apitweet [global options] auth cookie --auth-token <t> [--save-as <p>] # Create profile from token
|
|
52
|
+
apitweet [global options] config show # Show current config
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
apitweet --app prod users elonmusk sama
|
|
56
|
+
apitweet --profile founder tweet create --text "hello world" --media-url "https://example.com/image.jpg"
|
|
57
|
+
apitweet --app prod --profile founder article publish-md ./article.md --title "Launch Notes" --cover-image "https://example.com/cover.jpg"
|
|
58
|
+
apitweet --profile founder tweet like 1900000000000000000
|
|
59
|
+
apitweet --app prod trending tweets --country "United States" --topic "Sports" --content "NFL" --count 50
|
|
60
|
+
apitweet --app prod -X POST -d '["1900000000000000000"]' /api/twitter/tweets/lookup # Twitter/X tweet lookup
|
|
61
|
+
apitweet auth apps add --name prod --api-key "twitterx_..."
|
|
62
|
+
apitweet auth profiles add --name founder --cookie "ct0=...; auth_token=..."
|
|
63
|
+
apitweet auth cookie --auth-token "<auth_token>" --save-as founder
|
|
64
|
+
apitweet config show
|
|
65
|
+
|
|
66
|
+
Global options:
|
|
67
|
+
-X, --method <METHOD> HTTP method for generic path requests, default GET
|
|
68
|
+
-d, --data <JSON> JSON body for generic path requests
|
|
69
|
+
-H, --header <K:V> Extra header for generic path requests, can be repeated
|
|
70
|
+
--app <NAME> Use a saved app config
|
|
71
|
+
--profile <NAME> Use a saved profile config
|
|
72
|
+
--api-key <KEY> Override API key for this request
|
|
73
|
+
--base-url <URL> Override base URL for this request
|
|
74
|
+
--config-dir <DIR> Override config directory, default ~/.apitweet
|
|
75
|
+
--dry-run Print the request payload without sending it
|
|
76
|
+
--raw Print response body without pretty JSON formatting
|
|
77
|
+
-h, --help Show help
|
|
78
|
+
|
|
79
|
+
Notes:
|
|
80
|
+
Global options should be placed before the command.
|
|
81
|
+
Saved apps store API keys and base URLs.
|
|
82
|
+
Saved profiles store cookies or auth_token values for write actions.
|
|
83
|
+
Get API keys from https://apitweet.io/dashboard.
|
|
84
|
+
Copy the API key from the dashboard and save it with auth apps add.
|
|
85
|
+
`);
|
|
86
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
import { printHelp } from "./help.js";
|
|
3
|
+
import { parseGlobalArgs } from "./parser.js";
|
|
4
|
+
import { runCommand } from "./commands.js";
|
|
5
|
+
|
|
6
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
7
|
+
const state = parseGlobalArgs(argv);
|
|
8
|
+
if (state.help || state.commandArgs.length === 0) {
|
|
9
|
+
printHelp();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = await loadConfig(state.configDir);
|
|
14
|
+
await runCommand(state, config);
|
|
15
|
+
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG_DIR, LEADING_GLOBAL_OPTIONS } from "./constants.js";
|
|
2
|
+
import { parseHeader, parseJson, exitWithError } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export function parseGlobalArgs(argv) {
|
|
5
|
+
const state = {
|
|
6
|
+
method: "GET",
|
|
7
|
+
data: undefined,
|
|
8
|
+
headers: {},
|
|
9
|
+
appName: "",
|
|
10
|
+
profileName: "",
|
|
11
|
+
apiKey: "",
|
|
12
|
+
baseUrl: "",
|
|
13
|
+
configDir: DEFAULT_CONFIG_DIR,
|
|
14
|
+
dryRun: false,
|
|
15
|
+
raw: false,
|
|
16
|
+
help: false,
|
|
17
|
+
commandArgs: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let index = 0;
|
|
21
|
+
while (index < argv.length) {
|
|
22
|
+
const current = argv[index];
|
|
23
|
+
const descriptor = LEADING_GLOBAL_OPTIONS[current];
|
|
24
|
+
if (!descriptor) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (descriptor.takesValue) {
|
|
29
|
+
const value = argv[index + 1];
|
|
30
|
+
if (!value) {
|
|
31
|
+
exitWithError(`Missing value for ${current}.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (descriptor.key === "method") {
|
|
35
|
+
state.method = value.toUpperCase();
|
|
36
|
+
} else if (descriptor.key === "data") {
|
|
37
|
+
state.data = parseJson(current, value);
|
|
38
|
+
} else if (descriptor.key === "header") {
|
|
39
|
+
const [name, headerValue] = parseHeader(value);
|
|
40
|
+
state.headers[name] = headerValue;
|
|
41
|
+
} else {
|
|
42
|
+
state[descriptor.key] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
index += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
state[descriptor.key] = true;
|
|
50
|
+
index += 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
state.commandArgs = argv.slice(index);
|
|
54
|
+
return state;
|
|
55
|
+
}
|
package/src/request.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { resolveAppState } from "./config.js";
|
|
2
|
+
import { apiPath, exitWithError, printJson, sanitizeForOutput } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
export async function performRequest(state, config, request) {
|
|
5
|
+
const appState = resolveAppState(state, config);
|
|
6
|
+
if (!appState.apiKey && !state.dryRun) {
|
|
7
|
+
const guidance = [
|
|
8
|
+
"Use --api-key, set APITWEET_KEY, or configure an app with auth apps add.",
|
|
9
|
+
"Get an API key here: https://apitweet.io/dashboard",
|
|
10
|
+
'Example: apitweet auth apps add --name prod --api-key "twitterx_..."',
|
|
11
|
+
].join("\n");
|
|
12
|
+
exitWithError("Missing API key.", guidance);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const target = apiPath(request.path);
|
|
16
|
+
const url = /^https?:\/\//i.test(target) ? target : `${appState.baseUrl}${target}`;
|
|
17
|
+
const headers = {
|
|
18
|
+
Accept: "application/json",
|
|
19
|
+
...state.headers,
|
|
20
|
+
...request.headers,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (appState.apiKey) {
|
|
24
|
+
headers.Authorization = `Bearer ${appState.apiKey}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let body;
|
|
28
|
+
if (request.body !== undefined) {
|
|
29
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
30
|
+
body = headers["Content-Type"].includes("application/json")
|
|
31
|
+
? JSON.stringify(request.body)
|
|
32
|
+
: String(request.body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (state.dryRun) {
|
|
36
|
+
const preview = sanitizeForOutput({
|
|
37
|
+
method: request.method,
|
|
38
|
+
url,
|
|
39
|
+
headers,
|
|
40
|
+
...(body
|
|
41
|
+
? {
|
|
42
|
+
body: headers["Content-Type"]?.includes("application/json") ? JSON.parse(body) : body,
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
});
|
|
46
|
+
printJson(preview);
|
|
47
|
+
return { dryRun: true, data: preview };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
method: request.method,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const text = await response.text();
|
|
57
|
+
const contentType = response.headers.get("content-type") || "";
|
|
58
|
+
const isJson = contentType.includes("application/json");
|
|
59
|
+
let data = text;
|
|
60
|
+
|
|
61
|
+
if (isJson && text) {
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(text);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
exitWithError("Failed to parse JSON response.", error.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
if (isJson) {
|
|
71
|
+
console.error(JSON.stringify(data, null, 2));
|
|
72
|
+
} else {
|
|
73
|
+
console.error(text);
|
|
74
|
+
}
|
|
75
|
+
process.exit(response.status || 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!request.silent) {
|
|
79
|
+
if (state.raw || !isJson) {
|
|
80
|
+
process.stdout.write(text);
|
|
81
|
+
if (!text.endsWith("\n")) {
|
|
82
|
+
process.stdout.write("\n");
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
printJson(data);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
response,
|
|
91
|
+
text,
|
|
92
|
+
isJson,
|
|
93
|
+
data,
|
|
94
|
+
};
|
|
95
|
+
}
|