directus-template-cli 0.7.0-beta.5 → 0.7.0-beta.6

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.
@@ -7,57 +7,88 @@ import { glob } from 'glob';
7
7
  import fs from 'node:fs';
8
8
  import { detectPackageManager, installDependencies } from 'nypm';
9
9
  import path from 'pathe';
10
+ import dotenv from 'dotenv';
10
11
  import ApplyCommand from '../../commands/apply.js';
11
12
  import { createDocker } from '../../services/docker.js';
12
13
  import catchError from '../utils/catch-error.js';
13
14
  import { createGigetString, parseGitHubUrl } from '../utils/parse-github-url.js';
14
- import { DIRECTUS_CONFIG, DOCKER_CONFIG } from './config.js';
15
- export async function init(dir, flags) {
15
+ import { readTemplateConfig } from '../utils/template-config.js';
16
+ import { DOCKER_CONFIG } from './config.js';
17
+ export async function init({ dir, flags }) {
16
18
  // Check target directory
17
19
  const shouldForce = flags.overrideDir;
18
20
  if (fs.existsSync(dir) && !shouldForce) {
19
21
  throw new Error('Directory already exists. Use --override-dir to override.');
20
22
  }
21
- const frontendDir = path.join(dir, flags.frontend);
23
+ // If template is a URL, we need to handle it differently
24
+ const isDirectUrl = flags.template?.startsWith('http');
22
25
  const directusDir = path.join(dir, 'directus');
23
26
  let template;
27
+ let packageManager = null;
24
28
  try {
25
29
  // Download the template from GitHub
26
30
  const parsedUrl = parseGitHubUrl(flags.template);
31
+ // If it's a direct URL, we download the entire repository
32
+ // Otherwise, we use the template from the starters repo
27
33
  template = await downloadTemplate(createGigetString(parsedUrl), {
28
34
  dir,
29
35
  force: shouldForce,
30
36
  });
31
- // Cleanup the template
32
- if (flags.frontend) {
33
- // Ensure directus directory exists before cleaning up
37
+ // For direct URLs, we need to check if there's a directus directory
38
+ // If not, assume the entire repo is a directus template
39
+ if (isDirectUrl) {
34
40
  if (!fs.existsSync(directusDir)) {
41
+ // Move all files to directus directory
35
42
  fs.mkdirSync(directusDir, { recursive: true });
43
+ const files = fs.readdirSync(dir);
44
+ for (const file of files) {
45
+ if (file !== 'directus') {
46
+ fs.renameSync(path.join(dir, file), path.join(directusDir, file));
47
+ }
48
+ }
36
49
  }
37
- // Read and parse package.json
38
- const packageJsonPath = path.join(dir, 'package.json');
39
- if (!fs.existsSync(packageJsonPath)) {
40
- throw new Error('package.json not found in template');
50
+ }
51
+ // Read template configuration
52
+ const templateInfo = readTemplateConfig(dir);
53
+ let frontendDir;
54
+ // Handle frontends based on template configuration
55
+ if (flags.frontend && templateInfo) {
56
+ // Find the selected frontend in the configuration
57
+ const selectedFrontend = templateInfo.frontendOptions.find(f => f.id === flags.frontend);
58
+ if (!selectedFrontend) {
59
+ throw new Error(`Frontend "${flags.frontend}" not found in template configuration`);
41
60
  }
42
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
43
- const templateConfig = packageJson['directus:template'];
44
- // Get all frontend paths from the configuration
45
- const frontendPaths = Object.values(templateConfig?.frontends || {})
46
- .map(frontend => frontend.path.replace(/^\.\//, ''))
47
- .filter(path => path !== flags.frontend); // Exclude the selected frontend
48
- // Remove unused frontend directories
49
- for (const frontendPath of frontendPaths) {
50
- const pathToRemove = path.join(dir, frontendPath);
51
- if (fs.existsSync(pathToRemove)) {
52
- fs.rmSync(pathToRemove, { recursive: true });
61
+ // Remove all frontend directories except the selected one
62
+ for (const frontend of templateInfo.frontendOptions) {
63
+ if (frontend.id !== flags.frontend) {
64
+ const pathToRemove = path.join(dir, frontend.path);
65
+ if (fs.existsSync(pathToRemove)) {
66
+ fs.rmSync(pathToRemove, { recursive: true });
67
+ }
53
68
  }
54
69
  }
70
+ // Move the selected frontend to the correct location if needed
71
+ frontendDir = path.join(dir, selectedFrontend.path);
72
+ if (frontendDir !== path.join(dir, flags.frontend)) {
73
+ fs.renameSync(frontendDir, path.join(dir, flags.frontend));
74
+ frontendDir = path.join(dir, flags.frontend);
75
+ }
55
76
  }
77
+ const directusInfo = {
78
+ email: '',
79
+ password: '',
80
+ url: '',
81
+ };
56
82
  // Find and copy all .env.example files
57
83
  const envFiles = glob.sync(path.join(dir, '**', '.env.example'));
58
84
  for (const file of envFiles) {
59
85
  const envFile = file.replace('.env.example', '.env');
60
86
  fs.copyFileSync(file, envFile);
87
+ // Read default Directus login info from .env
88
+ const parsedEnv = dotenv.parse(fs.readFileSync(file, 'utf8'));
89
+ directusInfo.email = parsedEnv.ADMIN_EMAIL;
90
+ directusInfo.password = parsedEnv.ADMIN_PASSWORD;
91
+ directusInfo.url = parsedEnv.PUBLIC_URL;
61
92
  }
62
93
  // Start Directus and apply template only if directus directory exists
63
94
  if (fs.existsSync(directusDir)) {
@@ -70,12 +101,10 @@ export async function init(dir, flags) {
70
101
  }
71
102
  try {
72
103
  await dockerService.startContainers(directusDir);
73
- const healthCheckUrl = `${DIRECTUS_CONFIG.url}:${DIRECTUS_CONFIG.port}${DOCKER_CONFIG.healthCheckEndpoint}`;
104
+ const healthCheckUrl = `${directusInfo.url}${DOCKER_CONFIG.healthCheckEndpoint}`;
74
105
  await dockerService.waitForHealthy(healthCheckUrl);
75
106
  const templatePath = path.join(directusDir, 'template');
76
- // const s = spinner()
77
- // s.start(`Attempting to apply template from: ${templatePath}`)
78
- // ux.stdout(`Attempting to apply template from: ${templatePath}`)
107
+ ux.stdout(`Attempting to apply template from: ${templatePath}`);
79
108
  await ApplyCommand.run([
80
109
  '--directusUrl=http://localhost:8055',
81
110
  '-p',
@@ -83,50 +112,48 @@ export async function init(dir, flags) {
83
112
  '--userPassword=d1r3ctu5',
84
113
  `--templateLocation=${templatePath}`,
85
114
  ]);
86
- // s.stop('Template applied!')
87
115
  }
88
116
  catch (error) {
89
117
  ux.error('Failed to start Directus containers or apply template');
90
118
  throw error;
91
119
  }
92
120
  }
93
- // Install dependencies for frontend if it exists
94
- if (flags.installDeps && fs.existsSync(frontendDir)) {
121
+ // Install dependencies if requested
122
+ if (flags.installDeps) {
95
123
  const s = spinner();
96
124
  s.start('Installing dependencies');
97
- // ux.action.start('Installing dependencies')
98
125
  try {
99
- const packageManager = await detectPackageManager(frontendDir);
100
- await installDependencies({
101
- cwd: frontendDir,
102
- packageManager,
103
- silent: true,
104
- });
126
+ if (fs.existsSync(frontendDir)) {
127
+ packageManager = await detectPackageManager(frontendDir);
128
+ await installDependencies({
129
+ cwd: frontendDir,
130
+ packageManager,
131
+ silent: true,
132
+ });
133
+ }
105
134
  }
106
135
  catch (error) {
107
136
  ux.warn('Failed to install dependencies');
108
137
  throw error;
109
138
  }
110
- // ux.action.stop()
111
139
  s.stop('Dependencies installed!');
112
140
  }
113
141
  // Initialize Git repo
114
142
  if (flags.gitInit) {
115
143
  const s = spinner();
116
144
  s.start('Initializing git repository');
117
- // ux.action.start('Initializing git repository')
118
145
  await initGit(dir);
119
- // ux.action.stop()
120
146
  s.stop('Git repository initialized!');
121
147
  }
122
148
  // Finishing up
123
149
  const relativeDir = path.relative(process.cwd(), dir);
124
- const nextSteps = `- Directus is running on http://localhost:8055 \n- Navigate to your project directory using ${chalk.cyan(`cd ${relativeDir}`)} and start developing! \n- Review the \`./README.md\` file for next steps.`;
150
+ const directusText = `- Directus is running on ${directusInfo.url ?? 'http://localhost:8055'}. You can login with the email: ${chalk.cyan(directusInfo.email)} and password: ${chalk.cyan(directusInfo.password)}. \n`;
151
+ const frontendText = flags.frontend ? `- To start the frontend, run ${chalk.cyan(`cd ${flags.frontend}`)} and then ${chalk.cyan(`${packageManager?.name} run dev`)}. \n` : '';
152
+ const projectText = `- Navigate to your project directory using ${chalk.cyan(`cd ${relativeDir}`)}. \n`;
153
+ const readmeText = '- Review the \`./README.md\` file for more information and next steps.';
154
+ const nextSteps = chalk.white(`${directusText}${projectText}${frontendText}${readmeText}`);
125
155
  note(nextSteps, 'Next Steps');
126
- // ux.stdout('You\'ll find the following directories in your project:')
127
- // ux.stdout('• directus')
128
- // ux.stdout(`• ${flags.frontend}`)
129
- outro(`Problems? Join the community on Discord at ${chalk.underline(chalk.cyan('https://directus.chat'))}`);
156
+ outro(`Problems or questions? Hop into the community on Discord at ${chalk.underline(chalk.cyan('https://directus.chat'))}`);
130
157
  }
131
158
  catch (error) {
132
159
  catchError(error, {
@@ -137,7 +164,7 @@ export async function init(dir, flags) {
137
164
  }
138
165
  return {
139
166
  directusDir,
140
- frontendDir,
167
+ frontendDir: flags.frontend ? path.join(dir, flags.frontend) : undefined,
141
168
  template,
142
169
  };
143
170
  }
@@ -148,9 +175,7 @@ export async function init(dir, flags) {
148
175
  */
149
176
  async function initGit(targetDir) {
150
177
  try {
151
- // ux.action.start('Initializing git repository')
152
178
  await execa('git', ['init'], { cwd: targetDir });
153
- // ux.action.stop()
154
179
  }
155
180
  catch (error) {
156
181
  catchError(error, {
@@ -1,13 +1,2 @@
1
- interface ApplyFlags {
2
- content: boolean;
3
- dashboards: boolean;
4
- extensions: boolean;
5
- files: boolean;
6
- flows: boolean;
7
- permissions: boolean;
8
- schema: boolean;
9
- settings: boolean;
10
- users: boolean;
11
- }
1
+ import type { ApplyFlags } from './apply-flags.js';
12
2
  export default function apply(dir: string, flags: ApplyFlags): Promise<{}>;
13
- export {};
@@ -0,0 +1,18 @@
1
+ export interface DirectusTemplateFrontend {
2
+ name: string;
3
+ path: string;
4
+ }
5
+ export interface DirectusTemplateConfig {
6
+ name: string;
7
+ description: string;
8
+ template: string;
9
+ frontends: {
10
+ [key: string]: DirectusTemplateFrontend;
11
+ };
12
+ }
13
+ export interface TemplatePackageJson {
14
+ name: string;
15
+ version: string;
16
+ description: string;
17
+ 'directus:template'?: DirectusTemplateConfig;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -8,21 +8,23 @@ interface AuthFlags {
8
8
  * Get the Directus URL from the user
9
9
  * @returns The Directus URL
10
10
  */
11
- export declare function getDirectusUrl(): Promise<string | symbol>;
11
+ export declare function getDirectusUrl(): Promise<string>;
12
12
  /**
13
13
  * Get the Directus token from the user
14
14
  * @param directusUrl - The Directus URL
15
15
  * @returns The Directus token
16
16
  */
17
- export declare function getDirectusToken(directusUrl: string): Promise<string | symbol>;
17
+ export declare function getDirectusToken(directusUrl: string): Promise<string>;
18
18
  /**
19
- * Initialize the Directus API with the provided flags
20
- * @param flags - The validated ApplyFlags
21
- */
19
+ * Initialize the Directus API with the provided flags and log in the user
20
+ * @param flags - The validated ApplyFlags
21
+ * @returns {Promise<void>} - Returns nothing
22
+ */
22
23
  export declare function initializeDirectusApi(flags: AuthFlags): Promise<void>;
23
24
  /**
24
- * Validate the authentication flags
25
- * @param flags - The AuthFlags
26
- */
25
+ * Validate the authentication flags
26
+ * @param flags - The AuthFlags
27
+ * @returns {void} - Errors if the flags are invalid
28
+ */
27
29
  export declare function validateAuthFlags(flags: AuthFlags): void;
28
30
  export {};
@@ -1,5 +1,5 @@
1
1
  import { readMe } from '@directus/sdk';
2
- import { text } from '@clack/prompts';
2
+ import { text, log, isCancel } from '@clack/prompts';
3
3
  import { ux } from '@oclif/core';
4
4
  import { api } from '../sdk.js';
5
5
  import catchError from './catch-error.js';
@@ -13,6 +13,10 @@ export async function getDirectusUrl() {
13
13
  placeholder: 'http://localhost:8055',
14
14
  message: 'What is your Directus URL?',
15
15
  });
16
+ if (isCancel(directusUrl)) {
17
+ log.info('Exiting...');
18
+ ux.exit(0);
19
+ }
16
20
  // Validate URL
17
21
  if (!validateUrl(directusUrl)) {
18
22
  ux.warn('Invalid URL');
@@ -31,6 +35,10 @@ export async function getDirectusToken(directusUrl) {
31
35
  placeholder: 'admin-token-here',
32
36
  message: 'What is your Directus Admin Token?',
33
37
  });
38
+ if (isCancel(directusToken)) {
39
+ log.info('Exiting...');
40
+ ux.exit(0);
41
+ }
34
42
  // Validate token by fetching the user
35
43
  try {
36
44
  await api.loginWithToken(directusToken);
@@ -49,9 +57,10 @@ export async function getDirectusToken(directusUrl) {
49
57
  }
50
58
  }
51
59
  /**
52
- * Initialize the Directus API with the provided flags
53
- * @param flags - The validated ApplyFlags
54
- */
60
+ * Initialize the Directus API with the provided flags and log in the user
61
+ * @param flags - The validated ApplyFlags
62
+ * @returns {Promise<void>} - Returns nothing
63
+ */
55
64
  export async function initializeDirectusApi(flags) {
56
65
  api.initialize(flags.directusUrl);
57
66
  try {
@@ -71,9 +80,10 @@ export async function initializeDirectusApi(flags) {
71
80
  }
72
81
  }
73
82
  /**
74
- * Validate the authentication flags
75
- * @param flags - The AuthFlags
76
- */
83
+ * Validate the authentication flags
84
+ * @param flags - The AuthFlags
85
+ * @returns {void} - Errors if the flags are invalid
86
+ */
77
87
  export function validateAuthFlags(flags) {
78
88
  if (!flags.directusUrl) {
79
89
  ux.error('Directus URL is required.');
@@ -5,10 +5,15 @@ interface GitHubUrlParts {
5
5
  repo: string;
6
6
  }
7
7
  /**
8
- * Parse a GitHub URL into its components.
9
- * @param url - The GitHub URL to parse.
10
- * @returns The parsed GitHub URL components.
11
- */
8
+ * Parse a GitHub URL or path into its components
9
+ * @param url The URL or path to parse
10
+ * @returns The parsed components
11
+ */
12
12
  export declare function parseGitHubUrl(url: string): GitHubUrlParts;
13
- export declare function createGigetString({ owner, path, ref, repo }: GitHubUrlParts): string;
13
+ /**
14
+ * Creates a giget-compatible string from GitHub URL parts
15
+ * @param parts The parsed GitHub URL parts
16
+ * @returns A string in the format 'gh:owner/repo#ref[/path]'
17
+ */
18
+ export declare function createGigetString(parts: GitHubUrlParts): string;
14
19
  export {};
@@ -1,49 +1,89 @@
1
- import { DEFAULT_REPO } from '../constants.js';
1
+ import { DEFAULT_BRANCH, DEFAULT_REPO } from '../constants.js';
2
2
  /**
3
- * Parse a GitHub URL into its components.
4
- * @param url - The GitHub URL to parse.
5
- * @returns The parsed GitHub URL components.
6
- */
7
- export function parseGitHubUrl(url) {
8
- // Handle simple template names by using default repo
9
- if (!url.includes('/')) {
10
- return { ...DEFAULT_REPO, path: url };
11
- }
12
- // Handle different GitHub URL formats:
13
- // - https://github.com/owner/repo
14
- // - https://github.com/owner/repo/tree/branch
15
- // - https://github.com/owner/repo/tree/branch/path
16
- // - owner/repo
17
- // - owner/repo/path
3
+ * Clean and normalize a GitHub URL
4
+ * Handles various formats:
5
+ * - Full URLs with .git
6
+ * - URLs with query parameters
7
+ * - URLs with hash fragments
8
+ * - URLs with branches/refs
9
+ * - Repository paths
10
+ * @param url The URL or path to clean
11
+ * @returns Cleaned URL without .git, queries, or hashes
12
+ */
13
+ function cleanGitHubUrl(url) {
18
14
  try {
19
- if (url.startsWith('https://github.com/')) {
20
- url = url.replace('https://github.com/', '');
15
+ // If it's not a URL, return as is (might be a path)
16
+ if (!url.includes('://')) {
17
+ return url.replace(/\.git$/, '');
21
18
  }
22
- const parts = url.split('/');
23
- const owner = parts[0];
24
- const repo = parts[1];
25
- let path = '';
26
- let ref;
27
- if (parts.length > 2) {
28
- if (parts[2] === 'tree' && parts.length > 3) {
29
- ref = parts[3];
30
- path = parts.slice(4).join('/');
31
- }
32
- else {
33
- path = parts.slice(2).join('/');
19
+ // Parse the URL
20
+ const parsed = new URL(url);
21
+ // Remove .git suffix from pathname
22
+ parsed.pathname = parsed.pathname.replace(/\.git$/, '');
23
+ // Remove search params and hash
24
+ parsed.search = '';
25
+ parsed.hash = '';
26
+ return parsed.toString();
27
+ }
28
+ catch (error) {
29
+ // If URL parsing fails, just remove .git suffix
30
+ return url.replace(/\.git$/, '');
31
+ }
32
+ }
33
+ /**
34
+ * Parse a GitHub URL or path into its components
35
+ * @param url The URL or path to parse
36
+ * @returns The parsed components
37
+ */
38
+ export function parseGitHubUrl(url) {
39
+ if (!url) {
40
+ throw new Error('URL is required');
41
+ }
42
+ // Clean the URL first
43
+ const cleanedUrl = cleanGitHubUrl(url);
44
+ // Handle full GitHub URLs
45
+ if (cleanedUrl.includes('github.com')) {
46
+ try {
47
+ const parsed = new URL(cleanedUrl);
48
+ const parts = parsed.pathname.split('/').filter(Boolean);
49
+ if (parts.length < 2) {
50
+ throw new Error('Invalid GitHub URL format');
34
51
  }
52
+ const [owner, repo, ...rest] = parts;
53
+ const path = rest.length > 0 ? rest.join('/') : undefined;
54
+ const ref = parsed.searchParams.get('ref') || DEFAULT_BRANCH;
55
+ return { owner, repo, path, ref };
56
+ }
57
+ catch (error) {
58
+ throw new Error(`Invalid GitHub URL: ${url}`);
35
59
  }
36
- return { owner, path, ref, repo };
37
60
  }
38
- catch {
39
- throw new Error(`Invalid GitHub URL format: ${url}`);
61
+ // Handle repository paths (owner/repo format)
62
+ const parts = cleanedUrl.split('/').filter(Boolean);
63
+ if (parts.length >= 2) {
64
+ const [owner, repo, ...rest] = parts;
65
+ const path = rest.length > 0 ? rest.join('/') : undefined;
66
+ return { owner, repo, path, ref: DEFAULT_BRANCH };
40
67
  }
68
+ // Handle simple template names using DEFAULT_REPO
69
+ return {
70
+ ...DEFAULT_REPO,
71
+ path: cleanedUrl // The template name becomes the subpath
72
+ };
41
73
  }
42
- export function createGigetString({ owner, path, ref, repo }) {
43
- let source = `github:${owner}/${repo}`;
44
- if (path)
45
- source += `/${path}`;
46
- if (ref)
47
- source += `#${ref}`;
48
- return source;
74
+ /**
75
+ * Creates a giget-compatible string from GitHub URL parts
76
+ * @param parts The parsed GitHub URL parts
77
+ * @returns A string in the format 'gh:owner/repo#ref[/path]'
78
+ */
79
+ export function createGigetString(parts) {
80
+ // For the default repo case with a template name
81
+ if (parts.owner === DEFAULT_REPO.owner && parts.repo === DEFAULT_REPO.repo) {
82
+ return `gh:${parts.owner}/${parts.repo}/${parts.path}#${DEFAULT_REPO.ref}`;
83
+ }
84
+ // For other GitHub URLs
85
+ const base = `gh:${parts.owner}/${parts.repo}`;
86
+ const path = parts.path ? `/${parts.path}` : '';
87
+ const ref = parts.ref ? `#${parts.ref}` : '';
88
+ return `${base}${path}${ref}`;
49
89
  }
@@ -0,0 +1,3 @@
1
+ export declare const sanitizeFlags: (flags: Record<string, unknown>) => {
2
+ [k: string]: unknown;
3
+ };
@@ -0,0 +1,4 @@
1
+ const SENSITIVE_FLAGS = ['userEmail', 'userPassword', 'directusToken'];
2
+ export const sanitizeFlags = (flags) => {
3
+ return Object.fromEntries(Object.entries(flags).filter(([key]) => !SENSITIVE_FLAGS.includes(key)));
4
+ };
@@ -0,0 +1,16 @@
1
+ import type { DirectusTemplateConfig } from '../types.js';
2
+ export interface TemplateInfo {
3
+ config: DirectusTemplateConfig;
4
+ frontendOptions: Array<{
5
+ id: string;
6
+ name: string;
7
+ path: string;
8
+ }>;
9
+ }
10
+ /**
11
+ * Read and validate the template configuration from a directory
12
+ * @param dir Directory containing the template
13
+ * @returns Template configuration and frontend options
14
+ * @throws Error if package.json is missing or invalid
15
+ */
16
+ export declare function readTemplateConfig(dir: string): TemplateInfo | null;
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import path from 'pathe';
3
+ /**
4
+ * Read and validate the template configuration from a directory
5
+ * @param dir Directory containing the template
6
+ * @returns Template configuration and frontend options
7
+ * @throws Error if package.json is missing or invalid
8
+ */
9
+ export function readTemplateConfig(dir) {
10
+ try {
11
+ const packageJsonPath = path.join(dir, 'package.json');
12
+ if (!fs.existsSync(packageJsonPath)) {
13
+ return null;
14
+ }
15
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
16
+ const templateConfig = packageJson['directus:template'];
17
+ if (!templateConfig) {
18
+ return null;
19
+ }
20
+ // Convert frontends object to array of options
21
+ const frontendOptions = Object.entries(templateConfig.frontends || {}).map(([id, frontend]) => ({
22
+ id,
23
+ name: frontend.name,
24
+ path: frontend.path.replace(/^\.\//, ''), // Remove leading ./
25
+ }));
26
+ return {
27
+ config: templateConfig,
28
+ frontendOptions,
29
+ };
30
+ }
31
+ catch (error) {
32
+ return null;
33
+ }
34
+ }
@@ -1,7 +1,62 @@
1
1
  import { spinner } from '@clack/prompts';
2
2
  import { execa } from 'execa';
3
+ import net from 'node:net';
4
+ import { ux } from '@oclif/core';
3
5
  import catchError from '../lib/utils/catch-error.js';
4
6
  import { waitFor } from '../lib/utils/wait.js';
7
+ /**
8
+ * Check if a port is in use and what's using it
9
+ * @param port The port to check
10
+ * @returns Object indicating if port is in use and what's using it
11
+ */
12
+ async function checkPort(port) {
13
+ return new Promise((resolve) => {
14
+ const server = net.createServer();
15
+ server.once('error', async (err) => {
16
+ if (err.code === 'EADDRINUSE') {
17
+ // Try to get information about what's using the port
18
+ try {
19
+ const { stdout } = await execa('lsof', ['-i', `:${port}`]);
20
+ const process = stdout.split('\n')[1]?.split(/\s+/)[0]; // Get process name
21
+ resolve({ inUse: true, process });
22
+ }
23
+ catch {
24
+ resolve({ inUse: true });
25
+ }
26
+ }
27
+ else {
28
+ resolve({ inUse: false });
29
+ }
30
+ });
31
+ server.once('listening', () => {
32
+ server.close();
33
+ resolve({ inUse: false });
34
+ });
35
+ server.listen(port);
36
+ });
37
+ }
38
+ /**
39
+ * Check if required ports are available and warn if they're in use
40
+ * @returns Promise<void>
41
+ */
42
+ async function checkRequiredPorts() {
43
+ const portsToCheck = [
44
+ { port: 8055, name: 'Directus API' },
45
+ { port: 5432, name: 'PostgreSQL' },
46
+ ];
47
+ let hasConflicts = false;
48
+ for (const { port, name } of portsToCheck) {
49
+ const status = await checkPort(port);
50
+ if (status.inUse) {
51
+ hasConflicts = true;
52
+ const process = status.process ? ` by ${status.process}` : '';
53
+ ux.warn(`Port ${port} (${name}) is already in use${process}`);
54
+ }
55
+ }
56
+ if (hasConflicts) {
57
+ ux.warn('Please stop any conflicting services before continuing.');
58
+ }
59
+ }
5
60
  /**
6
61
  * Check if Docker is installed and running
7
62
  * @returns {Promise<DockerCheckResult>} Docker installation and running status
@@ -39,12 +94,12 @@ async function checkDocker() {
39
94
  */
40
95
  async function startContainers(cwd) {
41
96
  try {
42
- // ux.action.start('Starting Docker containers')
97
+ // Check if required ports are available
98
+ await checkRequiredPorts();
43
99
  const s = spinner();
44
100
  s.start('Starting Docker containers');
45
101
  return execa('docker-compose', ['up', '-d'], {
46
102
  cwd,
47
- // stdio: 'inherit',
48
103
  }).then(() => {
49
104
  s.stop('Docker containers running!');
50
105
  });
@@ -67,7 +122,6 @@ async function stopContainers(cwd) {
67
122
  try {
68
123
  return execa('docker-compose', ['down'], {
69
124
  cwd,
70
- // stdio: 'inherit',
71
125
  }).then(() => { });
72
126
  }
73
127
  catch (error) {