@synergenius/flow-weaver 0.10.10 → 0.10.12

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/dist/cli/index.js CHANGED
@@ -19,6 +19,7 @@ import { initCommand } from './commands/init.js';
19
19
  import { watchCommand } from './commands/watch.js';
20
20
  import { devCommand } from './commands/dev.js';
21
21
  import { listenCommand } from './commands/listen.js';
22
+ import { tunnelCommand } from './commands/tunnel.js';
22
23
  import { mcpServerCommand } from '../mcp/server.js';
23
24
  import { uiFocusNode, uiAddNode, uiOpenWorkflow, uiGetState, uiBatch } from './commands/ui.js';
24
25
  import { grammarCommand } from './commands/grammar.js';
@@ -269,6 +270,22 @@ program
269
270
  process.exit(1);
270
271
  }
271
272
  });
273
+ // Tunnel command
274
+ program
275
+ .command('tunnel')
276
+ .description('Connect cloud Studio to your local project directory')
277
+ .requiredOption('-k, --key <apiKey>', 'API key for cloud authentication (fw_xxxx)')
278
+ .option('-c, --cloud <url>', 'Cloud server URL', 'https://flowweaver.dev')
279
+ .option('-d, --dir <path>', 'Project directory', process.cwd())
280
+ .action(async (options) => {
281
+ try {
282
+ await tunnelCommand(options);
283
+ }
284
+ catch (error) {
285
+ logger.error(`Command failed: ${getErrorMessage(error)}`);
286
+ process.exit(1);
287
+ }
288
+ });
272
289
  // MCP server command
273
290
  program
274
291
  .command('mcp-server')
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Method dispatcher for the self-contained tunnel.
3
+ *
4
+ * Maps RPC method names to handler functions and wraps them
5
+ * in a standard try/catch envelope.
6
+ */
7
+ export interface TunnelContext {
8
+ workspaceRoot: string;
9
+ }
10
+ export type HandlerFn = (params: Record<string, unknown>, ctx: TunnelContext) => Promise<unknown>;
11
+ export declare function dispatch(method: string, params: Record<string, unknown>, ctx: TunnelContext): Promise<{
12
+ success: boolean;
13
+ result?: unknown;
14
+ error?: {
15
+ message: string;
16
+ };
17
+ }>;
18
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Method dispatcher for the self-contained tunnel.
3
+ *
4
+ * Maps RPC method names to handler functions and wraps them
5
+ * in a standard try/catch envelope.
6
+ */
7
+ import { fileOpsHandlers } from './handlers/file-ops.js';
8
+ import { astOpsHandlers } from './handlers/ast-ops.js';
9
+ import { mutationHandlers } from './handlers/mutations.js';
10
+ import { templateHandlers } from './handlers/templates.js';
11
+ import { executionHandlers } from './handlers/execution.js';
12
+ import { stubHandlers } from './handlers/stubs.js';
13
+ const handlers = {
14
+ ...stubHandlers,
15
+ ...fileOpsHandlers,
16
+ ...astOpsHandlers,
17
+ ...mutationHandlers,
18
+ ...templateHandlers,
19
+ ...executionHandlers,
20
+ };
21
+ export async function dispatch(method, params, ctx) {
22
+ const handler = handlers[method];
23
+ if (!handler) {
24
+ // Unknown methods return undefined — matches platform behaviour
25
+ return { success: true, result: undefined };
26
+ }
27
+ try {
28
+ const result = await handler(params, ctx);
29
+ return { success: true, result };
30
+ }
31
+ catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return { success: false, error: { message } };
34
+ }
35
+ }
36
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * File mutation lock — serializes concurrent mutations to the same file.
3
+ *
4
+ * Ported from flow-weaver-platform/src/services/ast-helpers.ts:54-85.
5
+ * Prevents file corruption when rapid Studio mutations (e.g. dragging nodes)
6
+ * trigger concurrent write operations.
7
+ */
8
+ export declare function withFileLock<T>(filePath: string, operation: () => Promise<T>): Promise<T>;
9
+ //# sourceMappingURL=file-lock.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * File mutation lock — serializes concurrent mutations to the same file.
3
+ *
4
+ * Ported from flow-weaver-platform/src/services/ast-helpers.ts:54-85.
5
+ * Prevents file corruption when rapid Studio mutations (e.g. dragging nodes)
6
+ * trigger concurrent write operations.
7
+ */
8
+ import * as path from 'node:path';
9
+ const fileMutationLocks = new Map();
10
+ export function withFileLock(filePath, operation) {
11
+ const normalizedPath = path.resolve(filePath);
12
+ const existingChain = fileMutationLocks.get(normalizedPath) || Promise.resolve();
13
+ let resolve;
14
+ let reject;
15
+ const resultPromise = new Promise((res, rej) => {
16
+ resolve = res;
17
+ reject = rej;
18
+ });
19
+ const newChain = existingChain.then(async () => {
20
+ try {
21
+ const result = await operation();
22
+ resolve(result);
23
+ }
24
+ catch (error) {
25
+ reject(error);
26
+ }
27
+ });
28
+ fileMutationLocks.set(normalizedPath, newChain);
29
+ newChain.finally(() => {
30
+ if (fileMutationLocks.get(normalizedPath) === newChain) {
31
+ fileMutationLocks.delete(normalizedPath);
32
+ }
33
+ });
34
+ return resultPromise;
35
+ }
36
+ //# sourceMappingURL=file-lock.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * AST read-operation handlers for the tunnel CLI.
3
+ * Ported from flow-weaver-platform/src/routes/studio-rpc.ts + src/services/ast-helpers.ts.
4
+ */
5
+ import type { HandlerFn } from '../dispatch.js';
6
+ declare function prepareMutationResult(ast: Record<string, unknown>, wsPath: string): Record<string, unknown>;
7
+ declare function getWorkflowName(params: Record<string, unknown>): string | undefined;
8
+ export { prepareMutationResult, getWorkflowName };
9
+ export declare const astOpsHandlers: Record<string, HandlerFn>;
10
+ //# sourceMappingURL=ast-ops.d.ts.map
@@ -0,0 +1,252 @@
1
+ /**
2
+ * AST read-operation handlers for the tunnel CLI.
3
+ * Ported from flow-weaver-platform/src/routes/studio-rpc.ts + src/services/ast-helpers.ts.
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { parser, resolveNpmNodeTypes } from '../../../parser.js';
8
+ import { validateWorkflow } from '../../../api/validate.js';
9
+ import { resolvePath, toVirtualPath } from '../path-resolver.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers — ported from studio-rpc.ts lines 98-186
12
+ // ---------------------------------------------------------------------------
13
+ function ensureASTDefaults(ast) {
14
+ return {
15
+ ...ast,
16
+ instances: ast.instances ?? [],
17
+ connections: ast.connections ?? [],
18
+ nodeTypes: ast.nodeTypes ?? [],
19
+ };
20
+ }
21
+ function virtualizeASTPaths(ast, wsPath) {
22
+ const result = { ...ast };
23
+ // 1. sourceFile
24
+ if (typeof result.sourceFile === 'string' && result.sourceFile.startsWith(wsPath)) {
25
+ result.sourceFile = toVirtualPath(wsPath, result.sourceFile);
26
+ }
27
+ // 2. nodeTypes[].sourceLocation.file and nodeTypes[].path
28
+ if (Array.isArray(result.nodeTypes)) {
29
+ result.nodeTypes = result.nodeTypes.map((nt) => {
30
+ const copy = { ...nt };
31
+ if (copy.sourceLocation?.file?.startsWith(wsPath)) {
32
+ copy.sourceLocation = { ...copy.sourceLocation, file: toVirtualPath(wsPath, copy.sourceLocation.file) };
33
+ }
34
+ if (typeof copy.path === 'string' && copy.path.startsWith(wsPath)) {
35
+ copy.path = toVirtualPath(wsPath, copy.path);
36
+ }
37
+ return copy;
38
+ });
39
+ }
40
+ // 3. instances[].sourceLocation.file
41
+ if (Array.isArray(result.instances)) {
42
+ result.instances = result.instances.map((inst) => {
43
+ if (inst.sourceLocation?.file?.startsWith(wsPath)) {
44
+ return {
45
+ ...inst,
46
+ sourceLocation: { ...inst.sourceLocation, file: toVirtualPath(wsPath, inst.sourceLocation.file) },
47
+ };
48
+ }
49
+ return inst;
50
+ });
51
+ }
52
+ return result;
53
+ }
54
+ function prepareMutationResult(ast, wsPath) {
55
+ return virtualizeASTPaths(ensureASTDefaults(ast), wsPath);
56
+ }
57
+ function getWorkflowName(params) {
58
+ return (params.functionName || params.workflowName || params.exportName);
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Core AST operations — ported from ast-helpers.ts
62
+ // ---------------------------------------------------------------------------
63
+ async function loadWorkflowAST(filePath, functionName) {
64
+ const parsed = parser.parse(filePath);
65
+ const workflows = parsed.workflows || [];
66
+ if (workflows.length === 0) {
67
+ throw new Error(`No workflows found in ${filePath}`);
68
+ }
69
+ const target = functionName
70
+ ? workflows.find((w) => w.functionName === functionName)
71
+ : workflows[0];
72
+ if (!target) {
73
+ throw new Error(`Workflow "${functionName}" not found in ${filePath}`);
74
+ }
75
+ return resolveNpmNodeTypes(target, path.dirname(filePath));
76
+ }
77
+ async function loadAllWorkflowsAST(wsPath) {
78
+ const entries = await fs.readdir(wsPath, { withFileTypes: true });
79
+ const results = [];
80
+ for (const entry of entries) {
81
+ if (!entry.isFile() || !entry.name.endsWith('.ts'))
82
+ continue;
83
+ if (entry.name === 'tsconfig.json' || entry.name === 'package.json')
84
+ continue;
85
+ const fullPath = path.join(wsPath, entry.name);
86
+ try {
87
+ const parsed = parser.parse(fullPath);
88
+ const workflows = parsed.workflows || [];
89
+ for (const wf of workflows) {
90
+ const resolved = await resolveNpmNodeTypes(wf, wsPath);
91
+ results.push({ filePath: fullPath, ast: resolved });
92
+ }
93
+ }
94
+ catch {
95
+ // Skip files that fail to parse
96
+ }
97
+ }
98
+ return results;
99
+ }
100
+ function parseWorkflowFromContent(content) {
101
+ const parsed = parser.parseFromString(content);
102
+ const workflows = parsed.workflows || [];
103
+ return workflows.map((wf) => ({
104
+ name: wf.name || wf.functionName,
105
+ ast: wf,
106
+ }));
107
+ }
108
+ function getDiagnostics(source) {
109
+ const parsed = parser.parseFromString(source);
110
+ if ((parsed.errors && parsed.errors.length > 0) || !parsed.workflows?.length) {
111
+ return {
112
+ valid: false,
113
+ errors: (parsed.errors || []).map((e) => ({ message: typeof e === 'string' ? e : e.message || String(e) })),
114
+ warnings: [],
115
+ };
116
+ }
117
+ const allErrors = [];
118
+ const allWarnings = [];
119
+ for (const wf of parsed.workflows) {
120
+ const result = validateWorkflow(wf);
121
+ if (result.errors)
122
+ allErrors.push(...result.errors);
123
+ if (result.warnings)
124
+ allWarnings.push(...result.warnings);
125
+ }
126
+ return { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings };
127
+ }
128
+ async function extractAllNodeTypes(wsPath) {
129
+ const all = await loadAllWorkflowsAST(wsPath);
130
+ const typeMap = new Map();
131
+ for (const { ast } of all) {
132
+ for (const nt of ast.nodeTypes || []) {
133
+ if (!typeMap.has(nt.name)) {
134
+ typeMap.set(nt.name, nt);
135
+ }
136
+ }
137
+ }
138
+ return Array.from(typeMap.values());
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Exported handlers
142
+ // ---------------------------------------------------------------------------
143
+ export { prepareMutationResult, getWorkflowName };
144
+ export const astOpsHandlers = {
145
+ loadWorkflowAST: async (params, ctx) => {
146
+ const filePath = params.filePath;
147
+ if (!filePath)
148
+ throw new Error('filePath is required');
149
+ const functionName = getWorkflowName(params);
150
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
151
+ const ast = await loadWorkflowAST(resolved, functionName);
152
+ return virtualizeASTPaths(ensureASTDefaults(ast), ctx.workspaceRoot);
153
+ },
154
+ loadAllWorkflowsAST: async (_params, ctx) => {
155
+ const all = await loadAllWorkflowsAST(ctx.workspaceRoot);
156
+ return all.map(({ filePath, ast }) => ({
157
+ filePath: toVirtualPath(ctx.workspaceRoot, filePath),
158
+ ast: virtualizeASTPaths(ensureASTDefaults(ast), ctx.workspaceRoot),
159
+ }));
160
+ },
161
+ parseWorkflowASTFromContent: async (params) => {
162
+ const content = params.content;
163
+ if (!content)
164
+ throw new Error('content is required');
165
+ const functionName = getWorkflowName(params);
166
+ const results = parseWorkflowFromContent(content);
167
+ if (functionName) {
168
+ const match = results.find((r) => r.name === functionName || r.ast?.functionName === functionName);
169
+ return match ? ensureASTDefaults(match.ast) : null;
170
+ }
171
+ return results.length > 0 ? ensureASTDefaults(results[0].ast) : null;
172
+ },
173
+ getAvailableWorkflowsInFile: async (params, ctx) => {
174
+ const filePath = params.filePath;
175
+ if (!filePath)
176
+ return { availableWorkflows: [] };
177
+ try {
178
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
179
+ const source = await fs.readFile(resolved, 'utf-8');
180
+ const results = parseWorkflowFromContent(source);
181
+ return {
182
+ availableWorkflows: results.map((w) => ({
183
+ name: w.ast?.name || w.name,
184
+ functionName: w.ast?.functionName || w.name,
185
+ isExported: true,
186
+ })),
187
+ };
188
+ }
189
+ catch {
190
+ return { availableWorkflows: [] };
191
+ }
192
+ },
193
+ getDiagnostics: async (params, ctx) => {
194
+ // Source resolution priority: openFiles > content > file on disk
195
+ let source;
196
+ const openFiles = params.openFiles;
197
+ const filePath = params.filePath;
198
+ if (openFiles && filePath && openFiles[filePath]) {
199
+ source = openFiles[filePath];
200
+ }
201
+ else if (typeof params.content === 'string') {
202
+ source = params.content;
203
+ }
204
+ else if (filePath) {
205
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
206
+ source = await fs.readFile(resolved, 'utf-8');
207
+ }
208
+ if (!source)
209
+ throw new Error('No source provided for diagnostics');
210
+ const { errors, warnings } = getDiagnostics(source);
211
+ // Transform to flat array for FileEditorPane
212
+ const result = [];
213
+ for (const e of errors) {
214
+ result.push({
215
+ severity: 'error',
216
+ message: e.message || String(e),
217
+ start: e.location ?? { line: 1, column: 0 },
218
+ });
219
+ }
220
+ for (const w of warnings) {
221
+ result.push({
222
+ severity: 'warning',
223
+ message: w.message || String(w),
224
+ start: w.location ?? { line: 1, column: 0 },
225
+ });
226
+ }
227
+ return result;
228
+ },
229
+ getNodeTypes: async (_params, ctx) => {
230
+ const types = await extractAllNodeTypes(ctx.workspaceRoot);
231
+ return types.map((t) => ({ name: t.name, label: t.name, nodeType: t }));
232
+ },
233
+ getNodeTypesBatch: async (params, ctx) => {
234
+ const cursor = params.cursor || '0';
235
+ const limit = params.limit || 50;
236
+ const offset = parseInt(cursor, 10) || 0;
237
+ const types = await extractAllNodeTypes(ctx.workspaceRoot);
238
+ const all = types.map((t) => ({ name: t.name, label: t.name, nodeType: t }));
239
+ const page = all.slice(offset, offset + limit);
240
+ const nextCursor = offset + limit < all.length ? String(offset + limit) : null;
241
+ return { types: page, cursor: nextCursor };
242
+ },
243
+ searchNodeTypes: async (params, ctx) => {
244
+ const query = (params.query || '').toLowerCase();
245
+ const types = await extractAllNodeTypes(ctx.workspaceRoot);
246
+ const all = types.map((t) => ({ name: t.name, label: t.name, nodeType: t }));
247
+ if (!query)
248
+ return all;
249
+ return all.filter((t) => t.name.toLowerCase().includes(query) || t.label.toLowerCase().includes(query));
250
+ },
251
+ };
252
+ //# sourceMappingURL=ast-ops.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Execution handlers for the tunnel CLI.
3
+ * executeFile, compileFile, generateDiagram
4
+ */
5
+ import type { HandlerFn } from '../dispatch.js';
6
+ export declare const executionHandlers: Record<string, HandlerFn>;
7
+ //# sourceMappingURL=execution.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Execution handlers for the tunnel CLI.
3
+ * executeFile, compileFile, generateDiagram
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import { compileWorkflow } from '../../../api/compile.js';
7
+ import { resolvePath } from '../path-resolver.js';
8
+ let sourceToSVG;
9
+ async function loadDiagram() {
10
+ if (sourceToSVG)
11
+ return sourceToSVG;
12
+ try {
13
+ const mod = await import('../../../diagram/index.js');
14
+ sourceToSVG = mod.sourceToSVG;
15
+ return sourceToSVG;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ export const executionHandlers = {
22
+ executeFile: async (params, ctx) => {
23
+ const filePath = params.filePath;
24
+ if (!filePath)
25
+ throw new Error('filePath is required');
26
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
27
+ const inputData = (params.inputData || params.input || {});
28
+ try {
29
+ // Dynamic import to avoid bundling issues — uses the MCP workflow executor
30
+ const { executeWorkflowFromFile } = await import('../../../mcp/workflow-executor.js');
31
+ const result = await executeWorkflowFromFile(resolved, inputData);
32
+ return result;
33
+ }
34
+ catch (err) {
35
+ return {
36
+ success: false,
37
+ error: err instanceof Error ? err.message : String(err),
38
+ };
39
+ }
40
+ },
41
+ compileFile: async (params, ctx) => {
42
+ const filePath = params.filePath;
43
+ if (!filePath)
44
+ throw new Error('filePath is required');
45
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
46
+ try {
47
+ const result = await compileWorkflow(resolved);
48
+ return result;
49
+ }
50
+ catch (err) {
51
+ return {
52
+ success: false,
53
+ error: err instanceof Error ? err.message : String(err),
54
+ };
55
+ }
56
+ },
57
+ generateDiagram: async (params, ctx) => {
58
+ const filePath = params.filePath;
59
+ const content = params.content;
60
+ let source;
61
+ if (content) {
62
+ source = content;
63
+ }
64
+ else if (filePath) {
65
+ const resolved = resolvePath(ctx.workspaceRoot, filePath);
66
+ source = await fs.readFile(resolved, 'utf-8');
67
+ }
68
+ else {
69
+ throw new Error('filePath or content is required');
70
+ }
71
+ const render = await loadDiagram();
72
+ if (!render) {
73
+ return { success: false, error: 'Diagram module not available' };
74
+ }
75
+ try {
76
+ const svg = render(source, {
77
+ workflowName: params.workflowName,
78
+ });
79
+ return { success: true, svg };
80
+ }
81
+ catch (err) {
82
+ return {
83
+ success: false,
84
+ error: err instanceof Error ? err.message : String(err),
85
+ };
86
+ }
87
+ },
88
+ };
89
+ //# sourceMappingURL=execution.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * File operation handlers for the tunnel CLI.
3
+ * Ported from flow-weaver-platform/src/routes/studio-rpc.ts file handlers.
4
+ */
5
+ import type { HandlerFn } from '../dispatch.js';
6
+ export declare const fileOpsHandlers: Record<string, HandlerFn>;
7
+ //# sourceMappingURL=file-ops.d.ts.map