@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.
Files changed (3) hide show
  1. package/README.md +271 -2
  2. package/dist/cli.js +681 -0
  3. package/package.json +53 -4
package/README.md CHANGED
@@ -1,3 +1,272 @@
1
- # Temporary Holding Version
1
+ # socifyr
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }