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.
@@ -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
+ };