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.
- package/dist/commands/apply.d.ts +3 -2
- package/dist/commands/apply.js +28 -4
- package/dist/lib/load/load-files.js +8 -8
- package/dist/lib/utils/get-template.d.ts +1 -0
- package/dist/lib/utils/get-template.js +65 -15
- package/dist/lib/utils/transform-github-url.d.ts +7 -0
- package/dist/lib/utils/transform-github-url.js +29 -9
- package/oclif.manifest.json +1 -1
- package/package.json +30 -31
package/dist/commands/apply.d.ts
CHANGED
|
@@ -45,9 +45,10 @@ export default class ApplyCommand extends BaseCommand {
|
|
|
45
45
|
private runProgrammatic;
|
|
46
46
|
/**
|
|
47
47
|
* INTERACTIVE
|
|
48
|
-
* Select a
|
|
49
|
-
* @param
|
|
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
|
}
|
package/dist/commands/apply.js
CHANGED
|
@@ -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
|
|
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
|
|
307
|
-
* @param
|
|
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 {
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
|
|
102
|
+
const resolvedDir = await downloadGithubTemplate(ghTemplateUrl);
|
|
103
|
+
const template = await readTemplate(resolvedDir);
|
|
104
|
+
if (template) {
|
|
105
|
+
return template;
|
|
80
106
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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,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
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directus-template-cli",
|
|
3
|
-
"version": "0.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": "
|
|
23
|
-
"@inquirer/prompts": "^
|
|
24
|
-
"@oclif/core": "^4.
|
|
25
|
-
"@oclif/plugin-help": "^6.2.
|
|
26
|
-
"@oclif/plugin-plugins": "^5.4.
|
|
27
|
-
"@octokit/rest": "^
|
|
28
|
-
"@sindresorhus/slugify": "^
|
|
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.
|
|
30
|
+
"chalk": "5.6.2",
|
|
31
31
|
"cli-progress": "^3.12.0",
|
|
32
|
-
"defu": "^6.1.
|
|
33
|
-
"dotenv": "^
|
|
34
|
-
"execa": "9.
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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": "^
|
|
40
|
+
"posthog-node": "^5.29.2"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
|
-
"@directus/types": "^
|
|
45
|
-
"@eslint/compat": "^
|
|
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.
|
|
46
|
+
"@oclif/test": "^4.1.18",
|
|
47
|
+
"@types/chai": "^5.2.3",
|
|
49
48
|
"@types/mocha": "^10",
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"chai": "^
|
|
52
|
-
"eslint": "^
|
|
53
|
-
"eslint-config-oclif": "^6.0.
|
|
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": "^
|
|
56
|
-
"oclif": "^4",
|
|
57
|
-
"prettier": "^3.
|
|
58
|
-
"shx": "^0.
|
|
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": "^
|
|
59
|
+
"typescript": "^6.0.3"
|
|
61
60
|
},
|
|
62
61
|
"oclif": {
|
|
63
62
|
"bin": "directus-template-cli",
|