Skip to content

Commit aba38cd

Browse files
committed
feat(DEV-1328): Add Directus Docker setup and management commands
1 parent fe12ebf commit aba38cd

12 files changed

Lines changed: 1465 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { filesystem, system } from '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+
export {};
9+
10+
// Directus commands require Docker or external services
11+
// These tests verify commands exist and handle missing dependencies gracefully
12+
// They do not create actual Directus instances or require running services
13+
14+
describe('Directus Commands', () => {
15+
describe('lt directus docker-setup', () => {
16+
test('checks for Docker installation', async () => {
17+
try {
18+
const output = await cli('directus docker-setup --noConfirm --name test-instance');
19+
// If Docker is installed, command should proceed (though may fail for other reasons)
20+
expect(output).toBeDefined();
21+
} catch (e: any) {
22+
// If Docker is not installed, should show error message
23+
const errorMsg = e.message || e.stderr || '';
24+
expect(
25+
errorMsg.includes('Docker') ||
26+
errorMsg.includes('docker') ||
27+
errorMsg.includes('Instance name is required')
28+
).toBe(true);
29+
}
30+
});
31+
32+
test('requires instance name with --noConfirm', async () => {
33+
try {
34+
const output = await cli('directus docker-setup --noConfirm');
35+
// Should fail because name is required with noConfirm
36+
expect(output).toContain('Instance name is required');
37+
} catch (e: any) {
38+
// Command might fail, but error should be related to missing name or Docker
39+
const errorMsg = e.message || e.stderr || '';
40+
expect(errorMsg).toBeDefined();
41+
}
42+
});
43+
});
44+
45+
describe('lt directus remove', () => {
46+
test('handles missing directus directory gracefully', async () => {
47+
try {
48+
const output = await cli('directus remove non-existent-instance --noConfirm');
49+
// Should either report no instances found or instance not found
50+
expect(
51+
output.includes('No Directus instances found') ||
52+
output.includes('not found')
53+
).toBe(true);
54+
} catch (e: any) {
55+
// Error is acceptable - command handles non-existent instances
56+
const errorMsg = e.message || e.stderr || '';
57+
expect(errorMsg).toBeDefined();
58+
}
59+
});
60+
61+
test('requires instance name with --noConfirm', async () => {
62+
try {
63+
const output = await cli('directus remove --noConfirm');
64+
// Should fail because name is required with noConfirm
65+
expect(
66+
output.includes('Instance name is required') ||
67+
output.includes('No Directus instances found')
68+
).toBe(true);
69+
} catch (e: any) {
70+
const errorMsg = e.message || e.stderr || '';
71+
expect(errorMsg).toBeDefined();
72+
}
73+
});
74+
});
75+
76+
describe('lt directus typegen', () => {
77+
test('requires token with --noConfirm', async () => {
78+
try {
79+
const output = await cli('directus typegen --noConfirm --url http://localhost:8055');
80+
// Should fail because token is required
81+
expect(output).toContain('token is required');
82+
} catch (e: any) {
83+
const errorMsg = e.message || e.stderr || '';
84+
expect(errorMsg.includes('token')).toBe(true);
85+
}
86+
});
87+
88+
test('requires url with --noConfirm', async () => {
89+
try {
90+
const output = await cli('directus typegen --noConfirm --token test-token');
91+
// Command should proceed with default URL or fail gracefully
92+
expect(output).toBeDefined();
93+
} catch (e: any) {
94+
// Expected to fail when connecting to Directus
95+
const errorMsg = e.message || e.stderr || '';
96+
expect(errorMsg).toBeDefined();
97+
}
98+
});
99+
100+
test('handles invalid URL/token gracefully', async () => {
101+
// Use a temp directory for output to avoid creating files in project
102+
const tempDir = filesystem.path(filesystem.homedir(), `.lt-test-${Date.now()}`);
103+
filesystem.dir(tempDir);
104+
const outputFile = filesystem.path(tempDir, 'test-schema.ts');
105+
106+
try {
107+
await cli(
108+
`directus typegen --noConfirm --url http://localhost:9999 --token invalid-token --output ${outputFile}`
109+
);
110+
} catch (e: any) {
111+
// Should fail when trying to connect to invalid Directus instance
112+
const errorMsg = e.message || e.stderr || '';
113+
expect(
114+
errorMsg.includes('Failed') ||
115+
errorMsg.includes('error') ||
116+
errorMsg.includes('connect')
117+
).toBe(true);
118+
} finally {
119+
filesystem.remove(tempDir);
120+
}
121+
});
122+
});
123+
124+
});

docs/commands.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This document provides a comprehensive reference for all `lt` CLI commands. For
1414
- [Config Commands](#config-commands)
1515
- [Utility Commands](#utility-commands)
1616
- [Database Commands](#database-commands)
17+
- [Directus Commands](#directus-commands)
1718
- [TypeScript Commands](#typescript-commands)
1819
- [Starter Commands](#starter-commands)
1920
- [Claude Commands](#claude-commands)
@@ -794,6 +795,129 @@ lt qdrant delete
794795

795796
---
796797

798+
## Directus Commands
799+
800+
### `lt directus docker-setup`
801+
802+
Sets up a local Directus Docker instance using docker-compose.
803+
804+
**Usage:**
805+
```bash
806+
lt directus docker-setup [options]
807+
```
808+
809+
**Options:**
810+
| Option | Description |
811+
|--------|-------------|
812+
| `--name <name>` / `-n` | Instance name (stored in ~/.lt/directus/<name>) |
813+
| `--version <version>` / `-v` | Directus version (default: latest) |
814+
| `--database <type>` / `--db <type>` | Database type: `postgres`, `mysql`, `sqlite` |
815+
| `--port <number>` / `-p` | Port number (default: auto-detect starting from 8055) |
816+
| `--update` | Update existing instance configuration |
817+
| `--noConfirm` | Skip confirmation prompts |
818+
819+
**Configuration:** `commands.directus.dockerSetup.*`, `defaults.noConfirm`
820+
821+
**Port Auto-detection:**
822+
- If `--port` is not specified, the CLI automatically finds an available port starting from 8055
823+
- Each instance gets its own port (8055, 8056, 8057, etc.)
824+
- This allows running multiple Directus instances simultaneously
825+
826+
**Generated files:**
827+
- `~/.lt/directus/<name>/docker-compose.yml` - Container configuration
828+
- `~/.lt/directus/<name>/.env` - Secrets and environment variables
829+
- `~/.lt/directus/<name>/README.md` - Usage instructions
830+
831+
**Examples:**
832+
```bash
833+
# Create PostgreSQL instance (auto-detects port 8055)
834+
lt directus docker-setup --name my-project --database postgres
835+
836+
# Create second instance (auto-detects port 8056)
837+
lt directus docker-setup --name another-project --database mysql
838+
839+
# Create with specific port
840+
lt directus docker-setup --name custom-app --database sqlite --port 9000
841+
842+
# Create with specific version
843+
lt directus docker-setup --name my-app --database mysql --version 10
844+
845+
# Update existing instance
846+
lt directus docker-setup --name my-project --version 11 --update
847+
```
848+
849+
---
850+
851+
### `lt directus remove`
852+
853+
Removes a Directus Docker instance and all its data.
854+
855+
**Usage:**
856+
```bash
857+
lt directus remove [name] [options]
858+
```
859+
860+
**Arguments:**
861+
| Argument | Description |
862+
|----------|-------------|
863+
| `name` | Instance name to remove (optional, will prompt if omitted) |
864+
865+
**Options:**
866+
| Option | Description |
867+
|--------|-------------|
868+
| `--noConfirm` | Skip confirmation prompts |
869+
870+
**Configuration:** `commands.directus.remove.*`, `defaults.noConfirm`
871+
872+
**What gets removed:**
873+
- Stops and removes Docker containers
874+
- Removes all Docker volumes (database, uploads, extensions)
875+
- Deletes instance directory from ~/.lt/directus/
876+
877+
**Examples:**
878+
```bash
879+
# Interactive (shows list of instances)
880+
lt directus remove
881+
882+
# Remove specific instance
883+
lt directus remove my-project
884+
885+
# Skip confirmation
886+
lt directus remove my-project --noConfirm
887+
```
888+
889+
---
890+
891+
### `lt directus typegen`
892+
893+
Generates TypeScript types from Directus collections.
894+
895+
**Usage:**
896+
```bash
897+
lt directus typegen [options]
898+
```
899+
900+
**Options:**
901+
| Option | Description |
902+
|--------|-------------|
903+
| `--url <url>` / `-u` | Directus API URL |
904+
| `--token <token>` / `-t` | Directus API token (Administrator permissions required) |
905+
| `--output <path>` / `-o` | Output file path |
906+
| `--noConfirm` | Skip confirmation prompts |
907+
908+
**Configuration:** `commands.directus.typegen.*`, `defaults.noConfirm`
909+
910+
**Examples:**
911+
```bash
912+
# Interactive
913+
lt directus typegen
914+
915+
# With all options
916+
lt directus typegen --url http://localhost:8055 --token <token> --output ./types.ts
917+
```
918+
919+
---
920+
797921
## TypeScript Commands
798922

799923
### `lt typescript create`

schemas/lt.config.schema.json

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,82 @@
170170
},
171171
"additionalProperties": false
172172
},
173+
"directus": {
174+
"type": "object",
175+
"description": "Directus-related configuration",
176+
"properties": {
177+
"typegen": {
178+
"type": "object",
179+
"description": "Configuration for 'lt directus typegen' command",
180+
"properties": {
181+
"url": {
182+
"type": "string",
183+
"description": "Directus API URL",
184+
"examples": ["http://localhost:8055"]
185+
},
186+
"token": {
187+
"type": "string",
188+
"description": "Directus API token (needs Administrator permissions)"
189+
},
190+
"output": {
191+
"type": "string",
192+
"description": "Default output file path",
193+
"examples": ["./directus-schema.ts"]
194+
},
195+
"noConfirm": {
196+
"type": "boolean",
197+
"description": "Skip confirmation prompts"
198+
}
199+
},
200+
"additionalProperties": false
201+
},
202+
"dockerSetup": {
203+
"type": "object",
204+
"description": "Configuration for 'lt directus docker-setup' command",
205+
"properties": {
206+
"name": {
207+
"type": "string",
208+
"description": "Default instance name",
209+
"examples": ["directus"]
210+
},
211+
"version": {
212+
"type": "string",
213+
"description": "Default Directus version",
214+
"examples": ["latest", "10", "10.8.0"]
215+
},
216+
"database": {
217+
"type": "string",
218+
"enum": ["postgres", "mysql", "sqlite"],
219+
"description": "Default database type"
220+
},
221+
"port": {
222+
"type": "number",
223+
"description": "Default port for Directus. If not specified, auto-detects next available port starting from 8055",
224+
"minimum": 1,
225+
"maximum": 65535,
226+
"examples": [8055]
227+
},
228+
"noConfirm": {
229+
"type": "boolean",
230+
"description": "Skip confirmation prompts"
231+
}
232+
},
233+
"additionalProperties": false
234+
},
235+
"remove": {
236+
"type": "object",
237+
"description": "Configuration for 'lt directus remove' command",
238+
"properties": {
239+
"noConfirm": {
240+
"type": "boolean",
241+
"description": "Skip confirmation prompts"
242+
}
243+
},
244+
"additionalProperties": false
245+
}
246+
},
247+
"additionalProperties": false
248+
},
173249
"frontend": {
174250
"type": "object",
175251
"description": "Frontend-related configuration",

src/commands/config/validate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ const KNOWN_KEYS: Record<string, Record<string, any>> = {
2929
prodRunner: 'string',
3030
testRunner: 'string',
3131
},
32+
directus: {
33+
dockerSetup: {
34+
database: ['postgres', 'mysql', 'sqlite'],
35+
name: 'string',
36+
noConfirm: 'boolean',
37+
port: 'number',
38+
version: 'string',
39+
},
40+
remove: { noConfirm: 'boolean' },
41+
typegen: { noConfirm: 'boolean', output: 'string', token: 'string', url: 'string' },
42+
},
3243
frontend: {
3344
angular: { branch: 'string', copy: 'string', link: 'string', localize: 'boolean', noConfirm: 'boolean' },
3445
nuxt: { branch: 'string', copy: 'string', link: 'string' },
@@ -148,6 +159,8 @@ function validateConfig(config: any, knownKeys: Record<string, any>, path = ''):
148159
result.errors.push(`${currentPath}: expected string, got ${typeof value}`);
149160
} else if (expectedType === 'boolean' && typeof value !== 'boolean') {
150161
result.errors.push(`${currentPath}: expected boolean, got ${typeof value}`);
162+
} else if (expectedType === 'number' && typeof value !== 'number') {
163+
result.errors.push(`${currentPath}: expected number, got ${typeof value}`);
151164
} else if (expectedType === 'array' && !Array.isArray(value)) {
152165
result.errors.push(`${currentPath}: expected array, got ${typeof value}`);
153166
}

src/commands/directus/directus.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ExtendedGluegunToolbox } from '../../interfaces/extended-gluegun-toolbox';
2+
3+
/**
4+
* Directus commands
5+
*/
6+
const command = {
7+
alias: ['d'],
8+
description: 'Directus commands',
9+
hidden: false,
10+
name: 'directus',
11+
run: async (toolbox: ExtendedGluegunToolbox) => {
12+
await toolbox.helper.showMenu('directus', {
13+
headline: 'Directus Commands',
14+
});
15+
},
16+
};
17+
18+
export default command;

0 commit comments

Comments
 (0)