symbiot 0.57.8 → 0.57.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.57.9
6
+
7
+ - **On Windows, Symbiot is in the Start menu and on your desktop.** Installing puts a Symbiot shortcut with the orb in both (your real desktop, even when it's in OneDrive), and opens Symbiot right away, into setup. The shortcuts start it through a small hidden script, so no black console window flashes up; `symbiot uninstall` takes them away again. Installing on a Windows desktop now opens Symbiot too, and says so in the terminal.
8
+
5
9
  ## 0.57.8
6
10
 
7
11
  - **An approved deploy with your server key goes through.** The membrane read `scp -i ~/.ssh/oracle_key …` as copying your key, when the key is only what scp signs in with (`ssh -i` already passed). So argena's deploy to the game server stayed blocked even after you approved it twice. Now scp's sign-in options don't count as files it copies. Copying a key or credentials themselves (`scp ~/.ssh/id_rsa host:`) is still stopped.
package/desktop.mjs CHANGED
@@ -5,10 +5,10 @@
5
5
  // log in, so the write-up still happens on days you never open Symbiot.
6
6
  // `symbiot app` then finds that running copy and opens its window.
7
7
  import { fileURLToPath } from "node:url";
8
- import { spawn } from "node:child_process";
8
+ import { spawn, spawnSync } from "node:child_process";
9
9
  import { homedir } from "node:os";
10
10
  import { join, dirname } from "node:path";
11
- import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, chmodSync } from "node:fs";
11
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync, readdirSync, statSync, chmodSync, rmdirSync } from "node:fs";
12
12
  import { CONFIG_DIR, loadConfig, saveConfig, hasCmd } from "./core.mjs";
13
13
 
14
14
  // The Android app (android/) runs this same code with SYMBIOT_ANDROID_APP=1. Its
@@ -191,9 +191,54 @@ function launcherPath(path, node) {
191
191
  for (const d of [...String(path || "").split(":"), dirname(node)]) if (d && !/node_modules[\\/]\.bin|node-gyp-bin/.test(d) && !seen.has(d)) { seen.add(d); out.push(d); }
192
192
  return out.join(":");
193
193
  }
194
+ // ---- Windows: the Start menu and the desktop -------------------------------------
195
+ // A shortcut in the Start menu and one on the desktop, with the orb (icon.ico). Each
196
+ // runs a small script through wscript (open.vbs), which starts `symbiot open` with
197
+ // its window hidden: node is a console program, and run straight from a shortcut it
198
+ // would flash a black window.
199
+ // Windows' own APPDATA (when it's in this home); the desktop as Windows reports it
200
+ // (often in OneDrive), kept in desktop.txt so uninstalling finds that shortcut again.
201
+ const winDirs = (home = homedir()) => {
202
+ const env = process.env.APPDATA || "", appdata = env && env.toLowerCase().startsWith(home.toLowerCase()) ? env : join(home, "AppData", "Roaming"), own = join(appdata, "Symbiot");
203
+ let desk = ""; try { desk = readFileSync(join(own, "desktop.txt"), "utf8").trim(); } catch {}
204
+ return { own, menu: join(appdata, "Microsoft", "Windows", "Start Menu", "Programs", "Symbiot.lnk"), desk: desk || join(home, "Desktop", "Symbiot.lnk") };
205
+ };
206
+ const ICO_SRC = fileURLToPath(new URL("./icon.ico", import.meta.url));
207
+ // VBScript: "" is a quote inside a string
208
+ const vbsContent = (node, script) => `' Symbiot: start it (or show it) with no console window\r\nCreateObject("WScript.Shell").Run """${node.replace(/"/g, '""')}"" ""${script.replace(/"/g, '""')}"" open", 0, False\r\n`;
209
+ // PowerShell that makes one shortcut: single quotes, '' is a quote inside one
210
+ function shortcutPs(lnk, vbs, ico) {
211
+ const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
212
+ return `$s = (New-Object -ComObject WScript.Shell).CreateShortcut(${q(lnk)}); $s.TargetPath = (Join-Path $env:WINDIR 'System32\\wscript.exe'); $s.Arguments = ${q('"' + vbs + '"')}; $s.IconLocation = ${q(ico + ",0")}; $s.Description = 'Symbiot'; $s.WorkingDirectory = $env:USERPROFILE; $s.Save()`;
213
+ }
214
+ function installWindows({ node, script, home, run }) {
215
+ const d = winDirs(home), vbs = join(d.own, "open.vbs"), ico = join(d.own, "symbiot.ico"), want = vbsContent(node, script);
216
+ let wrote = false;
217
+ try {
218
+ mkdirSync(d.own, { recursive: true });
219
+ if (!existsSync(join(d.own, "desktop.txt")) && home === homedir()) {
220
+ const r = run("powershell", ["-NoProfile", "-NonInteractive", "-Command", "[Environment]::GetFolderPath('Desktop')"]);
221
+ const dir = r && r.status === 0 ? String(r.stdout || "").trim() : "";
222
+ if (dir) { d.desk = join(dir, "Symbiot.lnk"); writeFileSync(join(d.own, "desktop.txt"), d.desk); }
223
+ }
224
+ let had = ""; try { had = readFileSync(vbs, "utf8"); } catch {}
225
+ if (had !== want) { writeFileSync(vbs, want); wrote = true; }
226
+ if (!existsSync(ico)) { writeFileSync(ico, readFileSync(ICO_SRC)); wrote = true; }
227
+ for (const lnk of [d.menu, d.desk]) {
228
+ if (!wrote && existsSync(lnk)) continue;
229
+ try { mkdirSync(join(lnk, ".."), { recursive: true }); } catch {}
230
+ const r = run("powershell", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", shortcutPs(lnk, vbs, ico)]);
231
+ if (r && r.status !== 0) return { error: String((r.stderr || r.stdout || "") || "PowerShell couldn't make the shortcut").trim().slice(0, 300) };
232
+ wrote = true;
233
+ }
234
+ } catch (e) { return { error: (e && e.message) || String(e) }; }
235
+ return { written: wrote, file: d.menu, desktop: d.desk };
236
+ }
194
237
  // Writes the entry and icon when they're missing or differ. { written, file } or { skipped }.
195
- function installLauncher({ node = process.execPath, script, home = homedir(), platform = OS, path = process.env.PATH || "" } = {}) {
196
- if (platform !== "linux" || !script) return { skipped: true };
238
+ function installLauncher({ node = process.execPath, script, home = homedir(), platform = OS, path = process.env.PATH || "", run = (c, a) => spawnSync(c, a, { encoding: "utf8", windowsHide: true, timeout: 30000 }) } = {}) {
239
+ if (!script) return { skipped: true };
240
+ if (platform === "win32") return installWindows({ node, script, home, run });
241
+ if (platform !== "linux") return { skipped: true };
197
242
  const file = launcherFile(home), icon = launcherIcon(home), want = launcherContent(node, script, launcherPath(path, node));
198
243
  let wrote = false;
199
244
  try {
@@ -207,8 +252,9 @@ function installLauncher({ node = process.execPath, script, home = homedir(), pl
207
252
  return { written: wrote, file };
208
253
  }
209
254
  function removeLauncher(home = homedir()) {
210
- const gone = [];
211
- for (const f of [launcherFile(home), launcherIcon(home)]) { try { if (existsSync(f)) { unlinkSync(f); gone.push(f); } } catch {} }
255
+ const gone = [], d = winDirs(home);
256
+ for (const f of [launcherFile(home), launcherIcon(home), d.menu, d.desk, join(d.own, "open.vbs"), join(d.own, "symbiot.ico"), join(d.own, "desktop.txt")]) { try { if (existsSync(f)) { unlinkSync(f); gone.push(f); } } catch {} }
257
+ try { rmdirSync(d.own); } catch {}
212
258
  return gone;
213
259
  }
214
260
  function autostartState() { const file = autostartFile(); return { on: existsSync(file), file, ...(OS === "android-app" ? { phone: "app" } : OS === "android" ? { phone: "termux" } : {}) }; }
@@ -226,4 +272,4 @@ function setAutostart(on, script) {
226
272
  } catch (e) { return { ...autostartState(), error: String((e && e.message) || e) }; }
227
273
  }
228
274
 
229
- export { iconSvg, setLauncherLook, launcherPath, launcherFile, launcherIcon, launcherContent, installLauncher, removeLauncher, weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
275
+ export { winDirs, vbsContent, shortcutPs, iconSvg, setLauncherLook, launcherPath, launcherFile, launcherIcon, launcherContent, installLauncher, removeLauncher, weeklyCfg, lastSlot, weeklyDue, setWeekly, weeklyState, latestWeek, runWeekly, startWeekly, notifyCmd, desktopNotify, autostartFile, autostartContent, autostartState, setAutostart };
package/icon.ico ADDED
Binary file
package/index.mjs CHANGED
@@ -360,7 +360,7 @@ async function cmdOpen() {
360
360
  if (p && p.version) { openApp(`http://127.0.0.1:${port}/?t=${cfg.appToken}`); return; }
361
361
  let fd = "ignore"; try { mkdirSync(CONFIG_DIR, { recursive: true }); fd = openSync(join(CONFIG_DIR, "app.log"), "a"); } catch {}
362
362
  const env = { ...process.env }; delete env.SYMBIOT_NO_OPEN;
363
- spawn(process.execPath, [realpathSync(fileURLToPath(import.meta.url)), "app"], { detached: true, stdio: ["ignore", fd, fd], env }).unref();
363
+ spawn(process.execPath, [realpathSync(fileURLToPath(import.meta.url)), "app"], { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true }).unref();
364
364
  }
365
365
  // symbiot uninstall: everything Symbiot put on this computer, then the program.
366
366
  // --keep-data keeps ~/.config/symbiot (settings, tasks, memory); --yes doesn't ask.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.57.8",
3
+ "version": "0.57.9",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,7 +48,8 @@
48
48
  "postinstall.mjs",
49
49
  "icon.svg",
50
50
  "scanworker.mjs",
51
- "estimate.mjs"
51
+ "estimate.mjs",
52
+ "icon.ico"
52
53
  ],
53
54
  "engines": {
54
55
  "node": ">=18"
package/postinstall.mjs CHANGED
@@ -1,19 +1,22 @@
1
- // npm install -g symbiot runs this: Symbiot in your app menu, with its icon, so you
2
- // never need a terminal to start it (desktop.mjs installLauncher), and then it opens,
3
- // so setup starts right away. Only for a global install on a desktop, and it never
4
- // fails the install. npm hides what install scripts print, so the line goes straight
5
- // to your terminal.
1
+ // npm install -g symbiot runs this: Symbiot where you start apps (the app menu on
2
+ // Linux, the Start menu and the desktop on Windows), with its icon, so you never need
3
+ // a terminal to start it (desktop.mjs installLauncher), and then it opens, so setup
4
+ // starts right away. Only for a global install on a desktop, and it never fails the
5
+ // install. npm hides what install scripts print, so the line goes straight to your
6
+ // terminal (its console on Windows).
6
7
  import { realpathSync, writeFileSync } from "node:fs";
7
8
  import { spawn } from "node:child_process";
8
9
  import { fileURLToPath } from "node:url";
9
- const say = (s) => { try { writeFileSync("/dev/tty", s + "\n"); } catch { console.log(s); } };
10
+ const WIN = process.platform === "win32";
11
+ const say = (s) => { try { writeFileSync(WIN ? "CONOUT$" : "/dev/tty", s + "\n"); } catch { console.log(s); } };
10
12
  try {
11
13
  if (process.env.npm_config_global === "true" && !process.env.SYMBIOT_NO_LAUNCHER) {
12
14
  const script = realpathSync(fileURLToPath(new URL("./index.mjs", import.meta.url)));
13
15
  const { installLauncher } = await import("./desktop.mjs");
14
16
  const r = installLauncher({ script });
15
- const desktop = !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY) && !process.env.CI && !process.env.SYMBIOT_NO_OPEN;
16
- if (desktop) { spawn(process.execPath, [script, "open"], { detached: true, stdio: "ignore" }).unref(); say("\nSymbiot is installed and opening now. Next time, open it from your app menu: no terminal needed.\n"); }
17
- else if (r.file) say("\nSymbiot is in your app menu: open it from there, no terminal needed.\n");
17
+ const desktop = (WIN || process.platform === "darwin" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) && !process.env.CI && !process.env.SYMBIOT_NO_OPEN;
18
+ const where = WIN ? "the Start menu or your desktop" : "your app menu";
19
+ if (desktop) { spawn(process.execPath, [script, "open"], { detached: true, stdio: "ignore", windowsHide: true }).unref(); say(`\nSymbiot is installed and opening now. Next time, open it from ${where}: no terminal needed.\n`); }
20
+ else if (r.file) say(`\nSymbiot is in ${where}: open it from there, no terminal needed.\n`);
18
21
  }
19
22
  } catch {}