UNPKG

logggai

Version:

AI-powered CLI for transforming your development work into professional content

692 lines 30.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.postCommand = postCommand; const inquirer_1 = require("inquirer"); const chalk = require("chalk"); const ora_1 = require("ora"); const { isLoggedIn, getCurrentContext, getCurrentOrganizationId, setConfig } = require('../lib/config'); const api_1 = require("../lib/api"); const upgrade_handler_1 = require("../lib/upgrade-handler"); const auth_1 = require("../lib/auth"); const MAX_CONTENT_LENGTH = 8000; // Helper function to synchronize CLI context with server async function synchronizeContextWithServer() { try { const currentContext = getCurrentContext() || 'personal'; const currentOrgId = getCurrentOrganizationId(); // Synchronize with server if (currentContext === 'organization' && currentOrgId) { await api_1.apiClient.switchContext(currentOrgId); } else { await api_1.apiClient.switchContext(); // No orgId = personal context } } catch (error) { if (error?.response?.status === 404 || error?.response?.status === 403) { setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); console.log(chalk.default.yellow("Contexte d'organisation invalide ou inaccessible, retour au contexte personnel.")); } else { console.log(chalk.default.yellow('Warning: Could not sync context with server, using personal context')); } } } async function postCommand(title, options) { return (0, auth_1.withAuth)(async () => { await _postCommand(title, options); }); } async function _postCommand(title, options) { // Gérer le switch d'organisation si --org est spécifié if (options.org) { await handleOrganizationSwitch(options.org); } else { // Si pas de --org spécifié, proposer le choix interactif await handleInteractiveContextSelection(); } // Gestion contextuelle inspirée de la commande context if (getCurrentContext() === 'personal') { setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); await api_1.apiClient.switchContext(undefined); } else { await synchronizeContextWithServer(); } let content = options.content; let tags = []; let useAI = undefined; // Sera défini plus tard via prompt ou option let promptId = options.prompt; // Gérer l'option --no-ai explicite (Commander.js set ai = false when --no-ai is used) if (options.ai === false) { useAI = false; } // Afficher le contexte actuel avec plus de détails await displayCurrentContextStatus(); // Si le contenu n'est pas fourni, le demander if (!content) { // Demander d'abord le mode de saisie const inputModeAnswer = await inquirer_1.default.prompt([ { type: 'list', name: 'inputMode', message: 'How would you like to write your content?', choices: [ { name: 'Type directly here (max 8k characters)', value: 'inline' }, { name: 'Open in editor (vim/nano/etc.) (max 8k characters)', value: 'editor' } ], default: 'inline' } ]); if (inputModeAnswer.inputMode === 'inline') { const contentAnswer = await inquirer_1.default.prompt([ { type: 'input', name: 'content', message: `Article content (max 8k characters):`, validate: (input) => { if (!input.trim()) { return 'Content is required'; } if (input.length > MAX_CONTENT_LENGTH) { return `Content is too long (${input.length} characters). Max allowed: 8k`; } return true; } } ]); content = contentAnswer.content; } else { // Demander quel éditeur utiliser const editorAnswer = await inquirer_1.default.prompt([ { type: 'list', name: 'editor', message: 'Choose your editor:', choices: [ { name: 'nano (simple and beginner-friendly)', value: 'nano' }, { name: 'VS Code (if installed)', value: 'code --wait' }, { name: 'vim (advanced)', value: 'vim' }, { name: 'System default', value: null } ], default: 'nano' } ]); // Sauvegarder l'éditeur système original const originalEditor = process.env.EDITOR; // Définir temporairement l'éditeur choisi if (editorAnswer.editor) { process.env.EDITOR = editorAnswer.editor; } try { const contentAnswer = await inquirer_1.default.prompt([ { type: 'editor', name: 'content', message: `Article content (max 8k characters):`, validate: (input) => { if (!input.trim()) { return 'Content is required'; } if (input.length > MAX_CONTENT_LENGTH) { return `Content is too long (${input.length} characters). Max allowed: 8k`; } return true; } } ]); // Nettoyage du contenu après édition content = contentAnswer.content ?.replace(/\r/g, '') // Supprime les retours chariot Windows .replace(/^\uFEFF/, '') // Supprime BOM éventuel .replace(/\s+$/g, '') // Trim fin de fichier (lignes vides) .trim(); // Trim global if (!content) { console.log(chalk.default.red('Content is empty after editing. Please try again.')); return; } if (content.length > MAX_CONTENT_LENGTH) { console.log(chalk.default.red(`Your content is too long (${content.length} characters). Max allowed: 8k`)); return; } } finally { // Restaurer l'éditeur système original if (originalEditor) { process.env.EDITOR = originalEditor; } else { delete process.env.EDITOR; } } } } // Vérification dure même si le contenu vient d'ailleurs if (content && content.length > MAX_CONTENT_LENGTH) { console.log(chalk.default.red(`Your content is too long (${content.length} characters). Please reduce it below 8k characters.`)); return; } if (content && content.length > 6000) { console.log(chalk.default.yellow(`Warning: Your content is quite long (${content.length} characters). AI processing may fail or be truncated above 8000 characters.`)); } // Parser les tags si fournis if (options.tags) { tags = options.tags.split(',').map(tag => tag.trim()).filter(Boolean); } else { const tagsAnswer = await inquirer_1.default.prompt([ { type: 'input', name: 'tags', message: 'Tags (comma separated, optional):', default: '' } ]); if (tagsAnswer.tags) { tags = tagsAnswer.tags.split(',').map((tag) => tag.trim()).filter(Boolean); } } // Ensure tags is always a native array of strings let tagsAny = tags; if (!Array.isArray(tagsAny)) { if (typeof tagsAny === 'string' && tagsAny.length > 0) { tags = [tagsAny]; } else if (tagsAny && typeof tagsAny !== 'string' && typeof tagsAny !== 'undefined') { tags = [String(tagsAny)]; } else { tags = []; } } // Demander si on veut utiliser l'IA (sauf si --no-ai est spécifié) if (useAI === undefined) { const aiAnswer = await inquirer_1.default.prompt([ { type: 'confirm', name: 'useAI', message: 'With AI enhancement?', default: true } ]); useAI = aiAnswer.useAI; } // Gestion des prompts : choix interactif si pas spécifié if (useAI && !promptId) { try { const [promptsData, defaultPromptData] = await Promise.all([ api_1.apiClient.getPrompts(), api_1.apiClient.getDefaultPrompt() ]); const prompts = promptsData.prompts || []; const defaultPromptId = defaultPromptData.promptId; if (prompts.length > 0) { // Trouver le prompt par défaut pour l'affichage const defaultPrompt = prompts.find((p) => p.id === defaultPromptId); const defaultChoice = defaultPrompt ? defaultPrompt.id : prompts[0].id; const promptAnswer = await inquirer_1.default.prompt([ { type: 'list', name: 'promptId', message: 'Choose AI prompt style:', choices: [ ...prompts.map((prompt) => ({ name: `${prompt.name}${prompt.id === defaultPromptId ? ' (default)' : ''}`, value: prompt.id })) ], default: defaultChoice } ]); promptId = promptAnswer.promptId; } else if (defaultPromptId) { promptId = defaultPromptId; } } catch (error) { console.log(chalk.default.yellow('Could not load prompts, using default AI enhancement')); } } // Messages de progression comme dans la démo if (useAI) { console.log(chalk.default.gray('• Crafting engaging post...')); } try { const result = await api_1.apiClient.createPost({ title, content: content, tags, // always array useAI, promptId }); // Note: Tinybird push is handled server-side in the API endpoint // Messages de succès comme dans la démo avec contexte const currentContext = getCurrentContext() || 'personal'; console.log(chalk.default.green('✓ Article ready to publish!')); if (currentContext === 'organization') { console.log(chalk.default.gray('✓ Available in organization dashboard')); } else { console.log(chalk.default.gray('✓ Available in personal dashboard')); } // Ajouter l'étape de publication sociale await handleSocialPublishing(result.post.id); } catch (error) { // Vérifier si c'est une erreur de limite et la gérer spécialement const limitHandled = await (0, upgrade_handler_1.handlePotentialLimitError)(error); if (!limitHandled) { // Gestion d'erreur explicite pour contenu trop long if (error?.response?.status === 413) { console.log(chalk.default.red('Your content is too large for the server. Please reduce its size (max 8000 characters).')); } else if (error?.message?.toLowerCase().includes('context length') || error?.message?.toLowerCase().includes('too many tokens')) { console.log(chalk.default.red('Your content is too long for AI processing. Please shorten it (max 8000 characters).')); } else { // Generic error console.log(chalk.default.red('Failed to create article')); console.log(chalk.default.red(error.message)); } } } } // Nouvelle fonction pour gérer le switch d'organisation async function handleOrganizationSwitch(orgIdentifier) { try { // Support spécial pour --org personal if (orgIdentifier.toLowerCase() === 'personal') { setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); console.log(chalk.default.blue(`Switched to Personal Workspace for this post`)); return; } const spinner = (0, ora_1.default)('Loading organizations...').start(); const userOrgs = await api_1.apiClient.getUserOrganizations(); spinner.stop(); if (!userOrgs.organizations || userOrgs.organizations.length === 0) { console.log(chalk.default.red('No organizations available')); console.log(chalk.default.gray('You need to be invited to an organization first')); console.log(chalk.default.gray('Use --org personal to create in personal workspace')); process.exit(1); } // Chercher l'organisation par nom, slug ou ID const org = userOrgs.organizations.find((o) => o.name.toLowerCase() === orgIdentifier.toLowerCase() || o.slug.toLowerCase() === orgIdentifier.toLowerCase() || o.id === orgIdentifier); if (!org) { console.log(chalk.default.red(`Organization "${orgIdentifier}" not found`)); console.log(chalk.default.gray('\nAvailable organizations:')); userOrgs.organizations.forEach((o) => { console.log(chalk.default.gray(` • ${o.name} (${o.slug})`)); }); console.log(chalk.default.gray(' • personal (Personal Workspace)')); process.exit(1); } // Switch vers l'organisation setConfig('currentContext', 'organization'); setConfig('currentOrganizationId', org.id); console.log(chalk.default.cyan(`Switched to ${org.name} for this post`)); } catch (error) { console.log(chalk.default.red(`Failed to switch to organization "${orgIdentifier}"`)); console.log(chalk.default.red(error.message)); process.exit(1); } } // Nouvelle fonction pour afficher le statut du contexte async function displayCurrentContextStatus() { const currentContext = getCurrentContext() || 'personal'; const currentOrgId = getCurrentOrganizationId(); try { if (currentContext === 'organization' && currentOrgId) { const contextData = await api_1.apiClient.getCurrentContext(); const orgName = contextData?.organization?.name || 'Unknown Organization'; console.log(chalk.default.cyan(`Creating in: ${orgName} (Organization)`)); } else { console.log(chalk.default.blue('Creating in: Personal Workspace')); } } catch (error) { // Fallback if API error if (currentContext === 'organization' && currentOrgId) { console.log(chalk.default.cyan(`Creating in: Organization (${currentOrgId})`)); } else { console.log(chalk.default.blue('Creating in: Personal Workspace')); } } } // Helper spécifique pour Jira : récupération cloudId et choix projet async function getJiraPublishConfig(accessToken) { const fetch = (await Promise.resolve().then(() => require('node-fetch'))).default; // 1. Récupérer accessible resources (cloudId) const res = await fetch('https://api.atlassian.com/oauth/token/accessible-resources', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const resources = await res.json(); if (!Array.isArray(resources) || resources.length === 0) { throw new Error('No Jira Cloud instance found for this account.'); } // Si plusieurs instances, demander à l'utilisateur let cloudId = resources[0].id; if (resources.length > 1) { const inquirer = (await Promise.resolve().then(() => require('inquirer'))).default; const answer = await inquirer.prompt([ { type: 'list', name: 'cloudId', message: 'Select Jira Cloud instance:', choices: resources.map((r) => ({ name: r.name, value: r.id })) } ]); cloudId = answer.cloudId; } // 2. Lister les projets Jira const projectsRes = await fetch(`https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const projects = await projectsRes.json(); if (!Array.isArray(projects) || projects.length === 0) { throw new Error('No Jira project found for this account.'); } const inquirer = (await Promise.resolve().then(() => require('inquirer'))).default; const projectAnswer = await inquirer.prompt([ { type: 'list', name: 'projectKey', message: 'Select Jira project to publish to:', choices: projects.map((p) => ({ name: p.name, value: p.key })) } ]); return { cloudId, projectKey: projectAnswer.projectKey }; } async function handleSocialPublishing(articleId) { // Display current context for integrations const currentContext = getCurrentContext() || 'personal'; const currentOrgId = getCurrentOrganizationId(); if (currentContext === 'organization' && currentOrgId) { try { const contextData = await api_1.apiClient.getCurrentContext(); const orgName = contextData?.organization?.name || 'Organization'; console.log(chalk.default.cyan(`\nLoading integrations from: ${orgName} (Organization)`)); } catch (error) { console.log(chalk.default.cyan(`\nLoading integrations from: Organization (${currentOrgId})`)); } } else { console.log(chalk.default.blue('\nLoading integrations from: Personal Workspace')); } try { // Load connected integrations const integrationsData = await api_1.apiClient.getIntegrations(); const connectedIntegrations = integrationsData.integrations.filter((i) => i.isConnected); if (connectedIntegrations.length === 0) { // Cas normal : utilisateur n'a pas encore connecté d'intégrations const contextType = currentContext === 'organization' ? 'organization' : 'personal'; console.log(chalk.default.blue(`📱 No social platforms connected yet`)); console.log(chalk.default.gray(' Want to publish automatically to LinkedIn, Notion, and more?')); console.log(chalk.default.gray(' → Connect your accounts in the dashboard')); console.log(chalk.default.gray(' → https://logggai.run/integrations')); console.log(''); console.log(chalk.default.green('✨ Your article is ready and saved in the dashboard!')); return; } // Display summary of available integrations with context console.log(chalk.default.green(`Found ${connectedIntegrations.length} connected platform(s):`)); connectedIntegrations.forEach((integration) => { const accountInfo = integration.metadata?.displayName || integration.metadata?.name || integration.metadata?.email || 'Connected Account'; const contextBadge = integration.contextType === 'organization' ? chalk.default.yellow('[ORG]') : chalk.default.blue('[PERSONAL]'); console.log(chalk.default.gray(` ${contextBadge} ${integration.name}: ${accountInfo}`)); }); // Ask if user wants to publish const publishAnswer = await inquirer_1.default.prompt([ { type: 'confirm', name: 'wantToPublish', message: 'Publish to social platforms?', default: false } ]); if (!publishAnswer.wantToPublish) { return; } // Present connected platforms for selection with more details const platformChoices = connectedIntegrations.map((integration) => { const accountInfo = integration.metadata?.displayName || integration.metadata?.name || integration.metadata?.email || 'Connected Account'; const contextBadge = integration.contextType === 'organization' ? '[ORG]' : '[PERSONAL]'; return { name: `${integration.name} - ${accountInfo} ${contextBadge}`, value: integration.platform, checked: false // User must explicitly choose }; }); const platformsAnswer = await inquirer_1.default.prompt([ { type: 'checkbox', name: 'platforms', message: 'Select platforms to publish to:', choices: platformChoices, validate: (choices) => { if (choices.length === 0) { return 'Please select at least one platform'; } return true; } } ]); const selectedPlatforms = platformsAnswer.platforms; if (selectedPlatforms.length === 0) { return; } // Display summary before publication console.log(chalk.default.gray(`\nPublishing to ${selectedPlatforms.length} platform(s):`)); const platformsWithConfig = []; for (const platform of selectedPlatforms) { if (platform === 'jira') { // Trouver l'intégration Jira connectée const jiraIntegration = connectedIntegrations.find((i) => i.platform === 'jira'); if (!jiraIntegration) continue; try { const { cloudId, projectKey } = await getJiraPublishConfig(jiraIntegration.accessToken); platformsWithConfig.push({ platform: 'jira', cloudId, projectKey }); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); console.log(chalk.default.red('Jira publish setup failed:'), errorMsg); continue; } } else { platformsWithConfig.push(platform); } } const spinner = (0, ora_1.default)('Publishing...').start(); try { const publishResult = await api_1.apiClient.publishToSocial(articleId, platformsWithConfig); spinner.stop(); if (publishResult.success && publishResult.summary.successful > 0) { console.log(chalk.default.green(`Published to ${publishResult.summary.successful} platform(s)!`)); // Display detailed results publishResult.results.forEach((result) => { if (result.success) { console.log(chalk.default.green(` ${getPlatformName(result.platform)}${result.url ? ` - ${result.url}` : ''}`)); } else { console.log(chalk.default.red(` ${getPlatformName(result.platform)} - ${result.error}`)); } }); } else { console.log(chalk.default.red('Failed to publish to social platforms')); publishResult.results.forEach((result) => { if (!result.success) { console.log(chalk.default.red(` ${getPlatformName(result.platform)} - ${result.error}`)); } }); } } catch (error) { spinner.stop(); console.log(chalk.default.red('Failed to publish to social platforms')); console.log(chalk.default.red(` Error: ${error.message}`)); } } catch (error) { // Vraie erreur technique (API inaccessible, réseau, etc.) console.log(chalk.default.yellow('⚠️ Unable to check integrations')); console.log(chalk.default.gray(' This might be due to network issues or server maintenance')); console.log(chalk.default.gray(' → Your article is saved in the dashboard')); console.log(chalk.default.gray(' → You can publish manually: https://logggai.run/dashboard')); } } function getPlatformName(platform) { switch (platform.toLowerCase()) { case 'linkedin': return 'LinkedIn'; case 'notion': return 'Notion'; default: return platform; } } // Nouvelle fonction pour gérer le choix interactif du contexte async function handleInteractiveContextSelection() { try { // Vérifier le contexte actuel d'abord const currentContext = getCurrentContext() || 'personal'; const currentOrgId = getCurrentOrganizationId(); // Si on est déjà en personnel et pas d'organisation configurée, pas besoin de charger if (currentContext === 'personal' && !currentOrgId) { // Demander seulement si l'utilisateur veut changer de contexte const shouldCheckOrgs = await inquirer_1.default.prompt([ { type: 'confirm', name: 'checkOrganizations', message: 'Check for available organizations?', default: false } ]); if (!shouldCheckOrgs.checkOrganizations) { return; // Reste en contexte personnel } } // Charger les organisations disponibles seulement si nécessaire const spinner = (0, ora_1.default)('Loading available contexts...').start(); const userOrgs = await api_1.apiClient.getUserOrganizations(); spinner.stop(); const organizations = userOrgs.organizations || []; // Si pas d'organisations, rester en personal if (organizations.length === 0) { console.log(chalk.default.blue('No organizations available - staying in Personal Workspace')); setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); return; } // Offer choice between Personal and Organization const contextChoices = [ { name: 'Personal Workspace', value: 'personal' }, { name: 'Organization', value: 'organization' } ]; const contextAnswer = await inquirer_1.default.prompt([ { type: 'list', name: 'contextType', message: 'Where do you want to create this post?', choices: contextChoices, default: currentContext } ]); if (contextAnswer.contextType === 'personal') { // Switch to personal setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); console.log(chalk.default.blue('Creating in Personal Workspace')); } else { // Séparer les organisations payées et non payées const paidOrgs = organizations.filter((org) => !org.requiresCheckout); const unpaidOrgs = organizations.filter((org) => org.requiresCheckout); const orgChoices = []; // Ajouter les organisations payées d'abord paidOrgs.forEach((org) => { const roleColor = getRoleColor(org.role); orgChoices.push({ name: `${org.name}${roleColor(org.role)}${org.slug}`, value: org.id }); }); // Ajouter les organisations non payées avec indication unpaidOrgs.forEach((org) => { const roleColor = getRoleColor(org.role); orgChoices.push({ name: `${org.name}${roleColor(org.role)}${org.slug} ${chalk.default.yellow('(requires subscription)')}`, value: org.id, disabled: 'Upgrade required' }); }); if (paidOrgs.length === 0) { console.log(chalk.default.yellow('Available organizations require a subscription')); console.log(chalk.default.gray(' → Upgrade your organization to create posts')); console.log(chalk.default.gray(' → https://logggai.run/organization/choose-plan')); console.log(chalk.default.blue('Continuing with Personal Workspace...')); // Force personal context setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); return; } const orgAnswer = await inquirer_1.default.prompt([ { type: 'list', name: 'organizationId', message: 'Select organization:', choices: orgChoices } ]); // Switch to selected organization const selectedOrg = organizations.find((o) => o.id === orgAnswer.organizationId); setConfig('currentContext', 'organization'); setConfig('currentOrganizationId', orgAnswer.organizationId); console.log(chalk.default.cyan(`Creating in ${selectedOrg.name}`)); } } catch (error) { console.log(chalk.default.yellow('Could not load organizations, using personal context')); // Force personal context en cas d'erreur setConfig('currentContext', 'personal'); setConfig('currentOrganizationId', undefined); } } function getRoleColor(role) { switch (role.toLowerCase()) { case 'owner': return chalk.default.red; case 'admin': return chalk.default.yellow; case 'editor': return chalk.default.blue; case 'member': return chalk.default.green; case 'viewer': return chalk.default.gray; default: return chalk.default.white; } } //# sourceMappingURL=post.js.map