directus-template-cli 0.7.0-beta.13 → 0.7.0-beta.14

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.
@@ -3,7 +3,7 @@ export interface InitFlags {
3
3
  frontend?: string;
4
4
  gitInit?: boolean;
5
5
  installDeps?: boolean;
6
- overrideDir?: boolean;
6
+ overwriteDir?: boolean;
7
7
  template?: string;
8
8
  disableTelemetry?: boolean;
9
9
  }
@@ -22,7 +22,7 @@ export default class InitCommand extends BaseCommand {
22
22
  frontend: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
23
23
  gitInit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
24
  installDeps: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
- overrideDir: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ overwriteDir: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
26
  template: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
27
  disableTelemetry: import("@oclif/core/interfaces").BooleanFlag<boolean>;
28
28
  };
@@ -45,7 +45,9 @@ export default class InitCommand extends BaseCommand {
45
45
  default: true,
46
46
  description: 'Install dependencies automatically',
47
47
  }),
48
- overrideDir: Flags.boolean({
48
+ overwriteDir: Flags.boolean({
49
+ aliases: ['overwrite-dir'],
50
+ allowNo: true,
49
51
  default: false,
50
52
  description: 'Override the default directory',
51
53
  }),
@@ -96,28 +98,31 @@ export default class InitCommand extends BaseCommand {
96
98
  }
97
99
  this.targetDir = dirResponse;
98
100
  }
99
- if (fs.existsSync(this.targetDir) && !flags.overrideDir) {
100
- const overrideDirResponse = await confirm({
101
+ if (fs.existsSync(this.targetDir) && !flags.overwriteDir) {
102
+ const overwriteDirResponse = await confirm({
101
103
  message: 'Directory already exists. Would you like to overwrite it?',
104
+ initialValue: false,
102
105
  });
103
- if (isCancel(overrideDirResponse)) {
106
+ if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
104
107
  cancel('Project creation cancelled.');
105
108
  process.exit(0);
106
109
  }
107
- if (overrideDirResponse) {
108
- flags.overrideDir = true;
110
+ if (overwriteDirResponse) {
111
+ flags.overwriteDir = true;
109
112
  }
110
113
  }
111
- // 1. Fetch available templates
114
+ // 1. Fetch available templates (now returns Array<{id: string, name: string, description?: string}>)
112
115
  const availableTemplates = await github.getTemplates();
113
116
  // 2. Prompt for template if not provided
114
- let { template } = flags;
117
+ let { template } = flags; // This will store the chosen template ID
118
+ let chosenTemplateObject;
115
119
  if (!template) {
116
120
  const templateResponse = await select({
117
121
  message: 'Which Directus backend template would you like to use?',
118
- options: availableTemplates.map(template => ({
119
- label: template,
120
- value: template,
122
+ options: availableTemplates.map(tmpl => ({
123
+ value: tmpl.id, // The value submitted will be the ID (directory name)
124
+ label: tmpl.name, // Display the friendly name
125
+ hint: tmpl.description, // Show the description as a hint
121
126
  })),
122
127
  });
123
128
  if (isCancel(templateResponse)) {
@@ -126,22 +131,25 @@ export default class InitCommand extends BaseCommand {
126
131
  }
127
132
  template = templateResponse;
128
133
  }
129
- // 3. Validate that the template exists, fetch subdirectories
130
- let directories = await github.getTemplateDirectories(template);
134
+ // Find the chosen template object for potential future use (e.g., display name later)
135
+ chosenTemplateObject = availableTemplates.find(t => t.id === template);
136
+ // 3. Validate that the template exists in the available list
131
137
  const isDirectUrl = template?.startsWith('http');
132
- while (!isDirectUrl && directories.length === 0) {
133
- this.log(`Template "${template}" doesn't seem to exist in directus-labs/directus-starters.`);
138
+ // Validate against the 'id' property of the template objects
139
+ while (!isDirectUrl && !availableTemplates.some(t => t.id === template)) {
140
+ // Keep the warning message simple or refer back to the list shown in the prompt
141
+ clackLog.warn(`Template ID "${template}" is not valid. Please choose from the list provided or enter a direct GitHub URL.`);
134
142
  const templateNameResponse = await text({
135
- message: 'Please enter a valid template name, or Ctrl+C to cancel:',
143
+ message: 'Please enter a valid template ID, a direct GitHub URL, or Ctrl+C to cancel:',
136
144
  });
137
145
  if (isCancel(templateNameResponse)) {
138
146
  cancel('Project creation cancelled.');
139
147
  process.exit(0);
140
148
  }
141
149
  template = templateNameResponse;
142
- directories = await github.getTemplateDirectories(template);
150
+ chosenTemplateObject = availableTemplates.find(t => t.id === template); // Update chosen object after re-entry
143
151
  }
144
- flags.template = template;
152
+ flags.template = template; // Ensure the flag stores the ID
145
153
  // Download the template to a temporary directory to read its configuration
146
154
  const tempDir = path.join(os.tmpdir(), `directus-template-${Date.now()}`);
147
155
  let chosenFrontend = flags.frontend;
@@ -220,7 +228,7 @@ export default class InitCommand extends BaseCommand {
220
228
  gitInit: initGit,
221
229
  installDeps,
222
230
  template,
223
- overrideDir: flags.overrideDir,
231
+ overwriteDir: flags.overwriteDir,
224
232
  },
225
233
  });
226
234
  // Track the command completion unless telemetry is disabled
@@ -234,7 +242,7 @@ export default class InitCommand extends BaseCommand {
234
242
  gitInit: initGit,
235
243
  installDeps,
236
244
  template,
237
- overrideDir: flags.overrideDir,
245
+ overwriteDir: flags.overwriteDir,
238
246
  },
239
247
  runId: this.runId,
240
248
  config: this.config,
@@ -18,4 +18,7 @@ export declare const POSTHOG_PUBLIC_KEY = "phc_STopE6gj6LDIjYonVF7493kQJK8S4v0Xr
18
18
  export declare const POSTHOG_HOST = "https://us.i.posthog.com";
19
19
  export declare const DEFAULT_BRANCH = "main";
20
20
  export declare const BSL_LICENSE_URL = "https://directus.io/bsl";
21
- export declare const BSL_LICENSE_TEXT: string;
21
+ export declare const BSL_EMAIL = "licensing@directus.io";
22
+ export declare 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.";
23
+ export declare 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.";
24
+ export declare const BSL_LICENSE_CTA: string;
@@ -1,5 +1,4 @@
1
1
  import chalk from 'chalk';
2
- import terminalLink from 'terminal-link';
3
2
  export const DIRECTUS_PURPLE = '#6644ff';
4
3
  export const DIRECTUS_PINK = '#FF99DD';
5
4
  export const SEPARATOR = '------------------';
@@ -20,6 +19,7 @@ export const POSTHOG_PUBLIC_KEY = 'phc_STopE6gj6LDIjYonVF7493kQJK8S4v0Xrl6YPr2z9
20
19
  export const POSTHOG_HOST = 'https://us.i.posthog.com';
21
20
  export const DEFAULT_BRANCH = 'main';
22
21
  export const BSL_LICENSE_URL = 'https://directus.io/bsl';
23
- const BSL_LINK = terminalLink(BSL_LICENSE_URL, BSL_LICENSE_URL);
24
- const BSL_MAILTO = terminalLink('sales-demo-with-evil-sales@directus.io', 'mailto:sales-demo-with-evil-sales@directus.io');
25
- 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.\nFor 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. \nDirectus is licensed under BSL1.1. Visit ${pinkText(BSL_LINK)} for more information or reach out to us at ${pinkText(BSL_MAILTO)}.`;
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)}.`;
@@ -7,19 +7,18 @@ import fs from 'node:fs';
7
7
  import { detectPackageManager, installDependencies } from 'nypm';
8
8
  import path from 'pathe';
9
9
  import dotenv from 'dotenv';
10
- import terminalLink from 'terminal-link';
11
10
  import ApplyCommand from '../../commands/apply.js';
12
11
  import { createDocker } from '../../services/docker.js';
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, pinkText } 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');
@@ -111,23 +110,33 @@ export async function init({ dir, flags }) {
111
110
  if (!isHealthy) {
112
111
  throw new Error('Directus failed to become healthy');
113
112
  }
114
- const templatePath = path.join(directusDir, 'template');
115
- ux.stdout(`Attempting to apply template from: ${templatePath}`);
116
- await ApplyCommand.run([
117
- '--directusUrl=http://localhost:8055',
118
- '-p',
119
- '--userEmail=admin@example.com',
120
- '--userPassword=d1r3ctu5',
121
- `--templateLocation=${templatePath}`,
122
- ]);
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}`);
120
+ await ApplyCommand.run([
121
+ `--directusUrl=${directusInfo.url || 'http://localhost:8055'}`,
122
+ '-p',
123
+ `--userEmail=${directusInfo.email}`,
124
+ `--userPassword=${directusInfo.password}`,
125
+ `--templateLocation=${templatePath}`,
126
+ ]);
127
+ }
128
+ else {
129
+ ux.stdout('Skipping backend template application.');
130
+ }
123
131
  }
132
+ // Detect package manager even if not installing dependencies
133
+ packageManager = await detectPackageManager(frontendDir);
124
134
  // Install dependencies if requested
125
135
  if (flags.installDeps) {
126
136
  const s = spinner();
127
137
  s.start('Installing dependencies');
128
138
  try {
129
139
  if (fs.existsSync(frontendDir)) {
130
- packageManager = await detectPackageManager(frontendDir);
131
140
  await installDependencies({
132
141
  cwd: frontendDir,
133
142
  packageManager,
@@ -151,14 +160,17 @@ export async function init({ dir, flags }) {
151
160
  // Finishing up
152
161
  const relativeDir = path.relative(process.cwd(), dir);
153
162
  const directusUrl = directusInfo.url ?? 'http://localhost:8055';
154
- const directusText = `- Directus is running on ${terminalLink(directusUrl, directusUrl)}. You can login with the email: ${pinkText(directusInfo.email)} and password: ${pinkText(directusInfo.password)}. \n`;
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`;
155
165
  const frontendText = flags.frontend ? `- To start the frontend, run ${pinkText(`cd ${flags.frontend}`)} and then ${pinkText(`${packageManager?.name} run dev`)}. \n` : '';
156
166
  const projectText = `- Navigate to your project directory using ${pinkText(`cd ${relativeDir}`)}. \n`;
157
167
  const readmeText = '- Review the \`./README.md\` file for more information and next steps.';
158
- const nextSteps = `${directusText}${projectText}${frontendText}${readmeText}`;
168
+ const nextSteps = `${directusText}${directusLoginText}${projectText}${frontendText}${readmeText}`;
159
169
  note(nextSteps, 'Next Steps');
160
- clackLog.warn(BSL_LICENSE_TEXT);
161
- outro(`Problems or questions? Hop into the community on Discord at ${pinkText(terminalLink('https://directus.chat', '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')}`);
162
174
  }
163
175
  catch (error) {
164
176
  catchError(error, {
@@ -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
  };
@@ -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';
@@ -88,24 +88,81 @@ async function checkDocker() {
88
88
  };
89
89
  }
90
90
  }
91
+ /**
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
+ }
91
139
  /**
92
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,
@@ -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 {};
@@ -1,6 +1,7 @@
1
1
  import { Octokit } from '@octokit/rest';
2
2
  import { DEFAULT_REPO } from '../lib/constants.js';
3
3
  import { parseGitHubUrl } from '../lib/utils/parse-github-url.js';
4
+ import { ux } from '@oclif/core';
4
5
  export function createGitHub(token) {
5
6
  const octokit = new Octokit({
6
7
  auth: token,
@@ -58,27 +59,112 @@ export function createGitHub(token) {
58
59
  }
59
60
  }
60
61
  /**
61
- * Get the templates for a repository.
62
- * @param customUrl - The custom URL to get the templates for.
63
- * @returns The templates for the repository.
62
+ * Get the templates for a repository, including name and description from package.json.
63
+ * Ensures 'blank' template appears last if found.
64
+ * If a direct URL to a template directory is provided, attempt to fetch its package.json.
65
+ * @param customUrl - The custom URL or base repository URL to get the templates for.
66
+ * @returns The templates for the repository with details, sorted.
64
67
  */
65
68
  async function getTemplates(customUrl) {
66
- // If customUrl is provided and it's a full repository URL, return it as the only template
69
+ // Handle direct URLs pointing to a specific template directory
67
70
  if (customUrl?.startsWith('http')) {
68
- return [customUrl];
71
+ const parsed = parseGitHubUrl(customUrl);
72
+ let name = parsed.path?.split('/').pop() || parsed.repo;
73
+ let description;
74
+ const packageJsonPath = joinPath(parsed.path || '', 'package.json');
75
+ try {
76
+ const { data: packageJsonContent } = await octokit.rest.repos.getContent({
77
+ owner: parsed.owner,
78
+ repo: parsed.repo,
79
+ path: packageJsonPath,
80
+ ref: parsed.ref,
81
+ mediaType: {
82
+ format: 'raw',
83
+ },
84
+ });
85
+ // getContent with mediaType: raw returns string directly
86
+ if (typeof packageJsonContent === 'string') {
87
+ const packageJson = JSON.parse(packageJsonContent);
88
+ const templateConfig = packageJson?.['directus:template'];
89
+ if (templateConfig?.name) {
90
+ name = templateConfig.name;
91
+ }
92
+ if (templateConfig?.description) {
93
+ description = templateConfig.description;
94
+ }
95
+ }
96
+ }
97
+ catch (error) {
98
+ // If package.json is missing or fails to parse, just use the default name derived from the URL.
99
+ // Don't warn here as it might be expected that a direct URL doesn't have this structure.
100
+ if (error.status !== 404) {
101
+ // Log other errors if needed for debugging, but don't show to user unless verbose?
102
+ console.error(`Error fetching package.json for direct URL ${customUrl}: ${error.message}`);
103
+ }
104
+ }
105
+ // Return a single item array for the direct URL case
106
+ return [{ id: customUrl, name, description }];
69
107
  }
70
108
  const repo = customUrl ? parseGitHubUrl(customUrl) : DEFAULT_REPO;
71
- const { data } = await octokit.rest.repos.getContent({
109
+ const { data: rootContent } = await octokit.rest.repos.getContent({
72
110
  owner: repo.owner,
73
111
  path: repo.path || '',
74
112
  ref: repo.ref,
75
113
  repo: repo.repo,
76
114
  });
77
- if (!Array.isArray(data))
115
+ if (!Array.isArray(rootContent))
78
116
  return [];
79
- return data
80
- .filter(item => item.type === 'dir')
81
- .map(item => item.name);
117
+ const directories = rootContent.filter(item => item.type === 'dir');
118
+ // Fetch package.json for each directory concurrently
119
+ let templateInfos = await Promise.all(directories.map(async (dir) => {
120
+ const packageJsonPath = joinPath(repo.path || '', dir.path, 'package.json');
121
+ let name = dir.name;
122
+ let description;
123
+ try {
124
+ const { data: packageJsonContent } = await octokit.rest.repos.getContent({
125
+ owner: repo.owner,
126
+ repo: repo.repo,
127
+ path: packageJsonPath,
128
+ ref: repo.ref,
129
+ mediaType: {
130
+ format: 'raw',
131
+ },
132
+ });
133
+ // getContent with mediaType: raw returns string directly
134
+ if (typeof packageJsonContent === 'string') {
135
+ const packageJson = JSON.parse(packageJsonContent);
136
+ const templateConfig = packageJson?.['directus:template'];
137
+ if (templateConfig?.name) {
138
+ name = templateConfig.name;
139
+ }
140
+ if (templateConfig?.description) {
141
+ description = templateConfig.description;
142
+ }
143
+ }
144
+ }
145
+ catch (error) {
146
+ // Handle cases where package.json is missing or fails to parse
147
+ if (error.status !== 404) {
148
+ ux.warn(`Could not fetch or parse package.json for template "${dir.name}": ${error.message}`);
149
+ }
150
+ }
151
+ return {
152
+ id: dir.name,
153
+ name,
154
+ description,
155
+ };
156
+ }));
157
+ // Sort the templates to put "blank" last
158
+ templateInfos.sort((a, b) => {
159
+ const aIsBlank = a.id.toLowerCase() === 'blank' || a.name.toLowerCase() === 'blank';
160
+ const bIsBlank = b.id.toLowerCase() === 'blank' || b.name.toLowerCase() === 'blank';
161
+ if (aIsBlank && !bIsBlank)
162
+ return 1; // a comes AFTER b
163
+ if (!aIsBlank && bIsBlank)
164
+ return -1; // a comes BEFORE b
165
+ return a.name.localeCompare(b.name);
166
+ });
167
+ return templateInfos;
82
168
  }
83
169
  return {
84
170
  getTemplateDirectories,
@@ -86,3 +172,7 @@ export function createGitHub(token) {
86
172
  parseGitHubUrl,
87
173
  };
88
174
  }
175
+ // Helper functions
176
+ function joinPath(...segments) {
177
+ return segments.filter(Boolean).join('/');
178
+ }
@@ -341,10 +341,13 @@
341
341
  "allowNo": true,
342
342
  "type": "boolean"
343
343
  },
344
- "overrideDir": {
344
+ "overwriteDir": {
345
+ "aliases": [
346
+ "overwrite-dir"
347
+ ],
345
348
  "description": "Override the default directory",
346
- "name": "overrideDir",
347
- "allowNo": false,
349
+ "name": "overwriteDir",
350
+ "allowNo": true,
348
351
  "type": "boolean"
349
352
  },
350
353
  "template": {
@@ -378,5 +381,5 @@
378
381
  ]
379
382
  }
380
383
  },
381
- "version": "0.7.0-beta.13"
384
+ "version": "0.7.0-beta.14"
382
385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus-template-cli",
3
- "version": "0.7.0-beta.13",
3
+ "version": "0.7.0-beta.14",
4
4
  "description": "CLI Utility for applying templates to a Directus instance.",
5
5
  "author": "bryantgillespie @bryantgillespie",
6
6
  "type": "module",
@@ -38,8 +38,7 @@
38
38
  "log-update": "^6.1.0",
39
39
  "nypm": "^0.6.0",
40
40
  "pathe": "^2.0.3",
41
- "posthog-node": "^4.10.1",
42
- "terminal-link": "^4.0.0"
41
+ "posthog-node": "^4.10.1"
43
42
  },
44
43
  "devDependencies": {
45
44
  "@directus/types": "^13.0.0",