social-light 0.0.1
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/.env.example +7 -0
- package/.instructions/checklist.md +45 -0
- package/.instructions/prd.md +182 -0
- package/.instructions/summary.md +122 -0
- package/README.md +196 -0
- package/bin/socialite +7 -0
- package/delete/tiktok.mjs +315 -0
- package/delete/twitter.mjs +258 -0
- package/package.json +51 -0
- package/server.png +0 -0
- package/src/commands/create.mjs +274 -0
- package/src/commands/edit.mjs +198 -0
- package/src/commands/init.mjs +256 -0
- package/src/commands/publish.mjs +192 -0
- package/src/commands/published.mjs +90 -0
- package/src/commands/unpublished.mjs +102 -0
- package/src/index.mjs +107 -0
- package/src/server/client/index.html +41 -0
- package/src/server/client/logo.jpg +0 -0
- package/src/server/client/main.mjs +953 -0
- package/src/server/client/styles.css +535 -0
- package/src/server/index.mjs +315 -0
- package/src/utils/ai.mjs +201 -0
- package/src/utils/config.mjs +127 -0
- package/src/utils/db.mjs +260 -0
- package/src/utils/fix-config.mjs +60 -0
- package/src/utils/social/base.mjs +142 -0
- package/src/utils/social/bluesky.mjs +302 -0
- package/src/utils/social/index.mjs +300 -0
@@ -0,0 +1,256 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import ora from "ora";
|
3
|
+
import inquirer from "inquirer";
|
4
|
+
import fs from "fs-extra";
|
5
|
+
import path from "path";
|
6
|
+
import os from "os";
|
7
|
+
import dotenv from "dotenv";
|
8
|
+
|
9
|
+
import {
|
10
|
+
createDefaultConfig,
|
11
|
+
configExists,
|
12
|
+
updateConfig,
|
13
|
+
updateCredentials,
|
14
|
+
} from "../utils/config.mjs";
|
15
|
+
import { initializeDb } from "../utils/db.mjs";
|
16
|
+
|
17
|
+
// Load environment variables
|
18
|
+
dotenv.config();
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Initialize the Social Light application
|
22
|
+
* @param {Object} argv - Command arguments
|
23
|
+
* @example
|
24
|
+
* await initialize({ force: true });
|
25
|
+
*/
|
26
|
+
export const initialize = async (argv) => {
|
27
|
+
const spinner = ora("Initializing Social Light...").start();
|
28
|
+
|
29
|
+
try {
|
30
|
+
const exists = configExists();
|
31
|
+
|
32
|
+
// If config already exists and not forcing re-init, prompt for confirmation
|
33
|
+
if (exists && !argv.force) {
|
34
|
+
spinner.stop();
|
35
|
+
|
36
|
+
const { confirm } = await inquirer.prompt([
|
37
|
+
{
|
38
|
+
type: "confirm",
|
39
|
+
name: "confirm",
|
40
|
+
message:
|
41
|
+
"Social Light is already initialized. Reinitialize with default settings?",
|
42
|
+
default: false,
|
43
|
+
},
|
44
|
+
]);
|
45
|
+
|
46
|
+
if (!confirm) {
|
47
|
+
console.log(chalk.yellow("Initialization cancelled."));
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
spinner.start("Reinitializing Social Light...");
|
52
|
+
}
|
53
|
+
|
54
|
+
// Create default config
|
55
|
+
const config = createDefaultConfig();
|
56
|
+
|
57
|
+
// Initialize database first
|
58
|
+
spinner.text = "Setting up database...";
|
59
|
+
const dbInitialized = initializeDb();
|
60
|
+
|
61
|
+
if (!dbInitialized) {
|
62
|
+
throw new Error("Failed to initialize database");
|
63
|
+
}
|
64
|
+
|
65
|
+
// Configure platforms
|
66
|
+
spinner.text = "Setting up platforms...";
|
67
|
+
|
68
|
+
// Bluesky is the only platform we support
|
69
|
+
const platforms = ["Bluesky"];
|
70
|
+
|
71
|
+
// Check if we need to collect Bluesky credentials
|
72
|
+
spinner.text = "Checking Bluesky credentials...";
|
73
|
+
spinner.stop();
|
74
|
+
|
75
|
+
// Get existing credentials from .env, if available
|
76
|
+
const blueskyHandle = process.env.BLUESKY_HANDLE || "";
|
77
|
+
const blueskyPassword = process.env.BLUESKY_APP_PASSWORD || "";
|
78
|
+
const blueskyService = process.env.BLUESKY_SERVICE || "https://bsky.social";
|
79
|
+
|
80
|
+
console.log(chalk.cyan("\nBluesky Account Setup"));
|
81
|
+
console.log(
|
82
|
+
chalk.gray("Create an app password in your Bluesky account settings")
|
83
|
+
);
|
84
|
+
|
85
|
+
const { collectCredentials } = await inquirer.prompt([
|
86
|
+
{
|
87
|
+
type: "confirm",
|
88
|
+
name: "collectCredentials",
|
89
|
+
message: "Would you like to set up your Bluesky credentials now?",
|
90
|
+
default: true,
|
91
|
+
},
|
92
|
+
]);
|
93
|
+
|
94
|
+
let blueskyCredentials = {};
|
95
|
+
|
96
|
+
if (collectCredentials) {
|
97
|
+
blueskyCredentials = await inquirer.prompt([
|
98
|
+
{
|
99
|
+
type: "input",
|
100
|
+
name: "handle",
|
101
|
+
message: "Enter your Bluesky handle (username with .bsky.social):",
|
102
|
+
default: blueskyHandle,
|
103
|
+
validate: (input) =>
|
104
|
+
input.includes(".")
|
105
|
+
? true
|
106
|
+
: "Handle should include domain (e.g., username.bsky.social)",
|
107
|
+
},
|
108
|
+
{
|
109
|
+
type: "password",
|
110
|
+
name: "password",
|
111
|
+
message:
|
112
|
+
"Enter your Bluesky app password (created in your Bluesky account settings):",
|
113
|
+
mask: "*",
|
114
|
+
validate: (input) =>
|
115
|
+
input.length > 0 ? true : "Password cannot be empty",
|
116
|
+
},
|
117
|
+
{
|
118
|
+
type: "input",
|
119
|
+
name: "service",
|
120
|
+
message: "Enter your Bluesky service URL:",
|
121
|
+
default: blueskyService,
|
122
|
+
},
|
123
|
+
]);
|
124
|
+
|
125
|
+
// Save credentials to .env file
|
126
|
+
const envPath = path.join(process.cwd(), ".env");
|
127
|
+
let envContent = fs.existsSync(envPath)
|
128
|
+
? fs.readFileSync(envPath, "utf8")
|
129
|
+
: "";
|
130
|
+
|
131
|
+
// Parse existing .env content
|
132
|
+
const envLines = envContent.split("\n");
|
133
|
+
const envMap = {};
|
134
|
+
|
135
|
+
envLines.forEach((line) => {
|
136
|
+
if (line.includes("=")) {
|
137
|
+
const [key, value] = line.split("=");
|
138
|
+
envMap[key.trim()] = value.trim();
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
// Update with new credentials
|
143
|
+
envMap["BLUESKY_HANDLE"] = blueskyCredentials.handle;
|
144
|
+
envMap["BLUESKY_APP_PASSWORD"] = blueskyCredentials.password;
|
145
|
+
envMap["BLUESKY_SERVICE"] = blueskyCredentials.service;
|
146
|
+
|
147
|
+
// Save to config.json as well
|
148
|
+
updateCredentials("bluesky", {
|
149
|
+
handle: blueskyCredentials.handle,
|
150
|
+
password: blueskyCredentials.password,
|
151
|
+
service: blueskyCredentials.service,
|
152
|
+
});
|
153
|
+
|
154
|
+
// If OpenAI key doesn't exist, prompt for it
|
155
|
+
let openaiKey = envMap["OPENAI_API_KEY"] || "";
|
156
|
+
if (!openaiKey) {
|
157
|
+
const response = await inquirer.prompt([
|
158
|
+
{
|
159
|
+
type: "input",
|
160
|
+
name: "openaiKey",
|
161
|
+
message:
|
162
|
+
"Enter your OpenAI API key for AI features (press Enter to skip):",
|
163
|
+
},
|
164
|
+
]);
|
165
|
+
|
166
|
+
if (response.openaiKey) {
|
167
|
+
openaiKey = response.openaiKey;
|
168
|
+
envMap["OPENAI_API_KEY"] = openaiKey;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
// Save OpenAI key to config.json
|
173
|
+
if (openaiKey) {
|
174
|
+
updateCredentials("openai", {
|
175
|
+
apiKey: openaiKey,
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
// Convert map back to env string
|
180
|
+
const newEnvContent = Object.entries(envMap)
|
181
|
+
.map(([key, value]) => `${key}=${value}`)
|
182
|
+
.join("\n");
|
183
|
+
|
184
|
+
// Write to .env file
|
185
|
+
fs.writeFileSync(envPath, newEnvContent);
|
186
|
+
|
187
|
+
console.log(
|
188
|
+
chalk.green("\n✓ Credentials saved to .env file and config.json")
|
189
|
+
);
|
190
|
+
}
|
191
|
+
|
192
|
+
spinner.start("Updating configuration...");
|
193
|
+
|
194
|
+
// Update config with selected platforms
|
195
|
+
updateConfig({ defaultPlatforms: platforms });
|
196
|
+
|
197
|
+
// Initialize database
|
198
|
+
spinner.text = "Setting up database...";
|
199
|
+
initializeDb();
|
200
|
+
|
201
|
+
spinner.succeed("Social Light initialized successfully!");
|
202
|
+
|
203
|
+
// Display configuration summary
|
204
|
+
console.log("\n", chalk.cyan("Configuration:"));
|
205
|
+
console.log(
|
206
|
+
` ${chalk.gray("•")} ${chalk.bold("Database path:")} ${config.dbPath}`
|
207
|
+
);
|
208
|
+
console.log(
|
209
|
+
` ${chalk.gray("•")} ${chalk.bold("Default platforms:")} ${
|
210
|
+
platforms.join(", ") || "None"
|
211
|
+
}`
|
212
|
+
);
|
213
|
+
console.log(
|
214
|
+
` ${chalk.gray("•")} ${chalk.bold("AI features:")} ${
|
215
|
+
config.aiEnabled ? chalk.green("Enabled") : chalk.red("Disabled")
|
216
|
+
}`
|
217
|
+
);
|
218
|
+
|
219
|
+
// Display credentials info
|
220
|
+
const hasOpenAI = Boolean(
|
221
|
+
process.env.OPENAI_API_KEY || config.credentials?.openai?.apiKey
|
222
|
+
);
|
223
|
+
const hasBluesky = Boolean(
|
224
|
+
(process.env.BLUESKY_HANDLE && process.env.BLUESKY_APP_PASSWORD) ||
|
225
|
+
(config.credentials?.bluesky?.handle &&
|
226
|
+
config.credentials?.bluesky?.password)
|
227
|
+
);
|
228
|
+
|
229
|
+
console.log("\n", chalk.cyan("Credentials:"));
|
230
|
+
console.log(
|
231
|
+
` ${chalk.gray("•")} ${chalk.bold("OpenAI API:")} ${
|
232
|
+
hasOpenAI ? chalk.green("Configured") : chalk.yellow("Not configured")
|
233
|
+
}`
|
234
|
+
);
|
235
|
+
console.log(
|
236
|
+
` ${chalk.gray("•")} ${chalk.bold("Bluesky:")} ${
|
237
|
+
hasBluesky ? chalk.green("Configured") : chalk.yellow("Not configured")
|
238
|
+
}`
|
239
|
+
);
|
240
|
+
|
241
|
+
console.log(
|
242
|
+
"\n",
|
243
|
+
chalk.green("✓"),
|
244
|
+
"You're all set! Get started with",
|
245
|
+
chalk.cyan("social-light create")
|
246
|
+
);
|
247
|
+
console.log(
|
248
|
+
chalk.gray(" Need help? Run"),
|
249
|
+
chalk.cyan("social-light --help")
|
250
|
+
);
|
251
|
+
} catch (error) {
|
252
|
+
spinner.fail(`Initialization failed: ${error.message}`);
|
253
|
+
console.error(chalk.red("Error details:"), error);
|
254
|
+
process.exit(1);
|
255
|
+
}
|
256
|
+
};
|
@@ -0,0 +1,192 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import ora from "ora";
|
3
|
+
import cron from "node-cron";
|
4
|
+
import { getPosts, markAsPublished, logAction } from "../utils/db.mjs";
|
5
|
+
import { getSocialAPI } from "../utils/social/index.mjs";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Check if a post is eligible for publishing
|
9
|
+
* @param {Object} post - Post object
|
10
|
+
* @returns {boolean} True if post is eligible for publishing
|
11
|
+
*/
|
12
|
+
const isEligibleForPublishing = (post) => {
|
13
|
+
// If no publish date specified, it's eligible immediately
|
14
|
+
if (!post.publish_date) {
|
15
|
+
return true;
|
16
|
+
}
|
17
|
+
|
18
|
+
// Check if publish date is today or in the past
|
19
|
+
const publishDate = new Date(post.publish_date);
|
20
|
+
const now = new Date();
|
21
|
+
|
22
|
+
// Reset time to compare dates only
|
23
|
+
publishDate.setHours(0, 0, 0, 0);
|
24
|
+
now.setHours(0, 0, 0, 0);
|
25
|
+
|
26
|
+
return publishDate <= now;
|
27
|
+
};
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Publish eligible posts once
|
31
|
+
* @returns {Promise<Array>} Array of published post IDs
|
32
|
+
*/
|
33
|
+
const publishEligiblePosts = async () => {
|
34
|
+
// Get unpublished posts
|
35
|
+
const posts = getPosts({ published: false });
|
36
|
+
|
37
|
+
// Filter eligible posts
|
38
|
+
const eligiblePosts = posts.filter(isEligibleForPublishing);
|
39
|
+
|
40
|
+
if (eligiblePosts.length === 0) {
|
41
|
+
return [];
|
42
|
+
}
|
43
|
+
|
44
|
+
// Initialize social API
|
45
|
+
const socialAPI = getSocialAPI();
|
46
|
+
|
47
|
+
// Track published post IDs
|
48
|
+
const publishedPostIds = [];
|
49
|
+
|
50
|
+
// Publish each eligible post
|
51
|
+
for (const post of eligiblePosts) {
|
52
|
+
try {
|
53
|
+
// Skip posts with no platforms
|
54
|
+
if (!post.platforms || post.platforms.trim() === "") {
|
55
|
+
console.log(
|
56
|
+
chalk.yellow(`Skipping post ID ${post.id}: No platforms specified`)
|
57
|
+
);
|
58
|
+
continue;
|
59
|
+
}
|
60
|
+
|
61
|
+
// Prepare platforms array
|
62
|
+
const platforms = post.platforms.split(",").map((p) => p.trim());
|
63
|
+
|
64
|
+
// Publish post to specified platforms
|
65
|
+
const result = await socialAPI.post({
|
66
|
+
text: post.content,
|
67
|
+
title: post.title,
|
68
|
+
platforms,
|
69
|
+
// Add more options as needed
|
70
|
+
});
|
71
|
+
|
72
|
+
// If post was successfully published to at least one platform, mark as published
|
73
|
+
const anySuccess = Object.values(result.results).some((r) => r.success);
|
74
|
+
|
75
|
+
if (anySuccess) {
|
76
|
+
markAsPublished(post.id);
|
77
|
+
publishedPostIds.push(post.id);
|
78
|
+
|
79
|
+
// Log the action with platform results
|
80
|
+
logAction("post_published", {
|
81
|
+
postId: post.id,
|
82
|
+
platforms: result.results,
|
83
|
+
title: post.title,
|
84
|
+
});
|
85
|
+
|
86
|
+
console.log(
|
87
|
+
chalk.green(
|
88
|
+
`✓ Published post ID ${post.id} to platforms: ${Object.keys(
|
89
|
+
result.results
|
90
|
+
)
|
91
|
+
.filter((p) => result.results[p].success)
|
92
|
+
.join(", ")}`
|
93
|
+
)
|
94
|
+
);
|
95
|
+
} else {
|
96
|
+
console.log(
|
97
|
+
chalk.red(`✗ Failed to publish post ID ${post.id} to any platform`)
|
98
|
+
);
|
99
|
+
|
100
|
+
// Log failures
|
101
|
+
for (const [platform, platformResult] of Object.entries(
|
102
|
+
result.results
|
103
|
+
)) {
|
104
|
+
if (!platformResult.success) {
|
105
|
+
console.log(chalk.red(` - ${platform}: ${platformResult.error}`));
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
} catch (error) {
|
110
|
+
console.error(
|
111
|
+
chalk.red(`Error publishing post ID ${post.id}:`),
|
112
|
+
error.message
|
113
|
+
);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
return publishedPostIds;
|
118
|
+
};
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Publish posts command handler
|
122
|
+
* @param {Object} argv - Command arguments
|
123
|
+
*/
|
124
|
+
export const publishPosts = async (argv) => {
|
125
|
+
// Check if continuous mode is enabled
|
126
|
+
if (argv.continuous) {
|
127
|
+
console.log(chalk.cyan("Starting continuous publishing mode..."));
|
128
|
+
console.log(chalk.gray("Press Ctrl+C to stop"));
|
129
|
+
|
130
|
+
// Initial publish
|
131
|
+
await runContinuousPublish();
|
132
|
+
|
133
|
+
// Set up cron job to run every minute
|
134
|
+
cron.schedule("* * * * *", async () => {
|
135
|
+
await runContinuousPublish();
|
136
|
+
});
|
137
|
+
|
138
|
+
// Keep process alive
|
139
|
+
process.stdin.resume();
|
140
|
+
} else {
|
141
|
+
// One-time publish
|
142
|
+
const spinner = ora("Publishing eligible posts...").start();
|
143
|
+
|
144
|
+
try {
|
145
|
+
const publishedPostIds = await publishEligiblePosts();
|
146
|
+
|
147
|
+
if (publishedPostIds.length === 0) {
|
148
|
+
spinner.info("No eligible posts found for publishing.");
|
149
|
+
} else {
|
150
|
+
spinner.succeed(
|
151
|
+
`Published ${publishedPostIds.length} post(s) successfully!`
|
152
|
+
);
|
153
|
+
console.log(
|
154
|
+
chalk.gray("Run"),
|
155
|
+
chalk.cyan("social-light published"),
|
156
|
+
chalk.gray("to see published posts.")
|
157
|
+
);
|
158
|
+
}
|
159
|
+
} catch (error) {
|
160
|
+
spinner.fail(`Error publishing posts: ${error.message}`);
|
161
|
+
console.error(error);
|
162
|
+
}
|
163
|
+
}
|
164
|
+
};
|
165
|
+
|
166
|
+
/**
|
167
|
+
* Run continuous publish cycle
|
168
|
+
*/
|
169
|
+
const runContinuousPublish = async () => {
|
170
|
+
try {
|
171
|
+
// Get current time for logging
|
172
|
+
const now = new Date().toLocaleTimeString();
|
173
|
+
|
174
|
+
console.log(chalk.gray(`[${now}] Checking for posts to publish...`));
|
175
|
+
|
176
|
+
const publishedPostIds = await publishEligiblePosts();
|
177
|
+
|
178
|
+
if (publishedPostIds.length > 0) {
|
179
|
+
console.log(
|
180
|
+
chalk.green(
|
181
|
+
`[${now}] Published ${publishedPostIds.length} post(s) successfully!`
|
182
|
+
)
|
183
|
+
);
|
184
|
+
} else {
|
185
|
+
console.log(
|
186
|
+
chalk.gray(`[${now}] No eligible posts found for publishing.`)
|
187
|
+
);
|
188
|
+
}
|
189
|
+
} catch (error) {
|
190
|
+
console.error(chalk.red("Error in publishing cycle:"), error.message);
|
191
|
+
}
|
192
|
+
};
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import { getPosts } from "../utils/db.mjs";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Format post content for display
|
6
|
+
* @param {string} content - Post content
|
7
|
+
* @param {number} maxLength - Maximum length to display
|
8
|
+
* @returns {string} Formatted content
|
9
|
+
*/
|
10
|
+
const formatContent = (content, maxLength = 60) => {
|
11
|
+
if (!content) return "";
|
12
|
+
const trimmed = content.replace(/\s+/g, " ").trim();
|
13
|
+
return trimmed.length > maxLength
|
14
|
+
? trimmed.substring(0, maxLength) + "..."
|
15
|
+
: trimmed;
|
16
|
+
};
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Format date for display
|
20
|
+
* @param {string} dateStr - Date string
|
21
|
+
* @returns {string} Formatted date
|
22
|
+
*/
|
23
|
+
const formatDate = (dateStr) => {
|
24
|
+
if (!dateStr) return "No date";
|
25
|
+
|
26
|
+
try {
|
27
|
+
const date = new Date(dateStr);
|
28
|
+
const today = new Date();
|
29
|
+
const yesterday = new Date(today);
|
30
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
31
|
+
|
32
|
+
// Check if it's today or yesterday
|
33
|
+
if (date.toDateString() === today.toDateString()) {
|
34
|
+
return chalk.green("Today");
|
35
|
+
} else if (date.toDateString() === yesterday.toDateString()) {
|
36
|
+
return chalk.yellow("Yesterday");
|
37
|
+
}
|
38
|
+
|
39
|
+
// Format as YYYY-MM-DD
|
40
|
+
return date.toISOString().split("T")[0];
|
41
|
+
} catch (error) {
|
42
|
+
return dateStr; // Fallback to raw date string
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
/**
|
47
|
+
* List all published posts
|
48
|
+
* @param {Object} argv - Command arguments
|
49
|
+
*/
|
50
|
+
export const listPublished = async (argv) => {
|
51
|
+
try {
|
52
|
+
// Get published posts from database
|
53
|
+
const posts = getPosts({ published: true });
|
54
|
+
|
55
|
+
if (posts.length === 0) {
|
56
|
+
console.log(chalk.yellow("No published posts found."));
|
57
|
+
console.log(
|
58
|
+
chalk.gray("Run"),
|
59
|
+
chalk.cyan("social-light publish"),
|
60
|
+
chalk.gray("to publish eligible posts.")
|
61
|
+
);
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
|
65
|
+
console.log(chalk.cyan(`\nPublished Posts (${posts.length}):`));
|
66
|
+
console.log(chalk.gray("─".repeat(80)));
|
67
|
+
|
68
|
+
// Display each post with index and details
|
69
|
+
posts.forEach((post, index) => {
|
70
|
+
const postNumber = chalk.bold(`[${index + 1}]`);
|
71
|
+
const postDate = formatDate(post.publish_date);
|
72
|
+
const postTitle = chalk.white(post.title || "No title");
|
73
|
+
const postContent = formatContent(post.content);
|
74
|
+
const postPlatforms = post.platforms
|
75
|
+
? chalk.blue(post.platforms)
|
76
|
+
: chalk.gray("No platforms");
|
77
|
+
|
78
|
+
console.log(`${postNumber} ${postDate} ${postTitle}`);
|
79
|
+
console.log(` ${chalk.gray(postContent)}`);
|
80
|
+
console.log(` ${postPlatforms}`);
|
81
|
+
console.log(chalk.gray("─".repeat(80)));
|
82
|
+
});
|
83
|
+
|
84
|
+
console.log("");
|
85
|
+
} catch (error) {
|
86
|
+
console.error(chalk.red("Error listing published posts:"), error.message);
|
87
|
+
console.error(error);
|
88
|
+
process.exit(1);
|
89
|
+
}
|
90
|
+
};
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import { getPosts } from "../utils/db.mjs";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Format post content for display
|
6
|
+
* @param {string} content - Post content
|
7
|
+
* @param {number} maxLength - Maximum length to display
|
8
|
+
* @returns {string} Formatted content
|
9
|
+
*/
|
10
|
+
const formatContent = (content, maxLength = 60) => {
|
11
|
+
if (!content) return "";
|
12
|
+
const trimmed = content.replace(/\s+/g, " ").trim();
|
13
|
+
return trimmed.length > maxLength
|
14
|
+
? trimmed.substring(0, maxLength) + "..."
|
15
|
+
: trimmed;
|
16
|
+
};
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Format date for display
|
20
|
+
* @param {string} dateStr - Date string
|
21
|
+
* @returns {string} Formatted date
|
22
|
+
*/
|
23
|
+
const formatDate = (dateStr) => {
|
24
|
+
if (!dateStr) return "No date";
|
25
|
+
|
26
|
+
try {
|
27
|
+
const date = new Date(dateStr);
|
28
|
+
const today = new Date();
|
29
|
+
const tomorrow = new Date(today);
|
30
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
31
|
+
|
32
|
+
// Check if it's today or tomorrow
|
33
|
+
if (date.toDateString() === today.toDateString()) {
|
34
|
+
return chalk.green("Today");
|
35
|
+
} else if (date.toDateString() === tomorrow.toDateString()) {
|
36
|
+
return chalk.yellow("Tomorrow");
|
37
|
+
}
|
38
|
+
|
39
|
+
// Format as YYYY-MM-DD
|
40
|
+
return date.toISOString().split("T")[0];
|
41
|
+
} catch (error) {
|
42
|
+
return dateStr; // Fallback to raw date string
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
/**
|
47
|
+
* List all unpublished posts
|
48
|
+
* @param {Object} argv - Command arguments
|
49
|
+
*/
|
50
|
+
export const listUnpublished = async (argv) => {
|
51
|
+
try {
|
52
|
+
// Get unpublished posts from database
|
53
|
+
const posts = getPosts({ published: false });
|
54
|
+
|
55
|
+
if (posts.length === 0) {
|
56
|
+
console.log(chalk.yellow("No unpublished posts found."));
|
57
|
+
console.log(
|
58
|
+
chalk.gray("Run"),
|
59
|
+
chalk.cyan("social-light create"),
|
60
|
+
chalk.gray("to create a new post.")
|
61
|
+
);
|
62
|
+
return;
|
63
|
+
}
|
64
|
+
|
65
|
+
console.log(chalk.cyan(`\nUnpublished Posts (${posts.length}):`));
|
66
|
+
console.log(chalk.gray("─".repeat(80)));
|
67
|
+
|
68
|
+
// Display each post with index and details
|
69
|
+
posts.forEach((post, index) => {
|
70
|
+
const postNumber = chalk.bold(`[${index + 1}]`);
|
71
|
+
const postDate = formatDate(post.publish_date);
|
72
|
+
const postTitle = chalk.white(post.title || "No title");
|
73
|
+
const postContent = formatContent(post.content);
|
74
|
+
const postPlatforms = post.platforms
|
75
|
+
? chalk.blue(post.platforms)
|
76
|
+
: chalk.gray("No platforms");
|
77
|
+
|
78
|
+
console.log(`${postNumber} ${postDate} ${postTitle}`);
|
79
|
+
console.log(` ${chalk.gray(postContent)}`);
|
80
|
+
console.log(` ${postPlatforms}`);
|
81
|
+
console.log(chalk.gray("─".repeat(80)));
|
82
|
+
});
|
83
|
+
|
84
|
+
// Display helpful commands
|
85
|
+
console.log("");
|
86
|
+
console.log(
|
87
|
+
`${chalk.green("✓")} Use ${chalk.cyan(
|
88
|
+
"social-light edit [index]"
|
89
|
+
)} to edit a post.`
|
90
|
+
);
|
91
|
+
console.log(
|
92
|
+
`${chalk.green("✓")} Use ${chalk.cyan(
|
93
|
+
"social-light publish"
|
94
|
+
)} to publish all eligible posts.`
|
95
|
+
);
|
96
|
+
console.log("");
|
97
|
+
} catch (error) {
|
98
|
+
console.error(chalk.red("Error listing unpublished posts:"), error.message);
|
99
|
+
console.error(error);
|
100
|
+
process.exit(1);
|
101
|
+
}
|
102
|
+
};
|