@timo9378/flow2code 0.1.3 → 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, {
@@ -2840,9 +2922,13 @@ function buildEdges(ctx) {
2840
2922
  }
2841
2923
  }
2842
2924
  }
2925
+ const resolveTargetPort = (targetNodeId, fallback) => {
2926
+ const tgt = ctx.nodes.get(targetNodeId);
2927
+ return tgt?.inputs?.[0]?.id ?? fallback;
2928
+ };
2843
2929
  for (const [parentId, children] of ctx.controlFlowChildren) {
2844
2930
  for (const child of children) {
2845
- addEdge(parentId, child.portId, child.nodeId, "input");
2931
+ addEdge(parentId, child.portId, child.nodeId, resolveTargetPort(child.nodeId, "input"));
2846
2932
  connectedTargets.add(child.nodeId);
2847
2933
  }
2848
2934
  }
@@ -2851,7 +2937,7 @@ function buildEdges(ctx) {
2851
2937
  const predNode = ctx.nodes.get(predId);
2852
2938
  if (!predNode) continue;
2853
2939
  const sourcePort = predNode.outputs[0]?.id ?? "output";
2854
- addEdge(predId, sourcePort, nodeId, "input");
2940
+ addEdge(predId, sourcePort, nodeId, resolveTargetPort(nodeId, "input"));
2855
2941
  }
2856
2942
  }
2857
2943
  function computeAuditHints(ctx) {
@@ -2983,7 +3069,9 @@ function trackVariableUses(nodeId, expression, ctx) {
2983
3069
  if (seen.has(ident)) continue;
2984
3070
  seen.add(ident);
2985
3071
  if (ctx.varDefs.has(ident)) {
2986
- uses.push({ nodeId, portId: "input", varName: ident });
3072
+ const targetNode = ctx.nodes.get(nodeId);
3073
+ const portId = targetNode?.inputs?.[0]?.id ?? "input";
3074
+ uses.push({ nodeId, portId, varName: ident });
2987
3075
  }
2988
3076
  }
2989
3077
  if (uses.length > 0) {
@@ -3859,7 +3947,7 @@ __export(server_exports, {
3859
3947
  });
3860
3948
  import { createServer } from "http";
3861
3949
  import { readFile, stat } from "fs/promises";
3862
- 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";
3863
3951
  import { fileURLToPath } from "url";
3864
3952
  import { existsSync as existsSync3 } from "fs";
3865
3953
  function resolveStaticDir() {
@@ -3920,7 +4008,7 @@ function sendJson(res, status, body) {
3920
4008
  res.end(JSON.stringify(body));
3921
4009
  }
3922
4010
  async function readBody(req) {
3923
- return new Promise((resolve3, reject) => {
4011
+ return new Promise((resolve4, reject) => {
3924
4012
  const chunks = [];
3925
4013
  let totalSize = 0;
3926
4014
  req.on("data", (chunk) => {
@@ -3932,7 +4020,7 @@ async function readBody(req) {
3932
4020
  }
3933
4021
  chunks.push(chunk);
3934
4022
  });
3935
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
4023
+ req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
3936
4024
  req.on("error", reject);
3937
4025
  });
3938
4026
  }
@@ -3941,7 +4029,13 @@ async function parseJsonBody(req) {
3941
4029
  return JSON.parse(raw);
3942
4030
  }
3943
4031
  async function serveStatic(staticDir, pathname, res) {
3944
- 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
+ }
3945
4039
  if (!extname2(filePath)) {
3946
4040
  filePath += ".html";
3947
4041
  }
@@ -4029,7 +4123,7 @@ function startServer(options = {}) {
4029
4123
  const projectRoot = options.projectRoot ?? process.cwd();
4030
4124
  const server = createServer((req, res) => {
4031
4125
  handleRequest(req, res, staticDir, projectRoot).catch((err) => {
4032
- console.error("[flow2code] Internal error:", err);
4126
+ logger.error("Internal error:", err);
4033
4127
  res.writeHead(500, { "Content-Type": "text/plain" });
4034
4128
  res.end("Internal Server Error");
4035
4129
  });
@@ -4040,20 +4134,20 @@ function startServer(options = {}) {
4040
4134
  if (options.onReady) {
4041
4135
  options.onReady(url);
4042
4136
  } else {
4043
- console.log(`
4044
- \u{1F680} Flow2Code Dev Server`);
4045
- console.log(` \u251C\u2500 Local: ${url}`);
4046
- console.log(` \u251C\u2500 API: ${url}/api/compile`);
4047
- console.log(` \u251C\u2500 Static: ${staticDir}`);
4048
- 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);
4049
4143
  if (!hasUI) {
4050
- console.log();
4051
- console.log(` \u26A0\uFE0F UI files not found (out/index.html missing).`);
4052
- console.log(` The API endpoints still work, but the visual editor won't load.`);
4053
- console.log(` To fix: run "pnpm build:ui" in the flow2code source directory,`);
4054
- 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");
4055
4149
  }
4056
- console.log();
4150
+ logger.blank();
4057
4151
  }
4058
4152
  });
4059
4153
  return server;
@@ -4063,6 +4157,7 @@ var init_server = __esm({
4063
4157
  "src/server/index.ts"() {
4064
4158
  "use strict";
4065
4159
  init_handlers();
4160
+ init_logger();
4066
4161
  init_handlers();
4067
4162
  __filename = fileURLToPath(import.meta.url);
4068
4163
  __dirname = dirname3(__filename);
@@ -4091,10 +4186,12 @@ var init_server = __esm({
4091
4186
 
4092
4187
  // src/cli/index.ts
4093
4188
  init_compiler();
4189
+ init_logger();
4094
4190
  init_validator();
4095
4191
  import { Command } from "commander";
4096
4192
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4, readdirSync as readdirSync2, rmSync as rmSync2 } from "fs";
4097
- 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";
4098
4195
  import { watch } from "chokidar";
4099
4196
  import { fileURLToPath as fileURLToPath2 } from "url";
4100
4197
 
@@ -4720,7 +4817,7 @@ var pkgJson = JSON.parse(readFileSync3(join4(__cliDirname, "..", "package.json")
4720
4817
  var program = new Command();
4721
4818
  program.name("flow2code").description("Visual AST Compiler: Compile .flow.json into native TypeScript").version(pkgJson.version);
4722
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) => {
4723
- const filePath = resolve2(file);
4820
+ const filePath = resolve3(file);
4724
4821
  if (!existsSync4(filePath)) {
4725
4822
  console.error(`\u274C File/directory not found: ${filePath}`);
4726
4823
  process.exit(1);
@@ -4758,7 +4855,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4758
4855
  process.exit(1);
4759
4856
  }
4760
4857
  const outputPath = options.output ?? result.filePath;
4761
- const fullOutputPath = resolve2(outputPath);
4858
+ const fullOutputPath = resolve3(outputPath);
4762
4859
  const outputDir = dirname4(fullOutputPath);
4763
4860
  if (!existsSync4(outputDir)) {
4764
4861
  mkdirSync3(outputDir, { recursive: true });
@@ -4771,7 +4868,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4771
4868
  console.log(`\u{1F5FA}\uFE0F Source Map: ${mapPath}`);
4772
4869
  }
4773
4870
  if (result.dependencies && result.dependencies.all.length > 0) {
4774
- const projectPkgPath = resolve2("package.json");
4871
+ const projectPkgPath = resolve3("package.json");
4775
4872
  if (existsSync4(projectPkgPath)) {
4776
4873
  try {
4777
4874
  const pkgJson2 = JSON.parse(readFileSync3(projectPkgPath, "utf-8"));
@@ -4794,7 +4891,7 @@ program.command("compile <file>").description("Compile .flow.json or YAML direct
4794
4891
  });
4795
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) => {
4796
4893
  const { decompile: decompile2 } = await Promise.resolve().then(() => (init_decompiler(), decompiler_exports));
4797
- const filePath = resolve2(file);
4894
+ const filePath = resolve3(file);
4798
4895
  if (!existsSync4(filePath)) {
4799
4896
  console.error(`\u274C File not found: ${filePath}`);
4800
4897
  process.exit(1);
@@ -4814,7 +4911,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4814
4911
  if (fmt === "json") {
4815
4912
  const output = JSON.stringify(result.ir, null, 2);
4816
4913
  if (options.output) {
4817
- writeFileSync3(resolve2(options.output), output, "utf-8");
4914
+ writeFileSync3(resolve3(options.output), output, "utf-8");
4818
4915
  console.log(`\u2705 IR written to: ${options.output}`);
4819
4916
  } else {
4820
4917
  console.log(output);
@@ -4822,7 +4919,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4822
4919
  } else if (fmt === "mermaid") {
4823
4920
  const mermaidOutput = irToMermaid(result.ir);
4824
4921
  if (options.output) {
4825
- writeFileSync3(resolve2(options.output), mermaidOutput, "utf-8");
4922
+ writeFileSync3(resolve3(options.output), mermaidOutput, "utf-8");
4826
4923
  console.log(`\u2705 Mermaid diagram written to: ${options.output}`);
4827
4924
  } else {
4828
4925
  console.log(mermaidOutput);
@@ -4849,7 +4946,7 @@ program.command("audit <file>").description("Decompile any TypeScript file into
4849
4946
  console.log("");
4850
4947
  if (options.output) {
4851
4948
  const irJson = JSON.stringify(result.ir, null, 2);
4852
- writeFileSync3(resolve2(options.output), irJson, "utf-8");
4949
+ writeFileSync3(resolve3(options.output), irJson, "utf-8");
4853
4950
  console.log(`\u2705 IR written to: ${options.output}`);
4854
4951
  }
4855
4952
  }
@@ -4883,8 +4980,8 @@ function irToMermaid(ir) {
4883
4980
  return lines.join("\n");
4884
4981
  }
4885
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) => {
4886
- const watchDir = resolve2(dir);
4887
- const projectRoot = resolve2(options.project ?? ".");
4983
+ const watchDir = resolve3(dir);
4984
+ const projectRoot = resolve3(options.project ?? ".");
4888
4985
  console.log(`\u{1F440} Watching: ${watchDir}/**/*.flow.json + **/*.yaml`);
4889
4986
  console.log(`\u{1F4C1} Output to: ${projectRoot}`);
4890
4987
  console.log("Press Ctrl+C to stop\n");
@@ -4895,17 +4992,28 @@ program.command("watch [dir]").description("Watch directory, auto-compile .flow.
4895
4992
  ignoreInitial: false
4896
4993
  }
4897
4994
  );
4995
+ const pendingTimers = /* @__PURE__ */ new Map();
4996
+ const DEBOUNCE_MS = 150;
4898
4997
  const handleChange = (filePath) => {
4899
- if (filePath.endsWith(".flow.json")) {
4900
- compileFile(filePath, projectRoot);
4901
- } else if (filePath.endsWith(".yaml")) {
4998
+ let compileKey = filePath;
4999
+ if (filePath.endsWith(".yaml")) {
4902
5000
  let dir2 = dirname4(filePath);
4903
5001
  if (basename2(dir2) === "nodes") dir2 = dirname4(dir2);
4904
- const metaPath = join4(dir2, "meta.yaml");
4905
- if (existsSync4(metaPath)) {
4906
- 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
+ }
4907
5015
  }
4908
- }
5016
+ }, DEBOUNCE_MS));
4909
5017
  };
4910
5018
  watcher.on("change", handleChange);
4911
5019
  watcher.on("add", handleChange);
@@ -4914,7 +5022,7 @@ program.command("watch [dir]").description("Watch directory, auto-compile .flow.
4914
5022
  });
4915
5023
  });
4916
5024
  program.command("init").description("Initialize Flow2Code in current project (Zero Pollution mode)").action(() => {
4917
- const flow2codeDir = resolve2(".flow2code");
5025
+ const flow2codeDir = resolve3(".flow2code");
4918
5026
  const flowsDir = join4(flow2codeDir, "flows");
4919
5027
  if (!existsSync4(flow2codeDir)) {
4920
5028
  mkdirSync3(flow2codeDir, { recursive: true });
@@ -4984,7 +5092,7 @@ program.command("init").description("Initialize Flow2Code in current project (Ze
4984
5092
  writeFileSync3(examplePath, JSON.stringify(exampleFlow, null, 2), "utf-8");
4985
5093
  console.log(`\u{1F4C4} Created example: ${examplePath}`);
4986
5094
  }
4987
- const gitignorePath = resolve2(".gitignore");
5095
+ const gitignorePath = resolve3(".gitignore");
4988
5096
  if (existsSync4(gitignorePath)) {
4989
5097
  const content = readFileSync3(gitignorePath, "utf-8");
4990
5098
  if (!content.includes(".flow2code/")) {
@@ -5003,7 +5111,7 @@ program.command("init").description("Initialize Flow2Code in current project (Ze
5003
5111
  console.log(` npx ${pkgJson.name} watch .flow2code/flows/`);
5004
5112
  });
5005
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) => {
5006
- const filePath = resolve2(file);
5114
+ const filePath = resolve3(file);
5007
5115
  if (!existsSync4(filePath)) {
5008
5116
  console.error(`\u274C File not found: ${filePath}`);
5009
5117
  process.exit(1);
@@ -5019,7 +5127,7 @@ program.command("split <file>").description("Split .flow.json into a Git-friendl
5019
5127
  const outputDir = options.output ?? filePath.replace(/\.flow\.json$|\.json$/, "");
5020
5128
  const written = splitToFileSystem(
5021
5129
  ir,
5022
- resolve2(outputDir),
5130
+ resolve3(outputDir),
5023
5131
  { mkdirSync: mkdirSync3, writeFileSync: (p, c) => writeFileSync3(p, c, "utf-8") },
5024
5132
  { join: join4 }
5025
5133
  );
@@ -5027,7 +5135,7 @@ program.command("split <file>").description("Split .flow.json into a Git-friendl
5027
5135
  written.forEach((f) => console.log(` \u{1F4C4} ${f}`));
5028
5136
  });
5029
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) => {
5030
- const dirPath = resolve2(dir);
5138
+ const dirPath = resolve3(dir);
5031
5139
  if (!existsSync4(dirPath)) {
5032
5140
  console.error(`\u274C Directory not found: ${dirPath}`);
5033
5141
  process.exit(1);
@@ -5039,7 +5147,7 @@ program.command("merge <dir>").description("Merge YAML directory structure into
5039
5147
  { join: join4 }
5040
5148
  );
5041
5149
  const outputFile = options.output ?? `${dirPath}.flow.json`;
5042
- writeFileSync3(resolve2(outputFile), JSON.stringify(ir, null, 2), "utf-8");
5150
+ writeFileSync3(resolve3(outputFile), JSON.stringify(ir, null, 2), "utf-8");
5043
5151
  console.log(`\u2705 Merged to: ${outputFile}`);
5044
5152
  console.log(` ${ir.nodes.length} nodes, ${ir.edges.length} edges`);
5045
5153
  } catch (err) {
@@ -5048,7 +5156,7 @@ program.command("merge <dir>").description("Merge YAML directory structure into
5048
5156
  }
5049
5157
  });
5050
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) => {
5051
- const filePath = resolve2(file);
5159
+ const filePath = resolve3(file);
5052
5160
  if (!existsSync4(filePath)) {
5053
5161
  console.error(`\u274C File not found: ${filePath}`);
5054
5162
  process.exit(1);
@@ -5069,7 +5177,7 @@ program.command("migrate <file>").description("Migrate .flow.json to Git-friendl
5069
5177
  }
5070
5178
  });
5071
5179
  program.command("trace <file> <line>").description("Trace a generated code line number back to its canvas node (Source Map)").action((file, lineStr) => {
5072
- const filePath = resolve2(file);
5180
+ const filePath = resolve3(file);
5073
5181
  const lineNum = parseInt(lineStr, 10);
5074
5182
  if (isNaN(lineNum) || lineNum < 1) {
5075
5183
  console.error("\u274C Line number must be a positive integer");
@@ -5099,8 +5207,8 @@ program.command("trace <file> <line>").description("Trace a generated code line
5099
5207
  }
5100
5208
  });
5101
5209
  program.command("diff <before> <after>").description("Compare semantic differences between two .flow.json files").action((beforeFile, afterFile) => {
5102
- const beforePath = resolve2(beforeFile);
5103
- const afterPath = resolve2(afterFile);
5210
+ const beforePath = resolve3(beforeFile);
5211
+ const afterPath = resolve3(afterFile);
5104
5212
  if (!existsSync4(beforePath)) {
5105
5213
  console.error(`\u274C File not found: ${beforePath}`);
5106
5214
  process.exit(1);
@@ -5122,8 +5230,8 @@ program.command("diff <before> <after>").description("Compare semantic differenc
5122
5230
  console.log(formatDiff(summary));
5123
5231
  });
5124
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) => {
5125
- const filePath = resolve2(file);
5126
- const envPath = resolve2(options.env);
5233
+ const filePath = resolve3(file);
5234
+ const envPath = resolve3(options.env);
5127
5235
  if (!existsSync4(filePath)) {
5128
5236
  console.error(`\u274C File not found: ${filePath}`);
5129
5237
  process.exit(1);
@@ -5140,7 +5248,7 @@ program.command("env-check <file>").description("Validate that environment varia
5140
5248
  const envContent = readFileSync3(envPath, "utf-8");
5141
5249
  declaredVars = parseEnvFile(envContent);
5142
5250
  } else {
5143
- const examplePath = resolve2(".env.example");
5251
+ const examplePath = resolve3(".env.example");
5144
5252
  if (existsSync4(examplePath)) {
5145
5253
  const envContent = readFileSync3(examplePath, "utf-8");
5146
5254
  declaredVars = parseEnvFile(envContent);
@@ -5151,7 +5259,11 @@ program.command("env-check <file>").description("Validate that environment varia
5151
5259
  `);
5152
5260
  }
5153
5261
  }
5154
- 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);
5155
5267
  console.log(formatEnvValidationReport(result));
5156
5268
  if (!result.valid) {
5157
5269
  process.exit(1);
@@ -5176,12 +5288,29 @@ async function startDevServer(options) {
5176
5288
  }
5177
5289
  });
5178
5290
  }
5179
- 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);
5180
- 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);
5181
- 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) {
5182
5311
  const startTime = Date.now();
5183
5312
  try {
5184
- const raw = readFileSync3(filePath, "utf-8");
5313
+ const raw = await readFile2(filePath, "utf-8");
5185
5314
  const ir = JSON.parse(raw);
5186
5315
  const validation = validateFlowIR(ir);
5187
5316
  if (!validation.valid) {
@@ -5202,9 +5331,9 @@ function compileFile(filePath, projectRoot) {
5202
5331
  const outputPath = join4(projectRoot, result.filePath);
5203
5332
  const outputDir = dirname4(outputPath);
5204
5333
  if (!existsSync4(outputDir)) {
5205
- mkdirSync3(outputDir, { recursive: true });
5334
+ await mkdir(outputDir, { recursive: true });
5206
5335
  }
5207
- writeFileSync3(outputPath, result.code, "utf-8");
5336
+ await writeFile(outputPath, result.code, "utf-8");
5208
5337
  const elapsed = Date.now() - startTime;
5209
5338
  console.log(`\u2705 [${elapsed}ms] ${filePath} \u2192 ${outputPath}`);
5210
5339
  } catch (err) {
@@ -5213,7 +5342,7 @@ function compileFile(filePath, projectRoot) {
5213
5342
  );
5214
5343
  }
5215
5344
  }
5216
- function compileFlowDir(dirPath, projectRoot) {
5345
+ async function compileFlowDirAsync(dirPath, projectRoot) {
5217
5346
  const startTime = Date.now();
5218
5347
  try {
5219
5348
  const project = loadFlowProject(dirPath);
@@ -5237,9 +5366,9 @@ function compileFlowDir(dirPath, projectRoot) {
5237
5366
  const outputPath = join4(projectRoot, result.filePath);
5238
5367
  const outputDir = dirname4(outputPath);
5239
5368
  if (!existsSync4(outputDir)) {
5240
- mkdirSync3(outputDir, { recursive: true });
5369
+ await mkdir(outputDir, { recursive: true });
5241
5370
  }
5242
- writeFileSync3(outputPath, result.code, "utf-8");
5371
+ await writeFile(outputPath, result.code, "utf-8");
5243
5372
  const elapsed = Date.now() - startTime;
5244
5373
  console.log(`\u2705 [${elapsed}ms] ${dirPath}/ \u2192 ${outputPath}`);
5245
5374
  } catch (err) {
@@ -5248,17 +5377,6 @@ function compileFlowDir(dirPath, projectRoot) {
5248
5377
  );
5249
5378
  }
5250
5379
  }
5251
- function generateEnvExample() {
5252
- const envExamplePath = resolve2(".env.example");
5253
- if (!existsSync4(envExamplePath)) {
5254
- writeFileSync3(
5255
- envExamplePath,
5256
- "# Flow2Code environment variables\n# Define API keys and sensitive information here\n",
5257
- "utf-8"
5258
- );
5259
- console.log("\u{1F4DD} Generated .env.example");
5260
- }
5261
- }
5262
5380
  if (process.argv.length <= 2) {
5263
5381
  program.help();
5264
5382
  }