hookpost 0.1.0
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/README.md +88 -0
- package/dist/index.js +241 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# hookpost
|
|
2
|
+
|
|
3
|
+
Schedule and publish social media posts from your terminal, powered by [Hookpost](https://hookpost.hookstep.in).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g hookpost
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Sign in
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
hookpost auth login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It asks for your API key — find it in Hookpost under **Settings → Public API** — verifies it before saving, and stores it at `~/.hookpost/config.json` with owner-only permissions.
|
|
18
|
+
|
|
19
|
+
In CI, skip the login and set an environment variable instead:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export HOOKPOST_API_KEY="your_key"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
auth login Store your API key
|
|
29
|
+
auth status Show who you are signed in as
|
|
30
|
+
auth logout Remove the stored key
|
|
31
|
+
|
|
32
|
+
channels List your connected channels
|
|
33
|
+
posts [--days N] List upcoming posts (default 7 days)
|
|
34
|
+
post <text> Create a post
|
|
35
|
+
delete <postId> Delete a post
|
|
36
|
+
slot [channelId] Show the next free slot
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Creating posts
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Schedule into your next free slot
|
|
43
|
+
hookpost post "Shipped something small today." --channel abc123
|
|
44
|
+
|
|
45
|
+
# Pick an exact time
|
|
46
|
+
hookpost post "Launch day" --channel abc123 --at 2026-09-10T09:00:00Z
|
|
47
|
+
|
|
48
|
+
# Publish immediately
|
|
49
|
+
hookpost post "Live now" --channel abc123 --now
|
|
50
|
+
|
|
51
|
+
# Save a draft
|
|
52
|
+
hookpost post "Rough idea" --channel abc123 --draft
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run `hookpost channels` to get channel ids.
|
|
56
|
+
|
|
57
|
+
Omit `--at` on a scheduled post and the CLI asks Hookpost for the next free slot in your posting schedule — the same default the web app uses.
|
|
58
|
+
|
|
59
|
+
### Channel settings
|
|
60
|
+
|
|
61
|
+
Some channels need extra fields. YouTube wants a title and visibility:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
hookpost post "New video" --channel abc123 --now \
|
|
65
|
+
--settings '{"__type":"youtube","title":"My video","type":"private","selfDeclaredMadeForKids":"no"}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Call `/public/v1/integration-settings/:id` to see what a given channel expects.
|
|
69
|
+
|
|
70
|
+
## Environment
|
|
71
|
+
|
|
72
|
+
| Variable | Purpose |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `HOOKPOST_API_KEY` | Use instead of `auth login` — handy in CI |
|
|
75
|
+
| `HOOKPOST_API_URL` | Override the API host when self-hosting |
|
|
76
|
+
|
|
77
|
+
## Security
|
|
78
|
+
|
|
79
|
+
The API key grants full access to your organisation's channels and posts. Keep it out of shared shells and public repos; prefer `HOOKPOST_API_KEY` from a secret store in automation.
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
- [Public API reference](https://hookpost.hookstep.in/docs/public-api)
|
|
84
|
+
- [OAuth apps](https://hookpost.hookstep.in/docs/oauth) — for acting on other people's accounts
|
|
85
|
+
|
|
86
|
+
## Licence
|
|
87
|
+
|
|
88
|
+
AGPL-3.0
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const readline_1 = require("readline");
|
|
8
|
+
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.hookpost');
|
|
9
|
+
const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
|
|
10
|
+
const DEFAULT_HOST = 'https://hookpost.hookstep.in/api';
|
|
11
|
+
const c = {
|
|
12
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
13
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
14
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
15
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
16
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
17
|
+
};
|
|
18
|
+
async function readConfig() {
|
|
19
|
+
// Environment wins over the stored file so CI can run without `auth login`.
|
|
20
|
+
const fromEnv = {
|
|
21
|
+
apiKey: process.env.HOOKPOST_API_KEY,
|
|
22
|
+
host: process.env.HOOKPOST_API_URL,
|
|
23
|
+
};
|
|
24
|
+
let stored = {};
|
|
25
|
+
try {
|
|
26
|
+
stored = JSON.parse(await fs_1.promises.readFile(CONFIG_FILE, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* no config yet */
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
apiKey: fromEnv.apiKey || stored.apiKey,
|
|
33
|
+
host: fromEnv.host || stored.host || DEFAULT_HOST,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function writeConfig(cfg) {
|
|
37
|
+
await fs_1.promises.mkdir((0, path_1.dirname)(CONFIG_FILE), { recursive: true });
|
|
38
|
+
await fs_1.promises.writeFile(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
39
|
+
// The key is a full-access credential, so keep it owner-readable only.
|
|
40
|
+
await fs_1.promises.chmod(CONFIG_FILE, 0o600);
|
|
41
|
+
}
|
|
42
|
+
async function api(method, path, body) {
|
|
43
|
+
const { apiKey, host } = await readConfig();
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
throw new Error('Not signed in. Run `hookpost auth login`, or set HOOKPOST_API_KEY.');
|
|
46
|
+
}
|
|
47
|
+
const res = await fetch(`${String(host).replace(/\/+$/, '')}/public/v1${path}`, {
|
|
48
|
+
method,
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
// Hookpost takes the raw key - a Bearer prefix is rejected.
|
|
52
|
+
Authorization: apiKey,
|
|
53
|
+
},
|
|
54
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
55
|
+
});
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
let data;
|
|
58
|
+
try {
|
|
59
|
+
data = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
data = text;
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const msg = data?.msg || data?.message || text || `HTTP ${res.status}`;
|
|
66
|
+
throw new Error(`${res.status} ${msg}`);
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
function ask(question) {
|
|
71
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
72
|
+
return new Promise((resolve) => rl.question(question, (a) => {
|
|
73
|
+
rl.close();
|
|
74
|
+
resolve(a.trim());
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
function flag(args, name) {
|
|
78
|
+
const i = args.indexOf(`--${name}`);
|
|
79
|
+
return i !== -1 ? args[i + 1] : undefined;
|
|
80
|
+
}
|
|
81
|
+
const HELP = `
|
|
82
|
+
${c.bold('hookpost')} - schedule and publish social media posts from your terminal
|
|
83
|
+
|
|
84
|
+
${c.bold('Usage')}
|
|
85
|
+
hookpost <command> [options]
|
|
86
|
+
|
|
87
|
+
${c.bold('Commands')}
|
|
88
|
+
auth login Store your API key
|
|
89
|
+
auth status Show who you are signed in as
|
|
90
|
+
auth logout Remove the stored key
|
|
91
|
+
|
|
92
|
+
channels List your connected channels
|
|
93
|
+
posts List upcoming posts
|
|
94
|
+
post <text> Create a post
|
|
95
|
+
delete <postId> Delete a post
|
|
96
|
+
slot [channelId] Show the next free slot
|
|
97
|
+
|
|
98
|
+
${c.bold('post options')}
|
|
99
|
+
--channel <id> Channel to post to (required)
|
|
100
|
+
--at <ISO date> When to publish; omitted uses your next free slot
|
|
101
|
+
--now Publish immediately
|
|
102
|
+
--draft Save as a draft
|
|
103
|
+
--settings <json> Per-channel settings, e.g. YouTube title/visibility
|
|
104
|
+
|
|
105
|
+
${c.bold('Examples')}
|
|
106
|
+
hookpost channels
|
|
107
|
+
hookpost post "Shipped something small today." --channel abc123
|
|
108
|
+
hookpost post "Live now" --channel abc123 --now
|
|
109
|
+
hookpost posts --days 14
|
|
110
|
+
|
|
111
|
+
${c.bold('Environment')}
|
|
112
|
+
HOOKPOST_API_KEY Use instead of \`auth login\` (handy in CI)
|
|
113
|
+
HOOKPOST_API_URL Override the API host when self-hosting
|
|
114
|
+
|
|
115
|
+
Docs: https://hookpost.hookstep.in/docs/public-api
|
|
116
|
+
`;
|
|
117
|
+
async function main() {
|
|
118
|
+
const argv = process.argv.slice(2);
|
|
119
|
+
const [cmd, sub] = argv;
|
|
120
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
121
|
+
console.log(HELP);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
125
|
+
console.log(require('../package.json').version);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (cmd === 'auth') {
|
|
129
|
+
if (sub === 'login') {
|
|
130
|
+
const key = await ask('API key (Settings -> Public API): ');
|
|
131
|
+
if (!key)
|
|
132
|
+
throw new Error('No key entered.');
|
|
133
|
+
const host = (await ask(`API host [${DEFAULT_HOST}]: `)) || DEFAULT_HOST;
|
|
134
|
+
await writeConfig({ apiKey: key, host });
|
|
135
|
+
// Prove the key works now rather than failing on first real use.
|
|
136
|
+
await api('GET', '/is-connected');
|
|
137
|
+
console.log(c.green(`Signed in. Key saved to ${CONFIG_FILE}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (sub === 'status') {
|
|
141
|
+
const { apiKey, host } = await readConfig();
|
|
142
|
+
if (!apiKey) {
|
|
143
|
+
console.log('Not signed in.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await api('GET', '/is-connected');
|
|
147
|
+
console.log(`${c.green('Signed in')} host=${host} key=${apiKey.slice(0, 8)}...`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (sub === 'logout') {
|
|
151
|
+
await fs_1.promises.rm(CONFIG_FILE, { force: true });
|
|
152
|
+
console.log('Signed out.');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Usage: hookpost auth <login|status|logout>');
|
|
156
|
+
}
|
|
157
|
+
if (cmd === 'channels') {
|
|
158
|
+
const list = await api('GET', '/integrations');
|
|
159
|
+
if (!Array.isArray(list) || !list.length) {
|
|
160
|
+
console.log('No channels connected yet.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const ch of list) {
|
|
164
|
+
console.log(`${c.cyan(ch.id)} ${String(ch.name).padEnd(24)} ${c.dim(ch.identifier || ch.providerIdentifier || '')}`);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (cmd === 'posts') {
|
|
169
|
+
const days = Number(flag(argv, 'days') || 7);
|
|
170
|
+
const startDate = new Date().toISOString();
|
|
171
|
+
const endDate = new Date(Date.now() + days * 864e5).toISOString();
|
|
172
|
+
const res = await api('GET', `/posts?startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}`);
|
|
173
|
+
const posts = res?.posts || res || [];
|
|
174
|
+
if (!posts.length) {
|
|
175
|
+
console.log(`No posts in the next ${days} days.`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const p of posts) {
|
|
179
|
+
const when = new Date(p.publishDate).toISOString().replace('T', ' ').slice(0, 16);
|
|
180
|
+
const text = String(p.content || '').replace(/<[^>]*>/g, '').slice(0, 50);
|
|
181
|
+
console.log(`${c.dim(when)} ${String(p.state).padEnd(9)} ${c.cyan(p.id)} ${text}`);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (cmd === 'post') {
|
|
186
|
+
const text = argv[1];
|
|
187
|
+
if (!text || text.startsWith('--')) {
|
|
188
|
+
throw new Error('Usage: hookpost post "your text" --channel <id>');
|
|
189
|
+
}
|
|
190
|
+
const channel = flag(argv, 'channel');
|
|
191
|
+
if (!channel)
|
|
192
|
+
throw new Error('--channel is required. Run `hookpost channels` to list them.');
|
|
193
|
+
const type = argv.includes('--now')
|
|
194
|
+
? 'now'
|
|
195
|
+
: argv.includes('--draft')
|
|
196
|
+
? 'draft'
|
|
197
|
+
: 'schedule';
|
|
198
|
+
let date = flag(argv, 'at');
|
|
199
|
+
if (type === 'schedule' && !date) {
|
|
200
|
+
const slot = await api('GET', `/find-slot/${channel}`);
|
|
201
|
+
date = slot?.date;
|
|
202
|
+
console.log(c.dim(`No --at given, using next free slot: ${date}`));
|
|
203
|
+
}
|
|
204
|
+
const settingsRaw = flag(argv, 'settings');
|
|
205
|
+
const settings = settingsRaw ? JSON.parse(settingsRaw) : undefined;
|
|
206
|
+
const res = await api('POST', '/posts', {
|
|
207
|
+
type,
|
|
208
|
+
date: date || new Date().toISOString(),
|
|
209
|
+
shortLink: false,
|
|
210
|
+
tags: [],
|
|
211
|
+
posts: [
|
|
212
|
+
{
|
|
213
|
+
integration: { id: channel },
|
|
214
|
+
value: [{ content: text, image: [] }],
|
|
215
|
+
...(settings ? { settings } : {}),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
console.log(c.green('Created.'), JSON.stringify(res));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (cmd === 'delete') {
|
|
223
|
+
const id = argv[1];
|
|
224
|
+
if (!id)
|
|
225
|
+
throw new Error('Usage: hookpost delete <postId>');
|
|
226
|
+
await api('DELETE', `/posts/${id}`);
|
|
227
|
+
console.log(c.green(`Deleted ${id}`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (cmd === 'slot') {
|
|
231
|
+
const id = argv[1];
|
|
232
|
+
const res = await api('GET', id ? `/find-slot/${id}` : '/is-connected');
|
|
233
|
+
console.log(JSON.stringify(res));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Unknown command: ${cmd}\nRun \`hookpost help\`.`);
|
|
237
|
+
}
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error(c.red(`Error: ${err.message}`));
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookpost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hookpost CLI - schedule and publish social media posts from your terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hookpost",
|
|
7
|
+
"cli",
|
|
8
|
+
"social media",
|
|
9
|
+
"scheduling",
|
|
10
|
+
"social media scheduler"
|
|
11
|
+
],
|
|
12
|
+
"license": "AGPL-3.0",
|
|
13
|
+
"author": "JR Consulting Co.",
|
|
14
|
+
"homepage": "https://hookpost.hookstep.in/docs/public-api",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/jatinder14/hookpost.git",
|
|
18
|
+
"directory": "apps/cli"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"hookpost": "dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.6.0",
|
|
33
|
+
"@types/node": "^22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|