feat(cli): add support for repeated directus tasks

This commit is contained in:
Cory Dransfeldt 2025-06-08 17:21:05 -07:00
parent 8a12e83b13
commit 1f9e2d856f
No known key found for this signature in database
11 changed files with 601 additions and 5 deletions

View file

@ -0,0 +1,80 @@
import inquirer from 'inquirer';
import { loadConfig } from '../config.js';
import { initDirectusClient, getDirectusClient, searchItems, createItem, updateItem } from '../directus/client.js';
export const addEpisodeToShow = async () => {
const config = await loadConfig();
initDirectusClient(config);
const directus = getDirectusClient();
const showResults = await inquirer.prompt({
name: 'query',
message: 'Search for a show:',
});
const matches = await searchItems('shows', showResults.query);
if (!matches.length) {
console.warn('⚠️ No matching shows found.');
return;
}
const { showId } = await inquirer.prompt({
type: 'list',
name: 'showId',
message: 'Select a show:',
choices: matches.map(s => ({
name: s.title || s.name || s.id,
value: s.id,
})),
});
const { season_number, episode_number, plays } = await inquirer.prompt([
{
name: 'season_number',
message: 'Season number:',
validate: val => !isNaN(val),
},
{
name: 'episode_number',
message: 'Episode number:',
validate: val => !isNaN(val),
},
{
name: 'plays',
message: 'Play count:',
default: 0,
validate: val => !isNaN(val),
},
]);
const existing = await searchItems('episodes', `${season_number} ${episode_number}`);
const match = existing.find(e =>
Number(e.season_number) === Number(season_number) &&
Number(e.episode_number) === Number(episode_number) &&
e.show === showId
);
if (match) {
const { update } = await inquirer.prompt({
type: 'confirm',
name: 'update',
message: `Episode exists. Update play count from ${match.plays ?? 0} to ${plays}?`,
default: true,
});
if (update) {
await updateItem('episodes', match.id, { plays });
console.log(`✅ Updated episode: S${season_number}E${episode_number}`);
} else {
console.warn('⚠️ Skipped update.');
}
} else {
await createItem('episodes', {
season_number: Number(season_number),
episode_number: Number(episode_number),
plays: Number(plays),
show: showId,
});
console.log(`📺 Created episode S${season_number}E${episode_number}`);
}
};

View file

@ -0,0 +1,93 @@
import inquirer from 'inquirer';
import { loadConfig } from '../config.js';
import { initDirectusClient, searchItems, createItem } from '../directus/client.js';
export const addLinkToShare = async () => {
const config = await loadConfig();
initDirectusClient(config);
const { title, link, description, authorQuery } = await inquirer.prompt([{
name: 'title',
message: '📝 Title for the link:',
validate: input => !!input || 'Title is required'
},
{
name: 'link',
message: '🔗 URL to share:',
validate: input => input.startsWith('http') || 'Must be a valid URL'
},
{
name: 'description',
message: '🗒 Description (optional):',
default: ''
},
{
name: 'authorQuery',
message: '👤 Search for an author:',
}]);
const authorMatches = await searchItems('authors', authorQuery);
if (!authorMatches.length) {
console.log('❌ No matching authors found.');
return;
}
const { author } = await inquirer.prompt({
type: 'list',
name: 'author',
message: 'Select an author:',
choices: authorMatches.map(a => ({
name: a.name || a.id,
value: a.id,
}))
});
let tagIds = [];
while (true) {
const { query } = await inquirer.prompt({
name: 'query',
message: '🏷 Search for tags (or leave blank to finish):',
});
const trimmedQuery = query.trim();
if (!trimmedQuery) break;
const tags = await searchItems('tags', trimmedQuery);
if (!tags.length) {
console.warn(`⚠️ No tags found matching "${trimmedQuery}"`);
continue;
}
const { selected } = await inquirer.prompt({
type: 'checkbox',
name: 'selected',
message: '✔ Select tags to add:',
choices: tags.map(tag => ({ name: tag.name, value: tag.id }))
});
tagIds.push(...selected);
const { again } = await inquirer.prompt({
type: 'confirm',
name: 'again',
message: 'Search and select more tags?',
default: false,
});
if (!again) break;
}
await createItem('links', {
title,
link,
description,
author,
link_tags: tagIds.map(tagId => ({ tags_id: tagId })),
date: new Date().toISOString()
});
console.log('✅ Link created successfully.');
};

186
cli/lib/tasks/addPost.js Normal file
View file

@ -0,0 +1,186 @@
import inquirer from 'inquirer';
import { loadConfig } from '../config.js';
import { initDirectusClient, createItem, searchItems } from '../directus/client.js';
import { promptForMultipleRelations } from '../directus/relationHelpers.js';
const ASSOCIATED_MEDIA_TYPES = ['artists', 'books', 'movies', 'shows', 'genres'];
const BLOCK_COLLECTIONS = [
'youtube_player',
'github_banner',
'npm_banner',
'rss_banner',
'calendar_banner',
'forgejo_banner'
];
export const addPost = async () => {
const config = await loadConfig();
initDirectusClient(config);
const { title, description, content, featured } = await inquirer.prompt([{
name: 'title',
message: '📝 Title:',
validate: input => !!input || 'Title is required'
},
{
name: 'description',
message: '🗒 Description:',
default: ''
},
{
name: 'content',
message: '📄 Content:',
default: ''
},
{
type: 'confirm',
name: 'featured',
message: '⭐ Featured?',
default: false
}]);
let tagIds = [];
while (true) {
const { query } = await inquirer.prompt({
name: 'query',
message: '🏷 Search for tags (or leave blank to finish):',
});
const trimmedQuery = query.trim();
if (!trimmedQuery) break;
const tags = await searchItems('tags', trimmedQuery);
if (!tags.length) {
console.warn(`⚠️ No tags found matching "${trimmedQuery}"`);
continue;
}
const { selected } = await inquirer.prompt({
type: 'checkbox',
name: 'selected',
message: '✔ Select tags to add:',
choices: tags.map(tag => ({ name: tag.name, value: tag.id }))
});
tagIds.push(...selected);
const { again } = await inquirer.prompt({
type: 'confirm',
name: 'again',
message: 'Search and select more tags?',
default: false,
});
if (!again) break;
}
const selectedBlocks = [];
const { includeBlocks } = await inquirer.prompt({
type: 'confirm',
name: 'includeBlocks',
message: ' Add blocks?',
default: false
});
if (includeBlocks) {
while (true) {
const { collection } = await inquirer.prompt({
type: 'list',
name: 'collection',
message: '🧱 Choose a block collection (or Cancel to finish):',
choices: [...BLOCK_COLLECTIONS, new inquirer.Separator(), 'Cancel']
});
if (collection === 'Cancel') break;
const { query } = await inquirer.prompt({
name: 'query',
message: `🔍 Search ${collection}:`
});
const results = await searchItems(collection, query);
if (!results.length) {
console.warn(`⚠️ No items found in "${collection}" matching "${query}"`);
continue;
}
const { itemId } = await inquirer.prompt({
type: 'list',
name: 'itemId',
message: `Select an item from ${collection}:`,
choices: results.map(item => ({
name: item.title || item.name || item.id,
value: item.id
}))
});
selectedBlocks.push({ collection, item: itemId });
const { again } = await inquirer.prompt({
type: 'confirm',
name: 'again',
message: ' Add another block?',
default: false
});
if (!again) break;
}
}
const associatedMediaPayload = {};
const { includeMedia } = await inquirer.prompt({
type: 'confirm',
name: 'includeMedia',
message: ' Add associated media?',
default: false
});
if (includeMedia) {
for (const mediaType of ASSOCIATED_MEDIA_TYPES) {
const { query } = await inquirer.prompt({
name: 'query',
message: `🔎 Search for ${mediaType} to associate (or leave blank to skip):`
});
if (!query.trim()) continue;
const matches = await searchItems(mediaType, query.trim());
if (!matches.length) {
console.warn(`⚠️ No ${mediaType} found matching "${query.trim()}"`);
continue;
}
const { selected } = await inquirer.prompt({
type: 'checkbox',
name: 'selected',
message: `✔ Select ${mediaType} to associate:`,
choices: matches.map(m => ({
name: m.name_string || m.title || m.name || m.label || m.id,
value: m.id
}))
});
if (selected.length) associatedMediaPayload[`${mediaType}`] = selected.map(id => ({ [`${mediaType}_id`]: id }));
}
}
const media = await promptForMultipleRelations('media', 'Associated media');
const payload = {
title,
description,
content,
featured,
date: new Date().toISOString(),
post_tags: tagIds.map(tagId => ({ tags_id: tagId })),
blocks: selectedBlocks,
...associatedMediaPayload
};
await createItem('posts', payload);
console.log('✅ Post created successfully.');
};

25
cli/lib/tasks/index.js Normal file
View file

@ -0,0 +1,25 @@
import inquirer from 'inquirer';
import { addPost } from './addPost.js';
import { addLinkToShare } from './addLinkToShare.js';
import { addEpisodeToShow } from './addEpisodeToShow.js';
import { updateReadingProgress } from './updateReadingProgress.js';
const TASKS = [
{ name: '📄 Add post', handler: addPost },
{ name: '🔗 Add link to share', handler: addLinkToShare },
{ name: ' Add episode to show', handler: addEpisodeToShow },
{ name: '📚 Update reading progress', handler: updateReadingProgress },
];
export const runTasksMenu = async () => {
const { task } = await inquirer.prompt([
{
type: 'list',
name: 'task',
message: 'Select a task to run:',
choices: TASKS.map(t => ({ name: t.name, value: t.handler }))
}
]);
await task();
};

View file

@ -0,0 +1,43 @@
import inquirer from 'inquirer';
import { loadConfig } from '../config.js';
import { initDirectusClient, searchItems, updateItem } from '../directus/client.js';
export const updateReadingProgress = async () => {
const config = await loadConfig();
initDirectusClient(config);
const readingBooks = await searchItems('books', '', { read_status: 'started' });
if (!readingBooks.length) {
console.log('📖 No books currently marked as "started".');
return;
}
const { bookId } = await inquirer.prompt({
type: 'list',
name: 'bookId',
message: '📚 Select a book to update progress:',
choices: readingBooks.map(book => {
const title = book.title || book.name || `Book #${book.id}`;
const progress = book.progress ?? 0;
return {
name: `${title} (${progress}%)`,
value: book.id
};
})
});
const { progress } = await inquirer.prompt({
name: 'progress',
message: '📕 New progress percentage (0100):',
validate: input => {
const num = Number(input);
return !isNaN(num) && num >= 0 && num <= 100 || 'Enter a number from 0 to 100';
}
});
await updateItem('books', bookId, { progress: Number(progress) });
console.log(`✅ Updated book progress to ${progress}%`);
};