symbiot 0.39.0 → 0.40.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 +140 -3
- package/core.mjs +23 -2
- package/desktop.mjs +29 -8
- package/headless.mjs +362 -0
- package/index.mjs +237 -56
- package/package.json +2 -1
- package/screens.mjs +44 -17
- package/ui.mjs +104 -24
package/README.md
CHANGED
|
@@ -89,7 +89,9 @@ it's already running just opens the existing window instead of starting a second
|
|
|
89
89
|
copy (`SYMBIOT_FORCE_NEW=1` starts one anyway). If something else holds the port,
|
|
90
90
|
it falls back to a free one.
|
|
91
91
|
It works on **Linux, macOS and Windows**; with no Chromium-family browser to open
|
|
92
|
-
the chrome-less window, it uses your default browser.
|
|
92
|
+
the chrome-less window, it uses your default browser. On an Android phone it runs
|
|
93
|
+
in Termux ([experimental](#on-your-phone-android-in-termux)), or as an
|
|
94
|
+
[Android app](#the-android-app-apk) of its own.
|
|
93
95
|
|
|
94
96
|
### Every week, and at login
|
|
95
97
|
|
|
@@ -110,6 +112,95 @@ repo is now archived), without Electron:
|
|
|
110
112
|
then opens that copy's window. Switch it off to remove the entry. It needs an
|
|
111
113
|
installed Symbiot (`npm install -g symbiot`), not `npx`.
|
|
112
114
|
|
|
115
|
+
### On your phone (Android, in Termux)
|
|
116
|
+
|
|
117
|
+
Experimental, and not yet tried on a real phone. Symbiot is a Node CLI, so it runs
|
|
118
|
+
in [Termux](https://termux.dev) (install it from F-Droid; the Play Store build is
|
|
119
|
+
out of date) next to the coding agent you use there. It works on the repos on your
|
|
120
|
+
phone, the same way it works on your computer's:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pkg install nodejs git gh termux-api # gh: Approve opens the PR; termux-api: notifications
|
|
124
|
+
npm install -g symbiot
|
|
125
|
+
symbiot app # opens in your phone's browser
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- The app opens in Symbiot's Android app if it's installed (below), else in the
|
|
129
|
+
phone's browser (through `termux-open-url`), and is laid out
|
|
130
|
+
for a phone screen: Tasks, Agents (with your agent's questions), Approve and
|
|
131
|
+
Settings all work there. Set your agent command in Settings as on a computer
|
|
132
|
+
(Claude Code is detected if it's on Termux's `PATH`). "Add to Home screen" in
|
|
133
|
+
Chrome gives it an icon.
|
|
134
|
+
- Clone repos into Termux's home (`~`), which Symbiot scans by default, along
|
|
135
|
+
with the home folders (`/root`, `/home/<you>`) of any Linux you run with
|
|
136
|
+
`proot-distro`. To reach
|
|
137
|
+
the phone's shared storage, run `termux-setup-storage` and add `~/storage/shared`
|
|
138
|
+
(or a folder in it) under Settings → Folders to scan.
|
|
139
|
+
- **Write my week and notify me** sends an Android notification when the
|
|
140
|
+
`termux-api` package and the Termux:API app are installed.
|
|
141
|
+
- **Start Symbiot in the background when the phone starts** writes a
|
|
142
|
+
[Termux:Boot](https://wiki.termux.com/wiki/Termux:Boot) script
|
|
143
|
+
(`~/.termux/boot/symbiot`) that takes a wake lock, so Android doesn't stop it.
|
|
144
|
+
Install Termux:Boot and open it once.
|
|
145
|
+
- **Screens** can't capture the screen from Termux; **Load image** still works.
|
|
146
|
+
|
|
147
|
+
### The Android app (APK)
|
|
148
|
+
|
|
149
|
+
Experimental, and so far tried in the Android emulator (Android 14) and on one
|
|
150
|
+
phone (a Galaxy A26, Android 16). The app is Symbiot on its own, with no Termux needed: Node, git and `gh`
|
|
151
|
+
are inside it (the same builds Termux installs), and it shows Symbiot full screen
|
|
152
|
+
with its own icon.
|
|
153
|
+
|
|
154
|
+
- **Install:** on your phone, download `symbiot-aarch64.apk` from the
|
|
155
|
+
[latest release](https://github.com/GarthGhostai/symbiot/releases/latest)
|
|
156
|
+
(or copy `symbiot-<version>-aarch64.apk` over from a computer that built it)
|
|
157
|
+
and open it (allow your browser or file manager to install apps). Android may warn that the
|
|
158
|
+
app was built for an older version of Android. That's on purpose: it's what lets
|
|
159
|
+
an app run programs it carries, like Node and git, the same reason Termux does.
|
|
160
|
+
It also keeps the app off the Play Store.
|
|
161
|
+
- **First start** unpacks Node, git and gh (a few seconds), then asks for **All
|
|
162
|
+
files access**, so it can find the repos in your phone's shared storage. It scans
|
|
163
|
+
shared storage (for example the folders Termux's `~/storage/shared` points at)
|
|
164
|
+
instead of a home folder. Until it has that access, the Map says so and has an
|
|
165
|
+
**Allow file access** button. Once you allow it and come back, the Map scans again.
|
|
166
|
+
- **Its own Symbiot only sees shared storage.** Projects in Termux's home folder
|
|
167
|
+
(`~`) are private to Termux, and no other app can read them. Keep a project in
|
|
168
|
+
shared storage (in Termux, run `termux-setup-storage` once, then work under
|
|
169
|
+
`~/storage/shared`), or have the app show the Symbiot running in Termux (next
|
|
170
|
+
point). The repos on your computer aren't on the phone either, so clone the
|
|
171
|
+
ones you want.
|
|
172
|
+
- **Your Termux projects: show the Symbiot running in Termux.** A Symbiot started
|
|
173
|
+
in Termux sees everything in its home folder and runs your agent there, and the
|
|
174
|
+
app can be its window. **Open Termux** (on the Map when it finds no repos, and
|
|
175
|
+
in Settings → Projects in Termux) copies a command that installs Node and
|
|
176
|
+
Symbiot in Termux if they're missing, then runs `symbiot app`. Paste it in
|
|
177
|
+
Termux (it also updates Symbiot there if it's older than the app's). Termux
|
|
178
|
+
then opens Symbiot's link in the app, as a `symbiot://` link that only the app
|
|
179
|
+
takes, and the app shows that Symbiot instead of its own, and stops its own. It
|
|
180
|
+
keeps doing so until you tap **Use the app's own Symbiot**. If that Symbiot
|
|
181
|
+
stops, the app says so and offers both buttons. If the page opens in your
|
|
182
|
+
browser instead, it offers **Open in the app**. Termux from
|
|
183
|
+
Google Play can't take commands from other apps (it
|
|
184
|
+
has no `RUN_COMMAND`), so the app can't start Symbiot there by itself.
|
|
185
|
+
- It keeps running with its window closed (a quiet "Symbiot is running"
|
|
186
|
+
notification has **Stop**), so the weekly write-up, agent runs and Approve keep
|
|
187
|
+
going. **Start Symbiot in the background when the phone starts** and **Write my
|
|
188
|
+
week and notify me** work as they do on a computer, with Android notifications.
|
|
189
|
+
**Update & restart** updates it from npm, as on a computer.
|
|
190
|
+
- **Your coding agent isn't inside it.** Keep running Claude Code (or your agent)
|
|
191
|
+
in Termux, on the same repos in shared storage, the way you do now.
|
|
192
|
+
- **Screens:** no capture and no Map page (a phone has no desktop browser to
|
|
193
|
+
drive); **Load image** works.
|
|
194
|
+
|
|
195
|
+
To build it yourself: `android/build.sh` (or `android/build.sh x86_64` for the
|
|
196
|
+
emulator) writes `android/build/symbiot-<version>-<arch>.apk`. It needs Node, the
|
|
197
|
+
Android SDK and a JDK 17 (or Docker). The script's header lists the details,
|
|
198
|
+
including the signing key to keep (`~/.android/symbiot.jks`): an update has to be
|
|
199
|
+
signed with the same key. Each release also builds the aarch64 APK and attaches it
|
|
200
|
+
to a GitHub release tagged `apk-<version>`, signed with that same key once it's
|
|
201
|
+
in the repo's `SYMBIOT_KEYSTORE_B64` and `SYMBIOT_KEYSTORE_PASS` secrets (see
|
|
202
|
+
`.github/workflows/publish.yml`).
|
|
203
|
+
|
|
113
204
|
## Hand tasks to your coding agent: `symbiot push`
|
|
114
205
|
|
|
115
206
|
```bash
|
|
@@ -416,7 +507,53 @@ Hovering shows the pixel under the pointer. Each region keeps its position and s
|
|
|
416
507
|
in the screenshot's own pixels, plus its **centre**, the point a click would aim at.
|
|
417
508
|
**Copy blueprint** copies them as JSON; everything is also saved in
|
|
418
509
|
`~/.config/symbiot/screens/screens.json` (next to each screen's PNG), where an
|
|
419
|
-
agent or script can read it.
|
|
510
|
+
agent or script can read it. Screenshots and that file are readable by you only.
|
|
511
|
+
|
|
512
|
+
**A web page maps itself.** Type a site next to **Map page** (`gmail`,
|
|
513
|
+
`github.com/pulls` or a full address) and Symbiot opens it in a hidden (headless)
|
|
514
|
+
browser, takes its screenshot and marks every button, link and field on it by
|
|
515
|
+
itself, named from the page (`Compose`, `Search mail`, …) with what each one is
|
|
516
|
+
and a CSS selector to find it again. Nothing to bring to the front, nothing to
|
|
517
|
+
drag. Only what you could click right now counts: anything hidden, below the
|
|
518
|
+
fold or covered by something else is left out. **Press** on a region clicks it
|
|
519
|
+
in that hidden browser and maps the page it leads to as a new screen, so map,
|
|
520
|
+
press, map is how an agent finds its way around a site. **Type** on a field types
|
|
521
|
+
your text into it there (replacing what was in it) and, with **Type, then Enter**
|
|
522
|
+
ticked, presses Enter, which is how a search or a one-line form is sent; the
|
|
523
|
+
result is mapped as a new screen too. The hidden browser stays open for five
|
|
524
|
+
minutes after each map, press or type, so a press or type on the screen it just
|
|
525
|
+
mapped carries on from that page as it is: type into a field without Enter, then
|
|
526
|
+
press the form's own button (**Send**, **Next**) on the new screen. On an older
|
|
527
|
+
screen, or once the browser has closed, it opens the page from its address again,
|
|
528
|
+
and anything typed there is gone. Press and
|
|
529
|
+
Type act on the real site, signed in as you, so they ask first, unless the site
|
|
530
|
+
is under **Trusted sites** in Settings: there, they go ahead without asking, for
|
|
531
|
+
you and for agents. A trusted site covers its subdomains (`google.com` covers
|
|
532
|
+
`mail.google.com`), and only you add sites, in Settings (there's no command for
|
|
533
|
+
an agent to do it). The hidden browser is your
|
|
534
|
+
Chrome, Chromium, Edge or Brave, with a profile of its own
|
|
535
|
+
(`~/.config/symbiot/browser`, separate from your everyday one). For a site behind
|
|
536
|
+
a sign-in, **Sign in** opens it there as a normal window. Sign in once, close the
|
|
537
|
+
window, and later maps are signed in. When a map lands on a sign-in page, Symbiot
|
|
538
|
+
says so. It works for web pages only. A desktop app still needs **Capture screen**.
|
|
539
|
+
|
|
540
|
+
From a terminal, or for your coding agent, the same thing prints JSON:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
symbiot screens map gmail # the screen's id, and each region's id, label, kind, centre and selector
|
|
544
|
+
symbiot screens press <id> Compose --yes # press a region (by id or label), map where it lands
|
|
545
|
+
symbiot screens type <id> "Search mail" "invoice" --enter --yes # type into a field, press Enter, map the result
|
|
546
|
+
# (--yes isn't needed on a site under Trusted sites in Settings)
|
|
547
|
+
symbiot screens type <id> To "sam@example.com" --yes # without --enter: the text stays in the field...
|
|
548
|
+
symbiot screens press <new id> Send --yes # ...for a press on the screen that just printed
|
|
549
|
+
symbiot screens show <id> # a saved screen's blueprint
|
|
550
|
+
symbiot screens signin gmail # sign in once, in Symbiot's browser window
|
|
551
|
+
symbiot screens # list them
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
While the app is running, these commands use its hidden browser, so it stays
|
|
555
|
+
open between them as it does in the app. Without the app, each command opens the
|
|
556
|
+
browser and closes it when done.
|
|
420
557
|
|
|
421
558
|
**More than one display?** Symbiot reads how your displays are laid out
|
|
422
559
|
(`cosmic-randr`, `wlr-randr`, `kscreen-doctor` or `xrandr` on Linux, PowerShell on
|
|
@@ -435,7 +572,7 @@ on your real screen. It asks you to confirm every time, and can wait 3, 5 or 10
|
|
|
435
572
|
seconds first so you can bring the right window to the front. It clicks with
|
|
436
573
|
`cliclick` on macOS (`brew install cliclick`), with PowerShell on Windows (nothing to
|
|
437
574
|
install), and on Linux with `xdotool` on X11 or `ydotool` on Wayland (which needs
|
|
438
|
-
access to `/dev/uinput`, and `ydotoold` running for ydotool 1.x). Nothing types yet.
|
|
575
|
+
access to `/dev/uinput`, and `ydotoold` running for ydotool 1.x). Nothing types on your real screen yet (Type works on a mapped web page only).
|
|
439
576
|
|
|
440
577
|
Capture uses the screenshot tool your system has: `screencapture` on macOS,
|
|
441
578
|
PowerShell on Windows, and on Linux `gnome-screenshot`, `spectacle`,
|
package/core.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from "node:fs";
|
|
7
7
|
|
|
8
8
|
// ---- config ---------------------------------------------------------------
|
|
9
9
|
const CONFIG_DIR = join(homedir(), ".config", "symbiot");
|
|
@@ -31,6 +31,27 @@ function sh(cmd) {
|
|
|
31
31
|
}
|
|
32
32
|
// "Is this command available?": command -v on posix, where on win.
|
|
33
33
|
function hasCmd(cmd) { try { return !!sh(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`).trim(); } catch { return false; } }
|
|
34
|
+
// A Chromium-family browser, per OS: the app's chrome-less --app window, and
|
|
35
|
+
// Screens' hidden (headless) browser.
|
|
36
|
+
function chromeBinary() {
|
|
37
|
+
const p = process.platform;
|
|
38
|
+
const exists = (f) => { try { return existsSync(f) ? f : null; } catch { return null; } };
|
|
39
|
+
if (p === "darwin") {
|
|
40
|
+
return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
41
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
42
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
43
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"].map(exists).find(Boolean) || null;
|
|
44
|
+
}
|
|
45
|
+
if (p === "win32") {
|
|
46
|
+
const bases = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
|
|
47
|
+
const rels = ["Google\\Chrome\\Application\\chrome.exe", "Chromium\\Application\\chrome.exe",
|
|
48
|
+
"Microsoft\\Edge\\Application\\msedge.exe", "BraveSoftware\\Brave-Browser\\Application\\brave.exe"];
|
|
49
|
+
for (const base of bases) for (const r of rels) { const f = exists(join(base, r)); if (f) return f; }
|
|
50
|
+
const w = sh("where chrome 2>NUL").split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
51
|
+
return w || null;
|
|
52
|
+
}
|
|
53
|
+
return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "microsoft-edge"].find(hasCmd) || null;
|
|
54
|
+
}
|
|
34
55
|
// Working-tree state, incl. detecting a stale/old checkout (not new work).
|
|
35
56
|
function repoState(repoPath) {
|
|
36
57
|
const branch = sh(`git -C ${JSON.stringify(repoPath)} rev-parse --abbrev-ref HEAD 2>/dev/null`).trim();
|
|
@@ -50,4 +71,4 @@ function repoState(repoPath) {
|
|
|
50
71
|
return { branch, dirty, del, mod, add, stale, staleBy, behind };
|
|
51
72
|
}
|
|
52
73
|
|
|
53
|
-
export { CONFIG_DIR, CONFIG_PATH, loadConfig, saveConfig, loadTasks, saveTasks, sh, hasCmd, repoState };
|
|
74
|
+
export { CONFIG_DIR, CONFIG_PATH, loadConfig, saveConfig, loadTasks, saveTasks, sh, hasCmd, chromeBinary, repoState };
|
package/desktop.mjs
CHANGED
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, chmodSync } from "node:fs";
|
|
11
11
|
import { CONFIG_DIR, loadConfig, saveConfig, hasCmd } from "./core.mjs";
|
|
12
12
|
|
|
13
|
+
// The Android app (android/) runs this same code with SYMBIOT_ANDROID_APP=1. Its
|
|
14
|
+
// Java side posts the notifications and starts Symbiot at boot, so here it's an
|
|
15
|
+
// OS of its own: "android-app" (Termux on Android is plain "android").
|
|
16
|
+
const OS = process.env.SYMBIOT_ANDROID_APP === "1" ? "android-app" : process.platform;
|
|
17
|
+
|
|
13
18
|
// ---- weekly write-up --------------------------------------------------------
|
|
14
19
|
// config.weekly = { on, day (0 = Sunday … 6 = Saturday), hour (0-23), last (ms) }.
|
|
15
20
|
// Each run saves the write-up to ~/.config/symbiot/weeks/<date>.md.
|
|
@@ -73,30 +78,39 @@ function startWeekly(produce) {
|
|
|
73
78
|
|
|
74
79
|
// ---- desktop notification (no dependencies) ---------------------------------
|
|
75
80
|
// [cmd, args] for this OS, or null when there's nothing to notify with.
|
|
76
|
-
function notifyCmd(title, body, platform =
|
|
81
|
+
function notifyCmd(title, body, platform = OS) {
|
|
77
82
|
if (platform === "darwin") return ["osascript", ["-e", `display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}`]];
|
|
78
83
|
if (platform === "win32") {
|
|
79
84
|
const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
|
|
80
85
|
return ["powershell", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", `Add-Type -AssemblyName System.Windows.Forms; $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(10000, ${q(title)}, ${q(body)}, 'Info'); Start-Sleep -Seconds 11; $n.Dispose()`]];
|
|
81
86
|
}
|
|
87
|
+
// Android (Termux): termux-notification, from the termux-api package + the Termux:API app
|
|
88
|
+
if (platform === "android") return hasCmd("termux-notification") ? ["termux-notification", ["--id", "symbiot", "--title", title, "--content", body]] : null;
|
|
82
89
|
return hasCmd("notify-send") ? ["notify-send", ["--app-name=Symbiot", title, body]] : null;
|
|
83
90
|
}
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
// In the app: a line in a file its service watches, and it posts the notification.
|
|
92
|
+
function desktopNotify(title, body, platform = OS, dir = CONFIG_DIR) {
|
|
93
|
+
if (platform === "android-app") { try { mkdirSync(dir, { recursive: true }); appendFileSync(join(dir, "android-notify.jsonl"), JSON.stringify({ title, body }) + "\n"); return true; } catch { return false; } }
|
|
94
|
+
const c = notifyCmd(title, body, platform); if (!c) return false;
|
|
86
95
|
try { const ch = spawn(c[0], c[1], { detached: true, stdio: "ignore" }); ch.on("error", () => {}); ch.unref(); return true; } catch { return false; }
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
// ---- start at login -----------------------------------------------------------
|
|
90
99
|
// One file per OS, in the place that OS starts things from at login: an XDG
|
|
91
|
-
// autostart entry (Linux), a LaunchAgent (macOS), the Startup folder (Windows)
|
|
100
|
+
// autostart entry (Linux), a LaunchAgent (macOS), the Startup folder (Windows),
|
|
101
|
+
// a Termux:Boot script (Android, run when the phone starts). The Android app
|
|
102
|
+
// starts itself at boot while its flag file is there.
|
|
92
103
|
// It runs `symbiot app` with SYMBIOT_NO_OPEN=1, so no window pops up. PATH is
|
|
93
104
|
// saved too, so agents and git resolve as they do in your terminal.
|
|
94
|
-
function autostartFile(platform =
|
|
105
|
+
function autostartFile(platform = OS, home = homedir()) {
|
|
106
|
+
if (platform === "android-app") return join(home, ".config", "symbiot", "android-boot");
|
|
95
107
|
if (platform === "darwin") return join(home, "Library", "LaunchAgents", "co.symbiot.app.plist");
|
|
96
108
|
if (platform === "win32") return join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "symbiot.cmd");
|
|
109
|
+
if (platform === "android") return join(home, ".termux", "boot", "symbiot");
|
|
97
110
|
return join(home, ".config", "autostart", "symbiot.desktop");
|
|
98
111
|
}
|
|
99
|
-
function autostartContent(node, script, platform =
|
|
112
|
+
function autostartContent(node, script, platform = OS, path = process.env.PATH || "") {
|
|
113
|
+
if (platform === "android-app") return "While this file exists, Symbiot's Android app starts itself when the phone starts.\n";
|
|
100
114
|
if (platform === "darwin") {
|
|
101
115
|
const x = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
102
116
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -109,13 +123,19 @@ function autostartContent(node, script, platform = process.platform, path = proc
|
|
|
109
123
|
</dict></plist>
|
|
110
124
|
`;
|
|
111
125
|
}
|
|
126
|
+
if (platform === "android") {
|
|
127
|
+
// sh single quotes; the wake lock stops Android from stopping Termux while Symbiot runs
|
|
128
|
+
const sq = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
129
|
+
return ["#!/data/data/com.termux/files/usr/bin/sh", "command -v termux-wake-lock >/dev/null && termux-wake-lock",
|
|
130
|
+
"export SYMBIOT_NO_OPEN=1 PATH=" + sq(path), "exec " + sq(node) + " " + sq(script) + " app", ""].join("\n");
|
|
131
|
+
}
|
|
112
132
|
if (platform === "win32") return ["@echo off", "set SYMBIOT_NO_OPEN=1", `start "Symbiot" /min "${node}" "${script}" app`, ""].join("\r\n");
|
|
113
133
|
// Desktop Entry Exec quoting: double quotes, with " ` $ \ backslash-escaped
|
|
114
134
|
const q = (s) => '"' + String(s).replace(/(["`$\\])/g, "\\$1") + '"';
|
|
115
135
|
return ["[Desktop Entry]", "Type=Application", "Name=Symbiot", "Comment=Symbiot in the background: the weekly write-up and its notification",
|
|
116
136
|
`Exec=env SYMBIOT_NO_OPEN=1 ${q("PATH=" + path)} ${q(node)} ${q(script)} app`, "Terminal=false", "NoDisplay=true", "X-GNOME-Autostart-enabled=true", ""].join("\n");
|
|
117
137
|
}
|
|
118
|
-
function autostartState() { const file = autostartFile(); return { on: existsSync(file), file }; }
|
|
138
|
+
function autostartState() { const file = autostartFile(); return { on: existsSync(file), file, ...(OS === "android-app" ? { phone: "app" } : OS === "android" ? { phone: "termux" } : {}) }; }
|
|
119
139
|
// `script` is index.mjs's real path. From npx that's a cache folder that goes
|
|
120
140
|
// away, so it has to be installed for this to keep working.
|
|
121
141
|
function setAutostart(on, script) {
|
|
@@ -125,6 +145,7 @@ function setAutostart(on, script) {
|
|
|
125
145
|
if (/[\\/]_npx[\\/]/.test(String(script))) return { ...autostartState(), error: "Symbiot is running from npx. Install it first (npm install -g symbiot), then switch this on." };
|
|
126
146
|
mkdirSync(dirname(file), { recursive: true });
|
|
127
147
|
writeFileSync(file, autostartContent(process.execPath, script));
|
|
148
|
+
if (OS === "android") chmodSync(file, 0o700); // Termux:Boot only runs executable scripts
|
|
128
149
|
return autostartState();
|
|
129
150
|
} catch (e) { return { ...autostartState(), error: String((e && e.message) || e) }; }
|
|
130
151
|
}
|