@synergenius/flow-weaver 0.23.4 → 0.24.0

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 (38) hide show
  1. package/dist/api/generate-in-place.d.ts +0 -9
  2. package/dist/api/generate-in-place.js +6 -54
  3. package/dist/api/generate.d.ts +4 -5
  4. package/dist/api/generate.js +6 -26
  5. package/dist/cli/commands/compile.d.ts +0 -5
  6. package/dist/cli/commands/compile.js +36 -8
  7. package/dist/cli/commands/context.js +4 -6
  8. package/dist/cli/commands/create.js +6 -14
  9. package/dist/cli/commands/describe.js +6 -10
  10. package/dist/cli/commands/diagram.js +18 -25
  11. package/dist/cli/commands/diff.js +7 -14
  12. package/dist/cli/commands/docs.js +3 -6
  13. package/dist/cli/commands/doctor.js +1 -1
  14. package/dist/cli/commands/export.js +1 -1
  15. package/dist/cli/commands/grammar.js +3 -4
  16. package/dist/cli/commands/implement.js +8 -13
  17. package/dist/cli/commands/market.js +4 -8
  18. package/dist/cli/commands/migrate.js +2 -1
  19. package/dist/cli/commands/modify.js +2 -1
  20. package/dist/cli/commands/openapi.js +2 -1
  21. package/dist/cli/commands/pattern.js +3 -2
  22. package/dist/cli/commands/strip.js +3 -6
  23. package/dist/cli/commands/validate.js +6 -1
  24. package/dist/cli/flow-weaver.mjs +789 -797
  25. package/dist/cli/index.js +10 -12
  26. package/dist/cli/postinstall.d.ts +16 -0
  27. package/dist/cli/postinstall.js +119 -0
  28. package/dist/cli/utils/parse-int-strict.d.ts +7 -0
  29. package/dist/cli/utils/parse-int-strict.js +17 -0
  30. package/dist/cli/utils/safe-write.d.ts +18 -0
  31. package/dist/cli/utils/safe-write.js +54 -0
  32. package/dist/generated-version.d.ts +1 -1
  33. package/dist/generated-version.js +1 -1
  34. package/dist/generator/unified.js +8 -6
  35. package/docs/reference/cli-reference.md +0 -1
  36. package/docs/reference/compilation.md +2 -10
  37. package/package.json +4 -2
  38. package/scripts/postinstall.cjs +86 -0
@@ -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
@@ -6,38 +6,31 @@ import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { fileToSVG, fileToHTML, fileToASCII } from '../../diagram/index.js';
8
8
  import { logger } from '../utils/logger.js';
9
- import { getErrorMessage } from '../../utils/error-utils.js';
9
+ import { safeWriteFile } from '../utils/safe-write.js';
10
10
  const ASCII_FORMATS = new Set(['ascii', 'ascii-compact', 'text']);
11
11
  export async function diagramCommand(input, options = {}) {
12
12
  const { output, format = 'svg', ...diagramOptions } = options;
13
13
  const filePath = path.resolve(input);
14
14
  if (!fs.existsSync(filePath)) {
15
- logger.error(`File not found: ${filePath}`);
16
- process.exit(1);
15
+ throw new Error(`File not found: ${filePath}`);
17
16
  }
18
- try {
19
- let result;
20
- if (ASCII_FORMATS.has(format)) {
21
- result = fileToASCII(filePath, { ...diagramOptions, format });
22
- }
23
- else if (format === 'html') {
24
- result = fileToHTML(filePath, diagramOptions);
25
- }
26
- else {
27
- result = fileToSVG(filePath, diagramOptions);
28
- }
29
- if (output) {
30
- const outputPath = path.resolve(output);
31
- fs.writeFileSync(outputPath, result, 'utf-8');
32
- logger.success(`Diagram written to ${outputPath}`);
33
- }
34
- else {
35
- process.stdout.write(result);
36
- }
17
+ let result;
18
+ if (ASCII_FORMATS.has(format)) {
19
+ result = fileToASCII(filePath, { ...diagramOptions, format });
37
20
  }
38
- catch (error) {
39
- logger.error(`Failed to generate diagram: ${getErrorMessage(error)}`);
40
- process.exit(1);
21
+ else if (format === 'html') {
22
+ result = fileToHTML(filePath, diagramOptions);
23
+ }
24
+ else {
25
+ result = fileToSVG(filePath, diagramOptions);
26
+ }
27
+ if (output) {
28
+ const outputPath = path.resolve(output);
29
+ safeWriteFile(outputPath, result);
30
+ logger.success(`Diagram written to ${outputPath}`);
31
+ }
32
+ else {
33
+ process.stdout.write(result);
41
34
  }
42
35
  }
43
36
  //# sourceMappingURL=diagram.js.map
@@ -13,12 +13,10 @@ export async function diffCommand(file1, file2, options = {}) {
13
13
  const filePath2 = path.resolve(file2);
14
14
  // Validate files exist
15
15
  if (!fs.existsSync(filePath1)) {
16
- logger.error(`File not found: ${filePath1}`);
17
- process.exit(1);
16
+ throw new Error(`File not found: ${filePath1}`);
18
17
  }
19
18
  if (!fs.existsSync(filePath2)) {
20
- logger.error(`File not found: ${filePath2}`);
21
- process.exit(1);
19
+ throw new Error(`File not found: ${filePath2}`);
22
20
  }
23
21
  try {
24
22
  // Parse both workflows
@@ -27,14 +25,10 @@ export async function diffCommand(file1, file2, options = {}) {
27
25
  parseWorkflow(filePath2, { workflowName }),
28
26
  ]);
29
27
  if (result1.errors.length > 0) {
30
- logger.error(`Parse errors in ${file1}:`);
31
- result1.errors.forEach((err) => logger.error(` ${err}`));
32
- process.exit(1);
28
+ throw new Error(`Parse errors in ${file1}:\n${result1.errors.map((err) => ` ${err}`).join('\n')}`);
33
29
  }
34
30
  if (result2.errors.length > 0) {
35
- logger.error(`Parse errors in ${file2}:`);
36
- result2.errors.forEach((err) => logger.error(` ${err}`));
37
- process.exit(1);
31
+ throw new Error(`Parse errors in ${file2}:\n${result2.errors.map((err) => ` ${err}`).join('\n')}`);
38
32
  }
39
33
  // Compare workflows
40
34
  const diff = WorkflowDiffer.compare(result1.ast, result2.ast);
@@ -50,15 +44,14 @@ export async function diffCommand(file1, file2, options = {}) {
50
44
  else {
51
45
  // eslint-disable-next-line no-console
52
46
  console.log(formatDiff(diff, format));
53
- // Exit with code 1 if there are differences (useful for CI)
47
+ // Throw if there are differences (useful for CI)
54
48
  if (!exitZero) {
55
- process.exit(1);
49
+ throw new Error('Workflows have differences');
56
50
  }
57
51
  }
58
52
  }
59
53
  catch (error) {
60
- logger.error(`Failed to diff workflows: ${getErrorMessage(error)}`);
61
- process.exit(1);
54
+ throw new Error(`Failed to diff workflows: ${getErrorMessage(error)}`);
62
55
  }
63
56
  }
64
57
  //# sourceMappingURL=diff.js.map
@@ -3,8 +3,7 @@ import { logger } from '../utils/logger.js';
3
3
  export async function docsListCommand(options) {
4
4
  const topics = listTopics();
5
5
  if (topics.length === 0) {
6
- logger.error('No documentation topics found.');
7
- process.exit(1);
6
+ throw new Error('No documentation topics found.');
8
7
  }
9
8
  if (options.json) {
10
9
  process.stdout.write(JSON.stringify({ topics }, null, 2) + '\n');
@@ -25,16 +24,14 @@ export async function docsReadCommand(topic, options) {
25
24
  if (options.json) {
26
25
  const structured = readTopicStructured(topic);
27
26
  if (!structured) {
28
- logger.error(`Unknown topic: "${topic}". Run "fw docs" to see available topics.`);
29
- process.exit(1);
27
+ throw new Error(`Unknown topic: "${topic}". Run "fw docs" to see available topics.`);
30
28
  }
31
29
  process.stdout.write(JSON.stringify(structured, null, 2) + '\n');
32
30
  return;
33
31
  }
34
32
  const doc = readTopic(topic, options.compact);
35
33
  if (!doc) {
36
- logger.error(`Unknown topic: "${topic}". Run "fw docs" to see available topics.`);
37
- process.exit(1);
34
+ throw new Error(`Unknown topic: "${topic}". Run "fw docs" to see available topics.`);
38
35
  }
39
36
  process.stdout.write(doc.content + '\n');
40
37
  }
@@ -707,7 +707,7 @@ export async function doctorCommand(options = {}) {
707
707
  }
708
708
  }
709
709
  if (!report.ok) {
710
- process.exit(1);
710
+ throw new Error('Doctor found issues that need to be fixed');
711
711
  }
712
712
  }
713
713
  //# sourceMappingURL=doctor.js.map
@@ -66,7 +66,7 @@ export async function exportCommand(input, options) {
66
66
  input,
67
67
  output: options.output,
68
68
  workflow: options.workflow,
69
- production: options.production ?? true,
69
+ production: options.production ?? false,
70
70
  bundle: options.bundle,
71
71
  dryRun: isDryRun,
72
72
  multi: isMulti,
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Grammar command - output JSDoc grammar as HTML railroad diagrams or EBNF text
3
3
  */
4
- import * as fs from 'fs';
5
4
  import { generateGrammarDiagrams, getAllGrammars, serializedToEBNF, } from '../../chevrotain-parser/grammar-diagrams.js';
6
5
  import { logger } from '../utils/logger.js';
7
6
  import { getErrorMessage } from '../../utils/error-utils.js';
7
+ import { safeWriteFile } from '../utils/safe-write.js';
8
8
  export async function grammarCommand(options = {}) {
9
9
  // Default to ebnf on TTY (readable in terminal), html when writing to file
10
10
  const defaultFormat = options.output ? 'html' : (process.stdout.isTTY ? 'ebnf' : 'html');
@@ -26,7 +26,7 @@ export async function grammarCommand(options = {}) {
26
26
  content = generateGrammarDiagrams();
27
27
  }
28
28
  if (output) {
29
- fs.writeFileSync(output, content, 'utf-8');
29
+ safeWriteFile(output, content);
30
30
  logger.success(`Grammar written to ${output}`);
31
31
  }
32
32
  else {
@@ -34,8 +34,7 @@ export async function grammarCommand(options = {}) {
34
34
  }
35
35
  }
36
36
  catch (error) {
37
- logger.error(`Grammar generation failed: ${getErrorMessage(error)}`);
38
- process.exit(1);
37
+ throw new Error(`Grammar generation failed: ${getErrorMessage(error)}`);
39
38
  }
40
39
  }
41
40
  //# sourceMappingURL=grammar.js.map