directus-template-cli 0.7.0-beta.9 → 0.7.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.
@@ -2,6 +2,8 @@ import chalk from 'chalk';
2
2
  export const DIRECTUS_PURPLE = '#6644ff';
3
3
  export const DIRECTUS_PINK = '#FF99DD';
4
4
  export const SEPARATOR = '------------------';
5
+ export const pinkText = chalk.hex(DIRECTUS_PINK);
6
+ export const purpleText = chalk.hex(DIRECTUS_PURPLE);
5
7
  export const COMMUNITY_TEMPLATE_REPO = {
6
8
  string: 'github:directus-labs/directus-templates',
7
9
  url: 'https://github.com/directus-labs/directus-templates',
@@ -17,4 +19,8 @@ export const POSTHOG_PUBLIC_KEY = 'phc_STopE6gj6LDIjYonVF7493kQJK8S4v0Xrl6YPr2z9
17
19
  export const POSTHOG_HOST = 'https://us.i.posthog.com';
18
20
  export const DEFAULT_BRANCH = 'main';
19
21
  export const BSL_LICENSE_URL = 'https://directus.io/bsl';
20
- export const BSL_LICENSE_TEXT = `You REQUIRE a license to use Directus if your organisation has more than $5MM USD a year in revenue and/or funding. For all organizations and people with less than $5MM USD a year in revenue and funding, Directus is free for personal projects, hobby projects and in production. This second group does not require a license. \nDirectus is licensed under BSL1.1. Visit ${chalk.underline(chalk.cyan(BSL_LICENSE_URL))} for more information or reach out to us at ${chalk.underline(chalk.cyan('sales-demo-with-evil-sales@directus.io'))}.`;
22
+ export const BSL_EMAIL = 'licensing@directus.io';
23
+ export const BSL_LICENSE_HEADLINE = 'You REQUIRE a license to use Directus if your organization has more than $5MM USD a year in revenue and/or funding.';
24
+ export const BSL_LICENSE_TEXT = 'For all organizations with less than $5MM USD a year in revenue and funding, Directus is free for personal projects, hobby projects and in production. This second group does not require a license. Directus is licensed under BSL 1.1.';
25
+ export const BSL_LICENSE_CTA = `Visit ${pinkText(BSL_LICENSE_URL)} for more information or reach out to us at ${pinkText(BSL_EMAIL)}.`;
26
+ export const DEFAULT_DIRECTUS_URL = 'http://localhost:8055';
@@ -1,6 +1,5 @@
1
1
  import { note, outro, spinner, log as clackLog } from '@clack/prompts';
2
2
  import { ux } from '@oclif/core';
3
- import chalk from 'chalk';
4
3
  import { execa } from 'execa';
5
4
  import { downloadTemplate } from 'giget';
6
5
  import { glob } from 'glob';
@@ -14,12 +13,12 @@ import catchError from '../utils/catch-error.js';
14
13
  import { createGigetString, parseGitHubUrl } from '../utils/parse-github-url.js';
15
14
  import { readTemplateConfig } from '../utils/template-config.js';
16
15
  import { DOCKER_CONFIG } from './config.js';
17
- import { BSL_LICENSE_TEXT } from '../constants.js';
16
+ import { BSL_LICENSE_TEXT, BSL_LICENSE_HEADLINE, BSL_LICENSE_CTA, pinkText } from '../constants.js';
18
17
  export async function init({ dir, flags }) {
19
18
  // Check target directory
20
- const shouldForce = flags.overrideDir;
19
+ const shouldForce = flags.overwriteDir;
21
20
  if (fs.existsSync(dir) && !shouldForce) {
22
- throw new Error('Directory already exists. Use --override-dir to override.');
21
+ throw new Error('Directory already exists. Use --overwrite-dir to override.');
23
22
  }
24
23
  // If template is a URL, we need to handle it differently
25
24
  const isDirectUrl = flags.template?.startsWith('http');
@@ -104,36 +103,40 @@ export async function init({ dir, flags }) {
104
103
  if (!dockerStatus.installed || !dockerStatus.running) {
105
104
  throw new Error(dockerStatus.message);
106
105
  }
107
- try {
108
- await dockerService.startContainers(directusDir);
109
- const healthCheckUrl = `${directusInfo.url || 'http://localhost:8055'}${DOCKER_CONFIG.healthCheckEndpoint}`;
110
- // Wait for healthy before proceeding
111
- const isHealthy = await dockerService.waitForHealthy(healthCheckUrl);
112
- if (!isHealthy) {
113
- throw new Error('Directus failed to become healthy');
114
- }
115
- const templatePath = path.join(directusDir, 'template');
116
- ux.stdout(`Attempting to apply template from: ${templatePath}`);
106
+ await dockerService.startContainers(directusDir);
107
+ const healthCheckUrl = `${directusInfo.url || 'http://localhost:8055'}${DOCKER_CONFIG.healthCheckEndpoint}`;
108
+ // Wait for healthy before proceeding
109
+ const isHealthy = await dockerService.waitForHealthy(healthCheckUrl);
110
+ if (!isHealthy) {
111
+ throw new Error('Directus failed to become healthy');
112
+ }
113
+ // Check if a template path is specified in the config and exists
114
+ let templatePath;
115
+ if (templateInfo?.config?.template && typeof templateInfo.config.template === 'string') {
116
+ templatePath = path.join(dir, templateInfo.config.template); // Path relative to root dir
117
+ }
118
+ if (templatePath && fs.existsSync(templatePath)) {
119
+ ux.stdout(`Applying template from: ${templatePath}`);
117
120
  await ApplyCommand.run([
118
- '--directusUrl=http://localhost:8055',
121
+ `--directusUrl=${directusInfo.url || 'http://localhost:8055'}`,
119
122
  '-p',
120
- '--userEmail=admin@example.com',
121
- '--userPassword=d1r3ctu5',
123
+ `--userEmail=${directusInfo.email}`,
124
+ `--userPassword=${directusInfo.password}`,
122
125
  `--templateLocation=${templatePath}`,
123
126
  ]);
124
127
  }
125
- catch (error) {
126
- ux.error('Failed to start Directus containers or apply template');
127
- throw error;
128
+ else {
129
+ ux.stdout('Skipping backend template application.');
128
130
  }
129
131
  }
132
+ // Detect package manager even if not installing dependencies
133
+ packageManager = await detectPackageManager(frontendDir);
130
134
  // Install dependencies if requested
131
135
  if (flags.installDeps) {
132
136
  const s = spinner();
133
137
  s.start('Installing dependencies');
134
138
  try {
135
139
  if (fs.existsSync(frontendDir)) {
136
- packageManager = await detectPackageManager(frontendDir);
137
140
  await installDependencies({
138
141
  cwd: frontendDir,
139
142
  packageManager,
@@ -156,14 +159,18 @@ export async function init({ dir, flags }) {
156
159
  }
157
160
  // Finishing up
158
161
  const relativeDir = path.relative(process.cwd(), dir);
159
- 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`;
160
- const frontendText = flags.frontend ? `- To start the frontend, run ${chalk.cyan(`cd ${flags.frontend}`)} and then ${chalk.cyan(`${packageManager?.name} run dev`)}. \n` : '';
161
- const projectText = `- Navigate to your project directory using ${chalk.cyan(`cd ${relativeDir}`)}. \n`;
162
+ const directusUrl = directusInfo.url ?? 'http://localhost:8055';
163
+ const directusText = `- Directus is running on ${directusUrl}. \n`;
164
+ const directusLoginText = `- You can login with the email: ${pinkText(directusInfo.email)} and password: ${pinkText(directusInfo.password)}. \n`;
165
+ const frontendText = flags.frontend ? `- To start the frontend, run ${pinkText(`cd ${flags.frontend}`)} and then ${pinkText(`${packageManager?.name} run dev`)}. \n` : '';
166
+ const projectText = `- Navigate to your project directory using ${pinkText(`cd ${relativeDir}`)}. \n`;
162
167
  const readmeText = '- Review the \`./README.md\` file for more information and next steps.';
163
- const nextSteps = chalk.white(`${directusText}${projectText}${frontendText}${readmeText}`);
168
+ const nextSteps = `${directusText}${directusLoginText}${projectText}${frontendText}${readmeText}`;
164
169
  note(nextSteps, 'Next Steps');
165
- clackLog.warn(BSL_LICENSE_TEXT);
166
- outro(`Problems or questions? Hop into the community on Discord at ${chalk.underline(chalk.cyan('https://directus.chat'))}`);
170
+ clackLog.warn(BSL_LICENSE_HEADLINE);
171
+ clackLog.info(BSL_LICENSE_TEXT);
172
+ clackLog.info(BSL_LICENSE_CTA);
173
+ outro(`Problems or questions? Hop into the community at ${pinkText('https://directus.chat')}`);
167
174
  }
168
175
  catch (error) {
169
176
  catchError(error, {
@@ -15,7 +15,8 @@ export interface ApplyFlags {
15
15
  templateType: 'community' | 'github' | 'local';
16
16
  userEmail: string;
17
17
  userPassword: string;
18
- users: boolean;
18
+ users?: boolean;
19
+ disableTelemetry?: boolean;
19
20
  }
20
21
  export declare const loadFlags: readonly ["content", "dashboards", "extensions", "files", "flows", "permissions", "schema", "settings", "users"];
21
22
  export declare function validateProgrammaticFlags(flags: ApplyFlags): ApplyFlags;
package/dist/lib/sdk.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { authentication, createDirectus, rest } from '@directus/sdk';
2
2
  import { ux } from '@oclif/core';
3
3
  import Bottleneck from 'bottleneck';
4
+ function log(message) {
5
+ ux.stdout(`${ux.colorize('dim', '--')} ${message}`);
6
+ }
4
7
  export class DirectusError extends Error {
5
8
  errors;
6
9
  headers;
@@ -54,38 +57,44 @@ class Api {
54
57
  retryCount: 3, // Retry a maximum of 3 times
55
58
  });
56
59
  this.limiter.on('failed', async (error, jobInfo) => {
60
+ // @ts-ignore
61
+ if (error instanceof TypeError && error.message === 'fetch failed' && error.cause?.code === 'ECONNREFUSED') {
62
+ log(`Connection refused. Please check the Directus URL and ensure the server is running. Not retrying. ${error.message}`);
63
+ return;
64
+ }
57
65
  if (error instanceof DirectusError) {
58
66
  const retryAfter = error.headers?.get('Retry-After');
59
67
  const statusCode = error.status;
68
+ // If the status code is 400 or 401, we don't want to retry
69
+ if (statusCode === 400 || statusCode === 401) {
70
+ log(`Request failed with status ${statusCode}. Not retrying. ${error.message}`);
71
+ return;
72
+ }
60
73
  if (statusCode === 429) {
61
74
  const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 60_000;
62
- ux.stdout(`${ux.colorize('dim', '--')} Rate limited. Retrying after ${delay}ms`);
75
+ log(`Rate limited. Retrying after ${delay}ms`);
63
76
  return delay;
64
77
  }
65
78
  if (statusCode === 503) {
66
79
  const delay = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : 5000;
67
- ux.stdout(`${ux.colorize('dim', '--')} Server under pressure. Retrying after ${delay}ms`);
80
+ log(`Server under pressure. Retrying after ${delay}ms`);
68
81
  return delay;
69
82
  }
70
- // If the status code is 400 or 401, we don't want to retry
71
- if (statusCode === 400 || statusCode === 401) {
72
- return;
73
- }
74
83
  }
75
84
  // For other errors, use exponential backoff, but only if we haven't exceeded retryCount
76
85
  if (jobInfo.retryCount < 3) {
77
86
  const delay = Math.min(1000 * 2 ** jobInfo.retryCount, 30_000);
78
- ux.stdout(`${ux.colorize('dim', '--')} Request failed. Retrying after ${delay}ms`);
87
+ log(`Request failed. Retrying after ${delay}ms`);
79
88
  return delay;
80
89
  }
81
- ux.stdout(`${ux.colorize('dim', '--')} Max retries reached, not retrying further`);
90
+ log('Max retries reached, not retrying further');
82
91
  });
83
92
  this.limiter.on('retry', (error, jobInfo) => {
84
- ux.stdout(`${ux.colorize('dim', '--')} Retrying job (attempt ${jobInfo.retryCount + 1})`);
93
+ log(`Retrying job (attempt ${jobInfo.retryCount + 1})`);
85
94
  });
86
95
  this.limiter.on('depleted', empty => {
87
96
  if (empty) {
88
- ux.stdout(`${ux.colorize('dim', '--')} Rate limit quota depleted. Requests will be queued.`);
97
+ log('Rate limit quota depleted. Requests will be queued.');
89
98
  }
90
99
  });
91
100
  }
@@ -5,7 +5,7 @@ export interface DirectusTemplateFrontend {
5
5
  export interface DirectusTemplateConfig {
6
6
  name: string;
7
7
  description: string;
8
- template: string;
8
+ template: string | null;
9
9
  frontends: {
10
10
  [key: string]: DirectusTemplateFrontend;
11
11
  };
@@ -15,6 +15,10 @@ export declare function getDirectusUrl(): Promise<string>;
15
15
  * @returns The Directus token
16
16
  */
17
17
  export declare function getDirectusToken(directusUrl: string): Promise<string>;
18
+ export declare function getDirectusEmailAndPassword(): Promise<{
19
+ userEmail: string;
20
+ userPassword: string;
21
+ }>;
18
22
  /**
19
23
  * Initialize the Directus API with the provided flags and log in the user
20
24
  * @param flags - The validated ApplyFlags
@@ -1,22 +1,27 @@
1
1
  import { readMe } from '@directus/sdk';
2
- import { text, log, isCancel } from '@clack/prompts';
2
+ import { text, log, isCancel, password } 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';
6
6
  import validateUrl from './validate-url.js';
7
+ import { DEFAULT_DIRECTUS_URL } from '../../lib/constants.js';
7
8
  /**
8
9
  * Get the Directus URL from the user
9
10
  * @returns The Directus URL
10
11
  */
11
12
  export async function getDirectusUrl() {
12
13
  const directusUrl = await text({
13
- placeholder: 'http://localhost:8055',
14
+ placeholder: DEFAULT_DIRECTUS_URL,
14
15
  message: 'What is your Directus URL?',
15
16
  });
16
17
  if (isCancel(directusUrl)) {
17
18
  log.info('Exiting...');
18
19
  ux.exit(0);
19
20
  }
21
+ if (!directusUrl) {
22
+ ux.warn(`No URL provided, using default: ${DEFAULT_DIRECTUS_URL}`);
23
+ return DEFAULT_DIRECTUS_URL;
24
+ }
20
25
  // Validate URL
21
26
  if (!validateUrl(directusUrl)) {
22
27
  ux.warn('Invalid URL');
@@ -56,6 +61,33 @@ export async function getDirectusToken(directusUrl) {
56
61
  return getDirectusToken(directusUrl);
57
62
  }
58
63
  }
64
+ export async function getDirectusEmailAndPassword() {
65
+ const userEmail = await text({
66
+ message: 'What is your email?',
67
+ validate(value) {
68
+ if (!value) {
69
+ return 'Email is required';
70
+ }
71
+ },
72
+ });
73
+ if (isCancel(userEmail)) {
74
+ log.info('Exiting...');
75
+ ux.exit(0);
76
+ }
77
+ const userPassword = await password({
78
+ message: 'What is your password?',
79
+ validate(value) {
80
+ if (!value) {
81
+ return 'Password is required';
82
+ }
83
+ },
84
+ });
85
+ if (isCancel(userPassword)) {
86
+ log.info('Exiting...');
87
+ ux.exit(0);
88
+ }
89
+ return { userEmail, userPassword };
90
+ }
59
91
  /**
60
92
  * Initialize the Directus API with the provided flags and log in the user
61
93
  * @param flags - The validated ApplyFlags
@@ -3,7 +3,7 @@
3
3
  */
4
4
  interface ErrorHandlerOptions {
5
5
  /** Additional context to be included in the error log. */
6
- context?: Record<string, any>;
6
+ context?: Record<string, unknown>;
7
7
  /** If true, the error will be treated as fatal and the process will exit. */
8
8
  fatal?: boolean;
9
9
  /** If true, the error will be logged to a file. */
@@ -1,6 +1,8 @@
1
1
  import { ux } from '@oclif/core';
2
2
  import { DirectusError } from '../sdk.js';
3
3
  import { logger } from '../utils/logger.js';
4
+ import { captureException } from '../../services/posthog.js';
5
+ import { getExecutionContext } from '../../services/execution-context.js';
4
6
  /**
5
7
  * Handles errors by formatting them and optionally logging to console and file.
6
8
  * @param error - The error to be handled.
@@ -9,6 +11,7 @@ import { logger } from '../utils/logger.js';
9
11
  */
10
12
  export default function catchError(error, options = {}) {
11
13
  const { context = {}, fatal = false, logToFile = true } = options;
14
+ const { distinctId, disableTelemetry } = getExecutionContext();
12
15
  let errorMessage;
13
16
  if (error instanceof DirectusError) {
14
17
  errorMessage = error.message;
@@ -19,6 +22,10 @@ export default function catchError(error, options = {}) {
19
22
  else {
20
23
  errorMessage = `Unknown error: ${JSON.stringify(error)}`;
21
24
  }
25
+ // Capture exception before logging/exiting
26
+ if (!disableTelemetry && distinctId) {
27
+ captureException({ error, distinctId, properties: { context } });
28
+ }
22
29
  // Format the error message with context if provided
23
30
  const formattedMessage = [
24
31
  errorMessage,
@@ -39,30 +39,59 @@ export function parseGitHubUrl(url) {
39
39
  if (!url) {
40
40
  throw new Error('URL is required');
41
41
  }
42
- // Clean the URL first
43
42
  const cleanedUrl = cleanGitHubUrl(url);
44
- // Handle full GitHub URLs
45
43
  if (cleanedUrl.includes('github.com')) {
46
44
  try {
47
45
  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');
46
+ const pathParts = parsed.pathname.split('/').filter(Boolean);
47
+ if (pathParts.length < 2) {
48
+ throw new Error('Invalid GitHub URL format: Needs owner and repo.');
51
49
  }
52
- const [owner, repo, ...rest] = parts;
53
- const path = rest.length > 0 ? rest.join('/') : undefined;
54
- const ref = parsed.searchParams.get('ref') || DEFAULT_BRANCH;
50
+ const owner = pathParts[0];
51
+ const repo = pathParts[1];
52
+ let ref = DEFAULT_BRANCH; // Default ref
53
+ let path;
54
+ // Check for /tree/ref/ or /blob/ref/ patterns
55
+ const treeIndex = pathParts.indexOf('tree');
56
+ const blobIndex = pathParts.indexOf('blob');
57
+ let refIndex = -1;
58
+ if (treeIndex > 1 && treeIndex + 1 < pathParts.length) {
59
+ refIndex = treeIndex + 1;
60
+ }
61
+ else if (blobIndex > 1 && blobIndex + 1 < pathParts.length) {
62
+ refIndex = blobIndex + 1;
63
+ }
64
+ if (refIndex !== -1) {
65
+ ref = pathParts[refIndex];
66
+ // Path is everything after the ref
67
+ path = pathParts.slice(refIndex + 1).join('/') || undefined;
68
+ }
69
+ else if (pathParts.length > 2) {
70
+ // If no tree/blob, but more parts exist, assume it's part of the path
71
+ // This handles cases like github.com/owner/repo/some/path without a specific ref marker
72
+ path = pathParts.slice(2).join('/') || undefined;
73
+ // If URL has an explicit ?ref= param, use that, otherwise keep default
74
+ ref = parsed.searchParams.get('ref') || ref;
75
+ }
76
+ else {
77
+ // No path, just owner/repo
78
+ ref = parsed.searchParams.get('ref') || ref;
79
+ }
80
+ // Ensure path is undefined if empty string
81
+ if (path === '')
82
+ path = undefined;
55
83
  return { owner, repo, path, ref };
56
84
  }
57
85
  catch (error) {
58
- throw new Error(`Invalid GitHub URL: ${url}`);
86
+ throw new Error(`Invalid GitHub URL: ${url}. Error: ${error.message}`);
59
87
  }
60
88
  }
61
- // Handle repository paths (owner/repo format)
89
+ // Handle repository paths (owner/repo/path format) without github.com
62
90
  const parts = cleanedUrl.split('/').filter(Boolean);
63
91
  if (parts.length >= 2) {
64
92
  const [owner, repo, ...rest] = parts;
65
93
  const path = rest.length > 0 ? rest.join('/') : undefined;
94
+ // Assume default branch for simple paths unless we add ref detection here too
66
95
  return { owner, repo, path, ref: DEFAULT_BRANCH };
67
96
  }
68
97
  // Handle simple template names using DEFAULT_REPO
@@ -1,4 +1,4 @@
1
- import { spinner } from '@clack/prompts';
1
+ import { spinner, log } from '@clack/prompts';
2
2
  import { execa } from 'execa';
3
3
  import net from 'node:net';
4
4
  import { ux } from '@oclif/core';
@@ -89,23 +89,80 @@ async function checkDocker() {
89
89
  }
90
90
  }
91
91
  /**
92
- * Start Docker containers using docker-compose
92
+ * Get the list of image names defined in the docker-compose file
93
+ * @param {string} cwd - The current working directory
94
+ * @returns {Promise<string[]>} - A list of image names
95
+ */
96
+ async function getRequiredImagesFromCompose(cwd) {
97
+ try {
98
+ const { stdout } = await execa('docker', ['compose', 'config', '--images'], { cwd });
99
+ // stdout contains a list of image names, one per line
100
+ return stdout.split('\n').filter(img => img.trim() !== ''); // Filter out empty lines
101
+ }
102
+ catch (error) {
103
+ // Handle potential errors, e.g., compose file not found or invalid
104
+ log.error('Failed to get images from docker-compose file.');
105
+ catchError(error, {
106
+ context: { cwd, function: 'getRequiredImagesFromCompose' },
107
+ fatal: false, // Don't necessarily exit, maybe let startContainers handle it
108
+ logToFile: true,
109
+ });
110
+ return []; // Return empty list on error
111
+ }
112
+ }
113
+ /**
114
+ * Check if a list of Docker images exist locally
115
+ * @param {string[]} imageNames - An array of Docker image names (e.g., "postgres:16")
116
+ * @returns {Promise<boolean>} - True if all images exist locally, false otherwise
117
+ */
118
+ async function checkImagesExist(imageNames) {
119
+ if (imageNames.length === 0) {
120
+ return true; // No images to check, technically they all "exist"
121
+ }
122
+ try {
123
+ // Use Promise.allSettled to check all images even if some commands fail
124
+ const results = await Promise.allSettled(imageNames.map(imageName => execa('docker', ['inspect', '--type=image', imageName])));
125
+ // Check if all inspect commands succeeded (exit code 0)
126
+ return results.every(result => result.status === 'fulfilled' && result.value.exitCode === 0);
127
+ }
128
+ catch (error) {
129
+ // This catch block might be redundant due to allSettled, but good for safety
130
+ log.error('Error checking for Docker images.');
131
+ catchError(error, {
132
+ context: { imageNames, function: 'checkImagesExist' },
133
+ fatal: false,
134
+ logToFile: true,
135
+ });
136
+ return false; // Assume images don't exist if there's an error checking
137
+ }
138
+ }
139
+ /**
140
+ * Start Docker containers using docker compose
93
141
  * @param {string} cwd - The current working directory
94
142
  * @returns {Promise<void>} - Returns nothing
95
143
  */
96
144
  async function startContainers(cwd) {
145
+ const s = spinner();
97
146
  try {
98
147
  // Check if required ports are available
99
148
  await checkRequiredPorts();
100
- const s = spinner();
101
- s.start('Starting Docker containers');
102
- return execa('docker-compose', ['up', '-d'], {
149
+ // Get required images from compose file
150
+ const requiredImages = await getRequiredImagesFromCompose(cwd);
151
+ const imagesExist = await checkImagesExist(requiredImages);
152
+ // Log a message if images need downloading
153
+ if (!imagesExist && requiredImages.length > 0) {
154
+ log.info('Required Docker image(s) are missing and will be downloaded.');
155
+ }
156
+ const startMessage = imagesExist || requiredImages.length === 0 ? 'Starting Docker containers...' : 'Downloading required Docker images...';
157
+ const endMessage = imagesExist || requiredImages.length === 0 ? 'Docker containers running!' : 'Docker images downloaded and containers started!';
158
+ s.start(startMessage); // Start spinner with the appropriate message
159
+ await execa('docker', ['compose', 'up', '-d'], {
103
160
  cwd,
104
- }).then(() => {
105
- s.stop('Docker containers running!');
106
161
  });
162
+ s.stop(endMessage); // Update spinner message on success
107
163
  }
108
164
  catch (error) {
165
+ s.stop('Error starting Docker containers.'); // Stop spinner on error
109
166
  catchError(error, {
110
167
  context: { cwd, function: 'startContainers' },
111
168
  fatal: true,
@@ -115,13 +172,13 @@ async function startContainers(cwd) {
115
172
  }
116
173
  }
117
174
  /**
118
- * Stop Docker containers
175
+ * Stop Docker containers using docker compose
119
176
  * @param {string} cwd - The current working directory
120
177
  * @returns {Promise<void>} - Returns nothing
121
178
  */
122
179
  async function stopContainers(cwd) {
123
180
  try {
124
- return execa('docker-compose', ['down'], {
181
+ return execa('docker', ['compose', 'down'], {
125
182
  cwd,
126
183
  }).then(() => { });
127
184
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Defines the structure for the execution context, holding telemetry information.
3
+ */
4
+ export interface ExecutionContext {
5
+ distinctId?: string;
6
+ disableTelemetry?: boolean;
7
+ }
8
+ /**
9
+ * Sets the global execution context.
10
+ * This should be called early in the command lifecycle.
11
+ * @param context The context object containing distinctId and disableTelemetry status.
12
+ */
13
+ export declare function setExecutionContext(context: ExecutionContext): void;
14
+ /**
15
+ * Gets the currently set global execution context.
16
+ * @returns The current execution context.
17
+ */
18
+ export declare function getExecutionContext(): ExecutionContext;
@@ -0,0 +1,20 @@
1
+ // Module-level variable to hold the current context.
2
+ // Initialize with default values (telemetry enabled, no distinctId yet).
3
+ let currentContext = {
4
+ disableTelemetry: false,
5
+ };
6
+ /**
7
+ * Sets the global execution context.
8
+ * This should be called early in the command lifecycle.
9
+ * @param context The context object containing distinctId and disableTelemetry status.
10
+ */
11
+ export function setExecutionContext(context) {
12
+ currentContext = context;
13
+ }
14
+ /**
15
+ * Gets the currently set global execution context.
16
+ * @returns The current execution context.
17
+ */
18
+ export function getExecutionContext() {
19
+ return currentContext;
20
+ }
@@ -5,14 +5,19 @@ interface GitHubUrlParts {
5
5
  ref?: string;
6
6
  repo: string;
7
7
  }
8
+ export interface TemplateInfo {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ }
8
13
  export interface GitHubService {
9
14
  getTemplateDirectories(template: string, customUrl?: string): Promise<string[]>;
10
- getTemplates(customUrl?: string): Promise<string[]>;
15
+ getTemplates(customUrl?: string): Promise<TemplateInfo[]>;
11
16
  parseGitHubUrl(url: string): GitHubUrlParts;
12
17
  }
13
18
  export declare function createGitHub(token?: string): {
14
19
  getTemplateDirectories: (template: string, customUrl?: string) => Promise<string[]>;
15
- getTemplates: (customUrl?: string) => Promise<string[]>;
20
+ getTemplates: (customUrl?: string) => Promise<TemplateInfo[]>;
16
21
  parseGitHubUrl: typeof parseGitHubUrl;
17
22
  };
18
23
  export {};