-
Notifications
You must be signed in to change notification settings - Fork 65
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?
Conversation
🦋 Changeset detectedLatest commit: ceedafe The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
7066dda to
ceedafe
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| cancel('Cancelled workflow setup'); | ||
|
|
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.
| cancel('Cancelled workflow setup'); |
The cancel() function is called unconditionally after successful task completion, which will display a cancellation message even when the workflow setup succeeds. This should only be called when the user actually cancels.
View Details
Analysis
Incorrect cancellation message displayed on successful workflow setup completion
What fails: The Init.run() method in packages/cli/src/commands/init.ts calls cancel('Cancelled workflow setup') unconditionally at line 270 after all setup tasks complete successfully, causing a cancellation message to be displayed to the user even though the workflow setup succeeded.
How to reproduce:
workflow init --yesResult: The user sees two conflicting messages:
- "└ Cancelled workflow setup" (from the incorrect
cancel()call at line 270) - "└ Success! Next steps:..." (from the
outro()call at line 272)
Expected: Only the success message should be displayed: "└ Success! Next steps:..."
According to the @clack/prompts documentation, the cancel() function displays a cancellation message and should only be called when the user actually cancels (detected via isCancel()). Lines 80, 97, 115, and 128 in the same file show the correct pattern where cancel() is called only when isCancel() returns true. The cancel() call at line 270 should be removed to allow only the success message to be displayed.
| { | ||
| 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'))}`; | ||
| }, |
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 "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 Details
diff --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');
Analysis
Next.js-specific tasks not guarded by template condition
What fails: The "Configuring Next.js config" (line 174) and "Creating API route handler" (line 244) tasks in packages/cli/src/commands/init.ts have no enabled condition and always execute, regardless of which template the user selected. When a user selects 'hono' or 'nitro' templates, these tasks attempt to:
- Read/modify
next.config.tswhich doesn't exist in non-Next.js projects (line 178) - Create API routes in the
app/api/directory structure withNextResponseimports, which are specific to Next.js (lines 247-266)
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:
- "Configuring Next.js config" would throw a file not found error when trying to read
next.config.ts - "Creating API route handler" would create files in the wrong directory structure and import from
next/serverwhich doesn't exist in Hono/Nitro projects
Expected: Only Next.js-specific tasks should execute when template === 'next'. Tasks that work across all templates (like "Creating example workflow") should have no template condition.
Fix applied: Added enabled: template === 'next' condition to both tasks at lines 174 and 244.
| 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 comment
The reason will be displayed to describe this comment to others. Learn more.
| mkdirSync(workflowsPath); | |
| mkdirSync(workflowsPath, { recursive: true }); |
The mkdirSync() call for creating the workflows directory is missing the recursive: true option, which could cause it to fail if parent directories don't exist.
View Details
Analysis
Missing recursive: true option in mkdirSync() call for workflows directory
What fails: The mkdirSync(workflowsPath) call at line 198 in packages/cli/src/commands/init.ts lacks the recursive: true option, inconsistent with the same file's line 247 mkdirSync(apiPath, { recursive: true }) and violating Node.js best practices for directory creation.
How to reproduce: While this particular code path won't fail in normal execution (since projectPath is guaranteed to exist), the missing option represents a defensive programming gap. The issue becomes apparent when:
- Code evolves to support nested workflows directories (e.g.,
workflows/sub/dir) - The pattern is compared with line 247 which correctly includes
{ recursive: true } - The codebase already uses this pattern elsewhere (e.g.,
packages/cli/src/lib/inspect/auth.tsline 65)
Expected behavior: According to Node.js fs.mkdirSync() documentation, when recursive is false (the default), mkdirSync throws an ENOENT error if parent directories don't exist. Using { recursive: true } follows Node.js best practices and matches the codebase's existing patterns.
Result: Changed line 198 from mkdirSync(workflowsPath) to mkdirSync(workflowsPath, { recursive: true }) for consistency and defensive programming alignment with line 247 and codebase patterns.
| { | ||
| 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'; | ||
| }, |
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 "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 Details
diff --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');
Analysis
Template-specific tasks execute regardless of template selection
What fails: Two tasks in the init command always execute even when user selects non-Next.js templates (hono or nitro):
- "Configuring Next.js config" task (lines 173-191) tries to read
next.config.ts - "Creating API route handler" task (lines 242-266) creates Next.js-specific API routes in
app/api/directory
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 readFileSync(path.join(projectPath, 'next.config.ts'), 'utf8') with error:
ENOENT: no such file or directory, open '.../next.config.ts'
Expected behavior: These tasks should only run when template === 'next', matching the pattern used by other template-dependent tasks like "Configuring TypeScript intellisense" (line 157) which has enabled: useTsPlugin.
Verification: @clack/prompts Task type documentation confirms tasks support enabled?: boolean property with comment: "If enabled === false the task will be skipped"
Fix: Added enabled: template === 'next' property to both tasks at lines 174 and 243.
| 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)}`; |
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-app regardless of the template selected.
View Details
📝 Patch Details
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..3dad590 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -133,15 +133,64 @@ export default class Init extends BaseCommand {
? path.join(process.cwd(), projectName)
: process.cwd();
- await tasks([
+ const templateLabel =
+ template === 'next' ? 'Next.js' : template === 'hono' ? 'Hono' : 'Nitro';
+
+ 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}\`);
+}`;
+
+ const tasksToRun = [
{
- title:
- template === 'next' ? 'Creating Next.js app' : 'Creating Hono app',
+ title: `Creating ${templateLabel} 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)}`;
+ if (template === 'next') {
+ 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)}`;
+ } else {
+ message(
+ `Creating a new ${template === 'hono' ? 'Hono' : 'Nitro'} app`
+ );
+ await execAsync(
+ `npx create-nitro@latest ${projectName} --template ${template === 'hono' ? 'hono' : 'nitro-app'}`
+ );
+ return `Created ${templateLabel} app in ${chalk.cyan(projectPath)}`;
+ }
},
},
{
@@ -160,6 +209,9 @@ export default class Init extends BaseCommand {
const tsConfig = JSON.parse(
readFileSync(path.join(projectPath, 'tsconfig.json'), 'utf8')
);
+ if (!tsConfig.compilerOptions.plugins) {
+ tsConfig.compilerOptions.plugins = [];
+ }
tsConfig.compilerOptions.plugins.push({
name: 'workflow',
});
@@ -171,23 +223,43 @@ export default class Init extends BaseCommand {
},
},
{
- title: 'Configuring Next.js config',
+ title: `Configuring ${templateLabel} app`,
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';
+ if (template === 'next') {
+ 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';
+ } else {
+ message(`Configuring ${templateLabel} app for Workflow`);
+ const nitroConfigPath = path.join(projectPath, 'nitro.config.ts');
+ let nitroConfig = readFileSync(nitroConfigPath, 'utf8');
+ nitroConfig = nitroConfig.replace(
+ /import { defineNitroConfig } from "nitro";/g,
+ "import { defineNitroConfig } from 'nitro';\nimport { withWorkflow } from 'workflow/nitro';"
+ );
+ nitroConfig = nitroConfig.replace(
+ /^export default defineNitroConfig\(/m,
+ 'export default withWorkflow(defineNitroConfig('
+ );
+ nitroConfig = nitroConfig.replace(/\);$/m, '));');
+ writeFileSync(nitroConfigPath, nitroConfig);
+ return `Configured ${templateLabel} app for Workflow`;
+ }
},
},
{
@@ -195,43 +267,7 @@ export default class Init extends BaseCommand {
task: async (message) => {
message(`Creating example workflow`);
const workflowsPath = path.join(projectPath, 'workflows');
- mkdirSync(workflowsPath);
- 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}\`);
-}`;
+ mkdirSync(workflowsPath, { recursive: true });
writeFileSync(
path.join(workflowsPath, 'user-signup.ts'),
workflowContent
@@ -242,12 +278,13 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
{
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';
+ if (template === 'next') {
+ 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";
@@ -261,11 +298,35 @@ export async function POST(request: Request) {
message: "User signup workflow started",
});
}`
- );
- return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`;
+ );
+ return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`;
+ } else {
+ message(`Creating API route handler`);
+ const apiPath = path.join(projectPath, 'routes', 'api');
+ mkdirSync(apiPath, { recursive: true });
+ writeFileSync(
+ path.join(apiPath, 'signup.post.ts'),
+ `import { start } from 'workflow/api';
+import { handleUserSignup } from "~/workflows/user-signup";
+
+export default defineEventHandler(async (event) => {
+ const { email } = await readBody(event);
+
+ // Executes asynchronously and doesn't block your app
+ await start(handleUserSignup, [email]);
+
+ return {
+ message: "User signup workflow started",
+ };
+});`
+ );
+ return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'routes', 'api', 'signup.post.ts'))}`;
+ }
},
},
- ]);
+ ];
+
+ await tasks(tasksToRun);
cancel('Cancelled workflow setup');
Analysis
Template selection logic ignored in workflow init command
What fails: The workflow init command accepts user selection of 'hono', 'next', or 'nitro' templates (lines 101-111) but always creates a Next.js app using create-next-app regardless 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:
- Use
create-nitrowith appropriate template flag for app scaffolding - Configure nitro.config.ts instead of next.config.ts
- Create routes in the Nitro/Hono directory structure (routes/api/signup.post.ts)
- Use appropriate framework APIs in generated code
Root cause: Git history shows template selection UI was added in commit ceedafe but implementation was never completed - the task logic still only supports Next.js.
Fix implemented:
- Modified the project creation task to conditionally execute
create-next-appfor 'next' orcreate-nitrofor 'hono'/'nitro' - Added template-specific configuration logic for Next.js config vs Nitro config files
- Added template-specific API route handler generation with correct framework APIs
- Updated all user-facing messages to reflect the selected template
now we cna do
workflow initto create a new basic workflow app from a list of user-selected options