@socifyr/cli 0.0.0-stage → 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 +271 -2
- package/dist/cli.js +681 -0
- package/package.json +53 -4
package/README.md
CHANGED
|
@@ -1,3 +1,272 @@
|
|
|
1
|
-
#
|
|
1
|
+
# socifyr
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Post to 10+ social platforms from your terminal. Powered by [`@socifyr/sdk`](../sdk/README.md).
|
|
4
|
+
|
|
5
|
+
**Supported platforms:** Instagram, Facebook, X (Twitter), LinkedIn, TikTok, YouTube, Threads, Pinterest, Reddit, Bluesky
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @socifyr/cli
|
|
11
|
+
# or
|
|
12
|
+
bun add -g @socifyr/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From the monorepo (unpublished):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd packages/cli
|
|
19
|
+
bun install
|
|
20
|
+
bun run build
|
|
21
|
+
node dist/cli.js --help # or link the bin: npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
socifyr configure --key sk_ac4f91eb... --workspace 6a206187ff07beb18995bb91
|
|
28
|
+
# ✓ Configuration saved
|
|
29
|
+
# API key: sk_ac4f91e...
|
|
30
|
+
# Workspace: 6a206187ff07beb18995bb91
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Get your API key at **https://socifyr.com/dashboard/integrations#api-keys**. Check what's stored:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
socifyr configure --show
|
|
37
|
+
# Current configuration:
|
|
38
|
+
# API key: sk_ac4f91e...
|
|
39
|
+
# Workspace: 6a206187ff07beb18995bb91
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Environment variables (override stored config)
|
|
43
|
+
|
|
44
|
+
Handy for CI and scripts — no `configure` step needed:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export SOCIFYR_API_KEY=sk_...
|
|
48
|
+
export SOCIFYR_WORKSPACE_ID=6a206187ff07beb18995bb91
|
|
49
|
+
export SOCIFYR_API_URL=https://api.socifyr.com/api # optional, self-hosted
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
### `socifyr connections`
|
|
57
|
+
|
|
58
|
+
List connected accounts and their slugs — **always start here** to learn what you can post to:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
socifyr connections
|
|
62
|
+
# 3 connected accounts:
|
|
63
|
+
#
|
|
64
|
+
# 📸 instagram-mybrand
|
|
65
|
+
# instagram · @mybrand
|
|
66
|
+
# 🎵 tiktok-mybrand
|
|
67
|
+
# tiktok · mybrand
|
|
68
|
+
# ▶️ youtube-mybrand
|
|
69
|
+
# youtube · My Brand Channel
|
|
70
|
+
#
|
|
71
|
+
# Use slugs with: socifyr post "text" --to <slug1,slug2>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Flags: `--json` (raw JSON array), alias `conn`. Warns when a token expires within 7 days.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### `socifyr post [text]`
|
|
79
|
+
|
|
80
|
+
Post text, images, video, or documents. Content can come from an argument, `--file`, or stdin:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Text post
|
|
84
|
+
socifyr post "Just shipped v2.0 🚀" --to x-myhandle,linkedin-mybrand
|
|
85
|
+
|
|
86
|
+
# Caption from a file
|
|
87
|
+
socifyr post --file CHANGELOG.md --to linkedin-mybrand --ai
|
|
88
|
+
|
|
89
|
+
# Pipe it
|
|
90
|
+
cat release-notes.md | socifyr post --to linkedin-mybrand --wait
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Media — local files** (auto-detects image vs video by extension; uploads to your media library first):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
socifyr post "New drop" --media photo1.jpg,photo2.jpg --to instagram-mybrand
|
|
97
|
+
socifyr post --media video.mp4 --to tiktok-mybrand,instagram-mybrand --ai --wait
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Media — library IDs or public URLs** (no re-upload; library IDs are resolved to their type, so a video ID is never posted as a photo):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
socifyr post "Reuse library asset" --media-id 6a04f715d3d7d93575e65d13 --to instagram-mybrand
|
|
104
|
+
socifyr post "From URL" --media-id https://example.com/banner.jpg --to linkedin-mybrand
|
|
105
|
+
socifyr post "Deck" --media-id 6a93c01f... --to linkedin-mybrand # document (PDF)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Scheduling & drafts:**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
socifyr post "Deploy complete" --to x-myhandle --at 2026-06-01T09:00
|
|
112
|
+
socifyr post "Maybe later" --to x-myhandle --draft
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Flag | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `-t, --to <slugs>` | Comma-separated connection slugs (required) |
|
|
118
|
+
| `-f, --file <path>` | Read caption from a text file |
|
|
119
|
+
| `-m, --media <paths>` | Comma-separated local file paths (jpg, png, gif, webp, heic, mp4, mov, avi, mkv, webm, m4v) |
|
|
120
|
+
| `--media-id <refs>` | Comma-separated media library IDs or public URLs |
|
|
121
|
+
| `--format <format>` | `short_video`, `story`, `feed_video`, `photo_single`, `photo_carousel`, `text_post`, `document_post` |
|
|
122
|
+
| `-a, --at <datetime>` | Schedule (ISO 8601) |
|
|
123
|
+
| `--ai` | Generate the caption with AI (platform-specific, derived from your `--to` slugs) |
|
|
124
|
+
| `-w, --wait` | Poll until done and print per-platform results |
|
|
125
|
+
| `--draft` | Save as draft instead of publishing |
|
|
126
|
+
| `--json` | Output raw JSON |
|
|
127
|
+
|
|
128
|
+
Output without `--wait`:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
✓ Post submitted
|
|
132
|
+
ID: 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
133
|
+
Check status: socifyr status 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Output with `--wait`:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
✓ published
|
|
140
|
+
|
|
141
|
+
✓ youtube → https://youtube.com/shorts/6zaztTmk3Ck
|
|
142
|
+
✗ instagram
|
|
143
|
+
Requires instagram_content_publish permission
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Validation rules enforced before submitting: only one video per post, recognized extensions, non-empty content, valid `--format`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `socifyr status <id>`
|
|
151
|
+
|
|
152
|
+
Check a post by upload ID, draft ID, or requestId:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
socifyr status 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
156
|
+
# Post 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
157
|
+
# Status: partial_failure
|
|
158
|
+
#
|
|
159
|
+
# ✓ youtube → https://youtube.com/shorts/6zaztTmk3Ck
|
|
160
|
+
# ✗ instagram
|
|
161
|
+
# Requires instagram_content_publish permission
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Flags: `--json`, `-w, --wait` (poll until terminal).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### `socifyr retry <id>`
|
|
169
|
+
|
|
170
|
+
Re-publish only the **failed** channels of a partial/failed post:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
socifyr retry 6b1f0c2a9d3e4f5a6b7c8d9e --wait
|
|
174
|
+
# Retrying failed channels...
|
|
175
|
+
# Waiting for results...
|
|
176
|
+
#
|
|
177
|
+
# Post 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
178
|
+
# Status: completed
|
|
179
|
+
#
|
|
180
|
+
# ✓ youtube → https://youtube.com/shorts/6zaztTmk3Ck
|
|
181
|
+
# ✓ instagram → https://instagram.com/p/DEF123/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Flags: `--json`, `-w, --wait`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### `socifyr publish <id>`
|
|
189
|
+
|
|
190
|
+
Publish a saved draft immediately:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
socifyr publish 6b1f0c2a9d3e4f5a6b7c8d9e --wait
|
|
194
|
+
# Publishing draft...
|
|
195
|
+
# Waiting for results...
|
|
196
|
+
#
|
|
197
|
+
# Post 6b1f0c2a9d3e4f5a6b7c8d9e
|
|
198
|
+
# Status: completed
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Flags: `--json`, `-w, --wait`.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `socifyr media`
|
|
206
|
+
|
|
207
|
+
Manage your media library.
|
|
208
|
+
|
|
209
|
+
**`media list`** — flags: `--type <image|video|document>`, `--search <query>`, `--limit <n>` (default 20), `--json`:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
socifyr media list --type video
|
|
213
|
+
# Media library (2 total)
|
|
214
|
+
#
|
|
215
|
+
# 🎬 launch-teaser.mp4
|
|
216
|
+
# ID: 6a93a4bfaa1135c3d8dd3df5 Size: 8.4MB video
|
|
217
|
+
# 🎬 demo-full.mp4
|
|
218
|
+
# ID: 6a93b7c2d1e0f3a4b5c6d7e8 Size: 156.2MB video
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**`media upload <file>`** — flags: `--alt <text>` (alt text/description), `--json`:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
socifyr media upload banner.png --alt "Product banner"
|
|
225
|
+
# Uploading banner.png...
|
|
226
|
+
# ✓ Uploaded
|
|
227
|
+
# ID: 6a937894df74747da8fdb8ba
|
|
228
|
+
# File: banner.png
|
|
229
|
+
# Size: 245.3KB
|
|
230
|
+
# URL: https://cdn.socifyr.com/...
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**`media get <id>`** — flags: `--json`:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
socifyr media get 6a937894df74747da8fdb8ba
|
|
237
|
+
# 🖼 banner.png
|
|
238
|
+
# ID: 6a937894df74747da8fdb8ba
|
|
239
|
+
# Type: image
|
|
240
|
+
# Alt: Product banner
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**`media delete <ids...>`** — one or many (bulk-deletes when given several), flags: `--json`:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
socifyr media delete 6a937894df74747da8fdb8ba
|
|
247
|
+
# ✓ Deleted 1 item
|
|
248
|
+
|
|
249
|
+
socifyr media delete 6a93... 6a93... 6a93...
|
|
250
|
+
# ✓ Deleted 3 items
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Exit codes
|
|
256
|
+
|
|
257
|
+
`0` on success, `1` on any failure (missing config, API error, validation). The error message from the API is printed on stderr — safe for scripting:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
socifyr post "hi" --to nonexistent-slug || echo "post failed"
|
|
261
|
+
# ✗ Failed to post
|
|
262
|
+
# Connection "nonexistent-slug" not found
|
|
263
|
+
# post failed
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## JSON mode
|
|
267
|
+
|
|
268
|
+
Every command accepts `--json` for machine-readable output (consumed on stdout; progress messages go to stderr):
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
socifyr status 6b1f0c2a... --json | jq '.status'
|
|
272
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/configure.ts
|
|
30
|
+
var import_chalk = __toESM(require("chalk"));
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var import_os = require("os");
|
|
34
|
+
var import_path = require("path");
|
|
35
|
+
var import_fs = require("fs");
|
|
36
|
+
var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".socifyr");
|
|
37
|
+
var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
|
|
38
|
+
function readConfig() {
|
|
39
|
+
if (!(0, import_fs.existsSync)(CONFIG_FILE)) return {};
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writeConfig(patch) {
|
|
47
|
+
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
48
|
+
const current = readConfig();
|
|
49
|
+
(0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify({ ...current, ...patch }, null, 2), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
function resolveApiKey() {
|
|
52
|
+
return process.env.SOCIFYR_API_KEY || readConfig().apiKey;
|
|
53
|
+
}
|
|
54
|
+
function resolveWorkspaceId() {
|
|
55
|
+
return process.env.SOCIFYR_WORKSPACE_ID || readConfig().workspaceId;
|
|
56
|
+
}
|
|
57
|
+
function resolveApiUrl() {
|
|
58
|
+
return process.env.SOCIFYR_API_URL || readConfig().apiUrl || "https://api.socifyr.com/api";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/commands/configure.ts
|
|
62
|
+
function registerConfigure(program2) {
|
|
63
|
+
program2.command("configure").description("Save your API key and workspace ID").option("-k, --key <key>", "API key (sk_...)").option("-w, --workspace <id>", "Workspace ID").option("--api-url <url>", "API base URL (for self-hosted)").option("--show", "Print current configuration").action((opts) => {
|
|
64
|
+
if (opts.show) {
|
|
65
|
+
const key = resolveApiKey();
|
|
66
|
+
const workspace = resolveWorkspaceId();
|
|
67
|
+
console.log(import_chalk.default.bold("Current configuration:"));
|
|
68
|
+
console.log(
|
|
69
|
+
` API key: ${key ? import_chalk.default.green(key.slice(0, 10) + "...") : import_chalk.default.dim("not set")}`
|
|
70
|
+
);
|
|
71
|
+
console.log(` Workspace: ${workspace ? import_chalk.default.green(workspace) : import_chalk.default.dim("not set")}`);
|
|
72
|
+
const config = readConfig();
|
|
73
|
+
if (config.apiUrl) console.log(` API URL: ${import_chalk.default.green(config.apiUrl)}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!opts.key && !opts.workspace && !opts.apiUrl) {
|
|
77
|
+
console.log(import_chalk.default.yellow("Usage:"));
|
|
78
|
+
console.log(` socifyr configure --key sk_... --workspace <id>`);
|
|
79
|
+
console.log(
|
|
80
|
+
`
|
|
81
|
+
Get your API key and workspace ID at ${import_chalk.default.underline("https://socifyr.com/dashboard/integrations#api-keys")}`
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const patch = {};
|
|
86
|
+
if (opts.key) patch.apiKey = opts.key;
|
|
87
|
+
if (opts.workspace) patch.workspaceId = opts.workspace;
|
|
88
|
+
if (opts.apiUrl) patch.apiUrl = opts.apiUrl;
|
|
89
|
+
writeConfig(patch);
|
|
90
|
+
console.log(import_chalk.default.green("\u2713 Configuration saved"));
|
|
91
|
+
if (opts.key) console.log(` API key: ${import_chalk.default.dim(opts.key.slice(0, 10) + "...")}`);
|
|
92
|
+
if (opts.workspace) console.log(` Workspace: ${import_chalk.default.dim(opts.workspace)}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/commands/connections.ts
|
|
97
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
98
|
+
|
|
99
|
+
// src/client.ts
|
|
100
|
+
var import_sdk = require("@socifyr/sdk");
|
|
101
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
102
|
+
function createClient() {
|
|
103
|
+
const apiKey = resolveApiKey();
|
|
104
|
+
const workspaceId = resolveWorkspaceId();
|
|
105
|
+
const apiUrl = resolveApiUrl();
|
|
106
|
+
if (!apiKey) {
|
|
107
|
+
console.error(import_chalk2.default.red("\u2717 No API key configured."));
|
|
108
|
+
console.error(
|
|
109
|
+
` Run ${import_chalk2.default.cyan("socifyr configure --key sk_...")} or set ${import_chalk2.default.cyan("SOCIFYR_API_KEY")}`
|
|
110
|
+
);
|
|
111
|
+
console.error(
|
|
112
|
+
` Get an API key at ${import_chalk2.default.underline("https://socifyr.com/dashboard/integrations#api-keys")}`
|
|
113
|
+
);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (!workspaceId) {
|
|
117
|
+
console.error(import_chalk2.default.red("\u2717 No workspace ID configured."));
|
|
118
|
+
console.error(
|
|
119
|
+
` Run ${import_chalk2.default.cyan("socifyr configure --workspace <id>")} or set ${import_chalk2.default.cyan("SOCIFYR_WORKSPACE_ID")}`
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
return new import_sdk.Socifyr({ apiKey, workspaceId, apiUrl });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/commands/connections.ts
|
|
127
|
+
var PLATFORM_ICONS = {
|
|
128
|
+
instagram: "\u{1F4F8}",
|
|
129
|
+
facebook: "\u{1F465}",
|
|
130
|
+
twitter: "\u{1F426}",
|
|
131
|
+
x: "\u2715 ",
|
|
132
|
+
linkedin: "\u{1F4BC}",
|
|
133
|
+
tiktok: "\u{1F3B5}",
|
|
134
|
+
youtube: "\u25B6\uFE0F ",
|
|
135
|
+
pinterest: "\u{1F4CC}",
|
|
136
|
+
reddit: "\u{1F534}",
|
|
137
|
+
bluesky: "\u{1F98B}",
|
|
138
|
+
threads: "\u{1F9F5}",
|
|
139
|
+
mastodon: "\u{1F418}"
|
|
140
|
+
};
|
|
141
|
+
function registerConnections(program2) {
|
|
142
|
+
program2.command("connections").alias("conn").description("List connected social accounts").option("--json", "Output as JSON").action(async (opts) => {
|
|
143
|
+
const client = createClient();
|
|
144
|
+
const workspaceId = resolveWorkspaceId();
|
|
145
|
+
try {
|
|
146
|
+
const connections = await client.workspaces.listConnections(workspaceId);
|
|
147
|
+
const active = connections.filter((c) => !c.disconnectedAt);
|
|
148
|
+
if (opts.json) {
|
|
149
|
+
console.log(JSON.stringify(active, null, 2));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (active.length === 0) {
|
|
153
|
+
console.log(import_chalk3.default.yellow("No connected accounts found."));
|
|
154
|
+
console.log(`Connect accounts at ${import_chalk3.default.underline("https://socifyr.com/dashboard/integrations")}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log(import_chalk3.default.bold(`
|
|
158
|
+
${active.length} connected account${active.length === 1 ? "" : "s"}:
|
|
159
|
+
`));
|
|
160
|
+
for (const c of active) {
|
|
161
|
+
const icon = PLATFORM_ICONS[c.platform] ?? "\u{1F517}";
|
|
162
|
+
const name = c.targetName ?? c.platformUsername;
|
|
163
|
+
const expiry = c.expiresAt ? new Date(c.expiresAt) : null;
|
|
164
|
+
const isExpiringSoon = expiry && expiry.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1e3;
|
|
165
|
+
console.log(` ${icon} ${import_chalk3.default.bold(c.slug)}`);
|
|
166
|
+
console.log(` ${import_chalk3.default.dim(c.platform)} \xB7 ${name}`);
|
|
167
|
+
if (isExpiringSoon) {
|
|
168
|
+
console.log(` ${import_chalk3.default.yellow(`\u26A0 Token expires ${expiry.toLocaleDateString()}`)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(import_chalk3.default.dim('Use slugs with: socifyr post "text" --to <slug1,slug2>'));
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error(import_chalk3.default.red("\u2717 Failed to fetch connections"));
|
|
175
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/commands/post.ts
|
|
182
|
+
var import_fs2 = require("fs");
|
|
183
|
+
var import_path2 = require("path");
|
|
184
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
185
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic"]);
|
|
186
|
+
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"]);
|
|
187
|
+
function parseConnections(raw) {
|
|
188
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
189
|
+
}
|
|
190
|
+
function parseMediaPaths(raw) {
|
|
191
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
192
|
+
}
|
|
193
|
+
function detectMediaType(paths) {
|
|
194
|
+
if (paths.length === 0) return null;
|
|
195
|
+
const ext = (0, import_path2.extname)(paths[0]).toLowerCase();
|
|
196
|
+
if (IMAGE_EXTS.has(ext)) return "photo";
|
|
197
|
+
if (VIDEO_EXTS.has(ext)) return "video";
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function formatStatus(status) {
|
|
201
|
+
switch (status) {
|
|
202
|
+
case "completed":
|
|
203
|
+
return import_chalk4.default.green("\u2713 published");
|
|
204
|
+
case "failed":
|
|
205
|
+
return import_chalk4.default.red("\u2717 failed");
|
|
206
|
+
case "partial_failure":
|
|
207
|
+
return import_chalk4.default.yellow("\u26A0 partial failure");
|
|
208
|
+
case "processing":
|
|
209
|
+
return import_chalk4.default.blue("\u27F3 processing");
|
|
210
|
+
case "publishing":
|
|
211
|
+
return import_chalk4.default.blue("\u27F3 publishing");
|
|
212
|
+
default:
|
|
213
|
+
return import_chalk4.default.dim(status);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function printResult(result) {
|
|
217
|
+
console.log(`
|
|
218
|
+
${formatStatus(result.status)}
|
|
219
|
+
`);
|
|
220
|
+
for (const ch of result.channels ?? []) {
|
|
221
|
+
const r = ch.result;
|
|
222
|
+
const icon = r?.status === "success" ? import_chalk4.default.green("\u2713") : import_chalk4.default.red("\u2717");
|
|
223
|
+
const link = r?.postUrl ? import_chalk4.default.dim(` \u2192 ${r.postUrl}`) : "";
|
|
224
|
+
console.log(` ${icon} ${import_chalk4.default.bold(ch.platform)}${link}`);
|
|
225
|
+
if (r?.error) console.log(` ${import_chalk4.default.red(r.error)}`);
|
|
226
|
+
}
|
|
227
|
+
console.log();
|
|
228
|
+
}
|
|
229
|
+
var VALID_FORMATS = ["short_video", "story", "feed_video", "photo_single", "photo_carousel", "text_post", "document_post"];
|
|
230
|
+
function registerPost(program2) {
|
|
231
|
+
program2.command("post [text]").description("Post to social platforms \u2014 text, images, or video").option("-t, --to <connections>", "Comma-separated connection slugs (e.g. twitter-me,linkedin-me)").option("-f, --file <path>", "Read caption from a text file").option("-m, --media <paths>", "Comma-separated local file paths (auto-detects image vs video)").option("--media-id <refs>", "Comma-separated media library IDs or public URLs (no upload needed)").option("--format <format>", `Content format: ${VALID_FORMATS.join(", ")}`).option("-a, --at <datetime>", "Schedule for a specific time (ISO 8601)").option("--ai", "Use AI to generate platform-specific captions").option("-w, --wait", "Wait for completion and show per-platform results").option("--json", "Output as JSON").option("--draft", "Save as draft without publishing").addHelpText("after", `
|
|
232
|
+
Media examples:
|
|
233
|
+
$ socifyr post "Check this out!" --media photo.jpg --to instagram-me
|
|
234
|
+
$ socifyr post --media video.mp4 --to tiktok-me,instagram-me --ai --wait
|
|
235
|
+
$ socifyr post "New product" --media img1.jpg,img2.jpg --to instagram-me
|
|
236
|
+
$ socifyr post "Reuse library asset" --media-id 6a04f715d3d7d93575e65d13 --to instagram-me
|
|
237
|
+
$ socifyr post "From URL" --media-id https://example.com/banner.jpg --to linkedin-me
|
|
238
|
+
$ socifyr post "Reel" --media video.mp4 --format short_video --to instagram-me
|
|
239
|
+
`).action(async (text, opts) => {
|
|
240
|
+
let caption = text ?? "";
|
|
241
|
+
if (opts.file) {
|
|
242
|
+
if (!(0, import_fs2.existsSync)(opts.file)) {
|
|
243
|
+
console.error(import_chalk4.default.red(`\u2717 File not found: ${opts.file}`));
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
caption = (0, import_fs2.readFileSync)(opts.file, "utf-8").trim();
|
|
247
|
+
}
|
|
248
|
+
if (!caption && !process.stdin.isTTY) {
|
|
249
|
+
caption = (0, import_fs2.readFileSync)("/dev/stdin", "utf-8").trim();
|
|
250
|
+
}
|
|
251
|
+
const mediaPaths = opts.media ? parseMediaPaths(opts.media) : [];
|
|
252
|
+
const mediaType = detectMediaType(mediaPaths);
|
|
253
|
+
if (mediaPaths.length > 0) {
|
|
254
|
+
for (const p of mediaPaths) {
|
|
255
|
+
if (!(0, import_fs2.existsSync)(p)) {
|
|
256
|
+
console.error(import_chalk4.default.red(`\u2717 Media file not found: ${p}`));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!mediaType) {
|
|
261
|
+
console.error(import_chalk4.default.red(`\u2717 Unrecognized media type: ${(0, import_path2.extname)(mediaPaths[0])}`));
|
|
262
|
+
console.error(` Supported: ${[...IMAGE_EXTS, ...VIDEO_EXTS].join(", ")}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const mediaIdRefs = opts.mediaId ? parseMediaPaths(opts.mediaId) : [];
|
|
267
|
+
const client = createClient();
|
|
268
|
+
let effectiveMediaType = mediaType;
|
|
269
|
+
if (effectiveMediaType === null && mediaIdRefs.length > 0) {
|
|
270
|
+
const isUrlRef = (ref) => ref.startsWith("http://") || ref.startsWith("https://");
|
|
271
|
+
let libraryTypes = [];
|
|
272
|
+
try {
|
|
273
|
+
libraryTypes = await Promise.all(
|
|
274
|
+
mediaIdRefs.filter((ref) => !isUrlRef(ref)).map(async (id) => (await client.media.get(id)).type)
|
|
275
|
+
);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(import_chalk4.default.red("\u2717 Failed to resolve media library ID"));
|
|
278
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
const hasVideoRef = libraryTypes.includes("video") || mediaIdRefs.some(
|
|
282
|
+
(ref) => isUrlRef(ref) && VIDEO_EXTS.has((0, import_path2.extname)(ref.split("?")[0]).toLowerCase())
|
|
283
|
+
);
|
|
284
|
+
effectiveMediaType = hasVideoRef ? "video" : libraryTypes.includes("document") ? "document" : "photo";
|
|
285
|
+
}
|
|
286
|
+
if (effectiveMediaType === "video" && mediaPaths.length + mediaIdRefs.length > 1) {
|
|
287
|
+
console.error(import_chalk4.default.red("\u2717 Only one video file can be posted at a time"));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (opts.format && !VALID_FORMATS.includes(opts.format)) {
|
|
291
|
+
console.error(import_chalk4.default.red(`\u2717 Invalid format: ${opts.format}`));
|
|
292
|
+
console.error(` Valid formats: ${VALID_FORMATS.join(", ")}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
if (!caption && !opts.ai && mediaPaths.length === 0 && mediaIdRefs.length === 0) {
|
|
296
|
+
console.error(import_chalk4.default.red("\u2717 No content provided."));
|
|
297
|
+
console.error(" Pass text inline, use --file, --media, --media-id, or pipe via stdin.");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
if (!opts.to) {
|
|
301
|
+
console.error(import_chalk4.default.red("\u2717 No connections specified."));
|
|
302
|
+
console.error(` Use ${import_chalk4.default.cyan("socifyr connections")} to see available slugs`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const connections = parseConnections(opts.to);
|
|
306
|
+
try {
|
|
307
|
+
if (opts.ai) {
|
|
308
|
+
if (!opts.json) process.stderr.write(import_chalk4.default.dim("Generating AI captions...\n"));
|
|
309
|
+
const uploadType = effectiveMediaType ?? "text";
|
|
310
|
+
const platforms = Array.from(new Set(
|
|
311
|
+
connections.map((c) => c.split("-")[0]).filter(Boolean)
|
|
312
|
+
));
|
|
313
|
+
const generated = await client.ai.generateContent({
|
|
314
|
+
uploadType,
|
|
315
|
+
platforms,
|
|
316
|
+
description: caption || void 0
|
|
317
|
+
});
|
|
318
|
+
const firstCaption = Object.values(generated.content ?? {})[0];
|
|
319
|
+
caption = firstCaption ?? caption;
|
|
320
|
+
}
|
|
321
|
+
const action = opts.draft ? "Saving draft" : opts.at ? `Scheduling for ${opts.at}` : "Publishing";
|
|
322
|
+
if (!opts.json) {
|
|
323
|
+
const totalMediaCount = mediaPaths.length + mediaIdRefs.length;
|
|
324
|
+
const typeLabel = effectiveMediaType === "video" ? "video" : effectiveMediaType === "document" ? `${totalMediaCount} document${totalMediaCount > 1 ? "s" : ""}` : effectiveMediaType === "photo" ? `${totalMediaCount} image${totalMediaCount > 1 ? "s" : ""}` : "text post";
|
|
325
|
+
process.stderr.write(import_chalk4.default.dim(`${action} ${typeLabel} to ${connections.length} account${connections.length === 1 ? "" : "s"}...
|
|
326
|
+
`));
|
|
327
|
+
}
|
|
328
|
+
const baseParams = {
|
|
329
|
+
connections,
|
|
330
|
+
text: caption || void 0,
|
|
331
|
+
scheduledAt: opts.at,
|
|
332
|
+
saveDraft: opts.draft,
|
|
333
|
+
...opts.format ? { contentFormat: opts.format } : {}
|
|
334
|
+
};
|
|
335
|
+
const uploadedIds = [];
|
|
336
|
+
for (const p of mediaPaths) {
|
|
337
|
+
if (!opts.json) {
|
|
338
|
+
process.stderr.write(import_chalk4.default.dim(`Uploading ${(0, import_path2.basename)(p)}...
|
|
339
|
+
`));
|
|
340
|
+
}
|
|
341
|
+
const item = await client.media.upload((0, import_fs2.readFileSync)(p), (0, import_path2.basename)(p));
|
|
342
|
+
uploadedIds.push(item.id);
|
|
343
|
+
}
|
|
344
|
+
const allMedias = [...uploadedIds, ...mediaIdRefs];
|
|
345
|
+
let upload;
|
|
346
|
+
if (effectiveMediaType === "video") {
|
|
347
|
+
upload = await client.uploads.video({
|
|
348
|
+
...baseParams,
|
|
349
|
+
medias: [allMedias[0]]
|
|
350
|
+
});
|
|
351
|
+
} else if (effectiveMediaType === "document") {
|
|
352
|
+
upload = await client.uploads.document({
|
|
353
|
+
...baseParams,
|
|
354
|
+
medias: allMedias
|
|
355
|
+
});
|
|
356
|
+
} else if (effectiveMediaType === "photo") {
|
|
357
|
+
upload = await client.uploads.photos({
|
|
358
|
+
...baseParams,
|
|
359
|
+
medias: allMedias
|
|
360
|
+
});
|
|
361
|
+
} else {
|
|
362
|
+
if (!caption) {
|
|
363
|
+
console.error(import_chalk4.default.red("\u2717 Text post requires non-empty content."));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
upload = await client.uploads.text({ ...baseParams, text: caption });
|
|
367
|
+
}
|
|
368
|
+
if (opts.wait && !opts.draft) {
|
|
369
|
+
if (!opts.json) process.stderr.write(import_chalk4.default.dim("Waiting for results...\n"));
|
|
370
|
+
const result = await client.uploads.waitForCompletion(upload.id);
|
|
371
|
+
if (opts.json) {
|
|
372
|
+
console.log(JSON.stringify(result, null, 2));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
printResult(result);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (opts.json) {
|
|
379
|
+
console.log(JSON.stringify(upload, null, 2));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const postId = upload.id;
|
|
383
|
+
const actionLabel = opts.draft ? "Draft saved" : opts.at ? "Scheduled" : "Post submitted";
|
|
384
|
+
console.log(import_chalk4.default.green(`\u2713 ${actionLabel}`));
|
|
385
|
+
console.log(` ID: ${import_chalk4.default.dim(postId)}`);
|
|
386
|
+
if (!opts.draft && !opts.at) {
|
|
387
|
+
console.log(` Check status: ${import_chalk4.default.cyan(`socifyr status ${postId}`)}`);
|
|
388
|
+
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(import_chalk4.default.red("\u2717 Failed to post"));
|
|
391
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/commands/status.ts
|
|
398
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
399
|
+
function statusColor(status) {
|
|
400
|
+
switch (status) {
|
|
401
|
+
case "completed":
|
|
402
|
+
return import_chalk5.default.green(status);
|
|
403
|
+
case "failed":
|
|
404
|
+
return import_chalk5.default.red(status);
|
|
405
|
+
case "partial_failure":
|
|
406
|
+
return import_chalk5.default.yellow(status);
|
|
407
|
+
case "processing":
|
|
408
|
+
case "publishing":
|
|
409
|
+
return import_chalk5.default.blue(status);
|
|
410
|
+
default:
|
|
411
|
+
return import_chalk5.default.dim(status);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function registerStatus(program2) {
|
|
415
|
+
program2.command("status <id>").description("Check the status of a post").option("--json", "Output as JSON").option("-w, --wait", "Poll until complete").action(async (id, opts) => {
|
|
416
|
+
const client = createClient();
|
|
417
|
+
try {
|
|
418
|
+
const result = opts.wait ? await client.uploads.waitForCompletion(id) : await client.uploads.getStatus(id);
|
|
419
|
+
if (opts.json) {
|
|
420
|
+
console.log(JSON.stringify(result, null, 2));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(`
|
|
424
|
+
Post ${import_chalk5.default.bold(id)}`);
|
|
425
|
+
console.log(`Status: ${statusColor(result.status)}
|
|
426
|
+
`);
|
|
427
|
+
for (const ch of result.channels ?? []) {
|
|
428
|
+
const r = ch.result;
|
|
429
|
+
const icon = r?.status === "success" ? import_chalk5.default.green("\u2713") : r?.status === "failed" ? import_chalk5.default.red("\u2717") : import_chalk5.default.dim("\xB7");
|
|
430
|
+
const link = r?.postUrl ? import_chalk5.default.dim(` \u2192 ${r.postUrl}`) : "";
|
|
431
|
+
console.log(` ${icon} ${import_chalk5.default.bold(ch.platform)}${link}`);
|
|
432
|
+
if (r?.error) console.log(` ${import_chalk5.default.red(r.error)}`);
|
|
433
|
+
}
|
|
434
|
+
console.log();
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error(import_chalk5.default.red("\u2717 Failed to fetch status"));
|
|
437
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/commands/manage.ts
|
|
444
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
445
|
+
function registerManage(program2) {
|
|
446
|
+
program2.command("retry <id>").description("Retry failed channels for a post").option("--json", "Output as JSON").option("-w, --wait", "Wait for completion and show results").action(async (id, opts) => {
|
|
447
|
+
const client = createClient();
|
|
448
|
+
try {
|
|
449
|
+
if (!opts.json) process.stderr.write(import_chalk6.default.dim("Retrying failed channels...\n"));
|
|
450
|
+
const upload = await client.uploads.retry(id);
|
|
451
|
+
if (opts.wait) {
|
|
452
|
+
if (!opts.json) process.stderr.write(import_chalk6.default.dim("Waiting for results...\n"));
|
|
453
|
+
const result = await client.uploads.waitForCompletion(id);
|
|
454
|
+
if (opts.json) {
|
|
455
|
+
console.log(JSON.stringify(result, null, 2));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
console.log(`
|
|
459
|
+
Post ${import_chalk6.default.bold(id)}`);
|
|
460
|
+
console.log(`Status: ${import_chalk6.default.green(result.status)}
|
|
461
|
+
`);
|
|
462
|
+
for (const ch of result.channels ?? []) {
|
|
463
|
+
const r = ch.result;
|
|
464
|
+
const icon = r?.status === "success" ? import_chalk6.default.green("\u2713") : r?.status === "failed" ? import_chalk6.default.red("\u2717") : import_chalk6.default.dim("\xB7");
|
|
465
|
+
const link = r?.postUrl ? import_chalk6.default.dim(` \u2192 ${r.postUrl}`) : "";
|
|
466
|
+
console.log(` ${icon} ${import_chalk6.default.bold(ch.platform)}${link}`);
|
|
467
|
+
if (r?.error) console.log(` ${import_chalk6.default.red(r.error)}`);
|
|
468
|
+
}
|
|
469
|
+
console.log();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (opts.json) {
|
|
473
|
+
console.log(JSON.stringify(upload, null, 2));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
console.log(import_chalk6.default.green("\u2713 Retry queued"));
|
|
477
|
+
console.log(` ID: ${import_chalk6.default.dim(upload.id)}`);
|
|
478
|
+
console.log(` Check status: ${import_chalk6.default.cyan(`socifyr status ${upload.id}`)}`);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(import_chalk6.default.red("\u2717 Failed to retry"));
|
|
481
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
program2.command("publish <id>").description("Publish a saved draft").option("--json", "Output as JSON").option("-w, --wait", "Wait for completion and show results").action(async (id, opts) => {
|
|
486
|
+
const client = createClient();
|
|
487
|
+
try {
|
|
488
|
+
if (!opts.json) process.stderr.write(import_chalk6.default.dim("Publishing draft...\n"));
|
|
489
|
+
const upload = await client.uploads.publishDraft(id);
|
|
490
|
+
if (opts.wait) {
|
|
491
|
+
if (!opts.json) process.stderr.write(import_chalk6.default.dim("Waiting for results...\n"));
|
|
492
|
+
const result = await client.uploads.waitForCompletion(id);
|
|
493
|
+
if (opts.json) {
|
|
494
|
+
console.log(JSON.stringify(result, null, 2));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
console.log(`
|
|
498
|
+
Post ${import_chalk6.default.bold(id)}`);
|
|
499
|
+
console.log(`Status: ${import_chalk6.default.green(result.status)}
|
|
500
|
+
`);
|
|
501
|
+
for (const ch of result.channels ?? []) {
|
|
502
|
+
const r = ch.result;
|
|
503
|
+
const icon = r?.status === "success" ? import_chalk6.default.green("\u2713") : r?.status === "failed" ? import_chalk6.default.red("\u2717") : import_chalk6.default.dim("\xB7");
|
|
504
|
+
const link = r?.postUrl ? import_chalk6.default.dim(` \u2192 ${r.postUrl}`) : "";
|
|
505
|
+
console.log(` ${icon} ${import_chalk6.default.bold(ch.platform)}${link}`);
|
|
506
|
+
if (r?.error) console.log(` ${import_chalk6.default.red(r.error)}`);
|
|
507
|
+
}
|
|
508
|
+
console.log();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (opts.json) {
|
|
512
|
+
console.log(JSON.stringify(upload, null, 2));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
console.log(import_chalk6.default.green("\u2713 Draft published"));
|
|
516
|
+
console.log(` ID: ${import_chalk6.default.dim(upload.id)}`);
|
|
517
|
+
console.log(` Check status: ${import_chalk6.default.cyan(`socifyr status ${upload.id}`)}`);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
console.error(import_chalk6.default.red("\u2717 Failed to publish draft"));
|
|
520
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/commands/media.ts
|
|
527
|
+
var import_fs3 = require("fs");
|
|
528
|
+
var import_path3 = require("path");
|
|
529
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
530
|
+
var TYPE_ICON = {
|
|
531
|
+
image: "\u{1F5BC} ",
|
|
532
|
+
video: "\u{1F3AC}",
|
|
533
|
+
document: "\u{1F4C4}"
|
|
534
|
+
};
|
|
535
|
+
function formatBytes(bytes) {
|
|
536
|
+
if (!bytes) return "\u2014";
|
|
537
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
538
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
539
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
540
|
+
}
|
|
541
|
+
function registerMedia(program2) {
|
|
542
|
+
const media = program2.command("media").description("Manage your media library");
|
|
543
|
+
media.command("list").description("List media library items").option("--type <type>", "Filter by type: image, video, document").option("--search <query>", "Search by filename").option("--limit <n>", "Max results (default: 20)", "20").option("--json", "Output as JSON").action(async (opts) => {
|
|
544
|
+
const client = createClient();
|
|
545
|
+
try {
|
|
546
|
+
const result = await client.media.list({
|
|
547
|
+
type: opts.type,
|
|
548
|
+
search: opts.search,
|
|
549
|
+
limit: parseInt(opts.limit ?? "20", 10)
|
|
550
|
+
});
|
|
551
|
+
if (opts.json) {
|
|
552
|
+
console.log(JSON.stringify(result, null, 2));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const items = result.results ?? result.data ?? [];
|
|
556
|
+
if (!items || items.length === 0) {
|
|
557
|
+
console.log(import_chalk7.default.dim("No media found."));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const total = result.total ?? items.length;
|
|
561
|
+
console.log(`
|
|
562
|
+
${import_chalk7.default.bold("Media library")} (${total} total)
|
|
563
|
+
`);
|
|
564
|
+
for (const item of items) {
|
|
565
|
+
const icon = TYPE_ICON[item.type] ?? " ";
|
|
566
|
+
console.log(` ${icon} ${import_chalk7.default.bold(item.name)}`);
|
|
567
|
+
console.log(` ${import_chalk7.default.dim(`ID: ${item.id} Size: ${formatBytes(item.sizeBytes)} ${item.type}`)}`);
|
|
568
|
+
if (item.alt) console.log(` ${import_chalk7.default.dim(`Alt: ${item.alt}`)}`);
|
|
569
|
+
}
|
|
570
|
+
console.log();
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.error(import_chalk7.default.red("\u2717 Failed to list media"));
|
|
573
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
media.command("upload <file>").description("Upload a file to the media library").option("--alt <text>", "Alt text / description").option("--json", "Output as JSON").action(async (file, opts) => {
|
|
578
|
+
if (!(0, import_fs3.existsSync)(file)) {
|
|
579
|
+
console.error(import_chalk7.default.red(`\u2717 File not found: ${file}`));
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
const client = createClient();
|
|
583
|
+
try {
|
|
584
|
+
if (!opts.json) process.stderr.write(import_chalk7.default.dim(`Uploading ${(0, import_path3.basename)(file)}...
|
|
585
|
+
`));
|
|
586
|
+
const item = await client.media.upload((0, import_fs3.readFileSync)(file), (0, import_path3.basename)(file));
|
|
587
|
+
if (opts.alt) {
|
|
588
|
+
await client.media.update(item.id, { alt: opts.alt });
|
|
589
|
+
}
|
|
590
|
+
if (opts.json) {
|
|
591
|
+
console.log(JSON.stringify(item, null, 2));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
console.log(import_chalk7.default.green(`\u2713 Uploaded`));
|
|
595
|
+
console.log(` ID: ${import_chalk7.default.cyan(item.id)}`);
|
|
596
|
+
console.log(` File: ${import_chalk7.default.dim(item.name)}`);
|
|
597
|
+
console.log(` Size: ${import_chalk7.default.dim(formatBytes(item.sizeBytes))}`);
|
|
598
|
+
console.log(` URL: ${import_chalk7.default.dim(item.url)}`);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(import_chalk7.default.red("\u2717 Upload failed"));
|
|
601
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
media.command("get <id>").description("Get details of a media item").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
606
|
+
const client = createClient();
|
|
607
|
+
try {
|
|
608
|
+
const item = await client.media.get(id);
|
|
609
|
+
if (opts.json) {
|
|
610
|
+
console.log(JSON.stringify(item, null, 2));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const icon = TYPE_ICON[item.type] ?? " ";
|
|
614
|
+
console.log(`
|
|
615
|
+
${icon} ${import_chalk7.default.bold(item.name)}`);
|
|
616
|
+
console.log(` ID: ${import_chalk7.default.cyan(item.id)}`);
|
|
617
|
+
console.log(` Type: ${import_chalk7.default.dim(item.type)}`);
|
|
618
|
+
console.log(` Size: ${import_chalk7.default.dim(formatBytes(item.sizeBytes))}`);
|
|
619
|
+
console.log(` URL: ${import_chalk7.default.dim(item.url)}`);
|
|
620
|
+
if (item.alt) console.log(` Alt: ${import_chalk7.default.dim(item.alt)}`);
|
|
621
|
+
if (item.description) console.log(` Desc: ${import_chalk7.default.dim(item.description)}`);
|
|
622
|
+
if (item.tags?.length) console.log(` Tags: ${import_chalk7.default.dim(item.tags.join(", "))}`);
|
|
623
|
+
console.log();
|
|
624
|
+
} catch (err) {
|
|
625
|
+
console.error(import_chalk7.default.red("\u2717 Failed to get media"));
|
|
626
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
media.command("delete <ids...>").description("Delete one or more media items").option("--json", "Output as JSON").action(async (ids, opts) => {
|
|
631
|
+
const client = createClient();
|
|
632
|
+
try {
|
|
633
|
+
if (ids.length === 1) {
|
|
634
|
+
await client.media.delete(ids[0]);
|
|
635
|
+
} else {
|
|
636
|
+
await client.media.bulkDelete(ids);
|
|
637
|
+
}
|
|
638
|
+
if (opts.json) {
|
|
639
|
+
console.log(JSON.stringify({ deleted: ids }));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
console.log(import_chalk7.default.green(`\u2713 Deleted ${ids.length} item${ids.length === 1 ? "" : "s"}`));
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error(import_chalk7.default.red("\u2717 Delete failed"));
|
|
645
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/cli.ts
|
|
652
|
+
import_commander.program.name("socifyr").description("Post to 10+ social platforms from your terminal").version("0.1.0").addHelpText("after", `
|
|
653
|
+
Examples:
|
|
654
|
+
$ socifyr configure --key sk_... --workspace <id>
|
|
655
|
+
$ socifyr connections
|
|
656
|
+
$ socifyr post "Just shipped v2.0 \u{1F680}" --to twitter-me,linkedin-me
|
|
657
|
+
$ socifyr post --file CHANGELOG.md --to linkedin-me --ai
|
|
658
|
+
$ socifyr post "Deploy complete" --to twitter-me --at "2026-06-01T09:00"
|
|
659
|
+
$ cat release-notes.md | socifyr post --to linkedin-me --wait
|
|
660
|
+
$ socifyr status <post-id>
|
|
661
|
+
$ socifyr retry <post-id> --wait
|
|
662
|
+
$ socifyr publish <draft-id>
|
|
663
|
+
$ socifyr media list --type image
|
|
664
|
+
$ socifyr media upload banner.png --alt "Product banner"
|
|
665
|
+
$ socifyr media delete <media-id>
|
|
666
|
+
|
|
667
|
+
Env vars (override stored config):
|
|
668
|
+
SOCIFYR_API_KEY Your API key
|
|
669
|
+
SOCIFYR_WORKSPACE_ID Your workspace ID
|
|
670
|
+
SOCIFYR_API_URL Custom API URL (self-hosted)
|
|
671
|
+
`);
|
|
672
|
+
registerConfigure(import_commander.program);
|
|
673
|
+
registerConnections(import_commander.program);
|
|
674
|
+
registerPost(import_commander.program);
|
|
675
|
+
registerStatus(import_commander.program);
|
|
676
|
+
registerManage(import_commander.program);
|
|
677
|
+
registerMedia(import_commander.program);
|
|
678
|
+
import_commander.program.parseAsync(process.argv).catch((err) => {
|
|
679
|
+
console.error(err instanceof Error ? err.message : err);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@socifyr/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Socifyr — post to 10+ social platforms from your terminal",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"bin": {
|
|
7
|
+
"socifyr": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"test:watch": "jest --watch"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@socifyr/sdk": "0.1.0",
|
|
26
|
+
"chalk": "^4.1.2",
|
|
27
|
+
"commander": "^12.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/jest": "^29.5.14",
|
|
31
|
+
"@types/node": "^25.5.2",
|
|
32
|
+
"jest": "^29.7.0",
|
|
33
|
+
"ts-jest": "^29.4.9",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"socifyr",
|
|
42
|
+
"social-media",
|
|
43
|
+
"cli",
|
|
44
|
+
"instagram",
|
|
45
|
+
"twitter",
|
|
46
|
+
"linkedin",
|
|
47
|
+
"tiktok"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/socifyr/socifyr",
|
|
52
|
+
"directory": "packages/cli"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://socifyr.com/docs/cli"
|
|
55
|
+
}
|