get-lem-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { program } = require('commander');
5
+ const { setup } = require('../src/setup');
6
+ const { install, uninstall } = require('../src/install');
7
+ const { checkout } = require('../src/checkout');
8
+
9
+ program
10
+ .name('lem-ai')
11
+ .description('lem-ai — generate Implementation.md on branch creation')
12
+ .version('1.0.0');
13
+
14
+ program.command('setup').description('Configure webhook URL').action(setup);
15
+ program.command('install').description('Install git hook into current repo').action(install);
16
+ program.command('uninstall').description('Remove git hook from current repo').action(uninstall);
17
+ program.command('checkout <branchName>').description('Internal: called by git hook').action(checkout);
18
+
19
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "get-lem-ai",
3
+ "version": "1.0.0",
4
+ "description": "lem-ai — Automate Implementation.md generation from Jira tickets on branch creation",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "get-lem-ai": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "keywords": [
14
+ "git",
15
+ "hook",
16
+ "jira",
17
+ "lem-ai",
18
+ "automation"
19
+ ],
20
+ "author": "lem-ai",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "axios": "^1.15.1",
24
+ "chalk": "^4.1.2",
25
+ "commander": "^14.0.3",
26
+ "ora": "^5.4.1"
27
+ }
28
+ }
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const axios = require('axios');
5
+ const chalk = require('chalk');
6
+ const { spawn } = require('child_process');
7
+ const { loadConfig, findGitRoot, WEBHOOK_URL } = require('./config');
8
+
9
+ function extractTicketId(branchName) {
10
+ const match = branchName.match(/([A-Z][A-Z0-9]+-\d+)/i);
11
+ return match ? match[1].toUpperCase() : null;
12
+ }
13
+
14
+ async function checkout(branchName) {
15
+ const config = loadConfig();
16
+
17
+ if (!config) {
18
+ console.log(chalk.yellow('\n⚠️ Not configured. Run: lem-ai setup\n'));
19
+ return;
20
+ }
21
+
22
+ // ────────────────────────────────────────────────────────────
23
+ // PHASE 1: Main Process — Spawn Background Worker & Exit
24
+ // ────────────────────────────────────────────────────────────
25
+ if (!process.env.LEMAI_BG) {
26
+ console.log(chalk.cyan(`\n[lem-ai] 🚀 Branch detected: ${chalk.bold(branchName)}`));
27
+ console.log(chalk.gray(`[lem-ai] ⚡ Generating Implementation.md in background...`));
28
+ console.log(chalk.gray(`[lem-ai] 🏁 Git checkout will proceed instantly.\n`));
29
+
30
+ const gitRoot = findGitRoot(process.cwd()) || process.cwd();
31
+ const logPath = path.join(gitRoot, '.lem-ai.log');
32
+
33
+ // Open log file for background process
34
+ const out = fs.openSync(logPath, 'a');
35
+ const err = fs.openSync(logPath, 'a');
36
+
37
+ // Spawn this same CLI command but with LEMAI_BG=true
38
+ const child = spawn(process.argv[0], [process.argv[1], 'checkout', branchName], {
39
+ detached: true,
40
+ stdio: ['ignore', out, err],
41
+ env: { ...process.env, LEMAI_BG: 'true' },
42
+ cwd: process.cwd()
43
+ });
44
+
45
+ child.unref();
46
+ process.exit(0);
47
+ }
48
+
49
+ // ────────────────────────────────────────────────────────────
50
+ // PHASE 2: Background Process — Do the heavy lifting
51
+ // ────────────────────────────────────────────────────────────
52
+ const ticketId = extractTicketId(branchName);
53
+ const startTime = new Date().toISOString();
54
+
55
+ const gitRoot = findGitRoot(process.cwd()) || process.cwd();
56
+ const logPath = path.join(gitRoot, '.lem-ai.log');
57
+
58
+ const log = (msg) => {
59
+ fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${msg}\n`);
60
+ };
61
+
62
+ log(`Starting background sync for ${branchName}...`);
63
+
64
+ const headers = { 'Content-Type': 'application/json' };
65
+ if (config.secret) headers['Authorization'] = `Bearer ${config.secret}`;
66
+ if (config.apiKey) headers['x-sdk-key'] = config.apiKey;
67
+
68
+ // Fire and Forget: Start the request without blocking the main flow
69
+ axios.post(
70
+ WEBHOOK_URL,
71
+ { branchName, ticketId, timestamp: startTime },
72
+ { headers, timeout: 300000 }
73
+ ).then(response => {
74
+ const { markdown } = response.data;
75
+
76
+ if (markdown) {
77
+ const outputPath = path.join(gitRoot, config.outputFile || 'Implementation.md');
78
+ fs.writeFileSync(outputPath, markdown, 'utf-8');
79
+ log(`✅ Implementation.md generated successfully.`);
80
+ } else {
81
+ log(`ℹ️ No markdown generated by backend.`);
82
+ }
83
+ process.exit(0);
84
+ }).catch(err => {
85
+ log(`❌ Error: ${err.message}`);
86
+ if (err.response) {
87
+ log(`Backend error ${err.response.status}: ${JSON.stringify(err.response.data)}`);
88
+ }
89
+ process.exit(1);
90
+ });
91
+ }
92
+
93
+ module.exports = { checkout };
package/src/config.js ADDED
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function findGitRoot(dir) {
6
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
7
+ const parent = path.dirname(dir);
8
+ if (parent === dir) return null;
9
+ return findGitRoot(parent);
10
+ }
11
+
12
+ function getConfigPath() {
13
+ const repoRoot = findGitRoot(process.cwd());
14
+ if (!repoRoot) return null;
15
+ return path.join(repoRoot, '.lem-ai.json');
16
+ }
17
+
18
+ function loadConfig() {
19
+ const configPath = getConfigPath();
20
+ if (!configPath || !fs.existsSync(configPath)) return null;
21
+ try { return JSON.parse(fs.readFileSync(configPath, 'utf-8')); }
22
+ catch { return null; }
23
+ }
24
+
25
+ function saveConfig(data) {
26
+ const configPath = getConfigPath();
27
+ if (!configPath) {
28
+ throw new Error('Not inside a git repository. Cannot save project-level config.');
29
+ }
30
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
31
+ }
32
+
33
+ const WEBHOOK_URL = 'https://api.getlem.ai/api/v1/webhooks/lem';
34
+
35
+ module.exports = { loadConfig, saveConfig, getConfigPath, findGitRoot, WEBHOOK_URL };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ module.exports = {
3
+ ...require('./checkout'),
4
+ ...require('./config'),
5
+ ...require('./install'),
6
+ ...require('./setup'),
7
+ };
package/src/install.js ADDED
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const { findGitRoot, getConfigPath } = require('./config');
6
+
7
+ const HOOK_SCRIPT = `#!/bin/sh
8
+ # Injected by lem-ai
9
+ # This hook captures all branch creation events (git branch, git checkout -b, git switch -c)
10
+ if [ "$1" = "committed" ]; then
11
+ while read -r old_rev new_rev ref_name; do
12
+ # Check if it's a brand new local branch (old_rev is all zeros)
13
+ if [ "$old_rev" = "0000000000000000000000000000000000000000" ] && [ "\${ref_name#refs/heads/}" != "$ref_name" ] && [ "$new_rev" != "0000000000000000000000000000000000000000" ]; then
14
+ BRANCH_NAME=\${ref_name#refs/heads/}
15
+ # Run synchronously since we fixed the Node hang issue
16
+ lem-ai checkout "$BRANCH_NAME"
17
+ fi
18
+ done
19
+ fi
20
+ `;
21
+
22
+ async function install() {
23
+ const { loadConfig } = require('./config');
24
+ const { setup } = require('./setup');
25
+ const config = loadConfig();
26
+
27
+ // If config is missing or the new apiKey field is missing, force setup
28
+ if (!config || !config.apiKey) {
29
+ console.log(chalk.yellow('⚠️ Configuration missing or outdated. Starting setup...'));
30
+ await setup();
31
+ }
32
+
33
+ const repoRoot = findGitRoot(process.cwd());
34
+ if (!repoRoot) {
35
+ console.error(chalk.red('❌ Not inside a git repository.'));
36
+ process.exit(1);
37
+ }
38
+
39
+ const hooksDir = path.join(repoRoot, '.git', 'hooks');
40
+ const hookPath = path.join(hooksDir, 'reference-transaction');
41
+
42
+ // Clean up ALL legacy hooks
43
+ const oldHooks = ['post-checkout', 'reference-transaction'];
44
+ for (const h of oldHooks) {
45
+ const p = path.join(hooksDir, h);
46
+ if (fs.existsSync(p)) {
47
+ const content = fs.readFileSync(p, 'utf-8');
48
+ if (content.includes('git-jira-hook') || content.includes('getlem') || content.includes('lem')) {
49
+ if (h === 'reference-transaction' && content.includes('Injected by lem-ai')) {
50
+ // This is current, skip
51
+ continue;
52
+ }
53
+ fs.unlinkSync(p);
54
+ console.log(chalk.gray(`ℹ️ Cleaned up legacy hook: ${h}`));
55
+ }
56
+ }
57
+ }
58
+
59
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
60
+
61
+ if (fs.existsSync(hookPath)) {
62
+ fs.copyFileSync(hookPath, hookPath + '.backup');
63
+ console.log(chalk.yellow('⚠️ Existing hook backed up'));
64
+ }
65
+
66
+ fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
67
+ console.log(chalk.green(`\n✅ Ref-Transaction Hook installed/updated → ${hookPath}`));
68
+ console.log(chalk.cyan('🎉 This will now trigger on ANY branch creation method:'));
69
+ console.log(chalk.gray(' - git branch <name>'));
70
+ console.log(chalk.gray(' - git checkout -b <name>'));
71
+ console.log(chalk.gray(' - git switch -c <name>\n'));
72
+ }
73
+
74
+ function uninstall() {
75
+ const repoRoot = findGitRoot(process.cwd());
76
+ if (!repoRoot) {
77
+ console.error(chalk.red('❌ Not inside a git repository.'));
78
+ process.exit(1);
79
+ }
80
+
81
+ const hookPath = path.join(repoRoot, '.git', 'hooks', 'reference-transaction');
82
+
83
+ if (fs.existsSync(hookPath)) {
84
+ const content = fs.readFileSync(hookPath, 'utf-8');
85
+ if (content.includes('lem-ai') || content.includes('getlem') || content.includes('lem') || content.includes('git-jira-hook')) {
86
+ fs.unlinkSync(hookPath);
87
+ console.log(chalk.green('\n✅ lem-ai Git Hook uninstalled successfully.\n'));
88
+ } else {
89
+ console.log(chalk.yellow('\n⚠️ Hook at this path was not created by lem-ai. Skipping.\n'));
90
+ }
91
+ } else {
92
+ console.log(chalk.gray('\nℹ️ No lem-ai hook found to uninstall.\n'));
93
+ }
94
+
95
+ // Also remove the project-level config file if it exists
96
+ const configPath = getConfigPath();
97
+ if (configPath && fs.existsSync(configPath)) {
98
+ fs.unlinkSync(configPath);
99
+ console.log(chalk.gray('ℹ️ Project configuration removed.'));
100
+ }
101
+ }
102
+
103
+ module.exports = { install, uninstall };
package/src/setup.js ADDED
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ const readline = require('readline');
3
+ const chalk = require('chalk');
4
+ const { saveConfig } = require('./config');
5
+
6
+ function ask(rl, question) {
7
+ return new Promise(resolve => rl.question(question, resolve));
8
+ }
9
+
10
+ async function setup() {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ console.log(chalk.cyan('\n🔧 lem-ai Setup\n'));
13
+
14
+ const outputFile = await ask(rl, 'Output filename [Implementation.md]: ') || 'Implementation.md';
15
+ const apiKey = await ask(rl, 'SDK API Key (from Lem settings): ');
16
+
17
+ rl.close();
18
+ saveConfig({
19
+ outputFile: outputFile.trim() || 'Implementation.md',
20
+ apiKey: apiKey.trim()
21
+ });
22
+
23
+ console.log(chalk.green('\n✅ Config saved to .lem-ai.json'));
24
+ console.log(chalk.yellow('👉 Now cd into a git repo and run: lem-ai install\n'));
25
+ }
26
+
27
+ module.exports = { setup };