@juspay/neurolink 11.22.0 → 11.22.1
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 +3 -3
- package/dist/cli/commands/ollama.js +55 -10
- package/dist/cli/factories/ollamaCommandFactory.d.ts +11 -3
- package/dist/cli/factories/ollamaCommandFactory.js +67 -20
- package/dist/cli/utils/ollamaUtils.d.ts +9 -0
- package/dist/cli/utils/ollamaUtils.js +66 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
## [11.22.
|
|
1
|
+
## [11.22.1](https://github.com/juspay/neurolink/compare/v11.22.0...v11.22.1) (2026-08-23)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
- **(cli):**
|
|
5
|
+
- **(cli):** bound the synchronous ollama and scanner subprocesses ([51109e3](https://github.com/juspay/neurolink/commit/51109e3eeb3a6665eb840cc7a3aa561bb115d4b7))
|
|
6
6
|
|
|
7
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
8
|
|
|
@@ -29,10 +29,16 @@ export function addOllamaCommands(cli) {
|
|
|
29
29
|
.demandCommand(1, "Please specify a command");
|
|
30
30
|
}, () => { });
|
|
31
31
|
}
|
|
32
|
+
/** See the note on the identically-named constant in ../utils/ollamaUtils.ts. */
|
|
33
|
+
const OLLAMA_QUERY_TIMEOUT_MS = 15_000;
|
|
32
34
|
async function listModelsHandler() {
|
|
33
35
|
const spinner = ora("Fetching installed models...").start();
|
|
34
36
|
try {
|
|
35
|
-
const res = spawnSync("ollama", ["list"], {
|
|
37
|
+
const res = spawnSync("ollama", ["list"], {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
40
|
+
killSignal: "SIGKILL",
|
|
41
|
+
});
|
|
36
42
|
if (res.error) {
|
|
37
43
|
throw res.error;
|
|
38
44
|
}
|
|
@@ -58,6 +64,9 @@ async function pullModelHandler(argv) {
|
|
|
58
64
|
logger.always(chalk.blue(`Downloading model: ${model}`));
|
|
59
65
|
logger.always(chalk.gray("This may take several minutes..."));
|
|
60
66
|
try {
|
|
67
|
+
// Deliberately unbounded: a model pull is hundreds of megabytes and
|
|
68
|
+
// legitimately runs for many minutes. It also inherits stdio, so the user
|
|
69
|
+
// sees progress and can Ctrl-C — the two things a wedged query lacks.
|
|
61
70
|
const res = spawnSync("ollama", ["pull", model], { stdio: "inherit" });
|
|
62
71
|
if (res.error) {
|
|
63
72
|
throw res.error;
|
|
@@ -91,7 +100,11 @@ async function removeModelHandler(argv) {
|
|
|
91
100
|
}
|
|
92
101
|
const spinner = ora(`Removing model ${model}...`).start();
|
|
93
102
|
try {
|
|
94
|
-
const res = spawnSync("ollama", ["rm", model], {
|
|
103
|
+
const res = spawnSync("ollama", ["rm", model], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
106
|
+
killSignal: "SIGKILL",
|
|
107
|
+
});
|
|
95
108
|
if (res.error) {
|
|
96
109
|
throw res.error;
|
|
97
110
|
}
|
|
@@ -110,7 +123,11 @@ async function removeModelHandler(argv) {
|
|
|
110
123
|
async function statusHandler() {
|
|
111
124
|
const spinner = ora("Checking Ollama service status...").start();
|
|
112
125
|
try {
|
|
113
|
-
const res = spawnSync("ollama", ["list"], {
|
|
126
|
+
const res = spawnSync("ollama", ["list"], {
|
|
127
|
+
encoding: "utf8",
|
|
128
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
129
|
+
killSignal: "SIGKILL",
|
|
130
|
+
});
|
|
114
131
|
if (res.error) {
|
|
115
132
|
throw res.error;
|
|
116
133
|
}
|
|
@@ -134,22 +151,42 @@ async function stopHandler() {
|
|
|
134
151
|
try {
|
|
135
152
|
if (process.platform === "darwin") {
|
|
136
153
|
try {
|
|
137
|
-
spawnSync("pkill", ["ollama"], {
|
|
154
|
+
spawnSync("pkill", ["ollama"], {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
157
|
+
killSignal: "SIGKILL",
|
|
158
|
+
});
|
|
138
159
|
}
|
|
139
160
|
catch {
|
|
140
|
-
spawnSync("killall", ["Ollama"], {
|
|
161
|
+
spawnSync("killall", ["Ollama"], {
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
164
|
+
killSignal: "SIGKILL",
|
|
165
|
+
});
|
|
141
166
|
}
|
|
142
167
|
}
|
|
143
168
|
else if (process.platform === "linux") {
|
|
144
169
|
try {
|
|
145
|
-
spawnSync("systemctl", ["stop", "ollama"], {
|
|
170
|
+
spawnSync("systemctl", ["stop", "ollama"], {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
173
|
+
killSignal: "SIGKILL",
|
|
174
|
+
});
|
|
146
175
|
}
|
|
147
176
|
catch {
|
|
148
|
-
spawnSync("pkill", ["ollama"], {
|
|
177
|
+
spawnSync("pkill", ["ollama"], {
|
|
178
|
+
encoding: "utf8",
|
|
179
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
180
|
+
killSignal: "SIGKILL",
|
|
181
|
+
});
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
else {
|
|
152
|
-
spawnSync("taskkill", ["/F", "/IM", "ollama.exe"], {
|
|
185
|
+
spawnSync("taskkill", ["/F", "/IM", "ollama.exe"], {
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
188
|
+
killSignal: "SIGKILL",
|
|
189
|
+
});
|
|
153
190
|
}
|
|
154
191
|
spinner.succeed("Ollama service stopped");
|
|
155
192
|
}
|
|
@@ -165,7 +202,11 @@ async function setupHandler() {
|
|
|
165
202
|
const checkSpinner = ora("Checking Ollama installation...").start();
|
|
166
203
|
let isInstalled = false;
|
|
167
204
|
try {
|
|
168
|
-
spawnSync("ollama", ["--version"], {
|
|
205
|
+
spawnSync("ollama", ["--version"], {
|
|
206
|
+
encoding: "utf8",
|
|
207
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
208
|
+
killSignal: "SIGKILL",
|
|
209
|
+
});
|
|
169
210
|
isInstalled = true;
|
|
170
211
|
checkSpinner.succeed("Ollama is installed");
|
|
171
212
|
}
|
|
@@ -204,7 +245,11 @@ async function setupHandler() {
|
|
|
204
245
|
// Check if service is running
|
|
205
246
|
let serviceRunning = false;
|
|
206
247
|
try {
|
|
207
|
-
spawnSync("ollama", ["list"], {
|
|
248
|
+
spawnSync("ollama", ["list"], {
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
251
|
+
killSignal: "SIGKILL",
|
|
252
|
+
});
|
|
208
253
|
serviceRunning = true;
|
|
209
254
|
logger.always(chalk.green("\n✅ Ollama service is running"));
|
|
210
255
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs";
|
|
2
|
-
/**
|
|
3
|
-
* Factory for creating Ollama CLI commands using the Factory Pattern
|
|
4
|
-
*/
|
|
5
2
|
export declare class OllamaCommandFactory {
|
|
6
3
|
/**
|
|
7
4
|
* Secure wrapper around spawnSync to prevent command injection.
|
|
@@ -10,6 +7,17 @@ export declare class OllamaCommandFactory {
|
|
|
10
7
|
/**
|
|
11
8
|
* Create the Ollama command group
|
|
12
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Every handler is registered `.bind(this)`.
|
|
12
|
+
*
|
|
13
|
+
* yargs invokes the handler as a plain function, so a bare `this.xHandler`
|
|
14
|
+
* reference arrives with `this` unset and the first `this.safeSpawn(...)`
|
|
15
|
+
* throws `TypeError: this.safeSpawn is not a function`. That made EVERY
|
|
16
|
+
* subcommand here fail on its first line — list-models, pull, remove, status
|
|
17
|
+
* and stop all reported their generic "Failed to ..." message regardless of
|
|
18
|
+
* whether Ollama was installed or running, which is exactly why the real
|
|
19
|
+
* cause stayed hidden.
|
|
20
|
+
*/
|
|
13
21
|
static createOllamaCommands(): CommandModule;
|
|
14
22
|
/**
|
|
15
23
|
* Handler for listing installed models
|
|
@@ -9,6 +9,17 @@ import { AIProviderName } from "../../types/index.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* Factory for creating Ollama CLI commands using the Factory Pattern
|
|
11
11
|
*/
|
|
12
|
+
/** See the note on the identically-named constant in cli/utils/ollamaUtils.ts. */
|
|
13
|
+
const OLLAMA_QUERY_TIMEOUT_MS = 15_000;
|
|
14
|
+
/**
|
|
15
|
+
* Explicit opt-out from the query bound, for commands that are long by design.
|
|
16
|
+
*
|
|
17
|
+
* `spawnSync` treats 0 as "no timeout" (verified: a 3s child under
|
|
18
|
+
* `timeout: 0` runs to completion with no error, while `timeout: 1000` returns
|
|
19
|
+
* at 1.0s with ETIMEDOUT). Named rather than written as a bare 0 at the call
|
|
20
|
+
* site, because `timeout: 0` reads like "expire immediately".
|
|
21
|
+
*/
|
|
22
|
+
const OLLAMA_NO_TIMEOUT = 0;
|
|
12
23
|
export class OllamaCommandFactory {
|
|
13
24
|
/**
|
|
14
25
|
* Secure wrapper around spawnSync to prevent command injection.
|
|
@@ -16,6 +27,12 @@ export class OllamaCommandFactory {
|
|
|
16
27
|
static safeSpawn(command, args, options = {}) {
|
|
17
28
|
// Command validation is now handled by TypeScript with AllowedCommand type
|
|
18
29
|
const defaultOptions = {
|
|
30
|
+
// spawnSync blocks the event loop, so an unresponsive daemon hangs the
|
|
31
|
+
// whole CLI with no output and no error. SIGKILL because spawnSync's
|
|
32
|
+
// timeout otherwise sends SIGTERM and keeps waiting. Callers with
|
|
33
|
+
// legitimately long commands override via `options`.
|
|
34
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
35
|
+
killSignal: "SIGKILL",
|
|
19
36
|
...options,
|
|
20
37
|
encoding: "utf8", // Always enforce utf8 encoding
|
|
21
38
|
};
|
|
@@ -24,31 +41,42 @@ export class OllamaCommandFactory {
|
|
|
24
41
|
/**
|
|
25
42
|
* Create the Ollama command group
|
|
26
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* Every handler is registered `.bind(this)`.
|
|
46
|
+
*
|
|
47
|
+
* yargs invokes the handler as a plain function, so a bare `this.xHandler`
|
|
48
|
+
* reference arrives with `this` unset and the first `this.safeSpawn(...)`
|
|
49
|
+
* throws `TypeError: this.safeSpawn is not a function`. That made EVERY
|
|
50
|
+
* subcommand here fail on its first line — list-models, pull, remove, status
|
|
51
|
+
* and stop all reported their generic "Failed to ..." message regardless of
|
|
52
|
+
* whether Ollama was installed or running, which is exactly why the real
|
|
53
|
+
* cause stayed hidden.
|
|
54
|
+
*/
|
|
27
55
|
static createOllamaCommands() {
|
|
28
56
|
return {
|
|
29
57
|
command: "ollama <command>",
|
|
30
58
|
describe: "Manage Ollama local AI models",
|
|
31
59
|
builder: (yargs) => {
|
|
32
60
|
return yargs
|
|
33
|
-
.command("list-models", "List installed Ollama models", {}, this.listModelsHandler)
|
|
61
|
+
.command("list-models", "List installed Ollama models", {}, this.listModelsHandler.bind(this))
|
|
34
62
|
.command("pull <model>", "Download an Ollama model", {
|
|
35
63
|
model: {
|
|
36
64
|
describe: "Model name to download",
|
|
37
65
|
type: "string",
|
|
38
66
|
demandOption: true,
|
|
39
67
|
},
|
|
40
|
-
}, this.pullModelHandler)
|
|
68
|
+
}, this.pullModelHandler.bind(this))
|
|
41
69
|
.command("remove <model>", "Remove an Ollama model", {
|
|
42
70
|
model: {
|
|
43
71
|
describe: "Model name to remove",
|
|
44
72
|
type: "string",
|
|
45
73
|
demandOption: true,
|
|
46
74
|
},
|
|
47
|
-
}, this.removeModelHandler)
|
|
48
|
-
.command("status", "Check Ollama service status", {}, this.statusHandler)
|
|
49
|
-
.command("start", "Start Ollama service", {}, this.startHandler)
|
|
50
|
-
.command("stop", "Stop Ollama service", {}, this.stopHandler)
|
|
51
|
-
.command("setup", "Interactive Ollama setup", {}, this.setupHandler)
|
|
75
|
+
}, this.removeModelHandler.bind(this))
|
|
76
|
+
.command("status", "Check Ollama service status", {}, this.statusHandler.bind(this))
|
|
77
|
+
.command("start", "Start Ollama service", {}, this.startHandler.bind(this))
|
|
78
|
+
.command("stop", "Stop Ollama service", {}, this.stopHandler.bind(this))
|
|
79
|
+
.command("setup", "Interactive Ollama setup", {}, this.setupHandler.bind(this))
|
|
52
80
|
.demandCommand(1, "Please specify a command");
|
|
53
81
|
},
|
|
54
82
|
handler: () => { }, // No-op handler as subcommands handle everything
|
|
@@ -89,8 +117,16 @@ export class OllamaCommandFactory {
|
|
|
89
117
|
logger.always(chalk.blue(`Downloading model: ${model}`));
|
|
90
118
|
logger.always(chalk.gray("This may take several minutes..."));
|
|
91
119
|
try {
|
|
120
|
+
// A model pull is hundreds of megabytes and legitimately runs for many
|
|
121
|
+
// minutes, so it MUST opt out of safeSpawn's default query bound.
|
|
122
|
+
// Passing only `stdio` here is what made the first revision of this
|
|
123
|
+
// change kill downloads at 15s: the wrapper spreads `options` over its
|
|
124
|
+
// defaults, so an unmentioned `timeout` keeps the default rather than
|
|
125
|
+
// being absent. It inherits stdio, so the user sees progress and can
|
|
126
|
+
// interrupt it — the two things a wedged query lacks.
|
|
92
127
|
const res = this.safeSpawn("ollama", ["pull", model], {
|
|
93
128
|
stdio: "inherit",
|
|
129
|
+
timeout: OLLAMA_NO_TIMEOUT,
|
|
94
130
|
});
|
|
95
131
|
if (res.error || res.status !== 0) {
|
|
96
132
|
throw res.error || new Error("pull failed");
|
|
@@ -211,25 +247,36 @@ export class OllamaCommandFactory {
|
|
|
211
247
|
*/
|
|
212
248
|
static async stopHandler() {
|
|
213
249
|
const spinner = ora("Stopping Ollama service...").start();
|
|
250
|
+
// `safeSpawn` RETURNS a result; it does not throw on a failed command. So
|
|
251
|
+
// the try/catch fallbacks that used to sit here never fired — `killall`
|
|
252
|
+
// and the pkill fallback were unreachable, and the handler reported
|
|
253
|
+
// "stopped" whatever happened. Bounding these calls made that worse rather
|
|
254
|
+
// than better: a timeout now yields `error: ETIMEDOUT` with `status: null`,
|
|
255
|
+
// which is still not a throw, so a wedged kill would be reported as a
|
|
256
|
+
// successful stop while Ollama stayed up.
|
|
257
|
+
//
|
|
258
|
+
// Branch on the result instead, which both restores the fallback and makes
|
|
259
|
+
// the success message conditional on something actually succeeding.
|
|
260
|
+
const ok = (r) => !r.error && r.status === 0;
|
|
214
261
|
try {
|
|
262
|
+
let stopped;
|
|
215
263
|
if (process.platform === "darwin") {
|
|
216
|
-
|
|
217
|
-
this.safeSpawn("pkill", ["ollama"])
|
|
218
|
-
|
|
219
|
-
catch {
|
|
220
|
-
this.safeSpawn("killall", ["Ollama"]);
|
|
221
|
-
}
|
|
264
|
+
stopped =
|
|
265
|
+
ok(this.safeSpawn("pkill", ["ollama"])) ||
|
|
266
|
+
ok(this.safeSpawn("killall", ["Ollama"]));
|
|
222
267
|
}
|
|
223
268
|
else if (process.platform === "linux") {
|
|
224
|
-
|
|
225
|
-
this.safeSpawn("systemctl", ["stop", "ollama"])
|
|
226
|
-
|
|
227
|
-
catch {
|
|
228
|
-
this.safeSpawn("pkill", ["ollama"]);
|
|
229
|
-
}
|
|
269
|
+
stopped =
|
|
270
|
+
ok(this.safeSpawn("systemctl", ["stop", "ollama"])) ||
|
|
271
|
+
ok(this.safeSpawn("pkill", ["ollama"]));
|
|
230
272
|
}
|
|
231
273
|
else {
|
|
232
|
-
this.safeSpawn("taskkill", ["/F", "/IM", "ollama.exe"]);
|
|
274
|
+
stopped = ok(this.safeSpawn("taskkill", ["/F", "/IM", "ollama.exe"]));
|
|
275
|
+
}
|
|
276
|
+
if (!stopped) {
|
|
277
|
+
spinner.fail("Could not confirm Ollama service stopped");
|
|
278
|
+
logger.error(chalk.red("No stop command succeeded — it may not have been running, or it may still be up. Check with: ollama list"));
|
|
279
|
+
return;
|
|
233
280
|
}
|
|
234
281
|
spinner.succeed("Ollama service stopped");
|
|
235
282
|
}
|
|
@@ -8,6 +8,15 @@ export declare class OllamaUtils {
|
|
|
8
8
|
* Secure wrapper around spawnSync to prevent command injection.
|
|
9
9
|
*/
|
|
10
10
|
static safeSpawn(command: AllowedCommand, args: string[], options?: SpawnSyncOptions): SpawnSyncReturns<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a `safeSpawn` call actually succeeded.
|
|
13
|
+
*
|
|
14
|
+
* `spawnSync` reports failure by RETURNING — `error` set (ENOENT, ETIMEDOUT)
|
|
15
|
+
* or a non-zero `status` — never by throwing. Every `try/catch` around one of
|
|
16
|
+
* these calls was therefore dead code, which is why several fallbacks in this
|
|
17
|
+
* file had never run.
|
|
18
|
+
*/
|
|
19
|
+
private static spawnSucceeded;
|
|
11
20
|
/**
|
|
12
21
|
* Check if Ollama command line is available
|
|
13
22
|
*/
|
|
@@ -2,6 +2,32 @@ import { spawnSync, spawn, } from "child_process";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* Ceiling for a synchronous Ollama query (`list`, `--version`, `rm`, …).
|
|
7
|
+
*
|
|
8
|
+
* `spawnSync` blocks the event loop, so an unresponsive daemon does not make
|
|
9
|
+
* the CLI slow — it makes it unkillable by anything short of Ctrl-C, with no
|
|
10
|
+
* output and no error. These calls are local IPC that normally answer in
|
|
11
|
+
* milliseconds, so a ceiling this generous only ever fires on a genuine wedge.
|
|
12
|
+
*
|
|
13
|
+
* `killSignal: "SIGKILL"` is not decoration. `spawnSync`'s `timeout` sends
|
|
14
|
+
* SIGTERM by default and then keeps waiting, so a child that ignores SIGTERM
|
|
15
|
+
* hangs forever anyway and the timeout buys nothing.
|
|
16
|
+
*
|
|
17
|
+
* Long-running commands (`ollama pull`) must pass their own `timeout` — the
|
|
18
|
+
* spread below lets a caller override this — because a model download
|
|
19
|
+
* legitimately runs for many minutes.
|
|
20
|
+
*/
|
|
21
|
+
const OLLAMA_QUERY_TIMEOUT_MS = 15_000;
|
|
22
|
+
/**
|
|
23
|
+
* Ceiling for the whole readiness loop, not one probe inside it.
|
|
24
|
+
*
|
|
25
|
+
* Ollama normally answers within seconds of starting; a wait longer than this
|
|
26
|
+
* means it is not coming up, and continuing to spin helps nobody. Sized to
|
|
27
|
+
* outlast a slow cold start while staying far below the ~15 minutes that 30
|
|
28
|
+
* attempts of bounded probes could otherwise reach.
|
|
29
|
+
*/
|
|
30
|
+
const READINESS_DEADLINE_MS = 90_000;
|
|
5
31
|
/**
|
|
6
32
|
* Shared Ollama utilities for CLI commands
|
|
7
33
|
*/
|
|
@@ -11,11 +37,24 @@ export class OllamaUtils {
|
|
|
11
37
|
*/
|
|
12
38
|
static safeSpawn(command, args, options = {}) {
|
|
13
39
|
const defaultOptions = {
|
|
40
|
+
timeout: OLLAMA_QUERY_TIMEOUT_MS,
|
|
41
|
+
killSignal: "SIGKILL",
|
|
14
42
|
...options,
|
|
15
43
|
encoding: "utf8", // Always enforce utf8 encoding
|
|
16
44
|
};
|
|
17
45
|
return spawnSync(command, args, defaultOptions);
|
|
18
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a `safeSpawn` call actually succeeded.
|
|
49
|
+
*
|
|
50
|
+
* `spawnSync` reports failure by RETURNING — `error` set (ENOENT, ETIMEDOUT)
|
|
51
|
+
* or a non-zero `status` — never by throwing. Every `try/catch` around one of
|
|
52
|
+
* these calls was therefore dead code, which is why several fallbacks in this
|
|
53
|
+
* file had never run.
|
|
54
|
+
*/
|
|
55
|
+
static spawnSucceeded(result) {
|
|
56
|
+
return !result.error && result.status === 0;
|
|
57
|
+
}
|
|
19
58
|
/**
|
|
20
59
|
* Check if Ollama command line is available
|
|
21
60
|
*/
|
|
@@ -70,7 +109,16 @@ export class OllamaUtils {
|
|
|
70
109
|
*/
|
|
71
110
|
static async waitForOllamaReady(maxAttempts = 30, initialDelay = 500) {
|
|
72
111
|
let delay = initialDelay;
|
|
112
|
+
// A per-call bound alone is not enough here, and adding one made this
|
|
113
|
+
// worse before it made it better: each attempt can now burn the full
|
|
114
|
+
// command bound plus the API bound, so 30 attempts is up to ~15 minutes of
|
|
115
|
+
// spinner rather than the seconds this loop was written to take. The
|
|
116
|
+
// per-attempt bound stops one wedged call; this stops the loop around it.
|
|
117
|
+
const deadline = Date.now() + READINESS_DEADLINE_MS;
|
|
73
118
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
119
|
+
if (Date.now() >= deadline) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
74
122
|
try {
|
|
75
123
|
// Try command line check first
|
|
76
124
|
if (!this.isOllamaCommandReady()) {
|
|
@@ -117,11 +165,17 @@ export class OllamaUtils {
|
|
|
117
165
|
try {
|
|
118
166
|
if (process.platform === "darwin") {
|
|
119
167
|
logger.always(chalk.gray("Starting Ollama on macOS..."));
|
|
120
|
-
|
|
121
|
-
|
|
168
|
+
// `safeSpawn` RETURNS a result; it does not throw on a failed command,
|
|
169
|
+
// so this catch only ever fired on a programming error and the
|
|
170
|
+
// `ollama serve` fallback beneath it was effectively unreachable.
|
|
171
|
+
// Bounding these calls made that worse: a timeout yields
|
|
172
|
+
// `error: ETIMEDOUT` with `status: null`, still not a throw, so a
|
|
173
|
+
// wedged launcher would have been reported as a successful start.
|
|
174
|
+
// Branch on the result so the fallback actually runs.
|
|
175
|
+
if (this.spawnSucceeded(this.safeSpawn("open", ["-a", "Ollama"]))) {
|
|
122
176
|
logger.always(chalk.green("✅ Ollama app started"));
|
|
123
177
|
}
|
|
124
|
-
|
|
178
|
+
else {
|
|
125
179
|
const child = spawn("ollama", ["serve"], {
|
|
126
180
|
stdio: "ignore",
|
|
127
181
|
detached: true,
|
|
@@ -135,11 +189,10 @@ export class OllamaUtils {
|
|
|
135
189
|
}
|
|
136
190
|
else if (process.platform === "linux") {
|
|
137
191
|
logger.always(chalk.gray("Starting Ollama service on Linux..."));
|
|
138
|
-
|
|
139
|
-
this.safeSpawn("systemctl", ["start", "ollama"]);
|
|
192
|
+
if (this.spawnSucceeded(this.safeSpawn("systemctl", ["start", "ollama"]))) {
|
|
140
193
|
logger.always(chalk.green("✅ Ollama service started"));
|
|
141
194
|
}
|
|
142
|
-
|
|
195
|
+
else {
|
|
143
196
|
const child = spawn("ollama", ["serve"], {
|
|
144
197
|
stdio: "ignore",
|
|
145
198
|
detached: true,
|
|
@@ -156,11 +209,16 @@ export class OllamaUtils {
|
|
|
156
209
|
// Security Note: Windows shell=true usage is intentional here for 'start' command.
|
|
157
210
|
// Arguments are controlled internally (no user input) and safeSpawn validates command names.
|
|
158
211
|
// This is safer than alternative Windows process creation methods for this specific use case.
|
|
159
|
-
this.safeSpawn("start", ["ollama", "serve"], {
|
|
212
|
+
const started = this.safeSpawn("start", ["ollama", "serve"], {
|
|
160
213
|
stdio: "ignore",
|
|
161
214
|
shell: true,
|
|
162
215
|
});
|
|
163
|
-
|
|
216
|
+
if (this.spawnSucceeded(started)) {
|
|
217
|
+
logger.always(chalk.green("✅ Ollama service started"));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
logger.always(chalk.yellow("⚠️ Could not confirm Ollama started — check with: ollama list"));
|
|
221
|
+
}
|
|
164
222
|
}
|
|
165
223
|
// Wait for service to become ready with readiness probe
|
|
166
224
|
const readinessSpinner = ora("Waiting for Ollama service to be ready...").start();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.22.
|
|
3
|
+
"version": "11.22.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|