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 CHANGED
@@ -126,189 +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
- // 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
- case "posts:create": {
161
- const content = str(flags, "content");
162
- const accounts = list(flags, "accounts");
163
- if (!content)
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
- out(await api("PATCH", `/api/v1/posts/${encodeURIComponent(id)}`, body));
220
- break;
221
- }
222
- case "posts:approve": {
223
- const id = positional[0];
224
- const schedule = str(flags, "schedule");
225
- if (!id)
226
- fail("Usage: posthive posts:approve <post_id> --schedule <ISO datetime>");
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
- case "posts:delete": {
242
- const id = positional[0];
243
- if (!id)
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
- case "templates:list": {
249
- out(await api("GET", "/api/v1/templates"));
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
- case "templates:use": {
253
- const id = positional[0];
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
- case "upload": {
275
- const filePath = positional[0];
276
- if (!filePath)
277
- fail("Usage: posthive upload <file_path>");
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
- const form = new FormData();
286
- form.append("file", new Blob([new Uint8Array(buf)]), basename(filePath));
287
- let res;
288
- try {
289
- res = await fetch(`${API_URL}/upload`, {
290
- method: "POST",
291
- headers: { Authorization: `Bearer ${API_KEY}` },
292
- 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.",
293
187
  });
188
+ break;
294
189
  }
295
- catch (err) {
296
- 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;
297
201
  }
298
- const text = await res.text();
299
- let json;
300
- try {
301
- 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;
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
- catch {
304
- json = { raw: text };
250
+ case "templates:list": {
251
+ out(await api("GET", "/api/v1/templates"));
252
+ break;
305
253
  }
306
- if (!res.ok) {
307
- fail(`Upload failed (${res.status}): ${text}`);
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
- out(json);
310
- break;
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" }).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.7",
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": {