spi-agent-gui 0.1.0 → 0.1.2

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
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - macOS Apple Silicon/Intel candidates remain separate from public Linux releases. Developer ID signing and notarization are not configured.
6
+
7
+ ## 0.1.2
8
+
9
+ - Linux x64/arm64 release containing the actual new editable buffer UI (not only Pi's native queue-count status).
10
+ - User-resizable persisted sidebar split, active directory/recent session highlighting and display-only home-path abbreviation.
11
+ - Main-process editable/cancellable outbox with fixed composer overlay, images, follow-up tail appends / newest-steer head insertion, Stop pause/resume and single-use submission/revision protection.
12
+ - Replace sPi permission modes/rule editor with a session-local YOLO checkbox: on automatically replies Allow once to compatible Pi tool approvals; off delegates to Pi without another sPi prompt. Legacy sPi permission files are preserved but no longer applied. Generic dialogs/direct blocks and Stop/close guards remain effective.
13
+ - Secondary processes exit before readiness; owning-window app quit uses the same confirmation/cleanup controller. Runtime sanitization preserves native helper modes and safe relative framework links.
14
+ - Published 0.1.0/0.1.1 assets remain unchanged. This public release is Linux-only; macOS/Windows public distribution remains deferred.
15
+
16
+ ## 0.1.1
17
+
18
+ - Original female-spy silhouette icon for the application window, favicon and platform packages.
19
+ - Owned Linux desktop entry for default personal installs, with icon refresh on update/rollback and conservative uninstall.
20
+ - Preserve unknown/modified desktop entries and refuse symlink directories; validate desktop Exec escaping.
21
+
3
22
  ## 0.1.0
4
23
 
5
24
  - Initial Pi launcher package and verified native desktop archive installer.
package/dist/spi.cjs CHANGED
@@ -11,7 +11,7 @@ var __commonJS = (cb, mod) => function __require() {
11
11
  var define_SPI_RELEASE_default;
12
12
  var init_define_SPI_RELEASE = __esm({
13
13
  "<define:__SPI_RELEASE__>"() {
14
- define_SPI_RELEASE_default = { version: "0.1.0", repository: "DiamondGo/spi", packageName: "spi-agent-gui" };
14
+ define_SPI_RELEASE_default = { version: "0.1.2", repository: "DiamondGo/spi", packageName: "spi-agent-gui" };
15
15
  }
16
16
  });
17
17
 
@@ -4608,12 +4608,81 @@ var require_yauzl = __commonJS({
4608
4608
  }
4609
4609
  });
4610
4610
 
4611
+ // packages/pi-spi/lib/desktop.cjs
4612
+ var require_desktop = __commonJS({
4613
+ "packages/pi-spi/lib/desktop.cjs"(exports2, module2) {
4614
+ init_define_SPI_RELEASE();
4615
+ var fs2 = require("node:fs");
4616
+ var path2 = require("node:path");
4617
+ var crypto2 = require("node:crypto");
4618
+ var digest = (file) => crypto2.createHash("sha256").update(fs2.readFileSync(file)).digest("hex");
4619
+ function exists(file) {
4620
+ try {
4621
+ fs2.lstatSync(file);
4622
+ return true;
4623
+ } catch (error) {
4624
+ if (error.code === "ENOENT") return false;
4625
+ throw error;
4626
+ }
4627
+ }
4628
+ function safeParents(directory) {
4629
+ for (let dir = directory; ; dir = path2.dirname(dir)) {
4630
+ if (exists(dir) && (!fs2.lstatSync(dir).isDirectory() || fs2.lstatSync(dir).isSymbolicLink())) throw Error("Refusing symlink desktop directory");
4631
+ if (dir === path2.dirname(dir)) break;
4632
+ }
4633
+ }
4634
+ function assertDesktopOwned2(record) {
4635
+ if (!record) return;
4636
+ if (!path2.isAbsolute(record.path) || path2.basename(record.path) !== "spi.desktop") throw Error("Invalid desktop entry record");
4637
+ safeParents(path2.dirname(record.path));
4638
+ if (exists(record.path) && (fs2.lstatSync(record.path).isSymbolicLink() || !fs2.lstatSync(record.path).isFile() || digest(record.path) !== record.sha256)) throw Error("Refusing modified desktop entry");
4639
+ }
4640
+ function removeDesktopEntry2(record) {
4641
+ assertDesktopOwned2(record);
4642
+ if (record) fs2.rmSync(record.path, { force: true });
4643
+ }
4644
+ var value = (s) => s.replaceAll("\\", "\\\\").replaceAll("\n", "\\n").replaceAll("\r", "\\r").replaceAll(" ", "\\t");
4645
+ function desktopText(command, icon) {
4646
+ if (/[\x00-\x1f\x7f]/.test(command + icon)) throw Error("Unsupported control character in desktop path");
4647
+ const argument = '"' + command.replace(/[\\"`$]/g, "\\$&").replaceAll("%", "%%") + '"';
4648
+ return "[Desktop Entry]\nType=Application\nName=sPi\nComment=Pi Coding Agent GUI\nExec=" + value(argument) + "\nIcon=" + value(icon) + "\nTerminal=false\nCategories=Development;\nStartupWMClass=spi\nStartupNotify=true\n";
4649
+ }
4650
+ function syncDesktopEntry2({ enabled, dataHome, root, command, entryPath, previous }) {
4651
+ if (!enabled) return previous;
4652
+ const icon = path2.join(path2.dirname(entryPath), "resources", "icon.png");
4653
+ if (!exists(icon)) {
4654
+ if (previous) removeDesktopEntry2(previous);
4655
+ return void 0;
4656
+ }
4657
+ if (!path2.isAbsolute(dataHome)) throw Error("XDG data home must be absolute");
4658
+ const directory = path2.join(dataHome, "applications"), file = path2.join(directory, "spi.desktop");
4659
+ safeParents(directory);
4660
+ if (exists(file)) {
4661
+ if (!previous || previous.path !== file) throw Error("Refusing unowned desktop entry");
4662
+ assertDesktopOwned2(previous);
4663
+ }
4664
+ if (previous && previous.path !== file) throw Error("Desktop location changed; existing entry was preserved");
4665
+ if (fs2.lstatSync(icon).isSymbolicLink() || !fs2.lstatSync(icon).isFile() || !fs2.realpathSync(icon).startsWith(fs2.realpathSync(root) + path2.sep)) throw Error("Invalid application icon");
4666
+ const text = desktopText(command, icon), temp = file + "." + crypto2.randomUUID() + ".tmp";
4667
+ fs2.mkdirSync(directory, { recursive: true });
4668
+ try {
4669
+ fs2.writeFileSync(temp, text, { mode: 420, flag: "wx" });
4670
+ fs2.renameSync(temp, file);
4671
+ } finally {
4672
+ fs2.rmSync(temp, { force: true });
4673
+ }
4674
+ return { path: file, sha256: digest(file) };
4675
+ }
4676
+ module2.exports = { syncDesktopEntry: syncDesktopEntry2, assertDesktopOwned: assertDesktopOwned2, removeDesktopEntry: removeDesktopEntry2, desktopText };
4677
+ }
4678
+ });
4679
+
4611
4680
  // package.json
4612
4681
  var require_package = __commonJS({
4613
4682
  "package.json"(exports2, module2) {
4614
4683
  module2.exports = {
4615
4684
  name: "spi",
4616
- version: "0.1.0",
4685
+ version: "0.1.2",
4617
4686
  description: "sPi - Simple Pi Agent GUI",
4618
4687
  private: true,
4619
4688
  license: "UNLICENSED",
@@ -4626,6 +4695,9 @@ var require_package = __commonJS({
4626
4695
  dev: "electron-vite dev",
4627
4696
  postinstall: "electron-builder install-app-deps",
4628
4697
  build: "electron-vite build",
4698
+ "build:icons": "node scripts/icons.cjs",
4699
+ "test:icons": "node scripts/icons.test.cjs",
4700
+ "test:native-build": "node scripts/native-build.test.cjs && node scripts/macos-integrity.test.cjs",
4629
4701
  start: "electron-vite preview",
4630
4702
  dist: "node scripts/build-portable.cjs",
4631
4703
  "dist:linux": "node scripts/build-portable.cjs",
@@ -4643,8 +4715,12 @@ var require_package = __commonJS({
4643
4715
  "test:terminal-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --terminal",
4644
4716
  "test:settings": "node scripts/settings.test.cjs",
4645
4717
  "test:settings-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --settings",
4646
- "test:permissions": "node scripts/permissions.test.cjs",
4647
- "test:permission-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --permissions",
4718
+ "test:yolo": "node scripts/yolo.test.cjs && node scripts/session-bridge.test.cjs",
4719
+ "test:extension-ui": "node scripts/extension-ui.test.cjs",
4720
+ "test:workspace-ui": "node scripts/workspace-ui.test.cjs",
4721
+ "test:workspace-ui-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --workspace-ui",
4722
+ "test:extension-ui-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --extension-ui",
4723
+ "test:yolo-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --yolo",
4648
4724
  "test:smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs",
4649
4725
  "test:advanced": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --advanced"
4650
4726
  },
@@ -4657,6 +4733,7 @@ var require_package = __commonJS({
4657
4733
  },
4658
4734
  devDependencies: {
4659
4735
  "@electron/asar": "^3.4.1",
4736
+ "@resvg/resvg-js": "2.6.2",
4660
4737
  "@types/react": "^18.3.12",
4661
4738
  "@types/react-dom": "^18.3.1",
4662
4739
  "@vitejs/plugin-react": "^4.3.4",
@@ -4695,6 +4772,7 @@ var { spawn } = require("node:child_process");
4695
4772
  var { pipeline } = require("node:stream/promises");
4696
4773
  var tar = require_index_min();
4697
4774
  var yauzl = require_yauzl();
4775
+ var { syncDesktopEntry, assertDesktopOwned, removeDesktopEntry } = require_desktop();
4698
4776
  var BUILD = typeof define_SPI_RELEASE_default === "undefined" ? { version: require_package().version, repository: require_release_config().repository, packageName: require_release_config().npmPackage } : define_SPI_RELEASE_default;
4699
4777
  var OWNER = "sPi-installer-v1";
4700
4778
  var MAX_ARCHIVE = 700 * 1024 * 1024;
@@ -4709,7 +4787,7 @@ function version(v) {
4709
4787
  }
4710
4788
  function target() {
4711
4789
  const t = process.platform + "-" + process.arch;
4712
- if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64"].includes(t)) fail("Unsupported platform: " + t);
4790
+ if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64", "darwin-x64"].includes(t)) fail("Unsupported platform: " + t);
4713
4791
  if (process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime) fail("Linux requires glibc; musl is not supported");
4714
4792
  return t;
4715
4793
  }
@@ -4774,6 +4852,10 @@ function validateEntries(entries) {
4774
4852
  }
4775
4853
  }
4776
4854
  }
4855
+ function checkExtractedLinks(entries, dest) {
4856
+ const base = fs.realpathSync(dest) + path.sep;
4857
+ for (const e of entries) if (e.link && !fs.realpathSync(path.join(dest, e.name)).startsWith(base)) fail("Extracted link escapes destination");
4858
+ }
4777
4859
  async function extractTar(file, dest) {
4778
4860
  const entries = [];
4779
4861
  await tar.t({ file, onReadEntry: (e) => entries.push({ name: e.path, size: e.size, type: { File: "file", Directory: "directory", SymbolicLink: "symlink", Link: "hardlink" }[e.type] || "unsupported", link: e.linkpath }) });
@@ -4783,10 +4865,7 @@ async function extractTar(file, dest) {
4783
4865
  const f = path.join(dest, e.name);
4784
4866
  fs.chmodSync(f, fs.statSync(f).mode & 511);
4785
4867
  }
4786
- for (const e of entries) if (e.link) {
4787
- const resolved = fs.realpathSync(path.join(dest, e.name));
4788
- if (!resolved.startsWith(path.resolve(dest) + path.sep)) fail("Extracted link escapes destination");
4789
- }
4868
+ checkExtractedLinks(entries, dest);
4790
4869
  }
4791
4870
  async function zipEntries(file) {
4792
4871
  return new Promise((resolve, reject) => yauzl.open(file, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (error, zip) => {
@@ -4858,7 +4937,7 @@ async function extractZip(file, dest) {
4858
4937
  fs.mkdirSync(path.dirname(out), { recursive: true });
4859
4938
  fs.symlinkSync(e.link, out);
4860
4939
  }
4861
- for (const e of entries) if (e.type === "symlink" && !fs.realpathSync(path.join(dest, e.name)).startsWith(path.resolve(dest) + path.sep)) fail("Extracted link escapes destination");
4940
+ checkExtractedLinks(entries, dest);
4862
4941
  }
4863
4942
  function trustedURL(value) {
4864
4943
  const u = new URL(value);
@@ -4956,7 +5035,7 @@ function readCurrent(root) {
4956
5035
  if (readJSON(path.join(root, ".spi-owned.json")).owner !== OWNER) fail("Not an sPi install");
4957
5036
  const current = readJSON(path.join(root, "current.json"));
4958
5037
  version(current.version);
4959
- if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64"].includes(current.target)) fail("Invalid install target");
5038
+ if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64", "darwin-x64"].includes(current.target)) fail("Invalid install target");
4960
5039
  if (current.directory !== "releases/" + current.version + "/" + current.target) fail("Invalid install record");
4961
5040
  safeName(current.entry);
4962
5041
  safeName(current.permissionExtension);
@@ -5009,7 +5088,13 @@ function register(root, options2 = {}) {
5009
5088
  fs.renameSync(temp, dest);
5010
5089
  const shims = (oldState.shims || []).filter((s) => s.path !== dest);
5011
5090
  shims.push({ path: dest, sha256: hash(dest) });
5012
- atomicJSON(path.join(root, "install-state.json"), { owner: OWNER, shims, launcher: { sha256: hash(cli) } });
5091
+ let desktop = oldState.desktop;
5092
+ if (process.platform === "linux" && root === path.resolve(defaultRoot())) try {
5093
+ desktop = syncDesktopEntry({ enabled: true, dataHome: process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), root, command: dest, entryPath: cur.entryPath, previous: desktop });
5094
+ } catch (error) {
5095
+ console.warn("[sPi] Desktop entry unchanged: " + error.message);
5096
+ }
5097
+ atomicJSON(path.join(root, "install-state.json"), { owner: OWNER, shims, desktop, launcher: { sha256: hash(cli) } });
5013
5098
  console.log("sPi command: " + dest);
5014
5099
  if (!(process.env.PATH || "").split(path.delimiter).some((d) => path.resolve(d) === dir)) console.log("Add this directory to your PATH: " + dir + " (shell configuration was not changed)");
5015
5100
  }
@@ -5157,9 +5242,11 @@ async function uninstall(options2) {
5157
5242
  if (fs.existsSync(launcher) && fs.lstatSync(launcher).isSymbolicLink()) fail("Refusing symlink launcher");
5158
5243
  const cli = path.join(launcher, "spi.cjs");
5159
5244
  if (fs.existsSync(cli) && (!state.launcher || hash(cli) !== state.launcher.sha256 || fs.lstatSync(cli).isSymbolicLink())) fail("Refusing modified launcher");
5245
+ assertDesktopOwned(state.desktop);
5160
5246
  for (const s of state.shims || []) if (fs.existsSync(s.path)) {
5161
5247
  if (fs.lstatSync(s.path).isSymbolicLink() || hash(s.path) !== s.sha256) fail("Refusing to remove a modified command");
5162
5248
  }
5249
+ removeDesktopEntry(state.desktop);
5163
5250
  for (const s of state.shims || []) fs.rmSync(s.path, { force: true });
5164
5251
  for (const dir of owned) fs.rmSync(dir, { recursive: true, force: true });
5165
5252
  fs.rmSync(path.join(launcher, "spi.cjs"), { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spi-agent-gui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "sPi - Simple Pi Agent GUI installer and Pi launcher",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",