md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
122 lines (120 loc) • 4.98 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigManager = void 0;
exports.getWorkflowTypeOrder = getWorkflowTypeOrder;
exports.stateNameToFolderName = stateNameToFolderName;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
class ConfigManager {
static async loadConfig(projectPath = process.cwd()) {
const configPath = path_1.default.join(projectPath, this.CONFIG_FILE);
if (!fs_1.default.existsSync(configPath)) {
throw new Error(`Configuration file ${this.CONFIG_FILE} not found. Run 'md-linear-sync init' to create it.`);
}
try {
const configContent = fs_1.default.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configContent);
this.validateConfig(config);
return config;
}
catch (error) {
throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
static async saveConfig(config, projectPath = process.cwd()) {
const configPath = path_1.default.join(projectPath, this.CONFIG_FILE);
this.validateConfig(config);
try {
const configContent = JSON.stringify(config, null, 2);
fs_1.default.writeFileSync(configPath, configContent, 'utf-8');
}
catch (error) {
throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
static createDefaultConfig() {
return {
teamId: '',
teamName: '',
projectId: '',
projectName: '',
statusMapping: {
'Todo': { id: '', folder: 'todo' },
'In Progress': { id: '', folder: 'in-progress' },
'In Review': { id: '', folder: 'in-review' },
'Backlog': { id: '', folder: 'backlog' },
'Done': { id: '', folder: 'done' },
'Cancelled': { id: '', folder: 'cancelled' }
},
labelMapping: {},
timezone: 'Asia/Singapore',
lastUpdated: new Date().toISOString()
};
}
static createEnvExample(projectPath = process.cwd()) {
const envExamplePath = path_1.default.join(projectPath, this.ENV_EXAMPLE_FILE);
const envContent = `# Linear API Configuration
LINEAR_API_KEY=your_linear_api_key_here
# Optional: Slack Notifications
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/slack/webhook
# Optional: Webhook Security
WEBHOOK_SECRET=your_webhook_secret_here
`;
fs_1.default.writeFileSync(envExamplePath, envContent, 'utf-8');
}
static loadEnvironmentConfig() {
const linearApiKey = process.env.LINEAR_API_KEY;
if (!linearApiKey) {
throw new Error('LINEAR_API_KEY environment variable is required');
}
return {
linear: {
apiKey: linearApiKey,
teamId: '', // Will be populated from config file
webhookSecret: process.env.WEBHOOK_SECRET
},
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL
}
};
}
static validateConfig(config) {
if (!config.teamId) {
throw new Error('teamId is required in configuration');
}
if (!config.statusMapping) {
throw new Error('statusMapping is required in configuration');
}
if (!config.timezone) {
throw new Error('timezone is required in configuration');
}
// Validate status mapping structure
for (const [stateName, mapping] of Object.entries(config.statusMapping)) {
if (!mapping.id || !mapping.folder) {
throw new Error(`statusMapping for '${stateName}' must have both 'id' and 'folder' properties`);
}
// Type is optional for backward compatibility with existing configs
}
}
}
exports.ConfigManager = ConfigManager;
ConfigManager.CONFIG_FILE = '.linear-sync.json';
ConfigManager.ENV_EXAMPLE_FILE = '.env.example';
// Define the desired workflow order
const WORKFLOW_TYPE_ORDER = ['started', 'unstarted', 'backlog', 'completed', 'canceled'];
function getWorkflowTypeOrder(type) {
const index = WORKFLOW_TYPE_ORDER.indexOf(type);
return index === -1 ? 999 : index; // Unknown types go to the end
}
function stateNameToFolderName(stateName, type, typeOrder, stateIndex) {
const baseName = stateName
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '');
// Add numbered prefix with subcategory: major.minor-name
return `${typeOrder + 1}.${stateIndex + 1}-${baseName}`;
}
//# sourceMappingURL=index.js.map