@d4niel-dev/dizel 0.0.0-stage → 2.0.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/README.md +59 -2
- package/bin/dizel.js +682 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dizel npm launcher
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```bash
|
|
4
|
+
npm install -g @d4niel-dev/dizel@latest
|
|
5
|
+
dizel install
|
|
6
|
+
dizel doctor
|
|
7
|
+
dizel tui
|
|
8
|
+
dizel gui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`dizel` is a small launcher, not the 9 GB app payload. It downloads the matching
|
|
12
|
+
GitHub Release artifacts, verifies their SHA-256, and keeps them under
|
|
13
|
+
`%LOCALAPPDATA%\Dizel` (Windows), `~/Library/Application Support/Dizel` (macOS),
|
|
14
|
+
or `~/.local/share/dizel` (Linux).
|
|
15
|
+
|
|
16
|
+
## What `npm install -g` gives you
|
|
17
|
+
|
|
18
|
+
- a global `dizel` command; npm creates `dizel.cmd` on Windows;
|
|
19
|
+
- `dizel install` to download the app version that matches this launcher;
|
|
20
|
+
- `dizel update` to refresh the terminal archive from the latest release;
|
|
21
|
+
- `dizel tui` to run the terminal client;
|
|
22
|
+
- `dizel gui` to open the installed desktop app;
|
|
23
|
+
- `dizel deps` to create a virtualenv and install the Python requirements;
|
|
24
|
+
- `dizel doctor` to check Node, Python, installed files, and model paths;
|
|
25
|
+
- `dizel update` to refresh the terminal archive from the latest release.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- **Node.js 18+** for this launcher.
|
|
30
|
+
- **Windows** for the desktop app. The current release publishes an NSIS installer.
|
|
31
|
+
- **Python 3.10+** for the TUI and the desktop sidecar. The desktop installer
|
|
32
|
+
includes the Python source but not a Python runtime.
|
|
33
|
+
- A model checkpoint. Weights are not part of npm or the launcher; use the
|
|
34
|
+
published v1.5 GGUF or a local `.pt` checkpoint:
|
|
35
|
+
`dizel tui --checkpoint <path>`.
|
|
36
|
+
|
|
37
|
+
The Python and model requirements are the reason this is a launcher rather than
|
|
38
|
+
a single self-contained npm tarball. A frozen Python sidecar is the next release
|
|
39
|
+
engineering step toward a true one-command install.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dizel install # desktop (Windows) + terminal
|
|
45
|
+
dizel install --tui # terminal only
|
|
46
|
+
dizel install --gui # desktop installer only
|
|
47
|
+
dizel install --dry-run # print asset URLs without downloading
|
|
48
|
+
dizel install --yes # launch the GUI installer without asking
|
|
49
|
+
dizel tui [args...] # run the terminal client
|
|
50
|
+
dizel gui # launch the installed desktop app
|
|
51
|
+
dizel deps # create the managed virtualenv and pip install requirements
|
|
52
|
+
dizel doctor [--json] # diagnostics
|
|
53
|
+
dizel update # refresh the terminal archive from latest
|
|
54
|
+
dizel model # model setup pointers
|
|
55
|
+
dizel version
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The launcher never stores model weights and never publishes secrets. Release
|
|
59
|
+
downloads are restricted to GitHub release hosts and verified against the
|
|
60
|
+
manifest SHA-256.
|
package/bin/dizel.js
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const os = require("node:os");
|
|
7
|
+
const https = require("node:https");
|
|
8
|
+
const crypto = require("node:crypto");
|
|
9
|
+
const readline = require("node:readline");
|
|
10
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
11
|
+
const pkg = require("../package.json");
|
|
12
|
+
|
|
13
|
+
const VERSION = pkg.version;
|
|
14
|
+
const REPO = "D4niel-dev/Dizel";
|
|
15
|
+
const MANIFEST_VERSION_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/dizel-update.json`;
|
|
16
|
+
const MANIFEST_LATEST_URL = `https://github.com/${REPO}/releases/latest/download/dizel-update.json`;
|
|
17
|
+
const DOCS_URL = "https://d4niel-dev.github.io/Dizel/guide/install.html";
|
|
18
|
+
const RELEASES_URL = `https://github.com/${REPO}/releases`;
|
|
19
|
+
const IS_WIN = process.platform === "win32";
|
|
20
|
+
const IS_MAC = process.platform === "darwin";
|
|
21
|
+
const ALLOWED_HOSTS = new Set([
|
|
22
|
+
"github.com",
|
|
23
|
+
"api.github.com",
|
|
24
|
+
"objects.githubusercontent.com",
|
|
25
|
+
"github-releases.githubusercontent.com",
|
|
26
|
+
"release-assets.githubusercontent.com",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function defaultHome() {
|
|
30
|
+
if (IS_WIN) {
|
|
31
|
+
return path.join(process.env.LOCALAPPDATA || os.homedir(), "Dizel");
|
|
32
|
+
}
|
|
33
|
+
if (IS_MAC) {
|
|
34
|
+
return path.join(os.homedir(), "Library", "Application Support", "Dizel");
|
|
35
|
+
}
|
|
36
|
+
const data = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
|
|
37
|
+
return path.join(data, "dizel");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const HOME = process.env.DIZEL_HOME
|
|
41
|
+
? path.resolve(process.env.DIZEL_HOME)
|
|
42
|
+
: defaultHome();
|
|
43
|
+
const STATE = path.join(HOME, "state.json");
|
|
44
|
+
const DOWNLOADS = path.join(HOME, "downloads");
|
|
45
|
+
const INSTALLERS = path.join(HOME, "installers");
|
|
46
|
+
|
|
47
|
+
function print(message = "") {
|
|
48
|
+
console.log(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(message) {
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureDir(dir) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readState() {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(STATE, "utf8"));
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveState(next) {
|
|
68
|
+
ensureDir(HOME);
|
|
69
|
+
fs.writeFileSync(STATE, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function venvPythonPath() {
|
|
73
|
+
return path.join(HOME, "venv", IS_WIN ? "Scripts/python.exe" : "bin/python");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function installedTui() {
|
|
77
|
+
const state = readState();
|
|
78
|
+
return state.tui && fs.existsSync(path.join(state.tui, "inference", "tui", "main.py"))
|
|
79
|
+
? state.tui
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatBytes(bytes) {
|
|
84
|
+
if (!bytes || bytes < 0) return "unknown size";
|
|
85
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
86
|
+
let value = bytes;
|
|
87
|
+
let unit = 0;
|
|
88
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
89
|
+
value /= 1024;
|
|
90
|
+
unit += 1;
|
|
91
|
+
}
|
|
92
|
+
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getAllowedResponse(url, redirects = 0) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = new URL(url);
|
|
100
|
+
} catch {
|
|
101
|
+
reject(new Error(`bad URL: ${url}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (parsed.protocol !== "https:") {
|
|
105
|
+
reject(new Error(`refusing non-HTTPS download: ${url}`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
|
|
109
|
+
reject(new Error(`refusing download host: ${parsed.hostname}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const request = https.get(
|
|
113
|
+
parsed,
|
|
114
|
+
{ headers: { "User-Agent": `dizel-npm/${VERSION}` } },
|
|
115
|
+
(response) => {
|
|
116
|
+
const status = response.statusCode || 0;
|
|
117
|
+
if ([301, 302, 303, 307, 308].includes(status) && response.headers.location) {
|
|
118
|
+
response.resume();
|
|
119
|
+
if (redirects >= 5) {
|
|
120
|
+
reject(new Error("too many redirects"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
resolve(
|
|
124
|
+
getAllowedResponse(
|
|
125
|
+
new URL(response.headers.location, url).href,
|
|
126
|
+
redirects + 1,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (status !== 200) {
|
|
132
|
+
response.resume();
|
|
133
|
+
reject(new Error(`HTTP ${status} for ${url}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
resolve(response);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
request.on("error", reject);
|
|
140
|
+
request.setTimeout(30000, () => request.destroy(new Error("request timed out")));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readResponse(response, limit = 4 * 1024 * 1024) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
let size = 0;
|
|
148
|
+
response.on("data", (chunk) => {
|
|
149
|
+
size += chunk.length;
|
|
150
|
+
if (size > limit) {
|
|
151
|
+
response.destroy();
|
|
152
|
+
reject(new Error("release manifest is too large"));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
chunks.push(chunk);
|
|
156
|
+
});
|
|
157
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
158
|
+
response.on("error", reject);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadManifest({ latest = false } = {}) {
|
|
163
|
+
if (process.env.DIZEL_RELEASE_MANIFEST) {
|
|
164
|
+
return JSON.parse(
|
|
165
|
+
fs.readFileSync(path.resolve(process.env.DIZEL_RELEASE_MANIFEST), "utf8"),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const response = await getAllowedResponse(
|
|
169
|
+
latest ? MANIFEST_LATEST_URL : MANIFEST_VERSION_URL,
|
|
170
|
+
);
|
|
171
|
+
const body = await readResponse(response);
|
|
172
|
+
return JSON.parse(body.toString("utf8"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assetUrl(manifest, asset) {
|
|
176
|
+
if (!asset || !asset.asset) fail("release manifest has no downloadable asset");
|
|
177
|
+
return `https://github.com/${REPO}/releases/download/${encodeURIComponent(
|
|
178
|
+
manifest.tag,
|
|
179
|
+
)}/${encodeURIComponent(asset.asset)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sha256File(file) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const hash = crypto.createHash("sha256");
|
|
185
|
+
const stream = fs.createReadStream(file);
|
|
186
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
187
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
188
|
+
stream.on("error", reject);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function downloadAsset(asset, url, destination) {
|
|
193
|
+
ensureDir(path.dirname(destination));
|
|
194
|
+
if (fs.existsSync(destination)) {
|
|
195
|
+
const digest = await sha256File(destination);
|
|
196
|
+
const size = fs.statSync(destination).size;
|
|
197
|
+
if (
|
|
198
|
+
(!asset.sha256 || digest === asset.sha256) &&
|
|
199
|
+
(!asset.size || size === asset.size)
|
|
200
|
+
) {
|
|
201
|
+
print(` cached ${asset.asset}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
fs.rmSync(destination, { force: true });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
print(` download ${asset.asset} (${formatBytes(asset.size)})`);
|
|
208
|
+
const response = await getAllowedResponse(url);
|
|
209
|
+
const temporary = `${destination}.part`;
|
|
210
|
+
fs.rmSync(temporary, { force: true });
|
|
211
|
+
const hash = crypto.createHash("sha256");
|
|
212
|
+
let bytes = 0;
|
|
213
|
+
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
const output = fs.createWriteStream(temporary);
|
|
216
|
+
response.on("data", (chunk) => {
|
|
217
|
+
hash.update(chunk);
|
|
218
|
+
bytes += chunk.length;
|
|
219
|
+
if (!output.write(chunk)) {
|
|
220
|
+
response.pause();
|
|
221
|
+
output.once("drain", () => response.resume());
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
response.on("error", reject);
|
|
225
|
+
response.on("end", () => output.end());
|
|
226
|
+
output.on("error", reject);
|
|
227
|
+
output.on("finish", () => {
|
|
228
|
+
try {
|
|
229
|
+
if (asset.size && bytes !== asset.size) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`size mismatch for ${asset.asset}: expected ${asset.size}, got ${bytes}`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const digest = hash.digest("hex");
|
|
235
|
+
if (asset.sha256 && digest !== asset.sha256) {
|
|
236
|
+
throw new Error(`SHA-256 mismatch for ${asset.asset}`);
|
|
237
|
+
}
|
|
238
|
+
fs.renameSync(temporary, destination);
|
|
239
|
+
resolve();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
reject(error);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}).catch((error) => {
|
|
245
|
+
fs.rmSync(temporary, { force: true });
|
|
246
|
+
throw error;
|
|
247
|
+
});
|
|
248
|
+
print(` verified ${asset.asset}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractZip(archive, destination) {
|
|
252
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
253
|
+
ensureDir(destination);
|
|
254
|
+
if (IS_WIN) {
|
|
255
|
+
const quote = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
256
|
+
const script = `Expand-Archive -LiteralPath ${quote(
|
|
257
|
+
archive,
|
|
258
|
+
)} -DestinationPath ${quote(destination)} -Force`;
|
|
259
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
});
|
|
262
|
+
if (result.status === 0) return;
|
|
263
|
+
throw new Error("PowerShell could not extract the TUI archive.");
|
|
264
|
+
}
|
|
265
|
+
const python = findPython();
|
|
266
|
+
if (python) {
|
|
267
|
+
const result = spawnSync(
|
|
268
|
+
python.exe,
|
|
269
|
+
[...python.args, "-m", "zipfile", "-e", archive, destination],
|
|
270
|
+
{ stdio: "inherit" },
|
|
271
|
+
);
|
|
272
|
+
if (result.status === 0) return;
|
|
273
|
+
}
|
|
274
|
+
const unzip = spawnSync("unzip", ["-q", archive, "-d", destination], {
|
|
275
|
+
stdio: "inherit",
|
|
276
|
+
});
|
|
277
|
+
if (unzip.status !== 0) {
|
|
278
|
+
throw new Error("Could not extract the TUI archive (install unzip or Python).");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findPython() {
|
|
283
|
+
const candidates = [];
|
|
284
|
+
if (process.env.DIZEL_PYTHON) {
|
|
285
|
+
candidates.push({ exe: process.env.DIZEL_PYTHON, args: [] });
|
|
286
|
+
}
|
|
287
|
+
const venv = venvPythonPath();
|
|
288
|
+
if (fs.existsSync(venv)) candidates.push({ exe: venv, args: [] });
|
|
289
|
+
if (IS_WIN) candidates.push({ exe: "py", args: ["-3"] });
|
|
290
|
+
candidates.push({ exe: "python3", args: [] }, { exe: "python", args: [] });
|
|
291
|
+
|
|
292
|
+
for (const candidate of candidates) {
|
|
293
|
+
const result = spawnSync(
|
|
294
|
+
candidate.exe,
|
|
295
|
+
[
|
|
296
|
+
...candidate.args,
|
|
297
|
+
"-c",
|
|
298
|
+
"import sys; print('%d.%d' % sys.version_info[:2]); raise SystemExit(0 if sys.version_info >= (3, 10) else 1)",
|
|
299
|
+
],
|
|
300
|
+
{ encoding: "utf8", windowsHide: true },
|
|
301
|
+
);
|
|
302
|
+
if (!result.error && result.status === 0) {
|
|
303
|
+
return { ...candidate, version: (result.stdout || "").trim() };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function findGui() {
|
|
310
|
+
if (!IS_WIN) return null;
|
|
311
|
+
const candidates = [];
|
|
312
|
+
if (process.env.LOCALAPPDATA) {
|
|
313
|
+
candidates.push(
|
|
314
|
+
path.join(process.env.LOCALAPPDATA, "Programs", "Dizel", "Dizel.exe"),
|
|
315
|
+
path.join(process.env.LOCALAPPDATA, "Dizel", "Dizel.exe"),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (process.env.ProgramFiles) {
|
|
319
|
+
candidates.push(path.join(process.env.ProgramFiles, "Dizel", "Dizel.exe"));
|
|
320
|
+
}
|
|
321
|
+
if (process.env["ProgramFiles(x86)"]) {
|
|
322
|
+
candidates.push(
|
|
323
|
+
path.join(process.env["ProgramFiles(x86)"], "Dizel", "Dizel.exe"),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function run(exe, args, options = {}) {
|
|
330
|
+
const result = spawnSync(exe, args, { stdio: "inherit", ...options });
|
|
331
|
+
if (result.error) throw result.error;
|
|
332
|
+
if (result.status !== 0) {
|
|
333
|
+
throw new Error(`${exe} exited with code ${result.status}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function confirm(question) {
|
|
338
|
+
if (!process.stdin.isTTY) return Promise.resolve(false);
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
const interface_ = readline.createInterface({
|
|
341
|
+
input: process.stdin,
|
|
342
|
+
output: process.stdout,
|
|
343
|
+
});
|
|
344
|
+
interface_.question(`${question} [Y/n] `, (answer) => {
|
|
345
|
+
interface_.close();
|
|
346
|
+
const value = answer.trim().toLowerCase();
|
|
347
|
+
resolve(value === "" || value === "y" || value === "yes");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function installTui(manifest, options) {
|
|
353
|
+
const asset = manifest.tui;
|
|
354
|
+
const url = assetUrl(manifest, asset);
|
|
355
|
+
const archive = path.join(DOWNLOADS, asset.asset);
|
|
356
|
+
if (options.dryRun) {
|
|
357
|
+
print(` TUI ${url}`);
|
|
358
|
+
print(` ${formatBytes(asset.size)} · sha256 ${asset.sha256 || "—"}`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await downloadAsset(asset, url, archive);
|
|
362
|
+
const destination = path.join(HOME, "tui", manifest.version);
|
|
363
|
+
print(` extract ${destination}`);
|
|
364
|
+
extractZip(archive, destination);
|
|
365
|
+
const state = readState();
|
|
366
|
+
state.version = manifest.version;
|
|
367
|
+
state.tui = destination;
|
|
368
|
+
state.installedAt = new Date().toISOString();
|
|
369
|
+
saveState(state);
|
|
370
|
+
print(` ready terminal ${manifest.version}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function installGui(manifest, options) {
|
|
374
|
+
if (!IS_WIN) {
|
|
375
|
+
print(" desktop Windows-only in the current release. Use the TUI for now.");
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const asset = manifest.gui;
|
|
379
|
+
const url = assetUrl(manifest, asset);
|
|
380
|
+
const installer = path.join(INSTALLERS, asset.asset);
|
|
381
|
+
if (options.dryRun) {
|
|
382
|
+
print(` desktop ${url}`);
|
|
383
|
+
print(` ${formatBytes(asset.size)} · sha256 ${asset.sha256 || "—"}`);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
await downloadAsset(asset, url, installer);
|
|
387
|
+
if (options.yes || (await confirm("Launch the Dizel desktop installer now?"))) {
|
|
388
|
+
spawn(installer, [], { detached: true, stdio: "ignore" }).unref();
|
|
389
|
+
print(` opened ${installer}`);
|
|
390
|
+
} else {
|
|
391
|
+
print(` installer saved to ${installer}`);
|
|
392
|
+
print(" run it when ready, then use: dizel gui");
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function installCommand(options, flags, { latest = false } = {}) {
|
|
398
|
+
const manifest = await loadManifest({ latest });
|
|
399
|
+
if (!manifest.version || !manifest.tag || !manifest.tui || !manifest.gui) {
|
|
400
|
+
fail("release manifest is missing version, tag, GUI, or TUI assets");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let wantTui = true;
|
|
404
|
+
let wantGui = IS_WIN;
|
|
405
|
+
const onlyTui = flags.has("--tui") && !flags.has("--gui") && !flags.has("--all");
|
|
406
|
+
const onlyGui = flags.has("--gui") && !flags.has("--tui") && !flags.has("--all");
|
|
407
|
+
if (onlyTui) wantGui = false;
|
|
408
|
+
if (onlyGui) wantTui = false;
|
|
409
|
+
if (flags.has("--all")) {
|
|
410
|
+
wantTui = true;
|
|
411
|
+
wantGui = IS_WIN;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (options.dryRun && options.json) {
|
|
415
|
+
const payload = {
|
|
416
|
+
version: manifest.version,
|
|
417
|
+
tag: manifest.tag,
|
|
418
|
+
tui: {
|
|
419
|
+
asset: manifest.tui.asset,
|
|
420
|
+
url: assetUrl(manifest, manifest.tui),
|
|
421
|
+
sha256: manifest.tui.sha256 || "",
|
|
422
|
+
size: manifest.tui.size || 0,
|
|
423
|
+
},
|
|
424
|
+
gui: {
|
|
425
|
+
asset: manifest.gui.asset,
|
|
426
|
+
url: assetUrl(manifest, manifest.gui),
|
|
427
|
+
sha256: manifest.gui.sha256 || "",
|
|
428
|
+
size: manifest.gui.size || 0,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
print(JSON.stringify(payload, null, 2));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
print(`Dizel ${manifest.version} (${manifest.tag})`);
|
|
436
|
+
if (wantTui) await installTui(manifest, options);
|
|
437
|
+
if (wantGui) await installGui(manifest, options);
|
|
438
|
+
if (!wantTui && !wantGui) {
|
|
439
|
+
fail("nothing selected; use --tui, --gui, or --all");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
print("");
|
|
443
|
+
if (wantGui) print("Finish the desktop installer, then open Dizel or run: dizel gui");
|
|
444
|
+
if (wantTui) {
|
|
445
|
+
print("First run: dizel deps");
|
|
446
|
+
print("Then: dizel tui --checkpoint <path>");
|
|
447
|
+
}
|
|
448
|
+
print(`Model weights are separate. See: ${DOCS_URL}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function depsCommand() {
|
|
452
|
+
const tui = installedTui();
|
|
453
|
+
if (!tui) fail("terminal archive is not installed; run: dizel install --tui");
|
|
454
|
+
const requirements = path.join(tui, "requirements.txt");
|
|
455
|
+
if (!fs.existsSync(requirements)) {
|
|
456
|
+
fail(`no requirements.txt in ${tui}`);
|
|
457
|
+
}
|
|
458
|
+
const system = findPython();
|
|
459
|
+
if (!system) {
|
|
460
|
+
fail("Python 3.10+ is required: https://www.python.org/downloads/");
|
|
461
|
+
}
|
|
462
|
+
const venv = path.join(HOME, "venv");
|
|
463
|
+
const venvPython = venvPythonPath();
|
|
464
|
+
if (!fs.existsSync(venvPython)) {
|
|
465
|
+
print(`Creating virtualenv: ${venv}`);
|
|
466
|
+
run(system.exe, [...system.args, "-m", "venv", venv]);
|
|
467
|
+
}
|
|
468
|
+
print("Installing runtime dependencies. This includes PyTorch and can take a while.");
|
|
469
|
+
run(venvPython, ["-m", "pip", "install", "--upgrade", "pip"]);
|
|
470
|
+
run(venvPython, ["-m", "pip", "install", "-r", requirements]);
|
|
471
|
+
print("");
|
|
472
|
+
print("Dependencies ready. Run: dizel tui --checkpoint <path>");
|
|
473
|
+
if (IS_WIN) {
|
|
474
|
+
print("The desktop installer still resolves its own Python; set DIZEL_PYTHON if needed.");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function runTui(args) {
|
|
479
|
+
const tui = installedTui();
|
|
480
|
+
if (!tui) fail("terminal archive is not installed; run: dizel install --tui");
|
|
481
|
+
const venvPython = venvPythonPath();
|
|
482
|
+
const python = fs.existsSync(venvPython)
|
|
483
|
+
? { exe: venvPython, args: [] }
|
|
484
|
+
: findPython();
|
|
485
|
+
if (!python) {
|
|
486
|
+
fail(
|
|
487
|
+
"Python 3.10+ with the Dizel dependencies is required. " +
|
|
488
|
+
"Install Python, then run: dizel deps",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const result = spawnSync(
|
|
492
|
+
python.exe,
|
|
493
|
+
[...python.args, "-m", "inference.tui.main", ...args],
|
|
494
|
+
{
|
|
495
|
+
cwd: tui,
|
|
496
|
+
stdio: "inherit",
|
|
497
|
+
env: { ...process.env, PYTHONPATH: tui },
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
if (result.error) throw result.error;
|
|
501
|
+
process.exitCode = result.status === null ? 1 : result.status;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function guiCommand() {
|
|
505
|
+
const exe = findGui();
|
|
506
|
+
if (!exe) {
|
|
507
|
+
if (!IS_WIN) {
|
|
508
|
+
print("The desktop app is Windows-only in the current release.");
|
|
509
|
+
print(`Installers: ${RELEASES_URL}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
fail("desktop app is not installed; run: dizel install --gui");
|
|
513
|
+
}
|
|
514
|
+
spawn(exe, [], { detached: true, stdio: "ignore" }).unref();
|
|
515
|
+
print(`Opened ${exe}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function modelRoots() {
|
|
519
|
+
return [
|
|
520
|
+
path.join(process.cwd(), "checkpoints"),
|
|
521
|
+
path.join(process.cwd(), "models"),
|
|
522
|
+
path.join(HOME, "checkpoints"),
|
|
523
|
+
path.join(HOME, "models"),
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function localModels() {
|
|
528
|
+
const models = [];
|
|
529
|
+
for (const root of modelRoots()) {
|
|
530
|
+
if (!fs.existsSync(root)) continue;
|
|
531
|
+
try {
|
|
532
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
533
|
+
if (!entry.isFile()) continue;
|
|
534
|
+
if (!/\.(pt|gguf|safetensors|bin)$/i.test(entry.name)) continue;
|
|
535
|
+
models.push(path.join(root, entry.name));
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
/* unreadable directory */
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return models;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function doctorCommand(options) {
|
|
545
|
+
const python = findPython();
|
|
546
|
+
const venv = fs.existsSync(venvPythonPath());
|
|
547
|
+
const tui = installedTui();
|
|
548
|
+
const gui = findGui();
|
|
549
|
+
const models = localModels();
|
|
550
|
+
const report = {
|
|
551
|
+
launcher: VERSION,
|
|
552
|
+
node: process.version,
|
|
553
|
+
platform: `${process.platform}-${process.arch}`,
|
|
554
|
+
home: HOME,
|
|
555
|
+
python: python ? python.version : null,
|
|
556
|
+
pythonExe: python ? python.exe : null,
|
|
557
|
+
venv,
|
|
558
|
+
tui,
|
|
559
|
+
gui,
|
|
560
|
+
models,
|
|
561
|
+
docs: DOCS_URL,
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
if (options.json) {
|
|
565
|
+
print(JSON.stringify(report, null, 2));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
print(`Dizel launcher ${VERSION}`);
|
|
570
|
+
print(` node ${process.version}`);
|
|
571
|
+
print(` platform ${process.platform}-${process.arch}`);
|
|
572
|
+
print(` home ${HOME}`);
|
|
573
|
+
print(` python ${python ? `${python.version} (${python.exe})` : "not found — install Python 3.10+"}`);
|
|
574
|
+
print(` venv ${venv ? "ready" : "not created — run dizel deps after install"}`);
|
|
575
|
+
print(` terminal ${tui || "not installed — dizel install --tui"}`);
|
|
576
|
+
print(` desktop ${gui || (IS_WIN ? "not installed — dizel install --gui" : "Windows-only")}`);
|
|
577
|
+
print(` models ${models.length ? `${models.length} local checkpoint(s)` : "none found"}`);
|
|
578
|
+
if (models.length) {
|
|
579
|
+
for (const model of models.slice(0, 5)) print(` ${model}`);
|
|
580
|
+
}
|
|
581
|
+
print("");
|
|
582
|
+
print("Next:");
|
|
583
|
+
if (!tui) print(" dizel install --tui");
|
|
584
|
+
else if (!venv) print(" dizel deps");
|
|
585
|
+
else print(" dizel tui --checkpoint <path>");
|
|
586
|
+
if (IS_WIN && !gui) print(" dizel install --gui");
|
|
587
|
+
if (!models.length) print(` model guides: ${DOCS_URL}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function modelCommand() {
|
|
591
|
+
print("Model weights are not part of npm or the launcher.");
|
|
592
|
+
print(`Docs: ${DOCS_URL}`);
|
|
593
|
+
print(`Releases: ${RELEASES_URL}`);
|
|
594
|
+
const models = localModels();
|
|
595
|
+
if (models.length) {
|
|
596
|
+
print("");
|
|
597
|
+
print("Local checkpoints:");
|
|
598
|
+
for (const model of models) print(` ${model}`);
|
|
599
|
+
}
|
|
600
|
+
print("");
|
|
601
|
+
print("Run: dizel tui --checkpoint <path>");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function help() {
|
|
605
|
+
print(`Dizel ${VERSION} — local-first AI model, desktop app, and terminal client`);
|
|
606
|
+
print("");
|
|
607
|
+
print("Usage:");
|
|
608
|
+
print(" dizel install [--tui|--gui|--all] [--yes] [--dry-run]");
|
|
609
|
+
print(" dizel install tui|gui|all");
|
|
610
|
+
print(" dizel tui [args...]");
|
|
611
|
+
print(" dizel gui");
|
|
612
|
+
print(" dizel deps");
|
|
613
|
+
print(" dizel doctor [--json]");
|
|
614
|
+
print(" dizel update");
|
|
615
|
+
print(" dizel model");
|
|
616
|
+
print(" dizel version");
|
|
617
|
+
print("");
|
|
618
|
+
print("The npm package is a launcher. It downloads the matching GitHub Release");
|
|
619
|
+
print("artifacts; weights, Electron, and Python remain separate downloads.");
|
|
620
|
+
print(`Docs: ${DOCS_URL}`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function main() {
|
|
624
|
+
const argv = process.argv.slice(2);
|
|
625
|
+
const command = argv[0] || "help";
|
|
626
|
+
|
|
627
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
628
|
+
help();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (["version", "--version", "-v"].includes(command)) {
|
|
632
|
+
print(`dizel ${VERSION}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (command === "tui") {
|
|
636
|
+
runTui(argv.slice(1));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const flags = new Set(
|
|
641
|
+
argv.slice(1).filter((value) => value.startsWith("--")),
|
|
642
|
+
);
|
|
643
|
+
const options = {
|
|
644
|
+
dryRun: flags.has("--dry-run"),
|
|
645
|
+
yes: flags.has("--yes"),
|
|
646
|
+
json: flags.has("--json"),
|
|
647
|
+
};
|
|
648
|
+
const selector = argv[1];
|
|
649
|
+
if (["tui", "gui", "all"].includes(selector)) flags.add(`--${selector}`);
|
|
650
|
+
|
|
651
|
+
switch (command) {
|
|
652
|
+
case "install":
|
|
653
|
+
await installCommand(options, flags);
|
|
654
|
+
return;
|
|
655
|
+
case "update": {
|
|
656
|
+
flags.add("--tui");
|
|
657
|
+
await installCommand(options, flags, { latest: true });
|
|
658
|
+
print("The desktop app updates itself; run dizel install --gui to refresh the installer.");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
case "deps":
|
|
662
|
+
await depsCommand();
|
|
663
|
+
return;
|
|
664
|
+
case "doctor":
|
|
665
|
+
doctorCommand(options);
|
|
666
|
+
return;
|
|
667
|
+
case "gui":
|
|
668
|
+
guiCommand();
|
|
669
|
+
return;
|
|
670
|
+
case "model":
|
|
671
|
+
modelCommand();
|
|
672
|
+
return;
|
|
673
|
+
default:
|
|
674
|
+
help();
|
|
675
|
+
process.exitCode = 1;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
main().catch((error) => {
|
|
680
|
+
console.error(`dizel: ${error.message || error}`);
|
|
681
|
+
process.exitCode = 1;
|
|
682
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d4niel-dev/dizel",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Dizel — local-first AI model, desktop app, and terminal client",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "D4niel-dev",
|
|
7
|
+
"homepage": "https://d4niel-dev.github.io/Dizel/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/D4niel-dev/Dizel.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/D4niel-dev/Dizel/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"dizel": "bin/dizel.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"dizel",
|
|
27
|
+
"local-ai",
|
|
28
|
+
"llm",
|
|
29
|
+
"cli",
|
|
30
|
+
"desktop",
|
|
31
|
+
"tui"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|