@synergenius/flow-weaver 0.23.5 → 0.24.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.
Files changed (49) hide show
  1. package/dist/agent/index.d.ts +2 -1
  2. package/dist/agent/index.js +2 -0
  3. package/dist/agent/providers/anthropic.js +14 -1
  4. package/dist/agent/providers/claude-cli.d.ts +1 -1
  5. package/dist/agent/providers/claude-cli.js +4 -1
  6. package/dist/agent/providers/openai-compat.d.ts +1 -1
  7. package/dist/agent/providers/openai-compat.js +2 -1
  8. package/dist/agent/providers/platform.d.ts +1 -1
  9. package/dist/agent/providers/platform.js +3 -1
  10. package/dist/agent/types.d.ts +21 -2
  11. package/dist/agent/types.js +6 -1
  12. package/dist/api/generate-in-place.d.ts +0 -9
  13. package/dist/api/generate-in-place.js +6 -54
  14. package/dist/api/generate.d.ts +4 -5
  15. package/dist/api/generate.js +6 -26
  16. package/dist/cli/commands/compile.d.ts +0 -5
  17. package/dist/cli/commands/compile.js +36 -8
  18. package/dist/cli/commands/context.js +4 -6
  19. package/dist/cli/commands/create.js +6 -14
  20. package/dist/cli/commands/describe.js +6 -10
  21. package/dist/cli/commands/diagram.js +18 -25
  22. package/dist/cli/commands/diff.js +7 -14
  23. package/dist/cli/commands/docs.js +3 -6
  24. package/dist/cli/commands/doctor.js +1 -1
  25. package/dist/cli/commands/export.js +1 -1
  26. package/dist/cli/commands/grammar.js +3 -4
  27. package/dist/cli/commands/implement.js +8 -13
  28. package/dist/cli/commands/market.js +4 -8
  29. package/dist/cli/commands/migrate.js +2 -1
  30. package/dist/cli/commands/modify.js +2 -1
  31. package/dist/cli/commands/openapi.js +2 -1
  32. package/dist/cli/commands/pattern.js +3 -2
  33. package/dist/cli/commands/strip.js +3 -6
  34. package/dist/cli/commands/validate.js +6 -1
  35. package/dist/cli/flow-weaver.mjs +781 -791
  36. package/dist/cli/index.js +10 -12
  37. package/dist/cli/postinstall.d.ts +16 -0
  38. package/dist/cli/postinstall.js +119 -0
  39. package/dist/cli/utils/parse-int-strict.d.ts +7 -0
  40. package/dist/cli/utils/parse-int-strict.js +17 -0
  41. package/dist/cli/utils/safe-write.d.ts +18 -0
  42. package/dist/cli/utils/safe-write.js +54 -0
  43. package/dist/generated-version.d.ts +1 -1
  44. package/dist/generated-version.js +1 -1
  45. package/dist/mcp/tools-debug.js +2 -2
  46. package/docs/reference/cli-reference.md +0 -1
  47. package/docs/reference/compilation.md +2 -10
  48. package/package.json +4 -2
  49. package/scripts/postinstall.cjs +86 -0
@@ -4,7 +4,8 @@
4
4
  * Provider-agnostic agent loop with MCP bridge for tool execution.
5
5
  * Built-in providers: Anthropic API, Claude CLI, OpenAI-compatible (GPT-4o, Groq, Ollama, etc).
6
6
  */
7
- export type { StreamEvent, AgentMessage, AgentProvider, ToolDefinition, ToolExecutor, ToolEvent, McpBridge, AgentLoopOptions, AgentLoopResult, StreamOptions, SpawnFn, ClaudeCliProviderOptions, CliSessionOptions, Logger, } from './types.js';
7
+ export type { SplitPrompt, StreamEvent, AgentMessage, AgentProvider, ToolDefinition, ToolExecutor, ToolEvent, McpBridge, AgentLoopOptions, AgentLoopResult, StreamOptions, SpawnFn, ClaudeCliProviderOptions, CliSessionOptions, Logger, } from './types.js';
8
+ export { joinSplitPrompt } from './types.js';
8
9
  export { runAgentLoop } from './agent-loop.js';
9
10
  export { AnthropicProvider, createAnthropicProvider } from './providers/anthropic.js';
10
11
  export type { AnthropicProviderOptions } from './providers/anthropic.js';
@@ -4,6 +4,8 @@
4
4
  * Provider-agnostic agent loop with MCP bridge for tool execution.
5
5
  * Built-in providers: Anthropic API, Claude CLI, OpenAI-compatible (GPT-4o, Groq, Ollama, etc).
6
6
  */
7
+ // Prompt utilities
8
+ export { joinSplitPrompt } from './types.js';
7
9
  // Agent loop
8
10
  export { runAgentLoop } from './agent-loop.js';
9
11
  // Providers
@@ -4,6 +4,19 @@
4
4
  *
5
5
  * Adapted from pack-weaver's streamAnthropicWithTools.
6
6
  */
7
+ /**
8
+ * Convert a SplitPrompt to Anthropic's system content block array.
9
+ * The prefix gets cache_control for prompt caching; the suffix does not.
10
+ */
11
+ function buildSystemBlocks(prompt) {
12
+ const blocks = [
13
+ { type: 'text', text: prompt.prefix, cache_control: { type: 'ephemeral' } },
14
+ ];
15
+ if (prompt.suffix) {
16
+ blocks.push({ type: 'text', text: prompt.suffix });
17
+ }
18
+ return blocks;
19
+ }
7
20
  export class AnthropicProvider {
8
21
  apiKey;
9
22
  model;
@@ -49,7 +62,7 @@ export class AnthropicProvider {
49
62
  model,
50
63
  max_tokens: maxTokens,
51
64
  stream: true,
52
- ...(options?.systemPrompt ? { system: options.systemPrompt } : {}),
65
+ ...(options?.systemPrompt ? { system: buildSystemBlocks(options.systemPrompt) } : {}),
53
66
  messages: apiMessages,
54
67
  ...(apiTools.length > 0 ? { tools: apiTools } : {}),
55
68
  });
@@ -5,7 +5,7 @@
5
5
  * Adapted from platform's streamClaudeCliChat. Platform-specific dependencies
6
6
  * (spawnSandboxed, getBinPath, config) are replaced with injectable options.
7
7
  */
8
- import type { AgentProvider, AgentMessage, ToolDefinition, StreamEvent, StreamOptions, ClaudeCliProviderOptions } from '../types.js';
8
+ import { type AgentProvider, type AgentMessage, type ToolDefinition, type StreamEvent, type StreamOptions, type ClaudeCliProviderOptions } from '../types.js';
9
9
  export declare class ClaudeCliProvider implements AgentProvider {
10
10
  private binPath;
11
11
  private cwd;
@@ -6,6 +6,7 @@
6
6
  * (spawnSandboxed, getBinPath, config) are replaced with injectable options.
7
7
  */
8
8
  import { spawn as nodeSpawn } from 'node:child_process';
9
+ import { joinSplitPrompt, } from '../types.js';
9
10
  import { StreamJsonParser } from '../streaming.js';
10
11
  import { createMcpBridge } from '../mcp-bridge.js';
11
12
  export class ClaudeCliProvider {
@@ -31,7 +32,9 @@ export class ClaudeCliProvider {
31
32
  const model = options?.model ?? this.model;
32
33
  // Format messages into a single prompt for -p mode
33
34
  const prompt = formatPrompt(messages);
34
- const systemPrompt = options?.systemPrompt;
35
+ const systemPrompt = options?.systemPrompt
36
+ ? joinSplitPrompt(options.systemPrompt)
37
+ : undefined;
35
38
  // Set up MCP bridge for tool access if tools are provided and no config given
36
39
  let bridge = null;
37
40
  let mcpConfigPath = this.mcpConfigPath;
@@ -5,7 +5,7 @@
5
5
  * No SDK dependency. Uses only Node.js native fetch + SSE parsing.
6
6
  * Converts OpenAI's delta format to the canonical StreamEvent union.
7
7
  */
8
- import type { AgentProvider, AgentMessage, ToolDefinition, StreamEvent, StreamOptions } from '../types.js';
8
+ import { type AgentProvider, type AgentMessage, type ToolDefinition, type StreamEvent, type StreamOptions } from '../types.js';
9
9
  export interface OpenAICompatProviderOptions {
10
10
  apiKey: string;
11
11
  model?: string;
@@ -5,6 +5,7 @@
5
5
  * No SDK dependency. Uses only Node.js native fetch + SSE parsing.
6
6
  * Converts OpenAI's delta format to the canonical StreamEvent union.
7
7
  */
8
+ import { joinSplitPrompt } from '../types.js';
8
9
  export class OpenAICompatProvider {
9
10
  apiKey;
10
11
  model;
@@ -38,7 +39,7 @@ export class OpenAICompatProvider {
38
39
  const body = {
39
40
  model,
40
41
  messages: [
41
- ...(options?.systemPrompt ? [{ role: 'system', content: options.systemPrompt }] : []),
42
+ ...(options?.systemPrompt ? [{ role: 'system', content: joinSplitPrompt(options.systemPrompt) }] : []),
42
43
  ...apiMessages,
43
44
  ],
44
45
  max_tokens: maxTokens,
@@ -3,7 +3,7 @@
3
3
  * Uses the platform's AI credits, no local API key needed.
4
4
  * Connects to POST /ai-chat/stream and parses SSE events.
5
5
  */
6
- import type { AgentProvider, AgentMessage, ToolDefinition, StreamEvent, StreamOptions } from '../types.js';
6
+ import { type AgentProvider, type AgentMessage, type ToolDefinition, type StreamEvent, type StreamOptions } from '../types.js';
7
7
  export interface PlatformProviderOptions {
8
8
  /** JWT token or API key for platform auth */
9
9
  token: string;
@@ -3,6 +3,7 @@
3
3
  * Uses the platform's AI credits, no local API key needed.
4
4
  * Connects to POST /ai-chat/stream and parses SSE events.
5
5
  */
6
+ import { joinSplitPrompt } from '../types.js';
6
7
  export class PlatformProvider {
7
8
  token;
8
9
  baseUrl;
@@ -37,7 +38,8 @@ export class PlatformProvider {
37
38
  const body = { message };
38
39
  if (options?.systemPrompt) {
39
40
  // Platform doesn't accept system prompt directly via API — embed in message
40
- body.message = `[System context: ${options.systemPrompt.slice(0, 2000)}]\n\n${message}`;
41
+ const systemStr = joinSplitPrompt(options.systemPrompt);
42
+ body.message = `[System context: ${systemStr.slice(0, 2000)}]\n\n${message}`;
41
43
  }
42
44
  const response = await fetch(`${this.baseUrl}/ai-chat/stream`, {
43
45
  method: 'POST',
@@ -65,8 +65,25 @@ export interface ToolEvent {
65
65
  result?: string;
66
66
  isError?: boolean;
67
67
  }
68
+ /**
69
+ * Split system prompt for Anthropic API cache optimization.
70
+ *
71
+ * The prefix (stable FW knowledge) is cached across calls via cache_control.
72
+ * The suffix (per-task context) varies per call but rides on the cached prefix.
73
+ *
74
+ * Providers that support structured system blocks (Anthropic) use both parts.
75
+ * Providers that only accept strings (CLI, OpenAI, platform) concatenate them.
76
+ */
77
+ export interface SplitPrompt {
78
+ /** Stable prefix — identical across calls. Cacheable. */
79
+ prefix: string;
80
+ /** Dynamic suffix — varies per task/call. Not cached. */
81
+ suffix: string;
82
+ }
83
+ /** Convert a SplitPrompt to a single string (for providers that don't support blocks). */
84
+ export declare function joinSplitPrompt(prompt: SplitPrompt): string;
68
85
  export interface StreamOptions {
69
- systemPrompt?: string;
86
+ systemPrompt?: SplitPrompt;
70
87
  model?: string;
71
88
  maxTokens?: number;
72
89
  signal?: AbortSignal;
@@ -89,7 +106,7 @@ export interface McpBridge {
89
106
  cleanup: () => void;
90
107
  }
91
108
  export interface AgentLoopOptions {
92
- systemPrompt?: string;
109
+ systemPrompt?: SplitPrompt;
93
110
  maxIterations?: number;
94
111
  maxTokens?: number;
95
112
  model?: string;
@@ -145,6 +162,8 @@ export interface CliSessionOptions {
145
162
  model: string;
146
163
  /** Pre-configured MCP config path. */
147
164
  mcpConfigPath?: string;
165
+ /** Disable specific built-in tools (e.g. ['Read', 'Edit', 'Write', 'Bash'] to force MCP tools). */
166
+ disallowedTools?: string[];
148
167
  /** Custom spawn function. Defaults to child_process.spawn. */
149
168
  spawnFn?: SpawnFn;
150
169
  /** Idle timeout in milliseconds. Defaults to 600000 (10 minutes). */
@@ -3,5 +3,10 @@
3
3
  *
4
4
  * All types are pure — no runtime imports, no side effects.
5
5
  */
6
- export {};
6
+ /** Convert a SplitPrompt to a single string (for providers that don't support blocks). */
7
+ export function joinSplitPrompt(prompt) {
8
+ if (!prompt.suffix)
9
+ return prompt.prefix;
10
+ return prompt.prefix + '\n\n' + prompt.suffix;
11
+ }
7
12
  //# sourceMappingURL=types.js.map
@@ -29,17 +29,8 @@ export interface InPlaceGenerateOptions {
29
29
  * @default 'esm'
30
30
  */
31
31
  moduleFormat?: TModuleFormat;
32
- /**
33
- * Force inline runtime even when @synergenius/flow-weaver package is installed.
34
- * When false/undefined, the compiler auto-detects the package and uses
35
- * external imports when available (smaller generated code, shared runtime).
36
- * @default false
37
- */
38
- inlineRuntime?: boolean;
39
32
  /**
40
33
  * Absolute path to the source file being compiled.
41
- * Used to detect if @synergenius/flow-weaver is installed relative to the file.
42
- * If not provided, falls back to process.cwd().
43
34
  */
44
35
  sourceFile?: string;
45
36
  /**
@@ -15,7 +15,6 @@ import { shouldWorkflowBeAsync } from '../generator/async-detection.js';
15
15
  import { detectSugarPatterns, filterStaleMacros } from '../sugar-optimizer.js';
16
16
  import * as ts from 'typescript';
17
17
  import * as path from 'path';
18
- import * as fs from 'fs';
19
18
  // Marker constants
20
19
  export const MARKERS = {
21
20
  RUNTIME_START: '// @flow-weaver-runtime-start',
@@ -23,30 +22,6 @@ export const MARKERS = {
23
22
  BODY_START: '// @flow-weaver-body-start',
24
23
  BODY_END: '// @flow-weaver-body-end',
25
24
  };
26
- /**
27
- * Check if `@synergenius/flow-weaver` is available as an npm package
28
- * by walking up from the given directory looking for node_modules.
29
- */
30
- function isFlowWeaverPackageInstalled(startDir) {
31
- let dir = startDir;
32
- const root = path.parse(dir).root;
33
- while (dir !== root) {
34
- const candidate = path.join(dir, 'node_modules', '@synergenius', 'flow-weaver');
35
- try {
36
- if (fs.existsSync(candidate)) {
37
- return true;
38
- }
39
- }
40
- catch {
41
- // Permission error or similar — skip and keep walking
42
- }
43
- const parent = path.dirname(dir);
44
- if (parent === dir)
45
- break;
46
- dir = parent;
47
- }
48
- return false;
49
- }
50
25
  /**
51
26
  * Generate executable code in-place, preserving user code.
52
27
  *
@@ -56,7 +31,7 @@ function isFlowWeaverPackageInstalled(startDir) {
56
31
  * @returns The updated source code with generated sections
57
32
  */
58
33
  export function generateInPlace(sourceCode, ast, options = {}) {
59
- const { production = false, allWorkflows, moduleFormat = 'esm', inlineRuntime = false, sourceFile, skipParamReturns = false } = options;
34
+ const { production = false, allWorkflows, moduleFormat = 'esm', sourceFile, skipParamReturns = false } = options;
60
35
  let result = sourceCode;
61
36
  let hasChanges = false;
62
37
  // Step 1: Update JSDoc annotations for node type functions
@@ -90,15 +65,8 @@ export function generateInPlace(sourceCode, ast, options = {}) {
90
65
  result = jsdocResult.code;
91
66
  hasChanges = true;
92
67
  }
93
- // Step 3: Generate and insert/replace runtime section
94
- // Auto-detect external runtime unless --inline-runtime is forced
95
- let useExternalRuntime = false;
96
- if (!inlineRuntime) {
97
- const lookupDir = sourceFile ? path.dirname(sourceFile) : (ast.sourceFile ? path.dirname(ast.sourceFile) : process.cwd());
98
- useExternalRuntime = isFlowWeaverPackageInstalled(lookupDir);
99
- }
100
- const externalRuntimePath = useExternalRuntime ? '@synergenius/flow-weaver/runtime' : undefined;
101
- const runtimeCode = generateRuntimeSection(ast.functionName, production, moduleFormat, externalRuntimePath);
68
+ // Step 3: Generate and insert/replace runtime section (always inlined — zero runtime dependencies)
69
+ const runtimeCode = generateRuntimeSection(ast.functionName, production, moduleFormat);
102
70
  const runtimeResult = replaceOrInsertSection(result, MARKERS.RUNTIME_START, MARKERS.RUNTIME_END, runtimeCode, 'top');
103
71
  if (runtimeResult.changed) {
104
72
  result = runtimeResult.code;
@@ -145,31 +113,15 @@ export function generateInPlace(sourceCode, ast, options = {}) {
145
113
  }
146
114
  /**
147
115
  * Generate the runtime section with proper markers.
148
- * When externalRuntimePath is provided, generates import statements instead of inline code.
116
+ * Runtime is always inlined zero runtime dependencies.
149
117
  */
150
- function generateRuntimeSection(functionName, production, moduleFormat = 'esm', externalRuntimePath) {
118
+ function generateRuntimeSection(functionName, production, moduleFormat = 'esm') {
151
119
  const lines = [];
152
120
  lines.push('// ============================================================================');
153
121
  lines.push('// DO NOT EDIT - This section is auto-generated by Flow Weaver');
154
122
  lines.push('// ============================================================================');
155
123
  lines.push('');
156
- if (externalRuntimePath) {
157
- // External runtime: generate import statements instead of inline code
158
- lines.push(`import { GeneratedExecutionContext, CancellationError } from '${externalRuntimePath}';`);
159
- if (!production) {
160
- lines.push(`import type { TDebugger, TDebugController } from '${externalRuntimePath}';`);
161
- // Declare __flowWeaverDebugger__ so body code can reference it
162
- lines.push('declare const __flowWeaverDebugger__: TDebugger | undefined;');
163
- }
164
- else {
165
- // Production mode still needs TDebugController for the __ctrl__ variable
166
- lines.push(`import type { TDebugController } from '${externalRuntimePath}';`);
167
- }
168
- }
169
- else {
170
- // Inline runtime: embed all types and classes directly
171
- lines.push(generateInlineRuntime(production));
172
- }
124
+ lines.push(generateInlineRuntime(production));
173
125
  return lines.join('\n');
174
126
  }
175
127
  /**
@@ -18,12 +18,11 @@ export interface GenerateOptions extends Partial<ASTGenerateOptions> {
18
18
  */
19
19
  moduleFormat?: TModuleFormat;
20
20
  /**
21
- * Path to external runtime module (relative from the generated file).
22
- * When set, generates import from this path instead of inlining runtime types.
23
- * Use this for multi-workflow bundles to avoid duplicate type declarations.
24
- * @example '../runtime/types.js'
21
+ * Enable bundle mode for multi-workflow bundles.
22
+ * When true, imports node types from node-types/ directory and workflows from sibling files.
23
+ * Runtime is always inlined regardless of this setting.
25
24
  */
26
- externalRuntimePath?: string;
25
+ bundleMode?: boolean;
27
26
  /**
28
27
  * Constants from source file(s) to include at the top of the generated file.
29
28
  * Used in bundle mode when local node functions are inlined and need their
@@ -38,7 +38,7 @@ export function generateModuleExports(functionNames) {
38
38
  return `module.exports = { ${functionNames.join(', ')} };`;
39
39
  }
40
40
  export function generateCode(ast, options) {
41
- const { production = false, sourceMap = false, allWorkflows = [], moduleFormat = 'esm', externalRuntimePath, constants = [], externalNodeTypes = {}, generateStubs = false, outputFormat = 'typescript', } = options || {};
41
+ const { production = false, sourceMap = false, allWorkflows = [], moduleFormat = 'esm', bundleMode = false, constants = [], externalNodeTypes = {}, generateStubs = false, outputFormat = 'typescript', } = options || {};
42
42
  // Check for stub nodes — refuse to generate unless explicitly allowed
43
43
  const stubNodeTypes = ast.nodeTypes.filter((nt) => nt.variant === 'STUB');
44
44
  if (stubNodeTypes.length > 0 && !generateStubs) {
@@ -78,8 +78,6 @@ export function generateCode(ast, options) {
78
78
  }
79
79
  };
80
80
  // Generate function body using existing body generator
81
- // Bundle mode uses params object pattern for node wrapper calls
82
- const bundleMode = !!externalRuntimePath;
83
81
  const functionBody = bodyGenerator.generateWithExecutionContext(ast, ast.nodeTypes, shouldBeAsync, production, bundleMode);
84
82
  // Build the complete module
85
83
  const lines = [];
@@ -87,25 +85,8 @@ export function generateCode(ast, options) {
87
85
  addLine();
88
86
  lines.push('');
89
87
  addLine();
90
- // Include runtime (either inline or external import)
91
- if (externalRuntimePath) {
92
- // Import from external runtime module to avoid duplicate declarations in multi-file bundles
93
- lines.push(`// Runtime imported from shared module`);
94
- addLine();
95
- lines.push(generateImportStatement(['GeneratedExecutionContext', 'CancellationError'], externalRuntimePath, moduleFormat));
96
- addLine();
97
- if (!production) {
98
- // Import TDebugger type from external runtime
99
- lines.push(moduleFormat === 'cjs'
100
- ? `const { TDebugger } = require('${externalRuntimePath}');`
101
- : `import type { TDebugger } from '${externalRuntimePath}';`);
102
- addLine();
103
- }
104
- lines.push('');
105
- addLine();
106
- }
107
- else {
108
- // Include inline runtime (types + GeneratedExecutionContext)
88
+ // Include inline runtime (always inlined zero runtime dependencies)
89
+ {
109
90
  const inlineRuntime = generateInlineRuntime(production);
110
91
  const runtimeLines = inlineRuntime.split('\n');
111
92
  runtimeLines.forEach((line) => {
@@ -172,11 +153,10 @@ export function generateCode(ast, options) {
172
153
  lines.push('');
173
154
  addLine();
174
155
  // Import regular node functions from source files
175
- // In bundle mode (externalRuntimePath is set), import from node-types directory
156
+ // In bundle mode, import from node-types directory
176
157
  // Otherwise import from .generated files in the same directory
177
158
  functionImportsByFile.forEach((nodes, sourceFile) => {
178
- if (externalRuntimePath) {
179
- // Bundle mode: import from node-types directory
159
+ if (bundleMode) {
180
160
  // Bundle mode: import _impl (positional data args for expression nodes, execute + data args for regular)
181
161
  // The wrapper is only for HTTP entry points, not internal workflow calls
182
162
  nodes.forEach((node) => {
@@ -199,7 +179,7 @@ export function generateCode(ast, options) {
199
179
  // Import workflows from their generated files
200
180
  // In bundle mode, import from sibling workflow files
201
181
  workflowImportsByFile.forEach((names, sourceFile) => {
202
- if (externalRuntimePath) {
182
+ if (bundleMode) {
203
183
  // Bundle mode: import each workflow from the workflows directory
204
184
  names.forEach((name) => {
205
185
  const relativePath = `./${name}.js`;
@@ -16,11 +16,6 @@ export interface CompileOptions {
16
16
  * - 'auto': Auto-detect from project's package.json (default)
17
17
  */
18
18
  format?: 'esm' | 'cjs' | 'auto';
19
- /**
20
- * Force inline runtime even when @synergenius/flow-weaver package is installed.
21
- * By default, the compiler uses external runtime imports when the package is available.
22
- */
23
- inlineRuntime?: boolean;
24
19
  /**
25
20
  * Omit redundant @param/@returns annotations from compiled output.
26
21
  * Useful for vibe coders who don't use the visual editor.
@@ -14,6 +14,7 @@ import { getFriendlyError } from '../../friendly-errors.js';
14
14
  import { detectProjectModuleFormat } from './doctor.js';
15
15
  import { compileTargetRegistry } from '../../generator/compile-target-registry.js';
16
16
  import { AnnotationParser } from '../../parser.js';
17
+ import { safeWriteFile, safeAppendFile } from '../utils/safe-write.js';
17
18
  /** Show path relative to cwd for cleaner output */
18
19
  function displayPath(filePath) {
19
20
  const rel = path.relative(process.cwd(), filePath);
@@ -36,7 +37,7 @@ function resolveModuleFormat(format, cwd) {
36
37
  return detection.format;
37
38
  }
38
39
  export async function compileCommand(input, options = {}) {
39
- const { production = false, sourceMap = false, strict = false, verbose = false, workflowName, dryRun = false, format, inlineRuntime = false, clean = false, target } = options;
40
+ const { production = false, sourceMap = false, strict = false, verbose = false, workflowName, dryRun = false, format, clean = false, target, output } = options;
40
41
  // Handle custom compile target
41
42
  if (target && target !== 'typescript') {
42
43
  return compileCustomTarget(target, input, { production, verbose, workflowName, dryRun, cron: options.cron, serve: options.serve, framework: options.framework, typedEvents: options.typedEvents, retries: options.retries, timeout: options.timeout });
@@ -67,6 +68,27 @@ export async function compileCommand(input, options = {}) {
67
68
  if (files.length === 0) {
68
69
  throw new Error(`No files found matching pattern: ${input}`);
69
70
  }
71
+ // Resolve --output: determine if it's a file or directory target
72
+ let outputDir;
73
+ let outputFile;
74
+ if (output) {
75
+ const isOutputDir = output.endsWith('/') || output.endsWith(path.sep) ||
76
+ (fs.existsSync(output) && fs.statSync(output).isDirectory());
77
+ if (isOutputDir) {
78
+ outputDir = output.endsWith('/') || output.endsWith(path.sep) ? output.slice(0, -1) : output;
79
+ if (!fs.existsSync(outputDir)) {
80
+ fs.mkdirSync(outputDir, { recursive: true });
81
+ }
82
+ }
83
+ else if (files.length > 1) {
84
+ // Multiple input files but output is a single file — ambiguous
85
+ throw new Error(`Cannot use --output with a file path when compiling multiple files. ` +
86
+ `Use a directory path instead (e.g. --output ${output}/)`);
87
+ }
88
+ else {
89
+ outputFile = path.resolve(output);
90
+ }
91
+ }
70
92
  const totalTimer = logger.timer();
71
93
  logger.section('Compiling Workflows');
72
94
  if (verbose) {
@@ -161,10 +183,16 @@ export async function compileCommand(input, options = {}) {
161
183
  // Read original source
162
184
  const sourceCode = fs.readFileSync(file, 'utf8');
163
185
  // Generate code in-place (preserves types, interfaces, etc.)
164
- const result = generateInPlace(sourceCode, parseResult.ast, { production, moduleFormat, inlineRuntime, sourceFile: file, skipParamReturns: clean });
165
- // Write back to original file (skip in dry-run mode)
186
+ const result = generateInPlace(sourceCode, parseResult.ast, { production, moduleFormat, sourceFile: file, skipParamReturns: clean });
187
+ // Determine where to write the compiled output
188
+ const writePath = outputFile
189
+ ? outputFile
190
+ : outputDir
191
+ ? path.join(outputDir, path.basename(file))
192
+ : file; // in-place
193
+ // Write compiled output (skip in dry-run mode)
166
194
  if (!dryRun) {
167
- fs.writeFileSync(file, result.code, 'utf8');
195
+ safeWriteFile(writePath, result.code);
168
196
  // Generate source map if requested
169
197
  if (sourceMap) {
170
198
  const mapResult = generateCode(parseResult.ast, {
@@ -173,11 +201,11 @@ export async function compileCommand(input, options = {}) {
173
201
  moduleFormat,
174
202
  });
175
203
  if (mapResult.sourceMap) {
176
- const mapPath = file + '.map';
177
- fs.writeFileSync(mapPath, mapResult.sourceMap, 'utf8');
204
+ const mapPath = writePath + '.map';
205
+ safeWriteFile(mapPath, mapResult.sourceMap);
178
206
  const sourceMappingComment = `\n//# sourceMappingURL=${path.basename(mapPath)}\n`;
179
207
  if (!result.code.includes('//# sourceMappingURL=')) {
180
- fs.appendFileSync(file, sourceMappingComment, 'utf8');
208
+ safeAppendFile(writePath, sourceMappingComment);
181
209
  }
182
210
  if (verbose) {
183
211
  logger.info(` source map: ${displayPath(mapPath)}`);
@@ -291,7 +319,7 @@ export async function compileCustomTarget(target, input, options) {
291
319
  }
292
320
  }
293
321
  else {
294
- fs.writeFileSync(outputPath, code, 'utf8');
322
+ safeWriteFile(outputPath, code);
295
323
  logger.success(`Compiled: ${displayPath(outputPath)}`);
296
324
  }
297
325
  logger.newline();
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Context command - generate LLM context bundles from documentation and grammar
3
3
  */
4
- import * as fs from 'fs';
5
4
  import { buildContext, PRESETS, PRESET_NAMES } from '../../context/index.js';
6
5
  import { logger } from '../utils/logger.js';
6
+ import { safeWriteFile } from '../utils/safe-write.js';
7
7
  export async function contextCommand(preset, options) {
8
8
  // --list: show presets and exit
9
9
  if (options.list) {
@@ -22,14 +22,12 @@ export async function contextCommand(preset, options) {
22
22
  // Validate preset
23
23
  const presetName = (preset ?? 'core');
24
24
  if (!PRESET_NAMES.includes(presetName) && !options.topics) {
25
- logger.error(`Unknown preset "${preset}". Available: ${PRESET_NAMES.join(', ')}. Or use --topics to specify topics directly.`);
26
- process.exit(1);
25
+ throw new Error(`Unknown preset "${preset}". Available: ${PRESET_NAMES.join(', ')}. Or use --topics to specify topics directly.`);
27
26
  }
28
27
  // Validate profile
29
28
  const profile = options.profile ?? 'standalone';
30
29
  if (profile !== 'standalone' && profile !== 'assistant') {
31
- logger.error(`Unknown profile "${profile}". Use "standalone" or "assistant".`);
32
- process.exit(1);
30
+ throw new Error(`Unknown profile "${profile}". Use "standalone" or "assistant".`);
33
31
  }
34
32
  const result = buildContext({
35
33
  preset: PRESET_NAMES.includes(presetName) ? presetName : 'core',
@@ -40,7 +38,7 @@ export async function contextCommand(preset, options) {
40
38
  });
41
39
  // Write output
42
40
  if (options.output) {
43
- fs.writeFileSync(options.output, result.content, 'utf-8');
41
+ safeWriteFile(options.output, result.content);
44
42
  logger.success(`Context written to ${options.output}`);
45
43
  }
46
44
  else {
@@ -41,9 +41,7 @@ export async function createWorkflowCommand(template, file, options = {}) {
41
41
  const { line, async: isAsync = false, preview = false, provider, model, config: configJson, } = options;
42
42
  const templateDef = getWorkflowTemplate(template);
43
43
  if (!templateDef) {
44
- logger.error(`Unknown workflow template: ${template}`);
45
- logger.info("Run 'fw templates' to see available templates");
46
- process.exit(1);
44
+ throw new Error(`Unknown workflow template: ${template}. Run 'fw templates' to see available templates`);
47
45
  }
48
46
  // Resolve to absolute path
49
47
  const filePath = path.resolve(file);
@@ -67,8 +65,7 @@ export async function createWorkflowCommand(template, file, options = {}) {
67
65
  Object.assign(config, JSON.parse(configJson));
68
66
  }
69
67
  catch {
70
- logger.error('Invalid --config JSON');
71
- process.exit(1);
68
+ throw new Error('Invalid --config JSON');
72
69
  }
73
70
  }
74
71
  // Generate the template code
@@ -91,8 +88,7 @@ export async function createWorkflowCommand(template, file, options = {}) {
91
88
  logger.info(` Workflow function: ${workflowName}`);
92
89
  }
93
90
  catch (error) {
94
- logger.error(`Failed to create workflow: ${getErrorMessage(error)}`);
95
- process.exit(1);
91
+ throw new Error(`Failed to create workflow: ${getErrorMessage(error)}`);
96
92
  }
97
93
  }
98
94
  /**
@@ -103,9 +99,7 @@ export async function createNodeCommand(name, file, options = {}) {
103
99
  const { line, template = 'processor', preview = false, strategy, config: configJson } = options;
104
100
  const templateDef = getNodeTemplate(template);
105
101
  if (!templateDef) {
106
- logger.error(`Unknown node template: ${template}`);
107
- logger.info("Run 'fw templates' to see available templates");
108
- process.exit(1);
102
+ throw new Error(`Unknown node template: ${template}. Run 'fw templates' to see available templates`);
109
103
  }
110
104
  // Resolve to absolute path
111
105
  const filePath = path.resolve(file);
@@ -118,8 +112,7 @@ export async function createNodeCommand(name, file, options = {}) {
118
112
  Object.assign(config, JSON.parse(configJson));
119
113
  }
120
114
  catch {
121
- logger.error('Invalid --config JSON');
122
- process.exit(1);
115
+ throw new Error('Invalid --config JSON');
123
116
  }
124
117
  }
125
118
  // Generate the template code with provided name and optional config
@@ -140,8 +133,7 @@ export async function createNodeCommand(name, file, options = {}) {
140
133
  logger.info(` Node function: ${nodeName}`);
141
134
  }
142
135
  catch (error) {
143
- logger.error(`Failed to create node: ${getErrorMessage(error)}`);
144
- process.exit(1);
136
+ throw new Error(`Failed to create node: ${getErrorMessage(error)}`);
145
137
  }
146
138
  }
147
139
  //# sourceMappingURL=create.js.map
@@ -9,6 +9,7 @@ import { validator } from '../../validator.js';
9
9
  import { getNode, getIncomingConnections, getOutgoingConnections } from '../../api/query.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { getErrorMessage } from '../../utils/error-utils.js';
12
+ import { safeWriteFile } from '../utils/safe-write.js';
12
13
  import { buildDiagramGraph } from '../../diagram/geometry.js';
13
14
  import { renderASCII, renderASCIICompact } from '../../diagram/ascii-renderer.js';
14
15
  export function buildNodeInfo(instance, nodeType) {
@@ -344,16 +345,13 @@ export async function describeCommand(input, options = {}) {
344
345
  const { format = 'json', node: focusNodeId, workflowName, compile = false } = options;
345
346
  const filePath = path.resolve(input);
346
347
  if (!fs.existsSync(filePath)) {
347
- logger.error(`File not found: ${filePath}`);
348
- process.exit(1);
348
+ throw new Error(`File not found: ${filePath}`);
349
349
  }
350
350
  try {
351
351
  // Parse the workflow
352
352
  const parseResult = await parseWorkflow(filePath, { workflowName });
353
353
  if (parseResult.errors.length > 0) {
354
- logger.error(`Parse errors:`);
355
- parseResult.errors.forEach((err) => logger.error(` ${err}`));
356
- process.exit(1);
354
+ throw new Error(`Parse errors:\n${parseResult.errors.map((err) => ` ${err}`).join('\n')}`);
357
355
  }
358
356
  const ast = parseResult.ast;
359
357
  // Only update runtime markers when explicitly requested via --compile
@@ -362,7 +360,7 @@ export async function describeCommand(input, options = {}) {
362
360
  const sourceCode = fs.readFileSync(filePath, 'utf8');
363
361
  const generated = generateInPlace(sourceCode, ast, { production: false });
364
362
  if (generated.hasChanges) {
365
- fs.writeFileSync(filePath, generated.code, 'utf8');
363
+ safeWriteFile(filePath, generated.code);
366
364
  logger.info(`Updated runtime markers in ${path.basename(filePath)}`);
367
365
  }
368
366
  }
@@ -373,11 +371,9 @@ export async function describeCommand(input, options = {}) {
373
371
  }
374
372
  catch (error) {
375
373
  if (error instanceof Error && error.message.startsWith('Node not found:')) {
376
- logger.error(error.message);
377
- process.exit(1);
374
+ throw error;
378
375
  }
379
- logger.error(`Failed to describe workflow: ${getErrorMessage(error)}`);
380
- process.exit(1);
376
+ throw new Error(`Failed to describe workflow: ${getErrorMessage(error)}`);
381
377
  }
382
378
  }
383
379
  //# sourceMappingURL=describe.js.map