posthive-cli 0.1.8 → 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 -174
- package/dist/login.js +7 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -126,192 +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
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
139
|
-
}
|
|
140
|
-
// Delay exit by a tick so libuv finishes closing the local callback
|
|
141
|
-
// server's handles first — calling process.exit() synchronously here
|
|
142
|
-
// can race with that cleanup and crash on Windows. Nothing below this
|
|
143
|
-
// block must run, since API_KEY was resolved before login wrote the
|
|
144
|
-
// new credentials.
|
|
145
|
-
await new Promise(r => setImmediate(r));
|
|
146
|
-
process.exit(0);
|
|
147
|
-
}
|
|
148
|
-
else if (command === "logout") {
|
|
149
|
-
await runLogout();
|
|
150
|
-
process.exit(0);
|
|
151
|
-
}
|
|
152
|
-
else if (!API_KEY) {
|
|
153
|
-
fail("Not logged in. Run `posthive login`, or set POSTHIVE_API_KEY for CI/scripts.");
|
|
154
|
-
}
|
|
155
|
-
if (command === "whoami") {
|
|
156
|
-
out(await api("GET", "/api/v1/me"));
|
|
157
|
-
}
|
|
158
|
-
switch (command) {
|
|
159
|
-
case "accounts:list": {
|
|
160
|
-
out(await api("GET", "/api/v1/accounts"));
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
case "posts:create": {
|
|
164
|
-
const content = str(flags, "content");
|
|
165
|
-
const accounts = list(flags, "accounts");
|
|
166
|
-
if (!content)
|
|
167
|
-
fail("--content is required");
|
|
168
|
-
if (!accounts?.length)
|
|
169
|
-
fail("--accounts is required (comma-separated account IDs; see accounts:list)");
|
|
170
|
-
const schedule = str(flags, "schedule");
|
|
171
|
-
const body = {
|
|
172
|
-
content,
|
|
173
|
-
accountIds: accounts,
|
|
174
|
-
draft: !schedule,
|
|
175
|
-
...(schedule ? { scheduledFor: schedule } : {}),
|
|
176
|
-
...(list(flags, "media") ? { images: list(flags, "media") } : {}),
|
|
177
|
-
...(str(flags, "media-type") ? { mediaType: str(flags, "media-type") } : {}),
|
|
178
|
-
...(str(flags, "youtube-type") ? { youtubeType: str(flags, "youtube-type") } : {}),
|
|
179
|
-
...(str(flags, "first-comment") ? { commentText: str(flags, "first-comment") } : {}),
|
|
180
|
-
...(flags["dry-run"] === true ? { dryRun: true } : {}),
|
|
181
|
-
};
|
|
182
|
-
const data = await api("POST", "/api/v1/posts", body);
|
|
183
|
-
out({
|
|
184
|
-
...data,
|
|
185
|
-
_note: schedule
|
|
186
|
-
? "Post scheduled — will publish at the given time."
|
|
187
|
-
: "Post saved as DRAFT. Approve with posts:approve or review in Posthive → Posts.",
|
|
188
|
-
});
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
case "posts:list": {
|
|
192
|
-
const params = new URLSearchParams();
|
|
193
|
-
const status = str(flags, "status");
|
|
194
|
-
const limit = str(flags, "limit");
|
|
195
|
-
if (status)
|
|
196
|
-
params.set("status", status);
|
|
197
|
-
if (limit)
|
|
198
|
-
params.set("limit", limit);
|
|
199
|
-
const qs = params.size ? `?${params}` : "";
|
|
200
|
-
out(await api("GET", `/api/v1/posts${qs}`));
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
case "posts:get": {
|
|
204
|
-
const id = positional[0];
|
|
205
|
-
if (!id)
|
|
206
|
-
fail("Usage: posthive posts:get <post_id>");
|
|
207
|
-
out(await api("GET", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
208
|
-
break;
|
|
129
|
+
async function main() {
|
|
130
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
131
|
+
out(HELP);
|
|
209
132
|
}
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const body = {
|
|
215
|
-
...(str(flags, "content") !== undefined ? { content: str(flags, "content") } : {}),
|
|
216
|
-
...(str(flags, "schedule") !== undefined ? { scheduledFor: str(flags, "schedule") } : {}),
|
|
217
|
-
...(str(flags, "first-comment") !== undefined ? { commentText: str(flags, "first-comment") } : {}),
|
|
218
|
-
};
|
|
219
|
-
if (Object.keys(body).length === 0) {
|
|
220
|
-
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);
|
|
221
137
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!schedule)
|
|
231
|
-
fail("--schedule is required (ISO 8601 datetime, must be in the future)");
|
|
232
|
-
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/approve`, { scheduledFor: schedule });
|
|
233
|
-
out({ ...data, _note: "Draft approved and scheduled." });
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
case "posts:duplicate": {
|
|
237
|
-
const id = positional[0];
|
|
238
|
-
if (!id)
|
|
239
|
-
fail("Usage: posthive posts:duplicate <post_id>");
|
|
240
|
-
const data = await api("POST", `/api/v1/posts/${encodeURIComponent(id)}/duplicate`);
|
|
241
|
-
out({ ...data, _note: "Duplicated as a new draft." });
|
|
242
|
-
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;
|
|
243
146
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
fail("Usage: posthive posts:delete <post_id>");
|
|
248
|
-
out(await api("DELETE", `/api/v1/posts/${encodeURIComponent(id)}`));
|
|
249
|
-
break;
|
|
147
|
+
if (command === "logout") {
|
|
148
|
+
await runLogout();
|
|
149
|
+
return;
|
|
250
150
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
break;
|
|
151
|
+
if (!API_KEY) {
|
|
152
|
+
fail("Not logged in. Run `posthive login`, or set POSTHIVE_API_KEY for CI/scripts.");
|
|
254
153
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const accounts = list(flags, "accounts");
|
|
258
|
-
if (!id)
|
|
259
|
-
fail("Usage: posthive templates:use <template_id> --accounts <id,id>");
|
|
260
|
-
if (!accounts?.length)
|
|
261
|
-
fail("--accounts is required (comma-separated account IDs)");
|
|
262
|
-
const schedule = str(flags, "schedule");
|
|
263
|
-
const body = {
|
|
264
|
-
accountIds: accounts,
|
|
265
|
-
draft: !schedule,
|
|
266
|
-
...(schedule ? { scheduledFor: schedule } : {}),
|
|
267
|
-
...(str(flags, "content") ? { contentOverride: str(flags, "content") } : {}),
|
|
268
|
-
...(str(flags, "first-comment") ? { firstCommentOverride: str(flags, "first-comment") } : {}),
|
|
269
|
-
};
|
|
270
|
-
const data = await api("POST", `/api/v1/templates/${encodeURIComponent(id)}/use`, body);
|
|
271
|
-
out({
|
|
272
|
-
...data,
|
|
273
|
-
_note: schedule ? "Post created from template and scheduled." : "Post created from template as DRAFT.",
|
|
274
|
-
});
|
|
275
|
-
break;
|
|
154
|
+
if (command === "whoami") {
|
|
155
|
+
out(await api("GET", "/api/v1/me"));
|
|
276
156
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
let buf;
|
|
282
|
-
try {
|
|
283
|
-
buf = await readFile(filePath);
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
fail(`Cannot read file: ${filePath}`);
|
|
157
|
+
switch (command) {
|
|
158
|
+
case "accounts:list": {
|
|
159
|
+
out(await api("GET", "/api/v1/accounts"));
|
|
160
|
+
break;
|
|
287
161
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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.",
|
|
296
187
|
});
|
|
188
|
+
break;
|
|
297
189
|
}
|
|
298
|
-
|
|
299
|
-
|
|
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;
|
|
300
201
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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;
|
|
305
242
|
}
|
|
306
|
-
|
|
307
|
-
|
|
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;
|
|
308
249
|
}
|
|
309
|
-
|
|
310
|
-
|
|
250
|
+
case "templates:list": {
|
|
251
|
+
out(await api("GET", "/api/v1/templates"));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
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;
|
|
311
313
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
default:
|
|
315
|
+
fail(`Unknown command: ${command}. Run "posthive help" for usage.`);
|
|
314
316
|
}
|
|
315
|
-
default:
|
|
316
|
-
fail(`Unknown command: ${command}. Run "posthive help" for usage.`);
|
|
317
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