create-lore 0.1.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/create-lore.js +98 -0
- package/package.json +16 -0
- package/test/create-lore.test.js +92 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
const TOOLS = ['Claude Code', 'Cursor', 'OpenCode'];
|
|
9
|
+
const REPO_URL = 'https://github.com/lorehq/lore.git';
|
|
10
|
+
|
|
11
|
+
async function prompt(question) {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer); }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function selectTools() {
|
|
17
|
+
console.log('\nWhich AI coding tools will you use?\n');
|
|
18
|
+
TOOLS.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
19
|
+
console.log();
|
|
20
|
+
const answer = await prompt('Select tools (comma-separated numbers, e.g. 1,3): ');
|
|
21
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
22
|
+
const selected = indices.filter(i => i >= 0 && i < TOOLS.length).map(i => TOOLS[i]);
|
|
23
|
+
return selected.length > 0 ? selected : ['Claude Code'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const name = args.find(a => !a.startsWith('-'));
|
|
29
|
+
const templateDir = args.find((a, i) => args[i - 1] === '--template') || process.env.LORE_TEMPLATE;
|
|
30
|
+
|
|
31
|
+
if (!name) {
|
|
32
|
+
console.error('Usage: create-lore <name> [--template <path>]');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isPath = name.includes('/') || name.includes(path.sep);
|
|
37
|
+
const targetDir = path.resolve(isPath ? name : `./lore-${name}`);
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(targetDir)) {
|
|
40
|
+
console.error(`Error: ${targetDir} already exists`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Clone or copy template
|
|
45
|
+
const tmpDir = path.join(require('os').tmpdir(), `create-lore-${Date.now()}`);
|
|
46
|
+
try {
|
|
47
|
+
if (templateDir) {
|
|
48
|
+
console.log(`Copying template from ${templateDir}...`);
|
|
49
|
+
copyDir(templateDir, tmpDir);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`Cloning ${REPO_URL}...`);
|
|
52
|
+
execSync(`git clone --depth 1 ${REPO_URL} "${tmpDir}"`, { stdio: 'pipe' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Remove .git from clone
|
|
56
|
+
const gitDir = path.join(tmpDir, '.git');
|
|
57
|
+
if (fs.existsSync(gitDir)) fs.rmSync(gitDir, { recursive: true });
|
|
58
|
+
|
|
59
|
+
// Copy to target
|
|
60
|
+
copyDir(tmpDir, targetDir);
|
|
61
|
+
} finally {
|
|
62
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Select tools (skip in non-interactive mode)
|
|
66
|
+
let tools = ['Claude Code'];
|
|
67
|
+
if (process.stdin.isTTY) {
|
|
68
|
+
tools = await selectTools();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Write .lore-config
|
|
72
|
+
const config = { name, tools, created: new Date().toISOString().split('T')[0] };
|
|
73
|
+
fs.writeFileSync(path.join(targetDir, '.lore-config'), JSON.stringify(config, null, 2) + '\n');
|
|
74
|
+
|
|
75
|
+
// Init git repo
|
|
76
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
77
|
+
|
|
78
|
+
console.log(`\nCreated lore-${name} at ${targetDir}`);
|
|
79
|
+
console.log(`Tools: ${tools.join(', ')}`);
|
|
80
|
+
console.log(`\nNext steps:`);
|
|
81
|
+
console.log(` cd ${targetDir}`);
|
|
82
|
+
console.log(` git add -A && git commit -m "Init Lore"`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function copyDir(src, dest) {
|
|
86
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
87
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
88
|
+
const srcPath = path.join(src, entry.name);
|
|
89
|
+
const destPath = path.join(dest, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
copyDir(srcPath, destPath);
|
|
92
|
+
} else {
|
|
93
|
+
fs.copyFileSync(srcPath, destPath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch(err => { console.error(err.message); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-lore",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Lore knowledge-persistent agent repo",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-lore": "bin/create-lore.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test test/create-lore.test.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["lore", "agent", "ai", "knowledge"],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { describe, it, before, after } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const BIN = path.resolve(__dirname, '../bin/create-lore.js');
|
|
8
|
+
const TEMPLATE = path.resolve(__dirname, '../../lore');
|
|
9
|
+
const OUTPUT = path.resolve(__dirname, '../test-output');
|
|
10
|
+
|
|
11
|
+
function run(args = '') {
|
|
12
|
+
return execSync(`node ${BIN} ${args}`, {
|
|
13
|
+
env: { ...process.env, LORE_TEMPLATE: TEMPLATE },
|
|
14
|
+
stdio: 'pipe',
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cleanup() {
|
|
20
|
+
if (fs.existsSync(OUTPUT)) fs.rmSync(OUTPUT, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('create-lore', () => {
|
|
24
|
+
before(cleanup);
|
|
25
|
+
after(cleanup);
|
|
26
|
+
|
|
27
|
+
it('exits with error when no name given', () => {
|
|
28
|
+
assert.throws(() => run(''), { status: 1 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('creates project directory with expected structure', () => {
|
|
32
|
+
const output = run(`${OUTPUT} --template ${TEMPLATE}`);
|
|
33
|
+
|
|
34
|
+
assert.ok(fs.existsSync(OUTPUT), 'output directory exists');
|
|
35
|
+
|
|
36
|
+
// .lore-config written with correct fields
|
|
37
|
+
const config = JSON.parse(fs.readFileSync(path.join(OUTPUT, '.lore-config'), 'utf8'));
|
|
38
|
+
assert.equal(config.name, OUTPUT);
|
|
39
|
+
assert.ok(Array.isArray(config.tools), 'tools is an array');
|
|
40
|
+
assert.ok(config.created, 'created date present');
|
|
41
|
+
|
|
42
|
+
// git repo initialized
|
|
43
|
+
assert.ok(fs.existsSync(path.join(OUTPUT, '.git')), 'git initialized');
|
|
44
|
+
|
|
45
|
+
// no template .git leaked
|
|
46
|
+
const entries = fs.readdirSync(path.join(OUTPUT, '.git'));
|
|
47
|
+
assert.ok(entries.includes('HEAD'), '.git looks like a fresh init');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('fails if target directory already exists', () => {
|
|
51
|
+
fs.mkdirSync(OUTPUT, { recursive: true });
|
|
52
|
+
assert.throws(() => run(`${OUTPUT} --template ${TEMPLATE}`), /already exists/);
|
|
53
|
+
fs.rmSync(OUTPUT, { recursive: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// These tests activate as lore repo gets populated (Phase 2+)
|
|
57
|
+
|
|
58
|
+
it('has AGENTS.md when template provides it', () => {
|
|
59
|
+
const agentsMd = path.join(TEMPLATE, 'AGENTS.md');
|
|
60
|
+
if (!fs.existsSync(agentsMd)) return; // skip until Phase 2
|
|
61
|
+
|
|
62
|
+
run(`${OUTPUT} --template ${TEMPLATE}`);
|
|
63
|
+
assert.ok(fs.existsSync(path.join(OUTPUT, 'AGENTS.md')), 'AGENTS.md copied');
|
|
64
|
+
const content = fs.readFileSync(path.join(OUTPUT, 'AGENTS.md'), 'utf8');
|
|
65
|
+
assert.ok(content.length > 0, 'AGENTS.md is non-empty');
|
|
66
|
+
cleanup();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('has hooks wired in .claude/settings.json when template provides it', () => {
|
|
70
|
+
const settings = path.join(TEMPLATE, '.claude/settings.json');
|
|
71
|
+
if (!fs.existsSync(settings)) return; // skip until Phase 3
|
|
72
|
+
|
|
73
|
+
run(`${OUTPUT} --template ${TEMPLATE}`);
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(OUTPUT, '.claude/settings.json'), 'utf8'));
|
|
75
|
+
assert.ok(parsed.hooks, 'hooks key exists in settings');
|
|
76
|
+
cleanup();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('passes validate-consistency.sh when template provides it', () => {
|
|
80
|
+
const script = path.join(TEMPLATE, 'scripts/validate-consistency.sh');
|
|
81
|
+
if (!fs.existsSync(script)) return; // skip until Phase 5
|
|
82
|
+
|
|
83
|
+
run(`${OUTPUT} --template ${TEMPLATE}`);
|
|
84
|
+
const result = execSync(`bash scripts/validate-consistency.sh`, {
|
|
85
|
+
cwd: OUTPUT,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: 'pipe',
|
|
88
|
+
});
|
|
89
|
+
assert.ok(result.includes('PASSED'), 'validation passes');
|
|
90
|
+
cleanup();
|
|
91
|
+
});
|
|
92
|
+
});
|