@sylphx/flow 3.16.0 → 3.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 3.17.1 (2026-02-05)
4
+
5
+ Add Hono RPC patterns and declarative engineering principles
6
+
7
+ ### šŸ“š Documentation
8
+
9
+ - **builder:** prioritize declarative style in engineering principles ([f2e5bc4](https://github.com/SylphxAI/flow/commit/f2e5bc482ce554097e2bc90943c22f3d497f0925))
10
+ - **builder:** add Hono RPC patterns for split clients ([c848796](https://github.com/SylphxAI/flow/commit/c8487965a8341aac1f9644d5de75c1dd36b1c661))
11
+
12
+ ## 3.17.0 (2026-02-05)
13
+
14
+ ### ā™»ļø Refactoring
15
+
16
+ - **flow:** migrate CLI prompts to Clack and logging to Pino ([08aceb7](https://github.com/SylphxAI/flow/commit/08aceb7d))
17
+
18
+ ### šŸ› Bug Fixes
19
+
20
+ - **flow:** remove unsafe separator pattern from Clack prompts ([aaf17a7](https://github.com/SylphxAI/flow/commit/aaf17a7d))
21
+
22
+ ### šŸ“ Documentation
23
+
24
+ - **builder:** add Zod v4 to tech stack ([2b37327](https://github.com/SylphxAI/flow/commit/2b373279))
25
+ - **builder:** restructure tech stack - Zod as standalone category ([90875ba](https://github.com/SylphxAI/flow/commit/90875baa))
26
+
3
27
  ## 3.16.0 (2026-02-05)
4
28
 
5
29
  ### ✨ Features
@@ -46,13 +46,15 @@ State-of-the-art industrial standard. Every time. Would you stake your reputatio
46
46
 
47
47
  **Framework & Runtime:** Next.js 16+, React, Bun
48
48
 
49
+ **Schema & Validation:** Zod v4
50
+
49
51
  **Data & API:** Hono + @hono/zod-openapi + hc (type-safe client), React Query, Drizzle ORM
50
52
 
51
53
  **Database & Infrastructure:** Neon PostgreSQL, Upstash Workflow, Vercel, Vercel Blob, Modal (serverless long-running)
52
54
 
53
55
  **UI & Styling:** Base UI, Tailwind CSS v4 (CSS-first), Motion v12 (animation)
54
56
 
55
- **Forms:** React Hook Form + Zod
57
+ **Forms:** React Hook Form + @hookform/resolvers
56
58
 
57
59
  **Tables & Lists:** TanStack Table, TanStack Virtual
58
60
 
@@ -121,9 +123,10 @@ State-of-the-art industrial standard. Every time. Would you stake your reputatio
121
123
 
122
124
  ## Engineering
123
125
 
126
+ - **Declarative over imperative** — describe WHAT, not HOW; prefer expressions over statements, data over control flow
127
+ - **Pure functions** — no side effects, deterministic output; isolate impure code at boundaries
124
128
  - **Single Source of Truth** — one authoritative source for every state, behavior, and decision
125
129
  - **Type safety** — end-to-end across all boundaries (Hono RPC, Zod, strict TypeScript)
126
- - **Pure functions** — no side effects, deterministic output; isolate impure code at boundaries
127
130
  - **Decoupling** — minimize dependencies, use interfaces and dependency injection
128
131
  - **Modularisation** — single responsibility, clear boundaries, independent deployability
129
132
  - **Composition over inheritance** — build primitives that compose
@@ -183,6 +186,42 @@ drizzle-kit migrate && drizzle-kit push --dry-run
183
186
  ```
184
187
  If there's any diff, migration is incomplete — fail the build.
185
188
 
189
+ ## Hono RPC
190
+
191
+ **Split clients by entity** — monolithic `hc<AppType>` kills IDE performance at 100+ routes.
192
+
193
+ ```typescript
194
+ // āœ… Split: one Hono app + one client per entity
195
+ const booksApp = new Hono()
196
+ .get('/', (c) => c.json([]))
197
+ .post('/', (c) => c.json({ id: 1 }))
198
+ .get('/:id', (c) => c.json({ id: c.req.param('id') }))
199
+
200
+ const authorsApp = new Hono()
201
+ .get('/', (c) => c.json([]))
202
+ .post('/', (c) => c.json({ id: 1 }))
203
+
204
+ // Main app — chain with .route()
205
+ const app = new Hono()
206
+ .route('/books', booksApp)
207
+ .route('/authors', authorsApp)
208
+
209
+ // Clients — split by entity, <100 routes each
210
+ export const booksClient = hc<typeof booksApp>('/api/books')
211
+ export const authorsClient = hc<typeof authorsApp>('/api/authors')
212
+ ```
213
+
214
+ **Chain routes** — separate `app.get()` calls break type inference:
215
+ ```typescript
216
+ // āœ… Chained — types work
217
+ const app = new Hono().get('/', h1).post('/', h2)
218
+
219
+ // āŒ Separate — types broken
220
+ const app = new Hono()
221
+ app.get('/', h1)
222
+ app.post('/', h2)
223
+ ```
224
+
186
225
  ## Frontend
187
226
 
188
227
  - **Semantic HTML** — correct elements (nav, main, article, section, aside, header, footer)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "3.16.0",
3
+ "version": "3.17.1",
4
4
  "description": "One CLI to rule them all. Unified orchestration layer for AI coding assistants. Auto-detection, auto-installation, auto-upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,16 +25,17 @@
25
25
  "prepublishOnly": "echo 'Using assets from packages/flow/assets'"
26
26
  },
27
27
  "dependencies": {
28
- "commander": "^14.0.2",
29
- "chalk": "^5.6.2",
28
+ "@clack/prompts": "^0.9.0",
30
29
  "boxen": "^8.0.1",
30
+ "chalk": "^5.6.2",
31
+ "commander": "^14.0.2",
32
+ "debug": "^4.4.3",
31
33
  "gradient-string": "^3.0.0",
32
- "ora": "^9.0.0",
33
- "inquirer": "^12.10.0",
34
34
  "gray-matter": "^4.0.3",
35
+ "pino": "^9.0.0",
36
+ "pino-pretty": "^11.0.0",
35
37
  "yaml": "^2.8.1",
36
- "zod": "^4.1.12",
37
- "debug": "^4.4.3"
38
+ "zod": "^4.1.12"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^24.9.2",
@@ -7,7 +7,6 @@ import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import chalk from 'chalk';
10
- import inquirer from 'inquirer';
11
10
  import { FlowExecutor } from '../../core/flow-executor.js';
12
11
  import { targetManager } from '../../core/target-manager.js';
13
12
  import { AutoUpgrade } from '../../services/auto-upgrade.js';
@@ -18,6 +17,7 @@ import { extractAgentInstructions, loadAgentContent } from '../../utils/agent-en
18
17
  import { showAttachSummary, showHeader } from '../../utils/display/banner.js';
19
18
  import { CLIError } from '../../utils/error-handler.js';
20
19
  import { UserCancelledError } from '../../utils/errors.js';
20
+ import { log, promptConfirm, promptSelect } from '../../utils/prompts/index.js';
21
21
  import { ensureTargetInstalled, promptForTargetSelection } from '../../utils/target-selection.js';
22
22
  import { resolvePrompt } from './prompt.js';
23
23
  import type { FlowOptions } from './types.js';
@@ -62,66 +62,52 @@ function configureProviderEnv(provider: 'kimi' | 'zai', apiKey: string): void {
62
62
  * Select and configure provider for Claude Code (silent unless prompting)
63
63
  */
64
64
  async function selectProvider(configService: GlobalConfigService): Promise<void> {
65
- try {
66
- const providerConfig = await configService.loadProviderConfig();
67
- const defaultProvider = providerConfig.claudeCode.defaultProvider;
68
-
69
- // If not "ask-every-time", use the default provider silently
70
- if (defaultProvider !== 'ask-every-time') {
71
- if (defaultProvider === 'kimi' || defaultProvider === 'zai') {
72
- const provider = providerConfig.claudeCode.providers[defaultProvider];
73
- if (provider?.apiKey) {
74
- configureProviderEnv(defaultProvider, provider.apiKey);
75
- }
65
+ const providerConfig = await configService.loadProviderConfig();
66
+ const defaultProvider = providerConfig.claudeCode.defaultProvider;
67
+
68
+ // If not "ask-every-time", use the default provider silently
69
+ if (defaultProvider !== 'ask-every-time') {
70
+ if (defaultProvider === 'kimi' || defaultProvider === 'zai') {
71
+ const provider = providerConfig.claudeCode.providers[defaultProvider];
72
+ if (provider?.apiKey) {
73
+ configureProviderEnv(defaultProvider, provider.apiKey);
76
74
  }
77
- return;
78
- }
79
-
80
- // Ask user which provider to use for this session
81
- const { selectedProvider, rememberChoice } = await inquirer.prompt([
82
- {
83
- type: 'list',
84
- name: 'selectedProvider',
85
- message: 'Select provider:',
86
- choices: [
87
- { name: 'Default (Claude Code built-in)', value: 'default' },
88
- { name: 'Kimi', value: 'kimi' },
89
- { name: 'Z.ai', value: 'zai' },
90
- ],
91
- default: 'default',
92
- },
93
- {
94
- type: 'confirm',
95
- name: 'rememberChoice',
96
- message: 'Remember this choice?',
97
- default: true,
98
- },
99
- ]);
100
-
101
- // Save choice if user wants to remember
102
- if (rememberChoice) {
103
- providerConfig.claudeCode.defaultProvider = selectedProvider;
104
- await configService.saveProviderConfig(providerConfig);
105
75
  }
76
+ return;
77
+ }
106
78
 
107
- // Configure environment variables based on selection
108
- if (selectedProvider === 'kimi' || selectedProvider === 'zai') {
109
- const provider = providerConfig.claudeCode.providers[selectedProvider];
79
+ // Ask user which provider to use for this session
80
+ const selectedProvider = await promptSelect({
81
+ message: 'Select provider:',
82
+ options: [
83
+ { label: 'Default (Claude Code built-in)', value: 'default' },
84
+ { label: 'Kimi', value: 'kimi' },
85
+ { label: 'Z.ai', value: 'zai' },
86
+ ],
87
+ initialValue: 'default',
88
+ });
89
+
90
+ const rememberChoice = await promptConfirm({
91
+ message: 'Remember this choice?',
92
+ initialValue: true,
93
+ });
94
+
95
+ // Save choice if user wants to remember
96
+ if (rememberChoice) {
97
+ providerConfig.claudeCode.defaultProvider = selectedProvider;
98
+ await configService.saveProviderConfig(providerConfig);
99
+ }
110
100
 
111
- if (!provider?.apiKey) {
112
- console.log(chalk.yellow(' API key not configured. Use: sylphx-flow settings'));
113
- return;
114
- }
101
+ // Configure environment variables based on selection
102
+ if (selectedProvider === 'kimi' || selectedProvider === 'zai') {
103
+ const provider = providerConfig.claudeCode.providers[selectedProvider];
115
104
 
116
- configureProviderEnv(selectedProvider, provider.apiKey);
117
- }
118
- } catch (error: unknown) {
119
- // Handle user cancellation (Ctrl+C)
120
- const err = error as Error & { name?: string };
121
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
122
- throw new UserCancelledError('Provider selection cancelled');
105
+ if (!provider?.apiKey) {
106
+ log.warn('API key not configured. Use: sylphx-flow settings');
107
+ return;
123
108
  }
124
- throw error;
109
+
110
+ configureProviderEnv(selectedProvider, provider.apiKey);
125
111
  }
126
112
  }
127
113
 
@@ -221,11 +207,11 @@ export async function executeFlowV2(
221
207
 
222
208
  if (!installedTargets.includes(selectedTargetId)) {
223
209
  const installation = targetInstaller.getInstallationInfo(selectedTargetId);
224
- console.log(chalk.yellow(`\n ${installation?.name} not installed`));
210
+ log.warn(`${installation?.name} not installed`);
225
211
  const installed = await targetInstaller.install(selectedTargetId, true);
226
212
 
227
213
  if (!installed) {
228
- console.log(chalk.red(` Cannot proceed: installation failed\n`));
214
+ log.error('Cannot proceed: installation failed');
229
215
  process.exit(1);
230
216
  }
231
217
  }
@@ -313,7 +299,7 @@ export async function executeFlowV2(
313
299
  } catch (error) {
314
300
  // Handle user cancellation gracefully
315
301
  if (error instanceof UserCancelledError) {
316
- console.log(chalk.yellow('\n Cancelled'));
302
+ log.warn('Cancelled');
317
303
  try {
318
304
  await executor.cleanup(projectPath);
319
305
  } catch {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import chalk from 'chalk';
7
- import inquirer from 'inquirer';
7
+ import { log, type MultiselectOption, promptMultiselect } from '../../utils/prompts/index.js';
8
8
 
9
9
  // ============================================================================
10
10
  // Types
@@ -47,16 +47,16 @@ export const getEnabledKeys = (config: ConfigMap): string[] =>
47
47
  Object.keys(config).filter((key) => config[key]?.enabled);
48
48
 
49
49
  /**
50
- * Build checkbox choices from available items
50
+ * Build multiselect options from available items
51
51
  */
52
- export const buildChoices = <T extends string>(
52
+ export const buildOptions = <T extends string>(
53
53
  available: Record<T, string>,
54
54
  enabledKeys: string[]
55
- ): Array<{ name: string; value: T; checked: boolean }> =>
55
+ ): MultiselectOption<T>[] =>
56
56
  Object.entries(available).map(([key, name]) => ({
57
- name: name as string,
57
+ label: name as string,
58
58
  value: key as T,
59
- checked: enabledKeys.includes(key),
59
+ hint: enabledKeys.includes(key) ? 'enabled' : undefined,
60
60
  }));
61
61
 
62
62
  /**
@@ -82,11 +82,11 @@ export const printHeader = (icon: string, title: string): void => {
82
82
  };
83
83
 
84
84
  /**
85
- * Print confirmation message
85
+ * Print confirmation message using Clack log
86
86
  */
87
87
  export const printConfirmation = (itemType: string, count: number): void => {
88
- console.log(chalk.green(`\nāœ“ ${itemType} configuration saved`));
89
- console.log(chalk.dim(` Enabled ${itemType.toLowerCase()}: ${count}`));
88
+ log.success(`${itemType} configuration saved`);
89
+ log.info(`Enabled ${itemType.toLowerCase()}: ${count}`);
90
90
  };
91
91
 
92
92
  // ============================================================================
@@ -108,15 +108,15 @@ export const handleCheckboxConfig = async <T extends string>(
108
108
  // Get current enabled items
109
109
  const enabledKeys = getEnabledKeys(current);
110
110
 
111
- // Show checkbox prompt
112
- const { selected } = await inquirer.prompt([
113
- {
114
- type: 'checkbox',
115
- name: 'selected',
116
- message,
117
- choices: buildChoices(available, enabledKeys),
118
- },
119
- ]);
111
+ // Build options for multiselect
112
+ const multiselectOptions = buildOptions(available, enabledKeys);
113
+
114
+ // Show multiselect prompt
115
+ const selected = await promptMultiselect<T>({
116
+ message,
117
+ options: multiselectOptions,
118
+ initialValues: enabledKeys as T[],
119
+ });
120
120
 
121
121
  // Update config
122
122
  const updated = updateConfig(available, selected);