directus-template-cli 0.7.7 → 0.7.8

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.
@@ -45,9 +45,10 @@ export default class ApplyCommand extends BaseCommand {
45
45
  private runProgrammatic;
46
46
  /**
47
47
  * INTERACTIVE
48
- * Select a local template from the given directory
49
- * @param localTemplateDir - The local template directory path
48
+ * Select a template from the given GitHub repository
49
+ * @param ghTemplateUrl - The GitHub repository URL
50
50
  * @returns {Promise<Template>} - Returns the selected template
51
51
  */
52
+ private selectGithubTemplate;
52
53
  private selectLocalTemplate;
53
54
  }
@@ -9,7 +9,7 @@ import apply from '../lib/load/index.js';
9
9
  import { animatedBunny } from '../lib/utils/animated-bunny.js';
10
10
  import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi } from '../lib/utils/auth.js';
11
11
  import catchError from '../lib/utils/catch-error.js';
12
- import { getCommunityTemplates, getGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate } from '../lib/utils/get-template.js';
12
+ import { getCommunityTemplates, getGithubTemplate, getInteractiveGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate } from '../lib/utils/get-template.js';
13
13
  import { logger } from '../lib/utils/logger.js';
14
14
  import openUrl from '../lib/utils/open-url.js';
15
15
  import { shutdown, track } from '../services/posthog.js';
@@ -138,7 +138,7 @@ export default class ApplyCommand extends BaseCommand {
138
138
  const ghTemplateUrl = await text({
139
139
  message: 'What is the public GitHub repository URL?',
140
140
  });
141
- template = await getGithubTemplate(ghTemplateUrl);
141
+ template = await this.selectGithubTemplate(ghTemplateUrl);
142
142
  break;
143
143
  }
144
144
  case 'local': {
@@ -303,10 +303,33 @@ export default class ApplyCommand extends BaseCommand {
303
303
  }
304
304
  /**
305
305
  * INTERACTIVE
306
- * Select a local template from the given directory
307
- * @param localTemplateDir - The local template directory path
306
+ * Select a template from the given GitHub repository
307
+ * @param ghTemplateUrl - The GitHub repository URL
308
308
  * @returns {Promise<Template>} - Returns the selected template
309
309
  */
310
+ async selectGithubTemplate(ghTemplateUrl) {
311
+ try {
312
+ const templates = await getInteractiveGithubTemplate(ghTemplateUrl);
313
+ if (templates.length === 1) {
314
+ return templates[0];
315
+ }
316
+ log.info('Multiple Directus templates found in this repository.');
317
+ const selectedTemplate = await select({
318
+ message: 'Select a template.',
319
+ options: templates.map(t => ({ label: t.templateName, value: t })),
320
+ });
321
+ return selectedTemplate;
322
+ }
323
+ catch (error) {
324
+ if (error instanceof Error) {
325
+ ux.error(error.message);
326
+ }
327
+ else {
328
+ ux.error('An unknown error occurred while getting the GitHub template.');
329
+ }
330
+ throw error;
331
+ }
332
+ }
310
333
  async selectLocalTemplate(localTemplateDir) {
311
334
  try {
312
335
  const templates = await getInteractiveLocalTemplate(localTemplateDir);
@@ -329,6 +352,7 @@ export default class ApplyCommand extends BaseCommand {
329
352
  else {
330
353
  ux.error('An unknown error occurred while getting the local template.');
331
354
  }
355
+ throw error;
332
356
  }
333
357
  }
334
358
  }
@@ -1,6 +1,6 @@
1
1
  import { readFiles, uploadFiles } from '@directus/sdk';
2
2
  import { ux } from '@oclif/core';
3
- import { FormData } from 'formdata-node';
3
+ import { File } from 'node:buffer';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import path from 'pathe';
6
6
  import { DIRECTUS_PINK } from '../constants.js';
@@ -17,9 +17,9 @@ export default async function loadFiles(dir) {
17
17
  fields: ['id', 'filename_disk'],
18
18
  limit: -1,
19
19
  }));
20
- const existingFileIds = new Set(existingFiles.map(file => file.id));
21
- const existingFileNames = new Set(existingFiles.map(file => file.filename_disk));
22
- const filesToUpload = files.filter(file => {
20
+ const existingFileIds = new Set(existingFiles.map((file) => file.id));
21
+ const existingFileNames = new Set(existingFiles.map((file) => file.filename_disk));
22
+ const filesToUpload = files.filter((file) => {
23
23
  if (existingFileIds.has(file.id)) {
24
24
  return false;
25
25
  }
@@ -31,7 +31,8 @@ export default async function loadFiles(dir) {
31
31
  await Promise.all(filesToUpload.map(async (asset) => {
32
32
  const fileName = asset.filename_disk;
33
33
  const assetPath = path.resolve(dir, 'assets', fileName);
34
- const fileStream = new Blob([readFileSync(assetPath)], { type: asset.type });
34
+ const mimeType = asset.type || 'application/octet-stream';
35
+ const file = new File([readFileSync(assetPath)], fileName, { type: mimeType });
35
36
  const form = new FormData();
36
37
  form.append('id', asset.id);
37
38
  if (asset.title)
@@ -40,9 +41,8 @@ export default async function loadFiles(dir) {
40
41
  form.append('description', asset.description);
41
42
  if (asset.folder)
42
43
  form.append('folder', asset.folder);
43
- if (asset.type)
44
- form.append('type', asset.type);
45
- form.append('file', fileStream, fileName);
44
+ form.append('type', mimeType);
45
+ form.append('file', file);
46
46
  try {
47
47
  await api.client.request(uploadFiles(form));
48
48
  }
@@ -6,4 +6,5 @@ export declare function getCommunityTemplates(): Promise<Template[]>;
6
6
  export declare function getLocalTemplate(localTemplateDir: string): Promise<Template>;
7
7
  export declare function getInteractiveLocalTemplate(localTemplateDir: string): Promise<Template[]>;
8
8
  export declare function getGithubTemplate(ghTemplateUrl: string): Promise<Template>;
9
+ export declare function getInteractiveGithubTemplate(ghTemplateUrl: string): Promise<Template[]>;
9
10
  export {};
@@ -3,9 +3,10 @@ import fs from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import path, { dirname } from 'pathe';
5
5
  import { COMMUNITY_TEMPLATE_REPO } from '../constants.js';
6
+ import { logger } from './logger.js';
6
7
  import resolvePathAndCheckExistence from './path.js';
7
8
  import { readAllTemplates, readTemplate } from './read-templates.js';
8
- import { transformGitHubUrl } from './transform-github-url.js';
9
+ import { parseGitHubUrl, transformGitHubUrl } from './transform-github-url.js';
9
10
  // Create __dirname equivalent for ESM
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
@@ -71,25 +72,74 @@ async function findNestedTemplates(dir, depth) {
71
72
  }
72
73
  return templates;
73
74
  }
75
+ async function downloadGithubTemplate(ghTemplateUrl) {
76
+ const ghString = transformGitHubUrl(ghTemplateUrl);
77
+ const downloadDir = resolvePathAndCheckExistence(path.join(__dirname, '..', 'downloads', 'github'), false);
78
+ if (!downloadDir) {
79
+ throw new Error(`Invalid download directory: ${path.join(__dirname, '..', 'downloads', 'github')}`);
80
+ }
81
+ const { dir } = await downloadTemplate(ghString, {
82
+ dir: downloadDir,
83
+ force: true,
84
+ forceClean: true,
85
+ });
86
+ const resolvedDir = resolvePathAndCheckExistence(dir);
87
+ if (!resolvedDir) {
88
+ throw new Error(`Downloaded template directory does not exist: ${dir}`);
89
+ }
90
+ return resolvedDir;
91
+ }
92
+ function buildSubpathUrl(ghTemplateUrl, templatePath) {
93
+ const { owner, ref, repo } = parseGitHubUrl(ghTemplateUrl);
94
+ const normalizedPath = templatePath.split(path.sep).join('/');
95
+ return `https://github.com/${owner}/${repo}/tree/${ref || 'HEAD'}/${normalizedPath}`;
96
+ }
97
+ function getErrorMessage(error) {
98
+ return error instanceof Error ? error.message : String(error);
99
+ }
74
100
  export async function getGithubTemplate(ghTemplateUrl) {
75
101
  try {
76
- const ghString = await transformGitHubUrl(ghTemplateUrl);
77
- const downloadDir = resolvePathAndCheckExistence(path.join(__dirname, '..', 'downloads', 'github'), false);
78
- if (!downloadDir) {
79
- throw new Error(`Invalid download directory: ${path.join(__dirname, '..', 'downloads', 'github')}`);
102
+ const resolvedDir = await downloadGithubTemplate(ghTemplateUrl);
103
+ const template = await readTemplate(resolvedDir);
104
+ if (template) {
105
+ return template;
80
106
  }
81
- const { dir } = await downloadTemplate(ghString, {
82
- dir: downloadDir,
83
- force: true,
84
- forceClean: true,
85
- });
86
- const resolvedDir = resolvePathAndCheckExistence(dir);
87
- if (!resolvedDir) {
88
- throw new Error(`Downloaded template directory does not exist: ${dir}`);
107
+ const nested = await findNestedTemplates(resolvedDir, 3);
108
+ if (nested.length === 1) {
109
+ const subpath = path.relative(resolvedDir, nested[0].directoryPath);
110
+ const pinnedUrl = buildSubpathUrl(ghTemplateUrl, subpath);
111
+ logger.log('warn', `Auto-selected nested template "${nested[0].templateName}" at ${subpath}. Pin --templateLocation="${pinnedUrl}" to avoid ambiguity if more templates are added.`);
112
+ return nested[0];
113
+ }
114
+ if (nested.length > 1) {
115
+ const list = nested
116
+ .map(t => {
117
+ const subpath = path.relative(resolvedDir, t.directoryPath);
118
+ return ` --templateLocation="${buildSubpathUrl(ghTemplateUrl, subpath)}" # ${t.templateName}`;
119
+ })
120
+ .join('\n');
121
+ throw new Error(`Found multiple Directus templates in ${ghTemplateUrl}. Re-run with one of:\n${list}`);
122
+ }
123
+ throw new Error(`No Directus template found at ${ghTemplateUrl}. A Directus template needs a package.json with a "templateName" field.`);
124
+ }
125
+ catch (error) {
126
+ throw new Error(`Failed to download GitHub template: ${getErrorMessage(error)}`, { cause: error });
127
+ }
128
+ }
129
+ export async function getInteractiveGithubTemplate(ghTemplateUrl) {
130
+ try {
131
+ const resolvedDir = await downloadGithubTemplate(ghTemplateUrl);
132
+ const template = await readTemplate(resolvedDir);
133
+ if (template) {
134
+ return [template];
135
+ }
136
+ const nested = await findNestedTemplates(resolvedDir, 3);
137
+ if (nested.length === 0) {
138
+ throw new Error(`No Directus template found at ${ghTemplateUrl}. A Directus template needs a package.json with a "templateName" field.`);
89
139
  }
90
- return readTemplate(resolvedDir);
140
+ return nested;
91
141
  }
92
142
  catch (error) {
93
- throw new Error(`Failed to download GitHub template: ${error}`);
143
+ throw new Error(`Failed to download GitHub template: ${getErrorMessage(error)}`, { cause: error });
94
144
  }
95
145
  }
@@ -1 +1,8 @@
1
+ export interface ParsedGitHubUrl {
2
+ owner: string;
3
+ ref?: string;
4
+ repo: string;
5
+ subpath?: string;
6
+ }
7
+ export declare function parseGitHubUrl(url: string): ParsedGitHubUrl;
1
8
  export declare function transformGitHubUrl(url: string): string;
@@ -1,11 +1,31 @@
1
+ export function parseGitHubUrl(url) {
2
+ const cleaned = url.trim().replace(/\/+$/, '');
3
+ const urlToParse = /^https?:\/\//i.test(cleaned) ? cleaned : `https://${cleaned}`;
4
+ let parsed;
5
+ try {
6
+ parsed = new URL(urlToParse);
7
+ }
8
+ catch {
9
+ throw new Error(`Invalid GitHub URL: ${url}`);
10
+ }
11
+ if (!['github.com', 'www.github.com'].includes(parsed.hostname.toLowerCase())) {
12
+ throw new Error(`Invalid GitHub URL: ${url}`);
13
+ }
14
+ const pathParts = parsed.pathname.split('/').filter(Boolean);
15
+ const [owner, rawRepo, tree, ref, ...subpathParts] = pathParts;
16
+ const repo = rawRepo?.replace(/\.git$/, '');
17
+ if (!owner || !repo || pathParts.length > 2 && tree !== 'tree') {
18
+ throw new Error(`Invalid GitHub URL: ${url}`);
19
+ }
20
+ if (tree === 'tree' && !ref) {
21
+ throw new Error(`Invalid GitHub URL: ${url}`);
22
+ }
23
+ const subpath = subpathParts.length > 0 ? subpathParts.join('/') : undefined;
24
+ return { owner, ref, repo, subpath };
25
+ }
1
26
  export function transformGitHubUrl(url) {
2
- // Regular expression to capture the repository name and any subsequent path after the 'tree'
3
- const regex = /github\.com\/([^/]+\/[^/]+)(?:\/tree\/[^/]+\/(.*))?$/;
4
- const match = url.match(regex);
5
- if (match) {
6
- const repo = match[1];
7
- const subpath = match[2] ? match[2] : '';
8
- return `github:${repo}/${subpath}`;
9
- }
10
- return 'Invalid URL';
27
+ const { owner, ref, repo, subpath } = parseGitHubUrl(url);
28
+ const pathPart = subpath ? `/${subpath}` : '';
29
+ const refPart = ref ? `#${ref}` : '';
30
+ return `github:${owner}/${repo}${pathPart}${refPart}`;
11
31
  }
@@ -401,5 +401,5 @@
401
401
  ]
402
402
  }
403
403
  },
404
- "version": "0.7.7"
404
+ "version": "0.7.8"
405
405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus-template-cli",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "CLI Utility for applying templates to a Directus instance.",
5
5
  "author": "bryantgillespie @bryantgillespie",
6
6
  "type": "module",
@@ -19,45 +19,44 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@clack/prompts": "^0.10.0",
22
- "@directus/sdk": "20.1.0",
23
- "@inquirer/prompts": "^7.3.3",
24
- "@oclif/core": "^4.2.9",
25
- "@oclif/plugin-help": "^6.2.26",
26
- "@oclif/plugin-plugins": "^5.4.34",
27
- "@octokit/rest": "^21.1.1",
28
- "@sindresorhus/slugify": "^2.2.1",
22
+ "@directus/sdk": "21.2.2",
23
+ "@inquirer/prompts": "^8.4.1",
24
+ "@oclif/core": "^4.10.5",
25
+ "@oclif/plugin-help": "^6.2.44",
26
+ "@oclif/plugin-plugins": "^5.4.61",
27
+ "@octokit/rest": "^22.0.1",
28
+ "@sindresorhus/slugify": "^3.0.0",
29
29
  "bottleneck": "^2.19.5",
30
- "chalk": "5.4.1",
30
+ "chalk": "5.6.2",
31
31
  "cli-progress": "^3.12.0",
32
- "defu": "^6.1.4",
33
- "dotenv": "^16.4.7",
34
- "execa": "9.5.2",
35
- "formdata-node": "^6.0.3",
36
- "giget": "^2.0.0",
37
- "glob": "^11.0.1",
38
- "log-update": "^6.1.0",
39
- "nypm": "^0.6.0",
32
+ "defu": "^6.1.7",
33
+ "dotenv": "^17.4.2",
34
+ "execa": "9.6.1",
35
+ "giget": "^3.2.0",
36
+ "glob": "^13.0.6",
37
+ "log-update": "^8.0.0",
38
+ "nypm": "^0.6.5",
40
39
  "pathe": "^2.0.3",
41
- "posthog-node": "^4.10.1"
40
+ "posthog-node": "^5.29.2"
42
41
  },
43
42
  "devDependencies": {
44
- "@directus/types": "^13.0.0",
45
- "@eslint/compat": "^1",
43
+ "@directus/types": "^15.0.2",
44
+ "@eslint/compat": "^2.0.5",
46
45
  "@oclif/prettier-config": "^0.2.1",
47
- "@oclif/test": "^4",
48
- "@types/chai": "^5.2.0",
46
+ "@oclif/test": "^4.1.18",
47
+ "@types/chai": "^5.2.3",
49
48
  "@types/mocha": "^10",
50
- "@types/node": "^18",
51
- "chai": "^5.2.0",
52
- "eslint": "^9.39.2",
53
- "eslint-config-oclif": "^6.0.130",
49
+ "@types/node": "^25.6.0",
50
+ "chai": "^6.2.2",
51
+ "eslint": "^10.2.1",
52
+ "eslint-config-oclif": "^6.0.157",
54
53
  "eslint-config-prettier": "^10",
55
- "mocha": "^10",
56
- "oclif": "^4",
57
- "prettier": "^3.7.4",
58
- "shx": "^0.3.3",
54
+ "mocha": "^11.7.5",
55
+ "oclif": "^4.23.0",
56
+ "prettier": "^3.8.3",
57
+ "shx": "^0.4.0",
59
58
  "ts-node": "^10",
60
- "typescript": "^5.8.2"
59
+ "typescript": "^6.0.3"
61
60
  },
62
61
  "oclif": {
63
62
  "bin": "directus-template-cli",