Skip to content
Draft
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/cyan-animals-walk.md
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
5 changes: 3 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@
"@workflow/tsconfig": "workspace:*"
},
"dependencies": {
"@clack/prompts": "1.0.0-alpha.6",
"@oclif/core": "^4.0.0",
"@oclif/plugin-help": "^6.0.0",
"@swc/core": "1.11.24",
"@workflow/swc-plugin": "workspace:*",
"@workflow/errors": "workspace:*",
"@workflow/core": "workspace:*",
"@workflow/errors": "workspace:*",
"@workflow/swc-plugin": "workspace:*",
"@workflow/web": "workspace:*",
"@workflow/world": "workspace:*",
"@workflow/world-local": "workspace:*",
Expand Down
271 changes: 249 additions & 22 deletions packages/cli/src/commands/init.ts
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',
Expand All @@ -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)}`;
Comment on lines +139 to +144
Copy link
Contributor

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 choice

Result: 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-nitro with 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-app for 'next' or create-nitro for '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

},
},
{
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
Copy link
Contributor

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):

  1. "Configuring Next.js config" task (lines 173-191) tries to read next.config.ts
  2. "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 --yes

Result: 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.

},
{
title: 'Creating example workflow',
task: async (message) => {
message(`Creating example workflow`);
const workflowsPath = path.join(projectPath, 'workflows');
mkdirSync(workflowsPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:

  1. Code evolves to support nested workflows directories (e.g., workflows/sub/dir)
  2. The pattern is compared with line 247 which correctly includes { recursive: true }
  3. The codebase already uses this pattern elsewhere (e.g., packages/cli/src/lib/inspect/auth.ts line 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.

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
Copy link
Contributor

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:

  1. Read/modify next.config.ts which doesn't exist in non-Next.js projects (line 178)
  2. Create API routes in the app/api/ directory structure with NextResponse imports, 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/server which 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.

},
]);

cancel('Cancelled workflow setup');

Comment on lines +270 to +271
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 --yes

Result: The user sees two conflicting messages:

  1. "└ Cancelled workflow setup" (from the incorrect cancel() call at line 270)
  2. "└ 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.

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')}`
);
}
}
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading