Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 106 additions & 101 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {cancel, log as clackLog, confirm, intro, isCancel, select, text} from '@clack/prompts'
import {Args, Flags, ux} from '@oclif/core'
import {Args, Flags} from '@oclif/core'
import chalk from 'chalk'
import {downloadTemplate} from 'giget'
import fs from 'node:fs'
Expand All @@ -12,7 +12,7 @@ import {init} from '../lib/init/index.js'
import {animatedBunny} from '../lib/utils/animated-bunny.js'
import {createGigetString, parseGitHubUrl} from '../lib/utils/parse-github-url.js'
import {readTemplateConfig} from '../lib/utils/template-config.js'
import {createGitHub} from '../services/github.js'
import {createGitHub, type TemplateInfo} from '../services/github.js'
import { shutdown, track } from '../services/posthog.js'
import { BaseCommand } from './base.js'

Expand All @@ -29,6 +29,11 @@ export interface InitArgs {
directory: string
}

interface ExplicitInitFlags {
gitInit: boolean
installDeps: boolean
}

export default class InitCommand extends BaseCommand {
static args = {
directory: Args.directory({
Expand Down Expand Up @@ -78,95 +83,80 @@ private targetDir = '.'
* @returns Promise that resolves when the command is complete.
*/
public async run(): Promise<void> {
const {args, flags} = await this.parse(InitCommand)
const {args, flags, metadata} = await this.parse(InitCommand)
const typedFlags = flags as InitFlags
const typedArgs = args as InitArgs
const explicitFlags: ExplicitInitFlags = {
gitInit: !metadata.flags.gitInit?.setFromDefault,
installDeps: !metadata.flags.installDeps?.setFromDefault,
}

// Set the target directory and create it if it doesn't exist
this.targetDir = path.resolve(args.directory as string)

await this.runInteractive(typedFlags, typedArgs)
await this.runInteractive(typedFlags, typedArgs, explicitFlags)
}

/**
* Interactive mode: prompts the user for each piece of info, with added template checks.
* @param flags - The flags passed to the command.
* @param args - The arguments passed to the command.
* @returns void
*/
private async runInteractive(flags: InitFlags, args: InitArgs): Promise<void> {

// Show animated intro
await animatedBunny('Let\'s create a new Directus project!')
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`)

// Check Docker availability before proceeding
const {createDocker} = await import('../services/docker.js')
const {DOCKER_CONFIG} = await import('../lib/init/config.js')
const dockerService = createDocker(DOCKER_CONFIG)
const dockerStatus = await dockerService.checkDocker()
private async confirmBooleanFlag(message: string): Promise<boolean> {
const response = await confirm({
initialValue: true,
message,
})

if (!dockerStatus.installed || !dockerStatus.running) {
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.')
process.exit(1)
if (isCancel(response)) {
cancel('Project creation cancelled.')
process.exit(0)
}

// Create GitHub service
const github = createGitHub()

// If no dir is provided, ask for it
if (!args.directory || args.directory === '.') {
let dirResponse = await text({
message: 'Enter the directory to create the project in:',
placeholder: './my-directus-project',
})
return response as boolean
}

private async confirmOverwriteDirectory(flags: InitFlags): Promise<boolean> {
if (!fs.existsSync(this.targetDir) || flags.overwriteDir) return Boolean(flags.overwriteDir)

if (isCancel(dirResponse)) {
cancel('Project creation cancelled.')
process.exit(0)
}

// If there's no response, set a default
if (!dirResponse) {
clackLog.warn('No directory provided, using default: ./my-directus-project')
dirResponse = './my-directus-project'
}
const overwriteDirResponse = await confirm({
initialValue: false,
message: 'Directory already exists. Would you like to overwrite it?',
})

this.targetDir = dirResponse as string
if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
cancel('Project creation cancelled.')
process.exit(0)
}

if (fs.existsSync(this.targetDir) && !flags.overwriteDir) {
const overwriteDirResponse = await confirm({
initialValue: false,
message: 'Directory already exists. Would you like to overwrite it?',
})
return true
}

if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
cancel('Project creation cancelled.')
process.exit(0)
}
private async promptForTargetDirectory(args: InitArgs): Promise<void> {
if (args.directory && args.directory !== '.') return

if (overwriteDirResponse) {
flags.overwriteDir = true
}
}
let dirResponse = await text({
message: 'Enter the directory to create the project in:',
placeholder: './my-directus-project',
})

// 1. Fetch available templates (now returns Array<{id: string, name: string, description?: string}>)
const availableTemplates = await github.getTemplates()
if (isCancel(dirResponse)) {
cancel('Project creation cancelled.')
process.exit(0)
}

// 2. Prompt for template if not provided
let {template} = flags // This will store the chosen template ID
let chosenTemplateObject: undefined | { description?: string; id: string; name: string; };
// If there's no response, set a default
if (!dirResponse) {
clackLog.warn('No directory provided, using default: ./my-directus-project')
dirResponse = './my-directus-project'
}

this.targetDir = dirResponse as string
}

private async promptForValidTemplate(template: string | undefined, availableTemplates: TemplateInfo[]): Promise<string> {
if (!template) {
const templateResponse = await select<any>({ // Explicit types for clarity
const templateResponse = await select<string>({
message: 'Which Directus backend template would you like to use?',
options: availableTemplates.map(tmpl => ({
hint: tmpl.description, // Show the description as a hint
label: tmpl.name, // Display the friendly name
value: tmpl.id, // The value submitted will be the ID (directory name)
hint: tmpl.description,
label: tmpl.name,
value: tmpl.id,
})),
})

Expand All @@ -178,16 +168,9 @@ private targetDir = '.'
template = templateResponse
}

// Find the chosen template object for potential future use (e.g., display name later)
chosenTemplateObject = availableTemplates.find(t => t.id === template);

// 3. Validate that the template exists in the available list
const isDirectUrl = template?.startsWith('http')

// Validate against the 'id' property of the template objects
while (!isDirectUrl && !availableTemplates.some(t => t.id === template)) {
// Keep the warning message simple or refer back to the list shown in the prompt
while (!template.startsWith('http') && !availableTemplates.some(t => t.id === template)) {
clackLog.warn(`Template ID "${template}" is not valid. Please choose from the list provided or enter a direct GitHub URL.`)
// eslint-disable-next-line no-await-in-loop
const templateNameResponse = await text({
message: 'Please enter a valid template ID, a direct GitHub URL, or Ctrl+C to cancel:',
})
Expand All @@ -198,9 +181,47 @@ private targetDir = '.'
}

template = templateNameResponse as string
chosenTemplateObject = availableTemplates.find(t => t.id === template); // Update chosen object after re-entry
}

return template
}

/**
* Interactive mode: prompts the user for each piece of info, with added template checks.
* @param flags - The flags passed to the command.
* @param args - The arguments passed to the command.
* @returns void
*/
private async runInteractive(flags: InitFlags, args: InitArgs, explicitFlags: ExplicitInitFlags): Promise<void> {

// Show animated intro
await animatedBunny('Let\'s create a new Directus project!')
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`)

// Check Docker availability before proceeding
const {createDocker} = await import('../services/docker.js')
const {DOCKER_CONFIG} = await import('../lib/init/config.js')
const dockerService = createDocker(DOCKER_CONFIG)
const dockerStatus = await dockerService.checkDocker()

if (!dockerStatus.installed || !dockerStatus.running) {
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.')
process.exit(1)
}

// Create GitHub service
const github = createGitHub()

// If no dir is provided, ask for it
await this.promptForTargetDirectory(args)
const overwriteDir = await this.confirmOverwriteDirectory(flags)

// 1. Fetch available templates (now returns Array<{id: string, name: string, description?: string}>)
const availableTemplates = await github.getTemplates()

// 2. Prompt for template if not provided, then validate it against known templates or a direct URL.
const template = await this.promptForValidTemplate(flags.template, availableTemplates)

flags.template = template // Ensure the flag stores the ID

// Download the template to a temporary directory to read its configuration
Expand All @@ -217,7 +238,7 @@ private targetDir = '.'
const templateInfo = readTemplateConfig(tempDir)

// 4. If template has frontends and user hasn't specified a valid one, ask from the list
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.find(f => f.id === chosenFrontend))) {
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.some(f => f.id === chosenFrontend))) {
const frontendResponse = await select({
message: 'Which frontend framework do you want to use?',
options:
Expand Down Expand Up @@ -245,29 +266,13 @@ private targetDir = '.'
}
}

const installDepsResponse = await confirm({
initialValue: true,
message: 'Would you like to install project dependencies automatically?',
})

if (isCancel(installDepsResponse)) {
cancel('Project creation cancelled.')
process.exit(0)
}

const installDeps = installDepsResponse as boolean

const initGitResponse = await confirm({
initialValue: true,
message: 'Initialize a new Git repository?',
})

if (isCancel(initGitResponse)) {
cancel('Project creation cancelled.')
process.exit(0)
}
const installDeps = explicitFlags.installDeps
? flags.installDeps ?? true
: await this.confirmBooleanFlag('Would you like to install project dependencies automatically?')

const initGit = initGitResponse as boolean
const initGit = explicitFlags.gitInit
? flags.gitInit ?? true
: await this.confirmBooleanFlag('Initialize a new Git repository?')

// Track the command start unless telemetry is disabled
if (!flags.disableTelemetry) {
Expand All @@ -293,7 +298,7 @@ private targetDir = '.'
frontend: chosenFrontend,
gitInit: initGit,
installDeps,
overwriteDir: flags.overwriteDir,
overwriteDir,
template,
},

Expand All @@ -309,7 +314,7 @@ private targetDir = '.'
frontend: chosenFrontend,
gitInit: initGit,
installDeps,
overwriteDir: flags.overwriteDir,
overwriteDir,
template,
},
lifecycle: 'complete',
Expand Down