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,274 @@
|
|
1
|
+
import fs from "fs-extra";
|
2
|
+
import chalk from "chalk";
|
3
|
+
import ora from "ora";
|
4
|
+
import inquirer from "inquirer";
|
5
|
+
import path from "path";
|
6
|
+
import os from "os";
|
7
|
+
|
8
|
+
import { getConfig, configExists } from "../utils/config.mjs";
|
9
|
+
import {
|
10
|
+
createPost as dbCreatePost,
|
11
|
+
logAction,
|
12
|
+
initializeDb,
|
13
|
+
} from "../utils/db.mjs";
|
14
|
+
import {
|
15
|
+
generateTitle,
|
16
|
+
suggestPublishDate,
|
17
|
+
enhanceContent,
|
18
|
+
} from "../utils/ai.mjs";
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Create a new social media post
|
22
|
+
* @param {Object} argv - Command arguments
|
23
|
+
* @example
|
24
|
+
* await createPost({ file: 'my-post.txt' });
|
25
|
+
*/
|
26
|
+
export const createPost = async (argv) => {
|
27
|
+
const config = getConfig();
|
28
|
+
let content = "";
|
29
|
+
|
30
|
+
try {
|
31
|
+
// Check if initialized and initialize if needed
|
32
|
+
if (!configExists()) {
|
33
|
+
console.log(chalk.yellow("Social Light is not initialized yet."));
|
34
|
+
console.log("Initializing with default settings...");
|
35
|
+
|
36
|
+
// Create config directory
|
37
|
+
const configDir = path.join(os.homedir(), ".social-light");
|
38
|
+
fs.ensureDirSync(configDir);
|
39
|
+
|
40
|
+
// Initialize database
|
41
|
+
const dbInitialized = initializeDb();
|
42
|
+
if (!dbInitialized) {
|
43
|
+
throw new Error("Failed to initialize database");
|
44
|
+
}
|
45
|
+
}
|
46
|
+
// File-based creation
|
47
|
+
if (argv.file) {
|
48
|
+
const spinner = ora(`Reading file ${argv.file}...`).start();
|
49
|
+
|
50
|
+
try {
|
51
|
+
content = await fs.readFile(argv.file, "utf8");
|
52
|
+
spinner.succeed(`File ${argv.file} loaded successfully!`);
|
53
|
+
} catch (error) {
|
54
|
+
spinner.fail(`Failed to read file: ${error.message}`);
|
55
|
+
process.exit(1);
|
56
|
+
}
|
57
|
+
}
|
58
|
+
// Interactive creation
|
59
|
+
else {
|
60
|
+
// Function to handle input with 3 empty lines to end
|
61
|
+
const getContentInput = async (prompt) => {
|
62
|
+
console.log(`${prompt} (Press Enter 3 times in a row when done)`);
|
63
|
+
let lines = [];
|
64
|
+
let emptyLineCount = 0;
|
65
|
+
|
66
|
+
// Set up recursive prompt for input
|
67
|
+
const promptLine = async () => {
|
68
|
+
const { input } = await inquirer.prompt([
|
69
|
+
{
|
70
|
+
type: "input",
|
71
|
+
name: "input",
|
72
|
+
message: ">",
|
73
|
+
},
|
74
|
+
]);
|
75
|
+
|
76
|
+
// Check for empty line
|
77
|
+
if (input.trim() === "") {
|
78
|
+
emptyLineCount++;
|
79
|
+
|
80
|
+
// If we have 3 consecutive empty lines, we're done
|
81
|
+
if (emptyLineCount >= 3) {
|
82
|
+
// Remove the last empty lines (if any) from the result
|
83
|
+
while (
|
84
|
+
lines.length > 0 &&
|
85
|
+
lines[lines.length - 1].trim() === ""
|
86
|
+
) {
|
87
|
+
lines.pop();
|
88
|
+
}
|
89
|
+
console.log(chalk.green("\n✓ Content input complete"));
|
90
|
+
return lines.join("\n");
|
91
|
+
}
|
92
|
+
} else {
|
93
|
+
// Reset empty line counter when non-empty input is received
|
94
|
+
emptyLineCount = 0;
|
95
|
+
}
|
96
|
+
|
97
|
+
lines.push(input);
|
98
|
+
return promptLine();
|
99
|
+
};
|
100
|
+
|
101
|
+
return promptLine();
|
102
|
+
};
|
103
|
+
|
104
|
+
// Get post content
|
105
|
+
content = await getContentInput("Enter your post content:");
|
106
|
+
}
|
107
|
+
|
108
|
+
// Generate title with AI or prompt for manual entry
|
109
|
+
let spinner = ora("Generating title suggestion...").start();
|
110
|
+
let title = "";
|
111
|
+
|
112
|
+
if (config.aiEnabled) {
|
113
|
+
title = await generateTitle(content);
|
114
|
+
spinner.succeed("Title suggestion generated");
|
115
|
+
} else {
|
116
|
+
spinner.info("AI is disabled, skipping title generation");
|
117
|
+
}
|
118
|
+
|
119
|
+
// Allow manual title override
|
120
|
+
const { titleInput } = await inquirer.prompt([
|
121
|
+
{
|
122
|
+
type: "input",
|
123
|
+
name: "titleInput",
|
124
|
+
message: "Enter post title (or press Enter to use suggestion):",
|
125
|
+
default: title,
|
126
|
+
},
|
127
|
+
]);
|
128
|
+
|
129
|
+
title = titleInput;
|
130
|
+
|
131
|
+
// Generate publish date with AI or prompt for manual entry
|
132
|
+
spinner = ora("Suggesting publish date...").start();
|
133
|
+
let publishDate = "";
|
134
|
+
|
135
|
+
if (config.aiEnabled) {
|
136
|
+
publishDate = await suggestPublishDate();
|
137
|
+
spinner.succeed(`Suggested publish date: ${publishDate}`);
|
138
|
+
} else {
|
139
|
+
spinner.info("AI is disabled, skipping date suggestion");
|
140
|
+
}
|
141
|
+
|
142
|
+
// Allow manual date override
|
143
|
+
const { dateInput } = await inquirer.prompt([
|
144
|
+
{
|
145
|
+
type: "input",
|
146
|
+
name: "dateInput",
|
147
|
+
message:
|
148
|
+
"Enter publish date (YYYY-MM-DD) or press Enter to use suggestion:",
|
149
|
+
default: publishDate,
|
150
|
+
validate: (input) => {
|
151
|
+
if (!input) return true;
|
152
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(input)
|
153
|
+
? true
|
154
|
+
: "Please use YYYY-MM-DD format";
|
155
|
+
},
|
156
|
+
},
|
157
|
+
]);
|
158
|
+
|
159
|
+
publishDate = dateInput;
|
160
|
+
|
161
|
+
// Select platforms
|
162
|
+
const { selectedPlatforms } = await inquirer.prompt([
|
163
|
+
{
|
164
|
+
type: "checkbox",
|
165
|
+
name: "selectedPlatforms",
|
166
|
+
message: "Select platforms to publish to:",
|
167
|
+
choices: [
|
168
|
+
{
|
169
|
+
name: "Bluesky",
|
170
|
+
value: "Bluesky",
|
171
|
+
checked: config.defaultPlatforms.includes("Bluesky"),
|
172
|
+
},
|
173
|
+
],
|
174
|
+
},
|
175
|
+
]);
|
176
|
+
|
177
|
+
const platforms = selectedPlatforms.join(",");
|
178
|
+
|
179
|
+
// Option to enhance content for the primary platform
|
180
|
+
if (config.aiEnabled && selectedPlatforms.length > 0) {
|
181
|
+
const primaryPlatform = selectedPlatforms[0];
|
182
|
+
|
183
|
+
const { enhance } = await inquirer.prompt([
|
184
|
+
{
|
185
|
+
type: "confirm",
|
186
|
+
name: "enhance",
|
187
|
+
message: `Would you like AI to enhance your content for ${primaryPlatform}?`,
|
188
|
+
default: false,
|
189
|
+
},
|
190
|
+
]);
|
191
|
+
|
192
|
+
if (enhance) {
|
193
|
+
spinner = ora(`Enhancing content for ${primaryPlatform}...`).start();
|
194
|
+
const enhancedContent = await enhanceContent(content, primaryPlatform);
|
195
|
+
|
196
|
+
if (enhancedContent !== content) {
|
197
|
+
spinner.succeed("Content enhanced");
|
198
|
+
|
199
|
+
// Show the original and enhanced versions
|
200
|
+
console.log("\n", chalk.cyan("Original:"), chalk.gray(content));
|
201
|
+
console.log(
|
202
|
+
"\n",
|
203
|
+
chalk.cyan("Enhanced:"),
|
204
|
+
chalk.white(enhancedContent)
|
205
|
+
);
|
206
|
+
|
207
|
+
const { useEnhanced } = await inquirer.prompt([
|
208
|
+
{
|
209
|
+
type: "confirm",
|
210
|
+
name: "useEnhanced",
|
211
|
+
message: "Use the enhanced version?",
|
212
|
+
default: true,
|
213
|
+
},
|
214
|
+
]);
|
215
|
+
|
216
|
+
if (useEnhanced) {
|
217
|
+
content = enhancedContent;
|
218
|
+
}
|
219
|
+
} else {
|
220
|
+
spinner.info("No significant enhancements suggested");
|
221
|
+
}
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
225
|
+
// Create post in database
|
226
|
+
spinner = ora("Saving post...").start();
|
227
|
+
|
228
|
+
const postId = dbCreatePost({
|
229
|
+
title,
|
230
|
+
content,
|
231
|
+
platforms,
|
232
|
+
publish_date: publishDate,
|
233
|
+
});
|
234
|
+
|
235
|
+
// Log the action
|
236
|
+
logAction("post_created", {
|
237
|
+
postId,
|
238
|
+
title,
|
239
|
+
platforms: selectedPlatforms,
|
240
|
+
publishDate,
|
241
|
+
});
|
242
|
+
|
243
|
+
spinner.succeed(`Post created successfully with ID: ${postId}`);
|
244
|
+
|
245
|
+
// Summary
|
246
|
+
console.log("\n", chalk.cyan("Post Summary:"));
|
247
|
+
console.log(` ${chalk.gray("•")} ${chalk.bold("Title:")} ${title}`);
|
248
|
+
console.log(
|
249
|
+
` ${chalk.gray("•")} ${chalk.bold("Platforms:")} ${platforms || "None"}`
|
250
|
+
);
|
251
|
+
console.log(
|
252
|
+
` ${chalk.gray("•")} ${chalk.bold("Publish Date:")} ${
|
253
|
+
publishDate || "Not scheduled"
|
254
|
+
}`
|
255
|
+
);
|
256
|
+
console.log(
|
257
|
+
` ${chalk.gray("•")} ${chalk.bold("Content:")} ${content.substring(
|
258
|
+
0,
|
259
|
+
50
|
260
|
+
)}${content.length > 50 ? "..." : ""}`
|
261
|
+
);
|
262
|
+
|
263
|
+
console.log("\n", chalk.green("✓"), "Post created successfully!");
|
264
|
+
console.log(
|
265
|
+
chalk.gray(" Run"),
|
266
|
+
chalk.cyan("social-light unpublished"),
|
267
|
+
chalk.gray("to see your scheduled posts")
|
268
|
+
);
|
269
|
+
} catch (error) {
|
270
|
+
console.error(chalk.red("Error creating post:"), error.message);
|
271
|
+
console.error(error);
|
272
|
+
process.exit(1);
|
273
|
+
}
|
274
|
+
};
|
@@ -0,0 +1,198 @@
|
|
1
|
+
import chalk from "chalk";
|
2
|
+
import inquirer from "inquirer";
|
3
|
+
import { getPosts, getPostById, updatePost, logAction } from "../utils/db.mjs";
|
4
|
+
import { getConfig } from "../utils/config.mjs";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Edit a draft post by index
|
8
|
+
* @param {Object} argv - Command arguments
|
9
|
+
*/
|
10
|
+
export const editPost = async (argv) => {
|
11
|
+
try {
|
12
|
+
// Get unpublished posts
|
13
|
+
const posts = getPosts({ published: false });
|
14
|
+
|
15
|
+
if (posts.length === 0) {
|
16
|
+
console.log(chalk.yellow("No unpublished posts found to edit."));
|
17
|
+
console.log(
|
18
|
+
chalk.gray("Run"),
|
19
|
+
chalk.cyan("social-light create"),
|
20
|
+
chalk.gray("to create a new post.")
|
21
|
+
);
|
22
|
+
return;
|
23
|
+
}
|
24
|
+
|
25
|
+
// Determine which post to edit
|
26
|
+
let postIndex = argv.index;
|
27
|
+
|
28
|
+
// If no index provided, prompt user to select a post
|
29
|
+
if (postIndex === undefined) {
|
30
|
+
const { selectedIndex } = await inquirer.prompt([
|
31
|
+
{
|
32
|
+
type: "list",
|
33
|
+
name: "selectedIndex",
|
34
|
+
message: "Select a post to edit:",
|
35
|
+
choices: posts.map((post, index) => ({
|
36
|
+
name: `[${index + 1}] ${
|
37
|
+
post.title || "No title"
|
38
|
+
} - ${post.content.substring(0, 40)}...`,
|
39
|
+
value: index + 1,
|
40
|
+
})),
|
41
|
+
},
|
42
|
+
]);
|
43
|
+
|
44
|
+
postIndex = selectedIndex;
|
45
|
+
}
|
46
|
+
|
47
|
+
// Validate post index
|
48
|
+
if (postIndex < 1 || postIndex > posts.length) {
|
49
|
+
console.error(
|
50
|
+
chalk.red(`Invalid post index. Must be between 1 and ${posts.length}.`)
|
51
|
+
);
|
52
|
+
process.exit(1);
|
53
|
+
}
|
54
|
+
|
55
|
+
// Get post by ID
|
56
|
+
const post = getPostById(posts[postIndex - 1].id);
|
57
|
+
|
58
|
+
if (!post) {
|
59
|
+
console.error(chalk.red("Post not found."));
|
60
|
+
process.exit(1);
|
61
|
+
}
|
62
|
+
|
63
|
+
// Function to handle multiline input
|
64
|
+
const getMultilineInput = async (prompt, defaultText) => {
|
65
|
+
console.log(`${prompt} (Type 'EOF' on a new line when done)`);
|
66
|
+
console.log(`Current content: ${defaultText}`);
|
67
|
+
console.log("Enter new content:");
|
68
|
+
|
69
|
+
let lines = [];
|
70
|
+
|
71
|
+
// Set up recursive prompt for multiline input
|
72
|
+
const promptLine = async () => {
|
73
|
+
const { input } = await inquirer.prompt([
|
74
|
+
{
|
75
|
+
type: "input",
|
76
|
+
name: "input",
|
77
|
+
message: ">",
|
78
|
+
},
|
79
|
+
]);
|
80
|
+
|
81
|
+
if (input === "EOF") {
|
82
|
+
return lines.length > 0 ? lines.join("\n") : defaultText;
|
83
|
+
}
|
84
|
+
|
85
|
+
lines.push(input);
|
86
|
+
return promptLine();
|
87
|
+
};
|
88
|
+
|
89
|
+
return promptLine();
|
90
|
+
};
|
91
|
+
|
92
|
+
// Ask if user wants to edit with multiline input
|
93
|
+
const { useMultiline } = await inquirer.prompt([
|
94
|
+
{
|
95
|
+
type: "confirm",
|
96
|
+
name: "useMultiline",
|
97
|
+
message: "Would you like to edit content with multiline input?",
|
98
|
+
default: true,
|
99
|
+
},
|
100
|
+
]);
|
101
|
+
|
102
|
+
// Get title
|
103
|
+
const { title } = await inquirer.prompt([
|
104
|
+
{
|
105
|
+
type: "input",
|
106
|
+
name: "title",
|
107
|
+
message: "Edit title:",
|
108
|
+
default: post.title || "",
|
109
|
+
},
|
110
|
+
]);
|
111
|
+
|
112
|
+
// Get content based on user preference
|
113
|
+
let content;
|
114
|
+
if (useMultiline) {
|
115
|
+
content = await getMultilineInput("Edit content:", post.content);
|
116
|
+
} else {
|
117
|
+
const result = await inquirer.prompt([
|
118
|
+
{
|
119
|
+
type: "input",
|
120
|
+
name: "content",
|
121
|
+
message: "Edit content:",
|
122
|
+
default: post.content,
|
123
|
+
},
|
124
|
+
]);
|
125
|
+
content = result.content;
|
126
|
+
}
|
127
|
+
|
128
|
+
// Get publish date and platforms
|
129
|
+
const { publishDate, platforms } = await inquirer.prompt([
|
130
|
+
{
|
131
|
+
type: "input",
|
132
|
+
name: "publishDate",
|
133
|
+
message: "Edit publish date (YYYY-MM-DD):",
|
134
|
+
default: post.publish_date || "",
|
135
|
+
validate: (input) => {
|
136
|
+
if (!input) return true;
|
137
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(input)
|
138
|
+
? true
|
139
|
+
: "Please use YYYY-MM-DD format";
|
140
|
+
},
|
141
|
+
},
|
142
|
+
{
|
143
|
+
type: "checkbox",
|
144
|
+
name: "platforms",
|
145
|
+
message: "Select platforms to publish to:",
|
146
|
+
choices: [
|
147
|
+
// { name: 'Twitter', value: 'Twitter', checked: post.platforms && post.platforms.includes('Twitter') },
|
148
|
+
{
|
149
|
+
name: "Bluesky",
|
150
|
+
value: "Bluesky",
|
151
|
+
checked: post.platforms && post.platforms.includes("Bluesky"),
|
152
|
+
},
|
153
|
+
// { name: 'TikTok', value: 'TikTok', checked: post.platforms && post.platforms.includes('TikTok') },
|
154
|
+
// { name: 'Instagram', value: 'Instagram', checked: post.platforms && post.platforms.includes('Instagram') },
|
155
|
+
// { name: 'LinkedIn', value: 'LinkedIn', checked: post.platforms && post.platforms.includes('LinkedIn') }
|
156
|
+
],
|
157
|
+
},
|
158
|
+
]);
|
159
|
+
|
160
|
+
const config = getConfig();
|
161
|
+
|
162
|
+
// Update post in database
|
163
|
+
const updatedPost = {
|
164
|
+
title,
|
165
|
+
content,
|
166
|
+
platforms: platforms.join(","),
|
167
|
+
publish_date: publishDate,
|
168
|
+
};
|
169
|
+
|
170
|
+
const success = updatePost(post.id, updatedPost);
|
171
|
+
|
172
|
+
if (success) {
|
173
|
+
// Log the action
|
174
|
+
logAction("post_edited", {
|
175
|
+
postId: post.id,
|
176
|
+
title: title,
|
177
|
+
});
|
178
|
+
|
179
|
+
console.log(chalk.green("\n✓ Post updated successfully!"));
|
180
|
+
console.log(
|
181
|
+
chalk.gray("Run"),
|
182
|
+
chalk.cyan("social-light unpublished"),
|
183
|
+
chalk.gray("to see your updated post.")
|
184
|
+
);
|
185
|
+
console.log(
|
186
|
+
chalk.gray("Run"),
|
187
|
+
chalk.cyan("social-light publish"),
|
188
|
+
chalk.gray("to publish eligible posts.")
|
189
|
+
);
|
190
|
+
} else {
|
191
|
+
console.error(chalk.red("Failed to update post."));
|
192
|
+
}
|
193
|
+
} catch (error) {
|
194
|
+
console.error(chalk.red("Error editing post:"), error.message);
|
195
|
+
console.error(error);
|
196
|
+
process.exit(1);
|
197
|
+
}
|
198
|
+
};
|