twokey 1.0.2 → 1.0.3

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 CHANGED
@@ -29,6 +29,7 @@ twokey
29
29
  ```
30
30
 
31
31
  Default behavior: start the native desktop app in background.
32
+ If no desktop binary is installed yet, `twokey` attempts to download an AppImage from the latest GitHub release into `~/.local/share/twokey/bin/` and starts it.
32
33
 
33
34
  Useful CLI options:
34
35
 
package/bin/twokey.js CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
4
7
  import readline from "node:readline";
5
8
 
6
- const VERSION = "1.0.2";
9
+ const VERSION = "1.0.3";
7
10
  const DEFAULT_MODEL = process.env.TWOKEY_OLLAMA_MODEL || "qwen2.5:3b";
8
11
  const DEFAULT_OLLAMA_URL = process.env.TWOKEY_OLLAMA_URL || "http://127.0.0.1:11434";
12
+ const LATEST_RELEASE_API = "https://api.github.com/repos/meinzeug/twokey/releases/latest";
13
+ const APPIMAGE_DIR = path.join(os.homedir(), ".local", "share", "twokey", "bin");
14
+ const APPIMAGE_PATH = path.join(APPIMAGE_DIR, "twokey-ai.AppImage");
9
15
 
10
16
  const args = process.argv.slice(2);
11
17
 
@@ -55,8 +61,8 @@ if (onceIndex >= 0) {
55
61
  process.exit(0);
56
62
  }
57
63
 
58
- console.error("No native desktop binary found in PATH.");
59
- console.error("Install the .deb/.AppImage release and ensure 'twokey-ai' is available in PATH.");
64
+ console.error("Could not start desktop app.");
65
+ console.error("Tried system binaries and auto-download from GitHub Releases.");
60
66
  console.error("Use 'twokey --cli' to run terminal mode.");
61
67
  process.exit(1);
62
68
  });
@@ -159,7 +165,7 @@ async function launchDesktopApp() {
159
165
  if (process.env.TWOKEY_DESKTOP_CMD) {
160
166
  candidates.push(process.env.TWOKEY_DESKTOP_CMD);
161
167
  }
162
- candidates.push("twokey-ai", "twokey-desktop");
168
+ candidates.push("twokey-ai", "twokey-desktop", APPIMAGE_PATH);
163
169
 
164
170
  for (const command of candidates) {
165
171
  const started = await spawnDetached(command);
@@ -168,9 +174,71 @@ async function launchDesktopApp() {
168
174
  }
169
175
  }
170
176
 
177
+ try {
178
+ const downloaded = await ensureLocalAppImage();
179
+ if (downloaded) {
180
+ return spawnDetached(APPIMAGE_PATH);
181
+ }
182
+ } catch {
183
+ return false;
184
+ }
185
+
171
186
  return false;
172
187
  }
173
188
 
189
+ async function ensureLocalAppImage() {
190
+ try {
191
+ await fs.promises.access(APPIMAGE_PATH, fs.constants.X_OK);
192
+ return true;
193
+ } catch {
194
+ // Not installed yet.
195
+ }
196
+
197
+ await fs.promises.mkdir(APPIMAGE_DIR, { recursive: true });
198
+ const assetUrl = await resolveLatestAppImageUrl();
199
+ if (!assetUrl) {
200
+ return false;
201
+ }
202
+
203
+ const response = await fetch(assetUrl, {
204
+ headers: {
205
+ "User-Agent": "twokey-cli",
206
+ Accept: "application/octet-stream",
207
+ },
208
+ });
209
+
210
+ if (!response.ok) {
211
+ return false;
212
+ }
213
+
214
+ const data = Buffer.from(await response.arrayBuffer());
215
+ await fs.promises.writeFile(APPIMAGE_PATH, data, { mode: 0o755 });
216
+
217
+ await fs.promises.chmod(APPIMAGE_PATH, 0o755);
218
+ return true;
219
+ }
220
+
221
+ async function resolveLatestAppImageUrl() {
222
+ const response = await fetch(LATEST_RELEASE_API, {
223
+ headers: {
224
+ "User-Agent": "twokey-cli",
225
+ Accept: "application/vnd.github+json",
226
+ },
227
+ });
228
+
229
+ if (!response.ok) {
230
+ return null;
231
+ }
232
+
233
+ const payload = await response.json();
234
+ const assets = Array.isArray(payload.assets) ? payload.assets : [];
235
+ const appImage = assets.find(
236
+ (asset) => typeof asset?.name === "string" && asset.name.endsWith(".AppImage") && asset.name.includes("amd64"),
237
+ ) || assets.find((asset) => typeof asset?.name === "string" && asset.name.endsWith(".AppImage"));
238
+
239
+ return appImage?.browser_download_url ?? null;
240
+ }
241
+
174
242
  function spawnDetached(command) {
175
243
  return new Promise((resolve) => {
176
244
  const child = spawn(command, [], {
@@ -201,4 +269,5 @@ function printHelp() {
201
269
  console.log(" --desktop Start native desktop app in background");
202
270
  console.log("");
203
271
  console.log("Without options, twokey starts the native desktop app in background.");
272
+ console.log("If no desktop binary is installed, twokey tries to download an AppImage from latest GitHub release.");
204
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twokey",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Linux-first desktop AI assistant built with Tauri, React, and TypeScript.",
5
5
  "license": "MIT",
6
6
  "repository": {