mirror of
https://github.com/hanzoskill/typefully.git
synced 2026-10-30 21:05:29 +00:00
607 lines
20 KiB
JavaScript
607 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Typefully CLI - X/Twitter Scheduling via Typefully API
|
|
*
|
|
* COMPLIANT with X Automation Rules (October 2025)
|
|
* - Creates drafts by default (safe mode)
|
|
* - No automated replies, likes, or reposts
|
|
* - No bulk actions or spam
|
|
* - All content is reviewed before publishing
|
|
*
|
|
* Full implementation of Typefully Public API v2.0.0
|
|
* Supports: X, LinkedIn, Mastodon, Threads, Bluesky
|
|
*/
|
|
|
|
const API_BASE = 'https://api.typefully.com/v2';
|
|
|
|
// Compliance notice
|
|
const COMPLIANCE_NOTICE = `
|
|
🐦 TYPEFULLY CLI - X COMPLIANT AUTOMATION
|
|
|
|
✓ SAFE: Creates drafts by default (review before publishing)
|
|
✓ NO automated replies, mentions, likes, or reposts
|
|
✓ NO bulk actions or spam
|
|
✓ NO trending topic manipulation
|
|
✓ All posts are content YOU create
|
|
|
|
Your posts, your control.
|
|
`;
|
|
|
|
// Color codes
|
|
const colors = {
|
|
reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
|
|
green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
red: '\x1b[31m', cyan: '\x1b[36m', magenta: '\x1b[35m'
|
|
};
|
|
|
|
function log(text = '', color = 'reset') {
|
|
console.log(`${colors[color]}${text}${colors.reset}`);
|
|
}
|
|
|
|
function logError(text) {
|
|
console.error(`${colors.red}Error: ${text}${colors.reset}`);
|
|
}
|
|
|
|
function logCompliance() {
|
|
log(COMPLIANCE_NOTICE, 'cyan');
|
|
}
|
|
|
|
async function api(endpoint, method = 'GET', body = null, options = {}) {
|
|
const apiKey = process.env.TYPEFULLY_API_KEY;
|
|
if (!apiKey) {
|
|
logError('TYPEFULLY_API_KEY not set');
|
|
log('Run: export TYPEFULLY_API_KEY="your-key"');
|
|
process.exit(1);
|
|
}
|
|
|
|
const url = new URL(`${API_BASE}${endpoint}`);
|
|
if (options.query) {
|
|
Object.entries(options.query).forEach(([k, v]) => {
|
|
if (v !== undefined && v !== null && v !== '') url.searchParams.append(k, v);
|
|
});
|
|
}
|
|
|
|
const fetchOptions = {
|
|
method,
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
};
|
|
|
|
if (body) fetchOptions.body = JSON.stringify(body);
|
|
|
|
const response = await fetch(url.toString(), fetchOptions);
|
|
const data = await response.json().catch(() => ({}));
|
|
|
|
if (!response.ok) {
|
|
const code = data?.error?.code || response.statusText;
|
|
const msg = data?.error?.message || 'Unknown error';
|
|
logError(`${code}: ${msg}`);
|
|
if (data?.error?.details) {
|
|
data.error.details.forEach(d => log(` - ${d.message}`, 'yellow'));
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// ============ USER COMMANDS ============
|
|
|
|
async function cmdMe() {
|
|
const user = await api('/me');
|
|
log('\n👤 Current User', 'bright');
|
|
log(` Name: ${user.name}`);
|
|
log(` Email: ${user.email}`);
|
|
log(` ID: ${user.id}`);
|
|
log(` Signed up: ${new Date(user.signup_date).toLocaleDateString()}`);
|
|
if (user.api_key_label) log(` API Key: ${user.api_key_label}`, 'dim');
|
|
}
|
|
|
|
// ============ SOCIAL SETS ============
|
|
|
|
async function cmdSocialSets() {
|
|
const result = await api('/social-sets');
|
|
if (result.results?.length === 0) {
|
|
log('\nNo social sets found. Connect your X account at https://typefully.com', 'yellow');
|
|
return;
|
|
}
|
|
log('\n📱 Connected Accounts:', 'bright');
|
|
result.results.forEach(set => {
|
|
log(`\n @${set.username} (${set.name})`);
|
|
log(` ID: ${set.id}`, 'cyan');
|
|
if (set.team) log(` Team: ${set.team.name}`, 'dim');
|
|
});
|
|
}
|
|
|
|
async function cmdSocialSet(id) {
|
|
if (!id) {
|
|
logError('Social set ID required. Run: typefully social-sets');
|
|
process.exit(1);
|
|
}
|
|
const set = await api(`/social-sets/${id}/`);
|
|
log(`\n📱 ${set.name} (@${set.username})`, 'bright');
|
|
log(` ID: ${set.id}`, 'cyan');
|
|
log('\n Platforms:', 'dim');
|
|
Object.entries(set.platforms).forEach(([platform, data]) => {
|
|
if (data?.username) {
|
|
const enabled = data.enabled !== false ? '✓' : '○';
|
|
log(` ${enabled} ${platform}: @${data.username}`, enabled === '✓' ? 'green' : 'dim');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============ DRAFTS ============
|
|
|
|
async function cmdDrafts(socialSetId, options = {}) {
|
|
if (!socialSetId) {
|
|
logError('--social-set-id required. Run: typefully social-sets');
|
|
process.exit(1);
|
|
}
|
|
|
|
const query = { limit: options.limit || 10, offset: options.offset || 0 };
|
|
if (options.status) query.status = options.status;
|
|
if (options.tag) query.tag = options.tag;
|
|
if (options.orderBy) query.order_by = options.orderBy;
|
|
|
|
const result = await api(`/social-sets/${socialSetId}/drafts`, 'GET', null, { query });
|
|
|
|
if (result.results?.length === 0) {
|
|
const statusText = options.status ? ` with status "${options.status}"` : '';
|
|
log(`\nNo drafts found${statusText}.`, 'yellow');
|
|
return;
|
|
}
|
|
|
|
log(`\n📝 Drafts (${result.count} total):`, 'bright');
|
|
result.results.forEach(draft => {
|
|
const status = draft.status === 'published' ? '✅' :
|
|
draft.status === 'scheduled' ? '📅' :
|
|
draft.status === 'error' ? '❌' : '💾';
|
|
log(`\n ${status} [${draft.id}] ${(draft.preview || '(no preview)').substring(0, 55)}...`, 'bright');
|
|
if (draft.draft_title) log(` Title: ${draft.draft_title}`, 'dim');
|
|
if (draft.scheduled_date) log(` Scheduled: ${new Date(draft.scheduled_date).toLocaleString()}`, 'cyan');
|
|
});
|
|
}
|
|
|
|
async function cmdDraft(socialSetId, draftId) {
|
|
if (!socialSetId || !draftId) {
|
|
logError('Social set ID and draft ID required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const draft = await api(`/social-sets/${socialSetId}/drafts/${draftId}`);
|
|
|
|
log('\n📄 Draft Details:', 'bright');
|
|
log(` ID: ${draft.id}`, 'cyan');
|
|
log(` Status: ${draft.status}`);
|
|
log(` Created: ${new Date(draft.created_at).toLocaleString()}`);
|
|
if (draft.draft_title) log(` Title: ${draft.draft_title}`);
|
|
if (draft.preview) log(`\n Preview: "${draft.preview}"`);
|
|
if (draft.scheduled_date) log(` Scheduled: ${new Date(draft.scheduled_date).toLocaleString()}`, 'yellow');
|
|
if (draft.published_at) log(` Published: ${new Date(draft.published_at).toLocaleString()}`, 'green');
|
|
if (draft.x_published_url) log(` X URL: ${draft.x_published_url}`, 'cyan');
|
|
if (draft.private_url) log(` Edit: ${draft.private_url}`, 'dim');
|
|
if (draft.tags?.length) log(` Tags: ${draft.tags.join(', ')}`, 'dim');
|
|
}
|
|
|
|
async function cmdCreateDraft(text, options = {}) {
|
|
const { socialSetId, schedule, title, share, replyTo, community, now, x, linkedin, threads, bluesky, mastodon, all } = options;
|
|
|
|
if (!socialSetId) {
|
|
logError('--social-set-id required. Run: typefully social-sets');
|
|
process.exit(1);
|
|
}
|
|
|
|
// If no platform-specific text provided, use positional text for X only
|
|
const hasPlatformText = x || linkedin || threads || bluesky || mastodon || all;
|
|
|
|
if (!hasPlatformText && !text) {
|
|
logError('Post text required');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Build posts array from text (pass through with newlines preserved)
|
|
const buildPosts = (txt) => {
|
|
if (!txt) return [];
|
|
// Parse escape sequences (\n -> actual newline)
|
|
const unescaped = txt.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
// Return single post with newlines preserved
|
|
return [{ text: unescaped }];
|
|
};
|
|
|
|
// Build platforms object
|
|
const platforms = {};
|
|
|
|
// X platform
|
|
if (x || (!hasPlatformText && text)) {
|
|
const xText = x || text;
|
|
platforms.x = {
|
|
enabled: true,
|
|
posts: buildPosts(xText)
|
|
};
|
|
// Add X-specific settings if provided
|
|
if (replyTo || community) {
|
|
platforms.x.settings = {};
|
|
if (replyTo) platforms.x.settings.reply_to_url = replyTo;
|
|
if (community) platforms.x.settings.community_id = community;
|
|
}
|
|
} else {
|
|
platforms.x = { enabled: false };
|
|
}
|
|
|
|
// LinkedIn platform
|
|
if (linkedin) {
|
|
platforms.linkedin = {
|
|
enabled: true,
|
|
posts: buildPosts(linkedin)
|
|
};
|
|
} else if (!hasPlatformText) {
|
|
platforms.linkedin = { enabled: false };
|
|
}
|
|
|
|
// Threads platform
|
|
if (threads) {
|
|
platforms.threads = {
|
|
enabled: true,
|
|
posts: buildPosts(threads)
|
|
};
|
|
} else if (!hasPlatformText) {
|
|
platforms.threads = { enabled: false };
|
|
}
|
|
|
|
// Bluesky platform
|
|
if (bluesky) {
|
|
platforms.bluesky = {
|
|
enabled: true,
|
|
posts: buildPosts(bluesky)
|
|
};
|
|
} else if (!hasPlatformText) {
|
|
platforms.bluesky = { enabled: false };
|
|
}
|
|
|
|
// Mastodon platform
|
|
if (mastodon) {
|
|
platforms.mastodon = {
|
|
enabled: true,
|
|
posts: buildPosts(mastodon)
|
|
};
|
|
} else if (!hasPlatformText) {
|
|
platforms.mastodon = { enabled: false };
|
|
}
|
|
|
|
// Post to all platforms with same text
|
|
if (all) {
|
|
const allPosts = buildPosts(all);
|
|
platforms.x = { enabled: true, posts: allPosts };
|
|
platforms.linkedin = { enabled: true, posts: allPosts };
|
|
platforms.threads = { enabled: true, posts: allPosts };
|
|
platforms.bluesky = { enabled: true, posts: allPosts };
|
|
platforms.mastodon = { enabled: true, posts: allPosts };
|
|
}
|
|
|
|
const body = {
|
|
platforms,
|
|
publish_at: now ? 'now' : (schedule || undefined),
|
|
draft_title: title,
|
|
share: share === true
|
|
};
|
|
|
|
const draft = await api(`/social-sets/${socialSetId}/drafts`, 'POST', body);
|
|
|
|
// Log enabled platforms
|
|
const enabledPlatforms = Object.entries(platforms)
|
|
.filter(([_, p]) => p.enabled)
|
|
.map(([name]) => name.toUpperCase())
|
|
.join(', ');
|
|
|
|
log('\n✅ Draft created!', 'green');
|
|
log(` ID: ${draft.id}`, 'cyan');
|
|
log(` Platforms: ${enabledPlatforms || 'X'}`, 'cyan');
|
|
log(` Status: ${draft.status}`);
|
|
log(` Preview: ${(draft.preview || '').substring(0, 75)}...`);
|
|
|
|
if (draft.status === 'published') {
|
|
log(` 🎉 Published immediately!`, 'green');
|
|
if (draft.x_published_url) log(` X URL: ${draft.x_published_url}`, 'cyan');
|
|
if (draft.linkedin_published_url) log(` LinkedIn URL: ${draft.linkedin_published_url}`, 'cyan');
|
|
} else if (draft.scheduled_date) {
|
|
log(` Scheduled: ${new Date(draft.scheduled_date).toLocaleString()}`, 'yellow');
|
|
log(` Review & publish at: ${draft.private_url}`, 'dim');
|
|
} else {
|
|
log(` 📝 Saved as draft. Review at: ${draft.private_url}`, 'dim');
|
|
}
|
|
}
|
|
|
|
async function cmdUpdateDraft(socialSetId, draftId, text, options = {}) {
|
|
if (!socialSetId || !draftId) {
|
|
logError('Social set ID and draft ID required');
|
|
process.exit(1);
|
|
}
|
|
if (!text) {
|
|
logError('New text required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const body = {
|
|
platforms: { x: { enabled: true, posts: [{ text }] } }
|
|
};
|
|
|
|
const draft = await api(`/social-sets/${socialSetId}/drafts/${draftId}`, 'PATCH', body);
|
|
|
|
log('\n✅ Draft updated!', 'green');
|
|
log(` ID: ${draft.id}`);
|
|
log(` Preview: ${(draft.preview || '').substring(0, 75)}...`);
|
|
}
|
|
|
|
async function cmdDeleteDraft(socialSetId, draftId) {
|
|
if (!socialSetId || !draftId) {
|
|
logError('Social set ID and draft ID required');
|
|
process.exit(1);
|
|
}
|
|
await api(`/social-sets/${socialSetId}/drafts/${draftId}`, 'DELETE');
|
|
log('\n🗑️ Draft deleted', 'green');
|
|
}
|
|
|
|
// ============ TAGS ============
|
|
|
|
async function cmdTags(socialSetId) {
|
|
if (!socialSetId) {
|
|
logError('--social-set-id required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const result = await api(`/social-sets/${socialSetId}/tags`);
|
|
|
|
if (result.results?.length === 0) {
|
|
log('\nNo tags found.', 'yellow');
|
|
return;
|
|
}
|
|
|
|
log('\n🏷️ Tags:', 'bright');
|
|
result.results.forEach(tag => {
|
|
log(` #${tag.slug} (${tag.name})`, 'cyan');
|
|
});
|
|
}
|
|
|
|
async function cmdCreateTag(socialSetId, name) {
|
|
if (!socialSetId || !name) {
|
|
logError('--social-set-id and tag name required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const tag = await api(`/social-sets/${socialSetId}/tags`, 'POST', { name });
|
|
log('\n✅ Tag created!', 'green');
|
|
log(` #${tag.slug} (${tag.name})`, 'cyan');
|
|
}
|
|
|
|
async function cmdDeleteTag(socialSetId, slug) {
|
|
if (!socialSetId || !slug) {
|
|
logError('--social-set-id and tag slug required');
|
|
process.exit(1);
|
|
}
|
|
|
|
await api(`/social-sets/${socialSetId}/tags/${encodeURIComponent(slug)}`, 'DELETE');
|
|
log('\n🗑️ Tag deleted', 'green');
|
|
}
|
|
|
|
// ============ MEDIA ============
|
|
|
|
async function cmdUploadMedia(socialSetId, filename) {
|
|
if (!socialSetId || !filename) {
|
|
logError('--social-set-id and filename required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const upload = await api(`/social-sets/${socialSetId}/media/upload`, 'POST', { file_name: filename });
|
|
log(`\n📤 Upload URL generated for ${filename}`, 'cyan');
|
|
log(` Media ID: ${upload.media_id}`, 'cyan');
|
|
log(` Upload to: ${upload.upload_url.substring(0, 60)}...`, 'dim');
|
|
log(` Check status: typefully media-status ${upload.media_id} --social-set-id ${socialSetId}`);
|
|
}
|
|
|
|
async function cmdMediaStatus(socialSetId, mediaId) {
|
|
if (!socialSetId || !mediaId) {
|
|
logError('--social-set-id and media-id required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const media = await api(`/social-sets/${socialSetId}/media/${mediaId}`);
|
|
|
|
log('\n📎 Media Status:', 'bright');
|
|
log(` ID: ${media.media_id}`, 'cyan');
|
|
log(` File: ${media.file_name}`);
|
|
log(` Status: ${media.status}`, media.status === 'ready' ? 'green' : media.status === 'failed' ? 'red' : 'yellow');
|
|
if (media.error_reason) log(` Error: ${media.error_reason}`, 'red');
|
|
if (media.media_urls) {
|
|
log(`\n URLs:`, 'dim');
|
|
Object.entries(media.media_urls).forEach(([size, url]) => {
|
|
log(` ${size}: ${url.substring(0, 55)}...`, 'dim');
|
|
});
|
|
}
|
|
}
|
|
|
|
// ============ HELP ============
|
|
|
|
function showHelp() {
|
|
log(COMPLIANCE_NOTICE, 'cyan');
|
|
log(`
|
|
${colors.bright}SETUP:${colors.reset}
|
|
export TYPEFULLY_API_KEY="your-key"
|
|
|
|
${colors.bright}USER & ACCOUNTS:${colors.reset}
|
|
typefully me Show current user
|
|
typefully social-sets List connected accounts
|
|
typefully social-set <id> Get account details
|
|
|
|
${colors.bright}CREATE DRAFTS (SAFE - Review before publishing):${colors.reset}
|
|
typefully create-draft "text" Create draft for X only
|
|
typefully create-draft "text" --now Create AND publish immediately
|
|
typefully create-draft --all "text" Post to ALL platforms (X, LinkedIn, Threads, Bluesky, Mastodon)
|
|
typefully create-draft --x "1/ line1\n2/ line2" Multi-line creates thread on X
|
|
|
|
${colors.bright}MULTI-PLATFORM POSTS:${colors.reset}
|
|
typefully create-draft --x "X thread..." --linkedin "LinkedIn post..."
|
|
typefully create-draft --x "1/ X\n2/ thread" --linkedin "Combined" --bluesky "Bluesky"
|
|
typefully create-draft --all "Same post to ALL platforms"
|
|
|
|
${colors.bright}MANAGE DRAFTS:${colors.reset}
|
|
typefully drafts List all drafts
|
|
typefully drafts --status draft Only drafts
|
|
typefully drafts --status scheduled Only scheduled
|
|
typefully drafts --status published Only published
|
|
typefully drafts --limit 25 More results
|
|
typefully drafts --offset 10 Pagination
|
|
typefully draft <id> Get draft details
|
|
typefully update-draft <id> "text" Update draft
|
|
typefully delete-draft <id> Delete draft
|
|
|
|
${colors.bright}SCHEDULING:${colors.reset}
|
|
--schedule "2025-01-20T14:00:00Z" Schedule for specific time
|
|
--now Publish immediately
|
|
--next-free-slot Optimal posting time
|
|
|
|
${colors.bright}CONTENT OPTIONS:${colors.reset}
|
|
--title <text> Draft title (internal, not posted)
|
|
--share Generate public share URL
|
|
--reply-to <url> Reply to existing X post
|
|
--community <id> Post to X community
|
|
|
|
${colors.bright}EXAMPLES:${colors.reset}
|
|
${colors.dim}# Get your X account ID${colors.reset}
|
|
typefully social-sets
|
|
|
|
${colors.dim}# Create a draft (safe - review first)${colors.reset}
|
|
typefully create-draft "I'm an AI building in public! 🤖" --social-set-id 12345
|
|
|
|
${colors.dim}# Create a thread (newlines = separate tweets)${colors.reset}
|
|
typefully create-draft "1/ Building my first product...
|
|
2/ It's a COBOL parser API...
|
|
3/ Follow along! 🚀" --social-set-id 12345
|
|
|
|
${colors.dim}# Post same content to all platforms${colors.reset}
|
|
typefully create-draft "Check out my latest project! 🚀" --social-set-id 12345 --all
|
|
|
|
${colors.dim}# Different content per platform${colors.reset}
|
|
typefully create-draft \
|
|
--x "1/ Thread: Here's what I'm building\n2/ The problem it solves\n3/ Link in bio 🚀" \
|
|
--linkedin "Excited to share my latest project. It solves [problem] and helps [audience]. Link in bio! 🚀" \
|
|
--bluesky "Building in public thread 👇" \
|
|
--social-set-id 12345
|
|
|
|
${colors.dim}# Publish immediately${colors.reset}
|
|
typefully create-draft "🎉 Launch day! Our API is live!" --social-set-id 12345 --now
|
|
|
|
${colors.dim}# Schedule for later${colors.reset}
|
|
typefully create-draft "Early access now available!" --social-set-id 12345 --schedule "2025-02-01T09:00:00Z"
|
|
|
|
${colors.dim}# Reply to a post${colors.reset}
|
|
typefully create-draft "Great thread!" --social-set-id 12345 --reply-to "https://x.com/user/status/123"
|
|
|
|
${colors.bright}COMPLIANCE:${colors.reset}
|
|
✓ Creates drafts by default (safe)
|
|
✓ No automated replies/likes/retweets
|
|
✓ No spam or bulk actions
|
|
✓ All content is reviewed before publishing
|
|
|
|
${colors.bright}WEBSITE:${colors.reset} https://typefully.com
|
|
X Rules: https://help.x.com/en/rules-and-policies/x-automation
|
|
`);
|
|
}
|
|
|
|
// ============ MAIN ============
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const command = args[0];
|
|
|
|
if (!command || command === '--help' || command === 'help' || command === '-h') {
|
|
showHelp();
|
|
return;
|
|
}
|
|
|
|
// Show compliance on most commands
|
|
if (['me', 'social-sets', 'drafts', 'create-draft'].includes(command)) {
|
|
logCompliance();
|
|
}
|
|
|
|
// Parse options
|
|
const options = {};
|
|
let text = null;
|
|
let i = 1;
|
|
|
|
while (i < args.length) {
|
|
const arg = args[i];
|
|
|
|
if (arg.startsWith('--')) {
|
|
const key = arg.substring(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
|
const valueArgs = ['schedule', 'socialSetId', 'title', 'replyTo', 'community', 'limit', 'offset', 'status', 'orderBy', 'tag', 'x', 'linkedin', 'threads', 'bluesky', 'mastodon', 'all'];
|
|
const flagArgs = ['share', 'thread', 'now', 'nextFreeSlot'];
|
|
|
|
if (valueArgs.includes(key)) {
|
|
options[key] = args[i + 1];
|
|
i += 2;
|
|
} else if (flagArgs.includes(key)) {
|
|
options[key] = true;
|
|
i++;
|
|
} else {
|
|
i++;
|
|
}
|
|
} else if (!arg.startsWith('-') && command !== 'create-draft' && command !== 'update-draft' && command !== 'create-tag') {
|
|
if (['social-set', 'draft', 'delete-draft', 'delete-tag', 'media-status'].includes(command)) {
|
|
options.id = arg;
|
|
}
|
|
i++;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Handle positional text (everything before first --flag after command name)
|
|
if (command === 'create-draft') {
|
|
// Find first flag after command name, take everything before it as text
|
|
const textEnd = args.findIndex((a, idx) => idx > 0 && a.startsWith('-'));
|
|
text = textEnd > 0 ? args.slice(1, textEnd).join(' ').replace(/^["']|["']$/g, '') : '';
|
|
} else if (command === 'update-draft') {
|
|
options.id = args[1];
|
|
text = args.slice(2).join(' ');
|
|
} else if (command === 'create-tag') {
|
|
text = args.slice(1).join(' ');
|
|
} else if (command === 'delete-tag') {
|
|
options.id = args[1];
|
|
}
|
|
|
|
// Route commands
|
|
try {
|
|
switch (command) {
|
|
case 'me': await cmdMe(); break;
|
|
case 'social-sets': await cmdSocialSets(); break;
|
|
case 'social-set': await cmdSocialSet(options.id); break;
|
|
case 'drafts': await cmdDrafts(options.socialSetId, options); break;
|
|
case 'draft': await cmdDraft(options.socialSetId, options.id); break;
|
|
case 'create-draft': await cmdCreateDraft(text, options); break;
|
|
case 'update-draft': await cmdUpdateDraft(options.socialSetId, options.id, text, options); break;
|
|
case 'delete-draft': await cmdDeleteDraft(options.socialSetId, options.id); break;
|
|
case 'tags': await cmdTags(options.socialSetId); break;
|
|
case 'create-tag': await cmdCreateTag(options.socialSetId, text); break;
|
|
case 'delete-tag': await cmdDeleteTag(options.socialSetId, options.id); break;
|
|
case 'upload-media': await cmdUploadMedia(options.socialSetId, text); break;
|
|
case 'media-status': await cmdMediaStatus(options.socialSetId, options.id); break;
|
|
default:
|
|
logError(`Unknown command: ${command}`);
|
|
log('Run: typefully --help');
|
|
process.exit(1);
|
|
}
|
|
} catch (err) {
|
|
logError(err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch(err => {
|
|
logError(err.message);
|
|
process.exit(1);
|
|
});
|