logggai
Version:
AI-powered CLI for transforming your development work into professional content
692 lines • 30.8 kB
JavaScript
;
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