md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
234 lines โข 9.26 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupSlackCommand = setupSlackCommand;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const clipboardy_1 = __importDefault(require("clipboardy"));
const execAsync = (0, util_1.promisify)(child_process_1.exec);
async function setupSlackCommand() {
console.log('๐ง Setting up Slack notifications\n');
await setupSlackApp();
}
async function setupSlackApp() {
console.log('This is the md-linear-sync Slack notification system setup wizard\n');
console.log('Step 1: We need to create a Slack app to process the notifications\n');
console.log('After pressing Enter:');
console.log('โข Manifest will be copied to clipboard');
console.log('โข Slack page will open');
console.log('โข Select "From an app manifest"');
console.log('โข Choose your workspace');
console.log('โข Paste the manifest and create the app');
console.log('โข Install the app to your workspace\n');
console.log('Press Enter to begin...');
await getInput('');
// Read the manifest file
const manifestPath = path_1.default.join(__dirname, '../../slack-app-manifest.json');
const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
// Copy manifest to clipboard
try {
await clipboardy_1.default.write(JSON.stringify(manifest, null, 2));
console.log('๐ Manifest copied to clipboard!');
}
catch (error) {
console.log('โ ๏ธ Could not copy to clipboard, here\'s the manifest:\n');
console.log(JSON.stringify(manifest, null, 2));
console.log('\n๐ Copy the above JSON');
}
// Open Slack apps page (non-blocking)
openURL('https://api.slack.com/apps?new_app=1').catch(() => { });
console.log('๐ Opening Slack app creation page...');
console.log('๐ Go here if page didn\'t open: https://api.slack.com/apps?new_app=1');
console.log('\nInstructions:');
console.log('1. Select "From an app manifest"');
console.log('2. Choose your workspace');
console.log('3. Paste the manifest (Cmd+V / Ctrl+V)');
console.log('4. Create the app');
console.log('5. Install the app to your workspace');
console.log('\nPress Enter once Slack app is created...');
await getInput('');
console.log('๐ Now we need the Bot User OAuth Token');
console.log(' Go to "OAuth & Permissions" โ Install to Workspace โ Copy the token starting with "xoxb-"');
const botToken = await getInput('Bot User OAuth Token: ');
if (!botToken.startsWith('xoxb-')) {
console.log('โ That doesn\'t look like a valid bot token');
console.log(' It should start with: xoxb-');
return;
}
// Update .env file
await updateEnvFile('SLACK_BOT_TOKEN', botToken);
// Auto-create notification channel
console.log('\n๐บ Creating notification channel...');
const channelName = 'md-linear-sync-notifications';
try {
const channelId = await createSlackChannel(botToken, channelName);
console.log(`โ
Created #${channelName} channel`);
// Invite bot to the channel
if (channelId) {
await inviteBotToChannel(botToken, channelId);
console.log(`๐ค Bot invited to #${channelName}`);
}
// Get team info for URL
const teamInfo = await getTeamInfo(botToken);
if (teamInfo && channelId) {
const channelUrl = `https://${teamInfo.domain}.slack.com/channels/${channelId}`;
console.log(`๐ Join the channel: ${channelUrl}`);
}
}
catch (error) {
console.log(`โ ๏ธ Could not create channel: ${error instanceof Error ? error.message : 'Unknown error'}`);
console.log(' You can manually invite the bot to any channel you want to use');
}
// Test the bot
await testSlackBot(botToken, channelName);
console.log('\n๐ Slack setup complete!');
console.log(`๐ก Notifications will be sent to #${channelName}`);
// Ensure process exits cleanly
process.exit(0);
}
async function updateEnvFile(key, value) {
const envPath = path_1.default.join(process.cwd(), '.env');
let envContent = '';
// Read existing .env file if it exists
if (fs_1.default.existsSync(envPath)) {
envContent = fs_1.default.readFileSync(envPath, 'utf-8');
}
// Check if key already exists
const lines = envContent.split('\n');
const keyIndex = lines.findIndex(line => line.startsWith(`${key}=`));
if (keyIndex >= 0) {
// Update existing key
lines[keyIndex] = `${key}=${value}`;
}
else {
// Add new key
lines.push(`${key}=${value}`);
}
// Write back to file
fs_1.default.writeFileSync(envPath, lines.filter(line => line.trim()).join('\n') + '\n');
console.log(`โ
Updated .env file with ${key}`);
}
async function getTeamInfo(botToken) {
try {
const response = await fetch('https://slack.com/api/team.info', {
headers: { 'Authorization': `Bearer ${botToken}` }
});
const result = await response.json();
return result.ok ? { domain: result.team.domain } : null;
}
catch (error) {
return null;
}
}
async function inviteBotToChannel(botToken, channelId) {
const response = await fetch('https://slack.com/api/conversations.join', {
method: 'POST',
headers: {
'Authorization': `Bearer ${botToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel: channelId
})
});
const result = await response.json();
if (!result.ok && result.error !== 'already_in_channel') {
throw new Error(result.error);
}
}
async function createSlackChannel(botToken, channelName) {
const response = await fetch('https://slack.com/api/conversations.create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${botToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: channelName,
is_private: false
})
});
const result = await response.json();
if (!result.ok) {
if (result.error === 'name_taken') {
console.log(`โ
Channel #${channelName} already exists`);
// Get existing channel info
const infoResponse = await fetch(`https://slack.com/api/conversations.list?types=public_channel&limit=200`, {
headers: { 'Authorization': `Bearer ${botToken}` }
});
const infoResult = await infoResponse.json();
const existingChannel = infoResult.channels?.find((ch) => ch.name === channelName);
return existingChannel?.id || '';
}
throw new Error(result.error);
}
return result.channel.id;
}
async function testSlackBot(botToken, channel) {
console.log('\n๐งช Testing bot token...');
try {
const response = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${botToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel: channel.replace('#', ''),
text: '๐ Linear Markdown Sync bot is now connected!'
})
});
const result = await response.json();
if (result.ok) {
console.log('โ
Test message sent successfully!');
console.log('๐ฑ Check your Slack channel for the test message');
}
else {
console.log('โ Test failed:', result.error);
if (result.error === 'channel_not_found') {
console.log('๐ก Make sure the bot is invited to the channel');
}
}
}
catch (error) {
console.log('โ Test failed:', error instanceof Error ? error.message : 'Unknown error');
}
}
// Utility functions
async function getUserChoice(maxChoice) {
while (true) {
const choice = await getInput(`Enter your choice (1-${maxChoice}): `);
const num = parseInt(choice.trim());
if (num >= 1 && num <= maxChoice) {
return num;
}
console.log(`Please enter a number between 1 and ${maxChoice}`);
}
}
async function getInput(prompt) {
process.stdout.write(prompt);
return new Promise((resolve) => {
process.stdin.once('data', (data) => {
resolve(data.toString().trim());
});
});
}
async function openURL(url) {
const platform = process.platform;
let command = '';
if (platform === 'darwin') {
command = `open "${url}"`;
}
else if (platform === 'win32') {
command = `start "${url}"`;
}
else {
command = `xdg-open "${url}"`;
}
await execAsync(command);
}
//# sourceMappingURL=slack-setup.js.map