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,315 @@
|
|
1
|
+
import express from "express";
|
2
|
+
import cors from "cors";
|
3
|
+
import path from "path";
|
4
|
+
import { fileURLToPath } from "url";
|
5
|
+
import chalk from "chalk";
|
6
|
+
import { exec } from "child_process";
|
7
|
+
import {
|
8
|
+
getPosts,
|
9
|
+
getPostById,
|
10
|
+
createPost,
|
11
|
+
updatePost,
|
12
|
+
markAsPublished,
|
13
|
+
logAction,
|
14
|
+
} from "../utils/db.mjs";
|
15
|
+
import { getSocialAPI } from "../utils/social/index.mjs";
|
16
|
+
import {
|
17
|
+
generateTitle,
|
18
|
+
suggestPublishDate,
|
19
|
+
enhanceContent,
|
20
|
+
} from "../utils/ai.mjs";
|
21
|
+
import { getConfig } from "../utils/config.mjs";
|
22
|
+
|
23
|
+
// Get directory name in ESM
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Start the web server
|
28
|
+
* @param {Object} argv - Command arguments
|
29
|
+
*/
|
30
|
+
export const startServer = async (argv) => {
|
31
|
+
const app = express();
|
32
|
+
const port = argv.port || 3000;
|
33
|
+
const shouldOpen = !argv.noOpen; // Open by default, disabled with --no-open
|
34
|
+
|
35
|
+
// Middleware
|
36
|
+
app.use(cors());
|
37
|
+
app.use(express.json());
|
38
|
+
|
39
|
+
// Serve static files from 'client' directory
|
40
|
+
app.use(express.static(path.join(__dirname, "client")));
|
41
|
+
|
42
|
+
// API routes
|
43
|
+
setupApiRoutes(app);
|
44
|
+
|
45
|
+
// Serve React app for all other routes
|
46
|
+
app.get("*", (req, res) => {
|
47
|
+
res.sendFile(path.join(__dirname, "client", "index.html"));
|
48
|
+
});
|
49
|
+
|
50
|
+
// Start server
|
51
|
+
app.listen(port, () => {
|
52
|
+
const url = `http://localhost:${port}`;
|
53
|
+
console.log(chalk.green(`✓ Server started on port ${port}`));
|
54
|
+
console.log(
|
55
|
+
chalk.cyan(
|
56
|
+
`Opening ${url} in your browser...`
|
57
|
+
)
|
58
|
+
);
|
59
|
+
console.log(chalk.gray("Press Ctrl+C to stop the server"));
|
60
|
+
|
61
|
+
// Open URL in default browser if not disabled
|
62
|
+
if (shouldOpen) {
|
63
|
+
// Open URL in default browser based on platform
|
64
|
+
let command;
|
65
|
+
switch (process.platform) {
|
66
|
+
case 'darwin': // macOS
|
67
|
+
command = `open ${url}`;
|
68
|
+
break;
|
69
|
+
case 'win32': // Windows
|
70
|
+
command = `start ${url}`;
|
71
|
+
break;
|
72
|
+
default: // Linux and others
|
73
|
+
command = `xdg-open ${url}`;
|
74
|
+
}
|
75
|
+
|
76
|
+
// Execute the command to open the browser
|
77
|
+
exec(command, (error) => {
|
78
|
+
if (error) {
|
79
|
+
console.error(chalk.yellow(`Unable to open browser automatically: ${error.message}`));
|
80
|
+
console.log(chalk.cyan(`Open ${url} in your browser to access the web interface`));
|
81
|
+
}
|
82
|
+
});
|
83
|
+
} else {
|
84
|
+
console.log(chalk.cyan(`Open ${url} in your browser to access the web interface`));
|
85
|
+
}
|
86
|
+
});
|
87
|
+
};
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Set up API routes
|
91
|
+
* @param {Express} app - Express app
|
92
|
+
*/
|
93
|
+
const setupApiRoutes = (app) => {
|
94
|
+
// Get API info
|
95
|
+
app.get("/api", (req, res) => {
|
96
|
+
res.json({
|
97
|
+
name: "Social Light API",
|
98
|
+
version: "1.0.0",
|
99
|
+
endpoints: [
|
100
|
+
"/api/posts",
|
101
|
+
"/api/posts/:id",
|
102
|
+
"/api/publish/:id",
|
103
|
+
"/api/ai/title",
|
104
|
+
"/api/ai/date",
|
105
|
+
"/api/ai/enhance",
|
106
|
+
"/api/config",
|
107
|
+
],
|
108
|
+
});
|
109
|
+
});
|
110
|
+
|
111
|
+
// Get configuration
|
112
|
+
app.get("/api/config", (req, res) => {
|
113
|
+
const config = getConfig();
|
114
|
+
|
115
|
+
// Remove sensitive information
|
116
|
+
const safeConfig = {
|
117
|
+
...config,
|
118
|
+
// Add default platforms for client
|
119
|
+
platforms: [
|
120
|
+
// { id: 'twitter', name: 'Twitter', icon: 'twitter' },
|
121
|
+
{ id: "bluesky", name: "Bluesky", icon: "cloud" },
|
122
|
+
// { id: 'tiktok', name: 'TikTok', icon: 'music' }
|
123
|
+
],
|
124
|
+
};
|
125
|
+
|
126
|
+
res.json(safeConfig);
|
127
|
+
});
|
128
|
+
|
129
|
+
// Get all posts
|
130
|
+
app.get("/api/posts", (req, res) => {
|
131
|
+
try {
|
132
|
+
const published = req.query.published === "true";
|
133
|
+
const posts = getPosts({ published });
|
134
|
+
res.json(posts);
|
135
|
+
} catch (error) {
|
136
|
+
res.status(500).json({ error: error.message });
|
137
|
+
}
|
138
|
+
});
|
139
|
+
|
140
|
+
// Get post by ID
|
141
|
+
app.get("/api/posts/:id", (req, res) => {
|
142
|
+
try {
|
143
|
+
const post = getPostById(parseInt(req.params.id, 10));
|
144
|
+
|
145
|
+
if (!post) {
|
146
|
+
return res.status(404).json({ error: "Post not found" });
|
147
|
+
}
|
148
|
+
|
149
|
+
res.json(post);
|
150
|
+
} catch (error) {
|
151
|
+
res.status(500).json({ error: error.message });
|
152
|
+
}
|
153
|
+
});
|
154
|
+
|
155
|
+
// Create new post
|
156
|
+
app.post("/api/posts", async (req, res) => {
|
157
|
+
try {
|
158
|
+
const { title, content, platforms, publish_date } = req.body;
|
159
|
+
|
160
|
+
if (!content) {
|
161
|
+
return res.status(400).json({ error: "Content is required" });
|
162
|
+
}
|
163
|
+
|
164
|
+
const postId = createPost({
|
165
|
+
title,
|
166
|
+
content,
|
167
|
+
platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
|
168
|
+
publish_date,
|
169
|
+
});
|
170
|
+
|
171
|
+
logAction("post_created", { postId, source: "web" });
|
172
|
+
|
173
|
+
res.status(201).json({ id: postId });
|
174
|
+
} catch (error) {
|
175
|
+
res.status(500).json({ error: error.message });
|
176
|
+
}
|
177
|
+
});
|
178
|
+
|
179
|
+
// Update post
|
180
|
+
app.put("/api/posts/:id", async (req, res) => {
|
181
|
+
try {
|
182
|
+
const id = parseInt(req.params.id, 10);
|
183
|
+
const { title, content, platforms, publish_date } = req.body;
|
184
|
+
|
185
|
+
const post = getPostById(id);
|
186
|
+
|
187
|
+
if (!post) {
|
188
|
+
return res.status(404).json({ error: "Post not found" });
|
189
|
+
}
|
190
|
+
|
191
|
+
const success = updatePost(id, {
|
192
|
+
title,
|
193
|
+
content,
|
194
|
+
platforms: Array.isArray(platforms) ? platforms.join(",") : platforms,
|
195
|
+
publish_date,
|
196
|
+
});
|
197
|
+
|
198
|
+
if (!success) {
|
199
|
+
return res.status(500).json({ error: "Failed to update post" });
|
200
|
+
}
|
201
|
+
|
202
|
+
logAction("post_updated", { postId: id, source: "web" });
|
203
|
+
|
204
|
+
res.json({ success: true });
|
205
|
+
} catch (error) {
|
206
|
+
res.status(500).json({ error: error.message });
|
207
|
+
}
|
208
|
+
});
|
209
|
+
|
210
|
+
// Publish post
|
211
|
+
app.post("/api/publish/:id", async (req, res) => {
|
212
|
+
try {
|
213
|
+
const id = parseInt(req.params.id, 10);
|
214
|
+
const post = getPostById(id);
|
215
|
+
|
216
|
+
if (!post) {
|
217
|
+
return res.status(404).json({ error: "Post not found" });
|
218
|
+
}
|
219
|
+
|
220
|
+
if (post.published) {
|
221
|
+
return res.status(400).json({ error: "Post is already published" });
|
222
|
+
}
|
223
|
+
|
224
|
+
if (!post.platforms) {
|
225
|
+
return res
|
226
|
+
.status(400)
|
227
|
+
.json({ error: "No platforms specified for post" });
|
228
|
+
}
|
229
|
+
|
230
|
+
// Initialize social API
|
231
|
+
const socialAPI = getSocialAPI();
|
232
|
+
const platforms = post.platforms.split(",").map((p) => p.trim());
|
233
|
+
|
234
|
+
// Publish post to specified platforms
|
235
|
+
const result = await socialAPI.post({
|
236
|
+
text: post.content,
|
237
|
+
title: post.title,
|
238
|
+
platforms,
|
239
|
+
});
|
240
|
+
|
241
|
+
// Check if post was successfully published to at least one platform
|
242
|
+
const anySuccess = Object.values(result.results).some((r) => r.success);
|
243
|
+
|
244
|
+
if (anySuccess) {
|
245
|
+
markAsPublished(id);
|
246
|
+
|
247
|
+
// Log the action with platform results
|
248
|
+
logAction("post_published", {
|
249
|
+
postId: id,
|
250
|
+
platforms: result.results,
|
251
|
+
source: "web",
|
252
|
+
});
|
253
|
+
|
254
|
+
res.json({
|
255
|
+
success: true,
|
256
|
+
platforms: result.results,
|
257
|
+
});
|
258
|
+
} else {
|
259
|
+
res.status(500).json({
|
260
|
+
success: false,
|
261
|
+
error: "Failed to publish to any platform",
|
262
|
+
platforms: result.results,
|
263
|
+
});
|
264
|
+
}
|
265
|
+
} catch (error) {
|
266
|
+
res.status(500).json({ error: error.message });
|
267
|
+
}
|
268
|
+
});
|
269
|
+
|
270
|
+
// Generate title with AI
|
271
|
+
app.post("/api/ai/title", async (req, res) => {
|
272
|
+
try {
|
273
|
+
const { content } = req.body;
|
274
|
+
|
275
|
+
if (!content) {
|
276
|
+
return res.status(400).json({ error: "Content is required" });
|
277
|
+
}
|
278
|
+
|
279
|
+
const title = await generateTitle(content);
|
280
|
+
res.json({ title });
|
281
|
+
} catch (error) {
|
282
|
+
res.status(500).json({ error: error.message });
|
283
|
+
}
|
284
|
+
});
|
285
|
+
|
286
|
+
// Suggest publish date with AI
|
287
|
+
app.get("/api/ai/date", async (req, res) => {
|
288
|
+
try {
|
289
|
+
const date = await suggestPublishDate();
|
290
|
+
res.json({ date });
|
291
|
+
} catch (error) {
|
292
|
+
res.status(500).json({ error: error.message });
|
293
|
+
}
|
294
|
+
});
|
295
|
+
|
296
|
+
// Enhance content with AI
|
297
|
+
app.post("/api/ai/enhance", async (req, res) => {
|
298
|
+
try {
|
299
|
+
const { content, platform } = req.body;
|
300
|
+
|
301
|
+
if (!content) {
|
302
|
+
return res.status(400).json({ error: "Content is required" });
|
303
|
+
}
|
304
|
+
|
305
|
+
if (!platform) {
|
306
|
+
return res.status(400).json({ error: "Platform is required" });
|
307
|
+
}
|
308
|
+
|
309
|
+
const enhanced = await enhanceContent(content, platform);
|
310
|
+
res.json({ enhanced });
|
311
|
+
} catch (error) {
|
312
|
+
res.status(500).json({ error: error.message });
|
313
|
+
}
|
314
|
+
});
|
315
|
+
};
|
package/src/utils/ai.mjs
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
import { OpenAI } from 'openai';
|
2
|
+
import { getConfig } from './config.mjs';
|
3
|
+
import { getPosts, getDb } from './db.mjs';
|
4
|
+
|
5
|
+
// Initialize OpenAI client
|
6
|
+
let openaiClient = null;
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Get OpenAI client instance
|
10
|
+
* @returns {OpenAI|null} OpenAI client or null if AI is disabled
|
11
|
+
* @example
|
12
|
+
* const openai = getOpenAIClient();
|
13
|
+
* if (openai) {
|
14
|
+
* // Use openai client
|
15
|
+
* }
|
16
|
+
*/
|
17
|
+
export const getOpenAIClient = () => {
|
18
|
+
const config = getConfig();
|
19
|
+
|
20
|
+
if (!config.aiEnabled) {
|
21
|
+
return null;
|
22
|
+
}
|
23
|
+
|
24
|
+
if (!openaiClient) {
|
25
|
+
// Attempt to get API key from environment variable
|
26
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
27
|
+
|
28
|
+
if (!apiKey) {
|
29
|
+
console.warn('Warning: OPENAI_API_KEY environment variable not set. AI features will be limited.');
|
30
|
+
return null;
|
31
|
+
}
|
32
|
+
|
33
|
+
openaiClient = new OpenAI({
|
34
|
+
apiKey
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
return openaiClient;
|
39
|
+
};
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Generate a title for a post using AI
|
43
|
+
* @param {string} content - Post content
|
44
|
+
* @returns {string} Generated title
|
45
|
+
* @example
|
46
|
+
* const title = await generateTitle('This is my first post about AI technology...');
|
47
|
+
*/
|
48
|
+
export const generateTitle = async (content) => {
|
49
|
+
const openai = getOpenAIClient();
|
50
|
+
|
51
|
+
if (!openai) {
|
52
|
+
// Fallback to simple title generation if AI is disabled
|
53
|
+
return content.split(' ').slice(0, 5).join(' ') + '...';
|
54
|
+
}
|
55
|
+
|
56
|
+
try {
|
57
|
+
const response = await openai.chat.completions.create({
|
58
|
+
model: 'gpt-3.5-turbo',
|
59
|
+
messages: [
|
60
|
+
{
|
61
|
+
role: 'system',
|
62
|
+
content: 'You are a headline writer assistant that creates catchy, engaging titles for social media posts. Generate a short, attention-grabbing title based on the content provided. Keep it under 60 characters if possible.'
|
63
|
+
},
|
64
|
+
{
|
65
|
+
role: 'user',
|
66
|
+
content: `Create a title for this social media post: "${content.substring(0, 500)}${content.length > 500 ? '...' : ''}"`
|
67
|
+
}
|
68
|
+
],
|
69
|
+
max_tokens: 60
|
70
|
+
});
|
71
|
+
|
72
|
+
return response.choices[0].message.content.trim().replace(/^"|"$/g, '');
|
73
|
+
} catch (error) {
|
74
|
+
console.error('Error generating title:', error.message);
|
75
|
+
// Fallback
|
76
|
+
return content.split(' ').slice(0, 5).join(' ') + '...';
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Suggest a publish date for a post using AI
|
82
|
+
* @returns {string} Suggested publish date in YYYY-MM-DD format
|
83
|
+
* @example
|
84
|
+
* const date = await suggestPublishDate();
|
85
|
+
*/
|
86
|
+
export const suggestPublishDate = async () => {
|
87
|
+
const openai = getOpenAIClient();
|
88
|
+
|
89
|
+
// Get database connection and check if posts table exists
|
90
|
+
const db = getDb();
|
91
|
+
|
92
|
+
try {
|
93
|
+
// Check if posts table exists
|
94
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='posts';").get();
|
95
|
+
|
96
|
+
if (!tableExists) {
|
97
|
+
// If table doesn't exist, just return tomorrow's date
|
98
|
+
const tomorrow = new Date();
|
99
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
100
|
+
return tomorrow.toISOString().split('T')[0];
|
101
|
+
}
|
102
|
+
|
103
|
+
// Get historical posts for analysis if table exists
|
104
|
+
const posts = getPosts();
|
105
|
+
const today = new Date();
|
106
|
+
|
107
|
+
if (!openai || posts.length < 3) {
|
108
|
+
// Fallback to simple date suggestion if AI is disabled or not enough data
|
109
|
+
const tomorrow = new Date(today);
|
110
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
111
|
+
return tomorrow.toISOString().split('T')[0];
|
112
|
+
}
|
113
|
+
|
114
|
+
// Format post history for the AI
|
115
|
+
const postHistory = posts
|
116
|
+
.filter(post => post.publish_date)
|
117
|
+
.map(post => ({
|
118
|
+
date: post.publish_date,
|
119
|
+
platform: post.platforms
|
120
|
+
}));
|
121
|
+
|
122
|
+
const response = await openai.chat.completions.create({
|
123
|
+
model: 'gpt-3.5-turbo',
|
124
|
+
messages: [
|
125
|
+
{
|
126
|
+
role: 'system',
|
127
|
+
content: 'You are a social media scheduling assistant. Based on the user\'s posting history, suggest an optimal date for their next post. Consider post frequency, patterns, and optimal timing. Return only the date in YYYY-MM-DD format.'
|
128
|
+
},
|
129
|
+
{
|
130
|
+
role: 'user',
|
131
|
+
content: `Here is my posting history: ${JSON.stringify(postHistory)}. Today is ${today.toISOString().split('T')[0]}. When should I schedule my next post?`
|
132
|
+
}
|
133
|
+
],
|
134
|
+
max_tokens: 20
|
135
|
+
});
|
136
|
+
|
137
|
+
const suggestedDate = response.choices[0].message.content.trim();
|
138
|
+
|
139
|
+
// Validate date format
|
140
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(suggestedDate)) {
|
141
|
+
return suggestedDate;
|
142
|
+
}
|
143
|
+
|
144
|
+
// Extract date if the AI included other text
|
145
|
+
const dateMatch = suggestedDate.match(/\d{4}-\d{2}-\d{2}/);
|
146
|
+
if (dateMatch) {
|
147
|
+
return dateMatch[0];
|
148
|
+
}
|
149
|
+
|
150
|
+
// Fallback
|
151
|
+
const tomorrow = new Date(today);
|
152
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
153
|
+
return tomorrow.toISOString().split('T')[0];
|
154
|
+
} catch (error) {
|
155
|
+
console.error('Error suggesting publish date:', error.message);
|
156
|
+
|
157
|
+
// Fallback
|
158
|
+
const today = new Date();
|
159
|
+
const tomorrow = new Date(today);
|
160
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
161
|
+
return tomorrow.toISOString().split('T')[0];
|
162
|
+
}
|
163
|
+
};
|
164
|
+
|
165
|
+
/**
|
166
|
+
* Enhance content with AI suggestions
|
167
|
+
* @param {string} content - Original content
|
168
|
+
* @param {string} platform - Target platform
|
169
|
+
* @returns {string} Enhanced content
|
170
|
+
* @example
|
171
|
+
* const enhanced = await enhanceContent('Check out our new product!', 'Twitter');
|
172
|
+
*/
|
173
|
+
export const enhanceContent = async (content, platform) => {
|
174
|
+
const openai = getOpenAIClient();
|
175
|
+
|
176
|
+
if (!openai) {
|
177
|
+
return content;
|
178
|
+
}
|
179
|
+
|
180
|
+
try {
|
181
|
+
const response = await openai.chat.completions.create({
|
182
|
+
model: 'gpt-3.5-turbo',
|
183
|
+
messages: [
|
184
|
+
{
|
185
|
+
role: 'system',
|
186
|
+
content: `You are a social media expert who helps enhance posts for ${platform}. Provide an improved version of the user's content, optimized for engagement on ${platform}. You may suggest hashtags, emojis, or slight rewording, but preserve the original message and voice.`
|
187
|
+
},
|
188
|
+
{
|
189
|
+
role: 'user',
|
190
|
+
content: `Enhance this ${platform} post: "${content}"`
|
191
|
+
}
|
192
|
+
],
|
193
|
+
max_tokens: 1000
|
194
|
+
});
|
195
|
+
|
196
|
+
return response.choices[0].message.content.trim().replace(/^"|"$/g, '');
|
197
|
+
} catch (error) {
|
198
|
+
console.error('Error enhancing content:', error.message);
|
199
|
+
return content;
|
200
|
+
}
|
201
|
+
};
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import fs from "fs-extra";
|
2
|
+
import path from "path";
|
3
|
+
import os from "os";
|
4
|
+
|
5
|
+
// Default configuration
|
6
|
+
const defaultConfig = {
|
7
|
+
dbPath: "~/.social-light/social-light.db",
|
8
|
+
defaultPlatforms: ["Bluesky"],
|
9
|
+
aiEnabled: true,
|
10
|
+
credentials: {
|
11
|
+
openai: {
|
12
|
+
apiKey: "",
|
13
|
+
},
|
14
|
+
bluesky: {
|
15
|
+
handle: "",
|
16
|
+
password: "",
|
17
|
+
service: "https://bsky.social",
|
18
|
+
},
|
19
|
+
},
|
20
|
+
};
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Get config file path
|
24
|
+
* @returns {string} Path to config file
|
25
|
+
* @example
|
26
|
+
* const configPath = getConfigPath();
|
27
|
+
*/
|
28
|
+
export const getConfigPath = () => {
|
29
|
+
return path.join(os.homedir(), ".social-light", "config.json");
|
30
|
+
};
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Check if config exists
|
34
|
+
* @returns {boolean} True if config exists
|
35
|
+
* @example
|
36
|
+
* const exists = configExists();
|
37
|
+
*/
|
38
|
+
export const configExists = () => {
|
39
|
+
return fs.existsSync(getConfigPath());
|
40
|
+
};
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Create default config
|
44
|
+
* @returns {Object} Created config object
|
45
|
+
* @example
|
46
|
+
* const config = createDefaultConfig();
|
47
|
+
*/
|
48
|
+
export const createDefaultConfig = () => {
|
49
|
+
const configPath = getConfigPath();
|
50
|
+
const configDir = path.dirname(configPath);
|
51
|
+
|
52
|
+
fs.ensureDirSync(configDir);
|
53
|
+
fs.writeJsonSync(configPath, defaultConfig, { spaces: 2 });
|
54
|
+
|
55
|
+
return defaultConfig;
|
56
|
+
};
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Get current config, creating default if it doesn't exist
|
60
|
+
* @returns {Object} Config object
|
61
|
+
* @example
|
62
|
+
* const config = getConfig();
|
63
|
+
* console.log(config.aiEnabled);
|
64
|
+
*/
|
65
|
+
export const getConfig = () => {
|
66
|
+
if (!configExists()) {
|
67
|
+
return createDefaultConfig();
|
68
|
+
}
|
69
|
+
|
70
|
+
return fs.readJsonSync(getConfigPath());
|
71
|
+
};
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Update config
|
75
|
+
* @param {Object} updates - Config fields to update
|
76
|
+
* @returns {Object} Updated config
|
77
|
+
* @example
|
78
|
+
* const updatedConfig = updateConfig({ aiEnabled: false });
|
79
|
+
*/
|
80
|
+
export const updateConfig = (updates) => {
|
81
|
+
const config = getConfig();
|
82
|
+
const newConfig = { ...config, ...updates };
|
83
|
+
|
84
|
+
fs.writeJsonSync(getConfigPath(), newConfig, { spaces: 2 });
|
85
|
+
|
86
|
+
return newConfig;
|
87
|
+
};
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Get credentials from config
|
91
|
+
* @param {string} platform - Platform name (e.g., 'bluesky', 'openai')
|
92
|
+
* @returns {Object} Credentials object
|
93
|
+
* @example
|
94
|
+
* const blueskyCredentials = getCredentials('bluesky');
|
95
|
+
*/
|
96
|
+
export const getCredentials = (platform) => {
|
97
|
+
const config = getConfig();
|
98
|
+
return config.credentials?.[platform.toLowerCase()] || {};
|
99
|
+
};
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Update credentials in config
|
103
|
+
* @param {string} platform - Platform name (e.g., 'bluesky', 'openai')
|
104
|
+
* @param {Object} credentials - Credentials object
|
105
|
+
* @returns {Object} Updated config
|
106
|
+
* @example
|
107
|
+
* const updatedConfig = updateCredentials('bluesky', { handle: 'user.bsky.social', password: 'apppassword' });
|
108
|
+
*/
|
109
|
+
export const updateCredentials = (platform, credentials) => {
|
110
|
+
const config = getConfig();
|
111
|
+
|
112
|
+
// Ensure credentials object exists
|
113
|
+
if (!config.credentials) {
|
114
|
+
config.credentials = {};
|
115
|
+
}
|
116
|
+
|
117
|
+
// Update credentials for the platform
|
118
|
+
config.credentials[platform.toLowerCase()] = {
|
119
|
+
...config.credentials[platform.toLowerCase()],
|
120
|
+
...credentials,
|
121
|
+
};
|
122
|
+
|
123
|
+
// Save updated config
|
124
|
+
fs.writeJsonSync(getConfigPath(), config, { spaces: 2 });
|
125
|
+
|
126
|
+
return config;
|
127
|
+
};
|