directus-template-cli 0.8.0-partials.0 → 0.8.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.
- package/dist/commands/apply.d.ts +3 -2
- package/dist/commands/apply.js +28 -4
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +90 -87
- package/dist/lib/extract/extract-content.js +4 -2
- package/dist/lib/init/index.d.ts +15 -0
- package/dist/lib/init/index.js +29 -13
- package/dist/lib/load/index.js +4 -1
- package/dist/lib/load/load-data.d.ts +1 -0
- package/dist/lib/load/load-data.js +3 -2
- package/dist/lib/template-plan/metadata-plan.js +4 -1
- package/dist/lib/template-plan/metadata.js +8 -5
- 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 -30
package/dist/commands/apply.d.ts
CHANGED
|
@@ -50,9 +50,10 @@ export default class ApplyCommand extends BaseCommand {
|
|
|
50
50
|
private runProgrammatic;
|
|
51
51
|
/**
|
|
52
52
|
* INTERACTIVE
|
|
53
|
-
* Select a
|
|
54
|
-
* @param
|
|
53
|
+
* Select a template from the given GitHub repository
|
|
54
|
+
* @param ghTemplateUrl - The GitHub repository URL
|
|
55
55
|
* @returns {Promise<Template>} - Returns the selected template
|
|
56
56
|
*/
|
|
57
|
+
private selectGithubTemplate;
|
|
57
58
|
private selectLocalTemplate;
|
|
58
59
|
}
|
package/dist/commands/apply.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as templatePlanFlags from '../lib/template-plan/flags.js';
|
|
|
10
10
|
import { animatedBunny } from '../lib/utils/animated-bunny.js';
|
|
11
11
|
import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi, } from '../lib/utils/auth.js';
|
|
12
12
|
import catchError from '../lib/utils/catch-error.js';
|
|
13
|
-
import { getCommunityTemplates, getGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate, } from '../lib/utils/get-template.js';
|
|
13
|
+
import { getCommunityTemplates, getGithubTemplate, getInteractiveGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate, } from '../lib/utils/get-template.js';
|
|
14
14
|
import { logger } from '../lib/utils/logger.js';
|
|
15
15
|
import openUrl from '../lib/utils/open-url.js';
|
|
16
16
|
import { shutdown, track } from '../services/posthog.js';
|
|
@@ -110,7 +110,7 @@ export default class ApplyCommand extends BaseCommand {
|
|
|
110
110
|
const ghTemplateUrl = await text({
|
|
111
111
|
message: 'What is the public GitHub repository URL?',
|
|
112
112
|
});
|
|
113
|
-
template = await
|
|
113
|
+
template = await this.selectGithubTemplate(ghTemplateUrl);
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
116
|
case 'local': {
|
|
@@ -267,10 +267,33 @@ export default class ApplyCommand extends BaseCommand {
|
|
|
267
267
|
}
|
|
268
268
|
/**
|
|
269
269
|
* INTERACTIVE
|
|
270
|
-
* Select a
|
|
271
|
-
* @param
|
|
270
|
+
* Select a template from the given GitHub repository
|
|
271
|
+
* @param ghTemplateUrl - The GitHub repository URL
|
|
272
272
|
* @returns {Promise<Template>} - Returns the selected template
|
|
273
273
|
*/
|
|
274
|
+
async selectGithubTemplate(ghTemplateUrl) {
|
|
275
|
+
try {
|
|
276
|
+
const templates = await getInteractiveGithubTemplate(ghTemplateUrl);
|
|
277
|
+
if (templates.length === 1) {
|
|
278
|
+
return templates[0];
|
|
279
|
+
}
|
|
280
|
+
log.info('Multiple Directus templates found in this repository.');
|
|
281
|
+
const selectedTemplate = await select({
|
|
282
|
+
message: 'Select a template.',
|
|
283
|
+
options: templates.map(t => ({ label: t.templateName, value: t })),
|
|
284
|
+
});
|
|
285
|
+
return selectedTemplate;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof Error) {
|
|
289
|
+
ux.error(error.message);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
ux.error('An unknown error occurred while getting the GitHub template.');
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
274
297
|
async selectLocalTemplate(localTemplateDir) {
|
|
275
298
|
try {
|
|
276
299
|
const templates = await getInteractiveLocalTemplate(localTemplateDir);
|
|
@@ -293,6 +316,7 @@ export default class ApplyCommand extends BaseCommand {
|
|
|
293
316
|
else {
|
|
294
317
|
ux.error('An unknown error occurred while getting the local template.');
|
|
295
318
|
}
|
|
319
|
+
throw error;
|
|
296
320
|
}
|
|
297
321
|
}
|
|
298
322
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export default class InitCommand extends BaseCommand {
|
|
|
32
32
|
* @returns Promise that resolves when the command is complete.
|
|
33
33
|
*/
|
|
34
34
|
run(): Promise<void>;
|
|
35
|
+
private confirmBooleanFlag;
|
|
36
|
+
private confirmOverwriteDirectory;
|
|
37
|
+
private promptForTargetDirectory;
|
|
38
|
+
private promptForValidTemplate;
|
|
35
39
|
/**
|
|
36
40
|
* Interactive mode: prompts the user for each piece of info, with added template checks.
|
|
37
41
|
* @param flags - The flags passed to the command.
|
package/dist/commands/init.js
CHANGED
|
@@ -62,76 +62,67 @@ export default class InitCommand extends BaseCommand {
|
|
|
62
62
|
* @returns Promise that resolves when the command is complete.
|
|
63
63
|
*/
|
|
64
64
|
async run() {
|
|
65
|
-
const { args, flags } = await this.parse(InitCommand);
|
|
65
|
+
const { args, flags, metadata } = await this.parse(InitCommand);
|
|
66
66
|
const typedFlags = flags;
|
|
67
67
|
const typedArgs = args;
|
|
68
|
+
const explicitFlags = {
|
|
69
|
+
gitInit: !metadata.flags.gitInit?.setFromDefault,
|
|
70
|
+
installDeps: !metadata.flags.installDeps?.setFromDefault,
|
|
71
|
+
};
|
|
68
72
|
// Set the target directory and create it if it doesn't exist
|
|
69
73
|
this.targetDir = path.resolve(args.directory);
|
|
70
|
-
await this.runInteractive(typedFlags, typedArgs);
|
|
74
|
+
await this.runInteractive(typedFlags, typedArgs, explicitFlags);
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await animatedBunny('Let\'s create a new Directus project!');
|
|
81
|
-
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`);
|
|
82
|
-
// Check Docker availability before proceeding
|
|
83
|
-
const { createDocker } = await import('../services/docker.js');
|
|
84
|
-
const { DOCKER_CONFIG } = await import('../lib/init/config.js');
|
|
85
|
-
const dockerService = createDocker(DOCKER_CONFIG);
|
|
86
|
-
const dockerStatus = await dockerService.checkDocker();
|
|
87
|
-
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
88
|
-
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.');
|
|
89
|
-
process.exit(1);
|
|
76
|
+
async confirmBooleanFlag(message) {
|
|
77
|
+
const response = await confirm({
|
|
78
|
+
initialValue: true,
|
|
79
|
+
message,
|
|
80
|
+
});
|
|
81
|
+
if (isCancel(response)) {
|
|
82
|
+
cancel('Project creation cancelled.');
|
|
83
|
+
process.exit(0);
|
|
90
84
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// If there's no response, set a default
|
|
104
|
-
if (!dirResponse) {
|
|
105
|
-
clackLog.warn('No directory provided, using default: ./my-directus-project');
|
|
106
|
-
dirResponse = './my-directus-project';
|
|
107
|
-
}
|
|
108
|
-
this.targetDir = dirResponse;
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
async confirmOverwriteDirectory(flags) {
|
|
88
|
+
if (!fs.existsSync(this.targetDir) || flags.overwriteDir)
|
|
89
|
+
return Boolean(flags.overwriteDir);
|
|
90
|
+
const overwriteDirResponse = await confirm({
|
|
91
|
+
initialValue: false,
|
|
92
|
+
message: 'Directory already exists. Would you like to overwrite it?',
|
|
93
|
+
});
|
|
94
|
+
if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
|
|
95
|
+
cancel('Project creation cancelled.');
|
|
96
|
+
process.exit(0);
|
|
109
97
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
async promptForTargetDirectory(args) {
|
|
101
|
+
if (args.directory && args.directory !== '.')
|
|
102
|
+
return;
|
|
103
|
+
let dirResponse = await text({
|
|
104
|
+
message: 'Enter the directory to create the project in:',
|
|
105
|
+
placeholder: './my-directus-project',
|
|
106
|
+
});
|
|
107
|
+
if (isCancel(dirResponse)) {
|
|
108
|
+
cancel('Project creation cancelled.');
|
|
109
|
+
process.exit(0);
|
|
122
110
|
}
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
// If there's no response, set a default
|
|
112
|
+
if (!dirResponse) {
|
|
113
|
+
clackLog.warn('No directory provided, using default: ./my-directus-project');
|
|
114
|
+
dirResponse = './my-directus-project';
|
|
115
|
+
}
|
|
116
|
+
this.targetDir = dirResponse;
|
|
117
|
+
}
|
|
118
|
+
async promptForValidTemplate(template, availableTemplates) {
|
|
128
119
|
if (!template) {
|
|
129
120
|
const templateResponse = await select({
|
|
130
121
|
message: 'Which Directus backend template would you like to use?',
|
|
131
122
|
options: availableTemplates.map(tmpl => ({
|
|
132
|
-
hint: tmpl.description,
|
|
133
|
-
label: tmpl.name,
|
|
134
|
-
value: tmpl.id,
|
|
123
|
+
hint: tmpl.description,
|
|
124
|
+
label: tmpl.name,
|
|
125
|
+
value: tmpl.id,
|
|
135
126
|
})),
|
|
136
127
|
});
|
|
137
128
|
if (isCancel(templateResponse)) {
|
|
@@ -140,14 +131,9 @@ export default class InitCommand extends BaseCommand {
|
|
|
140
131
|
}
|
|
141
132
|
template = templateResponse;
|
|
142
133
|
}
|
|
143
|
-
|
|
144
|
-
chosenTemplateObject = availableTemplates.find(t => t.id === template);
|
|
145
|
-
// 3. Validate that the template exists in the available list
|
|
146
|
-
const isDirectUrl = template?.startsWith('http');
|
|
147
|
-
// Validate against the 'id' property of the template objects
|
|
148
|
-
while (!isDirectUrl && !availableTemplates.some(t => t.id === template)) {
|
|
149
|
-
// Keep the warning message simple or refer back to the list shown in the prompt
|
|
134
|
+
while (!template.startsWith('http') && !availableTemplates.some(t => t.id === template)) {
|
|
150
135
|
clackLog.warn(`Template ID "${template}" is not valid. Please choose from the list provided or enter a direct GitHub URL.`);
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
151
137
|
const templateNameResponse = await text({
|
|
152
138
|
message: 'Please enter a valid template ID, a direct GitHub URL, or Ctrl+C to cancel:',
|
|
153
139
|
});
|
|
@@ -156,8 +142,37 @@ export default class InitCommand extends BaseCommand {
|
|
|
156
142
|
process.exit(0);
|
|
157
143
|
}
|
|
158
144
|
template = templateNameResponse;
|
|
159
|
-
chosenTemplateObject = availableTemplates.find(t => t.id === template); // Update chosen object after re-entry
|
|
160
145
|
}
|
|
146
|
+
return template;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Interactive mode: prompts the user for each piece of info, with added template checks.
|
|
150
|
+
* @param flags - The flags passed to the command.
|
|
151
|
+
* @param args - The arguments passed to the command.
|
|
152
|
+
* @returns void
|
|
153
|
+
*/
|
|
154
|
+
async runInteractive(flags, args, explicitFlags) {
|
|
155
|
+
// Show animated intro
|
|
156
|
+
await animatedBunny('Let\'s create a new Directus project!');
|
|
157
|
+
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`);
|
|
158
|
+
// Check Docker availability before proceeding
|
|
159
|
+
const { createDocker } = await import('../services/docker.js');
|
|
160
|
+
const { DOCKER_CONFIG } = await import('../lib/init/config.js');
|
|
161
|
+
const dockerService = createDocker(DOCKER_CONFIG);
|
|
162
|
+
const dockerStatus = await dockerService.checkDocker();
|
|
163
|
+
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
164
|
+
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
// Create GitHub service
|
|
168
|
+
const github = createGitHub();
|
|
169
|
+
// If no dir is provided, ask for it
|
|
170
|
+
await this.promptForTargetDirectory(args);
|
|
171
|
+
const overwriteDir = await this.confirmOverwriteDirectory(flags);
|
|
172
|
+
// 1. Fetch available templates (now returns Array<{id: string, name: string, description?: string}>)
|
|
173
|
+
const availableTemplates = await github.getTemplates();
|
|
174
|
+
// 2. Prompt for template if not provided, then validate it against known templates or a direct URL.
|
|
175
|
+
const template = await this.promptForValidTemplate(flags.template, availableTemplates);
|
|
161
176
|
flags.template = template; // Ensure the flag stores the ID
|
|
162
177
|
// Download the template to a temporary directory to read its configuration
|
|
163
178
|
const tempDir = path.join(os.tmpdir(), `directus-template-${Date.now()}`);
|
|
@@ -170,7 +185,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
170
185
|
// Read template configuration
|
|
171
186
|
const templateInfo = readTemplateConfig(tempDir);
|
|
172
187
|
// 4. If template has frontends and user hasn't specified a valid one, ask from the list
|
|
173
|
-
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.
|
|
188
|
+
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.some(f => f.id === chosenFrontend))) {
|
|
174
189
|
const frontendResponse = await select({
|
|
175
190
|
message: 'Which frontend framework do you want to use?',
|
|
176
191
|
options: templateInfo.frontendOptions.map(frontend => ({
|
|
@@ -194,24 +209,12 @@ export default class InitCommand extends BaseCommand {
|
|
|
194
209
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
195
210
|
}
|
|
196
211
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
process.exit(0);
|
|
204
|
-
}
|
|
205
|
-
const installDeps = installDepsResponse;
|
|
206
|
-
const initGitResponse = await confirm({
|
|
207
|
-
initialValue: true,
|
|
208
|
-
message: 'Initialize a new Git repository?',
|
|
209
|
-
});
|
|
210
|
-
if (isCancel(initGitResponse)) {
|
|
211
|
-
cancel('Project creation cancelled.');
|
|
212
|
-
process.exit(0);
|
|
213
|
-
}
|
|
214
|
-
const initGit = initGitResponse;
|
|
212
|
+
const installDeps = explicitFlags.installDeps
|
|
213
|
+
? flags.installDeps ?? true
|
|
214
|
+
: await this.confirmBooleanFlag('Would you like to install project dependencies automatically?');
|
|
215
|
+
const initGit = explicitFlags.gitInit
|
|
216
|
+
? flags.gitInit ?? true
|
|
217
|
+
: await this.confirmBooleanFlag('Initialize a new Git repository?');
|
|
215
218
|
// Track the command start unless telemetry is disabled
|
|
216
219
|
if (!flags.disableTelemetry) {
|
|
217
220
|
await track({
|
|
@@ -235,7 +238,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
235
238
|
frontend: chosenFrontend,
|
|
236
239
|
gitInit: initGit,
|
|
237
240
|
installDeps,
|
|
238
|
-
overwriteDir
|
|
241
|
+
overwriteDir,
|
|
239
242
|
template,
|
|
240
243
|
},
|
|
241
244
|
});
|
|
@@ -249,7 +252,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
249
252
|
frontend: chosenFrontend,
|
|
250
253
|
gitInit: initGit,
|
|
251
254
|
installDeps,
|
|
252
|
-
overwriteDir
|
|
255
|
+
overwriteDir,
|
|
253
256
|
template,
|
|
254
257
|
},
|
|
255
258
|
lifecycle: 'complete',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readCollections, readItems, readRelations } from '@directus/sdk';
|
|
2
|
-
import { ux } from '@oclif/core';
|
|
2
|
+
import { Errors, ux } from '@oclif/core';
|
|
3
3
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
4
|
import { api } from '../sdk.js';
|
|
5
5
|
import { getBrokenJunctionCollections, includesCollection, } from '../template-plan/index.js';
|
|
@@ -100,7 +100,6 @@ async function getDataFromCollection(collection, dir, relations, plan) {
|
|
|
100
100
|
context: { collection, function: 'getDataFromCollection' },
|
|
101
101
|
fatal: true,
|
|
102
102
|
});
|
|
103
|
-
throw error;
|
|
104
103
|
}
|
|
105
104
|
}
|
|
106
105
|
export async function extractContent(dir, plan) {
|
|
@@ -110,12 +109,15 @@ export async function extractContent(dir, plan) {
|
|
|
110
109
|
const relations = (await api.client.request(readRelations()));
|
|
111
110
|
const collections = await getCollections(relations, plan);
|
|
112
111
|
for (const collection of collections) {
|
|
112
|
+
// Keep extraction sequential so the shared ux.action.status reflects the active collection.
|
|
113
113
|
// eslint-disable-next-line no-await-in-loop
|
|
114
114
|
const collectionWarnings = await getDataFromCollection(collection, dir, relations, plan);
|
|
115
115
|
warnings.push(...collectionWarnings);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
catch (error) {
|
|
119
|
+
if (error instanceof Errors.CLIError)
|
|
120
|
+
throw error;
|
|
119
121
|
catchError(error, {
|
|
120
122
|
context: { function: 'extractContent' },
|
|
121
123
|
fatal: true,
|
package/dist/lib/init/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DownloadTemplateResult } from 'giget';
|
|
2
|
+
import { type PackageManager } from 'nypm';
|
|
2
3
|
import type { InitFlags } from '../../commands/init.js';
|
|
3
4
|
export declare function init({ dir, flags }: {
|
|
4
5
|
dir: string;
|
|
@@ -8,3 +9,17 @@ export declare function init({ dir, flags }: {
|
|
|
8
9
|
frontendDir: string;
|
|
9
10
|
template: DownloadTemplateResult;
|
|
10
11
|
}>;
|
|
12
|
+
type DependencyInstaller = (options: {
|
|
13
|
+
cwd: string;
|
|
14
|
+
packageManager?: PackageManager;
|
|
15
|
+
silent: boolean;
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
type InstallFrontendDependenciesOptions = {
|
|
18
|
+
frontendDir: string;
|
|
19
|
+
install?: DependencyInstaller;
|
|
20
|
+
onSkip?: () => void;
|
|
21
|
+
packageManager: null | PackageManager;
|
|
22
|
+
warn?: (message: string) => void;
|
|
23
|
+
};
|
|
24
|
+
export declare function installFrontendDependencies({ frontendDir, install, onSkip, packageManager, warn, }: InstallFrontendDependenciesOptions): Promise<boolean>;
|
|
25
|
+
export {};
|
package/dist/lib/init/index.js
CHANGED
|
@@ -133,21 +133,18 @@ export async function init({ dir, flags }) {
|
|
|
133
133
|
// Install dependencies if requested
|
|
134
134
|
if (flags.installDeps) {
|
|
135
135
|
const s = spinner();
|
|
136
|
+
let dependenciesInstalled = true;
|
|
136
137
|
s.start('Installing dependencies');
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
}
|
|
138
|
+
if (fs.existsSync(frontendDir)) {
|
|
139
|
+
dependenciesInstalled = await installFrontendDependencies({
|
|
140
|
+
frontendDir,
|
|
141
|
+
onSkip: () => s.stop('Dependency installation skipped'),
|
|
142
|
+
packageManager,
|
|
143
|
+
});
|
|
145
144
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
throw error;
|
|
145
|
+
if (dependenciesInstalled) {
|
|
146
|
+
s.stop('Dependencies installed!');
|
|
149
147
|
}
|
|
150
|
-
s.stop('Dependencies installed!');
|
|
151
148
|
}
|
|
152
149
|
// Initialize Git repo
|
|
153
150
|
if (flags.gitInit) {
|
|
@@ -165,7 +162,7 @@ export async function init({ dir, flags }) {
|
|
|
165
162
|
: `- Complete the onboarding form at ${pinkText(directusInfo.url || 'http://localhost:8055')} to create your admin account. \n`;
|
|
166
163
|
const frontendText = flags.frontend ? `- To start the frontend, run ${pinkText(`cd ${flags.frontend}`)} and then ${pinkText(`${packageManager?.name} run dev`)}. \n` : '';
|
|
167
164
|
const projectText = `- Navigate to your project directory using ${pinkText(`cd ${relativeDir}`)}. \n`;
|
|
168
|
-
const readmeText = '- Review the
|
|
165
|
+
const readmeText = '- Review the `./README.md` file for more information and next steps.';
|
|
169
166
|
const nextSteps = `${directusText}${directusLoginText}${projectText}${frontendText}${readmeText}`;
|
|
170
167
|
note(nextSteps, 'Next Steps');
|
|
171
168
|
clackLog.warn(BSL_LICENSE_HEADLINE);
|
|
@@ -203,3 +200,22 @@ async function initGit(targetDir) {
|
|
|
203
200
|
});
|
|
204
201
|
}
|
|
205
202
|
}
|
|
203
|
+
export async function installFrontendDependencies({ frontendDir, install = installDependencies, onSkip, packageManager, warn = ux.warn, }) {
|
|
204
|
+
try {
|
|
205
|
+
await install({
|
|
206
|
+
cwd: frontendDir,
|
|
207
|
+
packageManager: packageManager ?? undefined,
|
|
208
|
+
silent: true,
|
|
209
|
+
});
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
onSkip?.();
|
|
214
|
+
warn('Failed to install dependencies');
|
|
215
|
+
if (packageManager?.name === 'pnpm') {
|
|
216
|
+
warn('This starter uses pnpm. From the frontend directory, try running: corepack enable && pnpm install');
|
|
217
|
+
}
|
|
218
|
+
warn('You can install dependencies manually and continue using the generated project.');
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
package/dist/lib/load/index.js
CHANGED
|
@@ -45,7 +45,6 @@ export default async function apply(dir, flags) {
|
|
|
45
45
|
await loadCollections(source, effectivePlan);
|
|
46
46
|
await loadRelations(source, effectivePlan);
|
|
47
47
|
await finalizeCollections(source, effectivePlan);
|
|
48
|
-
await finalizeFields(source, effectivePlan);
|
|
49
48
|
}
|
|
50
49
|
if (components.permissions || components.users) {
|
|
51
50
|
await loadRoles(source);
|
|
@@ -63,6 +62,10 @@ export default async function apply(dir, flags) {
|
|
|
63
62
|
if (components.content) {
|
|
64
63
|
await loadData(source, effectivePlan);
|
|
65
64
|
}
|
|
65
|
+
if (components.schema) {
|
|
66
|
+
// Finalize fields after data loading because skeleton records rely on relaxed constraints.
|
|
67
|
+
await finalizeFields(source, effectivePlan);
|
|
68
|
+
}
|
|
66
69
|
if (components.dashboards) {
|
|
67
70
|
await loadDashboards(source);
|
|
68
71
|
}
|
|
@@ -26,10 +26,11 @@ function getContentCollections(dir) {
|
|
|
26
26
|
.filter((file) => file.endsWith('.json'))
|
|
27
27
|
.map((file) => path.basename(file, '.json')));
|
|
28
28
|
}
|
|
29
|
-
function getUserCollections(dir, plan) {
|
|
29
|
+
export function getUserCollections(dir, plan) {
|
|
30
30
|
const contentCollections = getContentCollections(dir);
|
|
31
31
|
const collections = readFile('collections', dir);
|
|
32
|
-
const
|
|
32
|
+
const relationsPath = path.join(dir, 'relations.json');
|
|
33
|
+
const relations = plan?.partial && fs.existsSync(relationsPath) ? readFile('relations', dir) : [];
|
|
33
34
|
const brokenJunctions = getBrokenJunctionCollections(relations, plan);
|
|
34
35
|
if (brokenJunctions.size > 0) {
|
|
35
36
|
ux.warn(`Skipping junction collections with excluded FK targets: ${[...brokenJunctions].join(', ')}`);
|
|
@@ -21,13 +21,16 @@ export function applyMetadataToPlan(plan, metadata) {
|
|
|
21
21
|
for (const component of componentNames) {
|
|
22
22
|
components[component] = components[component] && metadata.components[component];
|
|
23
23
|
}
|
|
24
|
-
const partial =
|
|
24
|
+
const partial = plan.partial ||
|
|
25
|
+
metadata.partial ||
|
|
26
|
+
componentNames.some((component) => components[component] !== plan.components[component]);
|
|
25
27
|
return {
|
|
26
28
|
...plan,
|
|
27
29
|
collections: intersectCollections('collections', plan.collections, metadata.collections),
|
|
28
30
|
components,
|
|
29
31
|
excludeCollections: mergeExcludedCollections(plan.excludeCollections, metadata.excludedCollections),
|
|
30
32
|
partial,
|
|
33
|
+
relationStrategy: metadata.relationStrategy ?? plan.relationStrategy,
|
|
31
34
|
schemaCollections: intersectCollections('schema collections', plan.schemaCollections, metadata.schemaCollections),
|
|
32
35
|
};
|
|
33
36
|
}
|
|
@@ -22,16 +22,19 @@ export function readTemplateMetadata(dir) {
|
|
|
22
22
|
const filePath = getTemplateMetadataPath(dir);
|
|
23
23
|
if (!fs.existsSync(filePath))
|
|
24
24
|
return undefined;
|
|
25
|
+
let metadata;
|
|
25
26
|
try {
|
|
26
|
-
|
|
27
|
-
if (metadata.version !== 2) {
|
|
28
|
-
catchError(new Error(`Unsupported template metadata version: ${metadata.version}`), { fatal: true });
|
|
29
|
-
}
|
|
30
|
-
return metadata;
|
|
27
|
+
metadata = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
31
28
|
}
|
|
32
29
|
catch (error) {
|
|
33
30
|
catchError(error, { fatal: true });
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (metadata.version !== 2) {
|
|
34
|
+
catchError(new Error(`Unsupported template metadata version: ${metadata.version}`), { fatal: true });
|
|
35
|
+
return undefined;
|
|
34
36
|
}
|
|
37
|
+
return metadata;
|
|
35
38
|
}
|
|
36
39
|
export async function writeTemplateMetadata(dir, plan, warnings = []) {
|
|
37
40
|
const filePath = getTemplateMetadataPath(dir);
|
|
@@ -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.8.0
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CLI Utility for applying templates to a Directus instance.",
|
|
5
5
|
"author": "bryantgillespie @bryantgillespie",
|
|
6
6
|
"type": "module",
|
|
@@ -19,44 +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
|
-
"giget": "^2.0
|
|
36
|
-
"glob": "^
|
|
37
|
-
"log-update": "^
|
|
38
|
-
"nypm": "^0.6.
|
|
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",
|
|
39
39
|
"pathe": "^2.0.3",
|
|
40
|
-
"posthog-node": "^
|
|
40
|
+
"posthog-node": "^5.29.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@directus/types": "^
|
|
44
|
-
"@eslint/compat": "^
|
|
43
|
+
"@directus/types": "^15.0.2",
|
|
44
|
+
"@eslint/compat": "^2.0.5",
|
|
45
45
|
"@oclif/prettier-config": "^0.2.1",
|
|
46
|
-
"@oclif/test": "^4",
|
|
47
|
-
"@types/chai": "^5.2.
|
|
46
|
+
"@oclif/test": "^4.1.18",
|
|
47
|
+
"@types/chai": "^5.2.3",
|
|
48
48
|
"@types/mocha": "^10",
|
|
49
|
-
"@types/node": "^
|
|
50
|
-
"chai": "^
|
|
51
|
-
"eslint": "^
|
|
52
|
-
"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",
|
|
53
53
|
"eslint-config-prettier": "^10",
|
|
54
|
-
"mocha": "^
|
|
55
|
-
"oclif": "^4",
|
|
56
|
-
"prettier": "^3.
|
|
57
|
-
"shx": "^0.
|
|
54
|
+
"mocha": "^11.7.5",
|
|
55
|
+
"oclif": "^4.23.0",
|
|
56
|
+
"prettier": "^3.8.3",
|
|
57
|
+
"shx": "^0.4.0",
|
|
58
58
|
"ts-node": "^10",
|
|
59
|
-
"typescript": "^
|
|
59
|
+
"typescript": "^6.0.3"
|
|
60
60
|
},
|
|
61
61
|
"oclif": {
|
|
62
62
|
"bin": "directus-template-cli",
|