feat(cli): add support for repeated directus tasks
This commit is contained in:
parent
8a12e83b13
commit
1f9e2d856f
11 changed files with 601 additions and 5 deletions
|
@ -8,14 +8,16 @@ import { downloadAsset } from '../lib/download.js';
|
||||||
import { runRootScript } from '../lib/runScript.js';
|
import { runRootScript } from '../lib/runScript.js';
|
||||||
import { handleExitError } from '../lib/handlers.js';
|
import { handleExitError } from '../lib/handlers.js';
|
||||||
import { runJobsMenu } from '../lib/jobs.js';
|
import { runJobsMenu } from '../lib/jobs.js';
|
||||||
|
import { runTasksMenu } from '../lib/tasks/index.js';
|
||||||
|
|
||||||
process.on('unhandledRejection', (err) => handleExitError(err, 'Unhandled rejection'));
|
process.on('unhandledRejection', (err) => handleExitError(err, 'Unhandled rejection'));
|
||||||
process.on('uncaughtException', (err) => handleExitError(err, 'Uncaught exception'));
|
process.on('uncaughtException', (err) => handleExitError(err, 'Uncaught exception'));
|
||||||
|
|
||||||
program.name('coryd').description('🪄 Run commands, jobs, download things and have fun.').version('2.0.0');
|
program.name('coryd').description('🪄 Run commands, jobs, download things and have fun.').version('3.0.0');
|
||||||
program.command('init').description('Initialize CLI and populate required config.').action(initConfig);
|
program.command('init').description('Initialize CLI and populate required config.').action(initConfig);
|
||||||
program.command('run [script]').description('Run site scripts and commands.').action(runRootScript);
|
program.command('run [script]').description('Run site scripts and commands.').action(runRootScript);
|
||||||
program.command('jobs').description('Trigger jobs and tasks.').action(runJobsMenu);
|
program.command('tasks').description('Handle repeated tasks.').action(runTasksMenu);
|
||||||
|
program.command('jobs').description('Trigger jobs and scripts.').action(runJobsMenu);
|
||||||
program.command('download').description('Download, name and store image assets.').action(downloadAsset);
|
program.command('download').description('Download, name and store image assets.').action(downloadAsset);
|
||||||
|
|
||||||
if (process.argv.length <= 2) {
|
if (process.argv.length <= 2) {
|
||||||
|
|
|
@ -111,6 +111,33 @@ export const initConfig = async () => {
|
||||||
).bookPath
|
).bookPath
|
||||||
: 'Media assets/books';
|
: 'Media assets/books';
|
||||||
|
|
||||||
|
if (config.directus?.apiUrl) {
|
||||||
|
const { updateApiUrl } = await inquirer.prompt([{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'updateApiUrl',
|
||||||
|
message: `Directus API URL is already set to "${config.directus.apiUrl}". Do you want to update it?`,
|
||||||
|
default: false
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (updateApiUrl) {
|
||||||
|
const { apiUrl } = await inquirer.prompt([{
|
||||||
|
name: 'apiUrl',
|
||||||
|
message: 'Enter your Directus instance URL:',
|
||||||
|
validate: input => input.startsWith('http') || 'Must be a valid URL'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
config.directus.apiUrl = apiUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { apiUrl } = await inquirer.prompt([{
|
||||||
|
name: 'apiUrl',
|
||||||
|
message: 'Enter your Directus URL:',
|
||||||
|
validate: input => input.startsWith('http') || 'Must be a valid URL'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
config.directus = { ...(config.directus || {}), apiUrl };
|
||||||
|
}
|
||||||
|
|
||||||
config.globals = await fetchGlobals();
|
config.globals = await fetchGlobals();
|
||||||
|
|
||||||
await fs.ensureDir(CACHE_DIR);
|
await fs.ensureDir(CACHE_DIR);
|
||||||
|
|
80
cli/lib/directus/client.js
Normal file
80
cli/lib/directus/client.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { createDirectus, staticToken, rest } from '@directus/sdk';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '..', '..', '..', '.env') });
|
||||||
|
|
||||||
|
let directus;
|
||||||
|
let API_URL;
|
||||||
|
|
||||||
|
export const initDirectusClient = (config) => {
|
||||||
|
API_URL = config.directus?.apiUrl;
|
||||||
|
|
||||||
|
const token = process.env.DIRECTUS_API_TOKEN;
|
||||||
|
|
||||||
|
if (!API_URL || !token) throw new Error('Missing Directus API URL or token.');
|
||||||
|
|
||||||
|
directus = createDirectus(API_URL).with(staticToken(process.env.DIRECTUS_API_TOKEN)).with(rest());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDirectusClient = () => {
|
||||||
|
if (!directus) throw new Error('Directus client not initialized.');
|
||||||
|
|
||||||
|
return directus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = async (method, endpoint, body = null) => {
|
||||||
|
const API_TOKEN = process.env.DIRECTUS_API_TOKEN;
|
||||||
|
const res = await fetch(`${API_URL}/items/${endpoint}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${API_TOKEN}`,
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
|
||||||
|
throw new Error(`Directus API error: ${res.status} ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchItems = async (collection, query = '', filters = {}) => {
|
||||||
|
const API_TOKEN = process.env.DIRECTUS_API_TOKEN;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (query) params.append('search', query);
|
||||||
|
|
||||||
|
params.append('limit', '50');
|
||||||
|
|
||||||
|
for (const [field, value] of Object.entries(filters)) {
|
||||||
|
params.append(`filter[${field}][_eq]`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/items/${collection}?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${API_TOKEN}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return data?.data ?? [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to search ${collection}:`, err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateItem = async (collection, id, values) => await request('PATCH', `${collection}/${id}`, values);
|
||||||
|
|
||||||
|
export const createItem = async (collection, values) => await request('POST', collection, values);
|
46
cli/lib/directus/relationHelpers.js
Normal file
46
cli/lib/directus/relationHelpers.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import inquirer from 'inquirer';
|
||||||
|
import { searchItems } from '../directus/client.js';
|
||||||
|
|
||||||
|
export const promptForMultipleRelations = async (collection, label = collection) => {
|
||||||
|
const selectedIds = new Set();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { query } = await inquirer.prompt({
|
||||||
|
name: 'query',
|
||||||
|
message: `🔍 Search ${label} (or leave blank to finish):`
|
||||||
|
});
|
||||||
|
const trimmed = query.trim();
|
||||||
|
|
||||||
|
if (!trimmed) break;
|
||||||
|
|
||||||
|
const results = await searchItems(collection, trimmed);
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
console.warn(`⚠️ No ${collection} found for "${query}".`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selected } = await inquirer.prompt({
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'selected',
|
||||||
|
message: `✔ Select ${label} to add:`,
|
||||||
|
choices: results.map(item => ({
|
||||||
|
name: item.name || item.title || item.id,
|
||||||
|
value: item.id
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
selected.forEach(id => selectedIds.add(id));
|
||||||
|
|
||||||
|
const { again } = await inquirer.prompt({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'again',
|
||||||
|
message: `Search and add more ${label}?`,
|
||||||
|
default: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!again) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(selectedIds);
|
||||||
|
};
|
80
cli/lib/tasks/addEpisodeToShow.js
Normal file
80
cli/lib/tasks/addEpisodeToShow.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
93
cli/lib/tasks/addLinkToShare.js
Normal file
93
cli/lib/tasks/addLinkToShare.js
Normal 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
186
cli/lib/tasks/addPost.js
Normal 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
25
cli/lib/tasks/index.js
Normal 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();
|
||||||
|
};
|
43
cli/lib/tasks/updateReadingProgress.js
Normal file
43
cli/lib/tasks/updateReadingProgress.js
Normal 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 (0–100):',
|
||||||
|
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}%`);
|
||||||
|
};
|
17
cli/package-lock.json
generated
17
cli/package-lock.json
generated
|
@ -1,13 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "coryd",
|
"name": "coryd",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "coryd",
|
"name": "coryd",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@directus/sdk": "^19.1.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"figlet": "^1.8.1",
|
"figlet": "^1.8.1",
|
||||||
|
@ -20,6 +21,18 @@
|
||||||
"coryd": "bin/index.js"
|
"coryd": "bin/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@directus/sdk": {
|
||||||
|
"version": "19.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-19.1.0.tgz",
|
||||||
|
"integrity": "sha512-Nqem9BsvvGyVtAa69mGPtoMoMVkZxdIREdsWvvTzNF4/1XqaFfEiFL7PhtUNfc46/Nufus2+QUKYQbNiAWe3ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/directus/directus?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@inquirer/checkbox": {
|
"node_modules/@inquirer/checkbox": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "coryd",
|
"name": "coryd",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"description": "The CLI for my site to run scripts, manage and download assets.",
|
"description": "The CLI for my site to run scripts, manage and download assets.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"author": "Cory Dransfeldt",
|
"author": "Cory Dransfeldt",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@directus/sdk": "^19.1.0",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"commander": "^14.0.0",
|
"commander": "^14.0.0",
|
||||||
"figlet": "^1.8.1",
|
"figlet": "^1.8.1",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue