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 CHANGED
@@ -126,192 +126,193 @@ const HELP = {
126
126
  };
127
127
  // ─── Main ────────────────────────────────────────────────────────────────────
128
128
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
129
- if (command === "help" || command === "--help" || command === "-h") {
130
- out(HELP);
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
- case "posts:update": {
211
- const id = positional[0];
212
- if (!id)
213
- fail("Usage: posthive posts:update <post_id> [--content] [--schedule] [--first-comment]");
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
- out(await api("PATCH", `/api/v1/posts/${encodeURIComponent(id)}`, body));
223
- break;
224
- }
225
- case "posts:approve": {
226
- const id = positional[0];
227
- const schedule = str(flags, "schedule");
228
- if (!id)
229
- fail("Usage: posthive posts:approve <post_id> --schedule <ISO datetime>");
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
- case "posts:delete": {
245
- const id = positional[0];
246
- if (!id)
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
- case "templates:list": {
252
- out(await api("GET", "/api/v1/templates"));
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
- case "templates:use": {
256
- const id = positional[0];
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
- case "upload": {
278
- const filePath = positional[0];
279
- if (!filePath)
280
- fail("Usage: posthive upload <file_path>");
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
- const form = new FormData();
289
- form.append("file", new Blob([new Uint8Array(buf)]), basename(filePath));
290
- let res;
291
- try {
292
- res = await fetch(`${API_URL}/upload`, {
293
- method: "POST",
294
- headers: { Authorization: `Bearer ${API_KEY}` },
295
- body: form,
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
- catch (err) {
299
- fail(`Cannot reach Posthive API at ${API_URL} — check POSTHIVE_API_URL and your network. (${err instanceof Error ? err.message : String(err)})`);
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
- const text = await res.text();
302
- let json;
303
- try {
304
- json = JSON.parse(text);
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
- catch {
307
- json = { raw: text };
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
- if (!res.ok) {
310
- fail(`Upload failed (${res.status}): ${text}`);
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
- out(json);
313
- break;
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" }).end(DENIED_HTML);
53
- server.close();
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" }).end(DENIED_HTML);
59
- server.close();
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" }).end(SUCCESS_HTML);
64
- server.close();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthive-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Posthive CLI — schedule posts to 11 social platforms from the command line or any AI agent (Claude Code, OpenClaw, Cursor)",
5
5
  "type": "module",
6
6
  "bin": {