@synergenius/flow-weaver 0.23.0 → 0.23.3
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/README.md +93 -442
- package/dist/agent/device-connection.d.ts +79 -0
- package/dist/agent/device-connection.js +184 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/providers/claude-cli.d.ts +1 -0
- package/dist/agent/providers/claude-cli.js +3 -0
- package/dist/agent/types.d.ts +2 -0
- package/dist/cli/commands/auth.js +33 -23
- package/dist/cli/commands/connect.d.ts +4 -0
- package/dist/cli/commands/connect.js +163 -0
- package/dist/cli/flow-weaver.mjs +721 -356
- package/dist/cli/index.js +9 -0
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/dist/marketplace/index.d.ts +1 -1
- package/dist/marketplace/index.js +1 -1
- package/dist/marketplace/registry.d.ts +16 -0
- package/dist/marketplace/registry.js +21 -0
- package/dist/marketplace/types.d.ts +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Connection — WebSocket client for connecting a local machine
|
|
3
|
+
* to the Flow Weaver platform as a mounted device.
|
|
4
|
+
*
|
|
5
|
+
* This is the transport layer. Packs register their own request handlers
|
|
6
|
+
* on top of this connection (e.g., improve status, bot management).
|
|
7
|
+
*
|
|
8
|
+
* The platform relays Studio requests to connected devices and forwards
|
|
9
|
+
* device events to Studio subscribers.
|
|
10
|
+
*/
|
|
11
|
+
export class DeviceConnection {
|
|
12
|
+
ws = null;
|
|
13
|
+
heartbeatInterval = null;
|
|
14
|
+
reconnectTimeout = null;
|
|
15
|
+
requestHandlers = new Map();
|
|
16
|
+
connected = false;
|
|
17
|
+
shouldReconnect = true;
|
|
18
|
+
options;
|
|
19
|
+
deviceInfo;
|
|
20
|
+
log;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.log = options.logger ?? (() => { });
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
this.deviceInfo = {
|
|
26
|
+
name: options.deviceName ?? os.hostname(),
|
|
27
|
+
hostname: os.hostname(),
|
|
28
|
+
projectDir: options.projectDir,
|
|
29
|
+
platform: process.platform,
|
|
30
|
+
capabilities: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Add a capability to advertise to the platform.
|
|
35
|
+
*/
|
|
36
|
+
addCapability(capability) {
|
|
37
|
+
if (!this.deviceInfo.capabilities.includes(capability)) {
|
|
38
|
+
this.deviceInfo.capabilities.push(capability);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Set the list of packs that contributed device handlers.
|
|
43
|
+
* The platform uses this to auto-install packs in the user's workspace.
|
|
44
|
+
*/
|
|
45
|
+
setPacks(packs) {
|
|
46
|
+
this.deviceInfo.packs = packs;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register a handler for incoming requests from the platform.
|
|
50
|
+
*/
|
|
51
|
+
onRequest(method, handler) {
|
|
52
|
+
this.requestHandlers.set(method, handler);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Connect to the platform. Reconnects automatically on disconnect.
|
|
56
|
+
*/
|
|
57
|
+
async connect() {
|
|
58
|
+
const wsUrl = this.options.platformUrl
|
|
59
|
+
.replace(/^http/, 'ws')
|
|
60
|
+
.replace(/\/$/, '') + '/ws/device';
|
|
61
|
+
this.log(`Connecting to ${wsUrl}...`);
|
|
62
|
+
this.shouldReconnect = true;
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
try {
|
|
65
|
+
this.ws = new WebSocket(`${wsUrl}?token=${encodeURIComponent(this.options.token)}`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
reject(err);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.ws.addEventListener('open', () => {
|
|
72
|
+
this.connected = true;
|
|
73
|
+
this.log(`Connected as "${this.deviceInfo.name}"`);
|
|
74
|
+
// Send device registration
|
|
75
|
+
this.send({ type: 'device:register', device: this.deviceInfo });
|
|
76
|
+
// Start heartbeat
|
|
77
|
+
this.heartbeatInterval = setInterval(() => {
|
|
78
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
79
|
+
this.send({ type: 'heartbeat', timestamp: Date.now() });
|
|
80
|
+
}
|
|
81
|
+
}, 30_000);
|
|
82
|
+
this.options.onConnect?.();
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
this.ws.addEventListener('message', async (event) => {
|
|
86
|
+
try {
|
|
87
|
+
const msg = JSON.parse(typeof event.data === 'string' ? event.data : String(event.data));
|
|
88
|
+
await this.handleMessage(msg);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
this.log(`Parse error: ${err instanceof Error ? err.message : err}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
this.ws.addEventListener('close', (event) => {
|
|
95
|
+
this.connected = false;
|
|
96
|
+
if (this.heartbeatInterval)
|
|
97
|
+
clearInterval(this.heartbeatInterval);
|
|
98
|
+
this.options.onDisconnect?.(event.code);
|
|
99
|
+
if (this.shouldReconnect) {
|
|
100
|
+
this.log(`Disconnected (${event.code}). Reconnecting in 5s...`);
|
|
101
|
+
this.scheduleReconnect();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.ws.addEventListener('error', () => {
|
|
105
|
+
if (!this.connected) {
|
|
106
|
+
reject(new Error('WebSocket connection failed'));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.log('Connection error');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Emit an event to the platform.
|
|
116
|
+
*/
|
|
117
|
+
emit(event) {
|
|
118
|
+
if (!this.connected)
|
|
119
|
+
return;
|
|
120
|
+
this.send({ type: 'device:event', event });
|
|
121
|
+
this.options.onEvent?.(event);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Disconnect from the platform. No auto-reconnect.
|
|
125
|
+
*/
|
|
126
|
+
disconnect() {
|
|
127
|
+
this.shouldReconnect = false;
|
|
128
|
+
if (this.heartbeatInterval)
|
|
129
|
+
clearInterval(this.heartbeatInterval);
|
|
130
|
+
if (this.reconnectTimeout)
|
|
131
|
+
clearTimeout(this.reconnectTimeout);
|
|
132
|
+
if (this.ws) {
|
|
133
|
+
this.ws.close(1000, 'Device disconnecting');
|
|
134
|
+
this.ws = null;
|
|
135
|
+
}
|
|
136
|
+
this.connected = false;
|
|
137
|
+
}
|
|
138
|
+
isConnected() {
|
|
139
|
+
return this.connected;
|
|
140
|
+
}
|
|
141
|
+
getDeviceInfo() {
|
|
142
|
+
return this.deviceInfo;
|
|
143
|
+
}
|
|
144
|
+
// --- Private ---
|
|
145
|
+
send(msg) {
|
|
146
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
147
|
+
this.ws.send(JSON.stringify(msg));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async handleMessage(msg) {
|
|
151
|
+
const type = String(msg.type ?? '');
|
|
152
|
+
const requestId = String(msg.requestId ?? '');
|
|
153
|
+
if (type === 'request') {
|
|
154
|
+
const method = String(msg.method ?? '');
|
|
155
|
+
const params = msg.params ?? {};
|
|
156
|
+
const handler = this.requestHandlers.get(method);
|
|
157
|
+
if (!handler) {
|
|
158
|
+
this.send({ type: 'response', requestId, success: false, error: `Unknown method: ${method}` });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const result = await handler(method, params);
|
|
163
|
+
this.send({ type: 'response', requestId, success: true, result });
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
this.send({ type: 'response', requestId, success: false, error: err instanceof Error ? err.message : String(err) });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
scheduleReconnect() {
|
|
171
|
+
if (this.reconnectTimeout)
|
|
172
|
+
clearTimeout(this.reconnectTimeout);
|
|
173
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
174
|
+
try {
|
|
175
|
+
await this.connect();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
this.log('Reconnect failed. Retrying in 10s...');
|
|
179
|
+
this.reconnectTimeout = setTimeout(() => this.scheduleReconnect(), 10_000);
|
|
180
|
+
}
|
|
181
|
+
}, 5_000);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=device-connection.js.map
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -17,4 +17,6 @@ export { createMcpBridge } from './mcp-bridge.js';
|
|
|
17
17
|
export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions, } from './cli-session.js';
|
|
18
18
|
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
19
19
|
export { StreamJsonParser } from './streaming.js';
|
|
20
|
+
export { DeviceConnection } from './device-connection.js';
|
|
21
|
+
export type { DeviceConnectionOptions, DeviceInfo, DeviceEvent } from './device-connection.js';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/agent/index.js
CHANGED
|
@@ -19,4 +19,6 @@ export { CliSession, getOrCreateCliSession, killCliSession, killAllCliSessions,
|
|
|
19
19
|
export { buildSafeEnv, buildSafeSpawnOpts, MINIMAL_PATH, ENV_ALLOWLIST } from './env-allowlist.js';
|
|
20
20
|
// Stream parser (for custom providers)
|
|
21
21
|
export { StreamJsonParser } from './streaming.js';
|
|
22
|
+
// Device connection (mount local machine into platform Studio)
|
|
23
|
+
export { DeviceConnection } from './device-connection.js';
|
|
22
24
|
//# sourceMappingURL=index.js.map
|
|
@@ -14,6 +14,7 @@ export declare class ClaudeCliProvider implements AgentProvider {
|
|
|
14
14
|
private mcpConfigPath;
|
|
15
15
|
private spawnFn;
|
|
16
16
|
private timeout;
|
|
17
|
+
private disallowedTools;
|
|
17
18
|
constructor(options?: ClaudeCliProviderOptions);
|
|
18
19
|
stream(messages: AgentMessage[], tools: ToolDefinition[], options?: StreamOptions): AsyncGenerator<StreamEvent>;
|
|
19
20
|
}
|
|
@@ -16,12 +16,14 @@ export class ClaudeCliProvider {
|
|
|
16
16
|
mcpConfigPath;
|
|
17
17
|
spawnFn;
|
|
18
18
|
timeout;
|
|
19
|
+
disallowedTools;
|
|
19
20
|
constructor(options = {}) {
|
|
20
21
|
this.binPath = options.binPath ?? 'claude';
|
|
21
22
|
this.cwd = options.cwd ?? process.cwd();
|
|
22
23
|
this.env = options.env ?? process.env;
|
|
23
24
|
this.model = options.model;
|
|
24
25
|
this.mcpConfigPath = options.mcpConfigPath;
|
|
26
|
+
this.disallowedTools = options.disallowedTools ?? [];
|
|
25
27
|
this.spawnFn = options.spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, { ...opts, stdio: opts.stdio }));
|
|
26
28
|
this.timeout = options.timeout ?? 600_000;
|
|
27
29
|
}
|
|
@@ -53,6 +55,7 @@ export class ClaudeCliProvider {
|
|
|
53
55
|
'bypassPermissions',
|
|
54
56
|
...(systemPrompt ? ['--system-prompt', systemPrompt] : []),
|
|
55
57
|
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath, '--strict-mcp-config'] : []),
|
|
58
|
+
...(this.disallowedTools.length > 0 ? ['--disallowed-tools', this.disallowedTools.join(' ')] : []),
|
|
56
59
|
...(model ? ['--model', model] : []),
|
|
57
60
|
];
|
|
58
61
|
// Spawn the CLI process
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface ClaudeCliProviderOptions {
|
|
|
131
131
|
spawnFn?: SpawnFn;
|
|
132
132
|
/** CLI timeout in milliseconds. Defaults to 120000. */
|
|
133
133
|
timeout?: number;
|
|
134
|
+
/** Disable specific built-in tools (e.g. ['Read', 'Edit', 'Write', 'Bash'] to force MCP tools). */
|
|
135
|
+
disallowedTools?: string[];
|
|
134
136
|
}
|
|
135
137
|
export interface CliSessionOptions {
|
|
136
138
|
/** Absolute path to the claude binary. */
|
|
@@ -3,8 +3,9 @@ import { loadCredentials, saveCredentials, clearCredentials, getPlatformUrl } fr
|
|
|
3
3
|
import { PlatformClient } from '../config/platform-client.js';
|
|
4
4
|
export async function loginCommand(options) {
|
|
5
5
|
const platformUrl = options.platformUrl ?? getPlatformUrl();
|
|
6
|
+
const displayUrl = platformUrl.replace(/^https?:\/\//, '');
|
|
6
7
|
console.log('');
|
|
7
|
-
console.log(
|
|
8
|
+
console.log(` \x1b[1mFlow Weaver\x1b[0m \x1b[2m(${displayUrl})\x1b[0m`);
|
|
8
9
|
console.log('');
|
|
9
10
|
// API key mode (for CI/headless)
|
|
10
11
|
if (options.apiKey) {
|
|
@@ -42,8 +43,8 @@ async function loginWithBrowser(platformUrl) {
|
|
|
42
43
|
interval = data.interval ?? 5;
|
|
43
44
|
}
|
|
44
45
|
catch {
|
|
45
|
-
console.error(
|
|
46
|
-
console.error(' Check
|
|
46
|
+
console.error(` \x1b[31m✗\x1b[0m Cannot connect to ${platformUrl}`);
|
|
47
|
+
console.error(' Check the URL or set FW_PLATFORM_URL');
|
|
47
48
|
process.exit(1);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
@@ -65,20 +66,24 @@ async function loginWithBrowser(platformUrl) {
|
|
|
65
66
|
}
|
|
66
67
|
console.log('');
|
|
67
68
|
// Step 3: Poll for completion
|
|
68
|
-
process.
|
|
69
|
+
process.stderr.write(' Waiting for approval');
|
|
69
70
|
let cancelled = false;
|
|
70
71
|
const sigHandler = () => { cancelled = true; };
|
|
71
72
|
process.on('SIGINT', sigHandler);
|
|
72
73
|
const maxAttempts = 120; // 10 minutes at 5s intervals
|
|
74
|
+
let networkErrors = 0;
|
|
73
75
|
for (let i = 0; i < maxAttempts && !cancelled; i++) {
|
|
74
76
|
await new Promise(r => setTimeout(r, interval * 1000));
|
|
75
77
|
try {
|
|
76
78
|
const resp = await fetch(`${platformUrl}/auth/device/poll?deviceCode=${deviceCode}`);
|
|
77
|
-
|
|
79
|
+
networkErrors = 0; // reset on successful connection
|
|
80
|
+
if (!resp.ok) {
|
|
81
|
+
process.stderr.write('.');
|
|
78
82
|
continue;
|
|
83
|
+
}
|
|
79
84
|
const data = await resp.json();
|
|
80
85
|
if (data.status === 'approved' && data.token && data.user) {
|
|
81
|
-
process.
|
|
86
|
+
process.stderr.write(' \x1b[32m✓\x1b[0m\n\n');
|
|
82
87
|
saveCredentials({
|
|
83
88
|
token: data.token,
|
|
84
89
|
email: data.user.email,
|
|
@@ -95,7 +100,7 @@ async function loginWithBrowser(platformUrl) {
|
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
102
|
if (data.status === 'expired') {
|
|
98
|
-
process.
|
|
103
|
+
process.stderr.write(' \x1b[31mtimed out\x1b[0m\n\n');
|
|
99
104
|
console.log(' Code expired. Run \x1b[36mfw login\x1b[0m again.');
|
|
100
105
|
console.log('');
|
|
101
106
|
process.removeListener('SIGINT', sigHandler);
|
|
@@ -103,27 +108,30 @@ async function loginWithBrowser(platformUrl) {
|
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
if (data.status === 'denied') {
|
|
106
|
-
process.
|
|
111
|
+
process.stderr.write(' \x1b[31mdenied\x1b[0m\n\n');
|
|
107
112
|
console.log(' Access denied.');
|
|
108
113
|
console.log('');
|
|
109
114
|
process.removeListener('SIGINT', sigHandler);
|
|
110
115
|
process.exit(1);
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
|
-
// Still pending — show a dot
|
|
114
|
-
|
|
115
|
-
process.stdout.write('.');
|
|
118
|
+
// Still pending — show a dot every poll
|
|
119
|
+
process.stderr.write('.');
|
|
116
120
|
}
|
|
117
121
|
catch {
|
|
118
|
-
|
|
122
|
+
networkErrors++;
|
|
123
|
+
if (networkErrors >= 3) {
|
|
124
|
+
process.stderr.write(' \x1b[33m!\x1b[0m');
|
|
125
|
+
networkErrors = 0;
|
|
126
|
+
}
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
process.removeListener('SIGINT', sigHandler);
|
|
122
130
|
if (cancelled) {
|
|
123
|
-
process.
|
|
131
|
+
process.stderr.write(' \x1b[33mcancelled\x1b[0m\n\n');
|
|
124
132
|
}
|
|
125
133
|
else {
|
|
126
|
-
process.
|
|
134
|
+
process.stderr.write(' \x1b[31mtimed out\x1b[0m\n\n');
|
|
127
135
|
console.log(' Authentication timed out. Run \x1b[36mfw login\x1b[0m again.');
|
|
128
136
|
console.log('');
|
|
129
137
|
}
|
|
@@ -209,23 +217,24 @@ export async function authStatusCommand() {
|
|
|
209
217
|
console.log('');
|
|
210
218
|
}
|
|
211
219
|
function prompt(message, hidden = false) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Hide password input
|
|
220
|
+
if (hidden && process.stdin.isTTY) {
|
|
221
|
+
// Raw-mode password input — no readline (it competes for stdin)
|
|
222
|
+
return new Promise((resolve) => {
|
|
216
223
|
process.stderr.write(message);
|
|
217
224
|
process.stdin.setRawMode(true);
|
|
225
|
+
process.stdin.resume();
|
|
218
226
|
let input = '';
|
|
219
227
|
const handler = (key) => {
|
|
220
228
|
const ch = key.toString();
|
|
221
229
|
if (ch === '\r' || ch === '\n') {
|
|
222
230
|
process.stdin.setRawMode(false);
|
|
231
|
+
process.stdin.pause();
|
|
223
232
|
process.stdin.removeListener('data', handler);
|
|
224
233
|
process.stderr.write('\n');
|
|
225
|
-
rl.close();
|
|
226
234
|
resolve(input);
|
|
227
235
|
}
|
|
228
236
|
else if (ch === '\x03') { // Ctrl+C
|
|
237
|
+
process.stdin.setRawMode(false);
|
|
229
238
|
process.exit(1);
|
|
230
239
|
}
|
|
231
240
|
else if (ch === '\x7f') { // Backspace
|
|
@@ -236,10 +245,11 @@ function prompt(message, hidden = false) {
|
|
|
236
245
|
}
|
|
237
246
|
};
|
|
238
247
|
process.stdin.on('data', handler);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
252
|
+
rl.question(message, (answer) => { rl.close(); resolve(answer); });
|
|
243
253
|
});
|
|
244
254
|
}
|
|
245
255
|
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DeviceConnection } from '../../agent/device-connection.js';
|
|
2
|
+
export declare function loadPackDeviceHandlers(conn: DeviceConnection, projectDir: string): Promise<string[]>;
|
|
3
|
+
export declare function handleConnect(projectDir: string): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import { DeviceConnection } from '../../agent/device-connection.js';
|
|
6
|
+
import { discoverDeviceHandlers } from '../../marketplace/registry.js';
|
|
7
|
+
function promptYesNo(message) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
10
|
+
rl.question(message, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
const normalized = answer.trim().toLowerCase();
|
|
13
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function loadPackDeviceHandlers(conn, projectDir) {
|
|
18
|
+
const loadedPacks = [];
|
|
19
|
+
try {
|
|
20
|
+
const handlers = await discoverDeviceHandlers(projectDir);
|
|
21
|
+
for (const handler of handlers) {
|
|
22
|
+
try {
|
|
23
|
+
const mod = await import(handler.entrypoint);
|
|
24
|
+
if (typeof mod.register === 'function') {
|
|
25
|
+
await mod.register(conn, { projectDir });
|
|
26
|
+
loadedPacks.push(handler.packageName);
|
|
27
|
+
process.stderr.write(` \x1b[2m+ ${handler.packageName} handlers\x1b[0m\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
process.stderr.write(` \x1b[33m⚠\x1b[0m Failed to load handlers from ${handler.packageName}: ${err instanceof Error ? err.message : err}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Discovery failed — non-fatal
|
|
37
|
+
}
|
|
38
|
+
return loadedPacks;
|
|
39
|
+
}
|
|
40
|
+
export async function handleConnect(projectDir) {
|
|
41
|
+
// Load credentials
|
|
42
|
+
const credPath = path.join(os.homedir(), '.fw', 'credentials.json');
|
|
43
|
+
if (!fs.existsSync(credPath)) {
|
|
44
|
+
console.error('\n Not logged in. Run: fw login\n');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const creds = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
48
|
+
if (!creds.token || !creds.platformUrl || creds.expiresAt < Date.now()) {
|
|
49
|
+
console.error('\n Credentials expired. Run: fw login\n');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const conn = new DeviceConnection({
|
|
53
|
+
platformUrl: creds.platformUrl,
|
|
54
|
+
token: creds.token,
|
|
55
|
+
projectDir,
|
|
56
|
+
deviceName: path.basename(projectDir),
|
|
57
|
+
logger: (msg) => process.stderr.write(` \x1b[2m${msg}\x1b[0m\n`),
|
|
58
|
+
});
|
|
59
|
+
// Register basic file handlers (any project can use these)
|
|
60
|
+
conn.addCapability('file_read');
|
|
61
|
+
conn.addCapability('file_list');
|
|
62
|
+
conn.onRequest('file:read', async (_method, params) => {
|
|
63
|
+
const filePath = path.resolve(projectDir, String(params.path ?? ''));
|
|
64
|
+
if (!filePath.startsWith(projectDir))
|
|
65
|
+
throw new Error('Path outside project directory');
|
|
66
|
+
if (!fs.existsSync(filePath))
|
|
67
|
+
throw new Error('File not found');
|
|
68
|
+
const stat = fs.statSync(filePath);
|
|
69
|
+
if (stat.isDirectory())
|
|
70
|
+
return { type: 'directory', entries: fs.readdirSync(filePath) };
|
|
71
|
+
if (stat.size > 1_048_576)
|
|
72
|
+
throw new Error('File too large (>1MB)');
|
|
73
|
+
return { type: 'file', content: fs.readFileSync(filePath, 'utf-8') };
|
|
74
|
+
});
|
|
75
|
+
conn.onRequest('file:list', async (_method, params) => {
|
|
76
|
+
const dirPath = path.resolve(projectDir, String(params.path ?? '.'));
|
|
77
|
+
if (!dirPath.startsWith(projectDir))
|
|
78
|
+
throw new Error('Path outside project directory');
|
|
79
|
+
if (!fs.existsSync(dirPath))
|
|
80
|
+
throw new Error('Directory not found');
|
|
81
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
82
|
+
return entries
|
|
83
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
|
|
84
|
+
.map(e => ({ name: e.name, type: e.isDirectory() ? 'directory' : 'file', path: path.relative(projectDir, path.join(dirPath, e.name)), hasUnfetchedChildren: e.isDirectory() }));
|
|
85
|
+
});
|
|
86
|
+
// Load pack device handlers (if any installed packs provide them)
|
|
87
|
+
const loadedPacks = await loadPackDeviceHandlers(conn, projectDir);
|
|
88
|
+
// Tell the platform which packs contributed handlers (for auto-install)
|
|
89
|
+
if (loadedPacks.length > 0) {
|
|
90
|
+
conn.setPacks(loadedPacks);
|
|
91
|
+
}
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(' \x1b[1mflow-weaver connect\x1b[0m');
|
|
94
|
+
console.log(` \x1b[2mProject: ${path.basename(projectDir)}\x1b[0m`);
|
|
95
|
+
console.log(` \x1b[2mPlatform: ${creds.platformUrl}\x1b[0m`);
|
|
96
|
+
console.log('');
|
|
97
|
+
// Check if packs need to be installed in the Studio workspace
|
|
98
|
+
if (loadedPacks.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
const checkRes = await fetch(`${creds.platformUrl}/devices/check-packs`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${creds.token}` },
|
|
103
|
+
body: JSON.stringify({ packs: loadedPacks }),
|
|
104
|
+
});
|
|
105
|
+
if (checkRes.ok) {
|
|
106
|
+
const { missing } = await checkRes.json();
|
|
107
|
+
if (missing.length > 0) {
|
|
108
|
+
console.log(' The following packs need to be installed in Studio:');
|
|
109
|
+
for (const p of missing) {
|
|
110
|
+
console.log(` \x1b[36m${p}\x1b[0m`);
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
const answer = await promptYesNo(' Install now? (Y/n) ');
|
|
114
|
+
if (answer) {
|
|
115
|
+
process.stderr.write(' Installing...');
|
|
116
|
+
const installRes = await fetch(`${creds.platformUrl}/devices/install-packs`, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${creds.token}` },
|
|
119
|
+
body: JSON.stringify({ packs: missing }),
|
|
120
|
+
});
|
|
121
|
+
if (installRes.ok) {
|
|
122
|
+
const { results } = await installRes.json();
|
|
123
|
+
const allOk = results.every(r => r.ok);
|
|
124
|
+
if (allOk) {
|
|
125
|
+
process.stderr.write(' \x1b[32m✓\x1b[0m\n\n');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
process.stderr.write(' \x1b[33mpartial\x1b[0m\n');
|
|
129
|
+
for (const r of results) {
|
|
130
|
+
if (!r.ok)
|
|
131
|
+
console.log(` \x1b[31m✗\x1b[0m ${r.pack}: ${r.error}`);
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
process.stderr.write(' \x1b[31mfailed\x1b[0m\n\n');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
console.log(' \x1b[2mSkipped. Install manually via Studio marketplace.\x1b[0m\n');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Check failed — non-fatal, continue connecting
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
await conn.connect();
|
|
152
|
+
console.log(' \x1b[2mPress Ctrl+C to disconnect.\x1b[0m\n');
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
process.on('SIGINT', () => { console.log('\n \x1b[2mDisconnecting...\x1b[0m'); conn.disconnect(); resolve(); });
|
|
155
|
+
process.on('SIGTERM', () => { conn.disconnect(); resolve(); });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.error(` \x1b[31m✗\x1b[0m Connection failed: ${err instanceof Error ? err.message : err}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=connect.js.map
|