@timo9378/flow2code 0.1.4 → 0.1.5

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
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [0.1.5] — 2026-03-05
9
+
10
+ ### Security
11
+ - **Path traversal fix** — `serveStatic` now uses `resolve()` + `startsWith()` guard with `decodeURIComponent` to prevent `../../etc/passwd` and `%2e%2e%2f` attacks
12
+
13
+ ### Fixed
14
+ - **DAG swallowed errors** — `.catch(() => {})` replaced with `.catch((err) => { console.error(...) })` so concurrent promise errors are logged instead of silently discarded
15
+ - **CLI watch sync I/O** — Watch mode now uses async `readFile`/`writeFile`/`mkdir` + 150ms debounce to prevent event loop blocking
16
+ - **Source map brittle regex** — Replaced full-line regex with robust `indexOf`-based scanner for `[nodeId] ---` suffix tokens; survives Prettier/ESLint reformatting
17
+ - **Compiler state mutation** — Centralized child block registration via `applyChildBlockRegistration()` helper; plugins no longer scatter-write to context
18
+ - **env-check false positives** — `env-check` command now includes `Object.keys(process.env)` in declared vars whitelist (CI/CD injected vars)
19
+ - **Plugin error guard** — `plugin.generate()` wrapped in try/catch with descriptive error message identifying plugin, node label, and node ID; preserves `{ cause }` chain
20
+
21
+ ### Added
22
+ - **Logger system** — `src/lib/logger.ts` with picocolors, log levels (debug/info/warn/error/silent), structured output (`kv`, `kvLast`, `blank`, `raw`), `--silent` CLI flag
23
+ - **picocolors** added as direct dependency (zero-dep, 3.8x faster than chalk)
24
+
25
+ ### Changed
26
+ - Dev script switched back to Turbopack (`next dev --turbopack`), removed `--webpack` workaround
27
+ - Server and CLI now use structured logger instead of raw `console.log`/`console.error`
28
+ - Test count: 405 tests / 33 test files
29
+
8
30
  ## [0.1.0] — 2026-02-27
9
31
 
10
32
  ### Added
package/dist/cli.js CHANGED
@@ -2079,7 +2079,7 @@ function generateNodeChainDAG(writer, triggerId, context) {
2079
2079
  generateNodeBody(writer, node, context);
2080
2080
  });
2081
2081
  writer.writeLine(`)();`);
2082
- writer.writeLine(`${promiseVar}.catch(() => {});`);
2082
+ writer.writeLine(`${promiseVar}.catch((err) => { console.error("[Flow2Code DAG Error]", err); });`);
2083
2083
  writer.blankLine();
2084
2084
  }
2085
2085
  if (dagNodeIds.length > 0) {
@@ -2152,7 +2152,18 @@ function generateNodeBody(writer, node, context) {
2152
2152
  const plugin = context.pluginRegistry.get(node.nodeType);
2153
2153
  if (plugin) {
2154
2154
  const pluginCtx = createPluginContext(context);
2155
- plugin.generate(node, writer, pluginCtx);
2155
+ try {
2156
+ plugin.generate(node, writer, pluginCtx);
2157
+ } catch (err) {
2158
+ const errMsg = err instanceof Error ? err.message : String(err);
2159
+ const stack = err instanceof Error ? err.stack : void 0;
2160
+ throw new Error(
2161
+ `[flow2code] Plugin "${node.nodeType}" threw an error while generating node "${node.label}" (${node.id}):
2162
+ ${errMsg}` + (stack ? `
2163
+ Stack: ${stack}` : ""),
2164
+ { cause: err }
2165
+ );
2166
+ }
2156
2167
  } else {
2157
2168
  throw new Error(
2158
2169
  `[flow2code] Unsupported node type: "${node.nodeType}". Register a plugin via pluginRegistry.register() or use a built-in node type.`
@@ -2160,6 +2171,13 @@ function generateNodeBody(writer, node, context) {
2160
2171
  }
2161
2172
  }
2162
2173
  function createPluginContext(context) {
2174
+ const pendingChildBlockIds = [];
2175
+ const applyChildBlockRegistration = (nodeId) => {
2176
+ pendingChildBlockIds.push(nodeId);
2177
+ context.childBlockNodeIds.add(nodeId);
2178
+ context.symbolTableExclusions.add(nodeId);
2179
+ context.generatedBlockNodeIds.add(nodeId);
2180
+ };
2163
2181
  return {
2164
2182
  ir: context.ir,
2165
2183
  nodeMap: context.nodeMap,
@@ -2185,9 +2203,7 @@ function createPluginContext(context) {
2185
2203
  return resolveEnvVars(url, context);
2186
2204
  },
2187
2205
  generateChildNode(writer, node) {
2188
- context.childBlockNodeIds.add(node.id);
2189
- context.symbolTableExclusions.add(node.id);
2190
- context.generatedBlockNodeIds.add(node.id);
2206
+ applyChildBlockRegistration(node.id);
2191
2207
  writer.writeLine(`// --- ${node.label} (${node.nodeType}) [${node.id}] ---`);
2192
2208
  generateNodeBody(writer, node, context);
2193
2209
  generateBlockContinuation(writer, node.id, context);
@@ -2231,27 +2247,27 @@ function collectRequiredPackages(ir, context) {
2231
2247
  function buildSourceMap(code, ir, filePath) {
2232
2248
  const lines = code.split("\n");
2233
2249
  const mappings = {};
2234
- const nodeMarkerRegex = /^[\s]*\/\/ --- .+? \(.+?\) \[(.+?)\] ---$/;
2250
+ const validNodeIds = new Set(ir.nodes.map((n) => n.id));
2235
2251
  let currentNodeId = null;
2236
2252
  let currentStartLine = 0;
2237
2253
  for (let i = 0; i < lines.length; i++) {
2238
2254
  const lineNum = i + 1;
2239
- const match = lines[i].match(nodeMarkerRegex);
2240
- if (match) {
2241
- if (currentNodeId) {
2242
- mappings[currentNodeId] = {
2243
- startLine: currentStartLine,
2244
- endLine: lineNum - 1
2245
- };
2246
- }
2247
- const [, nodeId] = match;
2248
- if (ir.nodes.some((n) => n.id === nodeId)) {
2249
- currentNodeId = nodeId;
2250
- currentStartLine = lineNum;
2251
- } else {
2252
- currentNodeId = null;
2253
- }
2255
+ const line = lines[i];
2256
+ const bracketOpen = line.indexOf("[");
2257
+ const bracketClose = line.indexOf("] ---", bracketOpen);
2258
+ if (bracketOpen === -1 || bracketClose === -1) continue;
2259
+ const trimmed = line.trimStart();
2260
+ if (!trimmed.startsWith("//")) continue;
2261
+ const candidateId = line.slice(bracketOpen + 1, bracketClose);
2262
+ if (!validNodeIds.has(candidateId)) continue;
2263
+ if (currentNodeId) {
2264
+ mappings[currentNodeId] = {
2265
+ startLine: currentStartLine,
2266
+ endLine: lineNum - 1
2267
+ };
2254
2268
  }
2269
+ currentNodeId = candidateId;
2270
+ currentStartLine = lineNum;
2255
2271
  }
2256
2272
  if (currentNodeId) {
2257
2273
  mappings[currentNodeId] = {
@@ -2302,6 +2318,72 @@ var init_compiler = __esm({
2302
2318
  }
2303
2319
  });
2304
2320
 
2321
+ // src/lib/logger.ts
2322
+ import pc from "picocolors";
2323
+ var LEVEL_ORDER, Logger, logger;
2324
+ var init_logger = __esm({
2325
+ "src/lib/logger.ts"() {
2326
+ "use strict";
2327
+ LEVEL_ORDER = {
2328
+ debug: 0,
2329
+ info: 1,
2330
+ warn: 2,
2331
+ error: 3,
2332
+ silent: 4
2333
+ };
2334
+ Logger = class {
2335
+ /** Minimum log level (default: "info"). Set to "silent" to suppress all output. */
2336
+ level = "info";
2337
+ /** Prefix for all log lines (default: "[flow2code]") */
2338
+ prefix = "[flow2code]";
2339
+ shouldLog(level) {
2340
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[this.level];
2341
+ }
2342
+ debug(...args) {
2343
+ if (!this.shouldLog("debug")) return;
2344
+ console.log(pc.gray(`${this.prefix} ${pc.dim("DEBUG")}`), ...args);
2345
+ }
2346
+ info(...args) {
2347
+ if (!this.shouldLog("info")) return;
2348
+ console.log(pc.blue(`${this.prefix}`), ...args);
2349
+ }
2350
+ success(...args) {
2351
+ if (!this.shouldLog("info")) return;
2352
+ console.log(pc.green(`${this.prefix} \u2705`), ...args);
2353
+ }
2354
+ warn(...args) {
2355
+ if (!this.shouldLog("warn")) return;
2356
+ console.warn(pc.yellow(`${this.prefix} \u26A0\uFE0F`), ...args);
2357
+ }
2358
+ error(...args) {
2359
+ if (!this.shouldLog("error")) return;
2360
+ console.error(pc.red(`${this.prefix} \u274C`), ...args);
2361
+ }
2362
+ /** Print a blank line (respects silent mode) */
2363
+ blank() {
2364
+ if (!this.shouldLog("info")) return;
2365
+ console.log();
2366
+ }
2367
+ /** Print raw text without prefix (respects silent mode) */
2368
+ raw(...args) {
2369
+ if (!this.shouldLog("info")) return;
2370
+ console.log(...args);
2371
+ }
2372
+ /** Formatted key-value line for startup banners */
2373
+ kv(key, value) {
2374
+ if (!this.shouldLog("info")) return;
2375
+ console.log(` ${pc.dim("\u251C\u2500")} ${pc.bold(key)} ${value}`);
2376
+ }
2377
+ /** Last key-value line (uses └─) */
2378
+ kvLast(key, value) {
2379
+ if (!this.shouldLog("info")) return;
2380
+ console.log(` ${pc.dim("\u2514\u2500")} ${pc.bold(key)} ${value}`);
2381
+ }
2382
+ };
2383
+ logger = new Logger();
2384
+ }
2385
+ });
2386
+
2305
2387
  // src/lib/compiler/decompiler.ts
2306
2388
  var decompiler_exports = {};
2307
2389
  __export(decompiler_exports, {
@@ -3865,7 +3947,7 @@ __export(server_exports, {
3865
3947
  });
3866
3948
  import { createServer } from "http";
3867
3949
  import { readFile, stat } from "fs/promises";
3868
- import { join as join3, extname as extname2, dirname as dirname3 } from "path";
3950
+ import { join as join3, extname as extname2, dirname as dirname3, resolve as resolve2 } from "path";
3869
3951
  import { fileURLToPath } from "url";
3870
3952
  import { existsSync as existsSync3 } from "fs";
3871
3953
  function resolveStaticDir() {
@@ -3926,7 +4008,7 @@ function sendJson(res, status, body) {
3926
4008
  res.end(JSON.stringify(body));
3927
4009
  }
3928
4010
  async function readBody(req) {
3929
- return new Promise((resolve3, reject) => {
4011
+ return new Promise((resolve4, reject) => {
3930
4012
  const chunks = [];
3931
4013
  let totalSize = 0;
3932
4014
  req.on("data", (chunk) => {
@@ -3938,7 +4020,7 @@ async function readBody(req) {
3938
4020
  }
3939
4021
  chunks.push(chunk);
3940
4022
  });
3941
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
4023
+ req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
3942
4024
  req.on("error", reject);
3943
4025
  });
3944
4026
  }
@@ -3947,7 +4029,13 @@ async function parseJsonBody(req) {
3947
4029
  return JSON.parse(raw);
3948
4030
  }
3949
4031
  async function serveStatic(staticDir, pathname, res) {
3950
- let filePath = join3(staticDir, pathname === "/" ? "index.html" : pathname);
4032
+ const decodedPath = decodeURIComponent(pathname);
4033
+ let filePath = join3(staticDir, decodedPath === "/" ? "index.html" : decodedPath);
4034
+ const resolvedPath = resolve2(filePath);
4035
+ const resolvedStaticDir = resolve2(staticDir);
4036
+ if (!resolvedPath.startsWith(resolvedStaticDir + (resolvedStaticDir.endsWith("/") || resolvedStaticDir.endsWith("\\") ? "" : process.platform === "win32" ? "\\" : "/"))) {
4037
+ return false;
4038
+ }
3951
4039
  if (!extname2(filePath)) {
3952
4040
  filePath += ".html";
3953
4041
  }
@@ -4035,7 +4123,7 @@ function startServer(options = {}) {
4035
4123
  const projectRoot = options.projectRoot ?? process.cwd();
4036
4124
  const server = createServer((req, res) => {
4037
4125
  handleRequest(req, res, staticDir, projectRoot).catch((err) => {
4038
- console.error("[flow2code] Internal error:", err);
4126
+ logger.error("Internal error:", err);
4039
4127
  res.writeHead(500, { "Content-Type": "text/plain" });
4040
4128
  res.end("Internal Server Error");
4041
4129
  });
@@ -4046,20 +4134,20 @@ function startServer(options = {}) {
4046
4134
  if (options.onReady) {
4047
4135
  options.onReady(url);
4048
4136
  } else {
4049
- console.log(`
4050
- \u{1F680} Flow2Code Dev Server`);
4051
- console.log(` \u251C\u2500 Local: ${url}`);
4052
- console.log(` \u251C\u2500 API: ${url}/api/compile`);
4053
- console.log(` \u251C\u2500 Static: ${staticDir}`);
4054
- console.log(` \u2514\u2500 Project: ${projectRoot}`);
4137
+ logger.blank();
4138
+ logger.info("Flow2Code Dev Server");
4139
+ logger.kv("Local:", url);
4140
+ logger.kv("API:", `${url}/api/compile`);
4141
+ logger.kv("Static:", staticDir);
4142
+ logger.kvLast("Project:", projectRoot);
4055
4143
  if (!hasUI) {
4056
- console.log();
4057
- console.log(` \u26A0\uFE0F UI files not found (out/index.html missing).`);
4058
- console.log(` The API endpoints still work, but the visual editor won't load.`);
4059
- console.log(` To fix: run "pnpm build:ui" in the flow2code source directory,`);
4060
- console.log(` or reinstall from npm: npm i @timo9378/flow2code@latest`);
4144
+ logger.blank();
4145
+ logger.warn("UI files not found (out/index.html missing).");
4146
+ logger.raw(" The API endpoints still work, but the visual editor won't load.");
4147
+ logger.raw(' To fix: run "pnpm build:ui" in the flow2code source directory,');
4148
+ logger.raw(" or reinstall from npm: npm i @timo9378/flow2code@latest");
4061
4149
  }
4062
- console.log();
4150
+ logger.blank();
4063
4151
  }
4064
4152
  });
4065
4153
  return server;
@@ -4069,6 +4157,7 @@ var init_server = __esm({
4069
4157
  "src/server/index.ts"() {
4070
4158
  "use strict";
4071
4159
  init_handlers();
4160
+ init_logger();
4072
4161
  init_handlers();
4073
4162
  __filename = fileURLToPath(import.meta.url);
4074
4163
  __dirname = dirname3(__filename);
@@ -4097,10 +4186,12 @@ var init_server = __esm({
4097
4186
 
4098
4187
  // src/cli/index.ts
4099
4188
  init_compiler();
4189
+ init_logger();
4100
4190
  init_validator();
4101
4191
  import { Command } from "commander";
4102
4192
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4, readdirSync as readdirSync2, rmSync as rmSync2 } from "fs";
4103
- import { join as join4, dirname as dirname4, resolve as resolve2, basename as basename2 } from "path";
4193
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
4194
+ import { join as join4, dirname as dirname4, resolve as resolve3, basename as basename2 } from "path";
4104
4195
  import { watch } from "chokidar";
4105
4196
  import { fileURLToPath as fileURLToPath2 } from "url";
4106
4197
 
@@ -4726,7 +4817,7 @@ var pkgJson = JSON.parse(readFileSync3(join4(__cliDirname, "..", "package.json")
4726
4817
  var program = new Command();
4727
4818
  program.name("flow2code").description("Visual AST Compiler: Compile .flow.json into native TypeScript").version(pkgJson.version);
4728
4819
  program.command("compile <file>").description("Compile .flow.json or YAML directory to TypeScript (auto-detects format)").option("-o, --output <path>", "Specify output path (overrides auto-detection)").option("--platform <name>", "Target platform: nextjs | express | cloudflare", "nextjs").option("--dry-run", "Display generated code without writing to file").option("--source-map", "Generate Source Map mapping file (.flow.map.json)").action((file, options) => {
4729
- const filePath = resolve2(file);
4820
+ const filePath = resolve3(file);
4730
4821
  if (!existsSync4(filePath)) {
4731
4822
  console.error(`\u274C File/directory not found: ${filePath}`);
4732
4823
  process.exit(1);
@@ -4764,7 +4855,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4764
4855
  process.exit(1);
4765
4856
  }
4766
4857
  const outputPath = options.output ?? result.filePath;
4767
- const fullOutputPath = resolve2(outputPath);
4858
+ const fullOutputPath = resolve3(outputPath);
4768
4859
  const outputDir = dirname4(fullOutputPath);
4769
4860
  if (!existsSync4(outputDir)) {
4770
4861
  mkdirSync3(outputDir, { recursive: true });
@@ -4777,7 +4868,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4777
4868
  console.log(`\u{1F5FA}\uFE0F Source Map: ${mapPath}`);
4778
4869
  }
4779
4870
  if (result.dependencies && result.dependencies.all.length > 0) {
4780
- const projectPkgPath = resolve2("package.json");
4871
+ const projectPkgPath = resolve3("package.json");
4781
4872
  if (existsSync4(projectPkgPath)) {
4782
4873
  try {
4783
4874
  const pkgJson2 = JSON.parse(readFileSync3(projectPkgPath, "utf-8"));
@@ -4800,7 +4891,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4800
4891
  });
4801
4892
  program.command("audit <file>").description("Decompile any TypeScript file into a visual FlowIR for code auditing").option("-o, --output <path>", "Write IR JSON to file instead of stdout").option("--format <fmt>", "Output format: json | mermaid | summary", "summary").option("--function <name>", "Target function name to decompile").option("--no-audit-hints", "Disable audit hints").action(async (file, options) => {
4802
4893
  const { decompile: decompile2 } = await Promise.resolve().then(() => (init_decompiler(), decompiler_exports));
4803
- const filePath = resolve2(file);
4894
+ const filePath = resolve3(file);
4804
4895
  if (!existsSync4(filePath)) {
4805
4896
  console.error(`\u274C File not found: ${filePath}`);
4806
4897
  process.exit(1);
@@ -4820,7 +4911,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4820
4911
  if (fmt === "json") {
4821
4912
  const output = JSON.stringify(result.ir, null, 2);
4822
4913
  if (options.output) {
4823
- writeFileSync3(resolve2(options.output), output, "utf-8");
4914
+ writeFileSync3(resolve3(options.output), output, "utf-8");
4824
4915
  console.log(`\u2705 IR written to: ${options.output}`);
4825
4916
  } else {
4826
4917
  console.log(output);
@@ -4828,7 +4919,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4828
4919
  } else if (fmt === "mermaid") {
4829
4920
  const mermaidOutput = irToMermaid(result.ir);
4830
4921
  if (options.output) {
4831
- writeFileSync3(resolve2(options.output), mermaidOutput, "utf-8");
4922
+ writeFileSync3(resolve3(options.output), mermaidOutput, "utf-8");
4832
4923
  console.log(`\u2705 Mermaid diagram written to: ${options.output}`);
4833
4924
  } else {
4834
4925
  console.log(mermaidOutput);
@@ -4855,7 +4946,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4855
4946
  console.log("");
4856
4947
  if (options.output) {
4857
4948
  const irJson = JSON.stringify(result.ir, null, 2);
4858
- writeFileSync3(resolve2(options.output), irJson, "utf-8");
4949
+ writeFileSync3(resolve3(options.output), irJson, "utf-8");
4859
4950
  console.log(`\u2705 IR written to: ${options.output}`);
4860
4951
  }
4861
4952
  }
@@ -4889,8 +4980,8 @@ function irToMermaid(ir) {
4889
4980
  return lines.join("\n");
4890
4981
  }
4891
4982
  program.command("watch [dir]").description("Watch directory, auto-compile .flow.json and YAML directory changes").option("-p, --project <path>", "Next.js project root directory", ".").action((dir = ".", options) => {
4892
- const watchDir = resolve2(dir);
4893
- const projectRoot = resolve2(options.project ?? ".");
4983
+ const watchDir = resolve3(dir);
4984
+ const projectRoot = resolve3(options.project ?? ".");
4894
4985
  console.log(`\u{1F440} Watching: ${watchDir}/**/*.flow.json + **/*.yaml`);
4895
4986
  console.log(`\u{1F4C1} Output to: ${projectRoot}`);
4896
4987
  console.log("Press Ctrl+C to stop\n");
@@ -4901,17 +4992,28 @@ program.command("watch [dir]").description("Watch directory, auto-compile .flow.
4901
4992
  ignoreInitial: false
4902
4993
  }
4903
4994
  );
4995
+ const pendingTimers = /* @__PURE__ */ new Map();
4996
+ const DEBOUNCE_MS = 150;
4904
4997
  const handleChange = (filePath) => {
4905
- if (filePath.endsWith(".flow.json")) {
4906
- compileFile(filePath, projectRoot);
4907
- } else if (filePath.endsWith(".yaml")) {
4998
+ let compileKey = filePath;
4999
+ if (filePath.endsWith(".yaml")) {
4908
5000
  let dir2 = dirname4(filePath);
4909
5001
  if (basename2(dir2) === "nodes") dir2 = dirname4(dir2);
4910
- const metaPath = join4(dir2, "meta.yaml");
4911
- if (existsSync4(metaPath)) {
4912
- compileFlowDir(dir2, projectRoot);
5002
+ compileKey = dir2;
5003
+ }
5004
+ const existing = pendingTimers.get(compileKey);
5005
+ if (existing) clearTimeout(existing);
5006
+ pendingTimers.set(compileKey, setTimeout(() => {
5007
+ pendingTimers.delete(compileKey);
5008
+ if (filePath.endsWith(".flow.json")) {
5009
+ compileFileAsync(filePath, projectRoot);
5010
+ } else if (filePath.endsWith(".yaml")) {
5011
+ const metaPath = join4(compileKey, "meta.yaml");
5012
+ if (existsSync4(metaPath)) {
5013
+ compileFlowDirAsync(compileKey, projectRoot);
5014
+ }
4913
5015
  }
4914
- }
5016
+ }, DEBOUNCE_MS));
4915
5017
  };
4916
5018
  watcher.on("change", handleChange);
4917
5019
  watcher.on("add", handleChange);
@@ -4920,7 +5022,7 @@ program.command("watch [dir]").description("Watch directory, auto-compile .flow.
4920
5022
  });
4921
5023
  });
4922
5024
  program.command("init").description("Initialize Flow2Code in current project (Zero Pollution mode)").action(() => {
4923
- const flow2codeDir = resolve2(".flow2code");
5025
+ const flow2codeDir = resolve3(".flow2code");
4924
5026
  const flowsDir = join4(flow2codeDir, "flows");
4925
5027
  if (!existsSync4(flow2codeDir)) {
4926
5028
  mkdirSync3(flow2codeDir, { recursive: true });
@@ -4990,7 +5092,7 @@ program.command("init").description("Initialize Flow2Code in current project (Ze
4990
5092
  writeFileSync3(examplePath, JSON.stringify(exampleFlow, null, 2), "utf-8");
4991
5093
  console.log(`\u{1F4C4} Created example: ${examplePath}`);
4992
5094
  }
4993
- const gitignorePath = resolve2(".gitignore");
5095
+ const gitignorePath = resolve3(".gitignore");
4994
5096
  if (existsSync4(gitignorePath)) {
4995
5097
  const content = readFileSync3(gitignorePath, "utf-8");
4996
5098
  if (!content.includes(".flow2code/")) {
@@ -5009,7 +5111,7 @@ program.command("init").description("Initialize Flow2Code in current project (Ze
5009
5111
  console.log(` npx ${pkgJson.name} watch .flow2code/flows/`);
5010
5112
  });
5011
5113
  program.command("split <file>").description("Split .flow.json into a Git-friendly YAML directory structure").option("-o, --output <dir>", "Specify output directory (default: same name as file)").action((file, options) => {
5012
- const filePath = resolve2(file);
5114
+ const filePath = resolve3(file);
5013
5115
  if (!existsSync4(filePath)) {
5014
5116
  console.error(`\u274C File not found: ${filePath}`);
5015
5117
  process.exit(1);
@@ -5025,7 +5127,7 @@ program.command("split <file>").description("Split .flow.json into a Git-friendl
5025
5127
  const outputDir = options.output ?? filePath.replace(/\.flow\.json$|\.json$/, "");
5026
5128
  const written = splitToFileSystem(
5027
5129
  ir,
5028
- resolve2(outputDir),
5130
+ resolve3(outputDir),
5029
5131
  { mkdirSync: mkdirSync3, writeFileSync: (p, c) => writeFileSync3(p, c, "utf-8") },
5030
5132
  { join: join4 }
5031
5133
  );
@@ -5033,7 +5135,7 @@ program.command("split <file>").description("Split .flow.json into a Git-friendl
5033
5135
  written.forEach((f) => console.log(` \u{1F4C4} ${f}`));
5034
5136
  });
5035
5137
  program.command("merge <dir>").description("Merge YAML directory structure into a .flow.json").option("-o, --output <file>", "Specify output file path").action((dir, options) => {
5036
- const dirPath = resolve2(dir);
5138
+ const dirPath = resolve3(dir);
5037
5139
  if (!existsSync4(dirPath)) {
5038
5140
  console.error(`\u274C Directory not found: ${dirPath}`);
5039
5141
  process.exit(1);
@@ -5045,7 +5147,7 @@ program.command("merge <dir>").description("Merge YAML directory structure into
5045
5147
  { join: join4 }
5046
5148
  );
5047
5149
  const outputFile = options.output ?? `${dirPath}.flow.json`;
5048
- writeFileSync3(resolve2(outputFile), JSON.stringify(ir, null, 2), "utf-8");
5150
+ writeFileSync3(resolve3(outputFile), JSON.stringify(ir, null, 2), "utf-8");
5049
5151
  console.log(`\u2705 Merged to: ${outputFile}`);
5050
5152
  console.log(` ${ir.nodes.length} nodes, ${ir.edges.length} edges`);
5051
5153
  } catch (err) {
@@ -5054,7 +5156,7 @@ program.command("merge <dir>").description("Merge YAML directory structure into
5054
5156
  }
5055
5157
  });
5056
5158
  program.command("migrate <file>").description("Migrate .flow.json to Git-friendly YAML directory (preserves original file)").option("--delete-json", "Delete original .flow.json after successful migration").action((file, options) => {
5057
- const filePath = resolve2(file);
5159
+ const filePath = resolve3(file);
5058
5160
  if (!existsSync4(filePath)) {
5059
5161
  console.error(`\u274C File not found: ${filePath}`);
5060
5162
  process.exit(1);
@@ -5075,7 +5177,7 @@ program.command("migrate <file>").description("Migrate .flow.json to Git-friendl
5075
5177
  }
5076
5178
  });
5077
5179
  program.command("trace <file> <line>").description("Trace a generated code line number back to its canvas node (Source Map)").action((file, lineStr) => {
5078
- const filePath = resolve2(file);
5180
+ const filePath = resolve3(file);
5079
5181
  const lineNum = parseInt(lineStr, 10);
5080
5182
  if (isNaN(lineNum) || lineNum < 1) {
5081
5183
  console.error("\u274C Line number must be a positive integer");
@@ -5105,8 +5207,8 @@ program.command("trace <file> <line>").description("Trace a generated code line
5105
5207
  }
5106
5208
  });
5107
5209
  program.command("diff <before> <after>").description("Compare semantic differences between two .flow.json files").action((beforeFile, afterFile) => {
5108
- const beforePath = resolve2(beforeFile);
5109
- const afterPath = resolve2(afterFile);
5210
+ const beforePath = resolve3(beforeFile);
5211
+ const afterPath = resolve3(afterFile);
5110
5212
  if (!existsSync4(beforePath)) {
5111
5213
  console.error(`\u274C File not found: ${beforePath}`);
5112
5214
  process.exit(1);
@@ -5128,8 +5230,8 @@ program.command("diff <before> <after>").description("Compare semantic differenc
5128
5230
  console.log(formatDiff(summary));
5129
5231
  });
5130
5232
  program.command("env-check <file>").description("Validate that environment variables referenced in .flow.json are declared").option("-e, --env <envFile>", "Specify .env file path", ".env").action((file, options) => {
5131
- const filePath = resolve2(file);
5132
- const envPath = resolve2(options.env);
5233
+ const filePath = resolve3(file);
5234
+ const envPath = resolve3(options.env);
5133
5235
  if (!existsSync4(filePath)) {
5134
5236
  console.error(`\u274C File not found: ${filePath}`);
5135
5237
  process.exit(1);
@@ -5146,7 +5248,7 @@ program.command("env-check <file>").description("Validate that environment varia
5146
5248
  const envContent = readFileSync3(envPath, "utf-8");
5147
5249
  declaredVars = parseEnvFile(envContent);
5148
5250
  } else {
5149
- const examplePath = resolve2(".env.example");
5251
+ const examplePath = resolve3(".env.example");
5150
5252
  if (existsSync4(examplePath)) {
5151
5253
  const envContent = readFileSync3(examplePath, "utf-8");
5152
5254
  declaredVars = parseEnvFile(envContent);
@@ -5157,7 +5259,11 @@ program.command("env-check <file>").description("Validate that environment varia
5157
5259
  `);
5158
5260
  }
5159
5261
  }
5160
- const result = validateEnvVars(ir, declaredVars);
5262
+ const systemEnvKeys = Object.keys(process.env).filter(
5263
+ (k) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)
5264
+ );
5265
+ const allDeclaredVars = [.../* @__PURE__ */ new Set([...declaredVars, ...systemEnvKeys])];
5266
+ const result = validateEnvVars(ir, allDeclaredVars);
5161
5267
  console.log(formatEnvValidationReport(result));
5162
5268
  if (!result.valid) {
5163
5269
  process.exit(1);
@@ -5182,12 +5288,29 @@ async function startDevServer(options) {
5182
5288
  }
5183
5289
  });
5184
5290
  }
5185
- program.command("dev").description("Start Flow2Code visual editor (standalone dev server)").option("-p, --port <port>", "Server port", "3100").option("--no-open", "Do not auto-open browser").action(startDevServer);
5186
- program.command("ui").description("Start Flow2Code visual editor (alias for dev)").option("-p, --port <port>", "Server port", "3100").option("--no-open", "Do not auto-open browser").action(startDevServer);
5187
- function compileFile(filePath, projectRoot) {
5291
+ program.command("dev").description("Start Flow2Code visual editor (standalone dev server)").option("-p, --port <port>", "Server port", "3100").option("--no-open", "Do not auto-open browser").option("--silent", "Suppress non-error output (for CI/CD)").action((opts) => {
5292
+ if (opts.silent) logger.level = "silent";
5293
+ startDevServer(opts);
5294
+ });
5295
+ program.command("ui").description("Start Flow2Code visual editor (alias for dev)").option("-p, --port <port>", "Server port", "3100").option("--no-open", "Do not auto-open browser").option("--silent", "Suppress non-error output (for CI/CD)").action((opts) => {
5296
+ if (opts.silent) logger.level = "silent";
5297
+ startDevServer(opts);
5298
+ });
5299
+ function generateEnvExample() {
5300
+ const envExamplePath = resolve3(".env.example");
5301
+ if (!existsSync4(envExamplePath)) {
5302
+ writeFileSync3(
5303
+ envExamplePath,
5304
+ "# Flow2Code environment variables\n# Define API keys and sensitive information here\n",
5305
+ "utf-8"
5306
+ );
5307
+ console.log("\u{1F4DD} Generated .env.example");
5308
+ }
5309
+ }
5310
+ async function compileFileAsync(filePath, projectRoot) {
5188
5311
  const startTime = Date.now();
5189
5312
  try {
5190
- const raw = readFileSync3(filePath, "utf-8");
5313
+ const raw = await readFile2(filePath, "utf-8");
5191
5314
  const ir = JSON.parse(raw);
5192
5315
  const validation = validateFlowIR(ir);
5193
5316
  if (!validation.valid) {
@@ -5208,9 +5331,9 @@ function compileFile(filePath, projectRoot) {
5208
5331
  const outputPath = join4(projectRoot, result.filePath);
5209
5332
  const outputDir = dirname4(outputPath);
5210
5333
  if (!existsSync4(outputDir)) {
5211
- mkdirSync3(outputDir, { recursive: true });
5334
+ await mkdir(outputDir, { recursive: true });
5212
5335
  }
5213
- writeFileSync3(outputPath, result.code, "utf-8");
5336
+ await writeFile(outputPath, result.code, "utf-8");
5214
5337
  const elapsed = Date.now() - startTime;
5215
5338
  console.log(`\u2705 [${elapsed}ms] ${filePath} \u2192 ${outputPath}`);
5216
5339
  } catch (err) {
@@ -5219,7 +5342,7 @@ function compileFile(filePath, projectRoot) {
5219
5342
  );
5220
5343
  }
5221
5344
  }
5222
- function compileFlowDir(dirPath, projectRoot) {
5345
+ async function compileFlowDirAsync(dirPath, projectRoot) {
5223
5346
  const startTime = Date.now();
5224
5347
  try {
5225
5348
  const project = loadFlowProject(dirPath);
@@ -5243,9 +5366,9 @@ function compileFlowDir(dirPath, projectRoot) {
5243
5366
  const outputPath = join4(projectRoot, result.filePath);
5244
5367
  const outputDir = dirname4(outputPath);
5245
5368
  if (!existsSync4(outputDir)) {
5246
- mkdirSync3(outputDir, { recursive: true });
5369
+ await mkdir(outputDir, { recursive: true });
5247
5370
  }
5248
- writeFileSync3(outputPath, result.code, "utf-8");
5371
+ await writeFile(outputPath, result.code, "utf-8");
5249
5372
  const elapsed = Date.now() - startTime;
5250
5373
  console.log(`\u2705 [${elapsed}ms] ${dirPath}/ \u2192 ${outputPath}`);
5251
5374
  } catch (err) {
@@ -5254,17 +5377,6 @@ function compileFlowDir(dirPath, projectRoot) {
5254
5377
  );
5255
5378
  }
5256
5379
  }
5257
- function generateEnvExample() {
5258
- const envExamplePath = resolve2(".env.example");
5259
- if (!existsSync4(envExamplePath)) {
5260
- writeFileSync3(
5261
- envExamplePath,
5262
- "# Flow2Code environment variables\n# Define API keys and sensitive information here\n",
5263
- "utf-8"
5264
- );
5265
- console.log("\u{1F4DD} Generated .env.example");
5266
- }
5267
- }
5268
5380
  if (process.argv.length <= 2) {
5269
5381
  program.help();
5270
5382
  }