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 +19 -0
- package/package.json +28 -0
- package/src/checkout.js +93 -0
- package/src/config.js +35 -0
- package/src/index.js +7 -0
- package/src/install.js +103 -0
- package/src/setup.js +27 -0
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
|
+
}
|
package/src/checkout.js
ADDED
|
@@ -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
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 };
|