@d4niel-dev/dizel 0.0.0-stage → 2.0.0

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.
Files changed (3) hide show
  1. package/README.md +59 -2
  2. package/bin/dizel.js +682 -0
  3. package/package.json +34 -4
package/README.md CHANGED
@@ -1,3 +1,60 @@
1
- # Temporary Holding Version
1
+ # Dizel npm launcher
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0",
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
+ }