Skip to content

Commit 3c38431

Browse files
committed
1.6.0: Introduce CLI utility commands, shell completion system, command history tracking, and comprehensive tests
1 parent b5135d0 commit 3c38431

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+5788
-2281
lines changed

CLAUDE.md

Lines changed: 97 additions & 547 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ $ lt
4242
- [Configuration Guide](docs/lt.config.md) - Configuration file documentation
4343
- [Plugin Guide](docs/plugins.md) - How to create plugins
4444

45+
## Quick Start
46+
47+
```bash
48+
# Check your environment
49+
$ lt doctor
50+
51+
# Show project status
52+
$ lt status
53+
54+
# Enable shell completions
55+
$ lt completion install
56+
```
57+
4558
## Configuration
4659

4760
The CLI supports project-specific configuration via `lt.config` files. This allows you to set default values for commands, reducing repetitive input.
@@ -91,6 +104,23 @@ $ lt git get <branch-name or part-of-branch-name>
91104
or
92105
$ lt git g <branch-name or part-of-branch-name>
93106
107+
// Preview what a command would do (dry-run)
108+
$ lt git clear --dry-run
109+
$ lt git reset --dry-run
110+
$ lt git squash --dry-run
111+
$ lt git rebase --dry-run
112+
113+
// Skip confirmation prompts (noConfirm)
114+
$ lt git get feature --noConfirm
115+
$ lt git squash dev --noConfirm
116+
117+
// Combine flags for CI/CD pipelines
118+
$ lt git clean --noConfirm
119+
$ lt server module User --noConfirm
120+
121+
// View command history
122+
$ lt history
123+
94124
...
95125
96126
```

__tests__/cli-commands.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${filesystem.path(src, 'bin', 'lt')} ${cmd}`);
7+
8+
describe('Config Commands', () => {
9+
describe('lt config show', () => {
10+
test('shows configuration or prompt to create', async () => {
11+
const output = await cli('config show');
12+
// Either shows config or prompts to create one
13+
expect(
14+
output.includes('Configuration') || output.includes('No configuration found')
15+
).toBe(true);
16+
});
17+
});
18+
19+
describe('lt config help', () => {
20+
test('shows config help', async () => {
21+
const output = await cli('config help');
22+
// Should contain configuration-related info
23+
expect(output.length).toBeGreaterThan(50);
24+
});
25+
});
26+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${filesystem.path(src, 'bin', 'lt')} ${cmd}`);
7+
8+
// Database commands require running services (MongoDB, Qdrant, Redis)
9+
// These tests only verify the commands exist and can be invoked
10+
// They will fail gracefully when services are not available
11+
12+
describe('Database Commands - Service Check', () => {
13+
describe('lt qdrant stats', () => {
14+
test('attempts to connect to Qdrant', async () => {
15+
try {
16+
const output = await cli('qdrant stats');
17+
// If Qdrant is running, we get stats
18+
expect(output).toBeDefined();
19+
} catch (e: any) {
20+
// If Qdrant is not running, we get an error message
21+
expect(e.message || e.stderr).toContain('Qdrant');
22+
}
23+
});
24+
});
25+
26+
describe('lt qdrant delete', () => {
27+
test('attempts to connect to Qdrant', async () => {
28+
try {
29+
const output = await cli('qdrant delete');
30+
expect(output).toBeDefined();
31+
} catch (e: any) {
32+
// Expected when Qdrant is not running
33+
expect(e.message || e.stderr).toContain('Qdrant');
34+
}
35+
});
36+
});
37+
});

__tests__/git-commands.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${filesystem.path(src, 'bin', 'lt')} ${cmd}`);
7+
8+
describe('Git Commands', () => {
9+
describe('lt git update', () => {
10+
test('updates current branch', async () => {
11+
const output = await cli('git update');
12+
// Should run git fetch/pull
13+
expect(output).toBeDefined();
14+
});
15+
});
16+
17+
describe('lt git update --dry-run', () => {
18+
test('shows dry-run message', async () => {
19+
const output = await cli('git update --dry-run');
20+
expect(output).toContain('DRY-RUN MODE');
21+
expect(output).toContain('Current branch:');
22+
});
23+
});
24+
25+
describe('lt git create --dry-run', () => {
26+
test('shows dry-run message', async () => {
27+
const output = await cli('git create test-branch-dry-run --base main --dry-run');
28+
expect(output).toContain('DRY-RUN MODE');
29+
expect(output).toContain('Would create branch');
30+
});
31+
});
32+
33+
describe('lt git force-pull --dry-run', () => {
34+
test('shows dry-run message', async () => {
35+
const output = await cli('git force-pull --dry-run');
36+
expect(output).toContain('DRY-RUN MODE');
37+
});
38+
});
39+
40+
describe('lt git reset --dry-run', () => {
41+
test('shows dry-run message', async () => {
42+
const output = await cli('git reset --dry-run');
43+
expect(output).toContain('DRY-RUN MODE');
44+
});
45+
});
46+
47+
describe('lt git undo --dry-run', () => {
48+
test('shows dry-run message', async () => {
49+
const output = await cli('git undo --dry-run');
50+
expect(output).toContain('DRY-RUN MODE');
51+
});
52+
});
53+
54+
describe('lt git rename --dry-run', () => {
55+
test('shows dry-run message or protected branch error', async () => {
56+
const output = await cli('git rename newname --dry-run');
57+
// On protected branches (main/dev/release), renaming is not allowed
58+
// On other branches, it shows DRY-RUN MODE
59+
expect(
60+
output.includes('DRY-RUN MODE') || output.includes('not allowed')
61+
).toBe(true);
62+
});
63+
});
64+
65+
describe('lt git squash --dry-run', () => {
66+
test('shows dry-run message or protected branch error', async () => {
67+
const output = await cli('git squash --dry-run');
68+
// On protected branches (main/dev/release), squashing is not allowed
69+
// On other branches, it shows DRY-RUN MODE
70+
expect(
71+
output.includes('DRY-RUN MODE') || output.includes('not allowed')
72+
).toBe(true);
73+
});
74+
});
75+
76+
describe('lt git clear --dry-run', () => {
77+
test('shows dry-run message', async () => {
78+
const output = await cli('git clear --dry-run');
79+
expect(output).toContain('DRY-RUN MODE');
80+
});
81+
});
82+
83+
describe('lt git clean --dry-run', () => {
84+
test('shows dry-run message', async () => {
85+
const output = await cli('git clean --dry-run');
86+
expect(output).toContain('DRY-RUN MODE');
87+
});
88+
});
89+
});

__tests__/other-commands.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${filesystem.path(src, 'bin', 'lt')} ${cmd}`);
7+
8+
describe('Tools Commands', () => {
9+
describe('lt tools crypt', () => {
10+
test('generates password hash', async () => {
11+
const output = await cli('tools crypt testpassword');
12+
// Should contain a bcrypt hash starting with $2
13+
expect(output).toContain('$2');
14+
});
15+
});
16+
17+
describe('lt tools sha256', () => {
18+
test('generates sha256 hash', async () => {
19+
const output = await cli('tools sha256 test');
20+
// SHA256 hash is 64 hex characters
21+
expect(output.trim().length).toBeGreaterThanOrEqual(64);
22+
});
23+
});
24+
25+
describe('lt tools jwt-read', () => {
26+
test('parses JWT payload', async () => {
27+
// Sample JWT with payload: { "sub": "1234567890", "name": "Test User", "iat": 1516239022 }
28+
const sampleJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
29+
const output = await cli(`tools jwt-read ${sampleJwt}`);
30+
// Should contain parsed JWT data
31+
expect(output).toContain('Test User');
32+
expect(output).toContain('1234567890');
33+
});
34+
});
35+
});

__tests__/server-commands.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${filesystem.path(src, 'bin', 'lt')} ${cmd}`);
7+
8+
describe('Server Commands', () => {
9+
describe('lt server createSecret', () => {
10+
test('generates a secret', async () => {
11+
const output = await cli('server createSecret');
12+
// Should output a base64 string (at least 20 chars)
13+
expect(output.trim().length).toBeGreaterThan(20);
14+
});
15+
16+
test('generates secret with custom length', async () => {
17+
const output = await cli('server createSecret --length 64');
18+
// Should output a longer base64 string
19+
expect(output.trim().length).toBeGreaterThan(80);
20+
});
21+
});
22+
});

__tests__/utility-commands.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const { filesystem, system } = require('gluegun');
2+
3+
const src = filesystem.path(__dirname, '..');
4+
5+
const cli = async (cmd: string) =>
6+
system.run(`node ${ filesystem.path(src, 'bin', 'lt') } ${cmd}`);
7+
8+
describe('Utility Commands', () => {
9+
describe('lt status', () => {
10+
test('outputs project status', async () => {
11+
const output = await cli('status');
12+
expect(output).toContain('Project Status');
13+
expect(output).toContain('Directory:');
14+
});
15+
});
16+
17+
describe('lt doctor', () => {
18+
test('runs doctor checks', async () => {
19+
const output = await cli('doctor');
20+
expect(output).toContain('lt doctor');
21+
// Check for doctor output (may contain ANSI codes)
22+
expect(output).toContain('Checks:');
23+
});
24+
});
25+
26+
describe('lt completion', () => {
27+
test('outputs help without arguments', async () => {
28+
const output = await cli('completion');
29+
expect(output).toContain('Usage: lt completion');
30+
expect(output).toContain('bash');
31+
expect(output).toContain('zsh');
32+
expect(output).toContain('fish');
33+
});
34+
35+
test('generates bash completion', async () => {
36+
const output = await cli('completion bash');
37+
expect(output).toContain('_lt_completions');
38+
expect(output).toContain('complete -F _lt_completions lt');
39+
});
40+
41+
test('generates zsh completion', async () => {
42+
const output = await cli('completion zsh');
43+
expect(output).toContain('#compdef lt');
44+
expect(output).toContain('_lt()');
45+
});
46+
47+
test('generates fish completion', async () => {
48+
const output = await cli('completion fish');
49+
expect(output).toContain('# lt completion for Fish shell');
50+
expect(output).toContain('complete -f -c lt');
51+
});
52+
});
53+
54+
describe('lt history', () => {
55+
test('outputs history or empty message', async () => {
56+
const output = await cli('history');
57+
// Either shows history or "No command history yet"
58+
expect(output.includes('Command History') || output.includes('No command history')).toBe(true);
59+
});
60+
});
61+
62+
describe('lt templates list', () => {
63+
test('lists available templates', async () => {
64+
const output = await cli('templates list');
65+
expect(output).toContain('Available Templates');
66+
expect(output).toContain('Built-in Templates');
67+
});
68+
});
69+
70+
describe('lt config validate', () => {
71+
test('reports when no config file found', async () => {
72+
// Run in a temp directory without config
73+
const tempDir = filesystem.path(filesystem.homedir(), `.lt-test-${ Date.now()}`);
74+
filesystem.dir(tempDir);
75+
76+
try {
77+
const output = await system.run(
78+
`cd ${tempDir} && node ${filesystem.path(src, 'bin', 'lt')} config validate`
79+
);
80+
expect(output).toContain('No lt.config file found');
81+
} finally {
82+
filesystem.remove(tempDir);
83+
}
84+
});
85+
});
86+
});
87+
88+
describe('Dry-run flags', () => {
89+
describe('lt git clear --dry-run', () => {
90+
test('shows dry-run message', async () => {
91+
const output = await cli('git clear --dry-run');
92+
expect(output).toContain('DRY-RUN MODE');
93+
});
94+
});
95+
96+
describe('lt git clean --dry-run', () => {
97+
test('shows dry-run message', async () => {
98+
const output = await cli('git clean --dry-run');
99+
expect(output).toContain('DRY-RUN MODE');
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)