posthive-cli 0.1.7 → 0.1.9
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/dist/index.js +175 -171
- package/dist/login.js +7 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -126,189 +126,193 @@ const HELP = {
|
|
|
126
126
|
};
|
|
127
127
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
128
128
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (command === "login") {
|
|
133
|
-
const loginUrl = (str(flags, "api-url") ?? process.env.POSTHIVE_API_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
134
|
-
try {
|
|
135
|
-
await runLogin(loginUrl);
|
|
136
|
-
// Delay exit by a tick so libuv finishes closing the local callback
|
|
137
|
-
// server's handles first — calling process.exit() synchronously here
|
|
138
|
-
// can race with that cleanup and crash on Windows.
|
|
139
|
-
setImmediate(() => process.exit(0));
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (command === "logout") {
|
|
146
|
-
await runLogout();
|
|
147
|
-
process.exit(0);
|
|
148
|
-
}
|
|
149
|
-
if (!API_KEY) {
|
|
150
|
-
fail("Not logged in. Run `posthive login`, or set POSTHIVE_API_KEY for CI/scripts.");
|
|
151
|
-
}
|
|
152
|
-
if (command === "whoami") {
|
|
153
|
-
out(await api("GET", "/api/v1/me"));
|
|
154
|
-
}
|
|
155
|
-
switch (command) {
|
|
156
|
-
case "accounts:list": {
|
|
157
|
-
out(await api("GET", "/api/v1/accounts"));
|
|
158
|
-
break;
|
|
129
|
+
async function main() {
|
|
130
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
131
|
+
out(HELP);
|
|
159
132
|
}
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
fail("--content is required");
|
|
165
|
-
if (!accounts?.length)
|
|
166
|
-
fail("--accounts is required (comma-separated account IDs; see accounts:list)");
|
|
167
|
-
const schedule = str(flags, "schedule");
|
|
168
|
-
const body = {
|
|
169
|
-
content,
|
|
170
|
-
accountIds: accounts,
|
|
171
|
-
draft: !schedule,
|
|
172
|
-
...(schedule ? { scheduledFor: schedule } : {}),
|
|
173
|
-
...(list(flags, "media") ? { images: list(flags, "media") } : {}),
|
|
174
|
-
...(str(flags, "media-type") ? { mediaType: str(flags, "media-type") } : {}),
|
|
175
|
-
...(str(flags, "youtube-type") ? { youtubeType: str(flags, "youtube-type") } : {}),
|
|
176
|
-
...(str(flags, "first-comment") ? { commentText: str(flags, "first-comment") } : {}),
|
|
177
|
-
...(flags["dry-run"] === true ? { dryRun: true } : {}),
|
|
178
|
-
};
|
|
179
|
-
const data = await api("POST", "/api/v1/posts", body);
|
|
180
|
-
out({
|
|
181
|
-
...data,
|
|
182
|
-
_note: schedule
|
|
183
|
-
? "Post scheduled — will publish at the given time."
|
|
184
|
-
: "Post saved as DRAFT. Approve with posts:approve or review in Posthive → Posts.",
|
|
185
|
-
});
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
case "posts:list": {
|
|
189
|
-
const params = new URLSearchParams();
|
|
190
|
-
const status = str(flags, "status");
|
|
191
|
-
const limit = str(flags, "limit");
|
|
192
|
-
if (status)
|
|
193
|
-
params.set("status", status);
|
|
194
|
-
if (limit)
|
|
195
|
-
params.set("limit", limit);
|
|
196
|
-
const qs = params.size ? `?${params}` : "";
|
|
197
|
-
out(await api("GET", `/api/v1/posts${qs}`));
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
case "posts:get": {
|
|
201
|
-
const id = positional[0];
|
|
202
|
-
if (!id)
|
|
203
|
-
fail("Usage: posthive posts:get <post_id>");
|
|
204
|
-
out(await api("GET", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
case "posts:update": {
|
|
208
|
-
const id = positional[0];
|
|
209
|
-
if (!id)
|
|
210
|
-
fail("Usage: posthive posts:update <post_id> [--content] [--schedule] [--first-comment]");
|
|
211
|
-
const body = {
|
|
212
|
-
...(str(flags, "content") !== undefined ? { content: str(flags, "content") } : {}),
|
|
213
|
-
...(str(flags, "schedule") !== undefined ? { scheduledFor: str(flags, "schedule") } : {}),
|
|
214
|
-
...(str(flags, "first-comment") !== undefined ? { commentText: str(flags, "first-comment") } : {}),
|
|
215
|
-
};
|
|
216
|
-
if (Object.keys(body).length === 0) {
|
|
217
|
-
fail("Provide at least one of --content, --schedule, --first-comment");
|
|
133
|
+
if (command === "login") {
|
|
134
|
+
const loginUrl = (str(flags, "api-url") ?? process.env.POSTHIVE_API_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
135
|
+
try {
|
|
136
|
+
await runLogin(loginUrl);
|
|
218
137
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!schedule)
|
|
228
|
-
fail("--schedule is required (ISO 8601 datetime, must be in the future)");
|
|
229
|
-
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/approve`, { scheduledFor: schedule });
|
|
230
|
-
out({ ...data, _note: "Draft approved and scheduled." });
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
case "posts:duplicate": {
|
|
234
|
-
const id = positional[0];
|
|
235
|
-
if (!id)
|
|
236
|
-
fail("Usage: posthive posts:duplicate <post_id>");
|
|
237
|
-
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/duplicate`);
|
|
238
|
-
out({ ...data, _note: "Duplicated as a new draft." });
|
|
239
|
-
break;
|
|
138
|
+
catch (err) {
|
|
139
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
140
|
+
}
|
|
141
|
+
// No explicit process.exit() here — the local callback server used during
|
|
142
|
+
// login is already closed by this point, so returning lets Node exit
|
|
143
|
+
// naturally once the event loop drains. Forcing an early exit races with
|
|
144
|
+
// libuv's handle cleanup on Windows and crashes with a native assertion.
|
|
145
|
+
return;
|
|
240
146
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
fail("Usage: posthive posts:delete <post_id>");
|
|
245
|
-
out(await api("DELETE", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
246
|
-
break;
|
|
147
|
+
if (command === "logout") {
|
|
148
|
+
await runLogout();
|
|
149
|
+
return;
|
|
247
150
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
break;
|
|
151
|
+
if (!API_KEY) {
|
|
152
|
+
fail("Not logged in. Run `posthive login`, or set POSTHIVE_API_KEY for CI/scripts.");
|
|
251
153
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const accounts = list(flags, "accounts");
|
|
255
|
-
if (!id)
|
|
256
|
-
fail("Usage: posthive templates:use <template_id> --accounts <id,id>");
|
|
257
|
-
if (!accounts?.length)
|
|
258
|
-
fail("--accounts is required (comma-separated account IDs)");
|
|
259
|
-
const schedule = str(flags, "schedule");
|
|
260
|
-
const body = {
|
|
261
|
-
accountIds: accounts,
|
|
262
|
-
draft: !schedule,
|
|
263
|
-
...(schedule ? { scheduledFor: schedule } : {}),
|
|
264
|
-
...(str(flags, "content") ? { contentOverride: str(flags, "content") } : {}),
|
|
265
|
-
...(str(flags, "first-comment") ? { firstCommentOverride: str(flags, "first-comment") } : {}),
|
|
266
|
-
};
|
|
267
|
-
const data = await api("POST", `/api/v1/templates/${encodeURIComponent(id)}/use`, body);
|
|
268
|
-
out({
|
|
269
|
-
...data,
|
|
270
|
-
_note: schedule ? "Post created from template and scheduled." : "Post created from template as DRAFT.",
|
|
271
|
-
});
|
|
272
|
-
break;
|
|
154
|
+
if (command === "whoami") {
|
|
155
|
+
out(await api("GET", "/api/v1/me"));
|
|
273
156
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let buf;
|
|
279
|
-
try {
|
|
280
|
-
buf = await readFile(filePath);
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
fail(`Cannot read file: ${filePath}`);
|
|
157
|
+
switch (command) {
|
|
158
|
+
case "accounts:list": {
|
|
159
|
+
out(await api("GET", "/api/v1/accounts"));
|
|
160
|
+
break;
|
|
284
161
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
162
|
+
case "posts:create": {
|
|
163
|
+
const content = str(flags, "content");
|
|
164
|
+
const accounts = list(flags, "accounts");
|
|
165
|
+
if (!content)
|
|
166
|
+
fail("--content is required");
|
|
167
|
+
if (!accounts?.length)
|
|
168
|
+
fail("--accounts is required (comma-separated account IDs; see accounts:list)");
|
|
169
|
+
const schedule = str(flags, "schedule");
|
|
170
|
+
const body = {
|
|
171
|
+
content,
|
|
172
|
+
accountIds: accounts,
|
|
173
|
+
draft: !schedule,
|
|
174
|
+
...(schedule ? { scheduledFor: schedule } : {}),
|
|
175
|
+
...(list(flags, "media") ? { images: list(flags, "media") } : {}),
|
|
176
|
+
...(str(flags, "media-type") ? { mediaType: str(flags, "media-type") } : {}),
|
|
177
|
+
...(str(flags, "youtube-type") ? { youtubeType: str(flags, "youtube-type") } : {}),
|
|
178
|
+
...(str(flags, "first-comment") ? { commentText: str(flags, "first-comment") } : {}),
|
|
179
|
+
...(flags["dry-run"] === true ? { dryRun: true } : {}),
|
|
180
|
+
};
|
|
181
|
+
const data = await api("POST", "/api/v1/posts", body);
|
|
182
|
+
out({
|
|
183
|
+
...data,
|
|
184
|
+
_note: schedule
|
|
185
|
+
? "Post scheduled — will publish at the given time."
|
|
186
|
+
: "Post saved as DRAFT. Approve with posts:approve or review in Posthive → Posts.",
|
|
293
187
|
});
|
|
188
|
+
break;
|
|
294
189
|
}
|
|
295
|
-
|
|
296
|
-
|
|
190
|
+
case "posts:list": {
|
|
191
|
+
const params = new URLSearchParams();
|
|
192
|
+
const status = str(flags, "status");
|
|
193
|
+
const limit = str(flags, "limit");
|
|
194
|
+
if (status)
|
|
195
|
+
params.set("status", status);
|
|
196
|
+
if (limit)
|
|
197
|
+
params.set("limit", limit);
|
|
198
|
+
const qs = params.size ? `?${params}` : "";
|
|
199
|
+
out(await api("GET", `/api/v1/posts${qs}`));
|
|
200
|
+
break;
|
|
297
201
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
202
|
+
case "posts:get": {
|
|
203
|
+
const id = positional[0];
|
|
204
|
+
if (!id)
|
|
205
|
+
fail("Usage: posthive posts:get <post_id>");
|
|
206
|
+
out(await api("GET", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "posts:update": {
|
|
210
|
+
const id = positional[0];
|
|
211
|
+
if (!id)
|
|
212
|
+
fail("Usage: posthive posts:update <post_id> [--content] [--schedule] [--first-comment]");
|
|
213
|
+
const body = {
|
|
214
|
+
...(str(flags, "content") !== undefined ? { content: str(flags, "content") } : {}),
|
|
215
|
+
...(str(flags, "schedule") !== undefined ? { scheduledFor: str(flags, "schedule") } : {}),
|
|
216
|
+
...(str(flags, "first-comment") !== undefined ? { commentText: str(flags, "first-comment") } : {}),
|
|
217
|
+
};
|
|
218
|
+
if (Object.keys(body).length === 0) {
|
|
219
|
+
fail("Provide at least one of --content, --schedule, --first-comment");
|
|
220
|
+
}
|
|
221
|
+
out(await api("PATCH", `/api/v1/posts/${encodeURIComponent(id)}`, body));
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "posts:approve": {
|
|
225
|
+
const id = positional[0];
|
|
226
|
+
const schedule = str(flags, "schedule");
|
|
227
|
+
if (!id)
|
|
228
|
+
fail("Usage: posthive posts:approve <post_id> --schedule <ISO datetime>");
|
|
229
|
+
if (!schedule)
|
|
230
|
+
fail("--schedule is required (ISO 8601 datetime, must be in the future)");
|
|
231
|
+
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/approve`, { scheduledFor: schedule });
|
|
232
|
+
out({ ...data, _note: "Draft approved and scheduled." });
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "posts:duplicate": {
|
|
236
|
+
const id = positional[0];
|
|
237
|
+
if (!id)
|
|
238
|
+
fail("Usage: posthive posts:duplicate <post_id>");
|
|
239
|
+
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/duplicate`);
|
|
240
|
+
out({ ...data, _note: "Duplicated as a new draft." });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "posts:delete": {
|
|
244
|
+
const id = positional[0];
|
|
245
|
+
if (!id)
|
|
246
|
+
fail("Usage: posthive posts:delete <post_id>");
|
|
247
|
+
out(await api("DELETE", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
248
|
+
break;
|
|
302
249
|
}
|
|
303
|
-
|
|
304
|
-
|
|
250
|
+
case "templates:list": {
|
|
251
|
+
out(await api("GET", "/api/v1/templates"));
|
|
252
|
+
break;
|
|
305
253
|
}
|
|
306
|
-
|
|
307
|
-
|
|
254
|
+
case "templates:use": {
|
|
255
|
+
const id = positional[0];
|
|
256
|
+
const accounts = list(flags, "accounts");
|
|
257
|
+
if (!id)
|
|
258
|
+
fail("Usage: posthive templates:use <template_id> --accounts <id,id>");
|
|
259
|
+
if (!accounts?.length)
|
|
260
|
+
fail("--accounts is required (comma-separated account IDs)");
|
|
261
|
+
const schedule = str(flags, "schedule");
|
|
262
|
+
const body = {
|
|
263
|
+
accountIds: accounts,
|
|
264
|
+
draft: !schedule,
|
|
265
|
+
...(schedule ? { scheduledFor: schedule } : {}),
|
|
266
|
+
...(str(flags, "content") ? { contentOverride: str(flags, "content") } : {}),
|
|
267
|
+
...(str(flags, "first-comment") ? { firstCommentOverride: str(flags, "first-comment") } : {}),
|
|
268
|
+
};
|
|
269
|
+
const data = await api("POST", `/api/v1/templates/${encodeURIComponent(id)}/use`, body);
|
|
270
|
+
out({
|
|
271
|
+
...data,
|
|
272
|
+
_note: schedule ? "Post created from template and scheduled." : "Post created from template as DRAFT.",
|
|
273
|
+
});
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case "upload": {
|
|
277
|
+
const filePath = positional[0];
|
|
278
|
+
if (!filePath)
|
|
279
|
+
fail("Usage: posthive upload <file_path>");
|
|
280
|
+
let buf;
|
|
281
|
+
try {
|
|
282
|
+
buf = await readFile(filePath);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
fail(`Cannot read file: ${filePath}`);
|
|
286
|
+
}
|
|
287
|
+
const form = new FormData();
|
|
288
|
+
form.append("file", new Blob([new Uint8Array(buf)]), basename(filePath));
|
|
289
|
+
let res;
|
|
290
|
+
try {
|
|
291
|
+
res = await fetch(`${API_URL}/upload`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
294
|
+
body: form,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
fail(`Cannot reach Posthive API at ${API_URL} — check POSTHIVE_API_URL and your network. (${err instanceof Error ? err.message : String(err)})`);
|
|
299
|
+
}
|
|
300
|
+
const text = await res.text();
|
|
301
|
+
let json;
|
|
302
|
+
try {
|
|
303
|
+
json = JSON.parse(text);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
json = { raw: text };
|
|
307
|
+
}
|
|
308
|
+
if (!res.ok) {
|
|
309
|
+
fail(`Upload failed (${res.status}): ${text}`);
|
|
310
|
+
}
|
|
311
|
+
out(json);
|
|
312
|
+
break;
|
|
308
313
|
}
|
|
309
|
-
|
|
310
|
-
|
|
314
|
+
default:
|
|
315
|
+
fail(`Unknown command: ${command}. Run "posthive help" for usage.`);
|
|
311
316
|
}
|
|
312
|
-
default:
|
|
313
|
-
fail(`Unknown command: ${command}. Run "posthive help" for usage.`);
|
|
314
317
|
}
|
|
318
|
+
await main();
|
package/dist/login.js
CHANGED
|
@@ -48,20 +48,21 @@ export async function runLogin(apiUrl) {
|
|
|
48
48
|
const returnedState = url.searchParams.get("state");
|
|
49
49
|
const authCode = url.searchParams.get("code");
|
|
50
50
|
const error = url.searchParams.get("error");
|
|
51
|
+
const shutdown = () => { server.close(); server.closeAllConnections(); };
|
|
51
52
|
if (error) {
|
|
52
|
-
res.writeHead(200, { "Content-Type": "text/html" })
|
|
53
|
-
|
|
53
|
+
res.writeHead(200, { "Content-Type": "text/html", "Connection": "close" });
|
|
54
|
+
res.end(DENIED_HTML, () => shutdown());
|
|
54
55
|
reject(new Error(url.searchParams.get("error_description") ?? error));
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
if (!authCode || returnedState !== state) {
|
|
58
|
-
res.writeHead(400, { "Content-Type": "text/html" })
|
|
59
|
-
|
|
59
|
+
res.writeHead(400, { "Content-Type": "text/html", "Connection": "close" });
|
|
60
|
+
res.end(DENIED_HTML, () => shutdown());
|
|
60
61
|
reject(new Error("State mismatch or missing authorization code."));
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
|
-
res.writeHead(200, { "Content-Type": "text/html" })
|
|
64
|
-
|
|
64
|
+
res.writeHead(200, { "Content-Type": "text/html", "Connection": "close" });
|
|
65
|
+
res.end(SUCCESS_HTML, () => shutdown());
|
|
65
66
|
resolve(authCode);
|
|
66
67
|
});
|
|
67
68
|
server.on("error", reject);
|
package/package.json
CHANGED