@crossdelta/flowcore

中文文档
0.1.3发布于 2026/10/26MITTypeScriptESM + CJS近 30 天下载 3871

Domain-agnostic CLI flow toolkit for interactive wizards

原始使用文档
文档目录33 节
  1. Why flowcore exists
  2. Core idea
  3. Installation
  4. Quick example
  5. What makes flowcore different
  6. Host-agnostic flows
  7. Fully type-safe context
  8. Non-interactive execution
  9. Step primitives
  10. Input
  11. Select
  12. Task
  13. Testing flows
  14. Flow Style Guide
  15. 1. Flows read top-to-bottom
  16. 2. Prefer explicit steps
  17. 3. Use skipWhen for flow decisions
  18. 4. Abort early and explicitly
  19. 5. Declare context mutations
  20. 6. Keep domain logic out of flows
  21. 7. Prefer small, named functions
  22. 8. No side effects outside tasks
  23. 9. One flow, one story
  24. 10. Flows are values
  25. Where flowcore fits
  26. Change tracking utilities
  27. When to use
  28. Non-fluent approach (recommended default)
  29. Fluent approach (optional sugar)
  30. API overview
  31. Important: Separation of concerns
  32. Philosophy
  33. License

@crossdelta/flowcore

Executable, type-safe flows for CLI, MCP, tests and agents.
Define what should happen — not how it is rendered.

flowcore is a small functional toolkit for building declarative, executable flows that can run in different hosts:

  • interactive CLI
  • non-interactive (tests, automation)
  • MCP / AI tools
  • future UIs or agents

It is not a prompt library.
It is a core execution primitive for flows.


Why flowcore exists

Most CLI tools tightly couple interaction with execution:

  • user prompts
  • control flow
  • side effects
  • rendering / TTY concerns

This coupling makes it hard to:

  • test flows without a terminal
  • reuse logic outside the CLI
  • expose the same behavior to MCP or AI tools
  • plan, simulate, or review side effects before execution

flowcore separates what happens from how it is driven.

One flow. Many hosts. Zero rewrites.


Core idea

Flow   = declarative steps + typed context
Runner = decides how the flow is executed

You describe what happens.
The runner decides how interaction works.


Installation

bun add @crossdelta/flowcore
# or
npm install @crossdelta/flowcore

Quick example

import { createFlow, runFlow, type FlowContext } from '@crossdelta/flowcore'

interface UserContext extends FlowContext {
  name: string
  role: 'admin' | 'user'
}

const flow = createFlow<UserContext>()

const steps = [
  flow.input('name').build(),

  flow.select('role')
    .choices(['admin', 'user'])
    .build(),

  flow.task('Save user')
    .steps([
      (ctx) => {
        console.log(`Hello ${ctx.name} (${ctx.role})`)
      },
    ])
    .produces('name', 'role')
    .build(),
]

await runFlow(steps, {
  initialContext: {},
})

What makes flowcore different

Host-agnostic flows

The same flow can run:

  • interactively in a CLI
  • non-interactively in tests
  • inside MCP tools
  • later in agents or UIs

No branching.
No duplication.


Fully type-safe context

Each step enriches a shared, typed context:

interface MyContext extends FlowContext {
  eventType: string
  service?: string
}

TypeScript guarantees correctness across steps.


Non-interactive execution

await runFlow(steps, {
  initialContext: {
    name: 'Max',
    role: 'admin',
  },
})

Perfect for:

  • tests
  • automation
  • AI execution
  • dry-runs

Step primitives

Input

flow.input('eventType')
  .prompt({
    title: 'Event Type',
    message: 'Event type to publish (e.g., order.created)',
  })
  .skipWhen((ctx) => ctx.eventType !== undefined)
  .build()

Select

flow.select('role')
  .choices(['admin', 'user'])
  .optional('Skip')
  .build()

Task

flow.task('Publish event')
  .steps([
    loadDataFromInput,
    publishToNats,
  ])
  .produces('data', 'published', 'usedMock')
  .build()

Testing flows

Flows do not depend on TTY or rendering.

Note on .produces(...)

.produces(...) documents which context fields are guaranteed to exist or be modified after a task completes.

It does not change runtime behavior, execute code, or perform validation. It is a declarative promise for readers, tooling, and future hosts (CLI, MCP, agents).

test('publish event flow', async () => {
  const result = await runFlow(steps, {
    initialContext: {
      eventType: 'order.created',
    },
  })

  expect(result.context.published).toBe(true)
})

No prompt mocks.
No renderer stubs.


Flow Style Guide

These rules are not enforced by the compiler, but strongly recommended.

1. Flows read top-to-bottom

A reader should understand the flow without jumping between files.

Good:

input → abort → input → task

Bad:

task → input → hidden condition → side effect

2. Prefer explicit steps

Always use steps([...]) for tasks with multiple operations.

Good:

flow.task('Publish event')
  .steps([loadData, publish])

Avoid:

.run(loadData).run(publish)

3. Use skipWhen for flow decisions

skipWhen expresses control flow, not validation.

Good:

.skipWhen(hasExplicitData)

4. Abort early and explicitly

Use abortIf as its own step.

Good:

flow.abortIf(isInvalid, getErrorMessage)

5. Declare context mutations

Always document what a task mutates.

Good:

.produces('data', 'published')

6. Keep domain logic out of flows

Flows orchestrate.
Domain logic lives in functions called by steps.


7. Prefer small, named functions

Anonymous lambdas are fine for trivial logic only.


8. No side effects outside tasks

Inputs and conditions must be pure.


9. One flow, one story

If a flow feels too long, split it.


10. Flows are values

Treat flows as data that can be executed by different hosts.


Where flowcore fits

flowcore is ideal if you:

  • build CLIs with complex wizards
  • want to expose the same logic to MCP or AI tools
  • care about testability and determinism
  • want to separate interaction from execution

It is not intended for simple one-off prompt scripts.


Change tracking utilities

flowcore provides utilities for tracking operation outcomes (created/updated/skipped).

These are generic and host-agnostic — not tied to files or any specific domain.

When to use

Use ChangeResult when you need to:

  • Track whether a resource was created, updated, or skipped
  • Record outcomes to an artifacts sink (console, file system, database)
  • React differently based on operation outcome
  • Keep track of what changed during a flow
import { change, recordChange, isCreated, type ArtifactsSink } from '@crossdelta/flowcore'

// Perform some operation
const result = await generateContract()

// Map to ChangeResult
const outcome = result.created
  ? change.created(result.path)
  : change.skipped('Already exists')

// Record to artifacts sink
recordChange(ctx.artifacts, 'Contract', outcome)

// React based on outcome
if (isCreated(outcome)) {
  ctx.effects.emit(contractCreated(outcome.ref, ctx.eventType))
}

Why this is the default:

  • Explicit and clear
  • Easy to follow in complex flows
  • No hidden behavior
  • Familiar imperative style

Fluent approach (optional sugar)

import { change, trackChange } from '@crossdelta/flowcore'

trackChange(change.created('/path/to/contract.ts'))
  .record(ctx.artifacts, 'Contract')
  .onCreated((ref) => {
    ctx.effects.emit(contractCreated(ref, ctx.eventType))
  })

Use sparingly:

  • Nice for simple linear cases
  • Avoid in complex conditional logic
  • Remember: .record() and .onX() hooks are independent (no implicit side effects)

API overview

Types:

type ChangeResult<Ref = string> =
  | { status: 'created'; ref: Ref }
  | { status: 'updated'; ref: Ref }
  | { status: 'skipped'; reason?: string }

interface ArtifactsSink {
  created(label: string, ref?: string): void
  updated(label: string, ref?: string): void
  skipped(label: string, reason?: string): void
}

Constructors:

change.created(ref)   // => ChangeResult with status: 'created'
change.updated(ref)   // => ChangeResult with status: 'updated'
change.skipped(reason?) // => ChangeResult with status: 'skipped'

Type guards:

isCreated(result)  // narrows to { status: 'created'; ref: Ref }
isUpdated(result)  // narrows to { status: 'updated'; ref: Ref }
isSkipped(result)  // narrows to { status: 'skipped'; reason?: string }

Non-fluent helper:

recordChange(artifacts, label, result) // records to sink, returns result

Fluent helper:

trackChange(result)
  .status()              // get status
  .ref()                 // get ref (or undefined if skipped)
  .record(artifacts, label)  // record to sink (returns this)
  .onCreated(fn)         // execute fn if created (returns this)
  .onUpdated(fn)         // execute fn if updated (returns this)
  .onSkipped(fn)         // execute fn if skipped (returns this)

Important: Separation of concerns

.record() and .onX() hooks are independent:

// ✅ This ONLY records to artifacts (hook NOT called)
trackChange(result).record(artifacts, 'File')

// ✅ This ONLY calls the hook (artifacts NOT recorded)
trackChange(result).onCreated((ref) => console.log(ref))

// ✅ Both together work independently
trackChange(result)
  .record(artifacts, 'File')
  .onCreated((ref) => console.log(ref))

Why this matters:

  • No hidden behavior
  • Explicit control over side effects
  • Easy to reason about in complex flows

Philosophy

  • Functional, not object-oriented
  • Declarative, not imperative
  • Testable by default
  • Minimal surface area
  • No magic

Flows are values. Execution is a detail.


License

MIT © crossdelta

版本与发布记录

@crossdelta/flowcore 共发布 4 个版本,首次发布于 2026/02/22;过去 12 个月发布了 4 个版本。

版本发布日期距今
0.1.3latest2026/10/2616 天前
0.1.22026/03/137 个月前
0.1.12026/02/278 个月前
0.1.02026/02/228 个月前

关于 @crossdelta/flowcore 的常见问题

@crossdelta/flowcore 是什么?

Domain-agnostic CLI flow toolkit for interactive wizards

如何安装 @crossdelta/flowcore?

在项目目录运行 npm install @crossdelta/flowcore,或使用 pnpm add / yarn add / bun add。

@crossdelta/flowcore 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crossdelta/flowcore 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@crossdelta/flowcore 可以商用吗?

@crossdelta/flowcore 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crossdelta/flowcore 的下载量有多少?

根据 npm 官方下载统计,@crossdelta/flowcore 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,871 次,日均约 129 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crossdelta/flowcore 还在维护吗?

最新版本 0.1.3 发布于 2026/10/26(16 天前);至今共发布 4 个版本,其中过去 12 个月发布了 4 个;首次发布于 2026/02/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。