@flowdevcli/flowdev 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/core/cli.js +1 -1
- package/src/utils/config-manager.js +59 -0
- package/src/utils/engine-check.js +73 -35
package/package.json
CHANGED
package/src/core/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ import { generateCommand } from '../commands/scaffold/generate.js';
|
|
|
34
34
|
import { auditCommand } from '../commands/ai/audit.js';
|
|
35
35
|
import { testCommand } from '../commands/ai/test.js';
|
|
36
36
|
import { updateCommand } from '../commands/system/update.js';
|
|
37
|
-
import { configCommand } from '
|
|
37
|
+
import { configCommand } from '../commands/config.js'
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
const require = createRequire(import.meta.url);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview FlowDev - Intelligent CLI tool
|
|
3
|
+
* @module flowdev
|
|
4
|
+
* @version 1.0.5
|
|
5
|
+
*
|
|
6
|
+
* @license MIT
|
|
7
|
+
* Copyright (c) 2026 FlowDev Technologies.
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions:
|
|
15
|
+
*
|
|
16
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
* copies or substantial portions of the Software.
|
|
18
|
+
*
|
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import Conf from 'conf';
|
|
25
|
+
const config = new Conf({
|
|
26
|
+
projectName: 'flowdev-cli',
|
|
27
|
+
projectSuffix: ''
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const CONFIG_KEYS = {
|
|
32
|
+
DEEPSEEK_KEY: 'api_keys.deepseek',
|
|
33
|
+
DEFAULT_PROVIDER: 'preferences.default_provider'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @returns {string|undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function getDeepSeekKey() {
|
|
41
|
+
return config.get(CONFIG_KEYS.DEEPSEEK_KEY);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param {string} key
|
|
47
|
+
*/
|
|
48
|
+
export function setDeepSeekKey(key) {
|
|
49
|
+
if (!key) return;
|
|
50
|
+
config.set(CONFIG_KEYS.DEEPSEEK_KEY, key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearDeepSeekKey() {
|
|
54
|
+
config.delete(CONFIG_KEYS.DEEPSEEK_KEY);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getAllConfig() {
|
|
58
|
+
return config.store;
|
|
59
|
+
}
|
|
@@ -3,8 +3,8 @@ import chalk from 'chalk';
|
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { exec, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
|
-
import { logger } from '
|
|
7
|
-
import { getDeepSeekKey } from '
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { getDeepSeekKey } from '../utils/config-manager.js';
|
|
8
8
|
|
|
9
9
|
const execAsync = promisify(exec);
|
|
10
10
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -24,24 +24,40 @@ async function waitForOllamaServer({ retries = 15, delayMs = 1000 } = {}) {
|
|
|
24
24
|
|
|
25
25
|
async function installOllamaEngine(spinner) {
|
|
26
26
|
const isWindows = process.platform === 'win32';
|
|
27
|
+
|
|
28
|
+
if (spinner) spinner.stop();
|
|
29
|
+
|
|
30
|
+
console.log(chalk.yellow('\n Ollama engine is missing.'));
|
|
31
|
+
console.log(chalk.dim('FlowDev requires Ollama to run local models without API keys.'));
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const { confirm } = await inquirer.prompt([{
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'confirm',
|
|
37
|
+
message: 'Do you want to install Ollama automatically now?',
|
|
38
|
+
default: true
|
|
39
|
+
}]);
|
|
40
|
+
if (!confirm) throw new Error("Ollama installation aborted by user.");
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if (spinner) spinner.start(chalk.yellow("Installing local neural components... (Please wait)"));
|
|
44
|
+
|
|
27
45
|
const installCmd = isWindows
|
|
28
46
|
? 'winget install Ollama.Ollama --silent --accept-source-agreements'
|
|
29
47
|
: 'curl -fsSL https://ollama.com/install.sh | sh';
|
|
30
48
|
|
|
31
|
-
spinner.text = chalk.yellow("Installing local neural components... (Please wait)");
|
|
32
|
-
|
|
33
49
|
try {
|
|
34
50
|
await execAsync(installCmd, { maxBuffer: 10 * 1024 * 1024 });
|
|
35
|
-
spinner.succeed(chalk.green('Local engine installed.'));
|
|
51
|
+
if (spinner) spinner.succeed(chalk.green('Local engine installed.'));
|
|
36
52
|
await sleep(1500);
|
|
37
53
|
} catch (error) {
|
|
38
|
-
spinner.fail(chalk.red("Automatic installation failed."));
|
|
39
|
-
logger.error(`Please install Ollama manually
|
|
54
|
+
if (spinner) spinner.fail(chalk.red("Automatic installation failed."));
|
|
55
|
+
logger.error(`Please install Ollama manually from https://ollama.com`);
|
|
40
56
|
throw error;
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
async function ensureOllamaReady(spinner, modelName) {
|
|
60
|
+
export async function ensureOllamaReady(spinner, modelName) {
|
|
45
61
|
try {
|
|
46
62
|
await execAsync('ollama --version');
|
|
47
63
|
} catch (e) {
|
|
@@ -49,7 +65,7 @@ async function ensureOllamaReady(spinner, modelName) {
|
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
if (!(await waitForOllamaServer({ retries: 3, delayMs: 1000 }))) {
|
|
52
|
-
spinner.text = chalk.blue('Starting local inference service...');
|
|
68
|
+
if (spinner) spinner.text = chalk.blue('Starting local inference service...');
|
|
53
69
|
const child = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' });
|
|
54
70
|
child.unref();
|
|
55
71
|
|
|
@@ -64,17 +80,22 @@ async function ensureOllamaReady(spinner, modelName) {
|
|
|
64
80
|
} catch (err) {}
|
|
65
81
|
|
|
66
82
|
if (!hasModel) {
|
|
67
|
-
spinner.text = chalk.magenta(`Acquiring the model ${modelName} (size: ~4GB)
|
|
83
|
+
if (spinner) spinner.text = chalk.magenta(`Acquiring the model ${modelName} (size: ~4GB)... This happens only once.`);
|
|
68
84
|
await ollama.pull({ model: modelName });
|
|
69
|
-
spinner.succeed(chalk.green('Local engine ready.'));
|
|
70
|
-
spinner.start();
|
|
85
|
+
if (spinner) spinner.succeed(chalk.green('Local engine ready.'));
|
|
86
|
+
if (spinner) spinner.start();
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
|
|
91
|
+
|
|
75
92
|
async function* streamDeepSeek(messages) {
|
|
76
93
|
const apiKey = getDeepSeekKey();
|
|
77
94
|
|
|
95
|
+
if (!apiKey) {
|
|
96
|
+
throw new Error("API Key missing. Please run 'flowdev config' to set it up.");
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
const response = await fetch('https://api.deepseek.com/chat/completions', {
|
|
79
100
|
method: 'POST',
|
|
80
101
|
headers: {
|
|
@@ -89,8 +110,13 @@ async function* streamDeepSeek(messages) {
|
|
|
89
110
|
});
|
|
90
111
|
|
|
91
112
|
if (!response.ok) {
|
|
92
|
-
const
|
|
93
|
-
|
|
113
|
+
const errText = await response.text();
|
|
114
|
+
if (response.status === 401) {
|
|
115
|
+
throw new Error(`Invalid API Key. Please run 'flowdev config' to update it.`);
|
|
116
|
+
} else if (response.status === 402) {
|
|
117
|
+
throw new Error(`Insufficient Balance on DeepSeek account.`);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`DeepSeek API Error: ${response.status} - ${errText}`);
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
const reader = response.body.getReader();
|
|
@@ -100,30 +126,39 @@ async function* streamDeepSeek(messages) {
|
|
|
100
126
|
while (true) {
|
|
101
127
|
const { done, value } = await reader.read();
|
|
102
128
|
if (done) break;
|
|
129
|
+
|
|
103
130
|
buffer += decoder.decode(value, { stream: true });
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
|
|
132
|
+
let boundary = buffer.indexOf('\n');
|
|
133
|
+
while (boundary !== -1) {
|
|
134
|
+
const line = buffer.slice(0, boundary).trim();
|
|
135
|
+
buffer = buffer.slice(boundary + 1);
|
|
136
|
+
|
|
137
|
+
if (line.startsWith("data: ")) {
|
|
138
|
+
const jsonStr = line.slice(6);
|
|
139
|
+
if (jsonStr === "[DONE]") continue;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const json = JSON.parse(jsonStr);
|
|
143
|
+
const content = json.choices[0]?.delta?.content;
|
|
144
|
+
if (content) yield { message: { content: content } };
|
|
145
|
+
} catch (e) {
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
boundary = buffer.indexOf('\n');
|
|
117
150
|
}
|
|
118
151
|
}
|
|
119
152
|
}
|
|
120
153
|
|
|
121
154
|
|
|
155
|
+
|
|
122
156
|
export async function getAIResponse(messages, spinner, forceModel = null) {
|
|
123
157
|
if (spinner) spinner.stop();
|
|
124
158
|
|
|
125
159
|
let provider = forceModel;
|
|
126
160
|
|
|
161
|
+
|
|
127
162
|
if (!provider) {
|
|
128
163
|
const hasKey = !!getDeepSeekKey();
|
|
129
164
|
|
|
@@ -134,12 +169,12 @@ export async function getAIResponse(messages, spinner, forceModel = null) {
|
|
|
134
169
|
message: 'Select AI Model:',
|
|
135
170
|
choices: [
|
|
136
171
|
{
|
|
137
|
-
name: '
|
|
172
|
+
name: 'Llama 3 (Local - Private, Free, requires RAM)',
|
|
138
173
|
value: 'llama3',
|
|
139
174
|
short: 'Llama 3 (Local)'
|
|
140
175
|
},
|
|
141
176
|
{
|
|
142
|
-
name: `DeepSeek V3 (Cloud - Fast)`,
|
|
177
|
+
name: ` DeepSeek V3 (Cloud - Fast, requires Key)`,
|
|
143
178
|
value: 'deepseek',
|
|
144
179
|
disabled: !hasKey ? 'run "flowdev config" first' : false,
|
|
145
180
|
short: 'DeepSeek (Cloud)'
|
|
@@ -153,10 +188,10 @@ export async function getAIResponse(messages, spinner, forceModel = null) {
|
|
|
153
188
|
if (spinner) spinner.start();
|
|
154
189
|
|
|
155
190
|
if (provider === 'llama3') {
|
|
156
|
-
if (spinner) spinner.text = chalk.cyan('
|
|
191
|
+
if (spinner) spinner.text = chalk.cyan('Checking local neural engine...');
|
|
157
192
|
await ensureOllamaReady(spinner, 'llama3');
|
|
158
193
|
|
|
159
|
-
if (spinner) spinner.text = chalk.magenta('Thinking...');
|
|
194
|
+
if (spinner) spinner.text = chalk.magenta('Thinking (Local)...');
|
|
160
195
|
return await ollama.chat({
|
|
161
196
|
model: 'llama3',
|
|
162
197
|
messages: messages,
|
|
@@ -164,16 +199,19 @@ export async function getAIResponse(messages, spinner, forceModel = null) {
|
|
|
164
199
|
});
|
|
165
200
|
}
|
|
166
201
|
else if (provider === 'deepseek') {
|
|
167
|
-
if (spinner) spinner.text = chalk.cyan('
|
|
202
|
+
if (spinner) spinner.text = chalk.cyan('Handshaking with DeepSeek server...');
|
|
203
|
+
|
|
204
|
+
|
|
168
205
|
try {
|
|
169
|
-
await fetch('https://www.google.com', { method: 'HEAD' });
|
|
206
|
+
await fetch('https://www.google.com', { method: 'HEAD', signal: AbortSignal.timeout(3000) });
|
|
170
207
|
} catch (e) {
|
|
171
|
-
throw new Error("
|
|
208
|
+
throw new Error("No internet connection detected for Cloud AI.");
|
|
172
209
|
}
|
|
173
210
|
|
|
174
|
-
if (spinner) spinner.text = chalk.magenta('
|
|
211
|
+
if (spinner) spinner.text = chalk.magenta('Thinking (Cloud)...');
|
|
175
212
|
return streamDeepSeek(messages);
|
|
176
213
|
}
|
|
177
214
|
}
|
|
178
215
|
|
|
216
|
+
|
|
179
217
|
export { ensureOllamaReady as ensureEngineReady };
|