-
Notifications
You must be signed in to change notification settings - Fork 68
feat: add workflow init command #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7d6695b
23977fd
ed85359
0fea8a6
37450de
dc293b3
bbc7f9f
ceedafe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@workflow/cli": patch | ||
| --- | ||
|
|
||
| Add cli init command for setting up workflows |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,8 +1,26 @@ | ||||||
| import { exec } from 'node:child_process'; | ||||||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||||||
| import path from 'node:path'; | ||||||
| import { promisify } from 'node:util'; | ||||||
| import { | ||||||
| cancel, | ||||||
| confirm, | ||||||
| intro, | ||||||
| isCancel, | ||||||
| log, | ||||||
| outro, | ||||||
| select, | ||||||
| tasks, | ||||||
| text, | ||||||
| } from '@clack/prompts'; | ||||||
| import { Flags } from '@oclif/core'; | ||||||
| import chalk from 'chalk'; | ||||||
| import { BaseCommand } from '../base.js'; | ||||||
|
|
||||||
| const execAsync = promisify(exec); | ||||||
|
|
||||||
| export default class Init extends BaseCommand { | ||||||
| static description = 'Initialize a new workflow project (Coming soon)'; | ||||||
| static description = 'Initialize a new workflow project'; | ||||||
|
|
||||||
| static examples = [ | ||||||
| '$ workflow init', | ||||||
|
|
@@ -13,39 +31,248 @@ export default class Init extends BaseCommand { | |||||
| static flags = { | ||||||
| template: Flags.string({ | ||||||
| description: 'template to use', | ||||||
| options: ['standalone', 'nextjs', 'express'], | ||||||
| options: ['next', 'hono', 'nitro'], | ||||||
| default: 'next', | ||||||
| }), | ||||||
| yes: Flags.boolean({ | ||||||
| char: 'y', | ||||||
| description: 'skip prompts and use defaults', | ||||||
| }), | ||||||
| }; | ||||||
|
|
||||||
| /** | ||||||
| * | ||||||
| * @returns true if the current directory is a Next.js app | ||||||
| */ | ||||||
| private isNextApp(): boolean { | ||||||
| const configFiles = [ | ||||||
| 'next.config.js', | ||||||
| 'next.config.mjs', | ||||||
| 'next.config.ts', | ||||||
| 'next.config.cjs', | ||||||
| ]; | ||||||
|
|
||||||
| return configFiles.some((file) => | ||||||
| existsSync(path.join(process.cwd(), file)) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| public async run(): Promise<void> { | ||||||
| const { flags } = await this.parse(Init); | ||||||
|
|
||||||
| this.logInfo('🚧 Project initialization is coming soon!'); | ||||||
| this.logInfo(''); | ||||||
| this.logInfo('This command will provide:'); | ||||||
| this.logInfo('• Interactive project setup'); | ||||||
| this.logInfo('• Multiple project templates'); | ||||||
| this.logInfo('• Automatic configuration file generation'); | ||||||
| this.logInfo('• Dependency installation'); | ||||||
| this.logInfo('• Example workflows and steps'); | ||||||
| this.logInfo('Or we might do a create-workflow-app package'); | ||||||
| this.logInfo(''); | ||||||
|
|
||||||
| if (flags.template) { | ||||||
| this.logInfo(`Selected template: ${flags.template}`); | ||||||
| const { args, flags } = await this.parse(Init); | ||||||
|
|
||||||
| intro('workflow init'); | ||||||
|
|
||||||
| let template = flags.template ?? 'next'; | ||||||
|
|
||||||
| const isNextApp = this.isNextApp(); | ||||||
|
|
||||||
| let createNewProject = true; | ||||||
|
|
||||||
| if (isNextApp) { | ||||||
| log.info('Detected Next.js app'); | ||||||
|
|
||||||
| createNewProject = (await confirm({ | ||||||
| message: 'Create a new project?', | ||||||
| initialValue: true, | ||||||
| })) as boolean; | ||||||
|
|
||||||
| if (isCancel(createNewProject)) { | ||||||
| cancel('Cancelled workflow setup'); | ||||||
| return; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| let projectName = 'my-workflow-app'; | ||||||
|
|
||||||
| if (createNewProject) { | ||||||
| projectName = | ||||||
| args.projectName || | ||||||
| ((await text({ | ||||||
| message: 'What is your project name?', | ||||||
| placeholder: 'my-workflow-app', | ||||||
| defaultValue: 'my-workflow-app', | ||||||
| })) as string); | ||||||
|
|
||||||
| if (isCancel(projectName)) { | ||||||
| cancel('Cancelled workflow setup'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| template = (await select({ | ||||||
| message: 'What template would you like to use?', | ||||||
| options: [ | ||||||
| { label: 'Hono', value: 'hono', hint: 'via Nitro v3' }, | ||||||
| { label: 'Next.js', value: 'next' }, | ||||||
| { | ||||||
| label: 'Nitro', | ||||||
| value: 'nitro', | ||||||
| }, | ||||||
| ], | ||||||
| initialValue: 'next', | ||||||
| })) as string; | ||||||
|
|
||||||
| if (isCancel(template)) { | ||||||
| cancel('Cancelled workflow setup'); | ||||||
| return; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (flags.yes) { | ||||||
| this.logInfo('Auto-accept mode enabled'); | ||||||
| const useTsPlugin = | ||||||
| flags.yes || | ||||||
| ((await confirm({ | ||||||
| message: `Configure TypeScript intellisense? ${chalk.dim('(recommended)')}`, | ||||||
| initialValue: true, | ||||||
| })) as boolean); | ||||||
|
|
||||||
| if (isCancel(useTsPlugin)) { | ||||||
| cancel('Cancelled workflow setup'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| this.logInfo(''); | ||||||
| this.logInfo( | ||||||
| 'For now, manually create your workflow files in a ./workflows directory' | ||||||
| const projectPath = createNewProject | ||||||
| ? path.join(process.cwd(), projectName) | ||||||
| : process.cwd(); | ||||||
|
|
||||||
| await tasks([ | ||||||
| { | ||||||
| title: | ||||||
| template === 'next' ? 'Creating Next.js app' : 'Creating Hono app', | ||||||
| enabled: createNewProject, | ||||||
| task: async (message) => { | ||||||
| message('Creating a new Next.js app'); | ||||||
| await execAsync(`npx create-next-app@latest ${projectName} --yes`); | ||||||
| return `Created Next.js app in ${chalk.cyan(projectPath)}`; | ||||||
| }, | ||||||
| }, | ||||||
| { | ||||||
| title: 'Installing `workflow` package', | ||||||
| task: async (message) => { | ||||||
| message(`Installing \`workflow\` package`); | ||||||
| await execAsync(`cd ${projectPath} && pnpm add workflow`); | ||||||
| return `Installed \`workflow\` package`; | ||||||
| }, | ||||||
| }, | ||||||
| { | ||||||
| title: 'Configuring TypeScript intellisense', | ||||||
| enabled: useTsPlugin, | ||||||
| task: async (message) => { | ||||||
| message(`Configuring TypeScript intellisense`); | ||||||
| const tsConfig = JSON.parse( | ||||||
| readFileSync(path.join(projectPath, 'tsconfig.json'), 'utf8') | ||||||
| ); | ||||||
| tsConfig.compilerOptions.plugins.push({ | ||||||
| name: 'workflow', | ||||||
| }); | ||||||
| writeFileSync( | ||||||
| path.join(projectPath, 'tsconfig.json'), | ||||||
| JSON.stringify(tsConfig, null, 2) | ||||||
| ); | ||||||
| return 'Configured TypeScript intellisense'; | ||||||
| }, | ||||||
| }, | ||||||
| { | ||||||
| title: 'Configuring Next.js config', | ||||||
| task: async (message) => { | ||||||
| message(`Configuring Next.js config`); | ||||||
| let nextConfig = readFileSync( | ||||||
| path.join(projectPath, 'next.config.ts'), | ||||||
| 'utf8' | ||||||
| ); | ||||||
| nextConfig = nextConfig.replace( | ||||||
| /import type { NextConfig } from "next";/g, | ||||||
| "import type { NextConfig } from 'next';\nimport { withWorkflow } from 'workflow/next';" | ||||||
| ); | ||||||
| nextConfig = nextConfig.replace( | ||||||
| /export default nextConfig;/g, | ||||||
| 'export default withWorkflow(nextConfig);' | ||||||
| ); | ||||||
| writeFileSync(path.join(projectPath, 'next.config.ts'), nextConfig); | ||||||
| return 'Configured Next.js config'; | ||||||
| }, | ||||||
|
Comment on lines
+173
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "Configuring Next.js config" task always runs even when the user selects a non-Next.js template (Hono or Nitro), which will cause the task to fail because those projects don't have Next.js configuration files. View Details📝 Patch Detailsdiff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..87dbdfb 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -172,6 +172,7 @@ export default class Init extends BaseCommand {
},
{
title: 'Configuring Next.js config',
+ enabled: template === 'next',
task: async (message) => {
message(`Configuring Next.js config`);
let nextConfig = readFileSync(
@@ -241,6 +242,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
},
{
title: 'Creating API route handler',
+ enabled: template === 'next',
task: async (message) => {
message(`Creating API route handler`);
const apiPath = path.join(projectPath, 'app', 'api', 'signup');
AnalysisTemplate-specific tasks execute regardless of template selectionWhat fails: Two tasks in the
Both tasks fail with file not found errors when template is 'hono' or 'nitro' because these projects don't have Next.js configuration or app router structure. How to reproduce: cd packages/cli
pnpm run build
# Then run: workflow init --template hono --yes
# Or: workflow init --template nitro --yesResult: Task fails at Expected behavior: These tasks should only run when Verification: @clack/prompts Task type documentation confirms tasks support Fix: Added |
||||||
| }, | ||||||
| { | ||||||
| title: 'Creating example workflow', | ||||||
| task: async (message) => { | ||||||
| message(`Creating example workflow`); | ||||||
| const workflowsPath = path.join(projectPath, 'workflows'); | ||||||
| mkdirSync(workflowsPath); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The View DetailsAnalysisMissing
|
||||||
| const workflowContent = `import { FatalError, sleep } from "workflow"; | ||||||
|
|
||||||
| export async function handleUserSignup(email: string) { | ||||||
| "use workflow"; | ||||||
|
|
||||||
| const user = await createUser(email); | ||||||
| await sendWelcomeEmail(user); | ||||||
|
|
||||||
| await sleep("5s"); // Pause for 5s - doesn't consume any resources | ||||||
| await sendOnboardingEmail(user); | ||||||
|
|
||||||
| return { userId: user.id, status: "onboarded" }; | ||||||
| } | ||||||
|
|
||||||
| async function createUser(email: string) { | ||||||
| "use step"; | ||||||
| console.log(\`Creating user with email: \${email}\`); | ||||||
| // Full Node.js access - database calls, APIs, etc. | ||||||
| return { id: crypto.randomUUID(), email }; | ||||||
| } | ||||||
| async function sendWelcomeEmail(user: { id: string; email: string; }) { | ||||||
| "use step"; | ||||||
| console.log(\`Sending welcome email to user: \${user.id}\`); | ||||||
| if (Math.random() < 0.3) { | ||||||
| // By default, steps will be retried for unhandled errors | ||||||
| throw new Error("Retryable!"); | ||||||
| } | ||||||
| } | ||||||
| async function sendOnboardingEmail(user: { id: string; email: string}) { | ||||||
| "use step"; | ||||||
| if (!user.email.includes("@")) { | ||||||
| // To skip retrying, throw a FatalError instead | ||||||
| throw new FatalError("Invalid Email"); | ||||||
| } | ||||||
| console.log(\`Sending onboarding email to user: \${user.id}\`); | ||||||
| }`; | ||||||
| writeFileSync( | ||||||
| path.join(workflowsPath, 'user-signup.ts'), | ||||||
| workflowContent | ||||||
| ); | ||||||
| return `Created example workflow in ${chalk.cyan(path.join(workflowsPath, 'user-signup.ts'))}`; | ||||||
| }, | ||||||
| }, | ||||||
| { | ||||||
| title: 'Creating API route handler', | ||||||
| task: async (message) => { | ||||||
| message(`Creating API route handler`); | ||||||
| const apiPath = path.join(projectPath, 'app', 'api', 'signup'); | ||||||
| mkdirSync(apiPath, { recursive: true }); | ||||||
| writeFileSync( | ||||||
| path.join(apiPath, 'route.ts'), | ||||||
| `import { start } from 'workflow/api'; | ||||||
| import { handleUserSignup } from "@/workflows/user-signup"; | ||||||
| import { NextResponse } from "next/server"; | ||||||
|
|
||||||
| export async function POST(request: Request) { | ||||||
| const { email } = await request.json(); | ||||||
|
|
||||||
| // Executes asynchronously and doesn't block your app | ||||||
| await start(handleUserSignup, [email]); | ||||||
|
|
||||||
| return NextResponse.json({ | ||||||
| message: "User signup workflow started", | ||||||
| }); | ||||||
| }` | ||||||
| ); | ||||||
| return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`; | ||||||
| }, | ||||||
|
Comment on lines
+242
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "Creating API route handler" task always runs and assumes a Next.js app structure, but will fail if the user selected Hono or Nitro templates which have different directory structures. View Details📝 Patch Detailsdiff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..87dbdfb 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -172,6 +172,7 @@ export default class Init extends BaseCommand {
},
{
title: 'Configuring Next.js config',
+ enabled: template === 'next',
task: async (message) => {
message(`Configuring Next.js config`);
let nextConfig = readFileSync(
@@ -241,6 +242,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
},
{
title: 'Creating API route handler',
+ enabled: template === 'next',
task: async (message) => {
message(`Creating API route handler`);
const apiPath = path.join(projectPath, 'app', 'api', 'signup');
AnalysisNext.js-specific tasks not guarded by template conditionWhat fails: The "Configuring Next.js config" (line 174) and "Creating API route handler" (line 244) tasks in
How to reproduce: # Select 'hono' or 'nitro' template when prompted
$ workflow init
# When prompted: "What template would you like to use?" select "Hono" or "Nitro"Result: If a proper Hono or Nitro project structure is created, these tasks would fail:
Expected: Only Next.js-specific tasks should execute when Fix applied: Added |
||||||
| }, | ||||||
| ]); | ||||||
|
|
||||||
| cancel('Cancelled workflow setup'); | ||||||
|
|
||||||
|
Comment on lines
+270
to
+271
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The View DetailsAnalysisIncorrect cancellation message displayed on successful workflow setup completionWhat fails: The How to reproduce: workflow init --yesResult: The user sees two conflicting messages:
Expected: Only the success message should be displayed: "└ Success! Next steps:..." According to the @clack/prompts documentation, the |
||||||
| outro( | ||||||
| `${chalk.green('Success!')} Next steps: | ||||||
| Run ${chalk.dim(`${createNewProject ? `cd ${projectName} && ` : ''}npm run dev`)} to start the development server | ||||||
| Trigger the workflow: ${chalk.dim('curl -X POST --json \'{"email":"[email protected]"}\' http://localhost:3000/api/signup')}` | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The template selection logic doesn't respect the user's choice. The code allows users to select from 'hono', 'next', or 'nitro' templates, but always creates a Next.js app using
create-next-appregardless of the template selected.View Details
📝 Patch Details
Analysis
Template selection logic ignored in workflow init command
What fails: The
workflow initcommand accepts user selection of 'hono', 'next', or 'nitro' templates (lines 101-111) but always creates a Next.js app usingcreate-next-appregardless of the user's choice. The subsequent configuration tasks (lines 139-295) are also hardcoded for Next.js only, making Hono and Nitro template options non-functional.How to reproduce:
workflow init --template hono # Select "Hono" when prompted for template choiceResult: Despite selecting 'hono', the command creates a Next.js project, reads/modifies next.config.ts, and creates app/api/signup routes specific to Next.js.
Expected: When selecting 'hono' or 'nitro', the command should:
create-nitrowith appropriate template flag for app scaffoldingRoot cause: Git history shows template selection UI was added in commit
ceedafebut implementation was never completed - the task logic still only supports Next.js.Fix implemented:
create-next-appfor 'next' orcreate-nitrofor 'hono'/'nitro'