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.
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +28 -20
- package/dist/lib/constants.d.ts +4 -1
- package/dist/lib/constants.js +4 -4
- package/dist/lib/init/index.js +30 -18
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/utils/parse-github-url.js +39 -10
- package/dist/services/docker.js +63 -6
- package/dist/services/github.d.ts +7 -2
- package/dist/services/github.js +100 -10
- package/oclif.manifest.json +7 -4
- package/package.json +2 -3
package/dist/commands/init.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface InitFlags {
|
|
|
3
3
|
frontend?: string;
|
|
4
4
|
gitInit?: boolean;
|
|
5
5
|
installDeps?: boolean;
|
|
6
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/commands/init.js
CHANGED
|
@@ -45,7 +45,9 @@ export default class InitCommand extends BaseCommand {
|
|
|
45
45
|
default: true,
|
|
46
46
|
description: 'Install dependencies automatically',
|
|
47
47
|
}),
|
|
48
|
-
|
|
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.
|
|
100
|
-
const
|
|
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(
|
|
106
|
+
if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
|
|
104
107
|
cancel('Project creation cancelled.');
|
|
105
108
|
process.exit(0);
|
|
106
109
|
}
|
|
107
|
-
if (
|
|
108
|
-
flags.
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
+
overwriteDir: flags.overwriteDir,
|
|
238
246
|
},
|
|
239
247
|
runId: this.runId,
|
|
240
248
|
config: this.config,
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/lib/constants.js
CHANGED
|
@@ -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
|
|
24
|
-
const
|
|
25
|
-
export const BSL_LICENSE_TEXT =
|
|
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)}.`;
|
package/dist/lib/init/index.js
CHANGED
|
@@ -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.
|
|
19
|
+
const shouldForce = flags.overwriteDir;
|
|
21
20
|
if (fs.existsSync(dir) && !shouldForce) {
|
|
22
|
-
throw new Error('Directory already exists. Use --
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 ${
|
|
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(
|
|
161
|
-
|
|
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, {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
|
49
|
-
if (
|
|
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
|
|
53
|
-
const
|
|
54
|
-
|
|
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
|
package/dist/services/docker.js
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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<
|
|
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<
|
|
20
|
+
getTemplates: (customUrl?: string) => Promise<TemplateInfo[]>;
|
|
16
21
|
parseGitHubUrl: typeof parseGitHubUrl;
|
|
17
22
|
};
|
|
18
23
|
export {};
|
package/dist/services/github.js
CHANGED
|
@@ -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
|
-
*
|
|
63
|
-
*
|
|
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
|
-
//
|
|
69
|
+
// Handle direct URLs pointing to a specific template directory
|
|
67
70
|
if (customUrl?.startsWith('http')) {
|
|
68
|
-
|
|
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(
|
|
115
|
+
if (!Array.isArray(rootContent))
|
|
78
116
|
return [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -341,10 +341,13 @@
|
|
|
341
341
|
"allowNo": true,
|
|
342
342
|
"type": "boolean"
|
|
343
343
|
},
|
|
344
|
-
"
|
|
344
|
+
"overwriteDir": {
|
|
345
|
+
"aliases": [
|
|
346
|
+
"overwrite-dir"
|
|
347
|
+
],
|
|
345
348
|
"description": "Override the default directory",
|
|
346
|
-
"name": "
|
|
347
|
-
"allowNo":
|
|
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.
|
|
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.
|
|
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",
|