@vierratale/ai 0.1.0-beta.13 → 0.1.0-beta.14
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/LICENSE +21 -0
- package/bin/vierrataleai.js +3 -13
- package/package.json +2 -1
- package/src/cli.js +6 -6
- package/src/engine.js +150 -0
- package/src/index.js +1 -1
- package/src/ui/branding.js +1 -1
- package/src/installer.js +0 -300
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vierratale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/vierrataleai.js
CHANGED
|
@@ -9,11 +9,9 @@
|
|
|
9
9
|
// Installation progress is always the fake animated bar — real engine/model
|
|
10
10
|
// output is deliberately hidden so first-run feels quick and clean.
|
|
11
11
|
import { run } from '../src/cli.js';
|
|
12
|
-
import {
|
|
12
|
+
import { Engine } from '../src/engine.js';
|
|
13
13
|
import { Config } from '../src/config.js';
|
|
14
14
|
import { Branding } from '../src/ui/branding.js';
|
|
15
|
-
import { Terminal } from '../src/ui/terminal.js';
|
|
16
|
-
import { dirname } from 'path';
|
|
17
15
|
|
|
18
16
|
const C = Branding.colors;
|
|
19
17
|
|
|
@@ -63,19 +61,11 @@ async function main() {
|
|
|
63
61
|
|
|
64
62
|
if (doInstall) {
|
|
65
63
|
console.log(`\n ${C.bold}${C.primary}${Branding.APP_NAME} ${Branding.VERSION}${C.reset} ${C.dim}— installer${C.reset}\n`);
|
|
66
|
-
|
|
67
|
-
if (!result.command) {
|
|
68
|
-
Terminal.printInfo('Could not create a global command; run it via the bin script.');
|
|
69
|
-
} else {
|
|
70
|
-
Terminal.printSuccess(`Engine + model installed; "vierrataleai" (or "vierratale") command available at ${result.commandAlias || result.command}`);
|
|
71
|
-
if (!result.commandOnPath) {
|
|
72
|
-
Terminal.printInfo(`"vierrataleai" won't resolve in a new shell: add ${dirname(result.command)} to PATH, e.g. export PATH="$HOME/.local/bin:$PATH", then reopen the terminal.`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
64
|
+
await fakeProgress(INSTALL_STEPS, Engine.ensure());
|
|
75
65
|
console.log();
|
|
76
66
|
} else if (!quick) {
|
|
77
67
|
// Auto-detect: install anything that's missing (engine, model) before starting.
|
|
78
|
-
await fakeProgress(READY_STEPS,
|
|
68
|
+
await fakeProgress(READY_STEPS, Engine.ensure());
|
|
79
69
|
console.log();
|
|
80
70
|
}
|
|
81
71
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vierratale/ai",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"description": "VierrataleAI - Intelligent terminal assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"src",
|
|
20
20
|
"scripts",
|
|
21
|
+
"LICENSE",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { Config } from './config.js';
|
|
4
4
|
import { Catalog } from './catalog.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Engine } from './engine.js';
|
|
6
6
|
import { ProviderFactory } from './providers/index.js';
|
|
7
7
|
import { Branding } from './ui/branding.js';
|
|
8
8
|
import { Terminal } from './ui/terminal.js';
|
|
@@ -811,7 +811,7 @@ async function chat(provider, systemPrompt) {
|
|
|
811
811
|
: Catalog.getCloudModels();
|
|
812
812
|
const installedSet = new Set();
|
|
813
813
|
if (provider.isLocal) {
|
|
814
|
-
for (const m of await
|
|
814
|
+
for (const m of await Engine.getInstalledModels(Config.get('engineHost'))) {
|
|
815
815
|
installedSet.add(m);
|
|
816
816
|
}
|
|
817
817
|
}
|
|
@@ -1114,18 +1114,18 @@ async function ensureLocalModel(modelName, notify) {
|
|
|
1114
1114
|
// dead-ends while at least one local model exists.
|
|
1115
1115
|
async function ensureRawLocalModel(realModel, notify) {
|
|
1116
1116
|
const host = Config.get('engineHost');
|
|
1117
|
-
const resolved = await
|
|
1117
|
+
const resolved = await Engine.resolveModel(host, realModel);
|
|
1118
1118
|
if (resolved.model) {
|
|
1119
1119
|
return { ok: true, model: resolved.model, reason: resolved.reason };
|
|
1120
1120
|
}
|
|
1121
1121
|
if (notify) notify(`Downloading ${realModel}… (this can take a while)`);
|
|
1122
|
-
const pulled = await
|
|
1122
|
+
const pulled = await Engine.pullModel(host, realModel);
|
|
1123
1123
|
if (pulled) {
|
|
1124
1124
|
if (notify) notify(`Model ${realModel} ready.`);
|
|
1125
1125
|
return { ok: true, model: realModel, reason: null };
|
|
1126
1126
|
}
|
|
1127
1127
|
if (notify) notify(`Could not download ${realModel}.`);
|
|
1128
|
-
const first = await
|
|
1128
|
+
const first = await Engine.firstInstalled(host);
|
|
1129
1129
|
if (first) {
|
|
1130
1130
|
return { ok: true, model: first, reason: `Could not download ${realModel}; using installed ${first}` };
|
|
1131
1131
|
}
|
|
@@ -1264,7 +1264,7 @@ export async function run() {
|
|
|
1264
1264
|
|
|
1265
1265
|
await Terminal.showProgress(
|
|
1266
1266
|
['Checking engine', 'Installing dependencies', 'Downloading model', 'Optimizing'],
|
|
1267
|
-
() =>
|
|
1267
|
+
() => Engine.ensure()
|
|
1268
1268
|
);
|
|
1269
1269
|
|
|
1270
1270
|
await ensureLocalModel(Config.get('model'));
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { execSync, exec } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { Catalog } from './catalog.js';
|
|
4
|
+
import { Config } from './config.js';
|
|
5
|
+
|
|
6
|
+
function runSilent(cmd) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
exec(cmd, { stdio: 'ignore' }, (err) => resolve(!err));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isEngineRunning(host) {
|
|
13
|
+
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
14
|
+
.then((r) => r.ok)
|
|
15
|
+
.catch(() => false);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isEngineInstalled() {
|
|
19
|
+
try {
|
|
20
|
+
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {}
|
|
23
|
+
const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
|
|
24
|
+
return paths.some((p) => existsSync(p));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function installEngine() {
|
|
28
|
+
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
29
|
+
return isEngineInstalled();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getInstalledModels(host) {
|
|
33
|
+
try {
|
|
34
|
+
const resp = await fetch(`${host}/api/tags`);
|
|
35
|
+
if (!resp.ok) return [];
|
|
36
|
+
const data = await resp.json();
|
|
37
|
+
return (data.models || []).map((m) => m.name);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function pullModel(host, model) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(`${host}/api/pull`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ name: model, stream: false }),
|
|
49
|
+
signal: AbortSignal.timeout(600000),
|
|
50
|
+
});
|
|
51
|
+
return resp.ok;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function familyOf(model) {
|
|
58
|
+
return model
|
|
59
|
+
.split(':')[0]
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sizeOf(model) {
|
|
65
|
+
const size = model.split(':')[1] || '';
|
|
66
|
+
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
67
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
68
|
+
const value = parseFloat(match[1]);
|
|
69
|
+
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function chooseModel(installed, realModel) {
|
|
73
|
+
if (installed.includes(realModel)) {
|
|
74
|
+
return { model: realModel, substituted: false, reason: null };
|
|
75
|
+
}
|
|
76
|
+
const fam = familyOf(realModel);
|
|
77
|
+
let best = null;
|
|
78
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
79
|
+
for (const m of installed) {
|
|
80
|
+
if (familyOf(m) !== fam) continue;
|
|
81
|
+
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
82
|
+
if (dist < bestDist) {
|
|
83
|
+
bestDist = dist;
|
|
84
|
+
best = m;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (best) {
|
|
88
|
+
return {
|
|
89
|
+
model: best,
|
|
90
|
+
substituted: true,
|
|
91
|
+
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function resolveModel(host, realModel) {
|
|
98
|
+
const installed = await getInstalledModels(host);
|
|
99
|
+
return chooseModel(installed, realModel);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function ensure() {
|
|
103
|
+
const host = Config.get('engineHost');
|
|
104
|
+
|
|
105
|
+
if (!isEngineInstalled()) {
|
|
106
|
+
await installEngine();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const running = await isEngineRunning(host);
|
|
110
|
+
if (!running) {
|
|
111
|
+
try {
|
|
112
|
+
const child = exec('ollama serve', {
|
|
113
|
+
stdio: 'ignore',
|
|
114
|
+
detached: true,
|
|
115
|
+
});
|
|
116
|
+
if (child.unref) child.unref();
|
|
117
|
+
for (let i = 0; i < 30; i++) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
119
|
+
if (await isEngineRunning(host)) break;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
125
|
+
const installed = await getInstalledModels(host);
|
|
126
|
+
const resolved = chooseModel(installed, realModel);
|
|
127
|
+
if (resolved.model == null) {
|
|
128
|
+
await pullModel(host, realModel);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function isReady() {
|
|
135
|
+
const host = Config.get('engineHost');
|
|
136
|
+
return isEngineRunning(host);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Engine = {
|
|
140
|
+
ensure,
|
|
141
|
+
isReady,
|
|
142
|
+
pullModel,
|
|
143
|
+
getInstalledModels,
|
|
144
|
+
resolveModel,
|
|
145
|
+
chooseModel,
|
|
146
|
+
firstInstalled: async (host) => {
|
|
147
|
+
const installed = await getInstalledModels(host);
|
|
148
|
+
return installed[0] || null;
|
|
149
|
+
},
|
|
150
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Catalog } from './catalog.js';
|
|
2
2
|
export { Config } from './config.js';
|
|
3
|
-
export {
|
|
3
|
+
export { Engine } from './engine.js';
|
|
4
4
|
export { CortexProvider } from './providers/cortex.js';
|
|
5
5
|
export { OpenAIProvider } from './providers/openai.js';
|
|
6
6
|
export { Branding } from './ui/branding.js';
|
package/src/ui/branding.js
CHANGED
package/src/installer.js
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import { execSync, exec } from 'child_process';
|
|
2
|
-
import { existsSync, mkdirSync, symlinkSync, chmodSync, writeFileSync, lstatSync, readlinkSync, rmSync, realpathSync } from 'fs';
|
|
3
|
-
import { join, dirname, resolve } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { homedir, platform } from 'os';
|
|
6
|
-
import { Catalog } from './catalog.js';
|
|
7
|
-
import { Config } from './config.js';
|
|
8
|
-
|
|
9
|
-
function runSilent(cmd) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
exec(cmd, { stdio: 'ignore' }, (err) => resolve(!err));
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isEngineRunning(host) {
|
|
16
|
-
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
17
|
-
.then((r) => r.ok)
|
|
18
|
-
.catch(() => false);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isEngineInstalled() {
|
|
22
|
-
try {
|
|
23
|
-
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
24
|
-
return true;
|
|
25
|
-
} catch {}
|
|
26
|
-
const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
|
|
27
|
-
return paths.some((p) => existsSync(p));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function installEngine() {
|
|
31
|
-
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
32
|
-
return isEngineInstalled();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function pullModel(host, model) {
|
|
36
|
-
try {
|
|
37
|
-
const resp = await fetch(`${host}/api/pull`, {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
body: JSON.stringify({ name: model, stream: false }),
|
|
41
|
-
signal: AbortSignal.timeout(600000),
|
|
42
|
-
});
|
|
43
|
-
return resp.ok;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function getInstalledModels(host) {
|
|
50
|
-
try {
|
|
51
|
-
const resp = await fetch(`${host}/api/tags`);
|
|
52
|
-
if (!resp.ok) return [];
|
|
53
|
-
const data = await resp.json();
|
|
54
|
-
return (data.models || []).map((m) => m.name);
|
|
55
|
-
} catch {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Scripts in the same model family share a base name before any variant
|
|
61
|
-
// suffix (e.g. qwen2.5-coder:1.5b is the sibling of qwen2.5:1.5b).
|
|
62
|
-
function familyOf(model) {
|
|
63
|
-
return model
|
|
64
|
-
.split(':')[0]
|
|
65
|
-
.toLowerCase()
|
|
66
|
-
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sizeOf(model) {
|
|
70
|
-
const size = model.split(':')[1] || '';
|
|
71
|
-
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
72
|
-
if (!match) return Number.POSITIVE_INFINITY;
|
|
73
|
-
const value = parseFloat(match[1]);
|
|
74
|
-
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Find the already-installed model that best matches the requested one.
|
|
78
|
-
// Prefers an exact match, then any same-family sibling (closest size).
|
|
79
|
-
function chooseModel(installed, realModel) {
|
|
80
|
-
if (installed.includes(realModel)) {
|
|
81
|
-
return { model: realModel, substituted: false, reason: null };
|
|
82
|
-
}
|
|
83
|
-
const fam = familyOf(realModel);
|
|
84
|
-
let best = null;
|
|
85
|
-
let bestDist = Number.POSITIVE_INFINITY;
|
|
86
|
-
for (const m of installed) {
|
|
87
|
-
if (familyOf(m) !== fam) continue;
|
|
88
|
-
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
89
|
-
if (dist < bestDist) {
|
|
90
|
-
bestDist = dist;
|
|
91
|
-
best = m;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (best) {
|
|
95
|
-
return {
|
|
96
|
-
model: best,
|
|
97
|
-
substituted: true,
|
|
98
|
-
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function resolveModel(host, realModel) {
|
|
105
|
-
const installed = await getInstalledModels(host);
|
|
106
|
-
return chooseModel(installed, realModel);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Best local bin dir for a globally-callable command on this platform.
|
|
110
|
-
function localBinDir() {
|
|
111
|
-
if (process.env.XDG_BIN_HOME) return process.env.XDG_BIN_HOME;
|
|
112
|
-
if (process.env.HOME) return join(homedir(), '.local', 'bin');
|
|
113
|
-
return '/usr/local/bin';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isOnPath(dir) {
|
|
117
|
-
const sep = platform() === 'win32' ? ';' : ':';
|
|
118
|
-
let dirResolved;
|
|
119
|
-
try {
|
|
120
|
-
dirResolved = existsSync(dir) ? realpathSync(dir) : resolve(dir);
|
|
121
|
-
} catch {
|
|
122
|
-
dirResolved = resolve(dir);
|
|
123
|
-
}
|
|
124
|
-
return String(process.env.PATH || '')
|
|
125
|
-
.split(sep)
|
|
126
|
-
.some((entry) => {
|
|
127
|
-
entry = entry.trim();
|
|
128
|
-
if (!entry) return false;
|
|
129
|
-
try {
|
|
130
|
-
const e = existsSync(entry) ? realpathSync(entry) : resolve(entry);
|
|
131
|
-
return e === dirResolved;
|
|
132
|
-
} catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Prefer a bin dir that is already on PATH (and writable) so the installed
|
|
139
|
-
// command is callable immediately; fall back to the local bin dir.
|
|
140
|
-
function chooseBinDir() {
|
|
141
|
-
const local = localBinDir();
|
|
142
|
-
if (isOnPath(local)) return local;
|
|
143
|
-
for (const candidate of ['/usr/local/bin', '/usr/bin']) {
|
|
144
|
-
try {
|
|
145
|
-
mkdirSync(candidate, { recursive: true, mode: 0o755 });
|
|
146
|
-
const probe = join(candidate, `.vierrataleai-write-test-${process.pid}`);
|
|
147
|
-
writeFileSync(probe, '');
|
|
148
|
-
rmSync(probe, { force: true });
|
|
149
|
-
return candidate;
|
|
150
|
-
} catch {}
|
|
151
|
-
}
|
|
152
|
-
return local;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Create (or refresh) a launcher link for `name` pointing at the node bin.
|
|
156
|
-
function writeLauncher(dir, name, target) {
|
|
157
|
-
const launcherPath = join(dir, name);
|
|
158
|
-
if (platform() === 'win32') {
|
|
159
|
-
if (!existsSync(launcherPath)) {
|
|
160
|
-
writeFileSync(launcherPath, `@echo off\r\nnode "${target}" %*\r\n`);
|
|
161
|
-
}
|
|
162
|
-
return launcherPath;
|
|
163
|
-
}
|
|
164
|
-
// Prefer a real symlink; fall back to a small shim that calls the node bin.
|
|
165
|
-
let live = false;
|
|
166
|
-
try {
|
|
167
|
-
live = lstatSync(launcherPath).isSymbolicLink() && readlinkSync(launcherPath) === target;
|
|
168
|
-
} catch {}
|
|
169
|
-
if (!live) {
|
|
170
|
-
rmSync(launcherPath, { force: true });
|
|
171
|
-
try {
|
|
172
|
-
symlinkSync(target, launcherPath);
|
|
173
|
-
chmodSync(launcherPath, 0o755);
|
|
174
|
-
} catch {
|
|
175
|
-
writeFileSync(launcherPath, `#!/bin/sh\nnode "${target}" "$@"\n`);
|
|
176
|
-
chmodSync(launcherPath, 0o755);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return launcherPath;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function installAppCommand() {
|
|
183
|
-
try {
|
|
184
|
-
const dir = chooseBinDir();
|
|
185
|
-
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
186
|
-
const defaultTarget = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'vierrataleai.js');
|
|
187
|
-
const target = process.argv[1] && existsSync(process.argv[1]) ? process.argv[1] : defaultTarget;
|
|
188
|
-
// Both spellings run the assistant.
|
|
189
|
-
const launcherPath = writeLauncher(dir, 'vierrataleai', target);
|
|
190
|
-
writeLauncher(dir, 'vierratale', target);
|
|
191
|
-
return launcherPath;
|
|
192
|
-
} catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export const Installer = {
|
|
198
|
-
async pullModel(host, model) {
|
|
199
|
-
return pullModel(host, model);
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
async getInstalledModels(host) {
|
|
203
|
-
return getInstalledModels(host);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
async resolveModel(host, realModel) {
|
|
207
|
-
return resolveModel(host, realModel);
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
async firstInstalled(host) {
|
|
211
|
-
const installed = await getInstalledModels(host);
|
|
212
|
-
return installed[0] || null;
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
chooseModel,
|
|
216
|
-
|
|
217
|
-
installAppCommand,
|
|
218
|
-
isOnPath,
|
|
219
|
-
|
|
220
|
-
async ensure() {
|
|
221
|
-
const host = Config.get('engineHost');
|
|
222
|
-
|
|
223
|
-
if (!isEngineInstalled()) {
|
|
224
|
-
await installEngine();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const running = await isEngineRunning(host);
|
|
228
|
-
if (!running) {
|
|
229
|
-
try {
|
|
230
|
-
// No systemd available here, so launch the daemon directly, detached,
|
|
231
|
-
// and poll until it responds instead of relying on a service manager.
|
|
232
|
-
const child = exec('ollama serve', {
|
|
233
|
-
stdio: 'ignore',
|
|
234
|
-
detached: true,
|
|
235
|
-
});
|
|
236
|
-
if (child.unref) child.unref();
|
|
237
|
-
for (let i = 0; i < 30; i++) {
|
|
238
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
239
|
-
if (await isEngineRunning(host)) break;
|
|
240
|
-
}
|
|
241
|
-
} catch {}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
245
|
-
const installed = await getInstalledModels(host);
|
|
246
|
-
const resolved = chooseModel(installed, realModel);
|
|
247
|
-
const needsPull = resolved.model == null;
|
|
248
|
-
|
|
249
|
-
if (needsPull) {
|
|
250
|
-
await pullModel(host, realModel);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return true;
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
async isReady() {
|
|
257
|
-
const host = Config.get('engineHost');
|
|
258
|
-
return isEngineRunning(host);
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
// Full install: engine + model + a global "vierrataleai" command.
|
|
262
|
-
async install({ includeCommand = true } = {}) {
|
|
263
|
-
const host = Config.get('engineHost');
|
|
264
|
-
|
|
265
|
-
if (!isEngineInstalled()) {
|
|
266
|
-
await installEngine();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const running = await isEngineRunning(host);
|
|
270
|
-
if (!running) {
|
|
271
|
-
try {
|
|
272
|
-
const child = exec('ollama serve', { stdio: 'ignore', detached: true });
|
|
273
|
-
if (child.unref) child.unref();
|
|
274
|
-
for (let i = 0; i < 30; i++) {
|
|
275
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
276
|
-
if (await isEngineRunning(host)) break;
|
|
277
|
-
}
|
|
278
|
-
} catch {}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
282
|
-
const installed = await getInstalledModels(host);
|
|
283
|
-
const resolved = chooseModel(installed, realModel);
|
|
284
|
-
if (resolved.model == null) {
|
|
285
|
-
await pullModel(host, realModel);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let command = null;
|
|
289
|
-
let commandAlias = null;
|
|
290
|
-
let commandOnPath = false;
|
|
291
|
-
if (includeCommand) {
|
|
292
|
-
command = installAppCommand();
|
|
293
|
-
if (command) {
|
|
294
|
-
commandAlias = join(dirname(command), 'vierratale');
|
|
295
|
-
commandOnPath = isOnPath(dirname(command));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return { installed: await isEngineInstalled(), running: await isEngineRunning(host), command, commandAlias, commandOnPath };
|
|
299
|
-
},
|
|
300
|
-
};
|