diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 67f9fbef..dc2c081b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -13,7 +13,6 @@ All PR titles must follow conventional commits with a **package scope**: | Scope | Meaning | | ---------------------- | ---------------------------------------------------------------- | | `[pi-package-name]` | Changes to `packages/[pi-package-name]` | -| `session-deck-desktop` | Changes to `apps/session-deck-desktop` | | `root` | Root-level changes (CI, workflows, configs, shared tooling) | | _omit scope_ | Changes affecting all packages equally | diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 54439934..93620489 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -2,10 +2,7 @@ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "packages": { "packages/pi-merge-ready": {}, - "packages/pi-session-deck": { - "draft": true, - "force-tag-creation": true - }, + "packages/pi-session-deck": {}, "packages/pi-openrouter": {}, "packages/pi-session-hygiene": {}, "packages/pi-spinner-verbs": {}, diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 94cde539..e8cc60e4 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -18,9 +18,6 @@ jobs: runs-on: ubuntu-latest outputs: paths_released: ${{ steps.release.outputs.paths_released }} - pi_session_deck_released: ${{ steps.release.outputs['packages/pi-session-deck--release_created'] }} - pi_session_deck_tag_name: ${{ steps.release.outputs['packages/pi-session-deck--tag_name'] }} - pi_session_deck_version: ${{ steps.release.outputs['packages/pi-session-deck--version'] }} steps: - uses: googleapis/release-please-action@v4 id: release @@ -56,203 +53,11 @@ jobs: if: steps.release.outputs.releases_created == 'true' run: pnpm -r --filter './packages/*' --if-present build - - name: Publish changed packages except pi-session-deck + - name: Publish changed packages if: steps.release.outputs.releases_created == 'true' run: | echo "Released packages: ${{ steps.release.outputs.paths_released }}" echo '${{ steps.release.outputs.paths_released }}' | jq -r '.[]' | while read -r pkg; do - if [ "$pkg" = "packages/pi-session-deck" ]; then - echo "Deferring $pkg until its desktop release is public" - continue - fi echo "Publishing $pkg" (cd "$pkg" && npm publish) done - - session-deck-desktop-build: - needs: release - if: needs.release.outputs.pi_session_deck_released == 'true' - name: Session Deck desktop (${{ matrix.arch }}) - strategy: - fail-fast: true - matrix: - include: - - runner: macos-15 - target: aarch64-apple-darwin - arch: arm64 - - runner: macos-15-intel - target: x86_64-apple-darwin - arch: x64 - runs-on: ${{ matrix.runner }} - permissions: - contents: read - steps: - - name: Checkout pi-session-deck release tag - uses: actions/checkout@v4 - with: - ref: ${{ needs.release.outputs.pi_session_deck_tag_name }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install native Rust target - run: rustup target add "${{ matrix.target }}" - - - name: Build and verify desktop app ZIP - run: | - pnpm --filter ./apps/session-deck-desktop artifact:macos \ - --version "${{ needs.release.outputs.pi_session_deck_version }}" \ - --target "${{ matrix.target }}" \ - --artifact-dir "dist/artifacts-${{ matrix.arch }}" - - - name: Stage architecture artifacts - uses: actions/upload-artifact@v4 - with: - name: session-deck-desktop-${{ matrix.arch }} - path: | - apps/session-deck-desktop/dist/artifacts-${{ matrix.arch }}/session-deck-desktop-v${{ needs.release.outputs.pi_session_deck_version }}-macos-${{ matrix.arch }}.zip - apps/session-deck-desktop/dist/artifacts-${{ matrix.arch }}/session-deck-desktop-v${{ needs.release.outputs.pi_session_deck_version }}-macos-${{ matrix.arch }}.zip.sha256 - if-no-files-found: error - retention-days: 7 - - session-deck-publish: - needs: [release, session-deck-desktop-build] - if: needs.release.outputs.pi_session_deck_released == 'true' - runs-on: ubuntu-latest - permissions: - actions: read - contents: write - id-token: write - steps: - - name: Checkout pi-session-deck release tag - uses: actions/checkout@v4 - with: - ref: ${{ needs.release.outputs.pi_session_deck_tag_name }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js and npm trusted publishing - uses: actions/setup-node@v4 - with: - node-version: '22.14' - cache: 'pnpm' - registry-url: 'https://registry.npmjs.org' - - - name: Install npm 11 - run: npm install -g npm@11 - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Merge native architecture artifacts - uses: actions/download-artifact@v4 - with: - pattern: session-deck-desktop-* - path: apps/session-deck-desktop/dist/release-artifacts - merge-multiple: true - - - name: Validate four desktop release files - env: - SESSION_DECK_RELEASE_TAG: ${{ needs.release.outputs.pi_session_deck_tag_name }} - SESSION_DECK_VERSION: ${{ needs.release.outputs.pi_session_deck_version }} - run: | - set -euo pipefail - artifact_dir="apps/session-deck-desktop/dist/release-artifacts" - stem="session-deck-desktop-v${SESSION_DECK_VERSION}-macos" - expected_tag="pi-session-deck-v${SESSION_DECK_VERSION}" - - test "$SESSION_DECK_RELEASE_TAG" = "$expected_tag" || { - echo "Release tag $SESSION_DECK_RELEASE_TAG does not match version $SESSION_DECK_VERSION" >&2 - exit 1 - } - test "$(node -p "require('./packages/pi-session-deck/package.json').version")" = "$SESSION_DECK_VERSION" || { - echo "Package version does not match release version $SESSION_DECK_VERSION" >&2 - exit 1 - } - - printf '%s\n' \ - "${stem}-arm64.zip" \ - "${stem}-arm64.zip.sha256" \ - "${stem}-x64.zip" \ - "${stem}-x64.zip.sha256" \ - | LC_ALL=C sort > "$RUNNER_TEMP/expected-assets.txt" - find "$artifact_dir" -mindepth 1 -maxdepth 1 -exec basename {} \; \ - | LC_ALL=C sort > "$RUNNER_TEMP/actual-assets.txt" - diff -u "$RUNNER_TEMP/expected-assets.txt" "$RUNNER_TEMP/actual-assets.txt" - - while IFS= read -r name; do - test -f "$artifact_dir/$name" && test ! -L "$artifact_dir/$name" && test -s "$artifact_dir/$name" || { - echo "Expected a non-empty regular file: $name" >&2 - exit 1 - } - done < "$RUNNER_TEMP/expected-assets.txt" - - ( - cd "$artifact_dir" - sha256sum "${stem}-arm64.zip" | cmp - "${stem}-arm64.zip.sha256" - sha256sum "${stem}-x64.zip" | cmp - "${stem}-x64.zip.sha256" - ) - - - name: Require an empty draft and prepare release notes - env: - GH_TOKEN: ${{ github.token }} - SESSION_DECK_RELEASE_TAG: ${{ needs.release.outputs.pi_session_deck_tag_name }} - run: | - set -euo pipefail - gh release view "$SESSION_DECK_RELEASE_TAG" --json isDraft,assets,body \ - > "$RUNNER_TEMP/release-before.json" - test "$(jq -r '.isDraft' "$RUNNER_TEMP/release-before.json")" = true || { - echo "Session Deck release must still be a draft" >&2 - exit 1 - } - test "$(jq '.assets | length' "$RUNNER_TEMP/release-before.json")" = 0 || { - echo "Refusing to overwrite preexisting release assets" >&2 - exit 1 - } - - jq -r '.body // ""' "$RUNNER_TEMP/release-before.json" \ - > "$RUNNER_TEMP/release-notes.md" - cat >> "$RUNNER_TEMP/release-notes.md" <<'EOF' - - ## macOS desktop build notice - - The macOS desktop ZIPs are ad-hoc signed. They are not Developer ID signed and are not notarized. macOS may block the first launch. First try to open the app, then, only if you trust this release, use **System Settings → Privacy & Security → Open Anyway** and confirm **Open**. The ad-hoc signature does not verify the publisher. - EOF - - - name: Upload four desktop assets to draft release - env: - GH_TOKEN: ${{ github.token }} - SESSION_DECK_RELEASE_TAG: ${{ needs.release.outputs.pi_session_deck_tag_name }} - SESSION_DECK_VERSION: ${{ needs.release.outputs.pi_session_deck_version }} - run: | - set -euo pipefail - artifact_dir="apps/session-deck-desktop/dist/release-artifacts" - stem="session-deck-desktop-v${SESSION_DECK_VERSION}-macos" - gh release upload "$SESSION_DECK_RELEASE_TAG" \ - "$artifact_dir/${stem}-arm64.zip" \ - "$artifact_dir/${stem}-arm64.zip.sha256" \ - "$artifact_dir/${stem}-x64.zip" \ - "$artifact_dir/${stem}-x64.zip.sha256" - - - name: Publish GitHub release - env: - GH_TOKEN: ${{ github.token }} - SESSION_DECK_RELEASE_TAG: ${{ needs.release.outputs.pi_session_deck_tag_name }} - run: gh release edit "$SESSION_DECK_RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-notes.md" --draft=false - - - name: Publish pi-session-deck after public release - run: cd packages/pi-session-deck && npm publish diff --git a/.github/workflows/session-deck-desktop.yml b/.github/workflows/session-deck-desktop.yml deleted file mode 100644 index b4366c06..00000000 --- a/.github/workflows/session-deck-desktop.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Session Deck Desktop CI - -on: - push: - branches: - - main - pull_request: - paths: - - apps/session-deck-desktop/** - - packages/pi-session-deck/extensions/** - - packages/pi-session-deck/package.json - - packages/pi-session-deck/tsconfig*.json - - .github/workflows/** - - package.json - - pnpm-lock.yaml - - pnpm-workspace.yaml - - tsconfig.base.json - - eslint.config.js - - .prettierrc - - .prettierignore - -permissions: - contents: read - -concurrency: - group: session-deck-desktop-ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}-{1}', github.run_id, github.run_attempt) }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - desktop-checks: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - name: Install Tauri Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - pkg-config \ - libglib2.0-dev \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - libxdo-dev \ - libssl-dev - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build publishable packages - run: pnpm -r --filter './packages/*' --if-present build - - - name: Verify Session Deck web assets are synchronized - run: | - pnpm --filter ./apps/session-deck-desktop sync:web - git diff --exit-code -- \ - apps/session-deck-desktop/web/index.html \ - apps/session-deck-desktop/web/style.css \ - apps/session-deck-desktop/web/session-deck-ui.js - - - name: Build Session Deck desktop app - run: pnpm --filter ./apps/session-deck-desktop build - - - name: Run Session Deck desktop lint - run: pnpm --filter ./apps/session-deck-desktop lint - - - name: Run Session Deck desktop format check - run: pnpm --filter ./apps/session-deck-desktop format:check - - - name: Run Session Deck desktop typecheck - run: pnpm --filter ./apps/session-deck-desktop typecheck - - desktop-tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - name: Install Tauri Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - pkg-config \ - libglib2.0-dev \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - libxdo-dev \ - libssl-dev - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run Session Deck desktop tests - run: pnpm --filter ./apps/session-deck-desktop test diff --git a/.gitignore b/.gitignore index e2c226bd..8f94151f 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,6 @@ benchmarks/test-runners/dotnet/obj/ # Rust / Cargo benchmarks/test-runners/cargo/target/ benchmarks/test-runners/cargo-build/target/ -apps/session-deck-desktop/src-tauri/target/ # PHPUnit / Pest benchmarks/test-runners/phpunit/vendor/ diff --git a/README.md b/README.md index c5a953d8..908d6edf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Small, focused packages to augment your Pi environment without adding unnecessar | Package | Description | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| [`pi-session-deck`](packages/pi-session-deck/README.md) | The full Pi session lifecycle in one place: create and organize sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI, desktop app, or iTerm2 Toolbelt. | +| [`pi-session-deck`](packages/pi-session-deck/README.md) | The full Pi session lifecycle in one place: create and organize sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI or iTerm2 Toolbelt. | | [`pi-merge-ready`](packages/pi-merge-ready/README.md) | Pull requests that explain and repair their own blockers. Give your agents the context they need to take your PR all the way to green. | | [`pi-openrouter`](packages/pi-openrouter/README.md) | OpenRouter usage/account overlays, model sync, api key management, and session tagging for Pi. | | [`pi-session-hygiene`](packages/pi-session-hygiene/README.md) | Status bar indicator for session cost, context, and cache rate to track session health | diff --git a/apps/session-deck-desktop/README.md b/apps/session-deck-desktop/README.md deleted file mode 100644 index 92ae0152..00000000 --- a/apps/session-deck-desktop/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Session Deck Desktop app - -Private Tauri desktop companion for Session Deck. This app lives under `apps/` so the `packages/*` tree remains limited to independently installable Pi packages. - -## What it does - -- Loads Session Deck snapshots through the installed Node helper. -- Reuses the existing open-terminal and worktree helper CLIs. -- Exposes the shared, generation-safe Restart Session action for eligible Session Deck-managed tmux sessions without sending private launch recipes to the webview. -- Prefers `~/.pi/session-deck/desktop/install.json` for desktop runtime metadata. -- Falls back to `~/.pi/session-deck/iterm2/install.json` only for development/back-compat. -- Rebuilds a safe helper `PATH` for Finder-launched app processes. - -## Commands - -- `pnpm --filter ./apps/session-deck-desktop sync:web` -- `pnpm --filter ./apps/session-deck-desktop typecheck` -- `pnpm --filter ./apps/session-deck-desktop test` -- `pnpm --filter ./apps/session-deck-desktop build` -- `pnpm --filter ./apps/session-deck-desktop dev:isolated` — build the local helper and launch from temporary checkout-specific metadata without replacing the installed app -- `pnpm --filter ./apps/session-deck-desktop tauri dev` -- `pnpm --filter ./apps/session-deck-desktop artifact:macos --version --target ` - -## Release artifacts - -`artifact:macos` builds one native Tauri `.app`, verifies that its executable contains only the target architecture, and packages the app with `ditto`. It emits one deterministic `session-deck-desktop-v-macos-.zip` and its `.sha256` sidecar. - -The app is ad-hoc signed with Tauri's `-` identity. It is not Developer ID signed and is not notarized. No Apple credentials are required. macOS may block the first launch; leave the app installed at the initial warning, then, only if you trust the release, use **System Settings → Privacy & Security → Open Anyway**. - -See [RELEASE.md](./RELEASE.md) for the dual-architecture release and publication contract. diff --git a/apps/session-deck-desktop/RELEASE.md b/apps/session-deck-desktop/RELEASE.md deleted file mode 100644 index 37f6d6e3..00000000 --- a/apps/session-deck-desktop/RELEASE.md +++ /dev/null @@ -1,94 +0,0 @@ -# Session Deck Desktop release runbook - -The release workflow is `.github/workflows/release-please.yml`. It publishes two prebuilt native macOS app ZIPs for `@robhowley/pi-session-deck` before publishing the npm package. - -## Native builds - -| GitHub runner | Rust target | Artifact architecture | -| ---------------- | ---------------------- | --------------------- | -| `macos-15` | `aarch64-apple-darwin` | `arm64` | -| `macos-15-intel` | `x86_64-apple-darwin` | `x64` | - -Each matrix leg builds only the Tauri `.app` bundle for its explicit target. The builder checks that the app's executable is non-empty, executable, and contains only the expected architecture. It then creates the ZIP with: - -```sh -/usr/bin/ditto -c -k --keepParent --sequesterRsrc --zlibCompressionLevel 9 \ - "Session Deck Desktop.app" .zip -``` - -This preserves the bundle structure, resource forks, and executable modes. Do not pass the `.app` directory directly to `actions/upload-artifact`. - -## Signing and first launch - -Both builds are ad-hoc signed with Tauri's free identity `-`. They are **not Developer ID signed** and **not notarized**. The ad-hoc signature does not verify the publisher, and no Apple credentials are required. - -macOS may block the first launch. Users should: - -1. Verify the downloaded ZIP against its published `.sha256` sidecar. -2. Extract the ZIP, move **Session Deck Desktop.app** to `/Applications`, and try to open it once; leave it installed at the initial warning. -3. Only if they trust the release, use **System Settings → Privacy & Security → Open Anyway**. -4. Confirm **Open** and authenticate if macOS asks. - -Do not remove quarantine attributes or recommend disabling Gatekeeper. - -## Four-file release contract - -For package version ``, the public `pi-session-deck-v` GitHub release contains exactly: - -```text -session-deck-desktop-v-macos-arm64.zip -session-deck-desktop-v-macos-arm64.zip.sha256 -session-deck-desktop-v-macos-x64.zip -session-deck-desktop-v-macos-x64.zip.sha256 -``` - -Each sidecar is one lowercase SHA-256 followed by two spaces and the ZIP basename. `/session-deck desktop install` selects the matching ZIP and sidecar for the host architecture. - -## Publication order and failure behavior - -1. Release Please creates the `pi-session-deck-v` draft and release tag. -2. The arm64 and x64 jobs build and stage one ZIP plus one sidecar each. -3. The fan-in job requires package, tag, and artifact versions to agree. It rejects anything except the exact four expected non-empty regular files and verifies both checksums. -4. The job requires the GitHub release to remain a draft with zero assets. -5. It appends the signing and first-launch notice, uploads four explicit paths without clobbering, and publishes the GitHub release. -6. Only after GitHub publication does `npm publish` run. - -A failed native leg prevents publication. A partial draft upload intentionally blocks an automatic retry because the release no longer has zero assets; inspect it rather than replacing uploaded bytes. GitHub and npm publication cannot be one transaction. If GitHub publication succeeds but npm publication fails, fix trusted-publisher access and publish the same package version from the original tag without changing the GitHub assets. - -## npm trusted publishing - -Configure an npm trusted publisher for `@robhowley/pi-session-deck` with: - -- provider: GitHub Actions; -- organization/user: `robhowley`; -- repository: `pi-userland`; -- workflow filename: `release-please.yml`; -- allowed action: `npm publish`; -- no environment restriction. - -The publication jobs use Node 22.14+, npm 11.5.1+, npm's registry setup, and `id-token: write`. Do not add a long-lived npm token. - -## Local checks - -```sh -pnpm exec vitest run \ - apps/session-deck-desktop/__tests__/release-artifacts.test.ts \ - apps/session-deck-desktop/__tests__/release-workflow.test.ts \ - apps/session-deck-desktop/__tests__/tauri-config.test.ts \ - packages/pi-session-deck/__tests__/session-deck/desktop-artifact.test.ts -pnpm --filter ./apps/session-deck-desktop typecheck -pnpm --filter ./apps/session-deck-desktop lint -pnpm --filter ./apps/session-deck-desktop format:check -pnpm --filter @robhowley/pi-session-deck typecheck -``` - -On an arm64 Mac, smoke-build the local target with: - -```sh -pnpm --filter ./apps/session-deck-desktop artifact:macos \ - --version 0.0.0 \ - --target aarch64-apple-darwin \ - --artifact-dir dist/smoke-arm64 -``` - -Extract the ZIP with `ditto`, confirm the executable mode and `lipo -archs` output, and inspect `codesign -dv --verbose=4` for `Signature=adhoc`. CI remains responsible for the equivalent x64 evidence on `macos-15-intel`. diff --git a/apps/session-deck-desktop/__tests__/ci-workflow.test.ts b/apps/session-deck-desktop/__tests__/ci-workflow.test.ts deleted file mode 100644 index b382d876..00000000 --- a/apps/session-deck-desktop/__tests__/ci-workflow.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { describe, expect, it } from 'vitest'; - -const WORKFLOWS_ROOT = new URL('../../../.github/workflows/', import.meta.url); -const packageWorkflow = readFileSync(new URL('ci.yml', WORKFLOWS_ROOT), 'utf8'); -const desktopWorkflow = readFileSync(new URL('session-deck-desktop.yml', WORKFLOWS_ROOT), 'utf8'); - -const packageTrigger = `on: - push: - branches: - - main - pull_request: - -permissions:`; - -const desktopTrigger = `on: - push: - branches: - - main - pull_request: - paths: - - apps/session-deck-desktop/** - - packages/pi-session-deck/extensions/** - - packages/pi-session-deck/package.json - - packages/pi-session-deck/tsconfig*.json - - .github/workflows/** - - package.json - - pnpm-lock.yaml - - pnpm-workspace.yaml - - tsconfig.base.json - - eslint.config.js - - .prettierrc - - .prettierignore - -permissions:`; - -const pullRequestCancellation = " cancel-in-progress: ${{ github.event_name == 'pull_request' }}"; -const packageConcurrencyGroup = - " group: packages-ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}-{1}', github.run_id, github.run_attempt) }}"; -const desktopConcurrencyGroup = - " group: session-deck-desktop-ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('run-{0}-{1}', github.run_id, github.run_attempt) }}"; - -describe('Session Deck CI workflow contract', () => { - it('keeps the repository-owned CI policy', () => { - expect(packageWorkflow).toContain(packageTrigger); - expect(desktopWorkflow).toContain(desktopTrigger); - - expect(packageWorkflow).toContain('jobs:\n package-checks:\n'); - expect(packageWorkflow).toContain('\n package-tests:\n'); - expect(desktopWorkflow).toContain('jobs:\n desktop-checks:\n'); - expect(desktopWorkflow).toContain('\n desktop-tests:\n'); - - for (const workflow of [packageWorkflow, desktopWorkflow]) { - expect(workflow).not.toMatch(/^ {4}(?:if|needs):/mu); - expect(workflow).toContain(pullRequestCancellation); - } - - expect(packageWorkflow).toContain(packageConcurrencyGroup); - expect(desktopWorkflow).toContain(desktopConcurrencyGroup); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/launch-branch.test.ts b/apps/session-deck-desktop/__tests__/launch-branch.test.ts deleted file mode 100644 index 1a9aa4e1..00000000 --- a/apps/session-deck-desktop/__tests__/launch-branch.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { createBranchState } from '../scripts/launch-branch.js'; - -describe('launch-branch development metadata', () => { - it('uses the development discriminator without persisting derived helper paths', () => { - const state = createBranchState('1.2.3', '2026-07-28T00:00:00.000Z'); - - expect(state).toMatchObject({ - schemaVersion: 1, - product: 'session-deck-desktop-development', - packageName: '@robhowley/pi-session-deck', - packageVersion: '1.2.3', - installedAt: '2026-07-28T00:00:00.000Z', - runtime: { - nodeExecutablePath: process.execPath, - helperPackageVersion: '1.2.3', - }, - }); - expect(Object.keys(state.runtime).sort()).toEqual([ - 'helperPackageVersion', - 'nodeExecutablePath', - 'packageRoot', - ]); - expect(state).not.toHaveProperty('app'); - expect(state).not.toHaveProperty('source'); - expect(state).not.toHaveProperty('ownedPaths'); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/release-artifacts.test.ts b/apps/session-deck-desktop/__tests__/release-artifacts.test.ts deleted file mode 100644 index 062f0503..00000000 --- a/apps/session-deck-desktop/__tests__/release-artifacts.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { createHash } from 'node:crypto'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { - applyDesktopReleaseVersion, - artifactArchForTarget, - assertAdHocSignature, - assertExecutableArchitecture, - bundleRootForTarget, - macosArtifactStem, - normalizeArtifactArch, - normalizeReleaseVersion, - parseMacosArtifactArgs, - tauriBuildArgsForOptions, - writeArtifactChecksum, -} from '../scripts/build-macos-artifacts.js'; - -const temporaryDirectories: string[] = []; - -afterEach(async () => { - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ); -}); - -describe('release artifact builder contract', () => { - it('derives exact native artifact names and target-specific bundle roots', () => { - expect(normalizeReleaseVersion('v0.9.0')).toBe('0.9.0'); - expect(normalizeArtifactArch('aarch64')).toBe('arm64'); - expect(artifactArchForTarget('x86_64-apple-darwin')).toBe('x64'); - expect(macosArtifactStem('v0.9.0', 'aarch64')).toBe('session-deck-desktop-v0.9.0-macos-arm64'); - expect(macosArtifactStem('0.9.0', 'x86_64')).toBe('session-deck-desktop-v0.9.0-macos-x64'); - expect(bundleRootForTarget('aarch64-apple-darwin')).toMatch( - /src-tauri\/target\/aarch64-apple-darwin\/release\/bundle$/u, - ); - expect(bundleRootForTarget('x86_64-apple-darwin')).toMatch( - /src-tauri\/target\/x86_64-apple-darwin\/release\/bundle$/u, - ); - }); - - it('requires an explicit supported target and derives its architecture', () => { - expect( - parseMacosArtifactArgs([ - '--version', - '0.9.0', - '--target', - 'x86_64-apple-darwin', - '--artifact-dir', - 'dist/test-artifacts', - ]), - ).toMatchObject({ - version: '0.9.0', - arch: 'x64', - target: 'x86_64-apple-darwin', - }); - expect(() => parseMacosArtifactArgs(['--version', '0.9.0'])).toThrow('Missing --target'); - expect(() => - parseMacosArtifactArgs(['--version', '0.9.0', '--target', 'universal-apple-darwin']), - ).toThrow('Unsupported macOS release target'); - }); - - it.each([ - ['arm64', 'arm64'], - ['x64', 'x86_64'], - ] as const)('accepts only one %s executable architecture', (arch, lipoOutput) => { - expect(() => assertExecutableArchitecture(lipoOutput, arch)).not.toThrow(); - expect(() => assertExecutableArchitecture(`${lipoOutput} x86_64 arm64`, arch)).toThrow( - 'expected only', - ); - }); - - it('requires an ad-hoc signature without an authenticated authority', () => { - expect(() => assertAdHocSignature('Executable=/tmp/app\nSignature=adhoc')).not.toThrow(); - expect(() => assertAdHocSignature('Signature=adhoc\nAuthority=Example')).toThrow( - 'authenticated signing authority', - ); - expect(() => assertAdHocSignature('Signature=CMS')).toThrow('ad-hoc signature'); - }); - - it('builds only the app for the explicit target and does not disable signing', () => { - const options = parseMacosArtifactArgs([ - '--version', - '0.9.0', - '--target', - 'aarch64-apple-darwin', - ]); - expect(tauriBuildArgsForOptions(options)).toEqual([ - 'build', - '--bundles', - 'app', - '--ci', - '--target', - 'aarch64-apple-darwin', - ]); - }); - - it('writes release versions into Tauri and Cargo metadata', () => { - const tauriConfig = JSON.stringify({ - productName: 'Session Deck Desktop', - version: '0.0.0', - bundle: { active: true, macOS: { minimumSystemVersion: '11.0', signingIdentity: '-' } }, - }); - const cargoToml = `[package]\nname = "pi-session-deck-desktop"\nversion = "0.0.0"\n`; - - const next = applyDesktopReleaseVersion(tauriConfig, cargoToml, '0.9.0'); - const nextTauriConfig = JSON.parse(next.tauriConfigText) as { - version: string; - bundle: { - macOS: { - bundleVersion: string; - minimumSystemVersion: string; - signingIdentity: string; - }; - }; - }; - expect(nextTauriConfig.version).toBe('0.9.0'); - expect(nextTauriConfig.bundle.macOS).toEqual({ - bundleVersion: '0.9.0', - minimumSystemVersion: '11.0', - signingIdentity: '-', - }); - expect(next.cargoTomlText).toContain('version = "0.9.0"'); - }); - - it('writes a lowercase SHA-256 sidecar for the ZIP basename', async () => { - const root = await mkdtemp(join(tmpdir(), 'session-deck-release-artifacts-')); - temporaryDirectories.push(root); - const name = 'session-deck-desktop-v0.9.0-macos-arm64.zip'; - const path = join(root, name); - const payload = Buffer.from('zip payload'); - await writeFile(path, payload); - - const result = await writeArtifactChecksum(path); - const expectedHash = createHash('sha256').update(payload).digest('hex'); - expect(result.sha256).toBe(expectedHash); - await expect(readFile(result.checksumPath, 'utf8')).resolves.toBe(`${expectedHash} ${name}\n`); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/release-workflow.test.ts b/apps/session-deck-desktop/__tests__/release-workflow.test.ts deleted file mode 100644 index 06674bf3..00000000 --- a/apps/session-deck-desktop/__tests__/release-workflow.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { describe, expect, it } from 'vitest'; - -const workflow = readFileSync( - new URL('../../../.github/workflows/release-please.yml', import.meta.url), - 'utf8', -); -const trigger = workflow.slice(workflow.indexOf('on:\n'), workflow.indexOf('\n\nconcurrency:')); -const builder = readFileSync( - new URL('../scripts/build-macos-artifacts.js', import.meta.url), - 'utf8', -); -const runbook = readFileSync(new URL('../RELEASE.md', import.meta.url), 'utf8'); -const releaseJob = workflow.slice( - workflow.indexOf(' release:\n'), - workflow.indexOf(' session-deck-desktop-build:\n'), -); -const buildJob = workflow.slice( - workflow.indexOf(' session-deck-desktop-build:\n'), - workflow.indexOf(' session-deck-publish:\n'), -); -const publicationJob = workflow.slice(workflow.indexOf(' session-deck-publish:\n')); -const buildHeader = buildJob.slice(0, buildJob.indexOf(' strategy:\n')); -const publicationHeader = publicationJob.slice(0, publicationJob.indexOf(' steps:\n')); - -describe('Session Deck release workflow contract', () => { - it('runs only for pushes to main', () => { - expect(trigger).toBe(['on:', ' push:', ' branches: [main]'].join('\n')); - expect(workflow).not.toMatch( - /^ {2}(?:pull_request|pull_request_target|workflow_dispatch|schedule|merge_group):/mu, - ); - }); - - it('keeps the desktop build and publication behind the exact release-created condition', () => { - expect(buildHeader.match(/^ {4}needs:.*$/gmu)).toEqual([' needs: release']); - expect(buildHeader.match(/^ {4}if:.*$/gmu)).toEqual([ - " if: needs.release.outputs.pi_session_deck_released == 'true'", - ]); - expect(publicationHeader.match(/^ {4}needs:.*$/gmu)).toEqual([ - ' needs: [release, session-deck-desktop-build]', - ]); - expect(publicationHeader.match(/^ {4}if:.*$/gmu)).toEqual([ - " if: needs.release.outputs.pi_session_deck_released == 'true'", - ]); - }); - - it('keeps exactly one unconditional arm64/x64 build matrix', () => { - const matrix = buildJob.slice( - buildJob.indexOf(' strategy:\n'), - buildJob.indexOf(' runs-on: '), - ); - - expect(matrix).toBe( - [ - ' strategy:', - ' fail-fast: true', - ' matrix:', - ' include:', - ' - runner: macos-15', - ' target: aarch64-apple-darwin', - ' arch: arm64', - ' - runner: macos-15-intel', - ' target: x86_64-apple-darwin', - ' arch: x64', - '', - ].join('\n'), - ); - expect(matrix).not.toMatch(/\b(?:if|exclude|paths?|labels?|changed-files?)\b/iu); - }); - - it('keeps release jobs isolated from classifier and selective path logic', () => { - const selectionReferences = - /classifier|selective|run_(?:desktop|web_sync|desktop_js|native)|changed-files|path-filter|paths-ignore/iu; - - expect(buildJob).not.toMatch(selectionReferences); - expect(publicationJob).not.toMatch(selectionReferences); - }); - - it('has no credential-backed desktop release inputs or alternate release mode', () => { - const secretNames = [...workflow.matchAll(/\$\{\{\s*secrets\.([A-Z0-9_]+)\s*\}\}/gu)].map( - (match) => match[1], - ); - const variableNames = [...workflow.matchAll(/\$\{\{\s*vars\.([A-Z0-9_]+)\s*\}\}/gu)].map( - (match) => match[1], - ); - - expect(secretNames).toEqual([]); - expect(variableNames).toEqual([]); - expect(builder).not.toMatch(/process\.env\[[^\]]+\]/u); - expect(buildJob).not.toMatch(/^\s+environment:/mu); - }); - - it('skips pi-session-deck before the generic npm publish loop reaches publish', () => { - const skipCondition = 'if [ "$pkg" = "packages/pi-session-deck" ]; then'; - const continuePosition = releaseJob.indexOf('continue', releaseJob.indexOf(skipCondition)); - const publishPosition = releaseJob.indexOf('(cd "$pkg" && npm publish)'); - - expect(releaseJob).toContain(skipCondition); - expect(continuePosition).toBeGreaterThan(releaseJob.indexOf(skipCondition)); - expect(publishPosition).toBeGreaterThan(continuePosition); - }); - - it('builds and stages one native app ZIP and checksum per architecture', () => { - expect(buildJob).toContain('runner: macos-15\n target: aarch64-apple-darwin'); - expect(buildJob).toContain('runner: macos-15-intel\n target: x86_64-apple-darwin'); - expect(buildJob).toContain('--target "${{ matrix.target }}"'); - expect(buildJob).toContain('--artifact-dir "dist/artifacts-${{ matrix.arch }}"'); - expect(buildJob).toContain('-macos-${{ matrix.arch }}.zip'); - expect(buildJob).toContain('-macos-${{ matrix.arch }}.zip.sha256'); - expect(buildJob).not.toContain('dist/artifacts-${{ matrix.arch }}/*'); - }); - - it('fans in and validates exactly four named non-empty files and both checksums', () => { - for (const suffix of [ - '${stem}-arm64.zip', - '${stem}-arm64.zip.sha256', - '${stem}-x64.zip', - '${stem}-x64.zip.sha256', - ]) { - expect(publicationJob).toContain(`"${suffix}"`); - } - expect(publicationJob).toContain('diff -u "$RUNNER_TEMP/expected-assets.txt"'); - expect(publicationJob).toContain('test -f "$artifact_dir/$name"'); - expect(publicationJob).toContain('test ! -L "$artifact_dir/$name"'); - expect(publicationJob).toContain('test -s "$artifact_dir/$name"'); - expect(publicationJob.match(/sha256sum "\$\{stem\}/gu)).toHaveLength(2); - expect(publicationJob.match(/\| cmp -/gu)).toHaveLength(2); - expect(publicationJob).toContain('expected_tag="pi-session-deck-v${SESSION_DECK_VERSION}"'); - expect(publicationJob).toContain("require('./packages/pi-session-deck/package.json').version"); - }); - - it('requires an empty draft, uploads without clobbering, and publishes GitHub first', () => { - expect(publicationJob).toContain('\'.isDraft\' "$RUNNER_TEMP/release-before.json")" = true'); - expect(publicationJob).toContain( - '\'.assets | length\' "$RUNNER_TEMP/release-before.json")" = 0', - ); - expect(publicationJob).toContain('Refusing to overwrite preexisting release assets'); - expect(publicationJob).not.toContain('--clobber'); - expect(publicationJob).not.toContain('npm view'); - expect(publicationJob).not.toContain('npm pack'); - - const orderedMarkers = [ - 'name: Validate four desktop release files', - 'name: Require an empty draft and prepare release notes', - 'gh release upload', - 'gh release edit', - 'name: Publish pi-session-deck after public release', - 'npm publish', - ]; - const positions = orderedMarkers.map((marker) => publicationJob.indexOf(marker)); - expect(positions.every((position) => position >= 0)).toBe(true); - expect(positions).toEqual([...positions].sort((left, right) => left - right)); - }); - - it('discloses the ad-hoc trust model and safe first-launch override', () => { - for (const text of [workflow, runbook]) { - expect(text).toContain('ad-hoc signed'); - expect(text).toContain('not Developer ID signed'); - expect(text).toContain('not notarized'); - expect(text).toContain('System Settings → Privacy & Security → Open Anyway'); - } - expect(`${workflow}\n${runbook}`).not.toMatch(/xattr|spctl\s+--master-disable/iu); - }); - - it('keeps npm trusted publishing runtime and OIDC permissions', () => { - expect(workflow.match(/npm publish/gu)).toHaveLength(2); - expect(releaseJob).toContain("node-version: '22.14'"); - expect(publicationJob).toContain("node-version: '22.14'"); - expect(releaseJob).toContain('npm install -g npm@11'); - expect(publicationJob).toContain('npm install -g npm@11'); - expect(publicationJob).toContain('id-token: write'); - expect(workflow).not.toMatch(/NPM_TOKEN|NODE_AUTH_TOKEN/u); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/sync-web-assets.test.ts b/apps/session-deck-desktop/__tests__/sync-web-assets.test.ts deleted file mode 100644 index 3a1c5771..00000000 --- a/apps/session-deck-desktop/__tests__/sync-web-assets.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { buildDesktopIndex, syncWebAssets } from '../scripts/sync-web-assets.js'; - -const CANONICAL_INDEX = ` - - - - - Session Deck - - - -
- - - - - -`; - -describe('sync-web-assets', () => { - it('rewrites the canonical index for the desktop host', () => { - const rewritten = buildDesktopIndex(CANONICAL_INDEX); - - expect(rewritten).not.toContain('session-deck-action-token'); - expect(rewritten).toContain('href="./style.css"'); - expect(rewritten).toContain(''); - expect(rewritten).toContain(''); - expect(rewritten).not.toContain('iterm2-host.js'); - }); - - it('copies the required canonical shared UI byte-for-byte', async () => { - const root = await mkdtemp(join(tmpdir(), 'session-deck-sync-web-')); - const sourceWebRoot = join(root, 'source'); - const destinationWebRoot = join(root, 'destination'); - const sharedUi = Buffer.from([0, 1, 2, 10, 13, 255]); - await mkdir(sourceWebRoot); - await Promise.all([ - writeFile(join(sourceWebRoot, 'index.html'), CANONICAL_INDEX), - writeFile(join(sourceWebRoot, 'style.css'), 'body { color: red; }\n'), - writeFile(join(sourceWebRoot, 'session-deck-ui.js'), sharedUi), - ]); - - await syncWebAssets({ sourceWebRoot, destinationWebRoot }); - - await expect(readFile(join(destinationWebRoot, 'session-deck-ui.js'))).resolves.toEqual( - sharedUi, - ); - }); - - it('rejects sync when the canonical shared UI is missing', async () => { - const root = await mkdtemp(join(tmpdir(), 'session-deck-sync-web-')); - const sourceWebRoot = join(root, 'source'); - const destinationWebRoot = join(root, 'destination'); - await mkdir(sourceWebRoot); - await Promise.all([ - writeFile(join(sourceWebRoot, 'index.html'), CANONICAL_INDEX), - writeFile(join(sourceWebRoot, 'style.css'), 'body {}\n'), - ]); - - await expect(syncWebAssets({ sourceWebRoot, destinationWebRoot })).rejects.toThrow( - 'session-deck-ui.js', - ); - await expect(readFile(join(destinationWebRoot, 'session-deck-ui.js'))).rejects.toMatchObject({ - code: 'ENOENT', - }); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/tauri-config.test.ts b/apps/session-deck-desktop/__tests__/tauri-config.test.ts deleted file mode 100644 index e0311083..00000000 --- a/apps/session-deck-desktop/__tests__/tauri-config.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { createHash } from 'node:crypto'; -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { inflateSync } from 'node:zlib'; -import { describe, expect, it } from 'vitest'; - -type TauriConfig = { - productName: string; - app: { - trayIcon: { - id: string; - iconPath: string; - iconAsTemplate: boolean; - tooltip: string; - showMenuOnLeftClick: boolean; - }; - windows: Array<{ - title: string; - theme?: string; - }>; - }; - bundle: { - icon: string[]; - targets: string[]; - macOS: { - bundleName: string; - signingIdentity?: string; - }; - }; -}; - -type PngMetadata = { - width: number; - height: number; - bitDepth: number; - colorType: number; -}; - -const TAURI_ROOT = new URL('../src-tauri/', import.meta.url); -const TAURI_CONFIG_PATH = fileURLToPath(new URL('tauri.conf.json', TAURI_ROOT)); -const INFO_PLIST_PATH = fileURLToPath(new URL('Info.plist', TAURI_ROOT)); -const CARGO_MANIFEST_PATH = fileURLToPath(new URL('Cargo.toml', TAURI_ROOT)); -const APP_ICON_PATH = fileURLToPath(new URL('icons/icon.png', TAURI_ROOT)); -const ICNS_ICON_PATH = fileURLToPath(new URL('icons/icon.icns', TAURI_ROOT)); -const TRAY_ICON_PATH = fileURLToPath(new URL('icons/tray-icon.png', TAURI_ROOT)); -const TRAY_SVG_PATH = fileURLToPath(new URL('icons/tray-icon.svg', TAURI_ROOT)); -const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); - -function pngMetadata(png: Buffer): PngMetadata { - if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) { - throw new Error('Invalid PNG signature'); - } - - return { - width: png.readUInt32BE(16), - height: png.readUInt32BE(20), - bitDepth: png.readUInt8(24), - colorType: png.readUInt8(25), - }; -} - -function paethPredictor(left: number, above: number, upperLeft: number): number { - const estimate = left + above - upperLeft; - const leftDistance = Math.abs(estimate - left); - const aboveDistance = Math.abs(estimate - above); - const upperLeftDistance = Math.abs(estimate - upperLeft); - - if (leftDistance <= aboveDistance && leftDistance <= upperLeftDistance) return left; - if (aboveDistance <= upperLeftDistance) return above; - return upperLeft; -} - -function decodePngAlpha(png: Buffer): (x: number, y: number) => number { - const { width, height, bitDepth, colorType } = pngMetadata(png); - if (bitDepth !== 8 || colorType !== 6) throw new Error('Expected an 8-bit RGBA PNG'); - - const idatChunks: Buffer[] = []; - for (let offset = PNG_SIGNATURE.length; offset < png.length; ) { - const length = png.readUInt32BE(offset); - const type = png.subarray(offset + 4, offset + 8).toString('ascii'); - if (type === 'IDAT') idatChunks.push(png.subarray(offset + 8, offset + 8 + length)); - offset += length + 12; - } - - const filtered = inflateSync(Buffer.concat(idatChunks)); - const stride = width * 4; - const rgba = Buffer.alloc(stride * height); - let inputOffset = 0; - - for (let y = 0; y < height; y += 1) { - const filter = filtered.readUInt8(inputOffset++); - for (let x = 0; x < stride; x += 1) { - const outputOffset = y * stride + x; - const left = x >= 4 ? rgba.readUInt8(outputOffset - 4) : 0; - const above = y > 0 ? rgba.readUInt8(outputOffset - stride) : 0; - const upperLeft = x >= 4 && y > 0 ? rgba.readUInt8(outputOffset - stride - 4) : 0; - let predictor: number; - - switch (filter) { - case 0: - predictor = 0; - break; - case 1: - predictor = left; - break; - case 2: - predictor = above; - break; - case 3: - predictor = Math.floor((left + above) / 2); - break; - case 4: - predictor = paethPredictor(left, above, upperLeft); - break; - default: - throw new Error(`Unsupported PNG filter: ${filter}`); - } - - rgba.writeUInt8((filtered.readUInt8(inputOffset++) + predictor) & 0xff, outputOffset); - } - } - - return (x, y) => rgba.readUInt8(y * stride + x * 4 + 3); -} - -describe('Tauri configuration', () => { - it('keeps the product name distinct from the macOS bundle and display names', () => { - const config = JSON.parse(readFileSync(TAURI_CONFIG_PATH, 'utf8')) as TauriConfig; - const infoPlist = readFileSync(INFO_PLIST_PATH, 'utf8'); - const displayName = infoPlist.match( - /CFBundleDisplayName<\/key>\s*([^<]+)<\/string>/u, - )?.[1]; - - expect({ - productName: config.productName, - bundleName: config.bundle.macOS.bundleName, - displayName, - }).toEqual({ - productName: 'Session Deck Desktop', - bundleName: 'Session Deck', - displayName: 'Session Deck', - }); - }); - - it('keeps the Session Deck title and uses a dark native appearance', () => { - const config = JSON.parse(readFileSync(TAURI_CONFIG_PATH, 'utf8')) as TauriConfig; - const [mainWindow] = config.app.windows; - - expect(mainWindow).toMatchObject({ - title: 'Session Deck', - theme: 'Dark', - }); - }); - - it('configures the template tray icon without an automatic left-click menu', () => { - const config = JSON.parse(readFileSync(TAURI_CONFIG_PATH, 'utf8')) as TauriConfig; - - expect(config.app.trayIcon).toEqual({ - id: 'session-deck', - iconPath: 'icons/tray-icon.png', - iconAsTemplate: true, - tooltip: 'Session Deck', - showMenuOnLeftClick: false, - }); - }); - - it('builds only the app with both icons and Tauri ad-hoc signing', () => { - const config = JSON.parse(readFileSync(TAURI_CONFIG_PATH, 'utf8')) as TauriConfig; - - expect(config.bundle.targets).toEqual(['app']); - expect(config.bundle.icon).toEqual(['icons/icon.png', 'icons/icon.icns']); - expect(config.bundle.macOS.signingIdentity).toBe('-'); - }); - - it('commits the confirmed app icon and valid macOS icon resources', () => { - const appIcon = readFileSync(APP_ICON_PATH); - const icnsIcon = readFileSync(ICNS_ICON_PATH); - - expect(createHash('sha256').update(appIcon).digest('hex')).toBe( - 'dce6b5ffc66207c0a53da18b170738ac67d51ca856c2ed3b855ae75306456177', - ); - expect(pngMetadata(appIcon)).toEqual({ - width: 1024, - height: 1024, - bitDepth: 8, - colorType: 6, - }); - expect(icnsIcon.subarray(0, 4).toString('ascii')).toBe('icns'); - expect(icnsIcon.readUInt32BE(4)).toBe(icnsIcon.length); - expect(icnsIcon.length).toBeGreaterThan(0); - }); - - it('keeps the exact tray geometry and transparent dot cutouts', () => { - const svg = readFileSync(TRAY_SVG_PATH, 'utf8'); - const trayIcon = readFileSync(TRAY_ICON_PATH); - - expect(svg.match(/'); - expect(svg).toContain(''); - expect(svg).toContain(''); - expect(svg.match(/'); - expect(svg).toContain(''); - expect(svg).toContain(''); - expect(svg).toContain(''); - - expect(pngMetadata(trayIcon)).toEqual({ width: 36, height: 36, bitDepth: 8, colorType: 6 }); - const alphaAt = decodePngAlpha(trayIcon); - expect({ - exterior: alphaAt(0, 0), - holes: [alphaAt(8, 8), alphaAt(8, 18), alphaAt(8, 28)], - rows: [alphaAt(18, 8), alphaAt(18, 18), alphaAt(18, 28)], - }).toEqual({ exterior: 0, holes: [0, 0, 0], rows: [255, 255, 255] }); - }); - - it('enables only the Tauri tray icon feature', () => { - const cargoManifest = readFileSync(CARGO_MANIFEST_PATH, 'utf8'); - - expect(cargoManifest.match(/^tauri = .*$/mu)?.[0]).toBe( - 'tauri = { version = "2", features = ["tray-icon"] }', - ); - }); -}); diff --git a/apps/session-deck-desktop/__tests__/tauri-host.test.ts b/apps/session-deck-desktop/__tests__/tauri-host.test.ts deleted file mode 100644 index 517996e5..00000000 --- a/apps/session-deck-desktop/__tests__/tauri-host.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { createTauriSessionDeckHost, resolveTauriInvoke } from '../web/tauri-host.js'; - -describe('tauri-host', () => { - it('resolves the global Tauri invoke bridge', async () => { - const invoke = vi.fn(async () => ({ ok: true })); - const resolved = resolveTauriInvoke({ - __TAURI__: { - core: { - invoke, - }, - }, - } as unknown as Window & typeof globalThis); - - await resolved('load_snapshot'); - - expect(invoke).toHaveBeenCalledWith('load_snapshot'); - }); - - it('maps the desktop host contract to the expected command names and payloads', async () => { - const invoke = vi.fn(async () => ({ ok: true })); - const host = createTauriSessionDeckHost({ - window: { - __TAURI__: { - core: { - invoke, - }, - }, - } as unknown as Window & typeof globalThis, - }); - - expect(host.doctorCommand).toBe( - 'Open desktop diagnostics or run /session-deck desktop doctor.', - ); - - await host.loadSnapshot(); - await host.previewWorktreeBaseRef({ repoIntent: { repoName: 'pi-userland' } }); - await host.previewWorktreeLaunchContext({ - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }); - await host.createWorktree({ - repoIntent: { repoName: 'pi-userland' }, - branchName: 'feat/desktop-shell', - baseRef: 'origin/main', - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }); - await host.createSession({ - action: 'create-session', - cwd: '~/scratch', - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }); - await host.openTerminal('runtime-1'); - await host.killSession('runtime-1'); - await host.restartSession({ - runtimeId: 'runtime-1', - generation: 'generation-1', - operationId: 'operation-1', - }); - await host.openExternal('https://example.com'); - await host.copyText('copied'); - await host.doctorStatus(); - - expect(invoke.mock.calls).toEqual([ - ['load_snapshot'], - ['preview_worktree_base_ref', { request: { repoIntent: { repoName: 'pi-userland' } } }], - [ - 'preview_worktree_launch_context', - { - request: { - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }, - }, - ], - [ - 'create_worktree', - { - request: { - repoIntent: { repoName: 'pi-userland' }, - branchName: 'feat/desktop-shell', - baseRef: 'origin/main', - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }, - }, - ], - [ - 'create_session', - { - request: { - action: 'create-session', - cwd: '~/scratch', - launch: { mode: 'tmux-detached', agentDir: { mode: 'ambient' } }, - }, - }, - ], - ['open_terminal', { request: { runtimeId: 'runtime-1' } }], - ['kill_session', { request: { runtimeId: 'runtime-1' } }], - [ - 'restart_session', - { - request: { - runtimeId: 'runtime-1', - generation: 'generation-1', - operationId: 'operation-1', - }, - }, - ], - ['open_external', { url: 'https://example.com' }], - ['copy_text', { text: 'copied' }], - ['doctor_status'], - ]); - }); - - it('preserves structured Tauri rejection fields on an Error', async () => { - const invoke = vi.fn(async () => { - throw { - code: 'mutating-helper-timeout', - message: - 'The desktop helper timed out before Session Deck could confirm whether the action completed.', - outcomeUnknown: true, - }; - }); - const host = createTauriSessionDeckHost({ - window: { - __TAURI__: { core: { invoke } }, - } as unknown as Window & typeof globalThis, - }); - - const error = await host - .createSession({ action: 'create-session', cwd: '~/scratch' }) - .catch((rejection: unknown) => rejection); - - expect(error).toBeInstanceOf(Error); - expect(error).toMatchObject({ - code: 'mutating-helper-timeout', - message: - 'The desktop helper timed out before Session Deck could confirm whether the action completed.', - outcomeUnknown: true, - }); - }); - - it.each([ - ['a string rejection', 'desktop unavailable'], - ['an Error rejection', new Error('desktop unavailable')], - ])('preserves %s handling', async (_label, rejection) => { - const invoke = vi.fn(async () => { - throw rejection; - }); - const host = createTauriSessionDeckHost({ - window: { - __TAURI__: { core: { invoke } }, - } as unknown as Window & typeof globalThis, - }); - - const error = await host.loadSnapshot().catch((caught: unknown) => caught); - - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toBe('desktop unavailable'); - if (rejection instanceof Error) { - expect(error).toBe(rejection); - } - }); - - it('fails clearly when the global Tauri bridge is unavailable', () => { - expect(() => resolveTauriInvoke(undefined)).toThrow( - 'Tauri invoke bridge is unavailable. Ensure app.withGlobalTauri is enabled.', - ); - }); -}); diff --git a/apps/session-deck-desktop/fixtures/runtime-layout-v1.json b/apps/session-deck-desktop/fixtures/runtime-layout-v1.json deleted file mode 100644 index c128bd3a..00000000 --- a/apps/session-deck-desktop/fixtures/runtime-layout-v1.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "schemaVersion": 1, - "snapshotHelperRelativePath": "dist/extensions/session-deck/iterm2/snapshot-cli.js", - "openActionHelperRelativePath": "dist/extensions/session-deck/iterm2/open-action-cli.js", - "killActionHelperRelativePath": "dist/extensions/session-deck/iterm2/kill-action-cli.js", - "worktreeActionHelperRelativePath": "dist/extensions/session-deck/worktree/action-cli.js", - "webRootRelativePath": "extensions/session-deck/iterm2/web" -} diff --git a/apps/session-deck-desktop/package.json b/apps/session-deck-desktop/package.json deleted file mode 100644 index d96e18bd..00000000 --- a/apps/session-deck-desktop/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@robhowley/pi-session-deck-desktop", - "version": "0.0.0", - "private": true, - "type": "module", - "description": "Tauri desktop companion for Session Deck using installed desktop runtime metadata.", - "scripts": { - "sync:web": "node ./scripts/sync-web-assets.js", - "dev": "pnpm run sync:web && node ./scripts/run-tauri.js dev", - "dev:isolated": "node ./scripts/launch-branch.js dev", - "launch:branch": "node ./scripts/launch-branch.js", - "build": "pnpm run sync:web && node ./scripts/run-tauri.js build --no-bundle --ci", - "bundle:macos": "node ./scripts/run-tauri.js build --bundles app --ci", - "artifact:macos": "node ./scripts/build-macos-artifacts.js", - "tauri": "node ./scripts/run-tauri.js", - "test": "vitest run __tests__ && node ./scripts/run-cargo.js test --manifest-path src-tauri/Cargo.toml", - "typecheck": "tsc --noEmit -p tsconfig.json && node ./scripts/run-cargo.js check --manifest-path src-tauri/Cargo.toml", - "lint": "eslint web/ scripts/ __tests__/", - "format:check": "prettier --check README.md RELEASE.md package.json tsconfig.json web/ scripts/ __tests__/ src-tauri/tauri.conf.json src-tauri/capabilities/default.json && node ./scripts/run-cargo.js fmt --check --manifest-path src-tauri/Cargo.toml", - "format:write": "prettier --write README.md RELEASE.md package.json tsconfig.json web/ scripts/ __tests__/ src-tauri/tauri.conf.json src-tauri/capabilities/default.json && node ./scripts/run-cargo.js fmt --manifest-path src-tauri/Cargo.toml" - }, - "dependencies": { - "@tauri-apps/api": "^2.0.0" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/node": "^22.15.17" - } -} diff --git a/apps/session-deck-desktop/scripts/build-macos-artifacts.js b/apps/session-deck-desktop/scripts/build-macos-artifacts.js deleted file mode 100644 index fef25854..00000000 --- a/apps/session-deck-desktop/scripts/build-macos-artifacts.js +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env node -/* global process, console */ -import { createHash } from 'node:crypto'; -import { createReadStream } from 'node:fs'; -import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; -import { platform as hostPlatform } from 'node:os'; -import { basename, dirname, join, resolve } from 'node:path'; -import { spawn } from 'node:child_process'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { runTauri } from './run-tauri.js'; - -const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..'); -const TAURI_CONF_PATH = resolve(PACKAGE_ROOT, 'src-tauri/tauri.conf.json'); -const CARGO_TOML_PATH = resolve(PACKAGE_ROOT, 'src-tauri/Cargo.toml'); -const DEFAULT_ARTIFACT_DIR = resolve(PACKAGE_ROOT, 'dist/artifacts'); -const PRODUCT_NAME = 'Session Deck Desktop'; -const ARTIFACT_PREFIX = 'session-deck-desktop'; - -/** - * @typedef {{ - * version: string, - * arch: 'arm64' | 'x64', - * target: 'aarch64-apple-darwin' | 'x86_64-apple-darwin', - * artifactDir: string, - * }} MacosArtifactOptions - */ - -/** - * @param {string[]} argv - * @returns {MacosArtifactOptions} - */ -export function parseMacosArtifactArgs(argv = process.argv.slice(2)) { - let version = null; - let target = null; - let artifactDir = DEFAULT_ARTIFACT_DIR; - - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (typeof arg !== 'string') continue; - - if (arg === '--version') { - version = readOptionValue(argv, index, arg); - index += 1; - continue; - } - if (arg.startsWith('--version=')) { - version = readEqualsOptionValue(arg, '--version='); - continue; - } - if (arg === '--target') { - target = readOptionValue(argv, index, arg); - index += 1; - continue; - } - if (arg.startsWith('--target=')) { - target = readEqualsOptionValue(arg, '--target='); - continue; - } - if (arg === '--artifact-dir') { - artifactDir = resolve(PACKAGE_ROOT, readOptionValue(argv, index, arg)); - index += 1; - continue; - } - if (arg.startsWith('--artifact-dir=')) { - artifactDir = resolve(PACKAGE_ROOT, readEqualsOptionValue(arg, '--artifact-dir=')); - continue; - } - throw new Error(`Unknown option: ${arg}`); - } - - if (version === null) { - throw new Error('Missing --version for desktop artifact naming.'); - } - if (target === null) { - throw new Error('Missing --target .'); - } - - const arch = artifactArchForTarget(target); - const releaseTarget = /** @type {'aarch64-apple-darwin' | 'x86_64-apple-darwin'} */ (target); - return { - version: normalizeReleaseVersion(version), - arch, - target: releaseTarget, - artifactDir, - }; -} - -/** @param {string[]} argv @param {number} index @param {string} optionName */ -function readOptionValue(argv, index, optionName) { - const value = argv[index + 1]; - if (typeof value !== 'string' || value.length === 0) { - throw new Error(`Missing value for ${optionName}.`); - } - return value; -} - -/** @param {string} arg @param {string} prefix */ -function readEqualsOptionValue(arg, prefix) { - const value = arg.slice(prefix.length); - if (value.length === 0) throw new Error(`Missing value for ${prefix.slice(0, -1)}.`); - return value; -} - -/** @param {string} rawVersion */ -export function normalizeReleaseVersion(rawVersion) { - const version = rawVersion.trim().replace(/^v/u, ''); - if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version)) { - throw new Error(`Invalid pi-session-deck release version: ${rawVersion}`); - } - return version; -} - -/** @param {string} rawArch @returns {'arm64' | 'x64'} */ -export function normalizeArtifactArch(rawArch) { - switch (rawArch) { - case 'aarch64': - case 'arm64': - return 'arm64'; - case 'amd64': - case 'x64': - case 'x86_64': - return 'x64'; - default: - throw new Error(`Unsupported macOS artifact architecture: ${rawArch}`); - } -} - -/** - * @param {string} target - * @returns {'arm64' | 'x64'} - */ -export function artifactArchForTarget(target) { - switch (target) { - case 'aarch64-apple-darwin': - return 'arm64'; - case 'x86_64-apple-darwin': - return 'x64'; - default: - throw new Error(`Unsupported macOS release target: ${target}`); - } -} - -/** @param {string} target */ -export function bundleRootForTarget(target) { - return resolve(PACKAGE_ROOT, 'src-tauri/target', target, 'release/bundle'); -} - -/** @param {string} version @param {string} arch */ -export function macosArtifactStem(version, arch) { - return `${ARTIFACT_PREFIX}-v${normalizeReleaseVersion(version)}-macos-${normalizeArtifactArch(arch)}`; -} - -/** @param {unknown} value @returns {value is Record} */ -function isRecord(value) { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** @param {string} tauriConfigText @param {string} cargoTomlText @param {string} version */ -export function applyDesktopReleaseVersion(tauriConfigText, cargoTomlText, version) { - const normalizedVersion = normalizeReleaseVersion(version); - const tauriConfig = /** @type {Record} */ (JSON.parse(tauriConfigText)); - const bundle = isRecord(tauriConfig['bundle']) ? tauriConfig['bundle'] : {}; - const macOS = isRecord(bundle['macOS']) ? bundle['macOS'] : {}; - - tauriConfig['version'] = normalizedVersion; - macOS['bundleVersion'] = normalizedVersion; - bundle['macOS'] = macOS; - tauriConfig['bundle'] = bundle; - - const nextCargoTomlText = cargoTomlText.replace( - /(^\[package\][\s\S]*?^version = ").*?("$)/mu, - `$1${normalizedVersion}$2`, - ); - if ( - nextCargoTomlText === cargoTomlText && - !cargoTomlText.includes(`version = "${normalizedVersion}"`) - ) { - throw new Error('Could not find [package] version in src-tauri/Cargo.toml.'); - } - - return { - tauriConfigText: `${JSON.stringify(tauriConfig, null, 2)}\n`, - cargoTomlText: nextCargoTomlText, - }; -} - -/** @param {string} version */ -async function writeDesktopReleaseVersion(version) { - const [tauriConfigText, cargoTomlText] = await Promise.all([ - readFile(TAURI_CONF_PATH, 'utf8'), - readFile(CARGO_TOML_PATH, 'utf8'), - ]); - const next = applyDesktopReleaseVersion(tauriConfigText, cargoTomlText, version); - await Promise.all([ - writeFile(TAURI_CONF_PATH, next.tauriConfigText, 'utf8'), - writeFile(CARGO_TOML_PATH, next.cargoTomlText, 'utf8'), - ]); -} - -/** @param {string} command @param {string[]} args */ -async function runCommand(command, args) { - await new Promise((resolvePromise, reject) => { - const child = spawn(command, args, { cwd: PACKAGE_ROOT, stdio: 'inherit' }); - child.on('error', reject); - child.on('exit', (code, signal) => { - if (signal !== null) return reject(new Error(`${command} exited from signal ${signal}`)); - if (code !== 0) return reject(new Error(`${command} exited with code ${code ?? 1}`)); - resolvePromise(undefined); - }); - }); -} - -/** @param {string} command @param {string[]} args */ -async function runCommandOutput(command, args) { - return await new Promise((resolvePromise, reject) => { - const child = spawn(command, args, { - cwd: PACKAGE_ROOT, - stdio: ['ignore', 'pipe', 'pipe'], - }); - let output = ''; - child.stdout.on('data', (chunk) => (output += chunk.toString())); - child.stderr.on('data', (chunk) => (output += chunk.toString())); - child.on('error', reject); - child.on('exit', (code, signal) => { - if (signal !== null) return reject(new Error(`${command} exited from signal ${signal}`)); - if (code !== 0) { - return reject(new Error(`${command} exited with code ${code ?? 1}: ${output.trim()}`)); - } - resolvePromise(output.trim()); - }); - }); -} - -/** - * @param {string} output - * @param {'arm64' | 'x64'} arch - * @param {string} [path] - */ -export function assertExecutableArchitecture(output, arch, path = 'Desktop executable') { - const actualArchitectures = output.trim() === '' ? [] : output.trim().split(/\s+/u); - const expectedArchitecture = arch === 'arm64' ? 'arm64' : 'x86_64'; - if (actualArchitectures.length !== 1 || actualArchitectures[0] !== expectedArchitecture) { - throw new Error( - `${path} architecture is ${actualArchitectures.join(', ') || 'unknown'}, expected only ${expectedArchitecture}.`, - ); - } -} - -/** @param {string} details @param {string} [path] */ -export function assertAdHocSignature(details, path = 'Desktop app') { - if (!/^Signature=adhoc$/mu.test(details)) { - throw new Error(`${path} does not have an ad-hoc signature.`); - } - if (/^Authority=/mu.test(details)) { - throw new Error(`${path} unexpectedly has an authenticated signing authority.`); - } -} - -/** - * @param {string} appBundlePath - * @param {'arm64' | 'x64'} arch - */ -async function verifyAppBundle(appBundlePath, arch) { - const appStats = await stat(appBundlePath); - if (!appStats.isDirectory()) throw new Error(`Expected an app bundle: ${appBundlePath}`); - - const executableName = await runCommandOutput('/usr/libexec/PlistBuddy', [ - '-c', - 'Print :CFBundleExecutable', - join(appBundlePath, 'Contents/Info.plist'), - ]); - if (executableName.length === 0 || executableName.includes('/')) { - throw new Error(`Invalid CFBundleExecutable in ${appBundlePath}.`); - } - - const executablePath = join(appBundlePath, 'Contents/MacOS', executableName); - const executableStats = await stat(executablePath); - if ( - !executableStats.isFile() || - executableStats.size <= 0 || - (executableStats.mode & 0o111) === 0 - ) { - throw new Error(`Expected a non-empty executable file: ${executablePath}`); - } - - const architectures = await runCommandOutput('/usr/bin/lipo', ['-archs', executablePath]); - assertExecutableArchitecture(architectures, arch, executablePath); - - await runCommand('/usr/bin/codesign', [ - '--verify', - '--deep', - '--strict', - '--verbose=2', - appBundlePath, - ]); - const signatureDetails = await runCommandOutput('/usr/bin/codesign', [ - '-dv', - '--verbose=4', - appBundlePath, - ]); - assertAdHocSignature(signatureDetails, appBundlePath); -} - -/** @param {string} appBundlePath @param {string} zipPath */ -async function zipAppBundle(appBundlePath, zipPath) { - await runCommand('/usr/bin/ditto', [ - '-c', - '-k', - '--keepParent', - '--sequesterRsrc', - '--zlibCompressionLevel', - '9', - appBundlePath, - zipPath, - ]); -} - -/** @param {string} filePath */ -async function sha256File(filePath) { - const hash = createHash('sha256'); - await new Promise((resolvePromise, reject) => { - const stream = createReadStream(filePath); - stream.on('data', (chunk) => hash.update(chunk)); - stream.on('error', reject); - stream.on('end', () => resolvePromise(undefined)); - }); - return hash.digest('hex'); -} - -/** @param {string} artifactPath */ -export async function writeArtifactChecksum(artifactPath) { - const artifactStats = await stat(artifactPath); - if (!artifactStats.isFile() || artifactStats.size <= 0) { - throw new Error(`Artifact is not a non-empty regular file: ${artifactPath}`); - } - - const sha256 = await sha256File(artifactPath); - const checksumPath = `${artifactPath}.sha256`; - await writeFile(checksumPath, `${sha256} ${basename(artifactPath)}\n`, 'utf8'); - return { checksumPath, sha256 }; -} - -/** @param {MacosArtifactOptions} options */ -export function tauriBuildArgsForOptions(options) { - return ['build', '--bundles', 'app', '--ci', '--target', options.target]; -} - -/** @param {MacosArtifactOptions} options */ -export async function buildMacosArtifactsFromOptions(options) { - if (hostPlatform() !== 'darwin') { - throw new Error('macOS desktop artifacts must be built and verified on a macOS runner.'); - } - if (artifactArchForTarget(options.target) !== options.arch) { - throw new Error( - `Target ${options.target} does not match artifact architecture ${options.arch}.`, - ); - } - - await writeDesktopReleaseVersion(options.version); - const exitCode = await runTauri(tauriBuildArgsForOptions(options)); - if (exitCode !== 0) throw new Error(`tauri build exited with code ${exitCode}`); - - const appBundlePath = join(bundleRootForTarget(options.target), 'macos', `${PRODUCT_NAME}.app`); - await verifyAppBundle(appBundlePath, options.arch); - - await rm(options.artifactDir, { recursive: true, force: true }); - await mkdir(options.artifactDir, { recursive: true }); - const zipPath = join( - options.artifactDir, - `${macosArtifactStem(options.version, options.arch)}.zip`, - ); - await zipAppBundle(appBundlePath, zipPath); - const checksum = await writeArtifactChecksum(zipPath); - - return { version: options.version, arch: options.arch, zipPath, ...checksum }; -} - -/** @param {string[]} argv */ -export async function buildMacosArtifacts(argv = process.argv.slice(2)) { - return buildMacosArtifactsFromOptions(parseMacosArtifactArgs(argv)); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - try { - const result = await buildMacosArtifacts(); - console.log(`Prepared ${basename(result.zipPath)} and its SHA-256 sidecar.`); - } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; - } -} diff --git a/apps/session-deck-desktop/scripts/launch-branch.js b/apps/session-deck-desktop/scripts/launch-branch.js deleted file mode 100644 index 0938fec5..00000000 --- a/apps/session-deck-desktop/scripts/launch-branch.js +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env node -/* global process, console */ -import { spawn } from 'node:child_process'; -import { createHash, randomUUID } from 'node:crypto'; -import { access, chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'; -import { homedir, tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const DESKTOP_ROOT = resolve(SCRIPT_DIR, '..'); -const REPO_ROOT = resolve(DESKTOP_ROOT, '../..'); -const PACKAGE_NAME = '@robhowley/pi-session-deck'; -const PACKAGE_ROOT = resolve(REPO_ROOT, 'packages/pi-session-deck'); -const DESKTOP_FILTER = './apps/session-deck-desktop'; -const APP_BUNDLE_NAME = 'Session Deck Desktop.app'; -const APP_BUNDLE_PATH = resolve( - DESKTOP_ROOT, - `src-tauri/target/release/bundle/macos/${APP_BUNDLE_NAME}`, -); -const DEV_APP_PATH = resolve(DESKTOP_ROOT, 'src-tauri/target/debug/pi-session-deck-desktop'); -const INSTALLED_APP_PATH = join(homedir(), 'Applications', APP_BUNDLE_NAME); -const INSTALLED_STATE_PATH = join(homedir(), '.pi/session-deck/desktop/install.json'); -const STATE_ENV = 'PI_SESSION_DECK_DESKTOP_STATE_PATH'; - -/** - * @typedef {'dev' | 'bundle'} LaunchMode - * - * @typedef {{ - * mode: LaunchMode, - * openBundle: boolean, - * help: boolean, - * }} LaunchOptions - * - * @typedef {{ code: number }} CommandResult - * - * @typedef {{ - * cwd: string, - * env?: NodeJS.ProcessEnv, - * label: string, - * allowFailure?: boolean, - * }} RunOptions - * - * @typedef {{ level: string, message: string }} CommandMessage - */ - -/** - * @param {string[]} argv - * @returns {Promise} - */ -async function main(argv = process.argv.slice(2)) { - const options = parseArgs(argv); - if (options.help) { - console.log(usage()); - return 0; - } - - const [branch, version] = await Promise.all([branchSummary(), packageVersion()]); - - console.log('Session Deck Desktop branch launcher'); - console.log(`branch: ${branch}`); - console.log(`packageRoot: ${PACKAGE_ROOT}`); - console.log(`packageVersion: ${version}`); - console.log(''); - - await run('pnpm', ['--filter', PACKAGE_NAME, 'build'], { - cwd: REPO_ROOT, - label: `build ${PACKAGE_NAME}`, - }); - - return options.mode === 'bundle' ? bundleMode(options) : devMode(version); -} - -/** - * @param {string[]} argv - * @returns {LaunchOptions} - */ -function parseArgs(argv) { - /** @type {LaunchOptions} */ - const options = { mode: 'dev', openBundle: true, help: false }; - for (const arg of argv) { - switch (arg) { - case '-h': - case '--help': - options.help = true; - break; - case 'dev': - case '--dev': - case '--mode=dev': - options.mode = 'dev'; - break; - case 'bundle': - case 'app-bundle': - case 'release': - case '--bundle': - case '--mode=bundle': - case '--mode=app-bundle': - case '--mode=release': - options.mode = 'bundle'; - break; - case '--no-open': - options.openBundle = false; - break; - default: - throw new Error(`Unknown argument: ${arg}\n${usage()}`); - } - } - return options; -} - -function usage() { - return [ - 'Usage: pnpm --filter ./apps/session-deck-desktop dev:isolated', - ' pnpm --filter ./apps/session-deck-desktop launch:branch [dev]', - ' pnpm --filter ./apps/session-deck-desktop launch:branch bundle [--no-open]', - '', - 'Isolated dev mode builds @robhowley/pi-session-deck, writes temporary branch runtime', - 'metadata, and runs the local Tauri dev app. Bundle mode builds bundle:macos,', - 'installs the .app with branch package metadata, and opens it unless --no-open is set.', - ].join('\n'); -} - -/** - * @param {string} version - * @returns {Promise} - */ -async function devMode(version) { - const statePath = await writeBranchState(version); - const command = ['--filter', DESKTOP_FILTER, 'dev']; - - console.log('mode: dev'); - console.log(`statePath: ${statePath}`); - console.log(`stateEnv: ${STATE_ENV}=${statePath}`); - console.log(`devApp: ${DEV_APP_PATH}`); - console.log(`command: ${formatCommand('pnpm', command)}`); - console.log('stop: press Ctrl-C in this terminal to stop the Tauri dev process.'); - console.log(''); - - const result = await run('pnpm', command, { - cwd: REPO_ROOT, - env: { ...process.env, [STATE_ENV]: statePath }, - label: 'launch Tauri dev app', - }); - return result.code; -} - -/** - * @param {LaunchOptions} options - * @returns {Promise} - */ -async function bundleMode(options) { - if (process.platform !== 'darwin') { - console.error(`Bundle mode is only supported on macOS, not ${process.platform}.`); - return 1; - } - - console.log('mode: bundle'); - console.log(`appBundle: ${APP_BUNDLE_PATH}`); - console.log(`installedApp: ${INSTALLED_APP_PATH}`); - console.log(`installedState: ${INSTALLED_STATE_PATH}`); - console.log(''); - - const bundle = await run('pnpm', ['--filter', DESKTOP_FILTER, 'bundle:macos'], { - cwd: REPO_ROOT, - label: 'build macOS app bundle', - allowFailure: true, - }); - if (bundle.code !== 0) { - if (!(await exists(APP_BUNDLE_PATH))) { - console.error(`bundle:macos failed and no app bundle was found at ${APP_BUNDLE_PATH}.`); - return bundle.code; - } - console.warn(`bundle:macos exited ${bundle.code}; continuing because the .app exists.`); - } - - const { installSessionDeckDesktop } = await importDist('desktop/install.js'); - const install = await installSessionDeckDesktop({ fromPath: APP_BUNDLE_PATH }); - printResult('install', install); - if (install.level === 'error') { - return 1; - } - - console.log(`packageRoot: ${PACKAGE_ROOT}`); - console.log(`appPath: ${INSTALLED_APP_PATH}`); - console.log(`statePath: ${INSTALLED_STATE_PATH}`); - console.log( - 'signing: this branch bundle is ad-hoc signed, not Developer ID signed or notarized; macOS may require System Settings → Privacy & Security → Open Anyway.', - ); - - if (!options.openBundle) { - console.log('open: skipped (--no-open).'); - return 0; - } - - const { openSessionDeckDesktop } = await importDist('desktop/open.js'); - const opened = await openSessionDeckDesktop({}); - printResult('open', opened); - return opened.level === 'error' ? 1 : 0; -} - -/** - * @param {string} relativePath - * @returns {Promise} - */ -async function importDist(relativePath) { - return import( - pathToFileURL(resolve(PACKAGE_ROOT, 'dist/extensions/session-deck', relativePath)).href - ); -} - -/** - * @param {string} version - * @returns {Promise} - */ -async function writeBranchState(version) { - const dir = join( - tmpdir(), - `session-deck-desktop-branch-launcher-${process.getuid?.() ?? 'user'}`, - ); - const file = `${hash(REPO_ROOT).slice(0, 16)}.install.json`; - const path = join(dir, file); - const tempPath = join(dir, `.${file}.${process.pid}.${randomUUID()}.tmp`); - const state = createBranchState(version, new Date().toISOString()); - - try { - await mkdir(dir, { recursive: true, mode: 0o700 }); - await chmod(dir, 0o700); - await writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, { - encoding: 'utf8', - mode: 0o600, - }); - await chmod(tempPath, 0o600); - await rename(tempPath, path); - await chmod(path, 0o600); - return path; - } catch (error) { - throw new Error(`Could not write branch runtime state at ${path}: ${message(error)}`); - } -} - -/** - * @param {string} version - * @param {string} installedAt - */ -export function createBranchState(version, installedAt) { - return { - schemaVersion: 1, - product: 'session-deck-desktop-development', - packageName: PACKAGE_NAME, - packageVersion: version, - installedAt, - runtime: { - nodeExecutablePath: process.execPath, - packageRoot: PACKAGE_ROOT, - helperPackageVersion: version, - }, - }; -} - -async function branchSummary() { - const [branch, head] = await Promise.all([ - capture('git', ['branch', '--show-current']), - capture('git', ['rev-parse', '--short', 'HEAD']), - ]); - if (branch !== null && branch.length > 0) { - return head === null ? branch : `${branch} (${head})`; - } - return head === null ? 'unknown' : `detached (${head})`; -} - -async function packageVersion() { - const path = resolve(PACKAGE_ROOT, 'package.json'); - const packageJson = JSON.parse(await readFile(path, 'utf8')); - if (typeof packageJson.version !== 'string' || packageJson.version.trim().length === 0) { - throw new Error(`Could not determine package version from ${path}.`); - } - return packageJson.version; -} - -/** - * @param {string} command - * @param {string[]} args - * @param {RunOptions} options - * @returns {Promise} - */ -async function run(command, args, options) { - console.log(`$ ${formatCommand(command, args)}`); - return new Promise((resolvePromise, reject) => { - const child = spawn(command, args, { - cwd: options.cwd, - env: options.env ?? process.env, - stdio: 'inherit', - }); - child.on('error', reject); - child.on('exit', (code, signal) => { - if (signal !== null) { - if (options.allowFailure === true) { - resolvePromise({ code: 1 }); - return; - } - reject(new Error(`${options.label} stopped after signal ${signal}.`)); - return; - } - const exitCode = code ?? 1; - if (exitCode !== 0 && options.allowFailure !== true) { - reject(new Error(`${options.label} failed with exit code ${exitCode}.`)); - return; - } - resolvePromise({ code: exitCode }); - }); - }); -} - -/** - * @param {string} command - * @param {string[]} args - * @returns {Promise} - */ -async function capture(command, args) { - return new Promise((resolvePromise) => { - let stdout = ''; - const child = spawn(command, args, { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'ignore'] }); - child.stdout.on('data', (chunk) => { - stdout += chunk; - }); - child.on('error', () => resolvePromise(null)); - child.on('exit', (code) => { - const output = stdout.trim(); - resolvePromise(code === 0 && output.length > 0 ? output : null); - }); - }); -} - -/** - * @param {string} path - * @returns {Promise} - */ -async function exists(path) { - try { - await access(path); - return true; - } catch { - return false; - } -} - -/** - * @param {string} text - * @returns {string} - */ -function hash(text) { - return createHash('sha256').update(text).digest('hex'); -} - -/** - * @param {string} command - * @param {string[]} args - * @returns {string} - */ -function formatCommand(command, args) { - return [command, ...args.map(quoteArg)].join(' '); -} - -/** - * @param {string} arg - * @returns {string} - */ -function quoteArg(arg) { - return /^[A-Za-z0-9_./:=@-]+$/u.test(arg) ? arg : JSON.stringify(arg); -} - -/** - * @param {string} label - * @param {CommandMessage} result - * @returns {void} - */ -function printResult(label, result) { - const text = `${label}: ${result.message}`; - if (result.level === 'error') { - console.error(text); - } else if (result.level === 'warning') { - console.warn(text); - } else { - console.log(text); - } -} - -/** - * @param {unknown} error - * @returns {string} - */ -function message(error) { - return error instanceof Error ? error.message : String(error); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - try { - process.exitCode = await main(); - } catch (error) { - console.error(message(error)); - process.exitCode = 1; - } -} diff --git a/apps/session-deck-desktop/scripts/run-cargo.js b/apps/session-deck-desktop/scripts/run-cargo.js deleted file mode 100644 index 67ed085e..00000000 --- a/apps/session-deck-desktop/scripts/run-cargo.js +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env node -/* global process */ -import { constants } from 'node:fs'; -import { access } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { delimiter, dirname, join, resolve } from 'node:path'; -import { spawn } from 'node:child_process'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..'); - -/** - * @param {NodeJS.ProcessEnv} env - * @param {string} homeDirectory - * @returns {Promise<{ cargoPath: string, env: NodeJS.ProcessEnv }>} - */ -export async function buildCargoExecutionContext(env = process.env, homeDirectory = homedir()) { - const cargoHomeBin = join(homeDirectory, '.cargo', 'bin'); - const cargoPath = - (await findExecutableOnPath('cargo', env['PATH'])) ?? - ((await isExecutable(join(cargoHomeBin, 'cargo'))) ? join(cargoHomeBin, 'cargo') : null); - - if (cargoPath === null) { - throw new Error( - 'Could not find cargo. Install Rust with rustup and ensure cargo is available.', - ); - } - - return { - cargoPath, - env: { - ...env, - PATH: prependPathEntry(env['PATH'], cargoHomeBin), - }, - }; -} - -/** - * @param {string[]} argv - * @returns {Promise} - */ -export async function runCargo(argv = process.argv.slice(2)) { - const { cargoPath, env } = await buildCargoExecutionContext(); - return await new Promise((resolve, reject) => { - const child = spawn(cargoPath, argv, { - cwd: PACKAGE_ROOT, - env, - stdio: 'inherit', - }); - - child.on('error', reject); - child.on('exit', (code, signal) => { - if (signal !== null) { - reject(new Error(`cargo exited from signal ${signal}`)); - return; - } - resolve(code ?? 1); - }); - }); -} - -/** - * @param {string | undefined} currentPath - * @param {string} entry - * @returns {string} - */ -function prependPathEntry(currentPath, entry) { - if (typeof currentPath !== 'string' || currentPath.length === 0) { - return entry; - } - - const entries = currentPath.split(delimiter); - return entries.includes(entry) ? currentPath : [entry, ...entries].join(delimiter); -} - -/** - * @param {string} command - * @param {string | undefined} currentPath - * @returns {Promise} - */ -async function findExecutableOnPath(command, currentPath) { - if (typeof currentPath !== 'string' || currentPath.length === 0) { - return null; - } - - for (const entry of currentPath.split(delimiter)) { - if (entry.length === 0) { - continue; - } - - const candidate = join(entry, command); - if (await isExecutable(candidate)) { - return candidate; - } - } - - return null; -} - -/** - * @param {string} path - * @returns {Promise} - */ -async function isExecutable(path) { - try { - await access(path, constants.X_OK); - return true; - } catch { - return false; - } -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - process.exitCode = await runCargo(); -} diff --git a/apps/session-deck-desktop/scripts/run-tauri.js b/apps/session-deck-desktop/scripts/run-tauri.js deleted file mode 100644 index 1509a39f..00000000 --- a/apps/session-deck-desktop/scripts/run-tauri.js +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env node -/* global process */ -import { spawn } from 'node:child_process'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { buildCargoExecutionContext } from './run-cargo.js'; - -const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..'); - -/** - * @param {string[]} argv - * @returns {Promise} - */ -export async function runTauri(argv = process.argv.slice(2)) { - const { env } = await buildCargoExecutionContext(); - return await new Promise((resolve, reject) => { - const child = spawn('pnpm', ['exec', 'tauri', ...argv], { - cwd: PACKAGE_ROOT, - env, - stdio: 'inherit', - }); - - child.on('error', reject); - child.on('exit', (code, signal) => { - if (signal !== null) { - reject(new Error(`tauri exited from signal ${signal}`)); - return; - } - resolve(code ?? 1); - }); - }); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - process.exitCode = await runTauri(); -} diff --git a/apps/session-deck-desktop/scripts/sync-web-assets.js b/apps/session-deck-desktop/scripts/sync-web-assets.js deleted file mode 100644 index bb14cb96..00000000 --- a/apps/session-deck-desktop/scripts/sync-web-assets.js +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node -/* global process */ -import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; - -const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); -const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..'); -const SOURCE_WEB_ROOT = resolve( - PACKAGE_ROOT, - '../../packages/pi-session-deck/extensions/session-deck/iterm2/web', -); -const DESTINATION_WEB_ROOT = resolve(PACKAGE_ROOT, 'web'); -const ACTION_TOKEN_META_PATTERN = /\n\s*/u; -const CANONICAL_SCRIPT_TAGS_PATTERN = - /\n\s*', - '', -].join('\n '); - -/** - * @param {string} sourceIndex - * @returns {string} - */ -export function buildDesktopIndex(sourceIndex) { - if (!CANONICAL_SCRIPT_TAGS_PATTERN.test(sourceIndex)) { - throw new Error( - 'Canonical Session Deck index.html no longer has the expected shared-ui/iTerm2/app script tags.', - ); - } - - return sourceIndex - .replace(ACTION_TOKEN_META_PATTERN, '') - .replaceAll('href="/style.css"', 'href="./style.css"') - .replace(CANONICAL_SCRIPT_TAGS_PATTERN, `\n ${DESKTOP_SCRIPT_TAGS}`); -} - -/** - * @param {{ sourceWebRoot?: string, destinationWebRoot?: string }} [options] - */ -export async function syncWebAssets(options = {}) { - const sourceWebRoot = options.sourceWebRoot ?? SOURCE_WEB_ROOT; - const destinationWebRoot = options.destinationWebRoot ?? DESTINATION_WEB_ROOT; - const [sourceIndex, sourceStyle, sourceSharedUi] = await Promise.all([ - readFile(resolve(sourceWebRoot, 'index.html'), 'utf8'), - readFile(resolve(sourceWebRoot, 'style.css'), 'utf8'), - readFile(resolve(sourceWebRoot, 'session-deck-ui.js')), - ]); - - await mkdir(destinationWebRoot, { recursive: true }); - await Promise.all([ - writeFile(resolve(destinationWebRoot, 'index.html'), buildDesktopIndex(sourceIndex), 'utf8'), - writeFile(resolve(destinationWebRoot, 'style.css'), sourceStyle, 'utf8'), - writeFile(resolve(destinationWebRoot, 'session-deck-ui.js'), sourceSharedUi), - ]); -} - -if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - await syncWebAssets(); -} diff --git a/apps/session-deck-desktop/src-tauri/Cargo.lock b/apps/session-deck-desktop/src-tauri/Cargo.lock deleted file mode 100644 index a880e389..00000000 --- a/apps/session-deck-desktop/src-tauri/Cargo.lock +++ /dev/null @@ -1,4825 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" - -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.1", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.2.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dom_query" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 1.1.3+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fax" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix", - "windows-link 0.2.1", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.1", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" -dependencies = [ - "byteorder", - "png 0.17.16", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png 0.18.1", - "tiff", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.13.1", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "markup5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", -] - -[[package]] -name = "muda" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "open" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" -dependencies = [ - "is-wsl", - "libc", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pi-session-deck-desktop" -version = "0.0.0" -dependencies = [ - "arboard", - "dirs 5.0.1", - "libc", - "open", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tempfile", - "url", - "wait-timeout", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plist" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" -dependencies = [ - "base64 0.22.1", - "indexmap 2.14.0", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.119", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" -dependencies = [ - "bitflags 2.13.1", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.35.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" -dependencies = [ - "bitflags 2.13.1", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "once_cell", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs 6.0.0", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.18", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs 6.0.0", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png 0.17.16", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.119", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-runtime" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.18", - "toml 1.1.3+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" -dependencies = [ - "dunce", - "embed-resource", - "toml 1.1.3+spec-1.1.0", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tiff" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", -] - -[[package]] -name = "time" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.14.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" -dependencies = [ - "crossbeam-channel", - "dirs 6.0.0", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.18", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wry" -version = "0.55.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs 6.0.0", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "gethostname", - "rustix", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zune-core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" - -[[package]] -name = "zune-jpeg" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" -dependencies = [ - "zune-core", -] diff --git a/apps/session-deck-desktop/src-tauri/Cargo.toml b/apps/session-deck-desktop/src-tauri/Cargo.toml deleted file mode 100644 index 7dd2ac97..00000000 --- a/apps/session-deck-desktop/src-tauri/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "pi-session-deck-desktop" -version = "0.0.0" -description = "Tauri desktop companion for Session Deck" -edition = "2021" - -[lib] -name = "pi_session_deck_desktop" -path = "src/lib.rs" - -[[bin]] -name = "pi-session-deck-desktop" -path = "src/main.rs" - -[build-dependencies] -tauri-build = { version = "2", features = [] } - -[dependencies] -arboard = "3" -dirs = "5" -libc = "0.2" -open = "5" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tauri = { version = "2", features = ["tray-icon"] } -url = "2" -wait-timeout = "0.2" - -[dev-dependencies] -tempfile = "3" diff --git a/apps/session-deck-desktop/src-tauri/Info.plist b/apps/session-deck-desktop/src-tauri/Info.plist deleted file mode 100644 index 48dd8fd1..00000000 --- a/apps/session-deck-desktop/src-tauri/Info.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - CFBundleDisplayName - Session Deck - - diff --git a/apps/session-deck-desktop/src-tauri/build.rs b/apps/session-deck-desktop/src-tauri/build.rs deleted file mode 100644 index d860e1e6..00000000 --- a/apps/session-deck-desktop/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/apps/session-deck-desktop/src-tauri/capabilities/default.json b/apps/session-deck-desktop/src-tauri/capabilities/default.json deleted file mode 100644 index 63836c10..00000000 --- a/apps/session-deck-desktop/src-tauri/capabilities/default.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Default capability for the Session Deck desktop window.", - "windows": ["main"], - "permissions": ["core:default"] -} diff --git a/apps/session-deck-desktop/src-tauri/gen/schemas/acl-manifests.json b/apps/session-deck-desktop/src-tauri/gen/schemas/acl-manifests.json deleted file mode 100644 index 0eebfc46..00000000 --- a/apps/session-deck-desktop/src-tauri/gen/schemas/acl-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/session-deck-desktop/src-tauri/gen/schemas/capabilities.json b/apps/session-deck-desktop/src-tauri/gen/schemas/capabilities.json deleted file mode 100644 index f023237b..00000000 --- a/apps/session-deck-desktop/src-tauri/gen/schemas/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"default":{"identifier":"default","description":"Default capability for the Session Deck desktop window.","local":true,"windows":["main"],"permissions":["core:default"]}} \ No newline at end of file diff --git a/apps/session-deck-desktop/src-tauri/gen/schemas/desktop-schema.json b/apps/session-deck-desktop/src-tauri/gen/schemas/desktop-schema.json deleted file mode 100644 index 32866459..00000000 --- a/apps/session-deck-desktop/src-tauri/gen/schemas/desktop-schema.json +++ /dev/null @@ -1,2292 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - } - } -} \ No newline at end of file diff --git a/apps/session-deck-desktop/src-tauri/gen/schemas/macOS-schema.json b/apps/session-deck-desktop/src-tauri/gen/schemas/macOS-schema.json deleted file mode 100644 index 32866459..00000000 --- a/apps/session-deck-desktop/src-tauri/gen/schemas/macOS-schema.json +++ /dev/null @@ -1,2292 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - } - } -} \ No newline at end of file diff --git a/apps/session-deck-desktop/src-tauri/icons/icon.icns b/apps/session-deck-desktop/src-tauri/icons/icon.icns deleted file mode 100644 index e83265c5..00000000 Binary files a/apps/session-deck-desktop/src-tauri/icons/icon.icns and /dev/null differ diff --git a/apps/session-deck-desktop/src-tauri/icons/icon.png b/apps/session-deck-desktop/src-tauri/icons/icon.png deleted file mode 100644 index 9f8e5b90..00000000 Binary files a/apps/session-deck-desktop/src-tauri/icons/icon.png and /dev/null differ diff --git a/apps/session-deck-desktop/src-tauri/icons/tray-icon.png b/apps/session-deck-desktop/src-tauri/icons/tray-icon.png deleted file mode 100644 index 557e52f8..00000000 Binary files a/apps/session-deck-desktop/src-tauri/icons/tray-icon.png and /dev/null differ diff --git a/apps/session-deck-desktop/src-tauri/icons/tray-icon.svg b/apps/session-deck-desktop/src-tauri/icons/tray-icon.svg deleted file mode 100644 index 027acbc0..00000000 --- a/apps/session-deck-desktop/src-tauri/icons/tray-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/session-deck-desktop/src-tauri/src/commands.rs b/apps/session-deck-desktop/src-tauri/src/commands.rs deleted file mode 100644 index b428afb0..00000000 --- a/apps/session-deck-desktop/src-tauri/src/commands.rs +++ /dev/null @@ -1,621 +0,0 @@ -use crate::doctor; -use crate::helper_runner::{self, CommandError, CommandErrorPayload}; -use crate::runtime::DoctorStatus; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use url::Url; - -const MAX_RUNTIME_ID_LENGTH: usize = 256; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RepoIntentRequest { - pub candidate_runtime_ids: Vec, - #[serde(default)] - pub preferred_runtime_id: Option, - #[serde(default)] - pub qualified_repo_name: Option, - #[serde(default)] - pub repo_name: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum LaunchMode { - #[serde(rename = "tmux-detached")] - TmuxDetached, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum LaunchAgentDirMode { - #[serde(rename = "ambient")] - Ambient, - #[serde(rename = "default")] - Default, - #[serde(rename = "custom")] - Custom, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LaunchAgentDirRequest { - pub mode: LaunchAgentDirMode, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub custom_dir: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LaunchRequest { - pub mode: LaunchMode, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub agent_dir: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PreviewWorktreeBaseRefRequest { - pub repo_intent: RepoIntentRequest, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PreviewWorktreeLaunchContextRequest { - #[serde(default = "default_launch_request")] - pub launch: LaunchRequest, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CreateWorktreeRequest { - pub repo_intent: RepoIntentRequest, - pub branch_name: String, - #[serde(default)] - pub base_ref: Option, - #[serde(default = "default_launch_request")] - pub launch: LaunchRequest, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CreateSessionRequest { - pub action: CreateSessionAction, - pub cwd: String, - #[serde(default = "default_launch_request")] - pub launch: LaunchRequest, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum CreateSessionAction { - #[serde(rename = "create-session")] - CreateSession, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct OpenTerminalRequest { - pub runtime_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct KillSessionRequest { - pub runtime_id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct RestartSessionRequest { - pub runtime_id: String, - pub generation: String, - pub operation_id: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct OperationStatus { - pub ok: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -impl RepoIntentRequest { - pub fn validate(&self) -> Result<(), CommandError> { - if self.candidate_runtime_ids.is_empty() - && self.preferred_runtime_id.is_none() - && self.qualified_repo_name.is_none() - && self.repo_name.is_none() - { - return Err(CommandError::new( - "repoIntent must identify at least one repository or runtime.", - )); - } - - for runtime_id in &self.candidate_runtime_ids { - validate_runtime_id(runtime_id)?; - } - - if let Some(preferred_runtime_id) = &self.preferred_runtime_id { - validate_runtime_id(preferred_runtime_id)?; - } - - Ok(()) - } -} - -impl LaunchAgentDirRequest { - pub fn validate(&self) -> Result<(), CommandError> { - match self.mode { - LaunchAgentDirMode::Ambient | LaunchAgentDirMode::Default => { - if self.custom_dir.is_some() { - return Err(CommandError::new( - "launch.agentDir.customDir is only valid for custom mode.", - )); - } - Ok(()) - } - LaunchAgentDirMode::Custom => { - let custom_dir = self.custom_dir.as_deref().ok_or_else(|| { - CommandError::new("launch.agentDir.customDir is required for custom mode.") - })?; - validate_custom_agent_dir(custom_dir) - } - } - } -} - -impl LaunchRequest { - pub fn validate(&self) -> Result<(), CommandError> { - if let Some(agent_dir) = &self.agent_dir { - agent_dir.validate()?; - } - Ok(()) - } -} - -impl PreviewWorktreeBaseRefRequest { - pub fn validate(&self) -> Result<(), CommandError> { - self.repo_intent.validate() - } -} - -impl PreviewWorktreeLaunchContextRequest { - pub fn validate(&self) -> Result<(), CommandError> { - self.launch.validate() - } -} - -impl CreateWorktreeRequest { - pub fn validate(&self) -> Result<(), CommandError> { - self.repo_intent.validate()?; - if self.branch_name.trim().is_empty() { - return Err(CommandError::new("branchName must be a non-empty string.")); - } - - if let Some(base_ref) = &self.base_ref { - if base_ref.trim().is_empty() { - return Err(CommandError::new( - "baseRef must not be empty when provided.", - )); - } - } - - self.launch.validate() - } -} - -impl CreateSessionRequest { - pub fn validate(&self) -> Result<(), CommandError> { - validate_cwd(&self.cwd)?; - self.launch.validate() - } -} - -impl OpenTerminalRequest { - pub fn validate(&self) -> Result<(), CommandError> { - validate_runtime_id(&self.runtime_id) - } -} - -impl KillSessionRequest { - pub fn validate(&self) -> Result<(), CommandError> { - validate_runtime_id(&self.runtime_id) - } -} - -impl RestartSessionRequest { - pub fn validate(&self) -> Result<(), CommandError> { - validate_runtime_id(&self.runtime_id)?; - if !is_uuid_v4(&self.runtime_id) { - return Err(CommandError::new("runtimeId must be a UUID v4.")); - } - validate_opaque_token("generation", &self.generation, 16, 128)?; - validate_opaque_token("operationId", &self.operation_id, 1, 128) - } -} - -#[tauri::command] -pub async fn load_snapshot() -> Result { - run_blocking(helper_runner::load_snapshot).await -} - -#[tauri::command] -pub async fn preview_worktree_base_ref( - request: PreviewWorktreeBaseRefRequest, -) -> Result { - run_blocking(move || helper_runner::preview_worktree_base_ref(request)).await -} - -#[tauri::command] -pub async fn preview_worktree_launch_context( - request: PreviewWorktreeLaunchContextRequest, -) -> Result { - run_blocking(move || helper_runner::preview_worktree_launch_context(request)).await -} - -#[tauri::command] -pub async fn create_worktree(request: CreateWorktreeRequest) -> Result { - run_mutating_blocking(move || helper_runner::create_worktree(request)).await -} - -#[tauri::command] -pub async fn create_session(request: CreateSessionRequest) -> Result { - run_mutating_blocking(move || helper_runner::create_session(request)).await -} - -#[tauri::command] -pub async fn open_terminal(request: OpenTerminalRequest) -> Result { - run_mutating_blocking(move || helper_runner::open_terminal(request)).await -} - -#[tauri::command] -pub async fn kill_session(request: KillSessionRequest) -> Result { - run_mutating_blocking(move || helper_runner::kill_session(request)).await -} - -#[tauri::command] -pub async fn restart_session(request: RestartSessionRequest) -> Result { - run_mutating_blocking(move || helper_runner::restart_session(request)).await -} - -#[tauri::command] -pub fn open_external(url: String) -> OperationStatus { - if !is_supported_external_url(&url) { - return OperationStatus::failed("Only http:// and https:// URLs are supported."); - } - - match open::that_detached(url) { - Ok(()) => OperationStatus::ok(), - Err(_) => OperationStatus::failed("Could not open the external link."), - } -} - -#[tauri::command] -pub fn copy_text(text: String) -> OperationStatus { - match arboard::Clipboard::new().and_then(|mut clipboard| clipboard.set_text(text)) { - Ok(()) => OperationStatus::ok(), - Err(_) => OperationStatus::failed("Could not copy text to the clipboard."), - } -} - -#[tauri::command] -pub fn doctor_status() -> DoctorStatus { - doctor::doctor_status() -} - -impl OperationStatus { - fn ok() -> Self { - Self { - ok: true, - message: None, - } - } - - fn failed(message: impl Into) -> Self { - Self { - ok: false, - message: Some(message.into()), - } - } -} - -fn default_launch_request() -> LaunchRequest { - LaunchRequest { - mode: LaunchMode::TmuxDetached, - agent_dir: None, - } -} - -fn is_supported_external_url(url: &str) -> bool { - Url::parse(url) - .map(|parsed| matches!(parsed.scheme(), "http" | "https")) - .unwrap_or(false) -} - -fn validate_cwd(cwd: &str) -> Result<(), CommandError> { - let trimmed = cwd.trim(); - if trimmed.is_empty() { - return Err(CommandError::new("cwd must be a non-empty string.")); - } - - if cwd.contains('\0') || cwd.contains('\r') || cwd.contains('\n') { - return Err(CommandError::new( - "cwd must not contain newlines or NUL bytes.", - )); - } - - if !(trimmed == "~" || trimmed.starts_with("~/") || trimmed.starts_with('/')) { - return Err(CommandError::new( - "cwd must be absolute, ~, or start with ~/.", - )); - } - - Ok(()) -} - -fn is_uuid_v4(value: &str) -> bool { - let bytes = value.as_bytes(); - bytes.len() == 36 - && [8, 13, 18, 23].iter().all(|index| bytes[*index] == b'-') - && bytes[14] == b'4' - && matches!(bytes[19].to_ascii_lowercase(), b'8' | b'9' | b'a' | b'b') - && bytes - .iter() - .enumerate() - .all(|(index, byte)| [8, 13, 18, 23].contains(&index) || byte.is_ascii_hexdigit()) -} - -fn validate_opaque_token( - field: &str, - value: &str, - min_length: usize, - max_length: usize, -) -> Result<(), CommandError> { - if value.len() < min_length - || value.len() > max_length - || !value - .bytes() - .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) - { - return Err(CommandError::new(format!("{field} is invalid."))); - } - Ok(()) -} - -fn validate_runtime_id(runtime_id: &str) -> Result<(), CommandError> { - if runtime_id.is_empty() { - return Err(CommandError::new("runtimeId must be a non-empty string.")); - } - - if runtime_id.trim() != runtime_id || runtime_id.chars().any(char::is_whitespace) { - return Err(CommandError::new( - "runtimeId must be a safe identity segment.", - )); - } - - if runtime_id.len() > MAX_RUNTIME_ID_LENGTH { - return Err(CommandError::new("runtimeId is too long.")); - } - - if runtime_id == "." || runtime_id == ".." { - return Err(CommandError::new( - "runtimeId must be a safe identity segment.", - )); - } - - if runtime_id.contains('/') - || runtime_id.contains('\\') - || runtime_id.chars().any(is_control_character) - { - return Err(CommandError::new( - "runtimeId must be a safe identity segment.", - )); - } - - Ok(()) -} - -fn validate_custom_agent_dir(custom_dir: &str) -> Result<(), CommandError> { - if custom_dir.contains('\0') || custom_dir.contains('\r') || custom_dir.contains('\n') { - return Err(CommandError::new( - "launch.agentDir.customDir must not contain newlines or NUL bytes.", - )); - } - - let trimmed = custom_dir.trim(); - if trimmed.is_empty() { - return Err(CommandError::new( - "launch.agentDir.customDir must be a non-empty string.", - )); - } - - if !(trimmed.starts_with('/') || trimmed.starts_with("~/")) { - return Err(CommandError::new( - "launch.agentDir.customDir must be absolute or start with ~/.", - )); - } - - Ok(()) -} - -fn is_control_character(character: char) -> bool { - character.is_control() -} - -async fn run_blocking(operation: F) -> Result -where - T: Send + 'static, - F: FnOnce() -> Result + Send + 'static, -{ - tauri::async_runtime::spawn_blocking(operation) - .await - .map_err(|error| format!("Desktop command worker failed: {error}"))? - .map_err(CommandError::into_public_message) -} - -async fn run_mutating_blocking(operation: F) -> Result -where - T: Send + 'static, - F: FnOnce() -> Result + Send + 'static, -{ - tauri::async_runtime::spawn_blocking(operation) - .await - .map_err(|error| { - CommandErrorPayload::Message(format!("Desktop command worker failed: {error}")) - })? - .map_err(CommandError::into_tauri_error) -} - -#[cfg(test)] -mod tests { - use super::{ - CreateSessionAction, CreateSessionRequest, CreateWorktreeRequest, KillSessionRequest, - LaunchAgentDirMode, LaunchMode, OpenTerminalRequest, PreviewWorktreeLaunchContextRequest, - RestartSessionRequest, - }; - use serde_json::json; - - #[test] - fn create_worktree_defaults_to_tmux_detached_launch() { - let parsed: CreateWorktreeRequest = serde_json::from_value(json!({ - "repoIntent": { - "candidateRuntimeIds": ["runtime-1"], - "repoName": "pi-userland" - }, - "branchName": "feat/desktop-shell" - })) - .unwrap(); - - assert_eq!(parsed.launch.mode, LaunchMode::TmuxDetached); - assert_eq!(parsed.launch.agent_dir, None); - } - - #[test] - fn preview_worktree_launch_context_defaults_to_tmux_detached_launch() { - let parsed: PreviewWorktreeLaunchContextRequest = - serde_json::from_value(json!({})).unwrap(); - - assert_eq!(parsed.launch.mode, LaunchMode::TmuxDetached); - assert_eq!(parsed.launch.agent_dir, None); - } - - #[test] - fn create_worktree_accepts_custom_agent_dir() { - let parsed: CreateWorktreeRequest = serde_json::from_value(json!({ - "repoIntent": { - "candidateRuntimeIds": ["runtime-1"], - "repoName": "pi-userland" - }, - "branchName": "feat/desktop-shell", - "launch": { - "mode": "tmux-detached", - "agentDir": { - "mode": "custom", - "customDir": "~/agent-work" - } - } - })) - .unwrap(); - - assert_eq!(parsed.launch.mode, LaunchMode::TmuxDetached); - assert_eq!( - parsed - .launch - .agent_dir - .as_ref() - .map(|agent_dir| agent_dir.mode.clone()), - Some(LaunchAgentDirMode::Custom) - ); - } - - #[test] - fn create_session_accepts_custom_agent_dir() { - let parsed: CreateSessionRequest = serde_json::from_value(json!({ - "action": "create-session", - "cwd": "~/scratch", - "launch": { - "mode": "tmux-detached", - "agentDir": { - "mode": "custom", - "customDir": "~/agent-work" - } - } - })) - .unwrap(); - - parsed.validate().unwrap(); - assert_eq!(parsed.action, CreateSessionAction::CreateSession); - assert_eq!(parsed.launch.mode, LaunchMode::TmuxDetached); - assert_eq!( - parsed - .launch - .agent_dir - .as_ref() - .map(|agent_dir| agent_dir.mode.clone()), - Some(LaunchAgentDirMode::Custom) - ); - } - - #[test] - fn create_session_rejects_relative_cwd() { - let request: CreateSessionRequest = serde_json::from_value(json!({ - "action": "create-session", - "cwd": "relative/path" - })) - .unwrap(); - - let error = request.validate().unwrap_err().into_public_message(); - assert!(error.contains("cwd must be absolute")); - } - - #[test] - fn open_terminal_request_rejects_whitespace_runtime_ids() { - let request = OpenTerminalRequest { - runtime_id: "runtime id".into(), - }; - - let error = request.validate().unwrap_err(); - assert_eq!( - error.into_public_message(), - "runtimeId must be a safe identity segment." - ); - } - - #[test] - fn restart_session_accepts_only_browser_safe_identity_tokens() { - let request: RestartSessionRequest = serde_json::from_value(json!({ - "runtimeId": "123e4567-e89b-42d3-a456-426614174000", - "generation": "opaque-generation-token", - "operationId": "operation-1" - })) - .unwrap(); - request.validate().unwrap(); - assert!(RestartSessionRequest { - runtime_id: "legacy-runtime".into(), - generation: "opaque-generation-token".into(), - operation_id: "operation-1".into(), - } - .validate() - .is_err()); - - assert!(serde_json::from_value::(json!({ - "runtimeId": "123e4567-e89b-42d3-a456-426614174000", - "generation": "opaque-generation-token", - "operationId": "operation-1", - "sessionFile": "/private/session.jsonl" - })) - .is_err()); - } - - #[test] - fn kill_session_request_rejects_unsafe_runtime_ids() { - let request = KillSessionRequest { - runtime_id: String::from("../runtime-1"), - }; - - let error = request.validate().unwrap_err(); - assert_eq!( - error.into_public_message(), - "runtimeId must be a safe identity segment." - ); - } -} diff --git a/apps/session-deck-desktop/src-tauri/src/doctor.rs b/apps/session-deck-desktop/src-tauri/src/doctor.rs deleted file mode 100644 index c016ebb2..00000000 --- a/apps/session-deck-desktop/src-tauri/src/doctor.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::runtime::{discover_runtime, DoctorStatus}; - -pub fn doctor_status() -> DoctorStatus { - discover_runtime().status -} diff --git a/apps/session-deck-desktop/src-tauri/src/helper_runner.rs b/apps/session-deck-desktop/src-tauri/src/helper_runner.rs deleted file mode 100644 index 5a8308e3..00000000 --- a/apps/session-deck-desktop/src-tauri/src/helper_runner.rs +++ /dev/null @@ -1,968 +0,0 @@ -use crate::commands::{ - CreateSessionRequest, CreateWorktreeRequest, KillSessionRequest, OpenTerminalRequest, - PreviewWorktreeBaseRefRequest, PreviewWorktreeLaunchContextRequest, RestartSessionRequest, -}; -use crate::runtime::{load_runtime_config, RuntimeConfig, OPEN_TERMINAL_ACTION_BRIDGE_SOCKET_ENV}; -use serde::Serialize; -use serde_json::{json, Value}; -use std::io::{Read, Write}; -use std::os::unix::process::CommandExt; -use std::process::{Child, Command, Stdio}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; -use wait_timeout::ChildExt; - -const SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(10); -const ACTION_TIMEOUT: Duration = Duration::from_secs(60); -const MUTATING_HELPER_TIMEOUT_CODE: &str = "mutating-helper-timeout"; -const MUTATING_HELPER_TIMEOUT_MESSAGE: &str = - "The desktop helper timed out before Session Deck could confirm whether the action completed."; - -#[derive(Debug, PartialEq, Serialize)] -#[serde(untagged)] -pub enum CommandErrorPayload { - Message(String), - OutcomeUnknown { - code: &'static str, - message: &'static str, - #[serde(rename = "outcomeUnknown")] - outcome_unknown: bool, - }, -} - -#[derive(Debug)] -pub struct CommandError { - payload: CommandErrorPayload, - detail: Option, -} - -impl CommandError { - pub fn new(public_message: impl Into) -> Self { - Self { - payload: CommandErrorPayload::Message(public_message.into()), - detail: None, - } - } - - pub fn with_detail(public_message: impl Into, detail: impl Into) -> Self { - Self { - payload: CommandErrorPayload::Message(public_message.into()), - detail: Some(detail.into()), - } - } - - fn timeout(timeout: Duration, outcome_unknown: bool, public_message: &str) -> Self { - let payload = if outcome_unknown { - CommandErrorPayload::OutcomeUnknown { - code: MUTATING_HELPER_TIMEOUT_CODE, - message: MUTATING_HELPER_TIMEOUT_MESSAGE, - outcome_unknown: true, - } - } else { - CommandErrorPayload::Message(String::from(public_message)) - }; - - Self { - payload, - detail: Some(format!( - "Helper process timed out after {} seconds.", - timeout.as_secs_f64() - )), - } - } - - pub fn into_public_message(self) -> String { - match self.into_tauri_error() { - CommandErrorPayload::Message(message) => message, - CommandErrorPayload::OutcomeUnknown { message, .. } => String::from(message), - } - } - - pub fn into_tauri_error(self) -> CommandErrorPayload { - if let Some(detail) = self.detail { - eprintln!("{detail}"); - } - self.payload - } -} - -pub fn load_snapshot() -> Result { - let runtime_config = load_config_for_command()?; - let output = run_helper( - &runtime_config, - HelperSpec { - script_path: &runtime_config.snapshot_helper_path, - stdin_payload: None, - timeout: SNAPSHOT_TIMEOUT, - bridge_socket_path: None, - public_error_message: - "Session Deck snapshot is unavailable. Open desktop diagnostics for details.", - outcome_unknown_on_timeout: false, - }, - )?; - - if !output.success { - return Err(CommandError::with_detail( - "Session Deck snapshot is unavailable. Open desktop diagnostics for details.", - format!( - "Snapshot helper exited with a non-zero status. {}", - format_process_detail(&output.stdout, &output.stderr) - ), - )); - } - - parse_json_object( - &output.stdout, - "Session Deck snapshot helper returned invalid JSON.", - "snapshot-helper-invalid-json", - ) -} - -pub fn preview_worktree_base_ref( - request: PreviewWorktreeBaseRefRequest, -) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.worktree_action_helper_path, - json!({ - "action": "preview-base-ref", - "repoIntent": request.repo_intent, - }), - "Create-worktree preview is unavailable. Open desktop diagnostics for details.", - false, - ) -} - -pub fn preview_worktree_launch_context( - request: PreviewWorktreeLaunchContextRequest, -) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.worktree_action_helper_path, - json!({ - "action": "preview-launch-context", - "launch": request.launch, - }), - "Pi config preview is unavailable. Open desktop diagnostics for details.", - false, - ) -} - -pub fn create_worktree(request: CreateWorktreeRequest) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.worktree_action_helper_path, - serde_json::to_value(&request).map_err(|error| { - CommandError::with_detail( - "Create-worktree request is invalid.", - format!("Could not serialize the worktree request: {error}"), - ) - })?, - "Create-worktree action is unavailable. Open desktop diagnostics for details.", - true, - ) -} - -pub fn create_session(request: CreateSessionRequest) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.worktree_action_helper_path, - serde_json::to_value(&request).map_err(|error| { - CommandError::with_detail( - "Create-session request is invalid.", - format!("Could not serialize the create-session request: {error}"), - ) - })?, - "Create-session action is unavailable. Open desktop diagnostics for details.", - true, - ) -} - -pub fn open_terminal(request: OpenTerminalRequest) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.open_action_helper_path, - serde_json::to_value(&request).map_err(|error| { - CommandError::with_detail( - "Open-terminal request is invalid.", - format!("Could not serialize the open-terminal request: {error}"), - ) - })?, - "Open-terminal action is unavailable. Open desktop diagnostics for details.", - true, - ) -} - -pub fn restart_session(request: RestartSessionRequest) -> Result { - request.validate()?; - let operation_id = request.operation_id.clone(); - let runtime_config = load_config_for_command()?; - match run_action_helper( - &runtime_config, - &runtime_config.worktree_action_helper_path, - json!({ - "action": "restart-session", - "runtimeId": request.runtime_id, - "generation": request.generation, - "operationId": request.operation_id, - }), - "Restart-session action is unavailable. Open desktop diagnostics for details.", - true, - ) { - Ok(value) => validate_restart_session_result(value, &operation_id), - Err(error) => map_restart_helper_error(error, &operation_id), - } -} - -fn map_restart_helper_error( - error: CommandError, - operation_id: &str, -) -> Result { - if matches!(&error.payload, CommandErrorPayload::OutcomeUnknown { .. }) { - return Ok(restart_outcome_unknown(operation_id)); - } - Err(error) -} - -fn restart_outcome_unknown(operation_id: &str) -> Value { - json!({ - "ok": false, - "status": "outcome-unknown", - "operationId": operation_id, - "reason": "operation-state-unknown", - "retryable": true, - "message": "Session Deck could not confirm the restart outcome. Reconcile before retrying.", - }) -} - -fn validate_restart_session_result( - value: Value, - expected_operation_id: &str, -) -> Result { - let Some(result) = value.as_object() else { - return Err(invalid_restart_result()); - }; - let expected_keys = [ - "ok", - "status", - "operationId", - "reason", - "retryable", - "message", - ]; - if result.len() != expected_keys.len() - || expected_keys.iter().any(|key| !result.contains_key(*key)) - { - return Err(invalid_restart_result()); - } - let status = result.get("status").and_then(Value::as_str); - let reason = result.get("reason").and_then(Value::as_str); - let retryable = result.get("retryable").and_then(Value::as_bool); - let ok = result.get("ok").and_then(Value::as_bool); - let operation_id = result.get("operationId").and_then(Value::as_str); - if !matches!( - operation_id, - Some(value) if !value.is_empty() && value == expected_operation_id - ) || result.get("message").and_then(Value::as_str).is_none() - || !is_coherent_restart_result(status, reason, retryable, ok) - { - return Err(invalid_restart_result()); - } - Ok(value) -} - -fn is_coherent_restart_result( - status: Option<&str>, - reason: Option<&str>, - retryable: Option, - ok: Option, -) -> bool { - // Keep this table aligned with restart domain outcomes; retain recipe-invalid for compatibility. - matches!( - (status, reason, retryable, ok), - ( - Some("restarted"), - Some("replacement-observed"), - Some(false), - Some(true) - ) | ( - Some("not-eligible"), - Some("managed-recipe-unavailable"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("recipe-not-bound"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("recipe-invalid"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("runtime-unavailable"), - Some(true), - Some(false) - ) | ( - Some("not-eligible"), - Some("identity-mismatch"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("session-file-unavailable"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("cwd-unavailable"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("pi-executable-unavailable"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("tmux-target-unavailable"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("tmux-pane-mismatch"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("unsafe-descendants"), - Some(true), - Some(false) - ) | ( - Some("not-eligible"), - Some("hosting-runtime"), - Some(false), - Some(false) - ) | ( - Some("not-eligible"), - Some("coordinator-runtime"), - Some(false), - Some(false) - ) | ( - Some("stale-generation"), - Some("generation-changed"), - Some(false), - Some(false) - ) | ( - Some("already-in-progress"), - Some("operation-in-progress"), - Some(true), - Some(false) - ) | ( - Some("stop-failed"), - Some("termination-failed"), - Some(true), - Some(false) - ) | ( - Some("stop-failed"), - Some("unsafe-descendants"), - Some(true), - Some(false) - ) | ( - Some("stopped-not-restarted"), - Some("pane-did-not-stop"), - Some(true), - Some(false) - ) | ( - Some("stopped-not-restarted"), - Some("tmux-target-unavailable"), - Some(true), - Some(false) - ) | ( - Some("stopped-not-restarted"), - Some("respawn-failed"), - Some(true), - Some(false) - ) | ( - Some("outcome-unknown"), - Some("replacement-unobserved"), - Some(true), - Some(false) - ) | ( - Some("outcome-unknown"), - Some("operation-state-unknown"), - Some(true), - Some(false) - ) - ) -} - -fn invalid_restart_result() -> CommandError { - CommandError::with_detail( - "Restart-session action returned an invalid response.", - "The helper response did not match the restart-session domain contract.", - ) -} - -pub fn kill_session(request: KillSessionRequest) -> Result { - request.validate()?; - let runtime_config = load_config_for_command()?; - run_action_helper( - &runtime_config, - &runtime_config.kill_action_helper_path, - serde_json::to_value(&request).map_err(|error| { - CommandError::with_detail( - "End-session request is invalid.", - format!("Could not serialize the end-session request: {error}"), - ) - })?, - "End-session action is unavailable. Open desktop diagnostics for details.", - true, - ) -} - -fn load_config_for_command() -> Result { - load_runtime_config().map_err(|detail| { - CommandError::with_detail( - "Session Deck desktop runtime is unavailable. Open desktop diagnostics for details.", - detail, - ) - }) -} - -fn run_action_helper( - runtime_config: &RuntimeConfig, - script_path: &std::path::Path, - payload: Value, - public_error_message: &str, - outcome_unknown_on_timeout: bool, -) -> Result { - let output = run_helper( - runtime_config, - HelperSpec { - script_path, - stdin_payload: Some(serde_json::to_vec(&payload).map_err(|error| { - CommandError::with_detail( - public_error_message, - format!("Could not encode JSON payload: {error}"), - ) - })?), - timeout: ACTION_TIMEOUT, - bridge_socket_path: if script_path == runtime_config.open_action_helper_path.as_path() { - Some(runtime_config.bridge_socket_path.as_path()) - } else { - None - }, - public_error_message, - outcome_unknown_on_timeout, - }, - )?; - - let parsed = parse_json_object( - &output.stdout, - public_error_message, - "action-helper-invalid-json", - )?; - - if output.success { - return Ok(parsed); - } - - Ok(parsed) -} - -fn parse_json_object( - stdout: &str, - public_error_message: &str, - detail_code: &str, -) -> Result { - let parsed: Value = serde_json::from_str(stdout).map_err(|error| { - CommandError::with_detail( - public_error_message, - format!("{detail_code}: could not parse helper stdout as JSON: {error}"), - ) - })?; - - if !parsed.is_object() { - return Err(CommandError::with_detail( - public_error_message, - format!("{detail_code}: helper stdout was not a JSON object."), - )); - } - - Ok(parsed) -} - -struct HelperSpec<'a> { - script_path: &'a std::path::Path, - stdin_payload: Option>, - timeout: Duration, - bridge_socket_path: Option<&'a std::path::Path>, - public_error_message: &'a str, - outcome_unknown_on_timeout: bool, -} - -#[derive(Debug)] -struct HelperOutput { - success: bool, - stdout: String, - stderr: String, -} - -type PipeReader = JoinHandle>>; - -struct HelperOutputReaders { - stdout: PipeReader, - stderr: PipeReader, -} - -fn run_helper( - runtime_config: &RuntimeConfig, - helper_spec: HelperSpec<'_>, -) -> Result { - let mut command = Command::new(&runtime_config.node_executable_path); - command - .arg(helper_spec.script_path) - .stdin(if helper_spec.stdin_payload.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("PATH", &runtime_config.effective_command_path.value); - - // SAFETY: setpgid only changes the child process before exec and does not access parent memory. - unsafe { - command.pre_exec(|| { - if libc::setpgid(0, 0) == -1 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - - if let Some(bridge_socket_path) = helper_spec.bridge_socket_path { - command.env( - OPEN_TERMINAL_ACTION_BRIDGE_SOCKET_ENV, - bridge_socket_path.as_os_str(), - ); - } - - let mut child = command.spawn().map_err(|error| { - CommandError::with_detail( - helper_spec.public_error_message, - format!( - "Could not spawn helper {} with node {}: {error}", - helper_spec.script_path.display(), - runtime_config.node_executable_path.display() - ), - ) - })?; - - let output_readers = match spawn_output_readers(&mut child, helper_spec.public_error_message) { - Ok(readers) => readers, - Err(error) => { - terminate_child(&mut child); - return Err(error); - } - }; - - if let Some(stdin_payload) = helper_spec.stdin_payload { - if let Err(error) = - write_helper_stdin(&mut child, &stdin_payload, helper_spec.public_error_message) - { - terminate_child(&mut child); - return Err(error); - } - } - - match child.wait_timeout(helper_spec.timeout) { - Ok(Some(status)) => { - let (stdout, stderr) = - match collect_helper_output(output_readers, helper_spec.public_error_message) { - Ok(output) => output, - Err(error) => { - terminate_child(&mut child); - return Err(error); - } - }; - - Ok(HelperOutput { - success: status.success(), - stdout, - stderr, - }) - } - Ok(None) => { - terminate_child(&mut child); - Err(CommandError::timeout( - helper_spec.timeout, - helper_spec.outcome_unknown_on_timeout, - helper_spec.public_error_message, - )) - } - Err(error) => { - terminate_child(&mut child); - Err(CommandError::with_detail( - helper_spec.public_error_message, - format!("Could not wait for helper completion: {error}"), - )) - } - } -} - -fn spawn_output_readers( - child: &mut Child, - public_error_message: &str, -) -> Result { - let stdout = child.stdout.take().ok_or_else(|| { - CommandError::with_detail( - public_error_message, - "Helper stdout was not available after spawning the process.", - ) - })?; - let stderr = child.stderr.take().ok_or_else(|| { - CommandError::with_detail( - public_error_message, - "Helper stderr was not available after spawning the process.", - ) - })?; - - Ok(HelperOutputReaders { - stdout: read_pipe_in_thread(stdout), - stderr: read_pipe_in_thread(stderr), - }) -} - -fn read_pipe_in_thread(mut pipe: R) -> PipeReader -where - R: Read + Send + 'static, -{ - thread::spawn(move || { - let mut output = Vec::new(); - pipe.read_to_end(&mut output)?; - Ok(output) - }) -} - -fn write_helper_stdin( - child: &mut Child, - stdin_payload: &[u8], - public_error_message: &str, -) -> Result<(), CommandError> { - let mut stdin = child.stdin.take().ok_or_else(|| { - CommandError::with_detail( - public_error_message, - "Helper stdin was not available after spawning the process.", - ) - })?; - - stdin.write_all(stdin_payload).map_err(|error| { - CommandError::with_detail( - public_error_message, - format!("Could not write helper stdin payload: {error}"), - ) - }) -} - -fn collect_helper_output( - output_readers: HelperOutputReaders, - public_error_message: &str, -) -> Result<(String, String), CommandError> { - Ok(( - collect_pipe_output(output_readers.stdout, "stdout", public_error_message)?, - collect_pipe_output(output_readers.stderr, "stderr", public_error_message)?, - )) -} - -fn collect_pipe_output( - reader: PipeReader, - stream_name: &str, - public_error_message: &str, -) -> Result { - let bytes = reader - .join() - .map_err(|_| { - CommandError::with_detail( - public_error_message, - format!("Helper {stream_name} reader panicked."), - ) - })? - .map_err(|error| { - CommandError::with_detail( - public_error_message, - format!("Could not read helper {stream_name}: {error}"), - ) - })?; - - Ok(String::from_utf8_lossy(&bytes).into_owned()) -} - -fn terminate_child(child: &mut Child) { - let process_group = child.id() as libc::pid_t; - // The direct child created this process group in pre_exec. Killing the group limits ordinary - // descendants; detached processes and external effects such as tmux are outside this boundary. - unsafe { - let _ = libc::kill(-process_group, libc::SIGKILL); - } - let _ = child.kill(); - let _ = child.wait(); -} - -fn format_process_detail(stdout: &str, stderr: &str) -> String { - let stdout = stdout.trim(); - let stderr = stderr.trim(); - - if !stderr.is_empty() { - return format!("stderr: {stderr}"); - } - - if !stdout.is_empty() { - return format!("stdout: {stdout}"); - } - - String::from("no stdout or stderr was captured") -} - -#[cfg(test)] -mod tests { - use super::{ - is_coherent_restart_result, map_restart_helper_error, run_helper, - validate_restart_session_result, CommandError, CommandErrorPayload, HelperSpec, - }; - use crate::runtime::{EffectiveCommandPath, RuntimeConfig, RuntimeMetadataSource}; - use std::fs; - use std::os::unix::fs::PermissionsExt; - use std::path::{Path, PathBuf}; - use std::time::Duration; - use tempfile::tempdir; - - #[test] - fn run_helper_drains_large_stdout_before_waiting_for_exit() { - let temp_dir = tempdir().unwrap(); - let node_shim_path = temp_dir.path().join("node-shim"); - fs::write( - &node_shim_path, - "#!/bin/sh\ndd if=/dev/zero bs=1024 count=256 2>/dev/null\n", - ) - .unwrap(); - let mut permissions = fs::metadata(&node_shim_path).unwrap().permissions(); - permissions.set_mode(0o700); - fs::set_permissions(&node_shim_path, permissions).unwrap(); - - let script_path = temp_dir.path().join("ignored-helper.js"); - fs::write(&script_path, "").unwrap(); - let runtime_config = runtime_config_for_test(temp_dir.path(), node_shim_path); - - let output = run_helper( - &runtime_config, - HelperSpec { - script_path: &script_path, - stdin_payload: None, - timeout: Duration::from_secs(2), - bridge_socket_path: None, - public_error_message: "helper failed", - outcome_unknown_on_timeout: false, - }, - ) - .unwrap(); - - assert!(output.success); - assert_eq!(output.stdout.as_bytes().len(), 256 * 1024); - assert_eq!(output.stderr, ""); - } - - #[test] - fn mutating_timeout_is_structured_and_terminates_same_group_descendants() { - let temp_dir = tempdir().unwrap(); - let marker_path = temp_dir.path().join("delayed-marker"); - let node_shim_path = temp_dir.path().join("node-shim"); - fs::write( - &node_shim_path, - format!( - "#!/bin/sh\n(sleep 0.2; printf late > '{}') &\nsleep 5\n", - marker_path.display() - ), - ) - .unwrap(); - let mut permissions = fs::metadata(&node_shim_path).unwrap().permissions(); - permissions.set_mode(0o700); - fs::set_permissions(&node_shim_path, permissions).unwrap(); - - let script_path = temp_dir.path().join("ignored-helper.js"); - fs::write(&script_path, "").unwrap(); - let runtime_config = runtime_config_for_test(temp_dir.path(), node_shim_path); - - let error = run_helper( - &runtime_config, - HelperSpec { - script_path: &script_path, - stdin_payload: None, - timeout: Duration::from_millis(50), - bridge_socket_path: None, - public_error_message: "helper failed", - outcome_unknown_on_timeout: true, - }, - ) - .unwrap_err(); - - assert_eq!( - serde_json::to_value(error.into_tauri_error()).unwrap(), - serde_json::json!({ - "code": "mutating-helper-timeout", - "message": "The desktop helper timed out before Session Deck could confirm whether the action completed.", - "outcomeUnknown": true - }) - ); - std::thread::sleep(Duration::from_millis(350)); - assert!(!marker_path.exists()); - } - - #[test] - fn restart_result_requires_the_exact_operation_and_domain_shape() { - let valid = serde_json::json!({ - "ok": true, - "status": "restarted", - "operationId": "operation-1", - "reason": "replacement-observed", - "retryable": false, - "message": "Session restarted." - }); - assert!(validate_restart_session_result(valid.clone(), "operation-1").is_ok()); - assert!(validate_restart_session_result(valid.clone(), "operation-2").is_err()); - let mut private = valid; - private["sessionFile"] = serde_json::json!("/private/session.jsonl"); - assert!(validate_restart_session_result(private, "operation-1").is_err()); - } - - #[test] - fn restart_result_accepts_current_domain_tuples() { - for (status, reason, retryable, ok) in [ - ("restarted", "replacement-observed", false, true), - ("not-eligible", "managed-recipe-unavailable", false, false), - ("not-eligible", "recipe-not-bound", false, false), - ("not-eligible", "recipe-invalid", false, false), - ("not-eligible", "runtime-unavailable", true, false), - ("not-eligible", "identity-mismatch", false, false), - ("not-eligible", "session-file-unavailable", false, false), - ("not-eligible", "cwd-unavailable", false, false), - ("not-eligible", "pi-executable-unavailable", false, false), - ("not-eligible", "tmux-target-unavailable", false, false), - ("not-eligible", "tmux-pane-mismatch", false, false), - ("not-eligible", "unsafe-descendants", true, false), - ("not-eligible", "hosting-runtime", false, false), - ("not-eligible", "coordinator-runtime", false, false), - ("stale-generation", "generation-changed", false, false), - ("already-in-progress", "operation-in-progress", true, false), - ("stop-failed", "termination-failed", true, false), - ("stop-failed", "unsafe-descendants", true, false), - ("stopped-not-restarted", "pane-did-not-stop", true, false), - ( - "stopped-not-restarted", - "tmux-target-unavailable", - true, - false, - ), - ("stopped-not-restarted", "respawn-failed", true, false), - ("outcome-unknown", "replacement-unobserved", true, false), - ("outcome-unknown", "operation-state-unknown", true, false), - ] { - assert!(is_coherent_restart_result( - Some(status), - Some(reason), - Some(retryable), - Some(ok) - )); - } - } - - #[test] - fn restart_result_rejects_incoherent_domain_tuples() { - let valid = serde_json::json!({ - "ok": true, - "status": "restarted", - "operationId": "operation-1", - "reason": "replacement-observed", - "retryable": false, - "message": "Session restarted." - }); - - for (field, value) in [ - ("status", serde_json::json!("outcome-unknown")), - ("reason", serde_json::json!("termination-failed")), - ("retryable", serde_json::json!(true)), - ("ok", serde_json::json!(false)), - ] { - let mut candidate = valid.clone(); - candidate[field] = value; - assert!( - validate_restart_session_result(candidate, "operation-1").is_err(), - "{field} should not change independently" - ); - } - } - - #[test] - fn restart_timeout_result_echoes_only_the_existing_operation_id() { - let result = map_restart_helper_error( - CommandError::timeout(Duration::from_secs(60), true, "restart unavailable"), - "operation-timeout", - ) - .unwrap(); - assert!(validate_restart_session_result(result.clone(), "operation-timeout").is_ok()); - assert_eq!( - result, - serde_json::json!({ - "ok": false, - "status": "outcome-unknown", - "operationId": "operation-timeout", - "reason": "operation-state-unknown", - "retryable": true, - "message": "Session Deck could not confirm the restart outcome. Reconcile before retrying." - }) - ); - } - - #[test] - fn read_only_timeout_remains_an_ordinary_error() { - assert_eq!( - CommandError::timeout(Duration::from_secs(10), false, "snapshot unavailable") - .into_tauri_error(), - CommandErrorPayload::Message(String::from("snapshot unavailable")) - ); - } - - fn runtime_config_for_test(root: &Path, node_executable_path: PathBuf) -> RuntimeConfig { - RuntimeConfig { - metadata_source: RuntimeMetadataSource::Desktop, - state_path: root.join("install.json"), - package_root: root.to_path_buf(), - package_version: String::from("0.0.0-test"), - helper_package_version: Some(String::from("0.0.0-test")), - node_executable_path, - snapshot_helper_path: root.join("snapshot-helper.js"), - open_action_helper_path: root.join("open-action-helper.js"), - kill_action_helper_path: root.join("kill-action-helper.js"), - worktree_action_helper_path: root.join("worktree-action-helper.js"), - web_root_path: root.join("web"), - bridge_socket_path: root.join("bridge.sock"), - effective_command_path: EffectiveCommandPath { - value: std::env::var("PATH").unwrap_or_else(|_| String::from("/usr/bin:/bin")), - provenance: String::from("test"), - }, - } - } -} diff --git a/apps/session-deck-desktop/src-tauri/src/lib.rs b/apps/session-deck-desktop/src-tauri/src/lib.rs deleted file mode 100644 index c2c8e22b..00000000 --- a/apps/session-deck-desktop/src-tauri/src/lib.rs +++ /dev/null @@ -1,69 +0,0 @@ -pub mod commands; -pub mod doctor; -pub mod helper_runner; -pub mod runtime; - -use tauri::{ - tray::{MouseButton, MouseButtonState, TrayIconEvent}, - Manager, -}; - -fn should_restore_main_window(button: MouseButton, state: MouseButtonState) -> bool { - button == MouseButton::Left && state == MouseButtonState::Up -} - -pub fn run() { - tauri::Builder::default() - .on_tray_icon_event(|app, event| { - if let TrayIconEvent::Click { - button, - button_state, - .. - } = event - { - if should_restore_main_window(button, button_state) { - if let Some(window) = app.get_webview_window("main") { - let _ = window.unminimize(); - let _ = window.show(); - let _ = window.set_focus(); - } - } - } - }) - .invoke_handler(tauri::generate_handler![ - commands::load_snapshot, - commands::preview_worktree_base_ref, - commands::preview_worktree_launch_context, - commands::create_worktree, - commands::create_session, - commands::open_terminal, - commands::kill_session, - commands::restart_session, - commands::open_external, - commands::copy_text, - commands::doctor_status, - ]) - .run(tauri::generate_context!()) - .expect("failed to run Session Deck desktop application"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn restores_only_for_left_button_release() { - assert!(should_restore_main_window( - MouseButton::Left, - MouseButtonState::Up, - )); - - for (button, state) in [ - (MouseButton::Left, MouseButtonState::Down), - (MouseButton::Right, MouseButtonState::Up), - (MouseButton::Middle, MouseButtonState::Up), - ] { - assert!(!should_restore_main_window(button, state)); - } - } -} diff --git a/apps/session-deck-desktop/src-tauri/src/main.rs b/apps/session-deck-desktop/src-tauri/src/main.rs deleted file mode 100644 index 07c88aea..00000000 --- a/apps/session-deck-desktop/src-tauri/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - pi_session_deck_desktop::run(); -} diff --git a/apps/session-deck-desktop/src-tauri/src/runtime.rs b/apps/session-deck-desktop/src-tauri/src/runtime.rs deleted file mode 100644 index 17fa6141..00000000 --- a/apps/session-deck-desktop/src-tauri/src/runtime.rs +++ /dev/null @@ -1,1850 +0,0 @@ -use dirs::home_dir; -use libc::{geteuid, getpwuid}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::ffi::{CStr, OsStr}; -use std::fs::{self, File}; -use std::os::unix::fs::{FileTypeExt, PermissionsExt}; -use std::os::unix::process::CommandExt; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::time::Duration; -use wait_timeout::ChildExt; - -pub const DOCTOR_COMMAND: &str = "/session-deck desktop doctor"; -pub const OPEN_TERMINAL_ACTION_BRIDGE_SOCKET_ENV: &str = - "PI_SESSION_DECK_ITERM2_BRIDGE_SOCKET_PATH"; -const DESKTOP_STATE_PATH_ENV: &str = "PI_SESSION_DECK_DESKTOP_STATE_PATH"; -const DESKTOP_INSTALL_COMMAND: &str = "/session-deck desktop install"; -const DESKTOP_STATE_SCHEMA_VERSION: u64 = 1; -const SESSION_DECK_PACKAGE_NAME: &str = "@robhowley/pi-session-deck"; -const SESSION_DECK_BUNDLE_IDENTIFIER: &str = "dev.pi-userland.session-deck.desktop"; -const ITERM2_STATE_SCHEMA_VERSION: u64 = 1; -const ITERM2_STATE_PRODUCT: &str = "pi-session-deck-iterm2"; -const SNAPSHOT_HELPER_RELATIVE_PATH: &str = "dist/extensions/session-deck/iterm2/snapshot-cli.js"; -const OPEN_HELPER_RELATIVE_PATH: &str = "dist/extensions/session-deck/iterm2/open-action-cli.js"; -const KILL_HELPER_RELATIVE_PATH: &str = "dist/extensions/session-deck/iterm2/kill-action-cli.js"; -const WORKTREE_HELPER_RELATIVE_PATH: &str = "dist/extensions/session-deck/worktree/action-cli.js"; -const WEB_ROOT_RELATIVE_PATH: &str = "extensions/session-deck/iterm2/web"; -const FALLBACK_COMMAND_PATH: &str = "/usr/bin:/bin:/usr/sbin:/sbin"; -const EFFECTIVE_COMMAND_PATH_TIMEOUT: Duration = Duration::from_secs(3); -const EFFECTIVE_COMMAND_PATH_START: &str = "__SESSION_DECK_EFFECTIVE_PATH__START"; -const EFFECTIVE_COMMAND_PATH_END: &str = "__SESSION_DECK_EFFECTIVE_PATH__END"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RuntimeMetadataSource { - Desktop, - Iterm2Fallback, -} - -impl RuntimeMetadataSource { - fn as_str(self) -> &'static str { - match self { - RuntimeMetadataSource::Desktop => "desktop", - RuntimeMetadataSource::Iterm2Fallback => "iterm2-fallback", - } - } -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DoctorIssue { - pub code: String, - pub message: String, - pub repair: String, - pub blocking: bool, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EffectiveCommandPath { - pub value: String, - pub provenance: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ExecutableStatus { - pub status: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LaunchPrereqReport { - pub path_provenance: String, - pub tmux: ExecutableStatus, - pub pi: ExecutableStatus, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RuntimeSummary { - pub metadata_source: String, - pub package_root: String, - pub package_version: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub declared_helper_package_version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub helper_package_version: Option, - pub node_executable_path: String, - pub snapshot_helper_path: String, - pub open_action_helper_path: String, - pub kill_action_helper_path: String, - pub worktree_action_helper_path: String, - pub web_root_path: String, - pub bridge_socket_path: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DoctorStatus { - pub ok: bool, - pub doctor_command: String, - pub metadata_source: String, - pub state_path: String, - pub desktop_state_path: String, - pub iterm2_fallback_state_path: String, - pub effective_command_path: EffectiveCommandPath, - pub launch_prereqs: LaunchPrereqReport, - pub issues: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub runtime: Option, -} - -#[derive(Debug, Clone)] -pub struct ResolvedInstallState { - pub metadata_source: RuntimeMetadataSource, - pub state_path: PathBuf, - pub package_root: PathBuf, - pub package_version: String, - pub declared_helper_package_version: Option, - pub node_executable_path: PathBuf, - pub snapshot_helper_path: PathBuf, - pub open_action_helper_path: PathBuf, - pub kill_action_helper_path: PathBuf, - pub worktree_action_helper_path: PathBuf, - pub web_root_path: PathBuf, - pub bridge_socket_path: PathBuf, -} - -#[derive(Debug, Clone)] -pub struct RuntimeConfig { - pub metadata_source: RuntimeMetadataSource, - pub state_path: PathBuf, - pub package_root: PathBuf, - pub package_version: String, - pub helper_package_version: Option, - pub node_executable_path: PathBuf, - pub snapshot_helper_path: PathBuf, - pub open_action_helper_path: PathBuf, - pub kill_action_helper_path: PathBuf, - pub worktree_action_helper_path: PathBuf, - pub web_root_path: PathBuf, - pub bridge_socket_path: PathBuf, - pub effective_command_path: EffectiveCommandPath, -} - -pub struct RuntimeDiscovery { - pub status: DoctorStatus, - pub config: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "product", deny_unknown_fields)] -enum RawDesktopInstallState { - #[serde(rename = "session-deck-desktop", rename_all = "camelCase")] - Production { - schema_version: u64, - package_name: String, - package_version: String, - installed_at: String, - app: RawDesktopAppState, - source: RawDesktopSourceState, - runtime: RawDesktopRuntimeState, - owned_paths: Vec, - }, - #[serde(rename = "session-deck-desktop-development", rename_all = "camelCase")] - Development { - schema_version: u64, - package_name: String, - package_version: String, - installed_at: String, - runtime: RawDesktopRuntimeState, - }, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawDesktopAppState { - path: String, - bundle_identifier: String, - name: String, - version: String, - sha256: String, -} - -#[derive(Debug, Deserialize)] -#[serde( - tag = "kind", - rename_all = "kebab-case", - rename_all_fields = "camelCase", - deny_unknown_fields -)] -enum RawDesktopSourceState { - LocalPath { - path: String, - sha256: String, - }, - GithubRelease { - release_tag: String, - asset_name: String, - url: String, - sha256: String, - }, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawDesktopRuntimeState { - node_executable_path: String, - package_root: String, - helper_package_version: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawIterm2InstallState { - schema_version: u64, - product: String, - package_version: String, - script: Value, - scripts_dir: String, - installed_at: String, - runtime: RawIterm2RuntimeState, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawIterm2RuntimeState { - node_executable_path: String, - snapshot_helper_path: String, - web_root_path: String, - bridge_socket_path: String, -} - -pub fn default_desktop_state_path() -> Result { - if let Some(raw_state_path) = std::env::var_os(DESKTOP_STATE_PATH_ENV) { - let state_path = PathBuf::from(raw_state_path); - if state_path.as_os_str().is_empty() { - return Err(format!("{DESKTOP_STATE_PATH_ENV} must not be empty.")); - } - if !state_path.is_absolute() { - return Err(format!( - "{DESKTOP_STATE_PATH_ENV} must be an absolute path." - )); - } - return Ok(state_path); - } - - let home = home_dir().ok_or_else(|| String::from("Could not determine the home directory."))?; - Ok(home.join(".pi/session-deck/desktop/install.json")) -} - -pub fn default_iterm2_fallback_state_path() -> Result { - let home = home_dir().ok_or_else(|| String::from("Could not determine the home directory."))?; - Ok(home.join(".pi/session-deck/iterm2/install.json")) -} - -pub fn default_state_path() -> Result { - default_desktop_state_path() -} - -pub fn derive_open_action_helper_path(snapshot_helper_path: &Path) -> PathBuf { - snapshot_helper_path - .parent() - .unwrap_or(snapshot_helper_path) - .join("open-action-cli.js") -} - -pub fn derive_kill_action_helper_path(snapshot_helper_path: &Path) -> PathBuf { - snapshot_helper_path - .parent() - .unwrap_or(snapshot_helper_path) - .join("kill-action-cli.js") -} - -pub fn derive_worktree_action_helper_path(snapshot_helper_path: &Path) -> PathBuf { - snapshot_helper_path - .parent() - .and_then(Path::parent) - .unwrap_or(snapshot_helper_path) - .join("worktree/action-cli.js") -} - -pub fn parse_desktop_install_state( - contents: &str, - state_path: &Path, -) -> Result { - let raw: RawDesktopInstallState = serde_json::from_str(contents) - .map_err(|error| format!("Invalid desktop install state JSON: {error}"))?; - - let (schema_version, package_name, package_version, installed_at, runtime) = match raw { - RawDesktopInstallState::Production { - schema_version, - package_name, - package_version, - installed_at, - app, - source, - runtime, - owned_paths, - } => { - validate_production_desktop_shape(&app, &source, &owned_paths)?; - ( - schema_version, - package_name, - package_version, - installed_at, - runtime, - ) - } - RawDesktopInstallState::Development { - schema_version, - package_name, - package_version, - installed_at, - runtime, - } => ( - schema_version, - package_name, - package_version, - installed_at, - runtime, - ), - }; - - if schema_version != DESKTOP_STATE_SCHEMA_VERSION { - return Err(format!( - "Expected schemaVersion {DESKTOP_STATE_SCHEMA_VERSION}, got {schema_version}." - )); - } - - if package_name != SESSION_DECK_PACKAGE_NAME { - return Err(format!( - "Expected packageName {SESSION_DECK_PACKAGE_NAME}, got {package_name}." - )); - } - - if package_version.trim().is_empty() { - return Err(String::from("packageVersion must be a non-empty string.")); - } - - if installed_at.trim().is_empty() { - return Err(String::from("installedAt must be a non-empty string.")); - } - - if runtime.helper_package_version.trim().is_empty() { - return Err(String::from( - "runtime.helperPackageVersion must be a non-empty string.", - )); - } - - let node_executable_path = - parse_absolute_path(&runtime.node_executable_path, "runtime.nodeExecutablePath")?; - let package_root = parse_absolute_path(&runtime.package_root, "runtime.packageRoot")?; - - Ok(ResolvedInstallState { - metadata_source: RuntimeMetadataSource::Desktop, - state_path: state_path.to_path_buf(), - package_root: package_root.clone(), - package_version, - declared_helper_package_version: Some(runtime.helper_package_version), - node_executable_path, - snapshot_helper_path: package_root.join(SNAPSHOT_HELPER_RELATIVE_PATH), - open_action_helper_path: package_root.join(OPEN_HELPER_RELATIVE_PATH), - kill_action_helper_path: package_root.join(KILL_HELPER_RELATIVE_PATH), - worktree_action_helper_path: package_root.join(WORKTREE_HELPER_RELATIVE_PATH), - web_root_path: package_root.join(WEB_ROOT_RELATIVE_PATH), - bridge_socket_path: default_bridge_socket_path(), - }) -} - -fn validate_production_desktop_shape( - app: &RawDesktopAppState, - source: &RawDesktopSourceState, - owned_paths: &[String], -) -> Result<(), String> { - let app_path = parse_absolute_path(&app.path, "app.path")?; - if app.bundle_identifier != SESSION_DECK_BUNDLE_IDENTIFIER { - return Err(format!( - "Expected app.bundleIdentifier {SESSION_DECK_BUNDLE_IDENTIFIER}, got {}.", - app.bundle_identifier - )); - } - validate_non_empty(&app.name, "app.name")?; - validate_non_empty(&app.version, "app.version")?; - validate_sha256(&app.sha256, "app.sha256")?; - - match source { - RawDesktopSourceState::LocalPath { path, sha256 } => { - parse_absolute_path(path, "source.path")?; - validate_sha256(sha256, "source.sha256")?; - } - RawDesktopSourceState::GithubRelease { - release_tag, - asset_name, - url, - sha256, - } => { - validate_non_empty(release_tag, "source.releaseTag")?; - validate_non_empty(asset_name, "source.assetName")?; - validate_non_empty(url, "source.url")?; - validate_sha256(sha256, "source.sha256")?; - } - } - - if owned_paths.is_empty() { - return Err(String::from("ownedPaths must contain at least one path.")); - } - let parsed_owned_paths = owned_paths - .iter() - .map(|path| parse_absolute_path(path, "ownedPaths entry")) - .collect::, _>>()?; - if !parsed_owned_paths.contains(&app_path) { - return Err(String::from("ownedPaths must include app.path.")); - } - - Ok(()) -} - -fn validate_non_empty(value: &str, field_name: &str) -> Result<(), String> { - if value.trim().is_empty() { - return Err(format!("{field_name} must be a non-empty string.")); - } - Ok(()) -} - -fn validate_sha256(value: &str, field_name: &str) -> Result<(), String> { - if value.len() != 64 - || !value - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) - { - return Err(format!("{field_name} must be a lowercase SHA-256 digest.")); - } - Ok(()) -} - -pub fn parse_iterm2_install_state( - contents: &str, - state_path: &Path, -) -> Result { - let raw: RawIterm2InstallState = serde_json::from_str(contents) - .map_err(|error| format!("Invalid iTerm2 install state JSON: {error}"))?; - - if raw.schema_version != ITERM2_STATE_SCHEMA_VERSION { - return Err(format!( - "Expected schemaVersion {ITERM2_STATE_SCHEMA_VERSION}, got {}.", - raw.schema_version - )); - } - - if raw.product != ITERM2_STATE_PRODUCT { - return Err(format!( - "Expected product {ITERM2_STATE_PRODUCT}, got {}.", - raw.product - )); - } - - if raw.package_version.trim().is_empty() { - return Err(String::from("packageVersion must be a non-empty string.")); - } - - if raw.scripts_dir.trim().is_empty() { - return Err(String::from("scriptsDir must be a non-empty string.")); - } - - if raw.installed_at.trim().is_empty() { - return Err(String::from("installedAt must be a non-empty string.")); - } - - let _ = raw.script; - - let node_executable_path = parse_absolute_path( - &raw.runtime.node_executable_path, - "runtime.nodeExecutablePath", - )?; - let snapshot_helper_path = parse_absolute_path( - &raw.runtime.snapshot_helper_path, - "runtime.snapshotHelperPath", - )?; - let web_root_path = parse_absolute_path(&raw.runtime.web_root_path, "runtime.webRootPath")?; - let bridge_socket_path = - parse_absolute_path(&raw.runtime.bridge_socket_path, "runtime.bridgeSocketPath")?; - let package_root = derive_package_root_from_snapshot_helper_path(&snapshot_helper_path) - .unwrap_or_else(|| { - snapshot_helper_path - .parent() - .unwrap_or(&snapshot_helper_path) - .to_path_buf() - }); - - Ok(ResolvedInstallState { - metadata_source: RuntimeMetadataSource::Iterm2Fallback, - state_path: state_path.to_path_buf(), - package_root, - package_version: raw.package_version, - declared_helper_package_version: None, - node_executable_path, - open_action_helper_path: derive_open_action_helper_path(&snapshot_helper_path), - kill_action_helper_path: derive_kill_action_helper_path(&snapshot_helper_path), - worktree_action_helper_path: derive_worktree_action_helper_path(&snapshot_helper_path), - snapshot_helper_path, - web_root_path, - bridge_socket_path, - }) -} - -pub fn parse_install_state( - contents: &str, - state_path: &Path, -) -> Result { - parse_iterm2_install_state(contents, state_path) -} - -pub fn discover_runtime() -> RuntimeDiscovery { - let desktop_state_path = default_desktop_state_path() - .unwrap_or_else(|_| PathBuf::from("~/.pi/session-deck/desktop/install.json")); - let iterm2_fallback_state_path = default_iterm2_fallback_state_path() - .unwrap_or_else(|_| PathBuf::from("~/.pi/session-deck/iterm2/install.json")); - - discover_runtime_from_state_paths( - &desktop_state_path, - &iterm2_fallback_state_path, - resolve_effective_command_path(), - ) -} - -fn discover_runtime_from_state_paths( - desktop_state_path: &Path, - iterm2_fallback_state_path: &Path, - effective_command_path: EffectiveCommandPath, -) -> RuntimeDiscovery { - let launch_prereqs = collect_launch_prereqs(&effective_command_path); - let mut issues = Vec::new(); - let mut runtime_summary = None; - let mut runtime_config = None; - - let resolved_state = - select_install_state(desktop_state_path, iterm2_fallback_state_path, &mut issues); - let selected_metadata_source = resolved_state - .as_ref() - .map(|state| state.metadata_source.as_str()) - .unwrap_or("unavailable") - .to_string(); - let selected_state_path = resolved_state - .as_ref() - .map(|state| state.state_path.clone()) - .unwrap_or_else(|| desktop_state_path.to_path_buf()); - - if let Some(resolved_state) = resolved_state { - let helper_package_version_result = - read_helper_package_version(&resolved_state.package_root); - let helper_package_version = helper_package_version_result.as_ref().ok().cloned(); - - validate_resolved_install_state( - &resolved_state, - helper_package_version_result - .as_deref() - .map_err(String::as_str), - &launch_prereqs, - &mut issues, - ); - - runtime_summary = Some(RuntimeSummary { - metadata_source: resolved_state.metadata_source.as_str().to_string(), - package_root: resolved_state.package_root.display().to_string(), - package_version: resolved_state.package_version.clone(), - declared_helper_package_version: resolved_state.declared_helper_package_version.clone(), - helper_package_version: helper_package_version.clone(), - node_executable_path: resolved_state.node_executable_path.display().to_string(), - snapshot_helper_path: resolved_state.snapshot_helper_path.display().to_string(), - open_action_helper_path: resolved_state.open_action_helper_path.display().to_string(), - kill_action_helper_path: resolved_state.kill_action_helper_path.display().to_string(), - worktree_action_helper_path: resolved_state - .worktree_action_helper_path - .display() - .to_string(), - web_root_path: resolved_state.web_root_path.display().to_string(), - bridge_socket_path: resolved_state.bridge_socket_path.display().to_string(), - }); - - if !issues.iter().any(|issue| issue.blocking) { - runtime_config = Some(RuntimeConfig { - metadata_source: resolved_state.metadata_source, - state_path: resolved_state.state_path, - package_root: resolved_state.package_root, - package_version: resolved_state.package_version, - helper_package_version, - node_executable_path: resolved_state.node_executable_path, - snapshot_helper_path: resolved_state.snapshot_helper_path, - open_action_helper_path: resolved_state.open_action_helper_path, - kill_action_helper_path: resolved_state.kill_action_helper_path, - worktree_action_helper_path: resolved_state.worktree_action_helper_path, - web_root_path: resolved_state.web_root_path, - bridge_socket_path: resolved_state.bridge_socket_path, - effective_command_path: effective_command_path.clone(), - }); - } - } else { - add_launch_prereq_issues(&launch_prereqs, &mut issues); - } - - let ok = !issues.iter().any(|issue| issue.blocking); - RuntimeDiscovery { - status: DoctorStatus { - ok, - doctor_command: String::from(DOCTOR_COMMAND), - metadata_source: selected_metadata_source, - state_path: selected_state_path.display().to_string(), - desktop_state_path: desktop_state_path.display().to_string(), - iterm2_fallback_state_path: iterm2_fallback_state_path.display().to_string(), - effective_command_path, - launch_prereqs, - issues, - runtime: runtime_summary, - }, - config: runtime_config, - } -} - -pub fn load_runtime_config() -> Result { - let discovery = discover_runtime(); - match discovery.config { - Some(config) => Ok(config), - None => { - let mut issues = discovery.status.issues.into_iter(); - let first_blocking = issues - .find(|issue| issue.blocking) - .map(|issue| format!("{} {}", issue.message, issue.repair)); - Err(first_blocking - .unwrap_or_else(|| String::from("Session Deck desktop runtime is unavailable."))) - } - } -} - -fn select_install_state( - desktop_state_path: &Path, - iterm2_fallback_state_path: &Path, - issues: &mut Vec, -) -> Option { - match fs::read_to_string(desktop_state_path) { - Ok(contents) => match parse_desktop_install_state(&contents, desktop_state_path) { - Ok(resolved_state) => Some(resolved_state), - Err(error) => { - issues.push(DoctorIssue { - code: String::from("desktop-install-state-invalid"), - message: format!( - "Desktop install state at {} is invalid: {error}.", - desktop_state_path.display() - ), - repair: format!( - "Run {DESKTOP_INSTALL_COMMAND} to repair the desktop runtime metadata, or {DOCTOR_COMMAND} for details." - ), - blocking: true, - }); - None - } - }, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - select_iterm2_fallback_state(desktop_state_path, iterm2_fallback_state_path, issues) - } - Err(error) => { - issues.push(DoctorIssue { - code: String::from("desktop-install-state-unreadable"), - message: format!( - "Could not read desktop install state at {}: {error}.", - desktop_state_path.display() - ), - repair: format!( - "Check filesystem permissions for the Session Deck desktop install state, then run {DESKTOP_INSTALL_COMMAND}." - ), - blocking: true, - }); - None - } - } -} - -fn select_iterm2_fallback_state( - desktop_state_path: &Path, - iterm2_fallback_state_path: &Path, - issues: &mut Vec, -) -> Option { - match fs::read_to_string(iterm2_fallback_state_path) { - Ok(contents) => match parse_iterm2_install_state(&contents, iterm2_fallback_state_path) { - Ok(resolved_state) => { - issues.push(DoctorIssue { - code: String::from("iterm2-install-state-fallback"), - message: format!( - "Desktop install state was not found at {}; using legacy iTerm2 install metadata at {} for development/back-compat.", - desktop_state_path.display(), - iterm2_fallback_state_path.display() - ), - repair: format!( - "Run {DESKTOP_INSTALL_COMMAND} to create first-class desktop runtime metadata." - ), - blocking: false, - }); - Some(resolved_state) - } - Err(error) => { - issues.push(DoctorIssue { - code: String::from("desktop-install-state-missing-iterm2-fallback-invalid"), - message: format!( - "Desktop install state was not found at {}; legacy iTerm2 fallback metadata at {} is invalid: {error}.", - desktop_state_path.display(), - iterm2_fallback_state_path.display() - ), - repair: format!( - "Run {DESKTOP_INSTALL_COMMAND} to create desktop runtime metadata, or {DOCTOR_COMMAND} for details." - ), - blocking: true, - }); - None - } - }, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - issues.push(DoctorIssue { - code: String::from("desktop-install-state-missing"), - message: format!( - "Desktop install state was not found at {}; no legacy iTerm2 fallback metadata was found at {}.", - desktop_state_path.display(), - iterm2_fallback_state_path.display() - ), - repair: format!( - "Run {DESKTOP_INSTALL_COMMAND} to create the runtime config used by the desktop companion." - ), - blocking: true, - }); - None - } - Err(error) => { - issues.push(DoctorIssue { - code: String::from("desktop-install-state-missing-iterm2-fallback-unreadable"), - message: format!( - "Desktop install state was not found at {}; legacy iTerm2 fallback metadata at {} could not be read: {error}.", - desktop_state_path.display(), - iterm2_fallback_state_path.display() - ), - repair: format!( - "Run {DESKTOP_INSTALL_COMMAND} to create first-class desktop runtime metadata." - ), - blocking: true, - }); - None - } - } -} - -fn validate_resolved_install_state( - resolved_state: &ResolvedInstallState, - helper_package_version: Result<&str, &str>, - launch_prereqs: &LaunchPrereqReport, - issues: &mut Vec, -) { - let repair = runtime_metadata_repair(resolved_state.metadata_source); - - validate_executable_path( - &resolved_state.node_executable_path, - issues, - "node-executable-missing", - "Node executable is missing or not executable.", - repair, - ); - validate_readable_file( - &resolved_state.snapshot_helper_path, - issues, - "snapshot-helper-missing", - "Snapshot helper is missing or unreadable.", - repair, - ); - validate_readable_file( - &resolved_state.open_action_helper_path, - issues, - "open-helper-missing", - "Open-terminal helper is missing or unreadable.", - repair, - ); - validate_readable_file( - &resolved_state.kill_action_helper_path, - issues, - "kill-helper-missing", - "End-session helper is missing or unreadable.", - repair, - ); - validate_readable_file( - &resolved_state.worktree_action_helper_path, - issues, - "worktree-helper-missing", - "Worktree helper is missing or unreadable.", - repair, - ); - validate_directory( - &resolved_state.web_root_path, - issues, - "web-root-missing", - "Installed Session Deck web root is missing.", - repair, - ); - validate_bridge_socket(&resolved_state.bridge_socket_path, issues); - add_launch_prereq_issues(launch_prereqs, issues); - - let expected_web_root = derive_expected_web_root(&resolved_state.package_root); - if expected_web_root != resolved_state.web_root_path { - issues.push(DoctorIssue { - code: String::from("web-root-mismatch"), - message: format!( - "Install metadata web root does not match the helper package layout: {}", - expected_web_root.display() - ), - repair: String::from(repair), - blocking: false, - }); - } - - if let Some(declared_helper_package_version) = - resolved_state.declared_helper_package_version.as_deref() - { - if declared_helper_package_version != resolved_state.package_version { - issues.push(DoctorIssue { - code: String::from("metadata-helper-package-version-mismatch"), - message: format!( - "Desktop install metadata packageVersion {} does not match runtime.helperPackageVersion {}.", - resolved_state.package_version, declared_helper_package_version - ), - repair: String::from(repair), - blocking: true, - }); - } - } - - match helper_package_version { - Ok(helper_package_version) => { - let expected_helper_package_version = resolved_state - .declared_helper_package_version - .as_deref() - .unwrap_or(&resolved_state.package_version); - if helper_package_version != expected_helper_package_version { - issues.push(DoctorIssue { - code: String::from("helper-package-version-mismatch"), - message: format!( - "Install metadata helper version {} does not match helper package version {}.", - expected_helper_package_version, helper_package_version - ), - repair: String::from(repair), - blocking: resolved_state.metadata_source == RuntimeMetadataSource::Desktop, - }); - } - } - Err(error) if resolved_state.metadata_source == RuntimeMetadataSource::Desktop => { - issues.push(DoctorIssue { - code: String::from("helper-package-version-invalid"), - message: format!("Could not verify the installed helper package version: {error}."), - repair: String::from(repair), - blocking: true, - }); - } - Err(_) => {} - } -} - -fn runtime_metadata_repair(metadata_source: RuntimeMetadataSource) -> &'static str { - match metadata_source { - RuntimeMetadataSource::Desktop => { - "Run /session-deck desktop install to refresh the desktop runtime metadata." - } - RuntimeMetadataSource::Iterm2Fallback => { - "Run /session-deck desktop install to replace the legacy fallback metadata with desktop runtime metadata." - } - } -} - -fn derive_expected_web_root(package_root: &Path) -> PathBuf { - package_root.join(WEB_ROOT_RELATIVE_PATH) -} - -fn read_helper_package_version(package_root: &Path) -> Result { - let package_json_path = package_root.join("package.json"); - let package_json = fs::read_to_string(&package_json_path) - .map_err(|error| format!("Could not read {}: {error}", package_json_path.display()))?; - let parsed: Value = serde_json::from_str(&package_json) - .map_err(|error| format!("Could not parse {}: {error}", package_json_path.display()))?; - let version = parsed - .get("version") - .and_then(Value::as_str) - .ok_or_else(|| { - format!( - "{} must contain a string version", - package_json_path.display() - ) - })?; - if version.trim().is_empty() { - return Err(format!( - "{} must contain a non-empty version", - package_json_path.display() - )); - } - Ok(String::from(version)) -} - -fn derive_package_root_from_snapshot_helper_path(snapshot_helper_path: &Path) -> Option { - snapshot_helper_path - .ancestors() - .nth(5) - .map(Path::to_path_buf) -} - -fn default_bridge_socket_path() -> PathBuf { - let uid = unsafe { geteuid() }; - std::env::temp_dir() - .join(format!("pi-session-deck-{uid}")) - .join("iterm2.sock") -} - -fn parse_absolute_path(raw_path: &str, field_name: &str) -> Result { - if raw_path.trim().is_empty() { - return Err(format!("{field_name} must be a non-empty string.")); - } - - let path = PathBuf::from(raw_path); - if !path.is_absolute() { - return Err(format!("{field_name} must be an absolute path.")); - } - - Ok(path) -} - -fn validate_executable_path( - path: &Path, - issues: &mut Vec, - code: &str, - message: &str, - repair: &str, -) { - if !path.is_file() || !is_executable(path) { - issues.push(DoctorIssue { - code: String::from(code), - message: format!("{message} Path: {}", path.display()), - repair: String::from(repair), - blocking: true, - }); - } -} - -fn validate_readable_file( - path: &Path, - issues: &mut Vec, - code: &str, - message: &str, - repair: &str, -) { - if !path.is_file() { - issues.push(DoctorIssue { - code: String::from(code), - message: format!("{message} Path: {}", path.display()), - repair: String::from(repair), - blocking: true, - }); - return; - } - - if File::open(path).is_err() { - issues.push(DoctorIssue { - code: String::from(code), - message: format!("{message} Path: {}", path.display()), - repair: String::from(repair), - blocking: true, - }); - } -} - -fn validate_directory( - path: &Path, - issues: &mut Vec, - code: &str, - message: &str, - repair: &str, -) { - if !path.is_dir() { - issues.push(DoctorIssue { - code: String::from(code), - message: format!("{message} Path: {}", path.display()), - repair: String::from(repair), - blocking: true, - }); - } -} - -fn validate_bridge_socket(path: &Path, issues: &mut Vec) { - match fs::metadata(path) { - Ok(metadata) if metadata.file_type().is_socket() => {} - Ok(_) => issues.push(DoctorIssue { - code: String::from("bridge-socket-invalid"), - message: format!( - "Bridge socket path exists but is not a Unix socket: {}", - path.display() - ), - repair: String::from( - "Fully quit and reopen iTerm2 so the Session Deck AutoLaunch bridge can recreate its socket.", - ), - blocking: false, - }), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => issues.push(DoctorIssue { - code: String::from("bridge-socket-missing"), - message: format!("Bridge socket is missing: {}", path.display()), - repair: String::from( - "Start iTerm2 and ensure the Session Deck AutoLaunch runtime is running.", - ), - blocking: false, - }), - Err(error) => issues.push(DoctorIssue { - code: String::from("bridge-socket-unreadable"), - message: format!("Could not inspect bridge socket {}: {error}", path.display()), - repair: String::from( - "Check the Session Deck bridge socket permissions and fully restart iTerm2.", - ), - blocking: false, - }), - } -} - -fn add_launch_prereq_issues(report: &LaunchPrereqReport, issues: &mut Vec) { - if report.tmux.status == "missing" { - issues.push(DoctorIssue { - code: String::from("tmux-missing"), - message: format!( - "tmux is missing on the effective command PATH derived from {}.", - report.path_provenance - ), - repair: String::from( - "Install tmux or fix the shell PATH used by Finder-launched apps.", - ), - blocking: false, - }); - } - - if report.pi.status == "missing" { - issues.push(DoctorIssue { - code: String::from("pi-missing"), - message: format!( - "pi is missing on the effective command PATH derived from {}.", - report.path_provenance - ), - repair: String::from("Install Pi or fix the shell PATH used by Finder-launched apps."), - blocking: false, - }); - } -} - -fn collect_launch_prereqs(effective_command_path: &EffectiveCommandPath) -> LaunchPrereqReport { - LaunchPrereqReport { - path_provenance: effective_command_path.provenance.clone(), - tmux: resolve_executable_on_path("tmux", effective_command_path), - pi: resolve_executable_on_path("pi", effective_command_path), - } -} - -fn resolve_executable_on_path( - command: &str, - effective_command_path: &EffectiveCommandPath, -) -> ExecutableStatus { - for entry in std::env::split_paths(OsStr::new(&effective_command_path.value)) { - let candidate = entry.join(command); - if candidate.is_file() && is_executable(&candidate) { - return ExecutableStatus { - status: String::from("available"), - path: Some(candidate.display().to_string()), - message: None, - }; - } - } - - ExecutableStatus { - status: String::from("missing"), - path: None, - message: None, - } -} - -fn is_executable(path: &Path) -> bool { - fs::metadata(path) - .map(|metadata| metadata.permissions().mode() & 0o111 != 0) - .unwrap_or(false) -} - -fn resolve_effective_command_path() -> EffectiveCommandPath { - let fallback = fallback_effective_command_path(); - let Some((shell_path, provenance)) = select_login_shell() else { - return fallback; - }; - - let shell_name = match shell_path.file_name().and_then(|value| value.to_str()) { - Some(value) => value, - None => return fallback, - }; - - let mut command = Command::new(&shell_path); - command - .arg("-i") - .arg("-c") - .arg(build_effective_command_path_probe_command()) - .arg0(format!("-{shell_name}")) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()); - - let Ok(mut child) = command.spawn() else { - return fallback; - }; - - match child.wait_timeout(EFFECTIVE_COMMAND_PATH_TIMEOUT) { - Ok(Some(status)) if status.success() => match child.wait_with_output() { - Ok(output) => match parse_effective_command_path_output(&output.stdout) { - Some(path_value) => EffectiveCommandPath { - value: path_value, - provenance, - }, - None => fallback, - }, - Err(_) => fallback, - }, - Ok(Some(_)) => fallback, - Ok(None) => { - let _ = child.kill(); - let _ = child.wait(); - fallback - } - Err(_) => fallback, - } -} - -fn fallback_effective_command_path() -> EffectiveCommandPath { - let inherited_path = std::env::var("PATH").ok(); - if let Some(value) = inherited_path { - if !value.trim().is_empty() { - return EffectiveCommandPath { - value, - provenance: String::from("inherited process PATH fallback"), - }; - } - } - - EffectiveCommandPath { - value: String::from(FALLBACK_COMMAND_PATH), - provenance: String::from("system fallback PATH"), - } -} - -fn select_login_shell() -> Option<(PathBuf, String)> { - if let Some(shell_path) = configured_login_shell_path() { - return Some(( - shell_path.clone(), - format!("configured login shell ({})", shell_path.display()), - )); - } - - let env_shell = std::env::var("SHELL").ok()?; - normalize_login_shell_path(&env_shell).map(|shell_path| { - ( - shell_path.clone(), - format!("$SHELL login shell fallback ({})", shell_path.display()), - ) - }) -} - -fn configured_login_shell_path() -> Option { - let uid = unsafe { geteuid() }; - let passwd = unsafe { getpwuid(uid) }; - if passwd.is_null() { - return None; - } - - let shell_ptr = unsafe { (*passwd).pw_shell }; - if shell_ptr.is_null() { - return None; - } - - let shell = unsafe { CStr::from_ptr(shell_ptr) } - .to_string_lossy() - .into_owned(); - normalize_login_shell_path(&shell) -} - -fn normalize_login_shell_path(candidate: &str) -> Option { - let trimmed = candidate.trim(); - if trimmed.is_empty() { - return None; - } - - let path = PathBuf::from(trimmed); - if !path.is_absolute() || !path.is_file() || !is_executable(&path) { - return None; - } - - Some(path) -} - -fn build_effective_command_path_probe_command() -> String { - format!( - "/usr/bin/printf '%s\\n' {EFFECTIVE_COMMAND_PATH_START}; /usr/bin/printenv PATH; /usr/bin/printf '%s\\n' {EFFECTIVE_COMMAND_PATH_END}" - ) -} - -fn parse_effective_command_path_output(stdout: &[u8]) -> Option { - let output = String::from_utf8_lossy(stdout); - let lines: Vec<&str> = output.lines().collect(); - let start_index = lines - .iter() - .position(|line| *line == EFFECTIVE_COMMAND_PATH_START)?; - let end_index = lines - .iter() - .skip(start_index + 1) - .position(|line| *line == EFFECTIVE_COMMAND_PATH_END)? - + start_index - + 1; - - if end_index != start_index + 2 { - return None; - } - - let path_value = lines[start_index + 1].trim(); - if path_value.is_empty() { - return None; - } - - Some(String::from(path_value)) -} - -#[cfg(test)] -mod tests { - use super::{ - derive_kill_action_helper_path, derive_open_action_helper_path, - derive_worktree_action_helper_path, discover_runtime_from_state_paths, - parse_desktop_install_state, parse_install_state, EffectiveCommandPath, - RuntimeMetadataSource, KILL_HELPER_RELATIVE_PATH, OPEN_HELPER_RELATIVE_PATH, - SNAPSHOT_HELPER_RELATIVE_PATH, WEB_ROOT_RELATIVE_PATH, WORKTREE_HELPER_RELATIVE_PATH, - }; - use serde::Deserialize; - use serde_json::{json, Value}; - use std::fs; - use std::os::unix::fs::PermissionsExt; - use std::path::Path; - use tempfile::tempdir; - - #[derive(Deserialize)] - #[serde(rename_all = "camelCase", deny_unknown_fields)] - struct RuntimeLayoutFixture { - schema_version: u64, - snapshot_helper_relative_path: String, - open_action_helper_relative_path: String, - kill_action_helper_relative_path: String, - worktree_action_helper_relative_path: String, - web_root_relative_path: String, - } - - #[test] - fn derives_allowlisted_paths_from_the_shared_runtime_layout_fixture() { - let layout: RuntimeLayoutFixture = - serde_json::from_str(include_str!("../../fixtures/runtime-layout-v1.json")).unwrap(); - let package_root = Path::new("/tmp/pi-session-deck"); - let snapshot_helper_path = package_root.join(&layout.snapshot_helper_relative_path); - - assert_eq!(layout.schema_version, 1); - assert_eq!( - layout.snapshot_helper_relative_path, - SNAPSHOT_HELPER_RELATIVE_PATH - ); - assert_eq!( - derive_open_action_helper_path(&snapshot_helper_path), - package_root.join(&layout.open_action_helper_relative_path) - ); - assert_eq!( - derive_kill_action_helper_path(&snapshot_helper_path), - package_root.join(&layout.kill_action_helper_relative_path) - ); - assert_eq!( - derive_worktree_action_helper_path(&snapshot_helper_path), - package_root.join(&layout.worktree_action_helper_relative_path) - ); - assert_eq!( - package_root.join(WEB_ROOT_RELATIVE_PATH), - package_root.join(&layout.web_root_relative_path) - ); - } - - #[test] - fn derives_helper_paths_from_snapshot_helper_path() { - let snapshot_helper_path = - Path::new("/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/snapshot-cli.js"); - - assert_eq!( - derive_open_action_helper_path(snapshot_helper_path), - Path::new( - "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/open-action-cli.js" - ) - ); - assert_eq!( - derive_kill_action_helper_path(snapshot_helper_path), - Path::new( - "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/kill-action-cli.js" - ) - ); - assert_eq!( - derive_worktree_action_helper_path(snapshot_helper_path), - Path::new("/tmp/pi-session-deck/dist/extensions/session-deck/worktree/action-cli.js") - ); - } - - #[test] - fn parses_desktop_install_state_and_derives_allowlisted_helper_paths() { - let package_root = Path::new("/tmp/pi-session-deck"); - let contents = desktop_state_json( - package_root, - Path::new("/usr/local/bin/node"), - "0.10.0", - "0.10.0", - ); - - let parsed = - parse_desktop_install_state(&contents, Path::new("/tmp/install.json")).unwrap(); - - assert_eq!(parsed.metadata_source, RuntimeMetadataSource::Desktop); - assert_eq!(parsed.package_root, package_root); - assert_eq!( - parsed.snapshot_helper_path, - package_root.join(SNAPSHOT_HELPER_RELATIVE_PATH) - ); - assert_eq!( - parsed.open_action_helper_path, - package_root.join(OPEN_HELPER_RELATIVE_PATH) - ); - assert_eq!( - parsed.kill_action_helper_path, - package_root.join(KILL_HELPER_RELATIVE_PATH) - ); - assert_eq!( - parsed.worktree_action_helper_path, - package_root.join(WORKTREE_HELPER_RELATIVE_PATH) - ); - } - - #[test] - fn accepts_isolated_development_metadata_with_its_distinct_discriminator() { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let package_root = temp.path().join("development-package"); - write_helper_package(&package_root, "0.10.0"); - let desktop_state_path = temp.path().join("desktop/install.json"); - write_state( - &desktop_state_path, - development_state_json(&package_root, &node_path, "0.10.0", "0.10.0"), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &temp.path().join("iterm2/install.json"), - write_effective_path_tools(temp.path()), - ); - let config = discovery - .config - .expect("development metadata should be usable"); - - assert_eq!(config.metadata_source, RuntimeMetadataSource::Desktop); - assert_eq!(config.package_version, "0.10.0"); - } - - #[test] - fn requires_the_complete_production_metadata_shape() { - let mut state: Value = serde_json::from_str(&desktop_state_json( - Path::new("/tmp/pi-session-deck"), - Path::new("/usr/local/bin/node"), - "0.10.0", - "0.10.0", - )) - .unwrap(); - state.as_object_mut().unwrap().remove("app"); - - let error = parse_desktop_install_state( - &serde_json::to_string(&state).unwrap(), - Path::new("/tmp/install.json"), - ) - .unwrap_err(); - - assert!(error.contains("missing field `app`")); - } - - #[test] - fn rejects_missing_non_string_or_blank_declared_helper_versions() { - let base: Value = serde_json::from_str(&desktop_state_json( - Path::new("/tmp/pi-session-deck"), - Path::new("/usr/local/bin/node"), - "0.10.0", - "0.10.0", - )) - .unwrap(); - - for (label, helper_version) in [ - ("missing", None), - ("non-string", Some(json!(12))), - ("blank", Some(json!(" "))), - ] { - let mut state = base.clone(); - let runtime = state.get_mut("runtime").unwrap().as_object_mut().unwrap(); - match helper_version { - Some(value) => { - runtime.insert(String::from("helperPackageVersion"), value); - } - None => { - runtime.remove("helperPackageVersion"); - } - } - - assert!( - parse_desktop_install_state( - &serde_json::to_string(&state).unwrap(), - Path::new("/tmp/install.json"), - ) - .is_err(), - "{label}" - ); - } - } - - #[test] - fn parses_install_state_and_derives_allowlisted_helper_paths() { - let contents = r#" - { - "schemaVersion": 1, - "product": "pi-session-deck-iterm2", - "packageVersion": "0.9.0", - "installedAt": "2026-07-17T00:00:00.000Z", - "scriptsDir": "/Users/tester/Library/Application Support/iTerm2/Scripts", - "script": { - "path": "/Users/tester/Library/Application Support/iTerm2/Scripts/AutoLaunch/session_deck.py", - "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "runtime": { - "nodeExecutablePath": "/usr/local/bin/node", - "snapshotHelperPath": "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/snapshot-cli.js", - "webRootPath": "/tmp/pi-session-deck/extensions/session-deck/iterm2/web", - "bridgeSocketPath": "/tmp/pi-session-deck/bridge.sock" - } - } - "#; - - let parsed = parse_install_state(contents, Path::new("/tmp/install.json")).unwrap(); - - assert_eq!( - parsed.metadata_source, - RuntimeMetadataSource::Iterm2Fallback - ); - assert_eq!(parsed.package_root, Path::new("/tmp/pi-session-deck")); - assert_eq!( - parsed.open_action_helper_path, - Path::new( - "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/open-action-cli.js" - ) - ); - assert_eq!( - parsed.kill_action_helper_path, - Path::new( - "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/kill-action-cli.js" - ) - ); - assert_eq!( - parsed.worktree_action_helper_path, - Path::new("/tmp/pi-session-deck/dist/extensions/session-deck/worktree/action-cli.js") - ); - } - - #[test] - fn rejects_relative_runtime_paths() { - let contents = r#" - { - "schemaVersion": 1, - "product": "pi-session-deck-iterm2", - "packageVersion": "0.9.0", - "installedAt": "2026-07-17T00:00:00.000Z", - "scriptsDir": "/tmp/scripts", - "script": { - "path": "/tmp/scripts/AutoLaunch/session_deck.py", - "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "runtime": { - "nodeExecutablePath": "node", - "snapshotHelperPath": "/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/snapshot-cli.js", - "webRootPath": "/tmp/pi-session-deck/extensions/session-deck/iterm2/web", - "bridgeSocketPath": "/tmp/pi-session-deck/bridge.sock" - } - } - "#; - - let error = parse_install_state(contents, Path::new("/tmp/install.json")).unwrap_err(); - assert!(error.contains("runtime.nodeExecutablePath must be an absolute path")); - } - - #[test] - fn discovers_desktop_metadata_before_iterm2_fallback() { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let effective_path = write_effective_path_tools(temp.path()); - - let desktop_package_root = temp.path().join("desktop-package"); - let fallback_package_root = temp.path().join("fallback-package"); - write_helper_package(&desktop_package_root, "1.2.0"); - write_helper_package(&fallback_package_root, "0.9.0"); - - let desktop_state_path = temp - .path() - .join("home/.pi/session-deck/desktop/install.json"); - let fallback_state_path = temp - .path() - .join("home/.pi/session-deck/iterm2/install.json"); - write_state( - &desktop_state_path, - desktop_state_json(&desktop_package_root, &node_path, "1.2.0", "1.2.0"), - ); - write_state( - &fallback_state_path, - iterm2_state_json(&fallback_package_root, &node_path, temp.path()), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &fallback_state_path, - effective_path, - ); - let config = discovery.config.expect("desktop metadata should be usable"); - - assert_eq!(config.metadata_source, RuntimeMetadataSource::Desktop); - assert_eq!(config.package_root, desktop_package_root); - assert_eq!(config.package_version, "1.2.0"); - assert_eq!(config.helper_package_version.as_deref(), Some("1.2.0")); - assert_eq!(discovery.status.metadata_source, "desktop"); - assert_eq!( - discovery.status.state_path, - desktop_state_path.display().to_string() - ); - assert!(discovery - .status - .issues - .iter() - .all(|issue| issue.code != "iterm2-install-state-fallback")); - } - - #[test] - fn blocks_desktop_runtime_when_the_actual_helper_version_is_invalid() { - let package_json_cases = [ - ("missing version", Some("{}")), - ("non-string version", Some(r#"{"version": 12}"#)), - ("blank version", Some(r#"{"version": " "}"#)), - ("malformed package JSON", Some("{")), - ("missing package JSON", None), - ]; - - for (label, package_json) in package_json_cases { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let package_root = temp.path().join("desktop-package"); - write_helper_files(&package_root); - if let Some(package_json) = package_json { - fs::write(package_root.join("package.json"), package_json).unwrap(); - } - let desktop_state_path = temp.path().join("desktop/install.json"); - write_state( - &desktop_state_path, - desktop_state_json(&package_root, &node_path, "1.2.0", "1.2.0"), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &temp.path().join("iterm2/install.json"), - write_effective_path_tools(temp.path()), - ); - - assert!(discovery.config.is_none(), "{label}"); - assert!( - discovery.status.issues.iter().any(|issue| { - issue.code == "helper-package-version-invalid" && issue.blocking - }), - "{label}" - ); - } - } - - #[test] - fn blocks_desktop_runtime_when_declared_or_actual_helper_versions_mismatch() { - for (label, declared_version, actual_version) in [ - ("declared mismatch", "1.1.0", "1.1.0"), - ("actual mismatch", "1.2.0", "1.1.0"), - ] { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let package_root = temp.path().join("desktop-package"); - write_helper_package(&package_root, actual_version); - let desktop_state_path = temp.path().join("desktop/install.json"); - write_state( - &desktop_state_path, - desktop_state_json(&package_root, &node_path, "1.2.0", declared_version), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &temp.path().join("iterm2/install.json"), - write_effective_path_tools(temp.path()), - ); - - assert!(discovery.config.is_none(), "{label}"); - assert!( - discovery - .status - .issues - .iter() - .any(|issue| issue.blocking && issue.code.contains("version-mismatch")), - "{label}" - ); - } - } - - #[test] - fn falls_back_to_iterm2_metadata_when_desktop_metadata_is_missing() { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let effective_path = write_effective_path_tools(temp.path()); - - let fallback_package_root = temp.path().join("fallback-package"); - write_helper_package(&fallback_package_root, "0.9.0"); - - let desktop_state_path = temp - .path() - .join("home/.pi/session-deck/desktop/install.json"); - let fallback_state_path = temp - .path() - .join("home/.pi/session-deck/iterm2/install.json"); - write_state( - &fallback_state_path, - iterm2_state_json(&fallback_package_root, &node_path, temp.path()), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &fallback_state_path, - effective_path, - ); - let config = discovery - .config - .expect("fallback metadata should be usable"); - - assert_eq!( - config.metadata_source, - RuntimeMetadataSource::Iterm2Fallback - ); - assert_eq!(config.package_root, fallback_package_root); - assert_eq!(discovery.status.metadata_source, "iterm2-fallback"); - assert_eq!( - discovery.status.state_path, - fallback_state_path.display().to_string() - ); - - let fallback_issue = discovery - .status - .issues - .iter() - .find(|issue| issue.code == "iterm2-install-state-fallback") - .expect("fallback diagnostic should be explicit"); - assert!(!fallback_issue.blocking); - assert!(fallback_issue - .message - .contains("legacy iTerm2 install metadata")); - assert!(fallback_issue - .repair - .contains("/session-deck desktop install")); - assert!(!fallback_issue.repair.contains("/session-deck iterm2")); - } - - #[test] - fn invalid_desktop_metadata_does_not_silently_fallback_to_iterm2() { - let temp = tempdir().unwrap(); - let node_path = temp.path().join("bin/node"); - write_executable(&node_path); - let effective_path = write_effective_path_tools(temp.path()); - - let fallback_package_root = temp.path().join("fallback-package"); - write_helper_package(&fallback_package_root, "0.9.0"); - - let desktop_state_path = temp - .path() - .join("home/.pi/session-deck/desktop/install.json"); - let fallback_state_path = temp - .path() - .join("home/.pi/session-deck/iterm2/install.json"); - write_state(&desktop_state_path, String::from("not json")); - write_state( - &fallback_state_path, - iterm2_state_json(&fallback_package_root, &node_path, temp.path()), - ); - - let discovery = discover_runtime_from_state_paths( - &desktop_state_path, - &fallback_state_path, - effective_path, - ); - - assert!(discovery.config.is_none()); - let issue = discovery - .status - .issues - .iter() - .find(|issue| issue.code == "desktop-install-state-invalid") - .expect("invalid desktop metadata should be reported"); - assert!(issue.blocking); - assert!(issue.repair.contains("/session-deck desktop install")); - assert!(!issue.repair.contains("/session-deck iterm2")); - } - - fn desktop_state_json( - package_root: &Path, - node_path: &Path, - package_version: &str, - helper_package_version: &str, - ) -> String { - serde_json::to_string_pretty(&json!({ - "schemaVersion": 1, - "product": "session-deck-desktop", - "packageName": "@robhowley/pi-session-deck", - "packageVersion": package_version, - "installedAt": "2026-07-17T00:00:00.000Z", - "app": { - "path": "/Users/tester/Applications/Session Deck.app", - "bundleIdentifier": "dev.pi-userland.session-deck.desktop", - "name": "Session Deck", - "version": package_version, - "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "source": { - "kind": "local-path", - "path": "/tmp/Session Deck.app", - "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }, - "runtime": { - "nodeExecutablePath": node_path, - "packageRoot": package_root, - "helperPackageVersion": helper_package_version - }, - "ownedPaths": ["/Users/tester/Applications/Session Deck.app"] - })) - .unwrap() - } - - fn development_state_json( - package_root: &Path, - node_path: &Path, - package_version: &str, - helper_package_version: &str, - ) -> String { - serde_json::to_string_pretty(&json!({ - "schemaVersion": 1, - "product": "session-deck-desktop-development", - "packageName": "@robhowley/pi-session-deck", - "packageVersion": package_version, - "installedAt": "2026-07-17T00:00:00.000Z", - "runtime": { - "nodeExecutablePath": node_path, - "packageRoot": package_root, - "helperPackageVersion": helper_package_version - } - })) - .unwrap() - } - - fn iterm2_state_json(package_root: &Path, node_path: &Path, temp_root: &Path) -> String { - serde_json::to_string_pretty(&json!({ - "schemaVersion": 1, - "product": "pi-session-deck-iterm2", - "packageVersion": "0.9.0", - "installedAt": "2026-07-17T00:00:00.000Z", - "scriptsDir": temp_root.join("scripts"), - "script": { - "path": temp_root.join("scripts/AutoLaunch/session_deck.py"), - "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - }, - "runtime": { - "nodeExecutablePath": node_path, - "snapshotHelperPath": package_root.join(SNAPSHOT_HELPER_RELATIVE_PATH), - "webRootPath": package_root.join(WEB_ROOT_RELATIVE_PATH), - "bridgeSocketPath": temp_root.join("bridge.sock") - } - })) - .unwrap() - } - - fn write_helper_package(package_root: &Path, version: &str) { - write_helper_files(package_root); - fs::write( - package_root.join("package.json"), - serde_json::to_string(&json!({ - "name": "@robhowley/pi-session-deck", - "version": version - })) - .unwrap(), - ) - .unwrap(); - } - - fn write_helper_files(package_root: &Path) { - fs::create_dir_all(package_root).unwrap(); - write_readable_file(&package_root.join(SNAPSHOT_HELPER_RELATIVE_PATH)); - write_readable_file(&package_root.join(OPEN_HELPER_RELATIVE_PATH)); - write_readable_file(&package_root.join(KILL_HELPER_RELATIVE_PATH)); - write_readable_file(&package_root.join(WORKTREE_HELPER_RELATIVE_PATH)); - fs::create_dir_all(package_root.join(WEB_ROOT_RELATIVE_PATH)).unwrap(); - } - - fn write_effective_path_tools(temp_root: &Path) -> EffectiveCommandPath { - let bin = temp_root.join("bin"); - write_executable(&bin.join("pi")); - write_executable(&bin.join("tmux")); - EffectiveCommandPath { - value: bin.display().to_string(), - provenance: String::from("test PATH"), - } - } - - fn write_state(path: &Path, contents: String) { - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, contents).unwrap(); - } - - fn write_readable_file(path: &Path) { - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, "// helper\n").unwrap(); - } - - fn write_executable(path: &Path) { - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, "#!/bin/sh\n").unwrap(); - let mut permissions = fs::metadata(path).unwrap().permissions(); - permissions.set_mode(0o755); - fs::set_permissions(path, permissions).unwrap(); - } -} diff --git a/apps/session-deck-desktop/src-tauri/tauri.conf.json b/apps/session-deck-desktop/src-tauri/tauri.conf.json deleted file mode 100644 index 9ee1db4f..00000000 --- a/apps/session-deck-desktop/src-tauri/tauri.conf.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "Session Deck Desktop", - "version": "0.0.0", - "identifier": "dev.pi-userland.session-deck.desktop", - "build": { - "beforeBuildCommand": "pnpm run sync:web", - "beforeDevCommand": "pnpm run sync:web", - "frontendDist": "../web" - }, - "app": { - "withGlobalTauri": true, - "trayIcon": { - "id": "session-deck", - "iconPath": "icons/tray-icon.png", - "iconAsTemplate": true, - "tooltip": "Session Deck", - "showMenuOnLeftClick": false - }, - "security": { - "csp": "default-src 'self'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'" - }, - "windows": [ - { - "label": "main", - "title": "Session Deck", - "theme": "Dark", - "width": 420, - "height": 920, - "resizable": true, - "visible": true - } - ] - }, - "bundle": { - "active": true, - "targets": ["app"], - "icon": ["icons/icon.png", "icons/icon.icns"], - "publisher": "Rob Howley", - "category": "DeveloperTool", - "shortDescription": "Desktop companion for Session Deck.", - "longDescription": "A private Tauri desktop companion distributed as a GitHub Release artifact for @robhowley/pi-session-deck.", - "macOS": { - "bundleName": "Session Deck", - "minimumSystemVersion": "11.0", - "signingIdentity": "-" - } - } -} diff --git a/apps/session-deck-desktop/tsconfig.json b/apps/session-deck-desktop/tsconfig.json deleted file mode 100644 index 791573cf..00000000 --- a/apps/session-deck-desktop/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "checkJs": true, - "declaration": false, - "declarationMap": false, - "lib": ["ES2022", "DOM"], - "types": ["node", "vitest/globals"] - }, - "include": ["web/**/*.js", "scripts/**/*.js", "__tests__/**/*.ts"], - "exclude": ["web/session-deck-ui.js"] -} diff --git a/apps/session-deck-desktop/web/app.js b/apps/session-deck-desktop/web/app.js deleted file mode 100644 index a25b7297..00000000 --- a/apps/session-deck-desktop/web/app.js +++ /dev/null @@ -1,88 +0,0 @@ -/* global document, window */ -import { createTauriSessionDeckHost } from './tauri-host.js'; - -const MISSING_SHARED_UI_MESSAGE = - 'Session Deck desktop could not load the shared session-deck-ui.js asset.'; -const NEXT_STEP_MESSAGE = - 'Rebuild the desktop web assets with `pnpm --filter ./apps/session-deck-desktop sync:web`, then relaunch the app.'; - -/** - * @param {{ - * document?: Document, - * host?: ReturnType, - * sessionDeckUi?: { mount: (options: { host: unknown, document: Document, window: Window & typeof globalThis }) => unknown } | null, - * window?: Window & typeof globalThis, - * }} [options] - * @returns {{ mode: 'shared-ui' | 'placeholder', host: ReturnType }} - */ -export function mountDesktopApp(options = {}) { - const windowLike = options.window ?? globalThis.window; - const documentLike = options.document ?? globalThis.document; - const host = options.host ?? createTauriSessionDeckHost({ window: windowLike }); - const sessionDeckUi = - options.sessionDeckUi ?? - /** @type {{ SessionDeckUI?: { mount: (options: { host: unknown, document: Document, window: Window & typeof globalThis }) => unknown } | null }} */ ( - windowLike ?? {} - ).SessionDeckUI ?? - null; - - if (sessionDeckUi && typeof sessionDeckUi.mount === 'function') { - sessionDeckUi.mount({ - host, - document: documentLike, - window: windowLike, - }); - return { mode: 'shared-ui', host }; - } - - renderPlaceholder(documentLike, host.doctorCommand); - return { mode: 'placeholder', host }; -} - -/** - * @param {Document} documentLike - * @param {string} doctorCommand - */ -export function renderPlaceholder(documentLike, doctorCommand) { - const summary = documentLike.getElementById('summary'); - const banner = documentLike.getElementById('banner'); - const list = documentLike.getElementById('list'); - const empty = documentLike.getElementById('empty'); - const diagnosticsPanel = documentLike.getElementById('diagnostics-panel'); - const diagnostics = documentLike.getElementById('diagnostics'); - - summary?.replaceChildren(documentLike.createTextNode('Desktop shell ready')); - - if (banner) { - banner.classList.remove('hidden'); - banner.replaceChildren(documentLike.createTextNode(MISSING_SHARED_UI_MESSAGE)); - } - - list?.replaceChildren(); - - if (empty) { - empty.classList.remove('hidden'); - empty.replaceChildren(documentLike.createTextNode(NEXT_STEP_MESSAGE)); - } - - if (diagnosticsPanel) { - diagnosticsPanel.classList.remove('hidden'); - } - - if (diagnostics) { - diagnostics.replaceChildren(); - const firstLine = documentLike.createElement('li'); - firstLine.className = 'diag-line'; - firstLine.textContent = 'Desktop bootstrap could not find the shared Session Deck UI asset.'; - - const secondLine = documentLike.createElement('li'); - secondLine.className = 'diag-line'; - secondLine.textContent = doctorCommand; - - diagnostics.append(firstLine, secondLine); - } -} - -if (typeof window !== 'undefined' && typeof document !== 'undefined') { - mountDesktopApp(); -} diff --git a/apps/session-deck-desktop/web/index.html b/apps/session-deck-desktop/web/index.html deleted file mode 100644 index d4e35d00..00000000 --- a/apps/session-deck-desktop/web/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - Session Deck - - - -
-
-
-

Loading…

-
- - - -
-
-
- - - -
-
- -
-
- - -
- - - - diff --git a/apps/session-deck-desktop/web/session-deck-ui.js b/apps/session-deck-desktop/web/session-deck-ui.js deleted file mode 100644 index a31c2ef7..00000000 --- a/apps/session-deck-desktop/web/session-deck-ui.js +++ /dev/null @@ -1,3389 +0,0 @@ -(function () { - function mountSessionDeckUI(options = {}) { - const host = options.host; - if (!host) { - throw new Error('Session Deck host is required.'); - } - - const document = options.document ?? globalThis.document; - const window = options.window ?? globalThis.window ?? globalThis; - const HTMLButtonElement = window.HTMLButtonElement ?? globalThis.HTMLButtonElement; - const HTMLInputElement = window.HTMLInputElement ?? globalThis.HTMLInputElement; - const URL = window.URL ?? globalThis.URL; - - const LAUNCH_AGENT_DIR_MODE_OPTIONS = Object.freeze(['ambient', 'default', 'custom']); - const RESTART_JOURNAL_STATES = new Set([ - 'preparing', - 'term-sent', - 'kill-sent', - 'stopped', - 'spawn-requested', - 'observing', - 'restarted', - 'stop-failed', - 'stopped-not-restarted', - 'outcome-unknown', - ]); - const RESTART_REASON_CODES = new Set([ - 'replacement-observed', - 'managed-recipe-unavailable', - 'recipe-not-bound', - 'recipe-invalid', - 'runtime-unavailable', - 'identity-mismatch', - 'session-file-unavailable', - 'cwd-unavailable', - 'pi-executable-unavailable', - 'tmux-target-unavailable', - 'tmux-pane-mismatch', - 'unsafe-descendants', - 'hosting-runtime', - 'coordinator-runtime', - 'generation-changed', - 'operation-in-progress', - 'termination-failed', - 'pane-did-not-stop', - 'respawn-failed', - 'replacement-unobserved', - 'operation-state-unknown', - ]); - - function formatLaunchAgentDirOptionLabel(mode) { - switch (mode) { - case 'default': - return 'Pi default'; - case 'custom': - return 'Custom…'; - default: - return 'Current'; - } - } - - function formatLaunchContextPreviewSummary(preview) { - if ( - typeof preview === 'object' && - preview !== null && - preview.status === 'resolved' && - typeof preview.effectiveDisplay === 'string' && - preview.effectiveDisplay.length > 0 - ) { - return `Pi config → ${preview.effectiveDisplay}`; - } - if (typeof preview === 'object' && preview !== null && preview.status === 'failed') { - return 'Pi config unavailable'; - } - return 'Pi config resolving…'; - } - - const AUTO_REFRESH_INTERVAL_MS = 15_000; - const COLLAPSED_CHIP_LIMIT = 2; - const DEFAULT_VISIBLE_STATES = new Set(['live', 'stale']); - const HOME_PREFIXES = ['/Users/', '/home/']; - const NO_REPO_GROUP_KEY = 'no-repo'; - const NO_REPO_LABEL = 'No repo'; - const NEW_SESSION_OWNER_KEY = 'new-session-footer'; - const SUCCESS_PENDING_WORKTREE_TTL_MS = 12_000; - const OPEN_TERMINAL_SUCCESS_TTL_MS = 4_000; - const KILL_SESSION_SUCCESS_TTL_MS = 6_000; - const DEFAULT_OPEN_TERMINAL_FAILURE_MESSAGE = 'Could not request terminal open.'; - const DEFAULT_KILL_SESSION_FAILURE_MESSAGE = 'Could not request session end.'; - const DEFAULT_RESTART_SESSION_FAILURE_MESSAGE = 'Could not restart this session.'; - const SPAWNED_CHILD_RUNTIME_TOOLTIP = 'Ephemeral child sessions excluded from the deck.'; - const KILL_SESSION_FAILURE_MESSAGES = { - 'invalid-runtime-id': 'Session runtime metadata is invalid.', - 'presence-missing': 'Session runtime metadata is no longer available.', - 'presence-malformed': 'Session runtime metadata is invalid.', - 'runtime-mismatch': 'Session runtime metadata is invalid.', - 'pid-reused': 'The recorded process no longer matches this session.', - 'pid-unverified': 'Could not safely verify the selected process.', - 'self-signal-denied': 'Session Deck cannot signal its own helper process.', - 'permission-denied': 'Termination is not permitted for this process.', - 'signal-failed': DEFAULT_KILL_SESSION_FAILURE_MESSAGE, - }; - const DEFAULT_DOCTOR_COMMAND = '/session-deck iterm2 doctor'; - const doctorCommand = host.doctorCommand ?? DEFAULT_DOCTOR_COMMAND; - const INLINE_WORKTREE_FAILURE_REASONS = new Set([ - 'invalid-branch', - 'invalid-base-ref', - 'repo-intent-unresolved', - 'repo-intent-ambiguous', - ]); - - const state = { - snapshot: null, - selectedRuntimeId: null, - detailVisible: false, - showAll: false, - loading: false, - fetchError: null, - expandedRepoKeys: new Set(), - activeWorktreeFormRepoKey: null, - worktreeForms: new Map(), - noRepoSessionForms: new Map(), - worktreeBasePreviews: new Map(), - worktreeLaunchPreviews: new Map(), - nextWorktreeBasePreviewRequestId: 0, - nextWorktreeLaunchPreviewRequestId: 0, - pendingWorktrees: new Map(), - openTerminalAction: null, - killSessionAction: null, - restartSessionAction: null, - highlightedRuntimeId: null, - }; - - const elements = { - summary: document.getElementById('summary'), - showAll: document.getElementById('show-all'), - liveCount: document.getElementById('live-count'), - refresh: document.getElementById('refresh'), - banner: document.getElementById('banner'), - listShell: document.getElementById('list-shell'), - list: document.getElementById('list'), - empty: document.getElementById('empty'), - newSession: document.getElementById('new-session'), - diagnosticsPanel: document.getElementById('diagnostics-panel'), - diagnostics: document.getElementById('diagnostics'), - }; - - if ( - Object.values(elements).some((element) => element === null) || - !(elements.showAll instanceof HTMLInputElement) || - !(elements.refresh instanceof HTMLButtonElement) - ) { - throw new Error('Session Deck web UI failed to initialize.'); - } - - function init() { - elements.showAll.addEventListener('change', () => { - state.showAll = elements.showAll.checked; - reconcileSelection(); - reconcileExpandedRepoKeys(); - reconcileOpenTerminalAction(); - reconcileKillSessionAction(); - reconcileRestartSessionAction(); - render(); - }); - - if (typeof window.addEventListener === 'function') { - window.addEventListener('keydown', (event) => { - if ( - event.key === 'Escape' && - (clearKillSessionConfirmation() || clearRestartSessionConfirmation()) - ) { - render(); - } - }); - } - - elements.refresh.addEventListener('click', () => { - void refreshSnapshot({ source: 'manual' }); - }); - - window.setInterval(() => { - void refreshSnapshot({ source: 'auto' }); - }, AUTO_REFRESH_INTERVAL_MS); - - void refreshSnapshot({ source: 'startup' }); - } - - async function refreshSnapshot({ source }) { - state.loading = source !== 'auto'; - if (source !== 'auto') { - state.fetchError = null; - render(); - } - - try { - state.snapshot = normalizeSnapshot(await host.loadSnapshot()); - state.fetchError = null; - reconcileSelection(); - reconcileExpandedRepoKeys(); - reconcilePendingWorktrees(); - reconcileOpenTerminalAction(); - reconcileKillSessionAction(); - reconcileRestartSessionAction(); - } catch (error) { - state.fetchError = error instanceof Error ? error.message : String(error); - if (source === 'startup') { - state.snapshot = emptySnapshot(`Snapshot request failed: ${state.fetchError}`); - reconcileSelection(); - reconcileExpandedRepoKeys(); - reconcileOpenTerminalAction(); - reconcileKillSessionAction(); - reconcileRestartSessionAction(); - } - } finally { - state.loading = false; - render(); - } - } - - function normalizeSnapshot(payload) { - if (!isObject(payload)) { - return emptySnapshot('Snapshot root is not an object.'); - } - - if (!isSessionDeckSnapshot(payload)) { - return emptySnapshot('Snapshot payload does not match SessionDeckSnapshot.'); - } - - return payload; - } - - function isSessionDeckSnapshot(candidate) { - return ( - typeof candidate.generatedAt === 'string' && - Array.isArray(candidate.records) && - candidate.records.every(isSessionDeckRecord) && - Array.isArray(candidate.diagnostics) && - candidate.diagnostics.every(isSessionDeckDiagnostic) - ); - } - - function isSessionDeckRecord(candidate) { - return ( - isObject(candidate) && - typeof candidate.runtimeId === 'string' && - isNullableNumber(candidate.pid) && - isPresenceState(candidate.presenceState) && - isOptionalString(candidate.presenceReason) && - typeof candidate.heartbeatAgeMs === 'number' && - isNullableString(candidate.sessionId) && - isNullableString(candidate.sessionName) && - isNullableString(candidate.repoName) && - isNullableString(candidate.qualifiedRepoName) && - isNullableString(candidate.cwd) && - isNullableString(candidate.branch) && - isNullableString(candidate.prUrl) && - isNullableBoolean(candidate.isLinkedWorktree) && - isNullableString(candidate.worktreeLabel) && - isOptionalRestartEligibility(candidate.restart) && - isActivityState(candidate.activityState) && - isNullableNumber(candidate.activityAgeMs) && - isNullableString(candidate.currentToolName) && - isNullableString(candidate.lastError) && - isNullableCompaction(candidate.compaction) && - Array.isArray(candidate.chips) && - candidate.chips.every((chip) => typeof chip === 'string') && - Array.isArray(candidate.diagnostics) && - candidate.diagnostics.every(isSessionDeckDiagnostic) - ); - } - - function isOptionalRestartEligibility(candidate) { - if (candidate === undefined) { - return true; - } - - if (!isObject(candidate)) { - return false; - } - - if (candidate.available === true) { - return ( - isNonBlankString(candidate.generation) && - (candidate.operation === undefined || isRestartOperation(candidate.operation)) - ); - } - - return candidate.available === false && isKnownRestartReason(candidate.reason); - } - - function isRestartOperation(candidate) { - return ( - isObject(candidate) && - isNonBlankString(candidate.operationId) && - isKnownRestartJournalState(candidate.status) && - typeof candidate.retryable === 'boolean' - ); - } - - function isKnownRestartJournalState(value) { - return typeof value === 'string' && RESTART_JOURNAL_STATES.has(value); - } - - function isKnownRestartReason(value) { - return typeof value === 'string' && RESTART_REASON_CODES.has(value); - } - - function isSessionDeckDiagnostic(candidate) { - return ( - isObject(candidate) && - typeof candidate.code === 'string' && - typeof candidate.message === 'string' && - isOptionalString(candidate.runtimeId) && - isOptionalString(candidate.filePath) - ); - } - - function isPresenceState(value) { - return ['live', 'stale', 'dead', 'unknown'].includes(value); - } - - function isActivityState(value) { - return [ - 'idle', - 'thinking', - 'tool-running', - 'compacting', - 'awaiting-input', - 'error', - 'unknown', - ].includes(value); - } - - function isNullableCompaction(value) { - if (value === undefined || value === null) { - return true; - } - - return ( - isObject(value) && - ['running', 'stale'].includes(value.state) && - typeof value.ageMs === 'number' && - typeof value.startedAt === 'string' && - (value.reason === null || ['manual', 'threshold', 'overflow'].includes(value.reason)) && - typeof value.willRetry === 'boolean' - ); - } - - function isNullableString(value) { - return typeof value === 'string' || value === null; - } - - function isOptionalString(value) { - return value === undefined || typeof value === 'string'; - } - - function isNullableNumber(value) { - return typeof value === 'number' || value === null; - } - - function isNullableBoolean(value) { - return typeof value === 'boolean' || value === null; - } - - function emptySnapshot(message) { - return { - generatedAt: new Date().toISOString(), - records: [], - diagnostics: [ - { code: 'toolbelt_snapshot_unavailable', message, runtimeId: null, filePath: null }, - ], - }; - } - - function reconcileSelection() { - const visibleRecords = getVisibleRecords(); - if (visibleRecords.length === 0) { - state.selectedRuntimeId = null; - return; - } - - const stillVisible = - state.selectedRuntimeId !== null && - visibleRecords.some((record) => record.runtimeId === state.selectedRuntimeId); - - if (!state.detailVisible) { - if (!stillVisible) { - state.selectedRuntimeId = null; - } - return; - } - - if (!stillVisible) { - state.selectedRuntimeId = visibleRecords[0].runtimeId; - } - } - - function getVisibleRecords() { - const records = state.snapshot?.records ?? []; - return records.filter( - (record) => - !isTempSession(record) && - (state.showAll || - DEFAULT_VISIBLE_STATES.has(record.presenceState) || - record.restart?.operation !== undefined), - ); - } - - function reconcileExpandedRepoKeys(repoGroups = createRepoGroups(getVisibleRecords())) { - const visibleRepoKeys = new Set(repoGroups.map((repoGroup) => repoGroup.key)); - for (const expandedRepoKey of state.expandedRepoKeys) { - if (!visibleRepoKeys.has(expandedRepoKey)) { - state.expandedRepoKeys.delete(expandedRepoKey); - } - } - } - - function reconcilePendingWorktrees(repoGroups = createRepoGroups(getVisibleRecords())) { - for (const [repoKey, pending] of [...state.pendingWorktrees.entries()]) { - if (pending.kind !== 'success' && pending.kind !== 'unknown') { - continue; - } - - if (hasObservedPendingActionSuccess(pending, repoGroups)) { - clearPendingWorktree(repoKey); - } - } - } - - function reconcileOpenTerminalAction() { - const action = state.openTerminalAction; - if (action === null) { - return; - } - - const isStillVisible = getVisibleRecords().some( - (record) => record.runtimeId === action.runtimeId, - ); - if (!isStillVisible) { - clearOpenTerminalAction(); - } - } - - function reconcileKillSessionAction() { - const action = state.killSessionAction; - if (action?.kind !== 'confirming' && action?.kind !== 'unknown') { - return; - } - - const isStillVisible = getVisibleRecords().some( - (record) => record.runtimeId === action.runtimeId, - ); - if (!isStillVisible) { - clearKillSessionAction(); - } - } - - function reconcileRestartSessionAction() { - const action = state.restartSessionAction; - if (action?.kind !== 'confirming' && action?.kind !== 'unknown') return; - const record = getVisibleRecords().find( - (candidate) => candidate.runtimeId === action.runtimeId, - ); - if (record === undefined) { - state.restartSessionAction = null; - return; - } - if ( - action.kind === 'unknown' && - record.restart?.available === true && - record.restart.operation?.operationId === action.operationId - ) { - state.restartSessionAction = { - ...action, - message: 'Restart outcome still needs reconciliation.', - }; - } - } - - function hasObservedPendingActionSuccess(pending, repoGroups) { - return repoGroups.some((repoGroup) => - repoGroup.records.some((record) => doesRecordMatchPendingAction(record, pending)), - ); - } - - function doesRecordMatchPendingAction(record, pending) { - if (isNonEmptyString(pending.runtimeId)) { - return record.runtimeId === pending.runtimeId; - } - - return pending.sessionKind === 'cwd' - ? doesRecordMatchPendingCwdSession(record, pending) - : doesRecordMatchPendingWorktree(record, pending); - } - - function doesRecordMatchPendingCwdSession(record, pending) { - if (!isNonEmptyString(record.cwd)) { - return false; - } - - return [pending.cwd, pending.resultCwd].some( - (cwd) => - isNonEmptyString(cwd) && (record.cwd === cwd || shortenHomePath(record.cwd) === cwd), - ); - } - - function doesRecordMatchPendingWorktree(record, pending) { - const request = pending.request; - if ( - !request || - record.branch !== request.branchName || - !repoIntentMatchesRecord(record, request.repoIntent) - ) { - return false; - } - - return record.isLinkedWorktree === true || isNonEmptyString(record.worktreeLabel); - } - - function repoIntentMatchesRecord(record, repoIntent) { - if (isNonEmptyString(repoIntent.qualifiedRepoName)) { - return record.qualifiedRepoName === repoIntent.qualifiedRepoName; - } - - if (!isNonEmptyString(repoIntent.repoName)) { - return false; - } - - return ( - record.repoName === repoIntent.repoName || - getQualifiedRepoShortName(record.qualifiedRepoName) === repoIntent.repoName - ); - } - - function getQualifiedRepoShortName(qualifiedRepoName) { - return isNonEmptyString(qualifiedRepoName) ? getRepoShortName(qualifiedRepoName) : null; - } - - function isNonEmptyString(value) { - return typeof value === 'string' && value.length > 0; - } - - function isNonBlankString(value) { - return typeof value === 'string' && value.trim().length > 0; - } - - function createRepoGroups(records) { - const qualifiedGroups = new Map(); - const qualifiedKeysByShortName = new Map(); - - for (const record of records) { - const qualifiedRepoName = getRepoIdentityValue(record.qualifiedRepoName); - if (!qualifiedRepoName) { - continue; - } - - const key = getQualifiedRepoKey(qualifiedRepoName); - if (!qualifiedGroups.has(key)) { - const repoGroup = { - key, - label: qualifiedRepoName, - kind: 'qualified', - records: [], - }; - qualifiedGroups.set(key, repoGroup); - - const shortName = getRepoShortName(qualifiedRepoName); - const matchingKeys = qualifiedKeysByShortName.get(shortName) ?? new Set(); - matchingKeys.add(key); - qualifiedKeysByShortName.set(shortName, matchingKeys); - } - } - - const unqualifiedTargets = new Map(); - for (const record of records) { - if (getRepoIdentityValue(record.qualifiedRepoName)) { - continue; - } - - const repoName = getRepoIdentityValue(record.repoName); - if (!repoName || unqualifiedTargets.has(repoName)) { - continue; - } - - const matchingQualifiedKeys = qualifiedKeysByShortName.get(repoName); - unqualifiedTargets.set( - repoName, - matchingQualifiedKeys?.size === 1 - ? matchingQualifiedKeys.values().next().value - : getUnqualifiedRepoKey(repoName), - ); - } - - const groupsByKey = new Map(); - for (const record of records) { - const qualifiedRepoName = getRepoIdentityValue(record.qualifiedRepoName); - if (qualifiedRepoName) { - getOrCreateRepoGroup( - groupsByKey, - getQualifiedRepoKey(qualifiedRepoName), - qualifiedRepoName, - 'qualified', - ).records.push(record); - continue; - } - - const repoName = getRepoIdentityValue(record.repoName); - if (repoName) { - const targetKey = unqualifiedTargets.get(repoName) ?? getUnqualifiedRepoKey(repoName); - const qualifiedGroup = qualifiedGroups.get(targetKey); - getOrCreateRepoGroup( - groupsByKey, - targetKey, - qualifiedGroup?.label ?? repoName, - qualifiedGroup?.kind ?? 'repo', - ).records.push(record); - continue; - } - - getOrCreateRepoGroup(groupsByKey, NO_REPO_GROUP_KEY, NO_REPO_LABEL, 'no-repo').records.push( - record, - ); - } - - return [...groupsByKey.values()].sort(compareRepoGroups); - } - - function getOrCreateRepoGroup(groupsByKey, key, label, kind) { - const existingGroup = groupsByKey.get(key); - if (existingGroup) { - return existingGroup; - } - - const repoGroup = { key, label, kind, records: [] }; - groupsByKey.set(key, repoGroup); - return repoGroup; - } - - function compareRepoGroups(left, right) { - if (left.kind === 'no-repo' || right.kind === 'no-repo') { - return left.kind === right.kind ? 0 : left.kind === 'no-repo' ? 1 : -1; - } - - const leftLabel = left.label.toLowerCase(); - const rightLabel = right.label.toLowerCase(); - const labelOrder = leftLabel.localeCompare(rightLabel); - return labelOrder === 0 ? left.key.localeCompare(right.key) : labelOrder; - } - - function getRepoIdentityValue(value) { - return typeof value === 'string' && value.length > 0 ? value : null; - } - - function getQualifiedRepoKey(qualifiedRepoName) { - return `qualified:${qualifiedRepoName}`; - } - - function getUnqualifiedRepoKey(repoName) { - return `repo:${repoName}`; - } - - function getRepoShortName(qualifiedRepoName) { - const parts = qualifiedRepoName.split('/'); - return parts[parts.length - 1] || qualifiedRepoName; - } - - function getRepoGroupRecordsId(repoGroupKey) { - const encodedKey = []; - for (let index = 0; index < repoGroupKey.length; index += 1) { - encodedKey.push(repoGroupKey.charCodeAt(index).toString(16).padStart(4, '0')); - } - return `repo-group-records-${encodedKey.join('-')}`; - } - - function formatRepoHeader(repoGroup) { - return `${repoGroup.label} · ${repoGroup.records.length}`; - } - - function getRepoLabelParts(label) { - const separatorIndex = label.lastIndexOf('/'); - if (separatorIndex <= 0 || separatorIndex === label.length - 1) { - return { owner: null, name: label }; - } - - return { - owner: label.slice(0, separatorIndex), - name: label.slice(separatorIndex + 1), - }; - } - - function createRepoHeaderLabel(repoGroup) { - const label = document.createElement('span'); - const labelParts = getRepoLabelParts(repoGroup.label); - label.className = 'repo-header-label'; - - if (repoGroup.kind === 'no-repo') { - label.append(createText('span', repoGroup.label)); - } else if (labelParts.owner) { - label.append( - createText('span', labelParts.owner, 'repo-owner'), - createText('span', '/', 'repo-owner repo-separator'), - createText('span', labelParts.name, 'repo-name'), - ); - } else { - label.append(createText('span', repoGroup.label, 'repo-name')); - } - - label.append( - createText('span', ' · ', 'repo-divider'), - createText('span', String(repoGroup.records.length), 'repo-count'), - ); - - return label; - } - - function render() { - const focusSnapshot = captureRenderFocus(); - renderSummary(); - renderBanner(); - renderList(); - renderNewSessionAction(); - renderDiagnostics(); - restoreRenderFocus(focusSnapshot); - } - - function captureRenderFocus() { - const activeElement = document.activeElement; - if (!(activeElement instanceof HTMLInputElement)) { - return null; - } - - const ariaLabel = activeElement.getAttribute('aria-label'); - if ( - ariaLabel !== 'Branch name' && - ariaLabel !== 'Working directory' && - ariaLabel !== 'Custom Pi config directory' - ) { - return null; - } - - return { - ariaLabel, - selectionStart: - typeof activeElement.selectionStart === 'number' ? activeElement.selectionStart : null, - selectionEnd: - typeof activeElement.selectionEnd === 'number' ? activeElement.selectionEnd : null, - }; - } - - function restoreRenderFocus(snapshot) { - if (snapshot === null) { - return; - } - - const input = findInputByAriaLabel(elements.listShell, snapshot.ariaLabel); - if (input === null) { - return; - } - - input.focus?.(); - if ( - typeof input.setSelectionRange === 'function' && - typeof snapshot.selectionStart === 'number' && - typeof snapshot.selectionEnd === 'number' - ) { - const valueLength = input.value.length; - input.setSelectionRange( - Math.min(snapshot.selectionStart, valueLength), - Math.min(snapshot.selectionEnd, valueLength), - ); - } - } - - function findInputByAriaLabel(node, ariaLabel) { - if (node instanceof HTMLInputElement && node.getAttribute('aria-label') === ariaLabel) { - return node; - } - - for (const child of node.childNodes ?? []) { - const match = findInputByAriaLabel(child, ariaLabel); - if (match !== null) { - return match; - } - } - - return null; - } - - function renderSummary() { - if (state.loading && state.snapshot === null) { - elements.summary.textContent = 'Loading…'; - elements.liveCount.textContent = ''; - elements.refresh.setAttribute('title', 'Refresh sessions'); - return; - } - - const snapshot = state.snapshot ?? emptySnapshot('Snapshot unavailable.'); - const records = snapshot.records.filter((record) => !isTempSession(record)); - const counts = countPresenceStates(records); - const summaryLabels = []; - - elements.liveCount.textContent = `${counts.live} live`; - - if (counts.stale > 0) { - summaryLabels.push(`${counts.stale} stale`); - } - - if (state.showAll) { - summaryLabels.push(`${counts.dead} dead`, `${counts.unknown} unknown`); - } - - elements.summary.replaceChildren( - ...summaryLabels.map((label) => createText('span', label, 'summary-count')), - ); - elements.refresh.setAttribute('title', `updated ${formatTimestamp(snapshot.generatedAt)}`); - } - - function renderBanner() { - const snapshot = state.snapshot; - const bannerMessage = - state.fetchError ?? - getSnapshotFailureMessage(snapshot?.diagnostics ?? [], snapshot?.records.length ?? 0); - - if (!bannerMessage) { - elements.banner.classList.add('hidden'); - elements.banner.textContent = ''; - return; - } - - elements.banner.textContent = bannerMessage; - elements.banner.classList.remove('hidden'); - } - - function renderList() { - const visibleRecords = getVisibleRecords(); - const repoGroups = createRepoGroups(visibleRecords); - reconcileExpandedRepoKeys(repoGroups); - elements.list.replaceChildren(); - elements.empty.textContent = state.showAll - ? 'No session records found.' - : 'No live or stale Pi sessions found.'; - elements.empty.classList.toggle('hidden', visibleRecords.length > 0); - - for (const repoGroup of repoGroups) { - elements.list.append(createRepoGroup(repoGroup)); - } - } - - function renderNewSessionAction() { - const isOpen = state.activeWorktreeFormRepoKey === NEW_SESSION_OWNER_KEY; - const cwdRequestPending = isCwdSessionRequestPending(); - const trigger = document.createElement('button'); - trigger.type = 'button'; - trigger.className = 'new-session-button'; - trigger.textContent = isOpen ? 'Cancel' : '+ New session'; - trigger.disabled = cwdRequestPending; - trigger.setAttribute('aria-label', isOpen ? 'cancel new session' : 'create new session'); - trigger.addEventListener('click', (event) => { - event.preventDefault?.(); - - if (isOpen) { - closeWorktreeForm(NEW_SESSION_OWNER_KEY); - return; - } - - if (cwdRequestPending) { - return; - } - - state.activeWorktreeFormRepoKey = NEW_SESSION_OWNER_KEY; - requestNoRepoSessionLaunchPreview(NEW_SESSION_OWNER_KEY); - render(); - }); - - const children = [trigger]; - if (isOpen) { - children.push(createNoRepoSessionForm(NEW_SESSION_OWNER_KEY)); - } - - const pending = state.pendingWorktrees.get(NEW_SESSION_OWNER_KEY); - if (pending) { - children.push(createPendingWorktreeCard(NEW_SESSION_OWNER_KEY, pending)); - } - - elements.newSession.replaceChildren(...children); - } - - function createRepoGroup(repoGroup) { - const isExpanded = state.expandedRepoKeys.has(repoGroup.key); - const section = document.createElement('section'); - section.className = 'repo-group'; - section.setAttribute('role', 'listitem'); - - const header = document.createElement('button'); - header.type = 'button'; - header.className = 'repo-header'; - header.setAttribute('aria-expanded', String(isExpanded)); - header.setAttribute('aria-label', formatRepoHeader(repoGroup)); - header.append(createRepoHeaderLabel(repoGroup)); - header.addEventListener('click', () => { - if (isExpanded) { - state.expandedRepoKeys.delete(repoGroup.key); - } else { - state.expandedRepoKeys.add(repoGroup.key); - } - render(); - }); - - const headerRow = document.createElement('div'); - headerRow.className = 'repo-header-row'; - headerRow.append(header); - - headerRow.append(createRepoActionButton(repoGroup)); - - section.append(headerRow); - - if (state.activeWorktreeFormRepoKey === repoGroup.key) { - section.append( - repoGroup.kind === 'no-repo' - ? createNoRepoSessionForm(repoGroup.key) - : createWorktreeForm(repoGroup), - ); - } - - if (isExpanded) { - const records = document.createElement('div'); - const recordsId = getRepoGroupRecordsId(repoGroup.key); - records.className = 'repo-group-records'; - records.setAttribute('id', recordsId); - records.setAttribute('role', 'list'); - records.setAttribute('aria-label', `${repoGroup.label} sessions`); - header.setAttribute('aria-controls', recordsId); - - const pending = state.pendingWorktrees.get(repoGroup.key); - if (pending) { - records.append(createPendingWorktreeCard(repoGroup.key, pending)); - } - - for (const record of repoGroup.records) { - records.append(createRecordCard(record)); - } - section.append(records); - } - - return section; - } - - function createRepoActionButton(repoGroup) { - const isOpen = state.activeWorktreeFormRepoKey === repoGroup.key; - const label = isOpen ? 'cancel new session' : 'create new session'; - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'repo-action-button'; - button.textContent = isOpen ? 'Cancel' : '+ New'; - button.setAttribute('aria-label', label); - button.setAttribute('title', label); - button.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - - if (isOpen) { - closeWorktreeForm(repoGroup.key); - return; - } - - if (state.pendingWorktrees.get(repoGroup.key)?.kind === 'pending') { - return; - } - - state.activeWorktreeFormRepoKey = repoGroup.key; - if (repoGroup.kind === 'no-repo') { - requestNoRepoSessionLaunchPreview(repoGroup.key); - render(); - return; - } - - if (!state.worktreeForms.has(repoGroup.key)) { - state.worktreeForms.set(repoGroup.key, createInitialWorktreeFormState()); - } - requestWorktreeBasePreview(repoGroup); - requestWorktreeLaunchPreview(repoGroup); - render(); - }); - return button; - } - - function closeWorktreeForm(repoKey) { - clearWorktreeFormState(repoKey); - render(); - } - - function clearWorktreeFormState(repoKey) { - state.worktreeForms.delete(repoKey); - state.noRepoSessionForms.delete(repoKey); - state.worktreeBasePreviews.delete(repoKey); - state.worktreeLaunchPreviews.delete(repoKey); - if (state.activeWorktreeFormRepoKey === repoKey) { - state.activeWorktreeFormRepoKey = null; - } - } - - function createWorktreeForm(repoGroup) { - const formState = getWorktreeFormState(repoGroup.key); - const preview = state.worktreeBasePreviews.get(repoGroup.key); - const launchPreview = state.worktreeLaunchPreviews.get(repoGroup.key); - const inlineMessage = getWorktreeFormInlineMessage(formState, preview, launchPreview); - const form = document.createElement('form'); - form.className = 'worktree-form'; - form.addEventListener('submit', (event) => { - event.preventDefault?.(); - submitWorktreeForm(repoGroup, formState); - }); - form.addEventListener('keydown', (event) => { - if (event.key !== 'Escape') { - return; - } - event.preventDefault?.(); - event.stopPropagation?.(); - if (formState.configDrawerOpen) { - formState.configDrawerOpen = false; - render(); - return; - } - closeWorktreeForm(repoGroup.key); - }); - - const labelInput = document.createElement('input'); - labelInput.type = 'text'; - labelInput.value = formState.branchName; - labelInput.setAttribute('aria-label', 'Branch name'); - labelInput.setAttribute('placeholder', 'feat/feature-name'); - labelInput.addEventListener('input', () => { - formState.branchName = labelInput.value; - }); - - const fieldMeta = createText( - 'span', - formatWorktreeBasePreviewCopy(preview), - 'worktree-field-meta', - ); - fieldMeta.setAttribute('data-state', preview?.status ?? 'loading'); - - const submit = document.createElement('button'); - submit.type = 'submit'; - submit.className = 'worktree-submit-button'; - submit.textContent = 'Create'; - submit.disabled = - !isResolvedWorktreeBasePreview(preview) || - !isWorktreeLaunchSubmitReady(formState, launchPreview) || - state.pendingWorktrees.has(repoGroup.key); - - const branchControl = document.createElement('div'); - branchControl.className = 'worktree-branch-control'; - branchControl.append(labelInput, submit); - - const composeRow = document.createElement('div'); - composeRow.className = 'worktree-compose-row'; - composeRow.append(fieldMeta, branchControl); - - form.append(composeRow, createWorktreeConfigRow(formState, launchPreview)); - if (formState.configDrawerOpen) { - form.append( - createWorktreeConfigDrawer(formState, () => requestWorktreeLaunchPreview(repoGroup)), - ); - } - if (inlineMessage) { - form.append(createText('div', inlineMessage, 'worktree-form-feedback worktree-form-error')); - } - return form; - } - - function createInitialWorktreeFormState() { - return { - branchName: '', - errorMessage: null, - agentDirSelection: { mode: 'ambient' }, - customDraft: '', - configDrawerOpen: false, - }; - } - - function getWorktreeFormState(repoKey) { - const existing = state.worktreeForms.get(repoKey); - if (existing) { - return existing; - } - const created = createInitialWorktreeFormState(); - state.worktreeForms.set(repoKey, created); - return created; - } - - function createNoRepoSessionForm(ownerKey) { - const formState = getNoRepoSessionFormState(ownerKey); - const launchPreview = state.worktreeLaunchPreviews.get(ownerKey); - const inlineMessage = getNoRepoSessionFormInlineMessage(formState, launchPreview); - const form = document.createElement('form'); - form.className = 'worktree-form'; - form.addEventListener('submit', (event) => { - event.preventDefault?.(); - submitNoRepoSessionForm(ownerKey, formState); - }); - form.addEventListener('keydown', (event) => { - if (event.key !== 'Escape') { - return; - } - event.preventDefault?.(); - event.stopPropagation?.(); - if (formState.configDrawerOpen) { - formState.configDrawerOpen = false; - render(); - return; - } - closeWorktreeForm(ownerKey); - }); - - const cwdInput = document.createElement('input'); - cwdInput.type = 'text'; - cwdInput.value = formState.cwd; - cwdInput.setAttribute('aria-label', 'Working directory'); - cwdInput.setAttribute('placeholder', '~/scratch'); - - const submit = document.createElement('button'); - submit.type = 'submit'; - submit.className = 'worktree-submit-button'; - submit.textContent = 'Create'; - submit.disabled = !isNoRepoSessionSubmitReady(formState, launchPreview); - - cwdInput.addEventListener('input', () => { - formState.cwd = cwdInput.value; - submit.disabled = !isNoRepoSessionSubmitReady(formState, launchPreview); - }); - - const fieldMeta = createText('span', 'cwd →', 'worktree-field-meta'); - fieldMeta.setAttribute('data-state', 'resolved'); - - const cwdControl = document.createElement('div'); - cwdControl.className = 'worktree-branch-control'; - cwdControl.append(cwdInput, submit); - - const composeRow = document.createElement('div'); - composeRow.className = 'worktree-compose-row'; - composeRow.append(fieldMeta, cwdControl); - - form.append(composeRow, createWorktreeConfigRow(formState, launchPreview)); - if (formState.configDrawerOpen) { - form.append( - createWorktreeConfigDrawer(formState, () => requestNoRepoSessionLaunchPreview(ownerKey)), - ); - } - if (inlineMessage) { - form.append(createText('div', inlineMessage, 'worktree-form-feedback worktree-form-error')); - } - return form; - } - - function createInitialNoRepoSessionFormState() { - return { - cwd: '~', - errorMessage: null, - agentDirSelection: { mode: 'ambient' }, - customDraft: '', - configDrawerOpen: false, - }; - } - - function getNoRepoSessionFormState(repoKey) { - const existing = state.noRepoSessionForms.get(repoKey); - if (existing) { - return existing; - } - const created = createInitialNoRepoSessionFormState(); - state.noRepoSessionForms.set(repoKey, created); - return created; - } - - function isNoRepoSessionSubmitReady(formState, launchPreview) { - return ( - formState.cwd.trim().length > 0 && - !isCwdSessionRequestPending() && - isWorktreeLaunchSubmitReady(formState, launchPreview) - ); - } - - function isCwdSessionRequestPending() { - return [...state.pendingWorktrees.values()].some( - (pending) => pending.sessionKind === 'cwd' && pending.kind === 'pending', - ); - } - - function getNoRepoSessionFormInlineMessage(formState, launchPreview) { - if (isNonEmptyString(formState.errorMessage)) { - return formState.errorMessage; - } - if (launchPreview?.status === 'failed' && isNonEmptyString(launchPreview.message)) { - return launchPreview.message; - } - return null; - } - - function createWorktreeConfigRow(formState, launchPreview) { - const row = document.createElement('div'); - row.className = 'worktree-config-row'; - row.setAttribute('aria-label', 'Pi config'); - - const summary = createText( - 'span', - formatLaunchContextPreviewSummary(launchPreview), - 'worktree-field-meta worktree-config-summary', - ); - summary.setAttribute('data-state', launchPreview?.status ?? 'loading'); - - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'worktree-config-button'; - button.textContent = 'Change'; - button.setAttribute('aria-label', 'Change Pi config'); - button.setAttribute('aria-expanded', String(formState.configDrawerOpen)); - button.addEventListener('click', (event) => { - event.preventDefault?.(); - formState.configDrawerOpen = !formState.configDrawerOpen; - render(); - }); - - row.append(summary, button); - return row; - } - - function createWorktreeConfigDrawer(formState, requestLaunchPreview) { - const drawer = document.createElement('div'); - drawer.className = 'worktree-config-drawer'; - drawer.setAttribute('role', 'radiogroup'); - drawer.setAttribute('aria-label', 'Pi config'); - - for (const mode of LAUNCH_AGENT_DIR_MODE_OPTIONS) { - const option = document.createElement('button'); - option.type = 'button'; - option.className = 'worktree-config-option'; - option.setAttribute('role', 'radio'); - option.setAttribute('aria-checked', String(formState.agentDirSelection.mode === mode)); - option.textContent = formatLaunchAgentDirOptionLabel(mode); - option.addEventListener('click', (event) => { - event.preventDefault?.(); - formState.agentDirSelection = - mode === 'custom' ? { mode, customDir: formState.customDraft } : { mode }; - requestLaunchPreview(); - render(); - }); - drawer.append(option); - } - - const customInput = document.createElement('input'); - customInput.type = 'text'; - customInput.value = formState.customDraft; - customInput.setAttribute('aria-label', 'Custom Pi config directory'); - customInput.setAttribute('placeholder', '~/.pi/agent-work'); - customInput.addEventListener('input', () => { - formState.customDraft = customInput.value; - if (formState.agentDirSelection.mode === 'custom') { - formState.agentDirSelection = { mode: 'custom', customDir: formState.customDraft }; - requestLaunchPreview(); - } - render(); - }); - drawer.append(customInput); - return drawer; - } - - function getWorktreeFormInlineMessage(formState, basePreview, launchPreview) { - if (isNonEmptyString(formState.errorMessage)) { - return formState.errorMessage; - } - if (basePreview?.status === 'failed' && isNonEmptyString(basePreview.message)) { - return basePreview.message; - } - if (launchPreview?.status === 'failed' && isNonEmptyString(launchPreview.message)) { - return launchPreview.message; - } - return null; - } - - function createPendingWorktreeCard(repoKey, pending) { - const card = document.createElement('article'); - card.className = `pending-worktree ${pending.tone}`; - - const header = document.createElement('div'); - header.className = 'pending-worktree-header'; - header.append(createText('div', pending.title, 'pending-worktree-title')); - if (isPendingWorktreeDismissible(pending)) { - header.append(createPendingWorktreeDismissButton(repoKey, pending)); - } - - card.append(header, createText('div', pending.message, 'pending-worktree-message')); - - if (Array.isArray(pending.actions) && pending.actions.length > 0) { - const actions = document.createElement('div'); - actions.className = 'pending-worktree-actions'; - for (const action of pending.actions) { - actions.append(createPendingWorktreeActionButton(action)); - } - card.append(actions); - } - - return card; - } - - function isPendingWorktreeDismissible(pending) { - return pending.kind !== 'pending'; - } - - function createPendingWorktreeDismissButton(repoKey, pending) { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'pending-worktree-dismiss'; - button.textContent = '×'; - button.setAttribute('aria-label', `Dismiss ${pending.title}`); - button.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - clearPendingWorktree(repoKey); - render(); - }); - return button; - } - - function createPendingWorktreeActionButton(action) { - const button = document.createElement('button'); - button.type = 'button'; - button.className = action.kind - ? `pending-worktree-action ${action.kind}` - : 'pending-worktree-action'; - button.textContent = action.label; - button.disabled = action.disabled === true || action.isDisabled?.() === true; - if (isNonEmptyString(action.title)) { - button.setAttribute('title', action.title); - } - button.addEventListener('click', (event) => { - event.preventDefault?.(); - action.onClick?.(); - }); - return button; - } - - function requestWorktreeBasePreview(repoGroup) { - const requestId = state.nextWorktreeBasePreviewRequestId + 1; - state.nextWorktreeBasePreviewRequestId = requestId; - state.worktreeBasePreviews.set(repoGroup.key, { status: 'loading', requestId }); - - void postWorktreeBasePreview(repoGroup) - .then((result) => { - const activePreview = state.worktreeBasePreviews.get(repoGroup.key); - if (activePreview?.requestId !== requestId) { - return; - } - - if ( - result?.ok && - typeof result.baseRef === 'string' && - result.baseRef.trim().length > 0 - ) { - state.worktreeBasePreviews.set(repoGroup.key, { - status: 'resolved', - baseRef: result.baseRef, - }); - } else { - state.worktreeBasePreviews.set(repoGroup.key, { - status: 'failed', - message: getWorktreeBasePreviewFailureMessage(result), - }); - } - render(); - }) - .catch(() => { - const activePreview = state.worktreeBasePreviews.get(repoGroup.key); - if (activePreview?.requestId !== requestId) { - return; - } - state.worktreeBasePreviews.set(repoGroup.key, { - status: 'failed', - message: 'Base unavailable.', - }); - render(); - }); - } - - function getWorktreeBasePreviewFailureMessage(result) { - return isNonEmptyString(result?.message) ? result.message : 'Base unavailable.'; - } - - function requestWorktreeLaunchPreview(repoGroup) { - requestLaunchPreviewForForm(repoGroup.key, getWorktreeFormState(repoGroup.key)); - } - - function requestNoRepoSessionLaunchPreview(ownerKey) { - requestLaunchPreviewForForm(ownerKey, getNoRepoSessionFormState(ownerKey)); - } - - function requestLaunchPreviewForForm(repoKey, formState) { - const requestId = state.nextWorktreeLaunchPreviewRequestId + 1; - state.nextWorktreeLaunchPreviewRequestId = requestId; - state.worktreeLaunchPreviews.set(repoKey, { status: 'loading', requestId }); - - void postWorktreeLaunchPreview(formState) - .then((result) => { - const activePreview = state.worktreeLaunchPreviews.get(repoKey); - if (activePreview?.requestId !== requestId) { - return; - } - - if (result?.ok && result.status === 'resolved') { - state.worktreeLaunchPreviews.set(repoKey, result); - } else { - state.worktreeLaunchPreviews.set(repoKey, { - status: 'failed', - message: isNonEmptyString(result?.message) - ? result.message - : 'Pi config unavailable.', - }); - } - render(); - }) - .catch((error) => { - const activePreview = state.worktreeLaunchPreviews.get(repoKey); - if (activePreview?.requestId !== requestId) { - return; - } - state.worktreeLaunchPreviews.set(repoKey, { - status: 'failed', - message: getErrorMessage(error) || 'Pi config unavailable.', - }); - render(); - }); - } - - function formatWorktreeBasePreviewCopy(preview) { - if (isResolvedWorktreeBasePreview(preview)) { - return `${formatBaseRefLabel(preview.baseRef)} →`; - } - if (preview?.status === 'failed') { - return 'Base unavailable'; - } - return 'Resolving…'; - } - - function isResolvedWorktreeBasePreview(preview) { - return ( - preview?.status === 'resolved' && - typeof preview.baseRef === 'string' && - preview.baseRef.trim().length > 0 - ); - } - - function formatBaseRefLabel(baseRef) { - const trimmed = typeof baseRef === 'string' ? baseRef.trim() : ''; - if (trimmed.length === 0 || trimmed === 'HEAD') { - return 'current HEAD'; - } - - const normalized = trimmed - .replace(/^refs\/remotes\//u, '') - .replace(/^refs\/heads\//u, '') - .replace(/^origin\//u, ''); - return normalized.length > 0 ? normalized : trimmed; - } - - function isWorktreeLaunchSubmitReady(formState, launchPreview) { - return ( - formState.agentDirSelection.mode !== 'custom' || - isResolvedWorktreeLaunchPreview(launchPreview) - ); - } - - function isResolvedWorktreeLaunchPreview(preview) { - return preview?.status === 'resolved' && preview.ok === true; - } - - function getWorktreeLaunchAgentDirIntent(formState) { - if (formState.agentDirSelection.mode === 'custom') { - return { mode: 'custom', customDir: formState.customDraft }; - } - return { mode: formState.agentDirSelection.mode }; - } - - function submitWorktreeForm(repoGroup, formState) { - const branchName = formState.branchName.trim(); - const preview = state.worktreeBasePreviews.get(repoGroup.key); - const launchPreview = state.worktreeLaunchPreviews.get(repoGroup.key); - if ( - branchName.length === 0 || - state.pendingWorktrees.has(repoGroup.key) || - !isResolvedWorktreeBasePreview(preview) || - !isWorktreeLaunchSubmitReady(formState, launchPreview) - ) { - return; - } - - const request = buildCreateWorktreeRequest( - repoGroup, - branchName, - preview.baseRef, - getWorktreeLaunchAgentDirIntent(formState), - ); - formState.branchName = branchName; - formState.errorMessage = null; - setPendingWorktree(repoGroup.key, { - sessionKind: 'worktree', - kind: 'pending', - title: 'New session', - message: 'Creating worktree…', - tone: 'pending', - }); - state.expandedRepoKeys.add(repoGroup.key); - state.activeWorktreeFormRepoKey = null; - render(); - - void postCreateWorktreeAction(request) - .then(async (result) => { - const inlineFailureMessage = getRecoverableInlineWorktreeFailureMessage(result); - if (inlineFailureMessage !== null) { - clearPendingWorktree(repoGroup.key); - state.worktreeForms.set(repoGroup.key, { - ...formState, - branchName, - errorMessage: inlineFailureMessage, - }); - state.activeWorktreeFormRepoKey = repoGroup.key; - render(); - return; - } - - clearWorktreeFormState(repoGroup.key); - applyWorktreeActionResult(repoGroup.key, request, result); - render(); - if (shouldRefreshAfterWorktreeActionResult(result)) { - await refreshSnapshot({ source: 'manual' }); - } - }) - .catch(async (error) => { - clearWorktreeFormState(repoGroup.key); - if (isOutcomeUnknownError(error)) { - setUnknownCreateAction(repoGroup.key, request, error); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setPendingWorktree(repoGroup.key, { - sessionKind: 'worktree', - kind: 'failure', - title: 'New session failed', - message: `Create worktree failed: ${getErrorMessage(error)}`, - tone: 'failed', - }); - render(); - }); - } - - function submitNoRepoSessionForm(ownerKey, formState) { - const cwd = formState.cwd.trim(); - const launchPreview = state.worktreeLaunchPreviews.get(ownerKey); - if (!isNoRepoSessionSubmitReady({ ...formState, cwd }, launchPreview)) { - return; - } - - formState.cwd = cwd; - formState.errorMessage = null; - const request = buildCreateSessionRequest(formState); - setPendingWorktree(ownerKey, { - sessionKind: 'cwd', - kind: 'pending', - title: 'New session', - message: 'Starting Pi session…', - tone: 'pending', - cwd, - }); - if (ownerKey === NO_REPO_GROUP_KEY) { - state.expandedRepoKeys.add(ownerKey); - } - state.activeWorktreeFormRepoKey = null; - render(); - - void postCreateSessionAction(request) - .then(async (result) => { - const inlineFailureMessage = getRecoverableInlineCreateSessionFailureMessage(result); - if (inlineFailureMessage !== null) { - clearPendingWorktree(ownerKey); - state.noRepoSessionForms.set(ownerKey, { - ...formState, - cwd, - errorMessage: inlineFailureMessage, - }); - state.activeWorktreeFormRepoKey = ownerKey; - render(); - if (shouldRefreshAfterCreateSessionActionResult(result)) { - await refreshSnapshot({ source: 'manual' }); - } - return; - } - - clearWorktreeFormState(ownerKey); - applyCreateSessionActionResult(ownerKey, request, result); - render(); - if (shouldRefreshAfterCreateSessionActionResult(result)) { - await refreshSnapshot({ source: 'manual' }); - } - }) - .catch(async (error) => { - clearWorktreeFormState(ownerKey); - if (isOutcomeUnknownError(error)) { - setUnknownCreateAction(ownerKey, request, error); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setPendingWorktree(ownerKey, { - sessionKind: 'cwd', - kind: 'failure', - title: 'New session failed', - message: `Create session failed: ${getErrorMessage(error)}`, - tone: 'failed', - }); - render(); - }); - } - - function buildWorktreeRepoIntent(repoGroup) { - return { - repoName: getRepoShortName(repoGroup.label), - qualifiedRepoName: repoGroup.kind === 'qualified' ? repoGroup.label : null, - candidateRuntimeIds: repoGroup.records.map((record) => record.runtimeId), - }; - } - - function buildCreateWorktreeRequest(repoGroup, branchName, baseRef, agentDir) { - return { - repoIntent: buildWorktreeRepoIntent(repoGroup), - branchName, - baseRef, - launch: { mode: 'tmux-detached', agentDir }, - }; - } - - function buildCreateSessionRequest(formState) { - return { - action: 'create-session', - cwd: formState.cwd.trim(), - launch: { - mode: 'tmux-detached', - agentDir: getWorktreeLaunchAgentDirIntent(formState), - }, - }; - } - - function postWorktreeBasePreview(repoGroup) { - return host.previewWorktreeBaseRef({ - repoIntent: buildWorktreeRepoIntent(repoGroup), - }); - } - - function postWorktreeLaunchPreview(formState) { - return host.previewWorktreeLaunchContext({ - launch: { - mode: 'tmux-detached', - agentDir: getWorktreeLaunchAgentDirIntent(formState), - }, - }); - } - - function postCreateWorktreeAction(request) { - return host.createWorktree(request); - } - - function postCreateSessionAction(request) { - return host.createSession(request); - } - - function postOpenTerminalAction(runtimeId) { - return host.openTerminal(runtimeId); - } - - function postKillSessionAction(runtimeId) { - return host.killSession(runtimeId); - } - - function getRecoverableInlineWorktreeFailureMessage(result) { - if (result?.ok === false && result?.status === 'preflight-failed') { - return getPreflightFailureMessage(result.preflight); - } - return result?.ok === false && - result?.status === 'failed' && - INLINE_WORKTREE_FAILURE_REASONS.has(result.worktree?.reason) - ? (result.worktree?.message ?? 'New session request is invalid.') - : null; - } - - function getRecoverableInlineCreateSessionFailureMessage(result) { - if (result?.ok === false && result?.status === 'preflight-failed') { - return getCreateSessionPreflightFailureMessage(result.preflight); - } - return result?.ok === false && result?.status === 'failed' - ? (result.message ?? 'Working directory is not valid.') - : null; - } - - function applyWorktreeActionResult(repoKey, request, result) { - setPendingWorktree(repoKey, summarizeWorktreeActionResult(repoKey, request, result)); - const runtimeId = getActionResultRuntimeId(result); - if (runtimeId) { - state.highlightedRuntimeId = runtimeId; - } - } - - function applyCreateSessionActionResult(repoKey, request, result) { - setPendingWorktree(repoKey, summarizeCreateSessionActionResult(repoKey, request, result)); - const runtimeId = getActionResultRuntimeId(result); - if (runtimeId) { - state.highlightedRuntimeId = runtimeId; - } - } - - function shouldRefreshAfterWorktreeActionResult(result) { - return result?.status !== 'preflight-failed'; - } - - function shouldRefreshAfterCreateSessionActionResult(result) { - return !(result?.ok === false && result?.status === 'failed'); - } - - function retryWorktreeAction(repoKey, request) { - setPendingWorktree(repoKey, { - sessionKind: 'worktree', - kind: 'pending', - title: 'Retrying launch', - message: 'Retrying Pi launch…', - tone: 'pending', - }); - state.expandedRepoKeys.add(repoKey); - render(); - - void postCreateWorktreeAction(request) - .then(async (result) => { - applyWorktreeActionResult(repoKey, request, result); - render(); - if (shouldRefreshAfterWorktreeActionResult(result)) { - await refreshSnapshot({ source: 'manual' }); - } - }) - .catch(async (error) => { - if (isOutcomeUnknownError(error)) { - setUnknownCreateAction(repoKey, request, error); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setPendingWorktree(repoKey, { - sessionKind: 'worktree', - kind: 'failure', - title: 'Retry failed', - message: `Create worktree failed: ${getErrorMessage(error)}`, - tone: 'failed', - }); - render(); - }); - } - - function retryCreateSessionAction(repoKey, request) { - if (isCwdSessionRequestPending()) { - return; - } - - setPendingWorktree(repoKey, { - sessionKind: 'cwd', - kind: 'pending', - title: 'Retrying launch', - message: 'Retrying Pi launch…', - tone: 'pending', - cwd: request.cwd, - }); - if (repoKey === NO_REPO_GROUP_KEY) { - state.expandedRepoKeys.add(repoKey); - } - render(); - - void postCreateSessionAction(request) - .then(async (result) => { - applyCreateSessionActionResult(repoKey, request, result); - render(); - if (shouldRefreshAfterCreateSessionActionResult(result)) { - await refreshSnapshot({ source: 'manual' }); - } - }) - .catch(async (error) => { - if (isOutcomeUnknownError(error)) { - setUnknownCreateAction(repoKey, request, error); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setPendingWorktree(repoKey, { - sessionKind: 'cwd', - kind: 'failure', - title: 'Retry failed', - message: `Create session failed: ${getErrorMessage(error)}`, - tone: 'failed', - }); - render(); - }); - } - - function isOutcomeUnknownError(error) { - return isObject(error) && error.outcomeUnknown === true; - } - - function setUnknownCreateAction(repoKey, request, error) { - const isCwdSession = request.action === 'create-session'; - setPendingWorktree(repoKey, { - sessionKind: isCwdSession ? 'cwd' : 'worktree', - kind: 'unknown', - title: 'Creation outcome unknown', - message: `${getErrorMessage(error)} Session Deck requested a session-list refresh; verify the matching session before trying again.`, - tone: 'partial', - request, - cwd: isCwdSession ? request.cwd : undefined, - }); - state.expandedRepoKeys.add(repoKey); - } - - function summarizeWorktreeActionResult(repoKey, request, result) { - if (result?.status === 'preflight-failed') { - return { - sessionKind: 'worktree', - kind: 'failure', - title: 'New session blocked', - message: getPreflightFailureMessage(result.preflight), - tone: 'failed', - }; - } - - if (result?.status === 'partial-launch-failed') { - return { - sessionKind: 'worktree', - kind: 'partial', - title: 'Worktree ready · Pi did not start', - message: getPartialLaunchFailureMessage(result.launch), - tone: 'partial', - actions: buildPartialLaunchActions(repoKey, request), - }; - } - - if (!result?.ok) { - return { - sessionKind: 'worktree', - kind: 'failure', - title: 'New session failed', - message: result.worktree?.message ?? 'New session failed.', - tone: 'failed', - }; - } - - if (!result.launch?.requested) { - return { - sessionKind: 'worktree', - kind: 'success', - title: 'Worktree ready', - message: 'Worktree created.', - tone: 'ready', - request, - autoDismissAfterMs: SUCCESS_PENDING_WORKTREE_TTL_MS, - }; - } - - return { - sessionKind: 'worktree', - kind: 'success', - title: - result.status === 'reused-and-launched' || result.launch.status === 'reused-existing' - ? 'Session reused' - : 'Session launched', - message: - result.status === 'reused-and-launched' || result.launch.status === 'reused-existing' - ? 'Reused the managed Pi session. Session Deck will pick it up automatically.' - : 'Pi session launched. Session Deck will pick it up automatically.', - tone: 'ready', - request, - runtimeId: getActionResultRuntimeId(result), - autoDismissAfterMs: SUCCESS_PENDING_WORKTREE_TTL_MS, - }; - } - - function summarizeCreateSessionActionResult(repoKey, request, result) { - if (result?.status === 'launch-failed') { - return { - sessionKind: 'cwd', - kind: 'failure', - title: 'New session failed', - message: getCreateSessionLaunchFailureMessage(result.launch), - tone: 'failed', - request, - cwd: request.cwd, - resultCwd: result.cwd, - actions: buildCreateSessionRetryActions(repoKey, request), - }; - } - - if (!result?.ok) { - return { - sessionKind: 'cwd', - kind: 'failure', - title: 'New session failed', - message: result?.message ?? 'New session failed.', - tone: 'failed', - request, - cwd: request.cwd, - resultCwd: result?.cwd, - }; - } - - const reused = - result.status === 'reused-existing' || result.launch?.status === 'reused-existing'; - return { - sessionKind: 'cwd', - kind: 'success', - title: reused ? 'Session reused' : 'Session launched', - message: reused - ? 'Reused the managed Pi session. Session Deck will pick it up automatically.' - : 'Pi session launched. Session Deck will pick it up automatically.', - tone: 'ready', - request, - cwd: request.cwd, - resultCwd: result.cwd, - runtimeId: getActionResultRuntimeId(result), - autoDismissAfterMs: SUCCESS_PENDING_WORKTREE_TTL_MS, - }; - } - - function getActionResultRuntimeId(result) { - return result?.launch?.ok && isNonEmptyString(result.launch.runtimeId) - ? result.launch.runtimeId - : null; - } - - function getPreflightFailureMessage(preflight) { - switch (preflight?.reason) { - case 'tmux-unavailable': - return `New Pi session requires tmux on PATH; no worktree was created. Run ${doctorCommand} or install tmux.`; - case 'pi-command-unavailable': - return `New Pi session requires the pi executable on PATH; no worktree was created. Run ${doctorCommand} or install Pi.`; - default: - return isNonEmptyString(preflight?.message) - ? preflight.message - : `New Pi session prerequisites are unavailable; no worktree was created. Run ${doctorCommand}.`; - } - } - - function getCreateSessionPreflightFailureMessage(preflight) { - switch (preflight?.reason) { - case 'tmux-unavailable': - return `New Pi session requires tmux on PATH; no session was launched. Run ${doctorCommand} or install tmux.`; - case 'pi-command-unavailable': - return `New Pi session requires the pi executable on PATH; no session was launched. Run ${doctorCommand} or install Pi.`; - default: - return isNonEmptyString(preflight?.message) - ? preflight.message - : `New Pi session prerequisites are unavailable; no session was launched. Run ${doctorCommand}.`; - } - } - - function getPartialLaunchFailureMessage(launch) { - switch (launch?.reason) { - case 'tmux-unavailable': - return `Worktree kept. Pi did not start because tmux is not available. Run ${doctorCommand} or install tmux, then retry.`; - case 'pi-command-unavailable': - return `Worktree kept. Pi did not start because the pi executable is not available. Run ${doctorCommand} or install Pi, then retry.`; - case 'tmux-name-collision': - return 'Worktree kept. Pi did not start because the generated tmux session name is already in use. Retry after resolving the collision.'; - case 'launch-context-mismatch': - return 'Worktree kept. Pi did not start because an existing managed tmux session may use a different Pi config. Attach to it or choose Current.'; - case 'presence-timeout': - return 'Worktree kept. Session Deck could not confirm the Pi launch. Retry after confirming the session can start.'; - case 'spawn-failed': - return 'Worktree kept. Pi did not start. Retry after fixing the launch issue.'; - default: - return isNonEmptyString(launch?.message) - ? `${launch.message} Retry after fixing the launch issue.` - : 'Worktree kept. Pi did not start. Retry after fixing the launch issue.'; - } - } - - function getCreateSessionLaunchFailureMessage(launch) { - switch (launch?.reason) { - case 'tmux-name-collision': - return 'Pi did not start because the generated tmux session name is already in use for a different cwd.'; - case 'launch-context-mismatch': - return 'Existing managed tmux session cannot be verified against the requested Pi config.'; - case 'presence-timeout': - return 'Pi did not remain running in tmux. Retry after confirming the session can start.'; - case 'cwd-mismatch': - return 'The launched tmux pane is not in the requested cwd. Retry after fixing the launch issue.'; - case 'spawn-failed': - return 'tmux could not start Pi. Retry after fixing the launch issue.'; - default: - return isNonEmptyString(launch?.message) - ? `${launch.message} Retry after fixing the launch issue.` - : 'Pi did not start. Retry after fixing the launch issue.'; - } - } - - function buildPartialLaunchActions(repoKey, request) { - return [ - { - label: 'Retry', - kind: 'primary', - onClick: () => { - retryWorktreeAction(repoKey, request); - }, - }, - ]; - } - - function buildCreateSessionRetryActions(repoKey, request) { - return [ - { - label: 'Retry', - kind: 'primary', - isDisabled: isCwdSessionRequestPending, - onClick: () => { - retryCreateSessionAction(repoKey, request); - }, - }, - ]; - } - - function setPendingWorktree(repoKey, pending) { - clearPendingWorktree(repoKey); - const nextPending = { ...pending }; - if ( - typeof nextPending.autoDismissAfterMs === 'number' && - typeof window.setTimeout === 'function' - ) { - nextPending.timeoutId = window.setTimeout(() => { - if (state.pendingWorktrees.get(repoKey) === nextPending) { - state.pendingWorktrees.delete(repoKey); - render(); - } - }, nextPending.autoDismissAfterMs); - } - state.pendingWorktrees.set(repoKey, nextPending); - } - - function clearPendingWorktree(repoKey) { - const pending = state.pendingWorktrees.get(repoKey); - if (pending && pending.timeoutId !== undefined && typeof window.clearTimeout === 'function') { - window.clearTimeout(pending.timeoutId); - } - state.pendingWorktrees.delete(repoKey); - } - - function setOpenTerminalAction(action) { - clearOpenTerminalAction(); - const nextAction = { ...action }; - if (nextAction.kind === 'success' && typeof window.setTimeout === 'function') { - nextAction.timeoutId = window.setTimeout(() => { - if (state.openTerminalAction === nextAction) { - state.openTerminalAction = null; - render(); - } - }, OPEN_TERMINAL_SUCCESS_TTL_MS); - } - state.openTerminalAction = nextAction; - return nextAction; - } - - function clearOpenTerminalAction() { - const action = state.openTerminalAction; - if (action && action.timeoutId !== undefined && typeof window.clearTimeout === 'function') { - window.clearTimeout(action.timeoutId); - } - state.openTerminalAction = null; - } - - function getOpenTerminalActionForRecord(record) { - return state.openTerminalAction?.runtimeId === record.runtimeId - ? state.openTerminalAction - : null; - } - - function openRecordTerminal(record) { - if (state.openTerminalAction?.kind === 'pending') { - return; - } - if (state.openTerminalAction?.kind === 'unknown') { - clearOpenTerminalAction(); - render(); - return; - } - - const pendingAction = setOpenTerminalAction({ kind: 'pending', runtimeId: record.runtimeId }); - render(); - - void postOpenTerminalAction(record.runtimeId) - .then((result) => { - if (state.openTerminalAction !== pendingAction) { - return; - } - - if (result?.ok === true) { - setOpenTerminalAction({ - kind: 'success', - runtimeId: record.runtimeId, - message: isNonEmptyString(result.message) - ? result.message - : 'Terminal open requested.', - }); - } else { - setOpenTerminalAction({ - kind: 'failure', - runtimeId: record.runtimeId, - message: getOpenTerminalActionFailureMessage(result), - }); - } - render(); - }) - .catch(async (error) => { - if (state.openTerminalAction !== pendingAction) { - return; - } - - if (isOutcomeUnknownError(error)) { - setOpenTerminalAction({ - kind: 'unknown', - runtimeId: record.runtimeId, - message: `${getErrorMessage(error)} Session Deck requested a session-list refresh; select to dismiss this status.`, - }); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setOpenTerminalAction({ - kind: 'failure', - runtimeId: record.runtimeId, - message: - error instanceof Error && isNonEmptyString(error.message) - ? error.message - : DEFAULT_OPEN_TERMINAL_FAILURE_MESSAGE, - }); - render(); - }); - } - - function getOpenTerminalActionFailureMessage(result) { - return isNonEmptyString(result?.message) - ? result.message - : DEFAULT_OPEN_TERMINAL_FAILURE_MESSAGE; - } - - function setKillSessionAction(action) { - clearKillSessionAction(); - const nextAction = { ...action }; - if (nextAction.kind === 'success' && typeof window.setTimeout === 'function') { - nextAction.timeoutId = window.setTimeout(() => { - if (state.killSessionAction === nextAction) { - state.killSessionAction = null; - render(); - } - }, KILL_SESSION_SUCCESS_TTL_MS); - } - state.killSessionAction = nextAction; - return nextAction; - } - - function clearKillSessionAction() { - const action = state.killSessionAction; - if (action && action.timeoutId !== undefined && typeof window.clearTimeout === 'function') { - window.clearTimeout(action.timeoutId); - } - state.killSessionAction = null; - } - - function clearKillSessionConfirmation() { - if (state.killSessionAction?.kind !== 'confirming') { - return false; - } - clearKillSessionAction(); - return true; - } - - function getKillSessionActionForRecord(record) { - return state.killSessionAction?.runtimeId === record.runtimeId - ? state.killSessionAction - : null; - } - - function openKillSessionConfirmation(record) { - if (state.killSessionAction?.kind === 'pending') { - return; - } - - setKillSessionAction({ kind: 'confirming', runtimeId: record.runtimeId }); - render(); - } - - function confirmKillSession(record) { - if (state.killSessionAction?.kind === 'pending') { - return; - } - - const pendingAction = setKillSessionAction({ kind: 'pending', runtimeId: record.runtimeId }); - render(); - - void postKillSessionAction(record.runtimeId) - .then((result) => { - if (state.killSessionAction !== pendingAction) { - return; - } - - if (result?.ok === true) { - setKillSessionAction({ - kind: 'success', - runtimeId: record.runtimeId, - message: getKillSessionActionSuccessMessage(result), - }); - void refreshSnapshot({ source: 'manual' }); - } else { - setKillSessionAction({ - kind: 'failure', - runtimeId: record.runtimeId, - message: getKillSessionActionFailureMessage(result), - }); - } - render(); - }) - .catch(async (error) => { - if (state.killSessionAction !== pendingAction) { - return; - } - - if (isOutcomeUnknownError(error)) { - setKillSessionAction({ - kind: 'unknown', - runtimeId: record.runtimeId, - message: `${getErrorMessage(error)} Session Deck requested a session-list refresh; verify whether the session is still running before trying again.`, - }); - render(); - await refreshSnapshot({ source: 'manual' }); - return; - } - - setKillSessionAction({ - kind: 'failure', - runtimeId: record.runtimeId, - message: - error instanceof Error && isNonEmptyString(error.message) - ? error.message - : DEFAULT_KILL_SESSION_FAILURE_MESSAGE, - }); - render(); - }); - } - - function getKillSessionActionSuccessMessage(result) { - return result?.status === 'already-exited' - ? 'This Pi session is no longer running.' - : 'End requested for this session.'; - } - - function getKillSessionActionFailureMessage(result) { - return KILL_SESSION_FAILURE_MESSAGES[result?.reason] ?? DEFAULT_KILL_SESSION_FAILURE_MESSAGE; - } - - function clearRestartSessionConfirmation() { - if (state.restartSessionAction?.kind !== 'confirming') return false; - state.restartSessionAction = null; - return true; - } - - function openRestartSessionConfirmation(record) { - if (state.restartSessionAction?.kind === 'pending') return; - if (record.restart?.available !== true) return; - clearKillSessionConfirmation(); - const previous = - state.restartSessionAction?.runtimeId === record.runtimeId - ? state.restartSessionAction - : null; - const recordedOperationId = record.restart.operation?.operationId; - const continuePrevious = - previous !== null && - (previous.kind === 'unknown' || - previous.status === 'stopped-not-restarted' || - previous.operationId === recordedOperationId); - state.restartSessionAction = { - kind: 'confirming', - runtimeId: record.runtimeId, - generation: continuePrevious ? previous.generation : record.restart.generation, - operationId: - recordedOperationId ?? (continuePrevious ? previous.operationId : createOperationId()), - }; - render(); - } - - function confirmRestartSession(record) { - const action = state.restartSessionAction; - if (action?.kind !== 'confirming' || action.runtimeId !== record.runtimeId) return; - const pending = { ...action, kind: 'pending' }; - state.restartSessionAction = pending; - render(); - void host - .restartSession({ - runtimeId: pending.runtimeId, - generation: pending.generation, - operationId: pending.operationId, - }) - .then((response) => { - if (state.restartSessionAction !== pending) return; - state.restartSessionAction = { - ...pending, - kind: - response?.status === 'restarted' - ? 'success' - : response?.status === 'outcome-unknown' - ? 'unknown' - : 'failure', - status: response?.status, - message: isNonEmptyString(response?.message) - ? response.message - : DEFAULT_RESTART_SESSION_FAILURE_MESSAGE, - }; - render(); - void refreshSnapshot({ source: 'manual' }); - }) - .catch(async (error) => { - if (state.restartSessionAction !== pending) return; - state.restartSessionAction = { - ...pending, - kind: isOutcomeUnknownError(error) ? 'unknown' : 'failure', - message: getErrorMessage(error), - }; - render(); - await refreshSnapshot({ source: 'manual' }); - }); - } - - function createOperationId() { - if (typeof window.crypto?.randomUUID === 'function') return window.crypto.randomUUID(); - return `restart-${Date.now()}-${Math.random().toString(16).slice(2)}`; - } - - function getRestartUnavailableMessage(restart) { - if (restart?.available !== false) return 'Restart this session'; - switch (restart.reason) { - case 'recipe-not-bound': - return 'Restart will become available after Session Deck verifies this managed session.'; - case 'hosting-runtime': - return 'Restarting the Session Deck TUI hosting runtime is unavailable.'; - case 'unsafe-descendants': - return 'Restart is unavailable while Pi owns child processes.'; - case 'generation-changed': - return 'The session generation changed; refresh before restarting.'; - default: - return 'Restart is available only for new Session Deck-managed tmux sessions.'; - } - } - - function createRecordCard(record) { - const isExpanded = state.detailVisible && record.runtimeId === state.selectedRuntimeId; - const title = getDisplayTitle(record); - const card = document.createElement('article'); - card.className = `card ${record.presenceState}`; - card.classList.toggle('expanded', isExpanded); - card.classList.toggle('highlighted', state.highlightedRuntimeId === record.runtimeId); - card.setAttribute('role', 'listitem'); - - const toggle = document.createElement('button'); - toggle.type = 'button'; - toggle.className = 'card-toggle'; - toggle.setAttribute('aria-expanded', String(isExpanded)); - toggle.addEventListener('click', () => { - clearKillSessionConfirmation(); - clearRestartSessionConfirmation(); - if (isExpanded) { - state.detailVisible = false; - } else { - state.selectedRuntimeId = record.runtimeId; - state.detailVisible = true; - } - render(); - }); - - toggle.append( - createLine('row-line1', [ - createActivityIcon(record), - createText('span', title.text, 'row-title'), - createText('span', getRowActivityLabel(record), 'row-activity'), - createText('span', formatDuration(getListAgeMs(record)), 'muted row-age'), - ]), - createLine( - 'row-line2', - [ - getRepoLabel(record, title.source), - formatPr(record.prUrl), - record.branch, - formatChildRuntimeLabel(record), - ] - .filter((value) => typeof value === 'string' && value.length > 0) - .map((value) => createText('span', value, 'muted')), - ), - ); - - if (record.chips.length > 0 && !isExpanded) { - const chips = document.createElement('div'); - chips.className = 'chips chips-inline'; - for (const chip of record.chips.slice(0, COLLAPSED_CHIP_LIMIT)) { - chips.append(createChip(chip)); - } - const hiddenChipCount = record.chips.length - COLLAPSED_CHIP_LIMIT; - if (hiddenChipCount > 0) { - chips.append(createChip(`+${hiddenChipCount}`, 'chip chip-subtle')); - } - toggle.append(chips); - } - - card.append(toggle, createRecordOpenButton(record, title.text)); - if (isExpanded) { - card.append(createRecordDetail(record)); - } - - return card; - } - - function createRecordOpenButton(record, title) { - const action = getOpenTerminalActionForRecord(record); - const isOpenPending = state.openTerminalAction?.kind === 'pending'; - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'card-open'; - button.disabled = isOpenPending; - button.textContent = getOpenTerminalButtonText(action); - const label = getOpenTerminalButtonLabel(title, action); - button.setAttribute('aria-label', label); - button.setAttribute('title', label); - if (action !== null) { - button.setAttribute('data-state', action.kind); - } - button.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - openRecordTerminal(record); - }); - return button; - } - - function getOpenTerminalButtonText(action) { - switch (action?.kind) { - case 'success': - return '✓'; - case 'failure': - return '!'; - case 'unknown': - return '?'; - default: - return '↗'; - } - } - - function getOpenTerminalButtonLabel(title, action) { - switch (action?.kind) { - case 'pending': - return `Opening terminal for ${title}`; - case 'success': - return `Terminal open requested for ${title}`; - case 'failure': - return `Open terminal failed for ${title}: ${action.message}`; - case 'unknown': - return `Terminal open outcome unknown for ${title}: ${action.message}`; - default: - return `Open terminal for ${title}`; - } - } - - function createRecordDetail(record) { - const detail = document.createElement('div'); - detail.className = 'detail card-detail'; - - const workspaceRepo = record.qualifiedRepoName ?? record.repoName; - const workspacePr = formatPr(record.prUrl); - const workspacePrHref = getPullRequestHref(record); - const checkout = formatCheckout(record); - - const identityRows = [ - createDetailRow('Session ID', record.sessionId, { - copyLabel: 'Session ID', - copyValue: record.sessionId, - middleTruncateTail: 12, - }), - createDetailRow('Runtime ID', record.runtimeId, { - copyLabel: 'Runtime ID', - copyValue: record.runtimeId, - middleTruncateTail: 12, - }), - createDetailRow('PID', record.pid === null ? null : String(record.pid), { - copyLabel: 'PID', - copyValue: record.pid === null ? null : String(record.pid), - }), - ]; - - const spawnedCount = countSpawnedChildRuntimeSessions(state.snapshot?.records ?? [], record); - if (spawnedCount > 0) { - identityRows.push( - createDetailRow('Spawned', String(spawnedCount), { - labelTitle: SPAWNED_CHILD_RUNTIME_TOOLTIP, - valueTitle: SPAWNED_CHILD_RUNTIME_TOOLTIP, - copyValue: null, - }), - ); - } - - detail.append( - createDetailSection('IDENTITY', identityRows), - createDetailSection('WORKSPACE', [ - createDetailRow('CWD', record.cwd === null ? null : shortenHomePath(record.cwd), { - copyLabel: 'CWD', - copyValue: record.cwd, - middleTruncateTail: 24, - }), - createDetailRow('Branch', record.branch, { - copyLabel: 'Branch', - copyValue: record.branch, - }), - createDetailRow('Repo', workspaceRepo, { - copyLabel: 'Repo', - copyValue: workspaceRepo, - }), - createDetailRow('Checkout', checkout, { - copyLabel: 'Checkout', - copyValue: checkout, - }), - createDetailRow('PR', workspacePr, { - copyLabel: 'PR', - copyValue: workspacePrHref ?? workspacePr, - linkHref: workspacePrHref, - }), - ]), - createStatusSection(record), - createKillSessionSection(record), - ); - - if (record.diagnostics.length > 0) { - const list = document.createElement('ul'); - list.className = 'detail-diagnostics'; - for (const diagnostic of record.diagnostics) { - list.append(createDiagnosticItem(diagnostic)); - } - detail.append(createDetailSection('Record diagnostics', [list])); - } - - return detail; - } - - function createKillSessionSection(record) { - const action = getKillSessionActionForRecord(record); - const restartAction = - state.restartSessionAction?.runtimeId === record.runtimeId - ? state.restartSessionAction - : null; - const content = []; - let sharedActionsRow = null; - - if (restartAction?.kind === 'confirming') { - const panel = document.createElement('div'); - panel.className = 'stop-confirmation'; - panel.append( - createText( - 'p', - 'Restart sends TERM, may force-kill Pi after 2 seconds, and can lose in-flight work. Restart is refused while Pi owns child processes.', - 'stop-confirmation-copy', - ), - ); - const actions = document.createElement('div'); - actions.className = 'stop-confirmation-actions'; - const confirm = document.createElement('button'); - confirm.type = 'button'; - confirm.className = 'stop-confirm stop-confirm-primary'; - confirm.textContent = 'Restart Session'; - confirm.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - confirmRestartSession(record); - }); - const cancel = document.createElement('button'); - cancel.type = 'button'; - cancel.className = 'stop-confirm'; - cancel.textContent = 'Cancel'; - cancel.addEventListener('click', () => { - state.restartSessionAction = null; - render(); - }); - actions.append(confirm, cancel); - panel.append(actions); - content.push(panel); - window.setTimeout?.(() => confirm.focus?.(), 0); - } else { - const actionsRow = document.createElement('div'); - actionsRow.className = 'stop-action-row'; - const restart = document.createElement('button'); - restart.type = 'button'; - restart.className = 'restart-action-button'; - restart.textContent = - restartAction?.kind === 'pending' - ? 'Restarting…' - : record.restart?.operation?.status === 'outcome-unknown' - ? 'Reconcile restart' - : record.restart?.operation?.status === 'stopped-not-restarted' || - record.restart?.operation?.status === 'stopped' - ? 'Retry restart' - : restartAction?.kind === 'unknown' - ? 'Reconcile restart' - : restartAction?.kind === 'failure' - ? 'Retry restart' - : 'Restart Session'; - restart.disabled = record.restart?.available !== true || restartAction?.kind === 'pending'; - const unavailableReason = getRestartUnavailableMessage(record.restart); - restart.setAttribute('aria-label', unavailableReason); - restart.setAttribute('title', unavailableReason); - restart.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - openRestartSessionConfirmation(record); - }); - actionsRow.append(restart); - content.push(actionsRow); - sharedActionsRow = actionsRow; - } - - if (restartAction?.kind && restartAction.kind !== 'confirming') { - content.push( - createText( - 'p', - restartAction.kind === 'pending' - ? 'Restarting session…' - : (restartAction.message ?? ''), - restartAction.kind === 'failure' - ? 'stop-action-message stop-action-failure' - : 'stop-action-message', - ), - ); - } - - if (action?.kind === 'confirming') { - const panel = document.createElement('div'); - panel.className = 'stop-confirmation'; - - const copy = document.createElement('p'); - copy.className = 'stop-confirmation-copy'; - copy.textContent = - 'Ending this session sends SIGTERM to the Pi runtime only. Session history is preserved.'; - - const actions = document.createElement('div'); - actions.className = 'stop-confirmation-actions'; - - const confirm = document.createElement('button'); - confirm.type = 'button'; - confirm.className = 'stop-confirm stop-confirm-primary'; - confirm.textContent = 'End session'; - confirm.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - confirmKillSession(record); - }); - - const cancel = document.createElement('button'); - cancel.type = 'button'; - cancel.className = 'stop-confirm'; - cancel.textContent = 'Cancel'; - cancel.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - clearKillSessionAction(); - render(); - }); - - actions.append(confirm, cancel); - panel.append(copy, actions); - content.push(panel); - - if (typeof window.setTimeout === 'function') { - window.setTimeout(() => { - confirm.focus?.(); - }, 0); - } - } else { - const row = sharedActionsRow ?? document.createElement('div'); - row.className = 'stop-action-row'; - - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'stop-action-button'; - button.textContent = getKillSessionButtonText(action); - button.disabled = state.killSessionAction?.kind === 'pending'; - button.addEventListener('click', (event) => { - event.preventDefault?.(); - event.stopPropagation?.(); - openKillSessionConfirmation(record); - }); - row.append(button); - if (sharedActionsRow === null) content.push(row); - } - - if (action?.kind === 'pending') { - content.push(createText('p', 'Requesting session end…', 'stop-action-message')); - } else if ( - action?.kind === 'success' || - action?.kind === 'failure' || - action?.kind === 'unknown' - ) { - content.push( - createText( - 'p', - action.message, - action.kind === 'failure' - ? 'stop-action-message stop-action-failure' - : 'stop-action-message', - ), - ); - } - - return createDetailSection(null, content, { - ariaLabel: 'Session actions', - ariaLive: 'polite', - }); - } - - function getKillSessionButtonText(action) { - switch (action?.kind) { - case 'pending': - return 'Ending…'; - case 'success': - return 'End requested'; - case 'failure': - return 'Retry end'; - case 'unknown': - return 'Outcome unknown'; - default: - return 'End session'; - } - } - - function createStatusSection(record) { - const content = []; - - if (record.chips.length > 0) { - const chips = document.createElement('div'); - chips.className = 'chips detail-chips'; - for (const chip of record.chips) { - chips.append(createChip(chip)); - } - content.push(chips); - } - - content.push( - createDetailRow('Activity', formatStatusActivityDetail(record)), - createDetailRow('Compaction', formatCompactionDetail(record.compaction)), - createDetailRow('Presence reason', humanizePresenceReason(record.presenceReason)), - createDetailRow('Child runtime', formatChildRuntimeDetail(record)), - createDetailRow('Current tool', record.currentToolName), - createDetailRow('Last error', record.lastError), - ); - - return createDetailSection('STATUS', content); - } - - function formatStatusActivityDetail(record) { - if (record.activityState !== 'awaiting-input' && record.activityState !== 'compacting') { - return null; - } - - const activity = getActivityDisplay(record); - return activity.detail === null ? activity.label : `${activity.label} · ${activity.detail}`; - } - - function formatCompactionDetail(compaction) { - if (compaction === null || compaction === undefined) { - return null; - } - - const parts = [compaction.state, formatDuration(compaction.ageMs)]; - if (compaction.reason !== null) { - parts.push(compaction.reason); - } - if (compaction.willRetry) { - parts.push('retrying'); - } - return parts.join(' · '); - } - - function createDetailSection(title, children, options = {}) { - const section = document.createElement('section'); - section.className = 'detail-section'; - if (options.ariaLabel) { - section.setAttribute('aria-label', options.ariaLabel); - } - if (title) { - section.append(createText('div', title, 'detail-section-title')); - } - - const body = document.createElement('div'); - body.className = 'detail-section-body'; - if (options.ariaLive) body.setAttribute('aria-live', options.ariaLive); - for (const child of children) { - if (child) { - body.append(child); - } - } - - section.append(body); - return section; - } - - function createDetailRow(label, value, options = {}) { - if (value === null || value === '') { - return null; - } - - const row = document.createElement('div'); - row.className = 'detail-row'; - - const labelElement = createText('div', label, 'detail-label'); - if (options.labelTitle) { - labelElement.setAttribute('title', options.labelTitle); - } - - row.append( - labelElement, - createDetailValue( - value, - options.linkHref ?? null, - options.middleTruncateTail ?? null, - options.valueTitle ?? null, - ), - ); - - const copyButton = - options.copyValue === null - ? null - : createCopyButton(options.copyLabel ?? label, options.copyValue ?? value); - if (copyButton) { - row.append(copyButton); - } - - return row; - } - - function createCopyButton(label, value) { - if (value === null || value === '') { - return null; - } - - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'copy-button'; - button.setAttribute('aria-label', `Copy ${label}`); - button.setAttribute('title', 'copy'); - button.textContent = '⧉'; - button.addEventListener('click', () => { - copyTextToClipboard(value); - }); - return button; - } - - function renderDiagnostics() { - const diagnostics = state.showAll ? (state.snapshot?.diagnostics ?? []) : []; - elements.diagnostics.replaceChildren(); - elements.diagnosticsPanel.classList.toggle('hidden', diagnostics.length === 0); - - for (const diagnostic of diagnostics) { - elements.diagnostics.append(createDiagnosticItem(diagnostic)); - } - } - - function createDiagnosticItem(diagnostic) { - const item = document.createElement('li'); - item.className = 'diag-line'; - item.textContent = formatDiagnostic(diagnostic); - return item; - } - - function countPresenceStates(records) { - return records.reduce( - (counts, record) => { - counts[record.presenceState] += 1; - return counts; - }, - { live: 0, stale: 0, dead: 0, unknown: 0 }, - ); - } - - function countSpawnedChildRuntimeSessions(records, parent) { - return records.filter((record) => isSpawnedChildRuntimeForParent(record, parent)).length; - } - - function isSpawnedChildRuntimeForParent(record, parent) { - return ( - isTempSession(record) && - isActiveSession(record) && - record.derivedFacets?.childRuntime?.parentRuntimeId === parent.runtimeId - ); - } - - function isActiveSession(record) { - return DEFAULT_VISIBLE_STATES.has(record.presenceState); - } - - function isTempSession(record) { - return record.derivedFacets?.rowKind === 'ephemeral_child_runtime'; - } - - function getDisplayTitle(record) { - if (record.sessionName) { - return { text: record.sessionName, source: 'sessionName' }; - } - if (record.repoName) { - return { text: record.repoName, source: 'repoName' }; - } - - const cwdBasename = getCwdBasename(record.cwd); - if (cwdBasename) { - return { text: cwdBasename, source: 'cwd' }; - } - - return { text: formatShortId(record.runtimeId), source: 'runtimeId' }; - } - - function getRepoLabel(record, titleSource) { - if (titleSource === 'repoName' || titleSource === 'cwd') { - return null; - } - return record.repoName ?? getCwdBasename(record.cwd); - } - - function getCwdBasename(cwd) { - if (!cwd) { - return null; - } - const normalized = cwd.replace(/\/+$/u, ''); - if (normalized.length === 0) { - return shortenHomePath(cwd); - } - const parts = normalized.split('/'); - return parts[parts.length - 1] || shortenHomePath(cwd); - } - - function formatCheckout(record) { - if (!record.isLinkedWorktree) { - return 'primary'; - } - return record.worktreeLabel ? `worktree · ${record.worktreeLabel}` : 'worktree'; - } - - function formatChildRuntimeLabel(record) { - const childRuntime = getUsefulChildRuntime(record); - if (!childRuntime) { - return null; - } - - return `child: ${childRuntime.confidence}`; - } - - function formatChildRuntimeDetail(record) { - const childRuntime = getUsefulChildRuntime(record); - if (!childRuntime) { - return null; - } - - const evidenceLabels = childRuntime.evidence - .filter((evidence) => evidence.confidence !== 'low') - .map((evidence) => formatChildRuntimeEvidence(evidence.code)) - .filter((label, index, labels) => labels.indexOf(label) === index) - .slice(0, 2); - const via = evidenceLabels.length === 0 ? '' : ` via ${evidenceLabels.join(' + ')}`; - const parent = childRuntime.parentRuntimeId - ? ` · parent ${formatShortId(childRuntime.parentRuntimeId)}` - : ''; - return `${childRuntime.confidence}${via}${parent}`; - } - - function getUsefulChildRuntime(record) { - const derivedFacets = record.derivedFacets; - const childRuntime = derivedFacets?.childRuntime; - if ( - derivedFacets?.rowKind !== 'ephemeral_child_runtime' || - !childRuntime || - !isUsefulChildRuntimeConfidence(childRuntime.confidence) || - !Array.isArray(childRuntime.evidence) - ) { - return null; - } - return childRuntime; - } - - function isUsefulChildRuntimeConfidence(confidence) { - return confidence === 'medium' || confidence === 'high' || confidence === 'explicit'; - } - - function formatChildRuntimeEvidence(code) { - switch (code) { - case 'explicit_header_parent': - return 'header parent'; - case 'inherited_deck_runtime': - return 'deck env'; - case 'process_ancestor_match': - return 'process ancestor'; - case 'started_during_parent_tool': - return 'parent tool'; - case 'same_terminal': - return 'same terminal'; - case 'headless_in_memory': - return 'headless in-memory'; - case 'automation_input_source': - return 'automation input'; - default: - return String(code).replaceAll('_', ' '); - } - } - - function formatPr(prUrl) { - if (!prUrl) { - return null; - } - const prNumber = parsePullRequestNumber(prUrl); - return prNumber ? `#${prNumber}` : prUrl; - } - - function getPullRequestHref(record) { - const prNumber = parsePullRequestNumber(record.prUrl); - if (prNumber !== null) { - const qualifiedRepoName = getQualifiedRepoName(record); - if (qualifiedRepoName !== null) { - return `https://github.com/${qualifiedRepoName}/pull/${prNumber}`; - } - } - - return isHttpUrl(record.prUrl) ? record.prUrl : null; - } - - function getQualifiedRepoName(record) { - const repoName = record.qualifiedRepoName ?? record.repoName; - return repoName && repoName.includes('/') ? repoName : null; - } - - function parsePullRequestNumber(prUrl) { - if (!prUrl) { - return null; - } - const match = prUrl.match(/\/pull\/(\d+)$/u); - return match ? match[1] : null; - } - - function getRowActivityLabel(record) { - if (record.presenceState !== 'live') { - return record.presenceState; - } - - const activity = getActivityDisplay(record); - return activity.label === 'idle' ? 'live' : activity.label; - } - - function getActivityDisplay(record) { - const ageLabel = record.activityAgeMs === null ? null : formatDuration(record.activityAgeMs); - - switch (record.activityState) { - case 'idle': - return { label: 'idle', detail: null, cardAgeLabel: null }; - case 'thinking': - return { label: 'thinking', detail: null, cardAgeLabel: ageLabel }; - case 'tool-running': - return { - label: 'tool-running', - detail: record.currentToolName, - cardAgeLabel: ageLabel, - }; - case 'awaiting-input': - return { - label: 'needs input', - detail: null, - cardAgeLabel: ageLabel, - }; - case 'compacting': - return { - label: 'compacting', - detail: record.compaction?.willRetry === true ? 'retrying' : null, - cardAgeLabel: ageLabel, - }; - case 'error': - return { - label: 'error', - detail: record.lastError, - cardAgeLabel: ageLabel, - }; - default: - return { - label: record.activityState, - detail: null, - cardAgeLabel: ageLabel, - }; - } - } - - function getListAgeMs(record) { - return record.presenceState === 'stale' || record.presenceState === 'dead' - ? record.heartbeatAgeMs - : (record.activityAgeMs ?? record.heartbeatAgeMs); - } - - function formatDuration(durationMs) { - if (!Number.isFinite(durationMs) || durationMs < 0) { - return 'n/a'; - } - if (durationMs < 1_000) { - return '<1s'; - } - if (durationMs < 60_000) { - return `${Math.round(durationMs / 1_000)}s`; - } - if (durationMs < 60 * 60_000) { - return `${Math.round(durationMs / 60_000)}m`; - } - return `${Math.round(durationMs / (60 * 60_000))}h`; - } - - function formatTimestamp(isoString) { - const date = new Date(isoString); - if (Number.isNaN(date.getTime())) { - return isoString; - } - return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); - } - - function createActivityIcon(record) { - const activity = getActivityDisplay(record); - const label = activity.detail ? `${activity.label}: ${activity.detail}` : activity.label; - const icon = document.createElement('span'); - icon.className = 'activity-icon'; - icon.setAttribute('data-activity', record.activityState); - icon.setAttribute('role', 'img'); - icon.setAttribute('aria-label', label); - icon.setAttribute('title', label); - - const svg = createSvgElement('svg'); - svg.setAttribute('viewBox', '0 0 16 16'); - svg.setAttribute('width', '16'); - svg.setAttribute('height', '16'); - svg.setAttribute('aria-hidden', 'true'); - - switch (record.activityState) { - case 'idle': - svg.append(createSvgCircle({ cx: '8', cy: '8', r: '5.5' })); - break; - case 'thinking': { - const orbit = createSvgCircle({ cx: '8', cy: '8', r: '6.35' }); - orbit.setAttribute('class', 'activity-icon-thinking-orbit'); - orbit.setAttribute('stroke-linecap', 'round'); - orbit.setAttribute('stroke-width', '1.2'); - svg.append( - createSvgPath( - 'M9.5 2A2.5 2.5 0 0 1 12 4.5v.2A2.5 2.5 0 0 1 14 7.1c0 .8-.4 1.5-1 2 .6.5 1 1.2 1 2A2.9 2.9 0 0 1 11.1 14H10a2 2 0 0 1-2-2V4a2 2 0 0 1 1.5-2ZM6.5 2A2.5 2.5 0 0 0 4 4.5v.2A2.5 2.5 0 0 0 2 7.1c0 .8.4 1.5 1 2-.6.5-1 1.2-1 2A2.9 2.9 0 0 0 4.9 14H6a2 2 0 0 0 2-2V4a2 2 0 0 0-1.5-2Z', - ), - orbit, - ); - break; - } - case 'tool-running': - svg.append( - createSvgPath( - 'M.1 2.2A3 3 0 0 0 3.8 5.9l6.3 6.3a3 3 0 0 0 3.7 3.7l-2.1-2.1a.5.5 0 0 1 .4-.9h1.4a.5.5 0 0 1 .4.1l2.1 2.1a3 3 0 0 0-3.7-3.7L5.9 5.1A3 3 0 0 0 2.2.1l2.1 2.1a.5.5 0 0 1-.4.9H2.5a.5.5 0 0 1-.4-.1L.1 2.2Z', - ), - ); - break; - case 'awaiting-input': - svg.append( - createSvgPath( - 'M3 4a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H8.6l-3.1 3.1A.9.9 0 0 1 4 12.5V10a2 2 0 0 1-1-1.7V4Zm3 1.4v1.4h4V5.4H6Zm0 2.4v1.4h2.8V7.8H6Z', - ), - ); - break; - case 'compacting': - svg.append( - createSvgPath( - 'M8 2a6 6 0 0 1 5.2 3H15l-2.8 3L9.4 5h1.7A4 4 0 0 0 4 6.6L2.3 5.5A6 6 0 0 1 8 2Zm5.7 8.5A6 6 0 0 1 2.8 11H1l2.8-3 2.8 3H4.9a4 4 0 0 0 7.1-1.6l1.7 1.1Z', - ), - ); - break; - case 'error': - svg.append( - createSvgPath( - 'M8.9 1.5l6.4 11c.3.5.1 1.1-.4 1.4-.2.1-.3.1-.5.1H1.6c-.6 0-1-.4-1-1 0-.2 0-.3.1-.5l6.4-11c.3-.5.9-.6 1.4-.4.2.2.4.3.4.4zM8 11c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1zm0-6c-.6 0-1 .4-1 1v3c0 .6.4 1 1 1s1-.4 1-1V6c0-.6-.4-1-1-1z', - ), - ); - break; - default: - svg.append(createSvgCircle({ cx: '8', cy: '8', r: '6.5' })); - svg.append(createSvgText('?')); - break; - } - - icon.append(svg); - return icon; - } - - function createSvgElement(tagName) { - return document.createElementNS('http://www.w3.org/2000/svg', tagName); - } - - function createSvgPath(pathData) { - const path = createSvgElement('path'); - path.setAttribute('d', pathData); - path.setAttribute('fill', 'currentColor'); - return path; - } - - function createSvgCircle(attributes) { - const circle = createSvgElement('circle'); - for (const [name, value] of Object.entries(attributes)) { - circle.setAttribute(name, value); - } - circle.setAttribute('fill', 'none'); - circle.setAttribute('stroke', 'currentColor'); - circle.setAttribute('stroke-width', '1.5'); - return circle; - } - - function createSvgText(textValue) { - const text = createSvgElement('text'); - text.setAttribute('x', '8'); - text.setAttribute('y', '11'); - text.setAttribute('fill', 'currentColor'); - text.setAttribute('text-anchor', 'middle'); - text.setAttribute('font-size', '10'); - text.setAttribute('font-weight', '700'); - text.textContent = textValue; - return text; - } - - function humanizePresenceReason(reason) { - if (!reason || reason === 'fresh_heartbeat') { - return null; - } - - return reason.replaceAll('_', ' '); - } - - function formatDiagnostic(diagnostic) { - const location = diagnostic.runtimeId - ? ` runtime=${diagnostic.runtimeId}` - : diagnostic.filePath - ? ` (${diagnostic.filePath})` - : ''; - return `${diagnostic.code}${location}: ${diagnostic.message}`; - } - - function getSnapshotFailureMessage(diagnostics, recordCount) { - if (state.loading) { - return null; - } - - const toolbeltDiagnostic = diagnostics.find( - (diagnostic) => diagnostic.code === 'toolbelt_snapshot_unavailable', - ); - if (recordCount > 0) { - return toolbeltDiagnostic?.message ?? null; - } - - return toolbeltDiagnostic?.message ?? diagnostics[0]?.message ?? null; - } - - function formatShortId(value) { - return value.length <= 8 ? value : value.slice(0, 8); - } - - function shortenHomePath(cwd) { - const homeDirectory = detectHomeDirectory(); - if (homeDirectory && cwd.startsWith(homeDirectory)) { - return `~${cwd.slice(homeDirectory.length)}`; - } - return cwd; - } - - function detectHomeDirectory() { - const cwd = state.snapshot?.records.find((record) => record.cwd)?.cwd; - if (!cwd || !HOME_PREFIXES.some((prefix) => cwd.startsWith(prefix))) { - return null; - } - - const segments = cwd.split('/'); - return segments.length >= 4 ? segments.slice(0, 3).join('/') : null; - } - - function copyTextToClipboard(text) { - void Promise.resolve(host.copyText(text)).catch(() => {}); - } - - function isHttpUrl(value) { - if (!value) { - return false; - } - - try { - const url = new URL(value); - return url.protocol === 'https:' || url.protocol === 'http:'; - } catch { - return false; - } - } - - function createLine(className, children) { - const line = document.createElement('div'); - line.className = className; - line.append(...children); - return line; - } - - function createText(tagName, text, className) { - const element = document.createElement(tagName); - if (className) { - element.className = className; - } - element.textContent = text; - return element; - } - - function createDetailValue(text, linkHref, middleTruncateTail, valueTitle) { - const value = document.createElement(linkHref ? 'a' : 'div'); - value.className = linkHref ? 'detail-value detail-link' : 'detail-value'; - - if (middleTruncateTail === null || text.length <= middleTruncateTail) { - value.textContent = text; - } else { - value.classList.add('detail-value-middle'); - value.setAttribute('title', text); - const { head, tail } = splitMiddleText(text, middleTruncateTail); - value.append( - createText('span', head, 'detail-value-head'), - createText('span', tail, 'detail-value-tail'), - ); - } - - if (valueTitle !== null) { - value.setAttribute('title', valueTitle); - } - - if (linkHref) { - value.setAttribute('href', linkHref); - value.setAttribute('target', '_blank'); - value.setAttribute('rel', 'noreferrer'); - value.addEventListener('click', (event) => { - event.preventDefault(); - void Promise.resolve(host.openExternal(linkHref)).catch(() => {}); - }); - } - - return value; - } - - function splitMiddleText(text, tailLength) { - return { - head: text.slice(0, -tailLength), - tail: text.slice(-tailLength), - }; - } - - function createChip(text, className = 'chip') { - return createText('span', text, className); - } - - function isObject(candidate) { - return typeof candidate === 'object' && candidate !== null; - } - - function getErrorMessage(error) { - return error instanceof Error ? error.message : String(error); - } - - init(); - } - - const target = globalThis.window ?? globalThis; - target.SessionDeckUI = { mount: mountSessionDeckUI }; -})(); diff --git a/apps/session-deck-desktop/web/style.css b/apps/session-deck-desktop/web/style.css deleted file mode 100644 index d79ea8f4..00000000 --- a/apps/session-deck-desktop/web/style.css +++ /dev/null @@ -1,1353 +0,0 @@ -:root { - color-scheme: dark; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --font-mono: 'SF Mono', 'Berkeley Mono', ui-monospace, Menlo, Monaco, Consolas, monospace; - --color-bg: #12161e; - --color-chrome: #151922; - --color-surface: #1c212c; - --color-repo-row: #232936; - --color-surface-raised: #202633; - --color-surface-hover: #252d3a; - --color-card-interactive: #252d3a; - --color-divider: #3b4252; - --color-divider-strong: #4c566a; - --color-text: #d8dee9; - --color-text-strong: #e5e9f0; - --color-text-muted: #a7b0c0; - --color-text-dim: #778196; - --color-rail: #8fbcbb; - --color-repo-rail: var(--color-rail); - --color-accent: #88c0d0; - --color-accent-deep: #5e81ac; - --color-live: #a3be8c; - --color-warn: #ebcb8b; - --color-danger: #bf616a; - --color-chip: #273141; - --color-chip-subtle: #202633; - --color-banner-bg: rgba(191, 97, 106, 0.12); - --color-banner-border: rgba(191, 97, 106, 0.44); - --color-banner-text: #e5c7cb; - --gutter-x: 13px; - --session-indent: 30px; - --activity-size: 18px; - background: var(--color-bg); - color: var(--color-text); - line-height: 1.4; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - overflow: hidden; - background: var(--color-bg); - color: var(--color-text); -} - -button, -input { - font: inherit; -} - -input[type='checkbox'] { - accent-color: var(--color-accent); -} - -button { - cursor: pointer; - border: 1px solid transparent; - background: transparent; - color: inherit; - border-radius: 7px; - padding: 0.35rem 0.65rem; -} - -button:hover { - background: var(--color-surface-hover); - border-color: var(--color-divider); -} - -button:focus-visible, -input:focus-visible { - outline: 1px solid var(--color-accent); - outline-offset: 1px; -} - -h1, -h2, -p, -ul { - margin: 0; -} - -h1 { - font-size: 1.1rem; - font-weight: 600; - line-height: 1.2; -} - -h2 { - color: var(--color-text-muted); - font-size: 0.8rem; - font-weight: 600; - letter-spacing: 0.02em; -} - -.app { - height: 100vh; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.topbar { - position: sticky; - top: 0; - z-index: 2; - padding: 9px 12px 8px; - border-bottom: 1px solid var(--color-divider); - background: var(--color-chrome); -} - -.topbar-copy { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 12px; - min-width: 0; - font-family: var(--font-mono); -} - -.topbar-copy::before { - content: '❯ deck'; - flex: 0 0 auto; - color: var(--color-rail); - font-weight: 700; - letter-spacing: 0.02em; - text-shadow: 0 0 10px rgba(143, 188, 187, 0.22); -} - -.controls { - display: inline-flex; - align-items: center; - gap: 10px; - flex: 0 0 auto; - flex-wrap: nowrap; - margin-left: auto; -} - -.toggle, -#live-count, -#refresh, -#summary { - font-size: 0.82rem; -} - -#summary { - flex: 1 1 14rem; - min-width: 0; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 4px 0; - overflow: visible; - text-overflow: clip; - white-space: normal; -} - -#summary > * { - display: inline-flex; - align-items: center; - white-space: nowrap; -} - -#summary > * + *::before { - content: '·'; - margin: 0 6px; - color: var(--color-text-muted); - opacity: 0.75; -} - -.summary-count { - color: var(--color-text-strong); - font-weight: 700; - line-height: 1.2; -} - -.toggle { - position: relative; - display: inline-flex; - align-items: center; - color: var(--color-text-muted); - border: 1px solid rgba(76, 86, 106, 0.7); - border-radius: 3px; - background: transparent; - font-family: var(--font-mono); - line-height: 1.2; - white-space: nowrap; -} - -.toggle input { - position: absolute; - inset: 0; - margin: 0; - cursor: pointer; - opacity: 0; -} - -.toggle span { - display: inline-flex; - justify-content: center; - min-width: 3.2rem; - border-radius: 2px; - padding: 2px 8px; - font-size: 0; - pointer-events: none; -} - -.toggle span::before { - content: 'Active'; - font-size: 0.82rem; -} - -.toggle input:checked + span { - color: var(--color-text-strong); - background: var(--color-surface-raised); -} - -.toggle input:checked + span::before { - content: 'All'; -} - -.toggle input:focus-visible + span { - outline: 1px solid var(--color-accent); - outline-offset: 1px; -} - -#refresh { - padding: 0.12rem 0.28rem; - color: var(--color-text-muted); - font-family: var(--font-mono); - line-height: 1; - opacity: 0.75; -} - -#refresh:hover { - border-color: transparent; - background: transparent; - color: var(--color-accent); - opacity: 1; -} - -.banner { - margin: 8px 12px 0; - border-radius: 3px; - padding: 8px 10px; - background: var(--color-banner-bg); - border: 1px solid var(--color-banner-border); - color: var(--color-banner-text); - font-family: var(--font-mono); -} - -.list-shell { - display: flex; - flex: 1 1 auto; - flex-direction: column; - min-height: 0; - overflow: hidden; - padding: 0 0 12px; -} - -.list { - align-content: start; - flex: 1 1 auto; - min-height: 0; - overflow-y: auto; -} - -.list, -.repo-group, -.repo-group-records { - display: grid; -} - -.repo-header-row { - position: relative; - border-bottom: 1px solid var(--color-divider); - background: var(--color-repo-row); -} - -.repo-header-row::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: var(--gutter-x); - width: 1px; - background: linear-gradient( - 180deg, - rgba(143, 188, 187, 0.15), - rgba(143, 188, 187, 0.88), - rgba(143, 188, 187, 0.32) - ); - box-shadow: 0 0 10px rgba(143, 188, 187, 0.16); - pointer-events: none; -} - -.repo-header, -.card-toggle { - position: relative; - width: 100%; - text-align: left; - border: 0; - border-radius: 0; - background: transparent; -} - -.repo-header { - display: flex; - align-items: center; - overflow: hidden; - padding: 10px 72px 10px calc(var(--gutter-x) + 12px); - border-bottom: 0; - background: transparent; - color: var(--color-text-strong); - font-family: var(--font-mono); - white-space: nowrap; -} - -.repo-header-label { - display: block; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.repo-owner, -.repo-divider { - color: var(--color-text-muted); -} - -.repo-owner { - opacity: 0.86; -} - -.repo-name { - font-weight: 700; -} - -.repo-count { - color: var(--color-text-dim); - font-variant-numeric: tabular-nums; -} - -.repo-group-records { - position: relative; - background: var(--color-bg); -} - -.repo-group-records::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: var(--gutter-x); - z-index: 2; - width: 1px; - background: linear-gradient(180deg, rgba(143, 188, 187, 0.58), rgba(143, 188, 187, 0.16)); - pointer-events: none; -} - -.card { - position: relative; - background: var(--color-surface); - border: 0; - border-bottom: 1px solid rgba(59, 66, 82, 0.82); - border-radius: 0; - overflow: hidden; -} - -.repo-group-records > .card::before { - content: ''; - position: absolute; - top: 22px; - left: var(--gutter-x); - z-index: 1; - width: calc(var(--session-indent) - var(--gutter-x) - 10px); - border-top: 1px solid rgba(143, 188, 187, 0.48); - pointer-events: none; -} - -.card.expanded { - background: var(--color-surface-raised); - border-bottom-color: var(--color-divider-strong); -} - -.card-toggle { - display: grid; - gap: 6px; - padding: 10px 52px 10px var(--session-indent); -} - -.card-open { - position: absolute; - top: 9px; - right: 24px; - z-index: 3; - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; - padding: 0; - border: 0; - background: transparent; - color: var(--color-text-dim); - font-family: var(--font-mono); - font-size: 0.86rem; - line-height: 1; - opacity: 0.82; -} - -.card-open:hover, -.card-open:focus-visible { - border-color: transparent; - background: rgba(136, 192, 208, 0.1); - color: var(--color-accent); - opacity: 1; -} - -.card-open:disabled { - cursor: not-allowed; - background: transparent; - opacity: 0.44; -} - -.card-open[data-state='pending'] { - color: var(--color-accent); - opacity: 0.72; -} - -.card-open[data-state='success'] { - color: var(--color-live); - opacity: 1; -} - -.card-open[data-state='failure'] { - color: var(--color-danger); - opacity: 0.95; -} - -.repo-header::after, -.card-toggle::after { - content: '›'; - position: absolute; - top: 50%; - right: 8px; - color: var(--color-text-dim); - font-size: 1.2rem; - font-weight: 600; - line-height: 1; - opacity: 0.7; - pointer-events: none; - transform: translateY(-50%); - transition: - color 120ms ease, - opacity 120ms ease; -} - -button.repo-header:hover::after, -button.repo-header:focus::after, -button.card-toggle:hover::after, -button.card-toggle:focus::after { - opacity: 0.95; -} - -button.repo-header[aria-expanded='true']::after, -button.card-toggle[aria-expanded='true']::after { - content: '⌄'; - color: var(--color-accent); - opacity: 1; -} - -.repo-header:hover { - background: rgba(136, 192, 208, 0.045); - border-color: transparent; - border-bottom-color: var(--color-divider); -} - -.card-toggle:hover, -.card-toggle:focus { - background: var(--color-card-interactive); -} - -.card.expanded > .card-toggle { - background: rgba(136, 192, 208, 0.055); -} - -.card-detail { - margin: 0 6px 10px calc(var(--gutter-x) + 4px); - padding: 8px 6px 10px 8px; - border-top: 1px solid var(--color-divider); - border-left: 1px solid rgba(143, 188, 187, 0.46); - background: var(--color-surface-raised); -} - -.stop-action-row, -.stop-confirmation-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.stop-action-row, -.stop-confirmation-actions { - justify-content: flex-end; -} - -.stop-action-button, -.restart-action-button, -.stop-confirm { - padding: 0.26rem 0.58rem; - font-family: var(--font-mono); - font-size: 0.78rem; - line-height: 1.05; -} - -.stop-action-button, -.restart-action-button { - border-color: rgba(76, 86, 106, 0.82); - background: rgba(191, 97, 106, 0.04); - color: rgba(229, 199, 203, 0.82); -} - -.stop-confirm { - border-color: rgba(191, 97, 106, 0.42); - color: var(--color-banner-text); -} - -.stop-action-button:hover, -.stop-action-button:focus-visible, -.restart-action-button:hover, -.restart-action-button:focus-visible { - border-color: rgba(191, 97, 106, 0.5); - background: rgba(191, 97, 106, 0.1); - color: var(--color-banner-text); -} - -.stop-confirm:hover, -.stop-confirm:focus-visible { - border-color: rgba(191, 97, 106, 0.72); - background: rgba(191, 97, 106, 0.14); -} - -.stop-action-button:disabled, -.restart-action-button:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.stop-confirmation { - display: grid; - gap: 8px; - border: 1px solid rgba(191, 97, 106, 0.38); - border-radius: 7px; - padding: 8px; - background: rgba(191, 97, 106, 0.08); -} - -.stop-confirmation-copy, -.stop-action-message { - color: var(--color-text-muted); - font-size: 0.82rem; -} - -.stop-confirm-primary, -.stop-action-failure { - color: var(--color-danger); -} - -.row-line1, -.row-line2, -.summary-line, -.diag-line { - display: flex; - gap: 8px; - align-items: baseline; - flex-wrap: wrap; -} - -.row-line1 { - position: relative; - align-items: center; - flex-wrap: nowrap; - min-width: 0; -} - -.row-line1 > .muted:not(.row-age) { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.row-line2, -.summary-line, -.diag-line, -.muted, -.empty { - color: var(--color-text-muted); -} - -.row-line2, -.summary-line, -.detail-section-title, -.toggle, -.diagnostics, -.detail-diagnostics { - font-size: 0.85rem; -} - -.row-line2, -.row-age, -.row-activity, -.summary-line, -.detail-section-title, -.detail-label, -.diagnostics, -.detail-diagnostics { - font-family: var(--font-mono); -} - -.row-title, -.detail-title { - font-weight: 600; -} - -.row-title { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - color: var(--color-text-strong); - text-overflow: ellipsis; - white-space: nowrap; -} - -.row-activity { - flex: 0 0 auto; - color: var(--color-text-dim); - font-size: 0.72rem; - letter-spacing: 0.04em; - text-transform: lowercase; - white-space: nowrap; -} - -.card.live .activity-icon[data-activity='idle'] ~ .row-activity { - color: var(--color-live); -} - -.activity-icon[data-activity='thinking'] ~ .row-activity, -.activity-icon[data-activity='tool-running'] ~ .row-activity, -.activity-icon[data-activity='compacting'] ~ .row-activity { - color: var(--color-accent); -} - -.activity-icon[data-activity='awaiting-input'] ~ .row-activity { - color: var(--color-warn); -} - -.card.stale .row-activity { - color: var(--color-warn); -} - -.card.dead .row-activity, -.activity-icon[data-activity='error'] ~ .row-activity { - color: var(--color-danger); -} - -.row-age { - margin-left: auto; - flex: 0 0 auto; - color: var(--color-text-dim); - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.activity-icon { - position: absolute; - top: 50%; - left: calc(var(--gutter-x) - var(--session-indent) - 9px); - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--activity-size); - min-width: var(--activity-size); - height: var(--activity-size); - border: 1px solid rgba(143, 188, 187, 0.42); - border-radius: 999px; - background: var(--color-surface); - color: var(--color-text-dim); - flex-shrink: 0; - opacity: 1; - transform: translateY(-50%); - z-index: 3; -} - -.card.expanded .activity-icon { - background: var(--color-surface-raised); - border-color: rgba(136, 192, 208, 0.62); -} - -.card.stale .activity-icon, -.card.dead .activity-icon, -.card.unknown .activity-icon { - border-style: dashed; -} - -.card.stale .activity-icon { - color: var(--color-warn); - border-color: rgba(235, 203, 139, 0.58); -} - -.card.dead .activity-icon, -.activity-icon[data-activity='error'] { - color: var(--color-danger); - border-color: rgba(191, 97, 106, 0.64); -} - -.card.live .activity-icon[data-activity='thinking'], -.card.live .activity-icon[data-activity='tool-running'], -.card.live .activity-icon[data-activity='compacting'] { - color: var(--color-accent); - border-color: rgba(136, 192, 208, 0.68); -} - -.card.live .activity-icon[data-activity='awaiting-input'] { - color: var(--color-warn); - border-color: rgba(235, 203, 139, 0.68); -} - -.activity-icon svg { - display: block; -} - -.activity-icon[data-activity='thinking'] .activity-icon-thinking-orbit { - opacity: 0.75; - stroke: var(--color-accent); - stroke-dasharray: 3 40; - animation: activity-icon-thinking-orbit 1.35s linear infinite; -} - -@keyframes activity-icon-thinking-orbit { - to { - stroke-dashoffset: -43; - } -} - -@media (prefers-reduced-motion: reduce) { - .activity-icon[data-activity='thinking'] .activity-icon-thinking-orbit { - animation: none; - } -} - -.chips { - display: flex; - gap: 6px; - flex-wrap: wrap; -} - -.chip { - border: 1px solid var(--color-divider-strong); - border-radius: 999px; - background: var(--color-chip); - color: var(--color-text); - padding: 2px 8px; - font-family: var(--font-mono); - font-size: 0.8rem; -} - -.chip-subtle { - background: var(--color-chip-subtle); - border-color: var(--color-divider); - color: var(--color-text-muted); -} - -.chips-inline .chip { - background: rgba(136, 192, 208, 0.055); - border-color: rgba(167, 176, 192, 0.18); - color: var(--color-text-muted); -} - -.chips-inline .chip-subtle { - background: transparent; - border-color: rgba(167, 176, 192, 0.12); - opacity: 0.82; -} - -.detail { - display: grid; -} - -.detail-summary { - display: grid; - gap: 6px; -} - -.detail-liveness { - margin-top: 2px; -} - -.detail-section { - display: grid; - gap: 5px; -} - -.detail-section + .detail-section { - margin-top: 7px; - padding-top: 7px; - border-top: 1px solid rgba(76, 86, 106, 0.58); -} - -.detail-section-title { - color: var(--color-text-dim); - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.detail-section-body { - display: grid; -} - -.detail-row { - position: relative; - display: grid; - grid-template-columns: minmax(80px, 84px) minmax(0, 1fr); - grid-template-areas: 'label value'; - gap: 4px 8px; - align-items: start; - padding: 2px 22px 2px 0; -} - -.detail-label { - grid-area: label; - color: var(--color-text-dim); - font-size: 0.8rem; - white-space: nowrap; -} - -.detail-value { - grid-area: value; - min-width: 0; - max-width: 100%; - justify-self: end; - color: var(--color-text); - font-family: var(--font-mono); - text-align: right; - font-size: 0.85rem; - overflow-wrap: anywhere; -} - -.detail-value-middle { - display: flex; - justify-content: flex-end; - overflow: hidden; - overflow-wrap: normal; - white-space: nowrap; -} - -.detail-value-head, -.detail-value-tail { - min-width: 0; - white-space: nowrap; -} - -.detail-value-head { - flex: 1 1 auto; - overflow: hidden; - text-overflow: ellipsis; -} - -.detail-value-tail { - flex: 0 0 auto; - overflow: hidden; - text-overflow: clip; -} - -.detail-link, -.detail-link:visited { - color: var(--color-accent); - text-decoration: none; -} - -.detail-link:hover, -.detail-link:focus-visible { - text-decoration: underline; -} - -.detail-chips { - padding: 0 0 2px; -} - -.copy-button { - position: absolute; - top: 2px; - right: 0; - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; - padding: 0; - color: var(--color-text-dim); - border-color: transparent; - background: transparent; - font-family: var(--font-mono); - line-height: 1; - opacity: 0.44; - transition: - opacity 120ms ease, - color 120ms ease, - background-color 120ms ease, - border-color 120ms ease; -} - -.detail-row:hover .copy-button, -.detail-row:focus-within .copy-button, -.copy-button:hover, -.copy-button:focus-visible { - opacity: 1; - color: var(--color-text-strong); - border-color: var(--color-divider); - background: rgba(136, 192, 208, 0.08); -} - -.detail-diagnostics { - display: grid; - gap: 4px; - list-style: none; - padding: 0; -} - -.diagnostics-panel { - display: grid; - gap: 8px; - padding: 10px 12px 14px; - border-top: 1px solid var(--color-divider); - background: var(--color-bg); -} - -.diagnostics-panel h2 { - color: var(--color-text-dim); - font-family: var(--font-mono); - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.diagnostics { - display: grid; - gap: 6px; - list-style: none; - padding: 0; -} - -.diagnostics .diag-line { - color: var(--color-text-muted); - font-family: var(--font-mono); - font-size: 0.76rem; -} - -.hidden { - display: none; -} - -.chips-inline { - margin-top: 2px; -} - -@media (max-width: 420px) { - :root { - --session-indent: 28px; - } - - .topbar-copy::before { - content: '❯'; - } - - .repo-header { - padding-right: 72px; - } - - .card-toggle { - padding-right: 52px; - } - - .card-open { - right: 24px; - } - - .detail-row { - grid-template-columns: minmax(80px, 84px) minmax(0, 1fr); - gap: 4px 6px; - } -} - -.empty { - padding: 12px; -} - -.repo-action-button { - position: absolute; - top: 50%; - right: 24px; - z-index: 1; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; - color: var(--color-accent); - font-family: var(--font-mono); - font-size: 0.82rem; - line-height: 1.2; - opacity: 0.9; - transform: translateY(-50%); - white-space: nowrap; -} - -.repo-action-button:hover, -.repo-action-button:focus-visible { - border-color: transparent; - background: transparent; - color: var(--color-text-strong); - opacity: 1; -} - -.new-session { - display: grid; - flex: 0 0 auto; - border-top: 1px solid var(--color-divider-strong); - border-bottom: 1px solid var(--color-divider); - background: var(--color-chrome); -} - -.new-session-button { - justify-self: start; - margin: 8px 12px; - padding: 0; - border: 0; - border-radius: 0; - color: var(--color-accent); - font-family: var(--font-mono); - font-size: 0.82rem; -} - -.new-session-button:hover:not(:disabled), -.new-session-button:focus-visible:not(:disabled) { - border-color: transparent; - background: transparent; - color: var(--color-text-strong); -} - -.new-session-button:disabled { - cursor: not-allowed; - opacity: 0.48; -} - -.worktree-form { - position: relative; - display: block; - margin: 0; - padding: 6px 12px 6px calc(var(--gutter-x) + 12px); - border: 0; - border-top: 1px solid rgba(143, 188, 187, 0.16); - border-bottom: 1px solid var(--color-divider); - border-radius: 0; - background: var(--color-repo-row); -} - -.worktree-form::before { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: var(--gutter-x); - width: 1px; - background: rgba(143, 188, 187, 0.58); - pointer-events: none; -} - -.worktree-compose-row { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; -} - -.worktree-config-row { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - margin-top: 5px; -} - -.worktree-config-summary { - flex: 1 1 auto; - max-width: none; -} - -.worktree-config-button { - flex: 0 0 auto; - padding: 0; - border: 0; - color: var(--color-accent); - font-family: var(--font-mono); - font-size: 0.72rem; -} - -.worktree-config-button:hover, -.worktree-config-button:focus-visible { - border-color: transparent; - background: transparent; - color: var(--color-text-strong); -} - -.worktree-config-drawer { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 6px; - margin-top: 6px; -} - -.worktree-config-option, -.worktree-config-drawer input[type='text'] { - min-width: 0; - height: 28px; - border: 1px solid var(--color-divider); - background: var(--color-chrome); - color: var(--color-text-muted); - font-family: var(--font-mono); - font-size: 0.72rem; -} - -.worktree-config-option[aria-checked='true'] { - border-color: var(--color-accent); - color: var(--color-text-strong); - background: rgba(136, 192, 208, 0.1); -} - -.worktree-config-drawer input[type='text'] { - grid-column: 1 / -1; - width: 100%; - border-radius: 7px; - padding: 0 0.56rem; -} - -.worktree-field-meta, -.pending-worktree-message { - color: var(--color-text-muted); - font-family: var(--font-mono); - font-size: 0.72rem; -} - -.worktree-field-meta { - flex: 0 0 auto; - max-width: 7.5rem; - overflow: hidden; - color: var(--color-accent); - text-overflow: ellipsis; - white-space: nowrap; -} - -.worktree-field-meta.worktree-config-summary { - flex: 1 1 auto; - max-width: none; -} - -.worktree-field-meta[data-state='loading'], -.worktree-field-meta[data-state='failed'] { - color: var(--color-text-dim); - opacity: 0.86; -} - -.worktree-field-meta[data-state='failed'] { - color: var(--color-warn); -} - -.worktree-branch-control { - display: flex; - align-items: stretch; - flex: 1 1 auto; - min-width: 0; - height: 32px; -} - -.worktree-form input[type='text'] { - width: 100%; - min-width: 0; - height: 32px; - color: var(--color-text-strong); - border: 1px solid var(--color-divider); - border-right: 0; - border-radius: 7px 0 0 7px; - padding: 0 0.56rem; - font-family: var(--font-mono); - font-size: 0.84rem; - line-height: 1.2; - background: var(--color-chrome); -} - -.worktree-form input[type='text']::placeholder { - color: rgba(167, 176, 192, 0.58); - opacity: 1; -} - -.worktree-form-feedback { - margin-top: 6px; - color: var(--color-text-muted); - font-family: var(--font-mono); - font-size: 0.72rem; - line-height: 1.35; -} - -.worktree-form-error { - color: var(--color-banner-text); -} - -.worktree-submit-button { - min-height: 32px; - height: 32px; - padding: 0 0.62rem; - border-color: var(--color-divider); - border-radius: 0 7px 7px 0; - background: rgba(136, 192, 208, 0.08); - color: var(--color-text-strong); - font-family: var(--font-mono); - font-size: 0.8rem; - font-weight: 700; - line-height: 1; -} - -.worktree-submit-button:hover:not(:disabled), -.worktree-submit-button:focus-visible:not(:disabled) { - background: rgba(136, 192, 208, 0.14); - border-color: var(--color-divider-strong); -} - -.worktree-submit-button:disabled { - cursor: not-allowed; - opacity: 0.48; -} - -.pending-worktree { - position: relative; - display: grid; - gap: 6px; - margin: 0; - padding: 9px 12px 9px var(--session-indent); - border: 0; - border-bottom: 1px solid rgba(59, 66, 82, 0.82); - border-radius: 0; - background: var(--color-surface); -} - -.pending-worktree::before { - content: ''; - position: absolute; - top: 20px; - left: var(--gutter-x); - width: calc(var(--session-indent) - var(--gutter-x) - 10px); - border-top: 1px solid rgba(143, 188, 187, 0.42); - pointer-events: none; -} - -.pending-worktree-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 8px; -} - -.pending-worktree-title { - color: var(--color-text-strong); - font-weight: 600; -} - -.pending-worktree-dismiss { - width: 24px; - min-width: 24px; - height: 24px; - padding: 0; - border-color: transparent; - color: inherit; - font-size: 1rem; - line-height: 1; - opacity: 0.68; -} - -.pending-worktree-dismiss:hover, -.pending-worktree-dismiss:focus-visible { - background: rgba(216, 222, 233, 0.08); - border-color: var(--color-divider); - opacity: 1; -} - -.pending-worktree.partial .pending-worktree-title { - color: var(--color-warn); -} - -.pending-worktree.failed { - color: var(--color-banner-text); -} - -.pending-worktree.failed .pending-worktree-title, -.pending-worktree.failed .pending-worktree-message { - color: inherit; -} - -.pending-worktree-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.pending-worktree-action { - min-height: 28px; - padding: 0.22rem 0.62rem; - border-color: var(--color-divider); - background: rgba(136, 192, 208, 0.08); - font-family: var(--font-mono); - font-size: 0.76rem; - line-height: 1; -} - -.pending-worktree-action.primary { - border-color: var(--color-divider-strong); - background: rgba(136, 192, 208, 0.14); -} - -.pending-worktree-action:hover:not(:disabled), -.pending-worktree-action:focus-visible:not(:disabled) { - background: rgba(136, 192, 208, 0.14); - border-color: var(--color-divider-strong); -} - -.pending-worktree-action:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.card.highlighted { - box-shadow: inset 3px 0 0 var(--color-accent); -} - -.card.highlighted::before { - border-top-color: var(--color-accent); -} diff --git a/apps/session-deck-desktop/web/tauri-host.js b/apps/session-deck-desktop/web/tauri-host.js deleted file mode 100644 index f218a8b0..00000000 --- a/apps/session-deck-desktop/web/tauri-host.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @typedef {{ - * loadSnapshot: () => Promise, - * previewWorktreeBaseRef: (request: { repoIntent: unknown }) => Promise, - * previewWorktreeLaunchContext: (request: { - * launch?: { - * mode: 'tmux-detached', - * agentDir?: { mode: 'ambient' | 'default' } | { mode: 'custom', customDir: string } - * } - * }) => Promise, - * createWorktree: (request: { - * repoIntent: unknown, - * branchName: string, - * baseRef?: string, - * launch?: { - * mode: 'tmux-detached', - * agentDir?: { mode: 'ambient' | 'default' } | { mode: 'custom', customDir: string } - * } - * }) => Promise, - * createSession: (request: { - * action: 'create-session', - * cwd: string, - * launch?: { - * mode: 'tmux-detached', - * agentDir?: { mode: 'ambient' | 'default' } | { mode: 'custom', customDir: string } - * } - * }) => Promise, - * openTerminal: (runtimeId: string) => Promise, - * killSession: (runtimeId: string) => Promise, - * restartSession: (request: { runtimeId: string, generation: string, operationId: string }) => Promise, - * openExternal: (url: string) => Promise<{ ok: boolean, message?: string }>, - * copyText: (text: string) => Promise<{ ok: boolean, message?: string }>, - * doctorCommand: string, - * doctorStatus: () => Promise, - * }} SessionDeckHost - */ - -/** - * @param {Window & typeof globalThis | undefined} [windowLike] - * @returns {(command: string, args?: Record) => Promise} - */ -export function resolveTauriInvoke(windowLike = globalThis.window) { - const tauriWindow = - /** @type {{ __TAURI__?: { core?: { invoke?: (command: string, args?: Record) => Promise } } }} */ ( - windowLike ?? {} - ); - const invoke = tauriWindow.__TAURI__?.core?.invoke; - if (typeof invoke !== 'function') { - throw new Error('Tauri invoke bridge is unavailable. Ensure app.withGlobalTauri is enabled.'); - } - return invoke; -} - -/** - * @param {{ window?: Window & typeof globalThis, doctorCommand?: string }} [options] - * @returns {SessionDeckHost} - */ -export function createTauriSessionDeckHost(options = {}) { - const invoke = resolveTauriInvoke(options.window); - /** - * @param {string} command - * @param {Record | undefined} [args] - */ - const invokeCommand = (command, args) => - (args === undefined ? invoke(command) : invoke(command, args)).catch((rejection) => { - throw normalizeTauriRejection(rejection); - }); - const doctorCommand = - options.doctorCommand ?? 'Open desktop diagnostics or run /session-deck desktop doctor.'; - - return { - loadSnapshot() { - return invokeCommand('load_snapshot'); - }, - previewWorktreeBaseRef(request) { - return invokeCommand('preview_worktree_base_ref', { request }); - }, - previewWorktreeLaunchContext(request) { - return invokeCommand('preview_worktree_launch_context', { request }); - }, - createWorktree(request) { - return invokeCommand('create_worktree', { request }); - }, - createSession(request) { - return invokeCommand('create_session', { request }); - }, - openTerminal(runtimeId) { - return invokeCommand('open_terminal', { request: { runtimeId } }); - }, - killSession(runtimeId) { - return invokeCommand('kill_session', { request: { runtimeId } }); - }, - restartSession(request) { - return invokeCommand('restart_session', { request }); - }, - openExternal(url) { - return /** @type {Promise<{ ok: boolean, message?: string }>} */ ( - invokeCommand('open_external', { url }) - ); - }, - copyText(text) { - return /** @type {Promise<{ ok: boolean, message?: string }>} */ ( - invokeCommand('copy_text', { text }) - ); - }, - doctorCommand, - doctorStatus() { - return invokeCommand('doctor_status'); - }, - }; -} - -/** @param {unknown} rejection */ -function normalizeTauriRejection(rejection) { - if (rejection instanceof Error) { - return rejection; - } - - if (typeof rejection === 'object' && rejection !== null) { - const structured = /** @type {Record} */ (rejection); - const error = new Error( - typeof structured['message'] === 'string' ? structured['message'] : String(rejection), - ); - return Object.assign(error, structured); - } - - return new Error(String(rejection)); -} diff --git a/packages/pi-session-deck/README.md b/packages/pi-session-deck/README.md index e4c792d0..5800cb0f 100644 --- a/packages/pi-session-deck/README.md +++ b/packages/pi-session-deck/README.md @@ -2,7 +2,9 @@ **The full Pi session lifecycle in one place.** -Create and organize Pi sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI, desktop app, or iTerm2 Toolbelt. +Create and organize Pi sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI or iTerm2 Toolbelt. + +> The unsupported Session Deck desktop app has been retired. The `/session-deck desktop ...` commands are no longer available. If you still have the app installed, remove `~/Applications/Session Deck Desktop.app` and, if present, `~/.pi/session-deck/desktop/` manually. The TUI and iTerm2 Toolbelt integrations remain supported. Session Deck running as an iTerm2 Toolbelt beside an active Pi terminal session @@ -24,15 +26,14 @@ Temporary child runtimes stay folded into their parent session, keeping the deck Session Deck iTerm2 Toolbelt showing Pi agents organized across betterby-bike and pi-userland repos -## One deck, three surfaces +## One deck, two surfaces Use Session Deck wherever it fits your workflow: - **Native Pi TUI** for a fast, keyboard-driven view inside Pi. -- **Desktop app** for a dedicated, always-available session window. - **iTerm2 Toolbelt** for an operational sidebar beside your terminals. -Each surface shows the same underlying sessions and gives you the same path through their lifecycle: launch, monitor, reopen, restart, and end. +Both surfaces show the same underlying sessions and give you the same path through their lifecycle: launch, monitor, reopen, restart, and end. ## Launch, reopen, restart, and end @@ -60,16 +61,6 @@ Run inside Pi: /session-deck ``` -### Desktop app - -Install the Session Deck desktop app: - -```text -/session-deck desktop install -``` - -If macOS blocks first launch, leave the app installed at the initial warning, then use **System Settings → Privacy & Security → Open Anyway**. - ### iTerm2 Toolbelt Install Session Deck as an iTerm2 Toolbelt: @@ -92,9 +83,6 @@ Enable the iTerm2 Python API if prompted, fully quit and reopen iTerm2, then ope | `/session-deck iterm2 install` | Install the iTerm2 Toolbelt integration. | | `/session-deck iterm2 doctor` | Diagnose Toolbelt setup and runtime issues. | | `/session-deck iterm2 uninstall` | Remove the iTerm2 Toolbelt integration. | -| `/session-deck desktop install` | Install the desktop app. | -| `/session-deck desktop doctor` | Diagnose the desktop app setup and runtime. | -| `/session-deck desktop uninstall` | Remove the desktop app. | Flags can be combined. @@ -121,4 +109,4 @@ Session Deck observes operational state, not conversation history. - Status chips contain sanitized visible text only. - Tool and assistant errors are reduced to compact, safe summaries. - Managed restart recipes are private user-only files. They contain only the fixed executable/PATH, agent/session directory intent, cwd, exact tmux target, session binding, and process generation needed to restart safely. -- JSON output, restart results, the desktop app, and the Toolbelt omit recipes, session-file paths, commands, PATH, raw terminal metadata, and tmux attachment details. +- JSON output, restart results, and the Toolbelt omit recipes, session-file paths, commands, PATH, raw terminal metadata, and tmux attachment details. diff --git a/packages/pi-session-deck/__tests__/session-deck/desktop-artifact.test.ts b/packages/pi-session-deck/__tests__/session-deck/desktop-artifact.test.ts deleted file mode 100644 index a6f3e853..00000000 --- a/packages/pi-session-deck/__tests__/session-deck/desktop-artifact.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { createHash } from 'node:crypto'; -import { mkdtemp, readFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; -import { - downloadSessionDeckDesktopArtifact, - parseSessionDeckDesktopSha256Sidecar, - resolveSessionDeckDesktopReleaseArtifact, - type SessionDeckDesktopFetch, -} from '../../extensions/session-deck/desktop/artifact.js'; -import { - getSessionDeckDesktopArtifactName, - getSessionDeckDesktopReleaseTag, -} from '../../extensions/session-deck/desktop/paths.js'; - -function okJson(value: unknown): Awaited> { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => value, - text: async () => JSON.stringify(value), - arrayBuffer: async () => toArrayBuffer(Buffer.from(JSON.stringify(value))), - }; -} - -function okText(value: string): Awaited> { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => JSON.parse(value) as unknown, - text: async () => value, - arrayBuffer: async () => toArrayBuffer(Buffer.from(value)), - }; -} - -function okBuffer(value: Buffer): Awaited> { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({}), - text: async () => value.toString('utf8'), - arrayBuffer: async () => toArrayBuffer(value), - }; -} - -function toArrayBuffer(value: Buffer): ArrayBuffer { - return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer; -} - -const FOUR_RELEASE_ASSETS = [ - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip', - browser_download_url: 'https://example.test/app-arm64.zip', - }, - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip.sha256', - browser_download_url: 'https://example.test/app-arm64.zip.sha256', - }, - { - name: 'session-deck-desktop-v0.9.0-macos-x64.zip', - browser_download_url: 'https://example.test/app-x64.zip', - }, - { - name: 'session-deck-desktop-v0.9.0-macos-x64.zip.sha256', - browser_download_url: 'https://example.test/app-x64.zip.sha256', - }, -]; - -describe('session-deck desktop artifacts', () => { - it('uses deterministic release tags and macOS asset names', () => { - expect(getSessionDeckDesktopReleaseTag('0.9.0')).toBe('pi-session-deck-v0.9.0'); - expect(getSessionDeckDesktopArtifactName('0.9.0', { platform: 'darwin', arch: 'arm64' })).toBe( - 'session-deck-desktop-v0.9.0-macos-arm64.zip', - ); - expect(getSessionDeckDesktopArtifactName('0.9.0', { platform: 'darwin', arch: 'x64' })).toBe( - 'session-deck-desktop-v0.9.0-macos-x64.zip', - ); - expect(() => - getSessionDeckDesktopArtifactName('0.9.0', { platform: 'linux', arch: 'x64' }), - ).toThrow('only available for macOS'); - }); - - it.each([ - ['arm64', 'app-arm64.zip'], - ['x64', 'app-x64.zip'], - ] as const)('resolves the %s ZIP and checksum from the four-file release', async (arch, file) => { - const fetch = vi.fn(async () => - okJson({ assets: FOUR_RELEASE_ASSETS }), - ); - - await expect( - resolveSessionDeckDesktopReleaseArtifact({ - version: '0.9.0', - platform: 'darwin', - arch, - fetch, - }), - ).resolves.toEqual({ - releaseTag: 'pi-session-deck-v0.9.0', - assetName: `session-deck-desktop-v0.9.0-macos-${arch}.zip`, - assetUrl: `https://example.test/${file}`, - checksumAssetName: `session-deck-desktop-v0.9.0-macos-${arch}.zip.sha256`, - checksumUrl: `https://example.test/${file}.sha256`, - }); - }); - - it('downloads and verifies checksum sidecars', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-artifact-')); - const artifact = Buffer.from('zip bytes'); - const sha256 = createHash('sha256').update(artifact).digest('hex'); - const fetch = vi.fn(async (url) => { - if (url.includes('/releases/tags/')) { - return okJson({ - assets: [ - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip', - browser_download_url: 'https://example.test/app.zip', - }, - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip.sha256', - browser_download_url: 'https://example.test/app.zip.sha256', - }, - ], - }); - } - if (url.endsWith('.sha256')) { - return okText(`${sha256} session-deck-desktop-v0.9.0-macos-arm64.zip\n`); - } - return okBuffer(artifact); - }); - - const downloaded = await downloadSessionDeckDesktopArtifact({ - version: '0.9.0', - platform: 'darwin', - arch: 'arm64', - fetch, - workDir: root, - }); - - expect(downloaded.sha256).toBe(sha256); - await expect(readFile(downloaded.path, 'utf8')).resolves.toBe('zip bytes'); - }); - - it('rejects downloaded ZIP bytes that do not match the sidecar before returning for extraction', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-artifact-')); - const sidecarSha256 = createHash('sha256').update('expected zip bytes').digest('hex'); - const fetch = vi.fn(async (url) => { - if (url.includes('/releases/tags/')) { - return okJson({ - assets: [ - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip', - browser_download_url: 'https://example.test/app.zip', - }, - { - name: 'session-deck-desktop-v0.9.0-macos-arm64.zip.sha256', - browser_download_url: 'https://example.test/app.zip.sha256', - }, - ], - }); - } - if (url.endsWith('.sha256')) { - return okText(`${sidecarSha256} session-deck-desktop-v0.9.0-macos-arm64.zip\n`); - } - return okBuffer(Buffer.from('different downloaded zip bytes')); - }); - - await expect( - downloadSessionDeckDesktopArtifact({ - version: '0.9.0', - platform: 'darwin', - arch: 'arm64', - fetch, - workDir: root, - }), - ).rejects.toThrow('Checksum mismatch for session-deck-desktop-v0.9.0-macos-arm64.zip'); - }); - - it('rejects malformed checksum sidecars', () => { - expect(() => parseSessionDeckDesktopSha256Sidecar('not-a-sha file.zip', 'file.zip')).toThrow( - 'does not start with a SHA-256 hash', - ); - }); -}); diff --git a/packages/pi-session-deck/__tests__/session-deck/desktop-command.test.ts b/packages/pi-session-deck/__tests__/session-deck/desktop-command.test.ts deleted file mode 100644 index c5dfea92..00000000 --- a/packages/pi-session-deck/__tests__/session-deck/desktop-command.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { - getSessionDeckDesktopCommandCompletions, - isSessionDeckDesktopCommand, - parseSessionDeckDesktopCommandArgs, - runSessionDeckDesktopCommand, - SESSION_DECK_DESKTOP_COMMAND_USAGE, -} from '../../extensions/session-deck/desktop/command.js'; - -const SHA256 = 'a'.repeat(64); - -describe('session-deck desktop command', () => { - it('detects the desktop subcommand without stealing normal flag mode', () => { - expect(isSessionDeckDesktopCommand('desktop')).toBe(true); - expect(isSessionDeckDesktopCommand('desktop install')).toBe(true); - expect(isSessionDeckDesktopCommand(' desktop doctor')).toBe(true); - expect(isSessionDeckDesktopCommand('--all')).toBe(false); - expect(isSessionDeckDesktopCommand('')).toBe(false); - }); - - it('parses install, open, doctor, and uninstall actions', () => { - expect(parseSessionDeckDesktopCommandArgs('desktop install')).toEqual({ - ok: true, - action: 'install', - }); - expect( - parseSessionDeckDesktopCommandArgs( - `desktop install --from-path "/tmp/Session Deck.app" --sha256 ${SHA256}`, - ), - ).toEqual({ - ok: true, - action: 'install', - fromPath: '/tmp/Session Deck.app', - sha256: SHA256, - }); - expect(parseSessionDeckDesktopCommandArgs('desktop install --version 0.9.0')).toEqual({ - ok: true, - action: 'install', - version: '0.9.0', - }); - expect(parseSessionDeckDesktopCommandArgs('desktop open')).toEqual({ - ok: true, - action: 'open', - }); - expect(parseSessionDeckDesktopCommandArgs('desktop doctor')).toEqual({ - ok: true, - action: 'doctor', - }); - expect(parseSessionDeckDesktopCommandArgs('desktop uninstall')).toEqual({ - ok: true, - action: 'uninstall', - }); - }); - - it('returns explicit usage errors for malformed desktop arguments', () => { - expect(parseSessionDeckDesktopCommandArgs('desktop')).toEqual({ - ok: false, - message: `Missing desktop action. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect(parseSessionDeckDesktopCommandArgs('desktop launch')).toEqual({ - ok: false, - message: `Unsupported desktop action: launch. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect(parseSessionDeckDesktopCommandArgs('desktop install --from-path')).toEqual({ - ok: false, - message: `Missing value for --from-path. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect( - parseSessionDeckDesktopCommandArgs('desktop install --from-path /tmp/a --from-path /tmp/b'), - ).toEqual({ - ok: false, - message: `Duplicate flag: --from-path. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect(parseSessionDeckDesktopCommandArgs('desktop install --sha256 abc')).toEqual({ - ok: false, - message: `--sha256 must be a lowercase SHA-256 hash. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect( - parseSessionDeckDesktopCommandArgs('desktop install --from-path /tmp/app --version 0.9.0'), - ).toEqual({ - ok: false, - message: `--from-path cannot be used with --version. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect( - parseSessionDeckDesktopCommandArgs('desktop install --from-path "/tmp/Session Deck.app'), - ).toEqual({ - ok: false, - message: `Unterminated quoted argument. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - expect(parseSessionDeckDesktopCommandArgs('desktop doctor --from-path /tmp/app')).toEqual({ - ok: false, - message: `--from-path is only supported for desktop install. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }); - }); - - it('offers subcommand and install flag completions', () => { - expect(getSessionDeckDesktopCommandCompletions('')).toEqual([ - { value: 'desktop', label: 'desktop' }, - ]); - expect(getSessionDeckDesktopCommandCompletions('des')).toEqual([ - { value: 'desktop', label: 'desktop' }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop ')).toEqual([ - { value: 'desktop install', label: 'install' }, - { value: 'desktop open', label: 'open' }, - { value: 'desktop uninstall', label: 'uninstall' }, - { value: 'desktop doctor', label: 'doctor' }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop d')).toEqual([ - { value: 'desktop doctor', label: 'doctor' }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop install ')).toEqual([ - { value: 'desktop install --from-path', label: '--from-path' }, - { value: 'desktop install --version', label: '--version' }, - { value: 'desktop install --sha256', label: '--sha256' }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop install --from-path')).toBeNull(); - expect( - getSessionDeckDesktopCommandCompletions('desktop install --from-path /tmp/app '), - ).toEqual([ - { - value: 'desktop install --from-path /tmp/app --sha256', - label: '--sha256', - }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop install --version 0.9.0 ')).toEqual([ - { - value: 'desktop install --version 0.9.0 --sha256', - label: '--sha256', - }, - ]); - expect(getSessionDeckDesktopCommandCompletions('desktop open ')).toBeNull(); - expect(getSessionDeckDesktopCommandCompletions('zzz')).toBeNull(); - }); - - it('dispatches to install, open, uninstall, and doctor handlers', async () => { - const install = vi.fn(async () => ({ level: 'info' as const, message: 'installed' })); - const open = vi.fn(async () => ({ level: 'info' as const, message: 'opened' })); - const uninstall = vi.fn(async () => ({ level: 'warning' as const, message: 'uninstalled' })); - const doctor = vi.fn(async () => ({ level: 'error' as const, message: 'doctor' })); - - await expect( - runSessionDeckDesktopCommand(`desktop install --from-path /tmp/app --sha256 ${SHA256}`, { - install, - open, - uninstall, - doctor, - }), - ).resolves.toEqual({ level: 'info', message: 'installed' }); - expect(install).toHaveBeenCalledWith({ fromPath: '/tmp/app', sha256: SHA256 }); - - await expect( - runSessionDeckDesktopCommand('desktop open', { install, open, uninstall, doctor }), - ).resolves.toEqual({ level: 'info', message: 'opened' }); - expect(open).toHaveBeenCalledWith({}); - - await expect( - runSessionDeckDesktopCommand('desktop uninstall', { install, open, uninstall, doctor }), - ).resolves.toEqual({ level: 'warning', message: 'uninstalled' }); - expect(uninstall).toHaveBeenCalledWith({}); - - await expect( - runSessionDeckDesktopCommand('desktop doctor', { install, open, uninstall, doctor }), - ).resolves.toEqual({ level: 'error', message: 'doctor' }); - expect(doctor).toHaveBeenCalledWith({}); - }); -}); diff --git a/packages/pi-session-deck/__tests__/session-deck/desktop-install.test.ts b/packages/pi-session-deck/__tests__/session-deck/desktop-install.test.ts deleted file mode 100644 index c3aaa7a2..00000000 --- a/packages/pi-session-deck/__tests__/session-deck/desktop-install.test.ts +++ /dev/null @@ -1,794 +0,0 @@ -import { rename, readdir, lstat, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { - installSessionDeckDesktop as installSessionDeckDesktopProduction, - type SessionDeckDesktopExecFile, -} from '../../extensions/session-deck/desktop/install.js'; -import type { SessionDeckDesktopFetch } from '../../extensions/session-deck/desktop/artifact.js'; -import { - getDefaultSessionDeckDesktopAppPath, - getSessionDeckDesktopArtifactName, - getSessionDeckDesktopReleaseTag, - getSessionDeckDesktopStatePath, - type SessionDeckDesktopRuntimePaths, -} from '../../extensions/session-deck/desktop/paths.js'; -import { - hashSessionDeckDesktopContent, - hashSessionDeckDesktopPath, - readSessionDeckDesktopInstallState, -} from '../../extensions/session-deck/desktop/state.js'; - -const NOW = new Date('2026-07-17T00:00:00.000Z'); -const RELEASE_VERSION = '0.11.1'; - -async function createFakeApp(path: string, version: string, marker: string): Promise { - await mkdir(join(path, 'Contents', 'MacOS'), { recursive: true }); - await writeFile( - join(path, 'Contents', 'Info.plist'), - ` - - - CFBundleIdentifier - dev.pi-userland.session-deck.desktop - CFBundleDisplayName - Session Deck Desktop - CFBundleShortVersionString - ${version} - CFBundleExecutable - session-deck-desktop - - -`, - ); - await writeFile(join(path, 'Contents', 'MacOS', 'session-deck-desktop'), marker, { - mode: 0o755, - }); -} - -function runtimePaths(root: string, version = RELEASE_VERSION): SessionDeckDesktopRuntimePaths { - return { - packageRoot: root, - packageVersion: version, - nodeExecutablePath: process.execPath, - }; -} - -function createReleaseFetch(options: { - arch: 'arm64' | 'x64'; - archive: Buffer; - version?: string; -}): { fetch: SessionDeckDesktopFetch; assetName: string; requestedUrls: string[] } { - const version = options.version ?? RELEASE_VERSION; - const assetName = getSessionDeckDesktopArtifactName(version, { - arch: options.arch, - platform: 'darwin', - }); - const assetUrl = `https://downloads.test/${assetName}`; - const checksumUrl = `${assetUrl}.sha256`; - const checksum = hashSessionDeckDesktopContent(options.archive); - const requestedUrls: string[] = []; - const fetch: SessionDeckDesktopFetch = async (url) => { - requestedUrls.push(url); - if (url.includes('/releases/tags/')) { - return createFetchResponse({ - json: { - assets: [ - { name: assetName, browser_download_url: assetUrl }, - { name: `${assetName}.sha256`, browser_download_url: checksumUrl }, - ], - }, - }); - } - if (url === checksumUrl) { - return createFetchResponse({ text: `${checksum} ${assetName}\n` }); - } - if (url === assetUrl) { - return createFetchResponse({ bytes: options.archive }); - } - return createFetchResponse({ ok: false, status: 404, statusText: 'Not Found' }); - }; - return { fetch, assetName, requestedUrls }; -} - -function createFetchResponse(options: { - ok?: boolean; - status?: number; - statusText?: string; - json?: unknown; - text?: string; - bytes?: Buffer; -}) { - const bytes = options.bytes ?? Buffer.alloc(0); - return { - ok: options.ok ?? true, - status: options.status ?? 200, - statusText: options.statusText ?? 'OK', - json: async () => options.json, - text: async () => options.text ?? '', - arrayBuffer: async () => - bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer, - }; -} - -function createDittoExtractor(version: string, marker: string): SessionDeckDesktopExecFile { - return (file, args, callback) => { - void (async () => { - expect(file).toBe('/usr/bin/ditto'); - expect(args.slice(0, 2)).toEqual(['-x', '-k']); - await createFakeApp(join(args[3]!, 'Session Deck Desktop.app'), version, marker); - })().then( - () => callback(null), - (error: unknown) => callback(error as Error), - ); - }; -} - -function createQuarantineExecFile(options: { - marker?: string; - quarantine?: Map; - readValue?: (path: string, currentValue: string | undefined) => string | undefined; - version?: string; - writeError?: Error; -}): { - calls: Array<{ file: string; args: string[] }>; - execFile: SessionDeckDesktopExecFile; - quarantine: Map; -} { - const calls: Array<{ file: string; args: string[] }> = []; - const quarantine = options.quarantine ?? new Map(); - const execFile: SessionDeckDesktopExecFile = (file, args, callback) => { - calls.push({ file, args: [...args] }); - if (file === '/usr/bin/ditto') { - void createFakeApp( - join(args[3]!, 'Session Deck Desktop.app'), - options.version ?? RELEASE_VERSION, - options.marker ?? 'downloaded', - ).then( - () => callback(null), - (error: unknown) => callback(error as Error), - ); - return; - } - - if (file !== '/usr/bin/xattr') { - callback(new Error(`Unexpected executable: ${file}`)); - return; - } - - if (args[0] === '-p') { - const path = args[2]!; - const currentValue = quarantine.get(path); - const value = options.readValue?.(path, currentValue) ?? currentValue; - if (value === undefined) { - const error = Object.assign( - new Error(`No such xattr: ${SESSION_DECK_QUARANTINE_ATTRIBUTE}`), - { code: 1 }, - ); - callback( - error, - '', - `xattr: ${path}: No such xattr: ${SESSION_DECK_QUARANTINE_ATTRIBUTE}\n`, - ); - return; - } - callback(null, `${value}\n`); - return; - } - - if (args[0] === '-w') { - if (options.writeError !== undefined) { - callback(options.writeError); - return; - } - quarantine.set(args[3]!, args[2]!); - callback(null); - return; - } - - callback(new Error(`Unexpected xattr arguments: ${args.join(' ')}`)); - }; - - return { calls, execFile, quarantine }; -} - -function installSessionDeckDesktop( - options: Parameters[0] = {}, -): ReturnType { - return installSessionDeckDesktopProduction({ - execFile: createQuarantineExecFile({}).execFile, - ...options, - }); -} - -const SESSION_DECK_QUARANTINE_ATTRIBUTE = 'com.apple.quarantine'; - -async function renameWithQuarantine( - quarantine: Map, - oldPath: string, - newPath: string, -): Promise { - await rename(oldPath, newPath); - const value = quarantine.get(oldPath); - if (value !== undefined) { - quarantine.delete(oldPath); - quarantine.set(newPath, value); - } -} - -async function executableMarker(home: string): Promise { - return readFile( - join(getDefaultSessionDeckDesktopAppPath(home), 'Contents', 'MacOS', 'session-deck-desktop'), - 'utf8', - ); -} - -describe('session-deck desktop install', () => { - it('keeps local artifacts with a differing app version and optional checksum valid', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const sourceApp = join(root, 'Session Deck Desktop.app'); - await createFakeApp(sourceApp, '0.0.0', 'local'); - - const result = await installSessionDeckDesktop({ - fromPath: sourceApp, - homeDirectory: home, - now: () => NOW, - platform: 'darwin', - runtimePaths: runtimePaths(root), - sha256: await hashSessionDeckDesktopPath(sourceApp), - }); - - const targetApp = getDefaultSessionDeckDesktopAppPath(home); - await expect(lstat(targetApp)).resolves.toMatchObject({}); - await expect(executableMarker(home)).resolves.toBe('local'); - const state = await readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)); - expect(result.level).toBe('info'); - expect(result.message).toContain('Installed Session Deck desktop app.'); - expect(result.message).toContain( - 'leave the app installed at the initial warning, then use System Settings → Privacy & Security → Open Anyway.', - ); - expect(state).toMatchObject({ - installedAt: NOW.toISOString(), - packageVersion: RELEASE_VERSION, - app: { - path: targetApp, - version: '0.0.0', - }, - source: { - kind: 'local-path', - path: resolve(sourceApp), - sha256: await hashSessionDeckDesktopPath(sourceApp), - }, - runtime: { - packageRoot: root, - helperPackageVersion: RELEASE_VERSION, - }, - ownedPaths: [targetApp], - }); - }); - - it('marks a GitHub release app before commit and verifies the exact quarantine value', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const archive = Buffer.from('release archive quarantine'); - const release = createReleaseFetch({ arch: 'arm64', archive }); - const quarantine = createQuarantineExecFile({ - marker: 'release', - version: RELEASE_VERSION, - }); - const targetAppPath = getDefaultSessionDeckDesktopAppPath(home); - - const result = await installSessionDeckDesktop({ - arch: 'arm64', - execFile: quarantine.execFile, - fetch: release.fetch, - homeDirectory: home, - now: () => NOW, - platform: 'darwin', - renamePath: (oldPath, newPath) => - renameWithQuarantine(quarantine.quarantine, oldPath, newPath), - runtimePaths: runtimePaths(root), - }); - - const writes = quarantine.calls.filter( - (call) => call.file === '/usr/bin/xattr' && call.args[0] === '-w', - ); - expect(result.level).toBe('info'); - expect(writes).toHaveLength(1); - const value = writes[0]!.args[2]!; - expect(value).toMatch( - new RegExp( - `^0081;${Math.floor(NOW.getTime() / 1000).toString(16)};Session Deck;[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$`, - ), - ); - expect(quarantine.quarantine.get(targetAppPath)).toBe(value); - expect( - quarantine.calls.some( - (call) => - call.file === '/usr/bin/xattr' && - call.args[0] === '-p' && - call.args[1] === SESSION_DECK_QUARANTINE_ATTRIBUTE && - call.args[2] === writes[0]!.args[3], - ), - ).toBe(true); - }); - - it('preserves the old app and state when quarantine marking fails before commit', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const oldApp = join(root, 'old', 'Session Deck Desktop.app'); - await createFakeApp(oldApp, '0.0.0', 'old'); - await installSessionDeckDesktop({ - fromPath: oldApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - const oldState = await readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)); - const release = createReleaseFetch({ arch: 'arm64', archive: Buffer.from('release archive') }); - const quarantine = createQuarantineExecFile({ - version: RELEASE_VERSION, - writeError: new Error('quarantine write blocked'), - }); - - const result = await installSessionDeckDesktop({ - arch: 'arm64', - execFile: quarantine.execFile, - fetch: release.fetch, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain('quarantine write blocked'); - await expect(executableMarker(home)).resolves.toBe('old'); - await expect( - readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)), - ).resolves.toEqual(oldState); - }); - - it('preserves the old app and state when quarantine readback mismatches', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const oldApp = join(root, 'old', 'Session Deck Desktop.app'); - await createFakeApp(oldApp, '0.0.0', 'old'); - await installSessionDeckDesktop({ - fromPath: oldApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - const oldState = await readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)); - const release = createReleaseFetch({ arch: 'arm64', archive: Buffer.from('release archive') }); - const quarantine = createQuarantineExecFile({ - readValue: () => '0081;wrong;Session Deck;WRONG', - version: RELEASE_VERSION, - }); - - const result = await installSessionDeckDesktop({ - arch: 'arm64', - execFile: quarantine.execFile, - fetch: release.fetch, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain('com.apple.quarantine verification failed'); - await expect(executableMarker(home)).resolves.toBe('old'); - await expect( - readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)), - ).resolves.toEqual(oldState); - }); - - it('preserves local quarantine and does not invent it when absent', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const sourceApp = join(root, 'Session Deck Desktop.app'); - await createFakeApp(sourceApp, '0.0.0', 'local'); - const localValue = '0081;6798f6c0;Safari;ABCDEFAB-1234-5678-9ABC-DEF012345678'; - const quarantine = createQuarantineExecFile({ - quarantine: new Map([[resolve(sourceApp), localValue]]), - }); - - const preservedResult = await installSessionDeckDesktop({ - execFile: quarantine.execFile, - fromPath: sourceApp, - homeDirectory: home, - platform: 'darwin', - renamePath: (oldPath, newPath) => - renameWithQuarantine(quarantine.quarantine, oldPath, newPath), - runtimePaths: runtimePaths(root), - }); - - const targetAppPath = getDefaultSessionDeckDesktopAppPath(home); - expect(preservedResult.level).toBe('info'); - expect(quarantine.quarantine.get(targetAppPath)).toBe(localValue); - expect( - quarantine.calls - .filter((call) => call.file === '/usr/bin/xattr' && call.args[0] === '-w') - .map((call) => call.args[2]), - ).toEqual([localValue]); - - const noQuarantineSource = join(root, 'No Quarantine.app'); - const noQuarantineHome = join(root, 'no-quarantine-home'); - await createFakeApp(noQuarantineSource, '0.0.0', 'local-no-quarantine'); - const noQuarantine = createQuarantineExecFile({}); - const noQuarantineResult = await installSessionDeckDesktop({ - execFile: noQuarantine.execFile, - fromPath: noQuarantineSource, - homeDirectory: noQuarantineHome, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(noQuarantineResult.level).toBe('info'); - expect(noQuarantine.quarantine.has(getDefaultSessionDeckDesktopAppPath(noQuarantineHome))).toBe( - false, - ); - expect( - noQuarantine.calls.filter((call) => call.file === '/usr/bin/xattr' && call.args[0] === '-w'), - ).toHaveLength(0); - }); - - it('preserves quarantine from local ZIP and DMG sources', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const localValue = '0081;6798f6c0;Browser;ABCDEFAB-1234-5678-9ABC-DEF012345678'; - - const zipPath = join(root, 'Session Deck Desktop.zip'); - const zipHome = join(root, 'zip-home'); - await writeFile(zipPath, 'zip bytes'); - const zipQuarantine = createQuarantineExecFile({ - quarantine: new Map([[resolve(zipPath), localValue]]), - marker: 'zip', - version: '0.0.0', - }); - const zipResult = await installSessionDeckDesktop({ - execFile: zipQuarantine.execFile, - fromPath: zipPath, - homeDirectory: zipHome, - platform: 'darwin', - renamePath: (oldPath, newPath) => - renameWithQuarantine(zipQuarantine.quarantine, oldPath, newPath), - runtimePaths: runtimePaths(root), - }); - - const dmgPath = join(root, 'Session Deck Desktop.dmg'); - const dmgHome = join(root, 'dmg-home'); - await writeFile(dmgPath, 'dmg bytes'); - const dmgQuarantine = createQuarantineExecFile({ - quarantine: new Map([[resolve(dmgPath), localValue]]), - marker: 'dmg', - version: '0.0.0', - }); - const dmgExecFile: SessionDeckDesktopExecFile = (file, args, callback) => { - if (file !== '/usr/bin/hdiutil') { - dmgQuarantine.execFile(file, args, callback); - return; - } - if (args[0] === 'attach') { - void createFakeApp(join(args[4]!, 'Session Deck Desktop.app'), '0.0.0', 'dmg').then( - () => callback(null), - (error: unknown) => callback(error as Error), - ); - return; - } - callback(null); - }; - const dmgResult = await installSessionDeckDesktop({ - execFile: dmgExecFile, - fromPath: dmgPath, - homeDirectory: dmgHome, - platform: 'darwin', - renamePath: (oldPath, newPath) => - renameWithQuarantine(dmgQuarantine.quarantine, oldPath, newPath), - runtimePaths: runtimePaths(root), - }); - - expect(zipResult.level).toBe('info'); - expect(dmgResult.level).toBe('info'); - expect(zipQuarantine.quarantine.get(getDefaultSessionDeckDesktopAppPath(zipHome))).toBe( - localValue, - ); - expect(dmgQuarantine.quarantine.get(getDefaultSessionDeckDesktopAppPath(dmgHome))).toBe( - localValue, - ); - }); - - it('fails a local quarantine read instead of hiding an unrelated xattr error', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const sourceApp = join(root, 'Session Deck Desktop.app'); - await createFakeApp(sourceApp, '0.0.0', 'local'); - const quarantine = createQuarantineExecFile({ - readValue: () => { - throw new Error('xattr helper failed unexpectedly'); - }, - }); - - const result = await installSessionDeckDesktop({ - execFile: quarantine.execFile, - fromPath: sourceApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain('xattr helper failed unexpectedly'); - }); - - it('fails local checksum verification without writing app or state', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const sourceApp = join(root, 'Session Deck Desktop.app'); - await createFakeApp(sourceApp, '0.0.0', 'local'); - - const result = await installSessionDeckDesktop({ - fromPath: sourceApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - sha256: '0'.repeat(64), - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain('Checksum mismatch'); - await expect(lstat(getDefaultSessionDeckDesktopAppPath(home))).rejects.toMatchObject({ - code: 'ENOENT', - }); - await expect( - readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)), - ).resolves.toBeNull(); - }); - - it('rejects a requested version differing from the running package before fetching or staging', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - let fetched = false; - const fetch: SessionDeckDesktopFetch = async () => { - fetched = true; - throw new Error('unexpected fetch'); - }; - - const result = await installSessionDeckDesktop({ - fetch, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - version: '0.10.0', - }); - - expect(result).toEqual({ - level: 'error', - message: `Requested desktop version 0.10.0 does not match running package version ${RELEASE_VERSION}.`, - }); - expect(fetched).toBe(false); - await expect(lstat(join(home, '.pi'))).rejects.toMatchObject({ code: 'ENOENT' }); - }); - - it('rejects --from-path with --version at the installer boundary', async () => { - const result = await installSessionDeckDesktop({ - fromPath: '/tmp/Session Deck Desktop.app', - platform: 'darwin', - version: RELEASE_VERSION, - }); - - expect(result).toEqual({ - level: 'error', - message: '--from-path and --version cannot be used together.', - }); - }); - - it('rejects a downloaded bundle version mismatch while preserving the old app and state', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const oldApp = join(root, 'old', 'Session Deck Desktop.app'); - await createFakeApp(oldApp, '0.0.0', 'old'); - await installSessionDeckDesktop({ - fromPath: oldApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - const oldState = await readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)); - const release = createReleaseFetch({ arch: 'arm64', archive: Buffer.from('mismatch zip') }); - - const result = await installSessionDeckDesktop({ - arch: 'arm64', - execFile: createDittoExtractor('0.10.0', 'downloaded'), - fetch: release.fetch, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain( - `Downloaded app bundle version 0.10.0 does not match requested version ${RELEASE_VERSION}.`, - ); - await expect(executableMarker(home)).resolves.toBe('old'); - await expect( - readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)), - ).resolves.toEqual(oldState); - }); - - it('restores the previous app and state when the pre-commit state rename fails', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const firstApp = join(root, 'first', 'Session Deck Desktop.app'); - const secondApp = join(root, 'second', 'Session Deck Desktop.app'); - await createFakeApp(firstApp, '0.0.0', 'v1'); - await createFakeApp(secondApp, '0.0.0', 'v2'); - await installSessionDeckDesktop({ - fromPath: firstApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - const statePath = getSessionDeckDesktopStatePath(home); - const oldState = await readSessionDeckDesktopInstallState(statePath); - const result = await installSessionDeckDesktop({ - fromPath: secondApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - renamePath: async (oldPath, newPath) => { - if (newPath === statePath) { - expect((await lstat(oldPath)).mode & 0o777).toBe(0o600); - throw new Error('state commit blocked'); - } - await rename(oldPath, newPath); - }, - }); - - expect(result).toMatchObject({ level: 'error' }); - expect(result.message).toContain('state commit blocked'); - expect(result.message).toContain('Previous app install and state were preserved.'); - await expect(executableMarker(home)).resolves.toBe('v1'); - await expect(readSessionDeckDesktopInstallState(statePath)).resolves.toEqual(oldState); - }); - - it('preserves the original error and reports recovery paths when rollback cleanup fails', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const firstApp = join(root, 'first', 'Session Deck Desktop.app'); - const secondApp = join(root, 'second', 'Session Deck Desktop.app'); - await createFakeApp(firstApp, '0.0.0', 'v1'); - await createFakeApp(secondApp, '0.0.0', 'v2'); - await installSessionDeckDesktop({ - fromPath: firstApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - const targetAppPath = getDefaultSessionDeckDesktopAppPath(home); - const statePath = getSessionDeckDesktopStatePath(home); - const result = await installSessionDeckDesktop({ - fromPath: secondApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - removePath: async (path) => { - if (path === targetAppPath) throw new Error('new app cleanup blocked'); - await rm(path, { force: true, recursive: true }); - }, - renamePath: async (oldPath, newPath) => { - if (newPath === statePath) throw new Error('state commit blocked'); - await rename(oldPath, newPath); - }, - }); - - const backupName = (await readdir(dirname(targetAppPath))).find((name) => - name.endsWith('.previous'), - ); - expect(result).toMatchObject({ level: 'error' }); - expect(result.message.indexOf('state commit blocked')).toBeLessThan( - result.message.indexOf('Rollback failed: new app cleanup blocked'), - ); - expect(result.message).toContain(`app ${targetAppPath}`); - expect(result.message).toContain(`state ${statePath}`); - expect(backupName).toBeDefined(); - expect(result.message).toContain(join(dirname(targetAppPath), backupName!)); - await expect( - readFile( - join(dirname(targetAppPath), backupName!, 'Contents', 'MacOS', 'session-deck-desktop'), - 'utf8', - ), - ).resolves.toBe('v1'); - await expect(executableMarker(home)).resolves.toBe('v2'); - await expect(readSessionDeckDesktopInstallState(statePath)).resolves.toMatchObject({ - source: { path: resolve(firstApp) }, - }); - }); - - it('keeps committed app and state when backup and work directory cleanup fail', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const firstApp = join(root, 'first', 'Session Deck Desktop.app'); - const secondApp = join(root, 'second', 'Session Deck Desktop.app'); - await createFakeApp(firstApp, '0.0.0', 'v1'); - await createFakeApp(secondApp, '0.0.0', 'v2'); - await installSessionDeckDesktop({ - fromPath: firstApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - const result = await installSessionDeckDesktop({ - fromPath: secondApp, - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - removePath: async (path) => { - if (path.endsWith('.previous') || path.includes(join('desktop', 'tmp'))) { - throw new Error('cleanup blocked'); - } - await rm(path, { force: true, recursive: true }); - }, - }); - - expect(result).toMatchObject({ level: 'warning' }); - expect(result.message).toContain('Installed Session Deck desktop app.'); - expect(result.message).toContain('Warning: cleanup left'); - expect(result.message).toContain('.previous'); - expect(result.message).toContain(join('desktop', 'tmp')); - await expect(executableMarker(home)).resolves.toBe('v2'); - await expect( - readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)), - ).resolves.toMatchObject({ source: { path: resolve(secondApp) } }); - }); - - it.each(['arm64', 'x64'] as const)( - 'joins the %s GitHub release download, checksum, ditto extraction, validation, and commit', - async (arch) => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-install-')); - const home = join(root, 'home'); - const archive = Buffer.from(`release archive ${arch}`); - const release = createReleaseFetch({ arch, archive }); - const quarantine = createQuarantineExecFile({ - marker: `release-${arch}`, - version: RELEASE_VERSION, - }); - - const result = await installSessionDeckDesktop({ - arch, - execFile: quarantine.execFile, - fetch: release.fetch, - homeDirectory: home, - now: () => NOW, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - const state = await readSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home)); - expect(result).toMatchObject({ level: 'info' }); - await expect(executableMarker(home)).resolves.toBe(`release-${arch}`); - expect(release.requestedUrls[0]).toContain( - `/releases/tags/${getSessionDeckDesktopReleaseTag(RELEASE_VERSION)}`, - ); - expect(release.requestedUrls.slice(1)).toEqual([ - `https://downloads.test/${release.assetName}.sha256`, - `https://downloads.test/${release.assetName}`, - ]); - expect(state).toMatchObject({ - packageVersion: RELEASE_VERSION, - app: { version: RELEASE_VERSION }, - source: { - kind: 'github-release', - releaseTag: getSessionDeckDesktopReleaseTag(RELEASE_VERSION), - assetName: release.assetName, - url: `https://downloads.test/${release.assetName}`, - sha256: hashSessionDeckDesktopContent(archive), - }, - }); - }, - ); -}); diff --git a/packages/pi-session-deck/__tests__/session-deck/desktop-open-doctor-uninstall.test.ts b/packages/pi-session-deck/__tests__/session-deck/desktop-open-doctor-uninstall.test.ts deleted file mode 100644 index 6a4e56af..00000000 --- a/packages/pi-session-deck/__tests__/session-deck/desktop-open-doctor-uninstall.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { chmod, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; -import { validateSessionDeckDesktopAppBundle } from '../../extensions/session-deck/desktop/bundle.js'; -import { doctorSessionDeckDesktopInstall } from '../../extensions/session-deck/desktop/doctor.js'; -import { openSessionDeckDesktop } from '../../extensions/session-deck/desktop/open.js'; -import { - getDefaultSessionDeckDesktopAppPath, - getSessionDeckDesktopCacheDir, - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - getSessionDeckDesktopStatePath, - getSessionDeckDesktopTmpDir, - type SessionDeckDesktopRuntimePaths, -} from '../../extensions/session-deck/desktop/paths.js'; -import { - hashSessionDeckDesktopPath, - writeSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, -} from '../../extensions/session-deck/desktop/state.js'; -import { uninstallSessionDeckDesktop } from '../../extensions/session-deck/desktop/uninstall.js'; - -const NOW = '2026-07-17T00:00:00.000Z'; - -async function createFakeApp( - path: string, - version = '0.9.0', - executableName: string | null = 'session-deck-desktop', -): Promise { - await mkdir(join(path, 'Contents', 'MacOS'), { recursive: true }); - const executableDeclaration = - executableName === null - ? '' - : ` CFBundleExecutable\n ${executableName}\n`; - await writeFile( - join(path, 'Contents', 'Info.plist'), - ` - - - CFBundleIdentifier - dev.pi-userland.session-deck.desktop - CFBundleName - Session Deck Desktop - CFBundleShortVersionString - ${version} -${executableDeclaration} - -`, - ); - await writeFile(join(path, 'Contents', 'MacOS', 'session-deck-desktop'), 'binary', { - mode: 0o755, - }); -} - -function runtimePaths(root: string): SessionDeckDesktopRuntimePaths { - return { - packageRoot: root, - packageVersion: '0.9.0', - nodeExecutablePath: process.execPath, - }; -} - -async function writeState( - home: string, - root: string, - overrides: Partial = {}, -): Promise { - const appPath = getDefaultSessionDeckDesktopAppPath(home); - const state: SessionDeckDesktopInstallState = { - schemaVersion: 1, - product: 'session-deck-desktop', - packageName: '@robhowley/pi-session-deck', - packageVersion: '0.9.0', - installedAt: NOW, - app: { - path: appPath, - bundleIdentifier: 'dev.pi-userland.session-deck.desktop', - name: 'Session Deck Desktop', - version: '0.9.0', - sha256: await hashSessionDeckDesktopPath(appPath), - }, - source: { - kind: 'local-path', - path: appPath, - sha256: await hashSessionDeckDesktopPath(appPath), - }, - runtime: { - nodeExecutablePath: process.execPath, - packageRoot: root, - helperPackageVersion: '0.9.0', - }, - ownedPaths: [appPath], - ...overrides, - }; - await writeSessionDeckDesktopInstallState(getSessionDeckDesktopStatePath(home), state); - return state; -} - -describe('session-deck desktop open, doctor, and uninstall', () => { - it('opens the installed app through /usr/bin/open argv without shell interpolation', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-open-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - await createFakeApp(appPath); - await writeState(home, root); - const execFile = vi.fn( - (_file: string, _args: string[], callback: (error: Error | null) => void) => { - callback(null); - }, - ); - - const result = await openSessionDeckDesktop({ - execFile, - homeDirectory: home, - platform: 'darwin', - }); - - expect(result).toEqual({ - level: 'info', - message: [ - `Opened Session Deck desktop app: ${appPath}`, - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - ].join('\n'), - }); - expect(execFile).toHaveBeenCalledWith('/usr/bin/open', [appPath], expect.any(Function)); - }); - - it('reports missing install state in doctor without mutating anything', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-doctor-')); - const home = join(root, 'home'); - - const result = await doctorSessionDeckDesktopInstall({ - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result.level).toBe('warning'); - expect(result.message).toContain('Session Deck desktop doctor'); - expect(result.message).toContain( - `Install state not found at ${getSessionDeckDesktopStatePath(home)}`, - ); - }); - - it('reports a valid install as healthy', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-doctor-')); - const home = join(root, 'home'); - await createFakeApp(getDefaultSessionDeckDesktopAppPath(home)); - await writeState(home, root); - - const result = await doctorSessionDeckDesktopInstall({ - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result.level).toBe('info'); - expect(result.message).toContain('Session Deck desktop doctor'); - expect(result.message).not.toContain('Issues:'); - }); - - it.each([ - { - name: 'missing declaration', - declaration: null, - form: 'unchanged', - error: 'missing CFBundleExecutable', - }, - { - name: 'unsafe declaration', - declaration: '../outside', - form: 'unchanged', - error: 'unsafe CFBundleExecutable', - }, - { - name: 'wrong declaration', - declaration: 'not-the-executable', - form: 'unchanged', - error: 'declared executable is missing', - }, - { - name: 'symlink declaration', - declaration: 'linked-executable', - form: 'symlink', - error: 'declared executable must not be a symlink', - }, - { - name: 'directory declaration', - declaration: 'executable-directory', - form: 'directory', - error: 'declared executable is not a regular file', - }, - { - name: 'empty declaration target', - declaration: 'empty-executable', - form: 'empty', - error: 'declared executable is empty', - }, - { - name: 'non-executable declaration target', - declaration: 'session-deck-desktop', - form: 'non-executable', - error: 'declared executable is not executable', - }, - ])('rejects a $name', async ({ declaration, form, error }) => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-bundle-')); - const appPath = join(root, 'Session Deck Desktop.app'); - const macosPath = join(appPath, 'Contents', 'MacOS'); - await createFakeApp(appPath, '0.9.0', declaration); - - if (form === 'symlink') { - await symlink('session-deck-desktop', join(macosPath, declaration!)); - } else if (form === 'directory') { - await mkdir(join(macosPath, declaration!)); - } else if (form === 'empty') { - await writeFile(join(macosPath, declaration!), '', { mode: 0o755 }); - } else if (form === 'non-executable') { - await chmod(join(macosPath, declaration!), 0o644); - } - - await expect(validateSessionDeckDesktopAppBundle(appPath)).rejects.toThrow(error); - }); - - it('reports a declared executable that loses execute permission in doctor', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-doctor-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - const executablePath = join(appPath, 'Contents', 'MacOS', 'session-deck-desktop'); - await createFakeApp(appPath); - await writeState(home, root); - await chmod(executablePath, 0o644); - - const result = await doctorSessionDeckDesktopInstall({ - homeDirectory: home, - platform: 'darwin', - runtimePaths: runtimePaths(root), - }); - - expect(result.level).toBe('warning'); - expect(result.message).toContain( - `Installed app bundle is invalid: App bundle declared executable is not executable: ${executablePath}`, - ); - expect(result.message).not.toContain('Installed app checksum differs from recorded state.'); - }); - - it('uninstalls only safe owned paths and leaves unsafe ownedPaths entries untouched', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-uninstall-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - const outsidePath = join(root, 'do-not-remove.txt'); - await createFakeApp(appPath); - await writeFile(outsidePath, 'keep'); - await writeState(home, root, { ownedPaths: [appPath, outsidePath] }); - - const result = await uninstallSessionDeckDesktop({ homeDirectory: home }); - - expect(result.level).toBe('warning'); - expect(result.message).toContain('Skipped unsafe ownedPaths entries:'); - await expect(lstat(appPath)).rejects.toMatchObject({ code: 'ENOENT' }); - await expect(lstat(getSessionDeckDesktopStatePath(home))).rejects.toMatchObject({ - code: 'ENOENT', - }); - await expect(readFile(outsidePath, 'utf8')).resolves.toBe('keep'); - }); - - it('stops after the first owned-path removal failure and completes on retry', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-uninstall-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - const failedPath = join(getSessionDeckDesktopCacheDir(home), 'failed-cache'); - const pendingPath = join(getSessionDeckDesktopTmpDir(home), 'pending-tmp'); - const outsidePath = join(root, 'do-not-remove.txt'); - await createFakeApp(appPath); - await mkdir(failedPath, { recursive: true }); - await mkdir(pendingPath, { recursive: true }); - await writeFile(outsidePath, 'keep'); - await writeState(home, root, { - ownedPaths: [appPath, failedPath, pendingPath, outsidePath], - }); - const attemptedPaths: string[] = []; - const removePath: typeof rm = async (path, options) => { - attemptedPaths.push(String(path)); - if (String(path) === failedPath) { - throw new Error('simulated removal failure'); - } - await rm(path, options); - }; - - const firstResult = await uninstallSessionDeckDesktop({ homeDirectory: home, removePath }); - - expect(firstResult.level).toBe('warning'); - expect(firstResult.message).toContain(`Removed owned paths:\n- ${appPath}`); - expect(firstResult.message).toContain( - `Failed owned path:\n- ${failedPath}: simulated removal failure`, - ); - expect(firstResult.message).toContain(`Pending owned paths:\n- ${pendingPath}`); - expect(firstResult.message).toContain( - `Install state retained for retry: ${getSessionDeckDesktopStatePath(home)}`, - ); - expect(firstResult.message).toContain(`Skipped unsafe ownedPaths entries:\n- ${outsidePath}`); - expect(attemptedPaths).toEqual([appPath, failedPath]); - await expect(lstat(appPath)).rejects.toMatchObject({ code: 'ENOENT' }); - await expect(lstat(failedPath)).resolves.toMatchObject({}); - await expect(lstat(pendingPath)).resolves.toMatchObject({}); - await expect(lstat(getSessionDeckDesktopStatePath(home))).resolves.toMatchObject({}); - await expect(readFile(outsidePath, 'utf8')).resolves.toBe('keep'); - - const retryResult = await uninstallSessionDeckDesktop({ homeDirectory: home }); - - expect(retryResult.level).toBe('warning'); - await expect(lstat(failedPath)).rejects.toMatchObject({ code: 'ENOENT' }); - await expect(lstat(pendingPath)).rejects.toMatchObject({ code: 'ENOENT' }); - await expect(lstat(getSessionDeckDesktopStatePath(home))).rejects.toMatchObject({ - code: 'ENOENT', - }); - await expect(readFile(outsidePath, 'utf8')).resolves.toBe('keep'); - }); - - it('warns truthfully when only install state removal fails and completes on retry', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-uninstall-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - const statePath = getSessionDeckDesktopStatePath(home); - await createFakeApp(appPath); - await writeState(home, root); - const removePath: typeof rm = async (path, options) => { - if (String(path) === statePath) { - throw new Error('simulated state removal failure'); - } - await rm(path, options); - }; - - const firstResult = await uninstallSessionDeckDesktop({ homeDirectory: home, removePath }); - - expect(firstResult.level).toBe('warning'); - expect(firstResult.message).toContain( - 'Session Deck desktop safe owned-path cleanup completed, but install state removal failed.', - ); - expect(firstResult.message).toContain(`Removed owned paths:\n- ${appPath}`); - expect(firstResult.message).toContain( - `Failed state path:\n- ${statePath}: simulated state removal failure`, - ); - expect(firstResult.message).toContain('Pending owned paths:\n- (none)'); - await expect(lstat(appPath)).rejects.toMatchObject({ code: 'ENOENT' }); - await expect(lstat(statePath)).resolves.toMatchObject({}); - - const retryResult = await uninstallSessionDeckDesktop({ homeDirectory: home }); - - expect(retryResult.level).toBe('info'); - await expect(lstat(statePath)).rejects.toMatchObject({ code: 'ENOENT' }); - }); - - it('does not remove the app when uninstall metadata is invalid', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-uninstall-')); - const home = join(root, 'home'); - const appPath = getDefaultSessionDeckDesktopAppPath(home); - await createFakeApp(appPath); - await mkdir(join(home, '.pi', 'session-deck', 'desktop'), { recursive: true }); - await writeFile(getSessionDeckDesktopStatePath(home), '{"not":"valid"}\n'); - - const result = await uninstallSessionDeckDesktop({ homeDirectory: home }); - - expect(result.level).toBe('warning'); - expect(result.message).toContain( - 'Nothing was removed because app ownership could not be verified.', - ); - await expect(lstat(appPath)).resolves.toMatchObject({}); - }); -}); diff --git a/packages/pi-session-deck/__tests__/session-deck/desktop-state.test.ts b/packages/pi-session-deck/__tests__/session-deck/desktop-state.test.ts deleted file mode 100644 index b1ff34df..00000000 --- a/packages/pi-session-deck/__tests__/session-deck/desktop-state.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { chmod, mkdtemp, readFile, stat } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { - parseSessionDeckDesktopInstallState, - readSessionDeckDesktopInstallState, - writeSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, -} from '../../extensions/session-deck/desktop/state.js'; -import { - SESSION_DECK_ITERM2_CREATE_WORKTREE_HELPER_RELATIVE_PATH, - SESSION_DECK_ITERM2_HELPER_RELATIVE_PATH, - SESSION_DECK_ITERM2_KILL_HELPER_RELATIVE_PATH, - SESSION_DECK_ITERM2_OPEN_HELPER_RELATIVE_PATH, - SESSION_DECK_ITERM2_WEB_ROOT_RELATIVE_PATH, -} from '../../extensions/session-deck/iterm2/paths.js'; - -const SHA256 = 'b'.repeat(64); - -function buildState( - overrides: Partial = {}, -): SessionDeckDesktopInstallState { - const appPath = '/Users/test/Applications/Session Deck Desktop.app'; - return { - schemaVersion: 1, - product: 'session-deck-desktop', - packageName: '@robhowley/pi-session-deck', - packageVersion: '0.9.0', - installedAt: '2026-07-17T00:00:00.000Z', - app: { - path: appPath, - bundleIdentifier: 'dev.pi-userland.session-deck.desktop', - name: 'Session Deck Desktop', - version: '0.9.0', - sha256: SHA256, - }, - source: { - kind: 'local-path', - path: '/tmp/Session Deck Desktop.app', - sha256: SHA256, - }, - runtime: { - nodeExecutablePath: '/usr/local/bin/node', - packageRoot: '/tmp/pi-session-deck', - helperPackageVersion: '0.9.0', - }, - ownedPaths: [appPath], - ...overrides, - }; -} - -describe('session-deck desktop state', () => { - it('round-trips install state atomically', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-state-')); - const statePath = join(root, 'desktop', 'install.json'); - const state = buildState(); - - await writeSessionDeckDesktopInstallState(statePath, state); - await chmod(statePath, 0o644); - await writeSessionDeckDesktopInstallState(statePath, state); - - await expect(readSessionDeckDesktopInstallState(statePath)).resolves.toEqual(state); - expect((await stat(statePath)).mode & 0o777).toBe(0o600); - }); - - it('returns null for missing state', async () => { - const root = await mkdtemp(join(tmpdir(), 'pi-session-deck-desktop-state-')); - await expect( - readSessionDeckDesktopInstallState(join(root, 'missing.json')), - ).resolves.toBeNull(); - }); - - it('rejects invalid schema and unowned app path', () => { - expect(() => - parseSessionDeckDesktopInstallState({ ...buildState(), schemaVersion: 2 }), - ).toThrow('State has an invalid shape.'); - expect(() => - parseSessionDeckDesktopInstallState({ ...buildState(), ownedPaths: ['/tmp/other'] }), - ).toThrow('State does not record the app path as owned.'); - }); - - it('parses GitHub release source state', () => { - const state = buildState({ - source: { - kind: 'github-release', - releaseTag: 'pi-session-deck-v0.9.0', - assetName: 'session-deck-desktop-v0.9.0-macos-arm64.zip', - url: 'https://example.test/asset.zip', - sha256: SHA256, - }, - }); - - expect(parseSessionDeckDesktopInstallState(state).source).toEqual(state.source); - }); - - it('matches the shared desktop runtime layout without persisting derived helper paths', async () => { - const fixture = JSON.parse( - await readFile( - new URL( - '../../../../apps/session-deck-desktop/fixtures/runtime-layout-v1.json', - import.meta.url, - ), - 'utf8', - ), - ) as { - schemaVersion: number; - snapshotHelperRelativePath: string; - openActionHelperRelativePath: string; - killActionHelperRelativePath: string; - worktreeActionHelperRelativePath: string; - webRootRelativePath: string; - }; - const relativeLayout = { - schemaVersion: 1, - snapshotHelperRelativePath: SESSION_DECK_ITERM2_HELPER_RELATIVE_PATH, - openActionHelperRelativePath: SESSION_DECK_ITERM2_OPEN_HELPER_RELATIVE_PATH, - killActionHelperRelativePath: SESSION_DECK_ITERM2_KILL_HELPER_RELATIVE_PATH, - worktreeActionHelperRelativePath: SESSION_DECK_ITERM2_CREATE_WORKTREE_HELPER_RELATIVE_PATH, - webRootRelativePath: SESSION_DECK_ITERM2_WEB_ROOT_RELATIVE_PATH, - }; - expect(relativeLayout).toEqual(fixture); - - const state = parseSessionDeckDesktopInstallState(buildState()); - expect({ - snapshotHelperPath: join( - state.runtime.packageRoot, - relativeLayout.snapshotHelperRelativePath, - ), - openActionHelperPath: join( - state.runtime.packageRoot, - relativeLayout.openActionHelperRelativePath, - ), - killActionHelperPath: join( - state.runtime.packageRoot, - relativeLayout.killActionHelperRelativePath, - ), - worktreeActionHelperPath: join( - state.runtime.packageRoot, - relativeLayout.worktreeActionHelperRelativePath, - ), - webRootPath: join(state.runtime.packageRoot, relativeLayout.webRootRelativePath), - }).toEqual({ - snapshotHelperPath: - '/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/snapshot-cli.js', - openActionHelperPath: - '/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/open-action-cli.js', - killActionHelperPath: - '/tmp/pi-session-deck/dist/extensions/session-deck/iterm2/kill-action-cli.js', - worktreeActionHelperPath: - '/tmp/pi-session-deck/dist/extensions/session-deck/worktree/action-cli.js', - webRootPath: '/tmp/pi-session-deck/extensions/session-deck/iterm2/web', - }); - expect(Object.keys(state.runtime).sort()).toEqual([ - 'helperPackageVersion', - 'nodeExecutablePath', - 'packageRoot', - ]); - }); -}); diff --git a/packages/pi-session-deck/__tests__/session-deck/identity-command.test.ts b/packages/pi-session-deck/__tests__/session-deck/identity-command.test.ts index 48c3c279..da1cfa87 100644 --- a/packages/pi-session-deck/__tests__/session-deck/identity-command.test.ts +++ b/packages/pi-session-deck/__tests__/session-deck/identity-command.test.ts @@ -206,7 +206,7 @@ describe('session-deck joined command', () => { }); }); - it('offers --all, --reap, --identity, --json, --session-id, desktop, and iterm2 completions', () => { + it('offers flags and iterm2 completions', () => { const { api, getRegistration } = createMockAPI(); registerSessionDeckCommand(api, { @@ -219,15 +219,8 @@ describe('session-deck joined command', () => { { value: '--identity', label: '--identity' }, { value: '--json', label: '--json' }, { value: '--session-id', label: '--session-id' }, - { value: 'desktop', label: 'desktop' }, { value: 'iterm2', label: 'iterm2' }, ]); - expect(getRegistration()?.getArgumentCompletions?.('desktop ')).toEqual([ - { value: 'desktop install', label: 'install' }, - { value: 'desktop open', label: 'open' }, - { value: 'desktop uninstall', label: 'uninstall' }, - { value: 'desktop doctor', label: 'doctor' }, - ]); expect(getRegistration()?.getArgumentCompletions?.('iterm2 ')).toEqual([ { value: 'iterm2 install', label: 'install' }, { value: 'iterm2 uninstall', label: 'uninstall' }, @@ -235,28 +228,6 @@ describe('session-deck joined command', () => { ]); }); - it('routes /session-deck desktop ... through the dedicated app flow before flag parsing', async () => { - const { api, getHandler } = createMockAPI(); - const runSessionDeckDesktopCommand = vi.fn(async () => ({ - level: 'info' as const, - message: 'opened desktop', - })); - - registerSessionDeckCommand(api, { - isSessionDeckDesktopCommand: (args) => args.startsWith('desktop'), - readSessionDeckSnapshot: vi.fn(async () => buildSnapshot()), - runSessionDeckDesktopCommand, - }); - - const handler = getHandler(); - const ctx = createCommandContext({ mode: 'rpc' }); - - await handler?.('desktop open', ctx); - - expect(runSessionDeckDesktopCommand).toHaveBeenCalledWith('desktop open'); - expect(vi.mocked(ctx.ui.notify)).toHaveBeenCalledWith('opened desktop', 'info'); - }); - it('routes /session-deck iterm2 ... through the dedicated installer flow before flag parsing', async () => { const { api, getHandler } = createMockAPI(); const runSessionDeckIterm2Command = vi.fn(async () => ({ diff --git a/packages/pi-session-deck/__tests__/session-deck/iterm2-web-ui.test.ts b/packages/pi-session-deck/__tests__/session-deck/iterm2-web-ui.test.ts index 5db43ea4..f861fb09 100644 --- a/packages/pi-session-deck/__tests__/session-deck/iterm2-web-ui.test.ts +++ b/packages/pi-session-deck/__tests__/session-deck/iterm2-web-ui.test.ts @@ -4234,7 +4234,7 @@ describe('Session Deck iTerm2 web UI', () => { let snapshotCount = 0; const timeout = Object.assign( new Error( - 'The desktop helper timed out before Session Deck could confirm whether the action completed.', + 'The action helper timed out before Session Deck could confirm whether the action completed.', ), { outcomeUnknown: true }, ); diff --git a/packages/pi-session-deck/__tests__/session-deck/session-deck-ui.test.ts b/packages/pi-session-deck/__tests__/session-deck/session-deck-ui.test.ts index fb2116b4..4ae0bda6 100644 --- a/packages/pi-session-deck/__tests__/session-deck/session-deck-ui.test.ts +++ b/packages/pi-session-deck/__tests__/session-deck/session-deck-ui.test.ts @@ -597,7 +597,7 @@ function getButtonByText(root: FakeNode, text: string): FakeButtonElement { function helperTimeoutError(): Error & { code: string; outcomeUnknown: true } { return Object.assign( new Error( - 'The desktop helper timed out before Session Deck could confirm whether the action completed.', + 'The action helper timed out before Session Deck could confirm whether the action completed.', ), { code: 'mutating-helper-timeout', outcomeUnknown: true as const }, ); diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/artifact.ts b/packages/pi-session-deck/extensions/session-deck/desktop/artifact.ts deleted file mode 100644 index dca9773f..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/artifact.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { hashSessionDeckDesktopPath } from './state.js'; -import { - SESSION_DECK_DESKTOP_RELEASE_OWNER, - SESSION_DECK_DESKTOP_RELEASE_REPO, - getSessionDeckDesktopArtifactName, - getSessionDeckDesktopReleaseTag, -} from './paths.js'; - -export interface SessionDeckDesktopReleaseAsset { - name: string; - url: string; -} - -export interface SessionDeckDesktopResolvedArtifact { - releaseTag: string; - assetName: string; - assetUrl: string; - checksumAssetName: string; - checksumUrl: string; -} - -export interface SessionDeckDesktopDownloadedArtifact extends SessionDeckDesktopResolvedArtifact { - path: string; - sha256: string; -} - -export type SessionDeckDesktopFetch = ( - url: string, - init?: { headers?: Record }, -) => Promise<{ - ok: boolean; - status: number; - statusText: string; - json: () => Promise; - text: () => Promise; - arrayBuffer: () => Promise; -}>; - -export async function resolveSessionDeckDesktopReleaseArtifact(options: { - version: string; - platform?: NodeJS.Platform; - arch?: NodeJS.Architecture; - fetch?: SessionDeckDesktopFetch; - apiBaseUrl?: string; -}): Promise { - const fetchImpl = getFetch(options.fetch); - const releaseTag = getSessionDeckDesktopReleaseTag(options.version); - const assetName = getSessionDeckDesktopArtifactName(options.version, { - ...(options.platform === undefined ? {} : { platform: options.platform }), - ...(options.arch === undefined ? {} : { arch: options.arch }), - }); - const checksumAssetName = `${assetName}.sha256`; - const releaseUrl = `${options.apiBaseUrl ?? 'https://api.github.com'}/repos/${SESSION_DECK_DESKTOP_RELEASE_OWNER}/${SESSION_DECK_DESKTOP_RELEASE_REPO}/releases/tags/${encodeURIComponent(releaseTag)}`; - const response = await fetchImpl(releaseUrl, { - headers: { Accept: 'application/vnd.github+json' }, - }); - - if (!response.ok) { - throw new Error( - `Could not query GitHub Release ${releaseTag}: HTTP ${response.status} ${response.statusText}`, - ); - } - - const release = await response.json(); - const assets = parseReleaseAssets(release); - const artifactAsset = assets.find((asset) => asset.name === assetName); - const checksumAsset = assets.find((asset) => asset.name === checksumAssetName); - - if (artifactAsset === undefined) { - throw new Error(`GitHub Release ${releaseTag} does not include ${assetName}.`); - } - - if (checksumAsset === undefined) { - throw new Error(`GitHub Release ${releaseTag} does not include ${checksumAssetName}.`); - } - - return { - releaseTag, - assetName, - assetUrl: artifactAsset.url, - checksumAssetName, - checksumUrl: checksumAsset.url, - }; -} - -export async function downloadSessionDeckDesktopArtifact(options: { - version: string; - workDir: string; - platform?: NodeJS.Platform; - arch?: NodeJS.Architecture; - fetch?: SessionDeckDesktopFetch; - apiBaseUrl?: string; - expectedSha256?: string; -}): Promise { - const fetchImpl = getFetch(options.fetch); - const resolved = await resolveSessionDeckDesktopReleaseArtifact({ - version: options.version, - ...(options.platform === undefined ? {} : { platform: options.platform }), - ...(options.arch === undefined ? {} : { arch: options.arch }), - fetch: fetchImpl, - ...(options.apiBaseUrl === undefined ? {} : { apiBaseUrl: options.apiBaseUrl }), - }); - - await mkdir(options.workDir, { recursive: true, mode: 0o700 }); - const checksumResponse = await fetchImpl(resolved.checksumUrl); - if (!checksumResponse.ok) { - throw new Error( - `Could not download checksum ${resolved.checksumAssetName}: HTTP ${checksumResponse.status} ${checksumResponse.statusText}`, - ); - } - const sha256 = parseSessionDeckDesktopSha256Sidecar( - await checksumResponse.text(), - resolved.assetName, - ); - if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256) { - throw new Error( - `Checksum sidecar for ${resolved.assetName} is ${sha256}, not requested ${options.expectedSha256}.`, - ); - } - - const artifactResponse = await fetchImpl(resolved.assetUrl); - if (!artifactResponse.ok) { - throw new Error( - `Could not download artifact ${resolved.assetName}: HTTP ${artifactResponse.status} ${artifactResponse.statusText}`, - ); - } - - const artifactPath = join(options.workDir, resolved.assetName); - await writeFile(artifactPath, Buffer.from(await artifactResponse.arrayBuffer()), { mode: 0o600 }); - const actualSha256 = await hashSessionDeckDesktopPath(artifactPath); - if (actualSha256 !== sha256) { - throw new Error( - `Checksum mismatch for ${resolved.assetName}: expected ${sha256}, got ${actualSha256}.`, - ); - } - - return { - ...resolved, - path: artifactPath, - sha256, - }; -} - -export function parseSessionDeckDesktopSha256Sidecar(text: string, assetName: string): string { - const tokens = text - .trim() - .split(/\s+/u) - .filter((token) => token.length > 0); - const checksum = tokens[0]?.toLowerCase(); - if (checksum === undefined || !/^[a-f0-9]{64}$/u.test(checksum)) { - throw new Error(`Checksum sidecar for ${assetName} does not start with a SHA-256 hash.`); - } - return checksum; -} - -function parseReleaseAssets(candidate: unknown): SessionDeckDesktopReleaseAsset[] { - if (!isRecord(candidate) || !Array.isArray(candidate['assets'])) { - throw new Error('GitHub Release response has an invalid assets shape.'); - } - - const assets: SessionDeckDesktopReleaseAsset[] = []; - for (const asset of candidate['assets']) { - if (!isRecord(asset)) { - continue; - } - - const name = asset['name']; - const url = asset['browser_download_url']; - if (typeof name === 'string' && typeof url === 'string') { - assets.push({ name, url }); - } - } - - return assets; -} - -function getFetch(fetchImpl: SessionDeckDesktopFetch | undefined): SessionDeckDesktopFetch { - if (fetchImpl !== undefined) { - return fetchImpl; - } - - if (typeof globalThis.fetch !== 'function') { - throw new Error('This Node runtime does not provide fetch; cannot download desktop artifacts.'); - } - - return globalThis.fetch as SessionDeckDesktopFetch; -} - -function isRecord(candidate: unknown): candidate is Record { - return typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate); -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/bundle.ts b/packages/pi-session-deck/extensions/session-deck/desktop/bundle.ts deleted file mode 100644 index e96fa598..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/bundle.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { constants } from 'node:fs'; -import { access, lstat, readFile, readdir } from 'node:fs/promises'; -import { basename, extname, join } from 'node:path'; -import { SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER } from './paths.js'; - -export interface SessionDeckDesktopBundleMetadata { - path: string; - bundleIdentifier: typeof SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER; - name: string; - version: string; -} - -export async function validateSessionDeckDesktopAppBundle( - appPath: string, - options: { - expectedBundleIdentifier?: typeof SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER; - } = {}, -): Promise { - const expectedBundleIdentifier = - options.expectedBundleIdentifier ?? SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER; - const appStat = await lstat(appPath); - if (!appStat.isDirectory() || extname(appPath) !== '.app') { - throw new Error(`Expected a macOS .app bundle directory: ${appPath}`); - } - - const contentsPath = join(appPath, 'Contents'); - const macosPath = join(contentsPath, 'MacOS'); - const infoPlistPath = join(contentsPath, 'Info.plist'); - - if (!(await pathIsDirectory(contentsPath))) { - throw new Error(`App bundle is missing Contents directory: ${contentsPath}`); - } - - if (!(await pathIsDirectory(macosPath))) { - throw new Error(`App bundle is missing Contents/MacOS directory: ${macosPath}`); - } - - const plist = await readFile(infoPlistPath, 'utf8'); - const executableName = readPlistString(plist, 'CFBundleExecutable'); - if (executableName === null) { - throw new Error(`App bundle Info.plist is missing CFBundleExecutable: ${infoPlistPath}`); - } - if (!isSafeExecutableName(executableName)) { - throw new Error( - `App bundle Info.plist has unsafe CFBundleExecutable ${JSON.stringify(executableName)}: ${infoPlistPath}`, - ); - } - await validateExecutable(join(macosPath, executableName)); - - const bundleIdentifier = readPlistString(plist, 'CFBundleIdentifier'); - if (bundleIdentifier !== expectedBundleIdentifier) { - throw new Error( - bundleIdentifier === null - ? `App bundle Info.plist is missing CFBundleIdentifier: ${infoPlistPath}` - : `App bundle identifier ${bundleIdentifier} does not match expected ${expectedBundleIdentifier}.`, - ); - } - - const version = - readPlistString(plist, 'CFBundleShortVersionString') ?? - readPlistString(plist, 'CFBundleVersion'); - if (version === null || version.trim().length === 0) { - throw new Error(`App bundle Info.plist is missing a version string: ${infoPlistPath}`); - } - - const name = - readPlistString(plist, 'CFBundleDisplayName') ?? - readPlistString(plist, 'CFBundleName') ?? - basename(appPath, '.app'); - - return { - path: appPath, - bundleIdentifier: SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER, - name, - version, - }; -} - -function isSafeExecutableName(value: string): boolean { - return ( - value.length > 0 && - value !== '.' && - value !== '..' && - !value.includes('\0') && - basename(value) === value - ); -} - -async function validateExecutable(path: string): Promise { - const pathStat = await lstat(path).catch((error: unknown) => { - if (isMissingFileError(error)) { - throw new Error(`App bundle declared executable is missing: ${path}`); - } - throw error; - }); - - if (pathStat.isSymbolicLink()) { - throw new Error(`App bundle declared executable must not be a symlink: ${path}`); - } - if (!pathStat.isFile()) { - throw new Error(`App bundle declared executable is not a regular file: ${path}`); - } - if (pathStat.size === 0) { - throw new Error(`App bundle declared executable is empty: ${path}`); - } - - try { - await access(path, constants.X_OK); - } catch { - throw new Error(`App bundle declared executable is not executable: ${path}`); - } -} - -function readPlistString(plist: string, key: string): string | null { - const pattern = new RegExp( - `\\s*${escapeRegExp(key)}\\s*\\s*([\\s\\S]*?)`, - 'u', - ); - const match = plist.match(pattern); - const value = match?.[1]; - return value === undefined ? null : decodeXmlEntities(value.trim()); -} - -function decodeXmlEntities(value: string): string { - return value - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('&', '&'); -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); -} - -async function pathIsDirectory(path: string): Promise { - try { - const pathStat = await lstat(path); - return pathStat.isDirectory(); - } catch (error) { - if (isMissingFileError(error)) { - return false; - } - throw error; - } -} - -async function pathExists(path: string): Promise { - try { - await access(path); - return true; - } catch { - return false; - } -} - -export async function findSessionDeckDesktopAppBundle(rootPath: string): Promise { - const candidates = await findAppBundles(rootPath); - const sessionDeckCandidate = candidates.find( - (candidate) => basename(candidate) === 'Session Deck Desktop.app', - ); - if (sessionDeckCandidate !== undefined) { - return sessionDeckCandidate; - } - - if (candidates.length === 1) { - return candidates[0]!; - } - - if (candidates.length === 0) { - throw new Error(`No .app bundle found in ${rootPath}.`); - } - - throw new Error(`Multiple .app bundles found in ${rootPath}; expected Session Deck Desktop.app.`); -} - -async function findAppBundles(rootPath: string): Promise { - if (!(await pathExists(rootPath))) { - throw new Error(`Artifact extraction path does not exist: ${rootPath}`); - } - - const rootStat = await lstat(rootPath); - if (!rootStat.isDirectory()) { - return extname(rootPath) === '.app' ? [rootPath] : []; - } - - if (extname(rootPath) === '.app') { - return [rootPath]; - } - - const matches: string[] = []; - const entries = (await readdir(rootPath, { withFileTypes: true })).sort((left, right) => - left.name.localeCompare(right.name), - ); - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - - const childPath = join(rootPath, entry.name); - if (extname(childPath) === '.app') { - matches.push(childPath); - continue; - } - - matches.push(...(await findAppBundles(childPath))); - } - - return matches; -} - -function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && 'code' in error && error.code === 'ENOENT'; -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/command.ts b/packages/pi-session-deck/extensions/session-deck/desktop/command.ts deleted file mode 100644 index 9900dd77..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/command.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { doctorSessionDeckDesktopInstall } from './doctor.js'; -import { installSessionDeckDesktop } from './install.js'; -import { openSessionDeckDesktop } from './open.js'; -import { uninstallSessionDeckDesktop } from './uninstall.js'; -import { - SESSION_DECK_DESKTOP_DOCTOR_ACTION, - SESSION_DECK_DESKTOP_FROM_PATH_FLAG, - SESSION_DECK_DESKTOP_INSTALL_ACTION, - SESSION_DECK_DESKTOP_OPEN_ACTION, - SESSION_DECK_DESKTOP_SHA256_FLAG, - SESSION_DECK_DESKTOP_SUBCOMMAND, - SESSION_DECK_DESKTOP_UNINSTALL_ACTION, - SESSION_DECK_DESKTOP_VERSION_FLAG, -} from './paths.js'; - -export const SESSION_DECK_DESKTOP_COMMAND_USAGE = - 'Usage: /session-deck desktop install [--from-path | --version ] [--sha256 ] | /session-deck desktop '; - -export interface SessionDeckDesktopCommandResult { - level: 'info' | 'warning' | 'error'; - message: string; -} - -export interface RunSessionDeckDesktopCommandOptions { - doctor?: typeof doctorSessionDeckDesktopInstall; - install?: typeof installSessionDeckDesktop; - open?: typeof openSessionDeckDesktop; - uninstall?: typeof uninstallSessionDeckDesktop; -} - -export type ParsedSessionDeckDesktopCommandArgs = - | { - ok: true; - action: 'install'; - fromPath?: string; - version?: string; - sha256?: string; - } - | { - ok: true; - action: 'open' | 'uninstall' | 'doctor'; - } - | { - ok: false; - message: string; - }; - -const SESSION_DECK_DESKTOP_ACTIONS = [ - SESSION_DECK_DESKTOP_INSTALL_ACTION, - SESSION_DECK_DESKTOP_OPEN_ACTION, - SESSION_DECK_DESKTOP_UNINSTALL_ACTION, - SESSION_DECK_DESKTOP_DOCTOR_ACTION, -] as const; -const SESSION_DECK_DESKTOP_INSTALL_FLAGS = [ - SESSION_DECK_DESKTOP_FROM_PATH_FLAG, - SESSION_DECK_DESKTOP_VERSION_FLAG, - SESSION_DECK_DESKTOP_SHA256_FLAG, -] as const; - -export function isSessionDeckDesktopCommand(args: string): boolean { - const trimmedArgs = args.trim(); - return ( - trimmedArgs === SESSION_DECK_DESKTOP_SUBCOMMAND || - trimmedArgs.startsWith(`${SESSION_DECK_DESKTOP_SUBCOMMAND} `) - ); -} - -export async function runSessionDeckDesktopCommand( - args: string, - options: RunSessionDeckDesktopCommandOptions = {}, -): Promise { - const parsedArgs = parseSessionDeckDesktopCommandArgs(args); - if (!parsedArgs.ok) { - return { level: 'error', message: parsedArgs.message }; - } - - switch (parsedArgs.action) { - case 'install': - return (options.install ?? installSessionDeckDesktop)({ - ...(parsedArgs.fromPath === undefined ? {} : { fromPath: parsedArgs.fromPath }), - ...(parsedArgs.version === undefined ? {} : { version: parsedArgs.version }), - ...(parsedArgs.sha256 === undefined ? {} : { sha256: parsedArgs.sha256 }), - }); - case 'open': - return (options.open ?? openSessionDeckDesktop)({}); - case 'uninstall': - return (options.uninstall ?? uninstallSessionDeckDesktop)({}); - case 'doctor': - return (options.doctor ?? doctorSessionDeckDesktopInstall)({}); - } -} - -export function parseSessionDeckDesktopCommandArgs( - args: string, -): ParsedSessionDeckDesktopCommandArgs { - const tokenizedArgs = tokenizeSessionDeckDesktopCommandArgs(args); - if (!tokenizedArgs.ok) { - return createUsageError(tokenizedArgs.message); - } - const tokens = tokenizedArgs.tokens; - - if (tokens[0] !== SESSION_DECK_DESKTOP_SUBCOMMAND) { - return createUsageError(`Unsupported argument: ${tokens[0] ?? ''}`); - } - - const actionToken = tokens[1]; - if ( - !SESSION_DECK_DESKTOP_ACTIONS.includes( - actionToken as (typeof SESSION_DECK_DESKTOP_ACTIONS)[number], - ) - ) { - return createUsageError( - actionToken === undefined - ? 'Missing desktop action' - : `Unsupported desktop action: ${actionToken}`, - ); - } - - if (actionToken !== SESSION_DECK_DESKTOP_INSTALL_ACTION) { - const extraToken = tokens[2]; - if (extraToken !== undefined) { - return createUsageError( - (SESSION_DECK_DESKTOP_INSTALL_FLAGS as readonly string[]).includes(extraToken) - ? `${extraToken} is only supported for desktop install` - : `Unsupported argument: ${extraToken}`, - ); - } - - return { - ok: true, - action: actionToken as 'open' | 'uninstall' | 'doctor', - }; - } - - const parsedFlags = parseInstallFlags(tokens.slice(2)); - if (!parsedFlags.ok) { - return parsedFlags; - } - - return { - ok: true, - action: SESSION_DECK_DESKTOP_INSTALL_ACTION, - ...(parsedFlags.fromPath === undefined ? {} : { fromPath: parsedFlags.fromPath }), - ...(parsedFlags.version === undefined ? {} : { version: parsedFlags.version }), - ...(parsedFlags.sha256 === undefined ? {} : { sha256: parsedFlags.sha256 }), - }; -} - -export function getSessionDeckDesktopCommandCompletions(prefix: string) { - const trimmedPrefix = prefix.trimStart(); - if (trimmedPrefix.length === 0) { - return [{ value: SESSION_DECK_DESKTOP_SUBCOMMAND, label: SESSION_DECK_DESKTOP_SUBCOMMAND }]; - } - - if (!trimmedPrefix.startsWith(SESSION_DECK_DESKTOP_SUBCOMMAND)) { - const matches = [SESSION_DECK_DESKTOP_SUBCOMMAND] - .filter((value) => value.startsWith(trimmedPrefix)) - .map((value) => ({ value, label: value })); - return matches.length > 0 ? matches : null; - } - - const remainder = trimmedPrefix.slice(SESSION_DECK_DESKTOP_SUBCOMMAND.length).trimStart(); - if (remainder.length === 0) { - return SESSION_DECK_DESKTOP_ACTIONS.map((value) => ({ - value: `${SESSION_DECK_DESKTOP_SUBCOMMAND} ${value}`, - label: value, - })); - } - - const segments = remainder.split(/\s+/).filter((token) => token.length > 0); - if (segments.length === 1 && !remainder.endsWith(' ')) { - const matches = SESSION_DECK_DESKTOP_ACTIONS.filter((value) => - value.startsWith(segments[0]!), - ).map((value) => ({ - value: `${SESSION_DECK_DESKTOP_SUBCOMMAND} ${value}`, - label: value, - })); - return matches.length > 0 ? matches : null; - } - - if (segments[0] !== SESSION_DECK_DESKTOP_INSTALL_ACTION) { - return null; - } - - const flagPrefix = remainder.endsWith(' ') ? '' : (segments[segments.length - 1] ?? ''); - if (SESSION_DECK_DESKTOP_INSTALL_FLAGS.some((flag) => flagPrefix === flag)) { - return null; - } - - const usedFlags = new Set( - segments.filter((segment): segment is (typeof SESSION_DECK_DESKTOP_INSTALL_FLAGS)[number] => - (SESSION_DECK_DESKTOP_INSTALL_FLAGS as readonly string[]).includes(segment), - ), - ); - const matches = SESSION_DECK_DESKTOP_INSTALL_FLAGS.filter( - (value) => - !usedFlags.has(value) && - !( - usedFlags.has(SESSION_DECK_DESKTOP_FROM_PATH_FLAG) && - value === SESSION_DECK_DESKTOP_VERSION_FLAG - ) && - !( - usedFlags.has(SESSION_DECK_DESKTOP_VERSION_FLAG) && - value === SESSION_DECK_DESKTOP_FROM_PATH_FLAG - ) && - value.startsWith(flagPrefix), - ).map((value) => ({ - value: `${trimmedPrefix.replace(/\s+$/u, '')} ${value}`.trim(), - label: value, - })); - return matches.length > 0 ? matches : null; -} - -function parseInstallFlags( - tokens: string[], -): - | Extract - | { ok: false; message: string } { - let fromPath: string | undefined; - let version: string | undefined; - let sha256: string | undefined; - - for (let index = 0; index < tokens.length; index += 1) { - const token = tokens[index]!; - if (!(SESSION_DECK_DESKTOP_INSTALL_FLAGS as readonly string[]).includes(token)) { - return createUsageError(`Unsupported argument: ${token}`); - } - - if (token === SESSION_DECK_DESKTOP_FROM_PATH_FLAG) { - if (fromPath !== undefined) { - return createUsageError(`Duplicate flag: ${SESSION_DECK_DESKTOP_FROM_PATH_FLAG}`); - } - const value = tokens[index + 1]; - if (value === undefined || value.length === 0 || value.startsWith('--')) { - return createUsageError(`Missing value for ${SESSION_DECK_DESKTOP_FROM_PATH_FLAG}`); - } - fromPath = value; - index += 1; - continue; - } - - if (token === SESSION_DECK_DESKTOP_VERSION_FLAG) { - if (version !== undefined) { - return createUsageError(`Duplicate flag: ${SESSION_DECK_DESKTOP_VERSION_FLAG}`); - } - const value = tokens[index + 1]; - if (value === undefined || value.length === 0 || value.startsWith('--')) { - return createUsageError(`Missing value for ${SESSION_DECK_DESKTOP_VERSION_FLAG}`); - } - version = value; - index += 1; - continue; - } - - if (sha256 !== undefined) { - return createUsageError(`Duplicate flag: ${SESSION_DECK_DESKTOP_SHA256_FLAG}`); - } - const value = tokens[index + 1]; - if (value === undefined || value.length === 0 || value.startsWith('--')) { - return createUsageError(`Missing value for ${SESSION_DECK_DESKTOP_SHA256_FLAG}`); - } - if (!/^[a-f0-9]{64}$/u.test(value)) { - return createUsageError( - `${SESSION_DECK_DESKTOP_SHA256_FLAG} must be a lowercase SHA-256 hash`, - ); - } - sha256 = value; - index += 1; - } - - if (fromPath !== undefined && version !== undefined) { - return createUsageError( - `${SESSION_DECK_DESKTOP_FROM_PATH_FLAG} cannot be used with ${SESSION_DECK_DESKTOP_VERSION_FLAG}`, - ); - } - - return { - ok: true, - action: SESSION_DECK_DESKTOP_INSTALL_ACTION, - ...(fromPath === undefined ? {} : { fromPath }), - ...(version === undefined ? {} : { version }), - ...(sha256 === undefined ? {} : { sha256 }), - }; -} - -function tokenizeSessionDeckDesktopCommandArgs( - args: string, -): { ok: true; tokens: string[] } | { ok: false; message: string } { - const tokens: string[] = []; - let currentToken = ''; - let tokenStarted = false; - let quote: '"' | "'" | null = null; - let escaping = false; - - for (const character of args.trim()) { - if (escaping) { - currentToken += character; - tokenStarted = true; - escaping = false; - continue; - } - - if (character === '\\') { - escaping = true; - tokenStarted = true; - continue; - } - - if (quote !== null) { - if (character === quote) { - quote = null; - continue; - } - currentToken += character; - tokenStarted = true; - continue; - } - - if (character === '"' || character === "'") { - quote = character; - tokenStarted = true; - continue; - } - - if (/\s/u.test(character)) { - if (tokenStarted) { - tokens.push(currentToken); - currentToken = ''; - tokenStarted = false; - } - continue; - } - - currentToken += character; - tokenStarted = true; - } - - if (escaping) { - currentToken += '\\'; - } - - if (quote !== null) { - return { ok: false, message: 'Unterminated quoted argument' }; - } - - if (tokenStarted) { - tokens.push(currentToken); - } - - return { ok: true, tokens }; -} - -function createUsageError(message: string): { ok: false; message: string } { - return { - ok: false, - message: `${message}. ${SESSION_DECK_DESKTOP_COMMAND_USAGE}`, - }; -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/doctor.ts b/packages/pi-session-deck/extensions/session-deck/desktop/doctor.ts deleted file mode 100644 index c7707534..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/doctor.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { constants } from 'node:fs'; -import { access, lstat } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { validateSessionDeckDesktopAppBundle } from './bundle.js'; -import { - getSessionDeckDesktopStatePath, - resolveSessionDeckDesktopRuntimePaths, - type SessionDeckDesktopRuntimePaths, -} from './paths.js'; -import { - hashSessionDeckDesktopPath, - readSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, -} from './state.js'; -import type { SessionDeckDesktopCommandResult } from './command.js'; - -export interface DoctorSessionDeckDesktopOptions { - homeDirectory?: string; - platform?: NodeJS.Platform; - runtimePaths?: SessionDeckDesktopRuntimePaths; - statePath?: string; -} - -export async function doctorSessionDeckDesktopInstall( - options: DoctorSessionDeckDesktopOptions = {}, -): Promise { - const homeDirectory = options.homeDirectory ?? homedir(); - const statePath = options.statePath ?? getSessionDeckDesktopStatePath(homeDirectory); - let state: SessionDeckDesktopInstallState | null = null; - let stateReadError: string | null = null; - try { - state = await readSessionDeckDesktopInstallState(statePath); - } catch (error) { - stateReadError = getErrorMessage(error); - } - - const lines = ['Session Deck desktop doctor']; - const issues: string[] = []; - const platform = options.platform ?? process.platform; - lines.push(`- platform: ${platform}`); - if (platform !== 'darwin') { - issues.push('Session Deck desktop app support is macOS-only.'); - } - - lines.push( - `- state: ${stateReadError === null ? (state === null ? 'missing' : statePath) : `invalid (${statePath})`}`, - ); - - if (stateReadError !== null) { - issues.push(`Install state at ${statePath} could not be read: ${stateReadError}`); - issues.push( - 'Manual recovery required: remove or repair the state file and verify/remove any Session Deck desktop app manually.', - ); - } else if (state === null) { - issues.push(`Install state not found at ${statePath}. Run /session-deck desktop install.`); - } else { - await checkInstalledApp(state, lines, issues); - appendSource(lines, state); - } - - let runtimePaths: SessionDeckDesktopRuntimePaths | null = options.runtimePaths ?? null; - if (runtimePaths === null) { - try { - runtimePaths = await resolveSessionDeckDesktopRuntimePaths(import.meta.url); - } catch (error) { - issues.push(`Could not resolve current package runtime paths: ${getErrorMessage(error)}`); - } - } - - if (runtimePaths !== null) { - await checkRuntimePaths(runtimePaths, state, lines, issues); - } - - if (issues.length > 0) { - lines.push(''); - lines.push('Issues:'); - for (const issue of issues) { - lines.push(`- ${issue}`); - } - } - - return { - level: issues.length === 0 ? 'info' : 'warning', - message: lines.join('\n'), - }; -} - -async function checkInstalledApp( - state: SessionDeckDesktopInstallState, - lines: string[], - issues: string[], -): Promise { - const appExists = await pathIsDirectory(state.app.path); - lines.push(`- app: ${state.app.path}${appExists ? '' : ' (missing)'}`); - lines.push(`- bundle id: ${state.app.bundleIdentifier}`); - lines.push(`- app version: ${state.app.version}`); - - if (!appExists) { - issues.push(`Installed app is missing: ${state.app.path}`); - return; - } - - try { - const bundle = await validateSessionDeckDesktopAppBundle(state.app.path); - if (bundle.bundleIdentifier !== state.app.bundleIdentifier) { - issues.push( - `Installed app bundle identifier ${bundle.bundleIdentifier} does not match recorded ${state.app.bundleIdentifier}.`, - ); - } - if (bundle.version !== state.app.version) { - issues.push( - `Installed app version ${bundle.version} does not match recorded ${state.app.version}.`, - ); - } - } catch (error) { - issues.push(`Installed app bundle is invalid: ${getErrorMessage(error)}`); - } - - try { - const installedSha256 = await hashSessionDeckDesktopPath(state.app.path); - lines.push(`- app sha256: ${installedSha256}`); - if (installedSha256 !== state.app.sha256) { - issues.push('Installed app checksum differs from recorded state. Reinstall recommended.'); - } - } catch (error) { - issues.push(`Installed app checksum could not be calculated: ${getErrorMessage(error)}`); - } -} - -async function checkRuntimePaths( - runtimePaths: SessionDeckDesktopRuntimePaths, - state: SessionDeckDesktopInstallState | null, - lines: string[], - issues: string[], -): Promise { - lines.push(`- package root: ${runtimePaths.packageRoot}`); - lines.push(`- helper package version: ${runtimePaths.packageVersion}`); - lines.push(`- node executable: ${runtimePaths.nodeExecutablePath}`); - - if (!(await pathIsDirectory(runtimePaths.packageRoot))) { - issues.push(`Package root is missing: ${runtimePaths.packageRoot}`); - } - - try { - await access(runtimePaths.nodeExecutablePath, constants.X_OK); - } catch { - issues.push(`Node executable is not available/executable: ${runtimePaths.nodeExecutablePath}`); - } - - if (state !== null) { - if (state.packageVersion !== runtimePaths.packageVersion) { - issues.push( - `Installed state package version ${state.packageVersion} does not match current package version ${runtimePaths.packageVersion}. Reinstall recommended.`, - ); - } - if (state.runtime.helperPackageVersion !== runtimePaths.packageVersion) { - issues.push( - `Installed helper package version ${state.runtime.helperPackageVersion} does not match current package version ${runtimePaths.packageVersion}. Reinstall recommended.`, - ); - } - if (state.runtime.packageRoot !== runtimePaths.packageRoot) { - issues.push('Package root changed since install. Reinstall recommended.'); - } - if (state.runtime.nodeExecutablePath !== runtimePaths.nodeExecutablePath) { - issues.push('Node executable path changed since install. Reinstall recommended.'); - } - } -} - -function appendSource(lines: string[], state: SessionDeckDesktopInstallState): void { - if (state.source.kind === 'local-path') { - lines.push(`- source: local path ${state.source.path}`); - return; - } - - lines.push(`- source: GitHub Release ${state.source.releaseTag} asset ${state.source.assetName}`); -} - -async function pathIsDirectory(path: string): Promise { - try { - const pathStat = await lstat(path); - return pathStat.isDirectory(); - } catch (error) { - if (isMissingFileError(error)) { - return false; - } - throw error; - } -} - -function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && 'code' in error && error.code === 'ENOENT'; -} - -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/install.ts b/packages/pi-session-deck/extensions/session-deck/desktop/install.ts deleted file mode 100644 index c4e261a9..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/install.ts +++ /dev/null @@ -1,690 +0,0 @@ -import { randomUUID } from 'node:crypto'; -import { execFile as nodeExecFile } from 'node:child_process'; -import { chmod, copyFile, lstat, mkdir, readdir, realpath, rename, rm } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { basename, dirname, extname, join, resolve } from 'node:path'; -import { downloadSessionDeckDesktopArtifact, type SessionDeckDesktopFetch } from './artifact.js'; -import { findSessionDeckDesktopAppBundle, validateSessionDeckDesktopAppBundle } from './bundle.js'; -import { - getDefaultSessionDeckDesktopAppPath, - getSessionDeckDesktopStatePath, - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - getSessionDeckDesktopTmpDir, - resolveSessionDeckDesktopRuntimePaths, - SESSION_DECK_DESKTOP_APP_BUNDLE_NAME, - SESSION_DECK_DESKTOP_PACKAGE_NAME, - type SessionDeckDesktopRuntimePaths, -} from './paths.js'; -import { - hashSessionDeckDesktopPath, - readSessionDeckDesktopInstallState, - stageSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, - type SessionDeckDesktopSourceState, -} from './state.js'; -import type { SessionDeckDesktopCommandResult } from './command.js'; - -export type SessionDeckDesktopExecFile = ( - file: string, - args: string[], - callback: (error: Error | null, stdout?: string, stderr?: string) => void, -) => void; - -export interface InstallSessionDeckDesktopOptions { - arch?: NodeJS.Architecture; - destinationAppPath?: string; - execFile?: SessionDeckDesktopExecFile; - fetch?: SessionDeckDesktopFetch; - fromPath?: string; - homeDirectory?: string; - now?: () => Date; - platform?: NodeJS.Platform; - runtimePaths?: SessionDeckDesktopRuntimePaths; - sha256?: string; - statePath?: string; - version?: string; - removePath?: (path: string) => Promise; - renamePath?: (oldPath: string, newPath: string) => Promise; -} - -interface PreparedDesktopArtifact { - appPath: string; - rootQuarantine: string | null; - source: SessionDeckDesktopSourceState; -} - -interface ExtractedDmgArtifact { - appPath: string; - rootQuarantine: string | null; -} - -const SESSION_DECK_DESKTOP_XATTR_PATH = '/usr/bin/xattr'; -const SESSION_DECK_DESKTOP_QUARANTINE_ATTRIBUTE = 'com.apple.quarantine'; - -export async function installSessionDeckDesktop( - options: InstallSessionDeckDesktopOptions = {}, -): Promise { - const platform = options.platform ?? process.platform; - if (platform !== 'darwin') { - return { - level: 'error', - message: `Session Deck desktop install is only supported on macOS, not ${platform}.`, - }; - } - - if (options.fromPath !== undefined && options.version !== undefined) { - return { - level: 'error', - message: '--from-path and --version cannot be used together.', - }; - } - - const homeDirectory = options.homeDirectory ?? homedir(); - const statePath = options.statePath ?? getSessionDeckDesktopStatePath(homeDirectory); - const targetAppPath = - options.destinationAppPath ?? getDefaultSessionDeckDesktopAppPath(homeDirectory); - let existingState: SessionDeckDesktopInstallState | null; - try { - existingState = await readSessionDeckDesktopInstallState(statePath); - } catch (error) { - return { - level: 'error', - message: [ - 'Could not install Session Deck desktop app.', - `State file at ${statePath} is invalid: ${getErrorMessage(error)}`, - 'Remove or repair the state file and verify/remove any existing Session Deck desktop app manually before installing.', - ].join('\n'), - }; - } - - if (existingState !== null && existingState.app.path !== targetAppPath) { - return { - level: 'error', - message: [ - 'Could not install Session Deck desktop app.', - `Existing state owns ${existingState.app.path}.`, - `Requested install target is ${targetAppPath}.`, - 'Run /session-deck desktop uninstall first, or reinstall to the same managed app path.', - ].join('\n'), - }; - } - - if ((await pathExists(targetAppPath)) && existingState === null) { - return { - level: 'error', - message: [ - 'Could not install Session Deck desktop app.', - `App target already exists and is not owned by Session Deck state: ${targetAppPath}`, - 'Nothing was overwritten. Move or verify the existing app manually, then rerun /session-deck desktop install.', - ].join('\n'), - }; - } - - let runtimePaths: SessionDeckDesktopRuntimePaths; - try { - runtimePaths = - options.runtimePaths ?? (await resolveSessionDeckDesktopRuntimePaths(import.meta.url)); - } catch (error) { - return { - level: 'error', - message: [ - 'Could not install Session Deck desktop app.', - `Could not resolve the current @robhowley/pi-session-deck runtime: ${getErrorMessage(error)}`, - ].join('\n'), - }; - } - - if (options.version !== undefined && options.version !== runtimePaths.packageVersion) { - return { - level: 'error', - message: `Requested desktop version ${options.version} does not match running package version ${runtimePaths.packageVersion}.`, - }; - } - - const installId = randomUUID(); - const workDir = join(getSessionDeckDesktopTmpDir(homeDirectory), installId); - const stagedAppPath = join( - dirname(targetAppPath), - `.${SESSION_DECK_DESKTOP_APP_BUNDLE_NAME}.${process.pid}.${installId}.tmp`, - ); - const removePath = options.removePath ?? removeInstallPath; - const renamePath = options.renamePath ?? rename; - const execFile = options.execFile ?? nodeExecFileAdapter; - const cleanupWarnings: string[] = []; - - try { - await mkdir(workDir, { recursive: true, mode: 0o700 }); - const prepared = - options.fromPath === undefined - ? await prepareDownloadedArtifact({ - ...(options.arch === undefined ? {} : { arch: options.arch }), - execFile, - ...(options.sha256 === undefined ? {} : { expectedSha256: options.sha256 }), - ...(options.fetch === undefined ? {} : { fetch: options.fetch }), - platform, - runtimePaths, - workDir, - }) - : await prepareLocalArtifact({ - execFile, - ...(options.sha256 === undefined ? {} : { expectedSha256: options.sha256 }), - fromPath: options.fromPath, - platform, - workDir, - }); - - const bundle = await validateSessionDeckDesktopAppBundle(prepared.appPath); - if ( - prepared.source.kind === 'github-release' && - bundle.version !== runtimePaths.packageVersion - ) { - throw new Error( - `Downloaded app bundle version ${bundle.version} does not match requested version ${runtimePaths.packageVersion}.`, - ); - } - - await mkdir(dirname(stagedAppPath), { recursive: true }); - await copyAppBundle(prepared.appPath, stagedAppPath); - const installedSha256 = await hashSessionDeckDesktopPath(stagedAppPath); - const installedAt = (options.now ?? (() => new Date()))(); - await applyRootQuarantine({ - appPath: stagedAppPath, - execFile, - installedAt, - rootQuarantine: prepared.rootQuarantine, - source: prepared.source, - }); - const state: SessionDeckDesktopInstallState = { - schemaVersion: 1, - product: 'session-deck-desktop', - packageName: SESSION_DECK_DESKTOP_PACKAGE_NAME, - packageVersion: runtimePaths.packageVersion, - installedAt: installedAt.toISOString(), - app: { - path: targetAppPath, - bundleIdentifier: bundle.bundleIdentifier, - name: bundle.name, - version: bundle.version, - sha256: installedSha256, - }, - source: prepared.source, - runtime: { - nodeExecutablePath: runtimePaths.nodeExecutablePath, - packageRoot: runtimePaths.packageRoot, - helperPackageVersion: runtimePaths.packageVersion, - }, - ownedPaths: [targetAppPath], - }; - - cleanupWarnings.push( - ...(await commitManagedAppInstall({ - state, - statePath, - stagedAppPath, - targetAppPath, - removePath, - renamePath, - })), - ); - const workDirWarning = await removeInstallPathWithWarning(workDir, removePath); - if (workDirWarning !== null) cleanupWarnings.push(workDirWarning); - - return { - level: cleanupWarnings.length === 0 ? 'info' : 'warning', - message: [ - 'Installed Session Deck desktop app.', - `App: ${targetAppPath}`, - `State: ${statePath}`, - `Source: ${formatSource(prepared.source)}`, - ...cleanupWarnings, - 'Next: double-click Session Deck Desktop in Applications, or run /session-deck desktop open.', - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - 'For diagnostics, run /session-deck desktop doctor.', - ].join('\n'), - }; - } catch (error) { - for (const path of [stagedAppPath, workDir]) { - const warning = await removeInstallPathWithWarning(path, removePath); - if (warning !== null) cleanupWarnings.push(warning); - } - return { - level: 'error', - message: [ - 'Could not install Session Deck desktop app.', - getErrorMessage(error), - ...cleanupWarnings, - ].join('\n'), - }; - } -} - -async function prepareDownloadedArtifact(options: { - arch?: NodeJS.Architecture; - execFile?: SessionDeckDesktopExecFile; - expectedSha256?: string; - fetch?: SessionDeckDesktopFetch; - platform: NodeJS.Platform; - runtimePaths: SessionDeckDesktopRuntimePaths; - workDir: string; -}): Promise { - const downloaded = await downloadSessionDeckDesktopArtifact({ - version: options.runtimePaths.packageVersion, - workDir: options.workDir, - platform: options.platform, - ...(options.arch === undefined ? {} : { arch: options.arch }), - ...(options.fetch === undefined ? {} : { fetch: options.fetch }), - ...(options.expectedSha256 === undefined ? {} : { expectedSha256: options.expectedSha256 }), - }); - const appPath = await extractZipArtifact(downloaded.path, options.workDir, { - execFile: options.execFile ?? nodeExecFileAdapter, - platform: options.platform, - }); - - return { - appPath, - rootQuarantine: null, - source: { - kind: 'github-release', - releaseTag: downloaded.releaseTag, - assetName: downloaded.assetName, - url: downloaded.assetUrl, - sha256: downloaded.sha256, - }, - }; -} - -async function prepareLocalArtifact(options: { - execFile?: SessionDeckDesktopExecFile; - expectedSha256?: string; - fromPath: string; - platform: NodeJS.Platform; - workDir: string; -}): Promise { - const sourcePath = resolve(options.fromPath); - const sourceSha256 = await hashSessionDeckDesktopPath(sourcePath); - verifyExpectedSha256(sourceSha256, options.expectedSha256, sourcePath); - - const execFile = options.execFile ?? nodeExecFileAdapter; - const sourceQuarantine = await readQuarantineAttribute(execFile, sourcePath); - const source: SessionDeckDesktopSourceState = { - kind: 'local-path', - path: sourcePath, - sha256: sourceSha256, - }; - const sourceStat = await lstat(sourcePath); - if (sourceStat.isDirectory() && extname(sourcePath) === '.app') { - return { appPath: sourcePath, rootQuarantine: sourceQuarantine, source }; - } - - if (!sourceStat.isFile()) { - throw new Error(`Unsupported local desktop artifact type: ${sourcePath}`); - } - - const extension = extname(sourcePath).toLowerCase(); - if (extension === '.zip') { - const appPath = await extractZipArtifact(sourcePath, options.workDir, { - execFile, - platform: options.platform, - }); - return { - appPath, - rootQuarantine: sourceQuarantine ?? (await readQuarantineAttribute(execFile, appPath)), - source, - }; - } - - if (extension === '.dmg') { - const extracted = await extractDmgArtifact(sourcePath, options.workDir, { - execFile, - platform: options.platform, - }); - return { - appPath: extracted.appPath, - rootQuarantine: sourceQuarantine ?? extracted.rootQuarantine, - source, - }; - } - - throw new Error( - `Unsupported local desktop artifact extension ${extension || ''}: ${sourcePath}`, - ); -} - -async function extractZipArtifact( - zipPath: string, - workDir: string, - options: { execFile: SessionDeckDesktopExecFile; platform: NodeJS.Platform }, -): Promise { - if (options.platform !== 'darwin') { - throw new Error( - 'Installing Session Deck desktop .zip artifacts requires macOS /usr/bin/ditto.', - ); - } - - const extractDir = join(workDir, 'zip-extract'); - await mkdir(extractDir, { recursive: true, mode: 0o700 }); - await execFilePromise(options.execFile, '/usr/bin/ditto', ['-x', '-k', zipPath, extractDir]); - return findSessionDeckDesktopAppBundle(extractDir); -} - -async function extractDmgArtifact( - dmgPath: string, - workDir: string, - options: { execFile: SessionDeckDesktopExecFile; platform: NodeJS.Platform }, -): Promise { - if (options.platform !== 'darwin') { - throw new Error( - 'Installing Session Deck desktop .dmg artifacts requires macOS /usr/bin/hdiutil.', - ); - } - - const mountDir = join(workDir, 'dmg-mount'); - const extractedDir = join(workDir, 'dmg-extract'); - await mkdir(mountDir, { recursive: true, mode: 0o700 }); - await mkdir(extractedDir, { recursive: true, mode: 0o700 }); - let mounted = false; - try { - await execFilePromise(options.execFile, '/usr/bin/hdiutil', [ - 'attach', - '-nobrowse', - '-readonly', - '-mountpoint', - mountDir, - dmgPath, - ]); - mounted = true; - const mountedApp = await findSessionDeckDesktopAppBundle(mountDir); - const rootQuarantine = await readQuarantineAttribute(options.execFile, mountedApp); - const extractedApp = join(extractedDir, basename(mountedApp)); - await copyAppBundle(mountedApp, extractedApp); - return { appPath: extractedApp, rootQuarantine }; - } finally { - if (mounted) { - await execFilePromise(options.execFile, '/usr/bin/hdiutil', ['detach', mountDir]).catch( - () => undefined, - ); - } - } -} - -async function applyRootQuarantine(options: { - appPath: string; - execFile: SessionDeckDesktopExecFile; - installedAt: Date; - rootQuarantine: string | null; - source: SessionDeckDesktopSourceState; -}): Promise { - const quarantine = - options.source.kind === 'github-release' - ? formatGitHubReleaseQuarantine(options.installedAt) - : options.rootQuarantine; - if (quarantine === null) return; - - await writeAndVerifyQuarantine(options.execFile, options.appPath, quarantine); -} - -function formatGitHubReleaseQuarantine(installedAt: Date): string { - const unixTimestamp = Math.floor(installedAt.getTime() / 1000); - if (!Number.isSafeInteger(unixTimestamp) || unixTimestamp < 0) { - throw new Error('Install clock must produce a non-negative Unix timestamp.'); - } - - return `0081;${unixTimestamp.toString(16).toLowerCase()};Session Deck;${randomUUID().toUpperCase()}`; -} - -async function writeAndVerifyQuarantine( - execFile: SessionDeckDesktopExecFile, - appPath: string, - expectedValue: string, -): Promise { - await execFilePromise(execFile, SESSION_DECK_DESKTOP_XATTR_PATH, [ - '-w', - SESSION_DECK_DESKTOP_QUARANTINE_ATTRIBUTE, - expectedValue, - appPath, - ]); - const actualValue = await readQuarantineAttribute(execFile, appPath); - if (actualValue !== expectedValue) { - throw new Error( - `com.apple.quarantine verification failed for ${appPath}: expected ${expectedValue}, got ${actualValue ?? ''}.`, - ); - } -} - -async function readQuarantineAttribute( - execFile: SessionDeckDesktopExecFile, - appPath: string, -): Promise { - return new Promise((resolvePromise, reject) => { - execFile( - SESSION_DECK_DESKTOP_XATTR_PATH, - ['-p', SESSION_DECK_DESKTOP_QUARANTINE_ATTRIBUTE, appPath], - (error, stdout, stderr) => { - if (error !== null) { - if (isMissingQuarantineAttributeError(error, stderr)) { - resolvePromise(null); - return; - } - reject(error); - return; - } - - resolvePromise(stripXattrTrailingNewline(stdout ?? '')); - }, - ); - }); -} - -function stripXattrTrailingNewline(value: string): string { - return value.replace(/\r?\n$/u, ''); -} - -function isMissingQuarantineAttributeError(error: unknown, stderr: string | undefined): boolean { - if (!(error instanceof Error) || !('code' in error) || error.code !== 1) return false; - - const diagnostic = `${stderr ?? ''}\n${error.message}`; - return /No such xattr:\s+com\.apple\.quarantine/u.test(diagnostic); -} - -async function commitManagedAppInstall(options: { - state: SessionDeckDesktopInstallState; - statePath: string; - stagedAppPath: string; - targetAppPath: string; - removePath: (path: string) => Promise; - renamePath: (oldPath: string, newPath: string) => Promise; -}): Promise { - await mkdir(dirname(options.targetAppPath), { recursive: true }); - const hadPreviousApp = await pathExists(options.targetAppPath); - const tempStatePath = await stageSessionDeckDesktopInstallState(options.statePath, options.state); - const previousAppPath = join( - dirname(options.targetAppPath), - `.${basename(options.targetAppPath)}.${process.pid}.${randomUUID()}.previous`, - ); - let movedPreviousApp = false; - let installedTarget = false; - - try { - if (hadPreviousApp) { - await options.renamePath(options.targetAppPath, previousAppPath); - movedPreviousApp = true; - } - await options.renamePath(options.stagedAppPath, options.targetAppPath); - installedTarget = true; - - // This atomic rename is the install commit point. The app and state stay installed after it. - await options.renamePath(tempStatePath, options.statePath); - } catch (error) { - const rollbackMessage = await rollbackManagedAppInstall({ - installedTarget, - movedPreviousApp, - previousAppPath, - statePath: options.statePath, - targetAppPath: options.targetAppPath, - removePath: options.removePath, - renamePath: options.renamePath, - }); - const stateWarning = await removeInstallPathWithWarning(tempStatePath, options.removePath); - throw new Error( - [ - getErrorMessage(error), - rollbackMessage, - ...(stateWarning === null ? [] : [stateWarning]), - ].join('\n'), - ); - } - - if (!hadPreviousApp) return []; - const backupWarning = await removeInstallPathWithWarning(previousAppPath, options.removePath); - return backupWarning === null ? [] : [backupWarning]; -} - -async function rollbackManagedAppInstall(options: { - installedTarget: boolean; - movedPreviousApp: boolean; - previousAppPath: string; - statePath: string; - targetAppPath: string; - removePath: (path: string) => Promise; - renamePath: (oldPath: string, newPath: string) => Promise; -}): Promise { - try { - if (options.installedTarget) { - await options.removePath(options.targetAppPath); - } - - if (options.movedPreviousApp) { - await options.renamePath(options.previousAppPath, options.targetAppPath); - return 'Previous app install and state were preserved.'; - } - - return 'Previous state was preserved; no previous managed app needed restoration.'; - } catch (error) { - return [ - `Rollback failed: ${getErrorMessage(error)}`, - `Recovery paths: app ${options.targetAppPath}; backup ${options.previousAppPath}; state ${options.statePath}.`, - ].join('\n'); - } -} - -async function copyAppBundle(sourcePath: string, targetPath: string): Promise { - const sourceRealPath = await realpath(sourcePath); - await copyDirectory(sourceRealPath, targetPath); -} - -async function copyDirectory(sourcePath: string, targetPath: string): Promise { - const sourceStat = await lstat(sourcePath); - if (sourceStat.isSymbolicLink()) { - throw new Error(`Refusing to copy symlink from Session Deck desktop artifact: ${sourcePath}`); - } - - if (!sourceStat.isDirectory()) { - throw new Error(`Expected directory while copying Session Deck desktop app: ${sourcePath}`); - } - - await mkdir(targetPath, { mode: sourceStat.mode & 0o777 }); - await chmod(targetPath, sourceStat.mode & 0o777); - const entries = (await readdir(sourcePath, { withFileTypes: true })).sort((left, right) => - left.name.localeCompare(right.name), - ); - for (const entry of entries) { - const sourceEntryPath = join(sourcePath, entry.name); - const targetEntryPath = join(targetPath, entry.name); - const entryStat = await lstat(sourceEntryPath); - if (entryStat.isSymbolicLink()) { - throw new Error( - `Refusing to copy symlink from Session Deck desktop artifact: ${sourceEntryPath}`, - ); - } - - if (entryStat.isDirectory()) { - await copyDirectory(sourceEntryPath, targetEntryPath); - continue; - } - - if (entryStat.isFile()) { - await copyFile(sourceEntryPath, targetEntryPath); - await chmod(targetEntryPath, entryStat.mode & 0o777); - continue; - } - - throw new Error(`Refusing to copy unsupported app bundle entry: ${sourceEntryPath}`); - } -} - -async function execFilePromise( - execFile: SessionDeckDesktopExecFile, - file: string, - args: string[], -): Promise { - await new Promise((resolvePromise, reject) => { - execFile(file, args, (error) => { - if (error !== null) { - reject(error); - return; - } - resolvePromise(); - }); - }); -} - -const nodeExecFileAdapter: SessionDeckDesktopExecFile = (file, args, callback) => { - const child = nodeExecFile(file, args, (error, stdout, stderr) => - callback(error, stdout, stderr), - ); - child.stdin?.end(); -}; - -function verifyExpectedSha256( - actualSha256: string, - expectedSha256: string | undefined, - label: string, -): void { - if (expectedSha256 !== undefined && actualSha256 !== expectedSha256) { - throw new Error( - `Checksum mismatch for ${label}: expected ${expectedSha256}, got ${actualSha256}.`, - ); - } -} - -function formatSource(source: SessionDeckDesktopSourceState): string { - return source.kind === 'local-path' - ? `${source.path} (${source.sha256})` - : `${source.releaseTag}/${source.assetName} (${source.sha256})`; -} - -async function removeInstallPath(path: string): Promise { - await rm(path, { recursive: true, force: true }); -} - -async function removeInstallPathWithWarning( - path: string, - removePath: (path: string) => Promise, -): Promise { - try { - await removePath(path); - return null; - } catch (error) { - return `Warning: cleanup left ${path}: ${getErrorMessage(error)}`; - } -} - -async function pathExists(path: string): Promise { - try { - await lstat(path); - return true; - } catch (error) { - if (isMissingFileError(error)) { - return false; - } - throw error; - } -} - -function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && 'code' in error && error.code === 'ENOENT'; -} - -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/open.ts b/packages/pi-session-deck/extensions/session-deck/desktop/open.ts deleted file mode 100644 index f94c003e..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/open.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { execFile as nodeExecFile } from 'node:child_process'; -import { lstat } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - getSessionDeckDesktopStatePath, -} from './paths.js'; -import { - readSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, -} from './state.js'; -import type { SessionDeckDesktopCommandResult } from './command.js'; -import type { SessionDeckDesktopExecFile } from './install.js'; - -export interface OpenSessionDeckDesktopOptions { - execFile?: SessionDeckDesktopExecFile; - homeDirectory?: string; - platform?: NodeJS.Platform; - statePath?: string; -} - -export async function openSessionDeckDesktop( - options: OpenSessionDeckDesktopOptions = {}, -): Promise { - const platform = options.platform ?? process.platform; - if (platform !== 'darwin') { - return { - level: 'error', - message: `Session Deck desktop open is only supported on macOS, not ${platform}.`, - }; - } - - const homeDirectory = options.homeDirectory ?? homedir(); - const statePath = options.statePath ?? getSessionDeckDesktopStatePath(homeDirectory); - const state = await readStateForOpen(statePath); - if (state.status !== 'ok') { - return state.result; - } - - if (!(await pathIsDirectory(state.installState.app.path))) { - return { - level: 'error', - message: [ - 'Could not open Session Deck desktop app.', - `Installed app is missing: ${state.installState.app.path}`, - 'Run /session-deck desktop doctor, or reinstall with /session-deck desktop install.', - ].join('\n'), - }; - } - - try { - await execFilePromise(options.execFile ?? nodeExecFileAdapter, '/usr/bin/open', [ - state.installState.app.path, - ]); - } catch (error) { - return { - level: 'error', - message: [ - 'Could not open Session Deck desktop app.', - `/usr/bin/open failed for ${state.installState.app.path}: ${getErrorMessage(error)}`, - ].join('\n'), - }; - } - - return { - level: 'info', - message: [ - `Opened Session Deck desktop app: ${state.installState.app.path}`, - SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE, - ].join('\n'), - }; -} - -async function readStateForOpen( - statePath: string, -): Promise< - | { status: 'ok'; installState: SessionDeckDesktopInstallState } - | { status: 'error'; result: SessionDeckDesktopCommandResult } -> { - try { - const installState = await readSessionDeckDesktopInstallState(statePath); - if (installState === null) { - return { - status: 'error', - result: { - level: 'warning', - message: `Session Deck desktop app is not installed. Run /session-deck desktop install. State not found at ${statePath}.`, - }, - }; - } - return { status: 'ok', installState }; - } catch (error) { - return { - status: 'error', - result: { - level: 'error', - message: [ - 'Could not open Session Deck desktop app.', - `Install state at ${statePath} could not be read: ${getErrorMessage(error)}`, - 'Run /session-deck desktop doctor or repair/remove the state file before opening.', - ].join('\n'), - }, - }; - } -} - -async function execFilePromise( - execFile: SessionDeckDesktopExecFile, - file: string, - args: string[], -): Promise { - await new Promise((resolvePromise, reject) => { - execFile(file, args, (error) => { - if (error !== null) { - reject(error); - return; - } - resolvePromise(); - }); - }); -} - -const nodeExecFileAdapter: SessionDeckDesktopExecFile = (file, args, callback) => { - const child = nodeExecFile(file, args, (error) => callback(error)); - child.stdin?.end(); -}; - -async function pathIsDirectory(path: string): Promise { - try { - const pathStat = await lstat(path); - return pathStat.isDirectory(); - } catch (error) { - if (isMissingFileError(error)) { - return false; - } - throw error; - } -} - -function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && 'code' in error && error.code === 'ENOENT'; -} - -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/paths.ts b/packages/pi-session-deck/extensions/session-deck/desktop/paths.ts deleted file mode 100644 index df76c7b7..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/paths.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { access, readFile } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -export const SESSION_DECK_DESKTOP_SUBCOMMAND = 'desktop'; -export const SESSION_DECK_DESKTOP_INSTALL_ACTION = 'install'; -export const SESSION_DECK_DESKTOP_OPEN_ACTION = 'open'; -export const SESSION_DECK_DESKTOP_UNINSTALL_ACTION = 'uninstall'; -export const SESSION_DECK_DESKTOP_DOCTOR_ACTION = 'doctor'; -export const SESSION_DECK_DESKTOP_FROM_PATH_FLAG = '--from-path'; -export const SESSION_DECK_DESKTOP_VERSION_FLAG = '--version'; -export const SESSION_DECK_DESKTOP_SHA256_FLAG = '--sha256'; -export const SESSION_DECK_DESKTOP_PACKAGE_NAME = '@robhowley/pi-session-deck'; -export const SESSION_DECK_DESKTOP_APP_BUNDLE_NAME = 'Session Deck Desktop.app'; -export const SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER = 'dev.pi-userland.session-deck.desktop'; -export const SESSION_DECK_DESKTOP_STATE_FILENAME = 'install.json'; -export const SESSION_DECK_DESKTOP_RELEASE_OWNER = 'robhowley'; -export const SESSION_DECK_DESKTOP_RELEASE_REPO = 'pi-userland'; -export const SESSION_DECK_DESKTOP_FIRST_LAUNCH_GUIDANCE = - 'If macOS blocks first launch, leave the app installed at the initial warning, then use System Settings → Privacy & Security → Open Anyway.'; - -export interface SessionDeckDesktopRuntimePaths { - packageRoot: string; - packageVersion: string; - nodeExecutablePath: string; -} - -export type SessionDeckDesktopAssetPlatform = 'macos-arm64' | 'macos-x64'; - -export function getDefaultSessionDeckDesktopAppPath(homeDirectory: string = homedir()): string { - return join(homeDirectory, 'Applications', SESSION_DECK_DESKTOP_APP_BUNDLE_NAME); -} - -export function getSessionDeckDesktopStateDir(homeDirectory: string = homedir()): string { - return join(homeDirectory, '.pi', 'session-deck', 'desktop'); -} - -export function getSessionDeckDesktopStatePath(homeDirectory: string = homedir()): string { - return join(getSessionDeckDesktopStateDir(homeDirectory), SESSION_DECK_DESKTOP_STATE_FILENAME); -} - -export function getSessionDeckDesktopTmpDir(homeDirectory: string = homedir()): string { - return join(getSessionDeckDesktopStateDir(homeDirectory), 'tmp'); -} - -export function getSessionDeckDesktopCacheDir(homeDirectory: string = homedir()): string { - return join(getSessionDeckDesktopStateDir(homeDirectory), 'cache'); -} - -export function getSessionDeckDesktopReleaseTag(version: string): string { - return `pi-session-deck-v${normalizeVersion(version)}`; -} - -export function getSessionDeckDesktopArtifactName( - version: string, - options: { platform?: NodeJS.Platform; arch?: NodeJS.Architecture } = {}, -): string { - return `session-deck-desktop-v${normalizeVersion(version)}-${getSessionDeckDesktopAssetPlatform(options)}.zip`; -} - -export function getSessionDeckDesktopAssetPlatform( - options: { platform?: NodeJS.Platform; arch?: NodeJS.Architecture } = {}, -): SessionDeckDesktopAssetPlatform { - const platform = options.platform ?? process.platform; - const arch = options.arch ?? process.arch; - - if (platform !== 'darwin') { - throw new Error( - `Session Deck desktop artifacts are only available for macOS, not ${platform}.`, - ); - } - - if (arch === 'arm64') { - return 'macos-arm64'; - } - - if (arch === 'x64') { - return 'macos-x64'; - } - - throw new Error(`Session Deck desktop artifacts are not available for macOS ${arch}.`); -} - -export async function resolveSessionDeckDesktopRuntimePaths( - importMetaUrl: string, - options: { - nodeExecutablePath?: string; - packageName?: string; - } = {}, -): Promise { - const packageName = options.packageName ?? SESSION_DECK_DESKTOP_PACKAGE_NAME; - const packageRoot = await findPackageRoot(dirname(fileURLToPath(importMetaUrl)), packageName); - const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')) as { - version?: string; - }; - const packageVersion = packageJson.version; - if (typeof packageVersion !== 'string' || packageVersion.length === 0) { - throw new Error( - `Could not determine package version from ${join(packageRoot, 'package.json')}.`, - ); - } - - return { - packageRoot, - packageVersion, - nodeExecutablePath: options.nodeExecutablePath ?? process.execPath, - }; -} - -function normalizeVersion(version: string): string { - const trimmed = version.trim(); - return trimmed.startsWith('v') ? trimmed.slice(1) : trimmed; -} - -async function findPackageRoot(startDirectory: string, packageName: string): Promise { - let currentDirectory = resolve(startDirectory); - - while (true) { - const packageJsonPath = join(currentDirectory, 'package.json'); - if (await pathExists(packageJsonPath)) { - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as { name?: string }; - if (packageJson.name === packageName) { - return currentDirectory; - } - } - - const parentDirectory = dirname(currentDirectory); - if (parentDirectory === currentDirectory) { - break; - } - currentDirectory = parentDirectory; - } - - throw new Error(`Could not find package root for ${packageName}.`); -} - -async function pathExists(path: string): Promise { - try { - await access(path); - return true; - } catch { - return false; - } -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/state.ts b/packages/pi-session-deck/extensions/session-deck/desktop/state.ts deleted file mode 100644 index 5c38b7f2..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/state.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { createHash, randomUUID } from 'node:crypto'; -import { chmod, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'; -import { dirname, isAbsolute, join, relative, sep } from 'node:path'; -import { - SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER, - SESSION_DECK_DESKTOP_PACKAGE_NAME, -} from './paths.js'; - -export const SESSION_DECK_DESKTOP_STATE_SCHEMA_VERSION = 1; -export const SESSION_DECK_DESKTOP_PRODUCT = 'session-deck-desktop'; - -export interface SessionDeckDesktopAppState { - path: string; - bundleIdentifier: typeof SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER; - name: string; - version: string; - sha256: string; -} - -export type SessionDeckDesktopSourceState = - | { - kind: 'local-path'; - path: string; - sha256: string; - } - | { - kind: 'github-release'; - releaseTag: string; - assetName: string; - url: string; - sha256: string; - }; - -export interface SessionDeckDesktopInstallState { - schemaVersion: typeof SESSION_DECK_DESKTOP_STATE_SCHEMA_VERSION; - product: typeof SESSION_DECK_DESKTOP_PRODUCT; - packageName: typeof SESSION_DECK_DESKTOP_PACKAGE_NAME; - packageVersion: string; - installedAt: string; - app: SessionDeckDesktopAppState; - source: SessionDeckDesktopSourceState; - runtime: { - nodeExecutablePath: string; - packageRoot: string; - helperPackageVersion: string; - }; - ownedPaths: string[]; -} - -export function hashSessionDeckDesktopContent(content: string | Buffer): string { - return createHash('sha256').update(content).digest('hex'); -} - -export async function hashSessionDeckDesktopPath(path: string): Promise { - const pathStat = await lstat(path); - if (pathStat.isFile()) { - return hashSessionDeckDesktopContent(await readFile(path)); - } - - const hash = createHash('sha256'); - await hashDirectoryInto(hash, path, ''); - return hash.digest('hex'); -} - -export async function readSessionDeckDesktopInstallState( - statePath: string, -): Promise { - try { - const raw = await readFile(statePath, 'utf8'); - const parsed = JSON.parse(raw) as unknown; - return parseSessionDeckDesktopInstallState(parsed); - } catch (error) { - if (isMissingFileError(error)) { - return null; - } - throw error; - } -} - -export async function stageSessionDeckDesktopInstallState( - statePath: string, - state: SessionDeckDesktopInstallState, -): Promise { - const stateDir = dirname(statePath); - await mkdir(stateDir, { recursive: true, mode: 0o700 }); - await chmod(stateDir, 0o700); - - const tempPath = join(stateDir, `.install.${process.pid}.${randomUUID()}.tmp`); - try { - await writeFile(tempPath, serializeSessionDeckDesktopInstallState(state), { - encoding: 'utf8', - mode: 0o600, - }); - await chmod(tempPath, 0o600); - return tempPath; - } catch (error) { - await rm(tempPath, { force: true }).catch(() => undefined); - throw error; - } -} - -export async function writeSessionDeckDesktopInstallState( - statePath: string, - state: SessionDeckDesktopInstallState, -): Promise { - const tempPath = await stageSessionDeckDesktopInstallState(statePath, state); - try { - await rename(tempPath, statePath); - } catch (error) { - await rm(tempPath, { force: true }).catch(() => undefined); - throw error; - } -} - -export function serializeSessionDeckDesktopInstallState( - state: SessionDeckDesktopInstallState, -): string { - return `${JSON.stringify(state, null, 2)}\n`; -} - -export function parseSessionDeckDesktopInstallState( - candidate: unknown, -): SessionDeckDesktopInstallState { - if (!isRecord(candidate)) { - throw new Error('State has an invalid shape.'); - } - - const packageVersion = candidate['packageVersion']; - const installedAt = candidate['installedAt']; - const app = candidate['app']; - const source = candidate['source']; - const runtime = candidate['runtime']; - const ownedPaths = candidate['ownedPaths']; - - if ( - !hasExactKeys(candidate, [ - 'schemaVersion', - 'product', - 'packageName', - 'packageVersion', - 'installedAt', - 'app', - 'source', - 'runtime', - 'ownedPaths', - ]) || - candidate['schemaVersion'] !== SESSION_DECK_DESKTOP_STATE_SCHEMA_VERSION || - candidate['product'] !== SESSION_DECK_DESKTOP_PRODUCT || - candidate['packageName'] !== SESSION_DECK_DESKTOP_PACKAGE_NAME || - !isNonEmptyString(packageVersion) || - !isNonEmptyString(installedAt) || - !isAppState(app) || - !isSourceState(source) || - !isRuntimeState(runtime) || - !isOwnedPaths(ownedPaths) - ) { - throw new Error('State has an invalid shape.'); - } - - if (!ownedPaths.includes(app.path)) { - throw new Error('State does not record the app path as owned.'); - } - - return { - schemaVersion: SESSION_DECK_DESKTOP_STATE_SCHEMA_VERSION, - product: SESSION_DECK_DESKTOP_PRODUCT, - packageName: SESSION_DECK_DESKTOP_PACKAGE_NAME, - packageVersion, - installedAt, - app: { - path: app.path, - bundleIdentifier: SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER, - name: app.name, - version: app.version, - sha256: app.sha256, - }, - source: - source.kind === 'local-path' - ? { - kind: 'local-path', - path: source.path, - sha256: source.sha256, - } - : { - kind: 'github-release', - releaseTag: source.releaseTag, - assetName: source.assetName, - url: source.url, - sha256: source.sha256, - }, - runtime: { - nodeExecutablePath: runtime.nodeExecutablePath, - packageRoot: runtime.packageRoot, - helperPackageVersion: runtime.helperPackageVersion, - }, - ownedPaths: [...ownedPaths], - }; -} - -async function hashDirectoryInto( - hash: ReturnType, - path: string, - relativePath: string, -): Promise { - const pathStat = await lstat(path); - if (pathStat.isSymbolicLink()) { - throw new Error(`Cannot checksum symlink in Session Deck desktop artifact: ${path}`); - } - - const normalizedRelativePath = relativePath.split(sep).join('/'); - if (pathStat.isDirectory()) { - hash.update(`dir\0${normalizedRelativePath}\0`); - const entries = (await readdir(path, { withFileTypes: true })).sort((left, right) => - left.name.localeCompare(right.name), - ); - for (const entry of entries) { - await hashDirectoryInto(hash, join(path, entry.name), join(relativePath, entry.name)); - } - return; - } - - if (!pathStat.isFile()) { - throw new Error( - `Cannot checksum unsupported file type in Session Deck desktop artifact: ${path}`, - ); - } - - hash.update(`file\0${normalizedRelativePath}\0${pathStat.size}\0`); - hash.update(await readFile(path)); -} - -function isAppState(candidate: unknown): candidate is SessionDeckDesktopAppState { - return ( - isRecord(candidate) && - hasExactKeys(candidate, ['path', 'bundleIdentifier', 'name', 'version', 'sha256']) && - isAbsoluteNonEmptyString(candidate['path']) && - candidate['bundleIdentifier'] === SESSION_DECK_DESKTOP_BUNDLE_IDENTIFIER && - isNonEmptyString(candidate['name']) && - isNonEmptyString(candidate['version']) && - isSha256(candidate['sha256']) - ); -} - -function isSourceState(candidate: unknown): candidate is SessionDeckDesktopSourceState { - if (!isRecord(candidate) || typeof candidate['kind'] !== 'string') { - return false; - } - - if (candidate['kind'] === 'local-path') { - return ( - hasExactKeys(candidate, ['kind', 'path', 'sha256']) && - isAbsoluteNonEmptyString(candidate['path']) && - isSha256(candidate['sha256']) - ); - } - - return ( - candidate['kind'] === 'github-release' && - hasExactKeys(candidate, ['kind', 'releaseTag', 'assetName', 'url', 'sha256']) && - isNonEmptyString(candidate['releaseTag']) && - isNonEmptyString(candidate['assetName']) && - isNonEmptyString(candidate['url']) && - isSha256(candidate['sha256']) - ); -} - -function isRuntimeState( - candidate: unknown, -): candidate is SessionDeckDesktopInstallState['runtime'] { - return ( - isRecord(candidate) && - hasExactKeys(candidate, ['nodeExecutablePath', 'packageRoot', 'helperPackageVersion']) && - isAbsoluteNonEmptyString(candidate['nodeExecutablePath']) && - isAbsoluteNonEmptyString(candidate['packageRoot']) && - isNonEmptyString(candidate['helperPackageVersion']) - ); -} - -function isOwnedPaths(candidate: unknown): candidate is string[] { - return ( - Array.isArray(candidate) && candidate.length > 0 && candidate.every(isAbsoluteNonEmptyString) - ); -} - -function isSha256(candidate: unknown): candidate is string { - return typeof candidate === 'string' && /^[a-f0-9]{64}$/u.test(candidate); -} - -function isAbsoluteNonEmptyString(candidate: unknown): candidate is string { - return isNonEmptyString(candidate) && isAbsolute(candidate); -} - -function isNonEmptyString(candidate: unknown): candidate is string { - return typeof candidate === 'string' && candidate.trim().length > 0; -} - -function isRecord(candidate: unknown): candidate is Record { - return typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate); -} - -function hasExactKeys(candidate: Record, keys: readonly string[]): boolean { - const actualKeys = Object.keys(candidate); - return actualKeys.length === keys.length && actualKeys.every((key) => keys.includes(key)); -} - -function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { - return error instanceof Error && 'code' in error && error.code === 'ENOENT'; -} - -export function isPathInside(parentPath: string, candidatePath: string): boolean { - const relativePath = relative(parentPath, candidatePath); - return relativePath.length === 0 || (!relativePath.startsWith('..') && !isAbsolute(relativePath)); -} diff --git a/packages/pi-session-deck/extensions/session-deck/desktop/uninstall.ts b/packages/pi-session-deck/extensions/session-deck/desktop/uninstall.ts deleted file mode 100644 index 15790149..00000000 --- a/packages/pi-session-deck/extensions/session-deck/desktop/uninstall.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { rm } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { resolve } from 'node:path'; -import { - getDefaultSessionDeckDesktopAppPath, - getSessionDeckDesktopCacheDir, - getSessionDeckDesktopStateDir, - getSessionDeckDesktopStatePath, - getSessionDeckDesktopTmpDir, -} from './paths.js'; -import { - isPathInside, - readSessionDeckDesktopInstallState, - type SessionDeckDesktopInstallState, -} from './state.js'; -import type { SessionDeckDesktopCommandResult } from './command.js'; - -export interface UninstallSessionDeckDesktopOptions { - homeDirectory?: string; - removePath?: typeof rm; - statePath?: string; -} - -export async function uninstallSessionDeckDesktop( - options: UninstallSessionDeckDesktopOptions = {}, -): Promise { - const homeDirectory = options.homeDirectory ?? homedir(); - const statePath = options.statePath ?? getSessionDeckDesktopStatePath(homeDirectory); - let state: SessionDeckDesktopInstallState | null; - try { - state = await readSessionDeckDesktopInstallState(statePath); - } catch (error) { - return { - level: 'warning', - message: [ - 'Could not uninstall Session Deck desktop app automatically.', - `Install state at ${statePath} could not be read: ${getErrorMessage(error)}`, - 'Nothing was removed because app ownership could not be verified.', - 'Manual recovery required: remove or repair the state file and verify/remove any Session Deck desktop app manually.', - ].join('\n'), - }; - } - - if (state === null) { - return { - level: 'warning', - message: `No Session Deck desktop install state found at ${statePath}.`, - }; - } - - const removePath = options.removePath ?? rm; - const removalPlan = getOwnedRemovalPlan(state, homeDirectory, statePath); - const removedPaths: string[] = []; - for (const [index, path] of removalPlan.safePaths.entries()) { - try { - await removePath(path, { recursive: true, force: true }); - removedPaths.push(path); - } catch (error) { - return { - level: 'warning', - message: formatOwnedPathRemovalFailure({ - failedPath: path, - failure: getErrorMessage(error), - pendingPaths: removalPlan.safePaths.slice(index + 1), - removedPaths, - skippedPaths: removalPlan.skippedPaths, - statePath, - }), - }; - } - } - - try { - await removePath(statePath, { force: true }); - } catch (error) { - return { - level: 'warning', - message: formatStateRemovalFailure({ - failure: getErrorMessage(error), - removedPaths, - skippedPaths: removalPlan.skippedPaths, - statePath, - }), - }; - } - - const lines = ['Uninstalled Session Deck desktop app.']; - if (removedPaths.length > 0) { - lines.push('Removed owned paths:'); - for (const path of removedPaths) { - lines.push(`- ${path}`); - } - } else { - lines.push('No owned app/cache paths were present to remove.'); - } - lines.push(`Removed state: ${statePath}`); - - if (removalPlan.skippedPaths.length > 0) { - lines.push('Skipped unsafe ownedPaths entries:'); - for (const path of removalPlan.skippedPaths) { - lines.push(`- ${path}`); - } - } - - return { - level: removalPlan.skippedPaths.length === 0 ? 'info' : 'warning', - message: lines.join('\n'), - }; -} - -function getOwnedRemovalPlan( - state: SessionDeckDesktopInstallState, - homeDirectory: string, - statePath: string, -): { safePaths: string[]; skippedPaths: string[] } { - const defaultAppPath = resolve(getDefaultSessionDeckDesktopAppPath(homeDirectory)); - const resolvedStatePath = resolve(statePath); - const stateDir = resolve(getSessionDeckDesktopStateDir(homeDirectory)); - const cacheDir = resolve(getSessionDeckDesktopCacheDir(homeDirectory)); - const tmpDir = resolve(getSessionDeckDesktopTmpDir(homeDirectory)); - const safePaths: string[] = []; - const skippedPaths: string[] = []; - - for (const ownedPath of state.ownedPaths) { - const resolvedPath = resolve(ownedPath); - if (resolvedPath === resolvedStatePath) { - continue; - } - - if ( - resolvedPath === defaultAppPath || - isPathInside(cacheDir, resolvedPath) || - isPathInside(tmpDir, resolvedPath) - ) { - if (!safePaths.includes(resolvedPath)) { - safePaths.push(resolvedPath); - } - continue; - } - - if (isPathInside(stateDir, resolvedPath) && resolvedPath !== stateDir) { - if (!safePaths.includes(resolvedPath)) { - safePaths.push(resolvedPath); - } - continue; - } - - skippedPaths.push(ownedPath); - } - - return { safePaths, skippedPaths }; -} - -function formatOwnedPathRemovalFailure(options: { - failedPath: string; - failure: string; - pendingPaths: string[]; - removedPaths: string[]; - skippedPaths: string[]; - statePath: string; -}): string { - const lines = [ - 'Session Deck desktop uninstall stopped after an owned path could not be removed.', - 'Removed owned paths:', - ...formatPaths(options.removedPaths), - 'Failed owned path:', - `- ${options.failedPath}: ${options.failure}`, - 'Pending owned paths:', - ...formatPaths(options.pendingPaths), - `Install state retained for retry: ${options.statePath}`, - ]; - appendSkippedPaths(lines, options.skippedPaths); - return lines.join('\n'); -} - -function formatStateRemovalFailure(options: { - failure: string; - removedPaths: string[]; - skippedPaths: string[]; - statePath: string; -}): string { - const lines = [ - 'Session Deck desktop safe owned-path cleanup completed, but install state removal failed.', - 'Removed owned paths:', - ...formatPaths(options.removedPaths), - 'Failed state path:', - `- ${options.statePath}: ${options.failure}`, - 'Pending owned paths:', - '- (none)', - 'Retry /session-deck desktop uninstall to finish state cleanup.', - ]; - appendSkippedPaths(lines, options.skippedPaths); - return lines.join('\n'); -} - -function formatPaths(paths: string[]): string[] { - return paths.length === 0 ? ['- (none)'] : paths.map((path) => `- ${path}`); -} - -function appendSkippedPaths(lines: string[], skippedPaths: string[]): void { - if (skippedPaths.length === 0) { - return; - } - - lines.push('Skipped unsafe ownedPaths entries:', ...skippedPaths.map((path) => `- ${path}`)); -} - -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/packages/pi-session-deck/extensions/session-deck/identity/command.ts b/packages/pi-session-deck/extensions/session-deck/identity/command.ts index 1c7dfa26..806f7890 100644 --- a/packages/pi-session-deck/extensions/session-deck/identity/command.ts +++ b/packages/pi-session-deck/extensions/session-deck/identity/command.ts @@ -1,9 +1,4 @@ import type { Theme } from '@earendil-works/pi-coding-agent'; -import { - getSessionDeckDesktopCommandCompletions, - isSessionDeckDesktopCommand, - runSessionDeckDesktopCommand, -} from '../desktop/command.js'; import { getSessionDeckIterm2CommandCompletions, isSessionDeckIterm2Command, @@ -97,11 +92,9 @@ export interface PresenceCommandAPI { } export interface RegisterSessionDeckCommandOptions extends ReadSessionDeckSnapshotOptions { - isSessionDeckDesktopCommand?: typeof isSessionDeckDesktopCommand; isSessionDeckIterm2Command?: typeof isSessionDeckIterm2Command; readSessionDeckSnapshot?: typeof readSessionDeckSnapshot; reapPresenceRecords?: typeof reapPresenceRecords; - runSessionDeckDesktopCommand?: typeof runSessionDeckDesktopCommand; runSessionDeckIterm2Command?: typeof runSessionDeckIterm2Command; unlink?: ReapPresenceRecordsOptions['unlink']; openTerminal?: (runtimeId: string) => Promise; @@ -162,24 +155,16 @@ export function registerSessionDeckCommand( pi: PresenceCommandAPI, options: RegisterSessionDeckCommandOptions = {}, ): void { - const isDesktopCommand = options.isSessionDeckDesktopCommand ?? isSessionDeckDesktopCommand; const isIterm2Command = options.isSessionDeckIterm2Command ?? isSessionDeckIterm2Command; const readSnapshot = options.readSessionDeckSnapshot ?? readSessionDeckSnapshot; const reapPresence = options.reapPresenceRecords ?? reapPresenceRecords; - const runDesktopCommand = options.runSessionDeckDesktopCommand ?? runSessionDeckDesktopCommand; const runIterm2Command = options.runSessionDeckIterm2Command ?? runSessionDeckIterm2Command; pi.registerCommand(SESSION_DECK_COMMAND_NAME, { description: - 'Show Pi session presence, identity, activity, and chips from ~/.pi/session-deck, or manage the desktop app and iTerm2 Toolbelt installs', + 'Show Pi session presence, identity, activity, and chips from ~/.pi/session-deck, or manage the iTerm2 Toolbelt integration', getArgumentCompletions: getSessionDeckCommandCompletions, handler: async (args: string, ctx: PresenceCommandContext) => { - if (isDesktopCommand(args)) { - const result = await runDesktopCommand(args); - ctx.ui.notify(result.message, result.level); - return; - } - if (isIterm2Command(args)) { const result = await runIterm2Command(args); ctx.ui.notify(result.message, result.level); @@ -658,13 +643,8 @@ function pluralize(count: number, singular: string): string { function getSessionDeckCommandCompletions(prefix: string) { const trimmedPrefix = prefix.trimStart(); - const desktopMatches = getSessionDeckDesktopCommandCompletions(trimmedPrefix); const iterm2Matches = getSessionDeckIterm2CommandCompletions(trimmedPrefix); - if (trimmedPrefix.startsWith('desktop')) { - return desktopMatches; - } - if (trimmedPrefix.startsWith('iterm2')) { return iterm2Matches; } @@ -675,8 +655,7 @@ function getSessionDeckCommandCompletions(prefix: string) { label: flag, }), ); - const commandMatches = [desktopMatches, iterm2Matches].flatMap((matches) => matches ?? []); - const matches = [...flagMatches, ...commandMatches]; + const matches = [...flagMatches, ...(iterm2Matches ?? [])]; return matches.length > 0 ? matches : null; } diff --git a/packages/pi-session-deck/package.json b/packages/pi-session-deck/package.json index 5e334521..d549bb20 100644 --- a/packages/pi-session-deck/package.json +++ b/packages/pi-session-deck/package.json @@ -2,7 +2,7 @@ "name": "@robhowley/pi-session-deck", "version": "0.14.0", "type": "module", - "description": "The full Pi session lifecycle in one place: create and organize sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI, desktop app, or iTerm2 Toolbelt.", + "description": "The full Pi session lifecycle in one place: create and organize sessions across repos and worktrees, see what each agent is doing or waiting on, and reopen or end them from a TUI or iTerm2 Toolbelt.", "files": [ "dist", "extensions", @@ -26,8 +26,7 @@ "iterm2-toolbelt", "tmux", "git-worktree", - "terminal", - "tauri" + "terminal" ], "pi": { "extensions": [ diff --git a/packages/pi-session-deck/tsconfig.build.json b/packages/pi-session-deck/tsconfig.build.json index fdeb45bb..e48cc5a1 100644 --- a/packages/pi-session-deck/tsconfig.build.json +++ b/packages/pi-session-deck/tsconfig.build.json @@ -11,9 +11,7 @@ "extensions/session-deck/iterm2/snapshot-cli.ts", "extensions/session-deck/iterm2/open-action-cli.ts", "extensions/session-deck/iterm2/kill-action-cli.ts", - "extensions/session-deck/worktree/action-cli.ts", - "extensions/session-deck/desktop/install.ts", - "extensions/session-deck/desktop/open.ts" + "extensions/session-deck/worktree/action-cli.ts" ], "exclude": ["__tests__"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26553fad..c43a209d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,19 +90,6 @@ importers: specifier: ^22.15.17 version: 22.19.17 - apps/session-deck-desktop: - dependencies: - '@tauri-apps/api': - specifier: ^2.0.0 - version: 2.11.1 - devDependencies: - '@tauri-apps/cli': - specifier: ^2.0.0 - version: 2.11.4 - '@types/node': - specifier: ^22.15.17 - version: 22.19.17 - packages/pi-session-hygiene: dependencies: '@mariozechner/pi-ai': @@ -1384,85 +1371,6 @@ packages: resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} engines: {node: '>=18.0.0'} - '@tauri-apps/api@2.11.1': - resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==, tarball: https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz} - - '@tauri-apps/cli-darwin-arm64@2.11.4': - resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tauri-apps/cli-darwin-x64@2.11.4': - resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==, tarball: https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tauri-apps/cli-linux-x64-musl@2.11.4': - resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==, tarball: https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==, tarball: https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==, tarball: https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tauri-apps/cli@2.11.4': - resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==, tarball: https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz} - engines: {node: '>= 10'} - hasBin: true - '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -5411,55 +5319,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@tauri-apps/api@2.11.1': {} - - '@tauri-apps/cli-darwin-arm64@2.11.4': - optional: true - - '@tauri-apps/cli-darwin-x64@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-arm64-musl@2.11.4': - optional: true - - '@tauri-apps/cli-linux-riscv64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-x64-gnu@2.11.4': - optional: true - - '@tauri-apps/cli-linux-x64-musl@2.11.4': - optional: true - - '@tauri-apps/cli-win32-arm64-msvc@2.11.4': - optional: true - - '@tauri-apps/cli-win32-ia32-msvc@2.11.4': - optional: true - - '@tauri-apps/cli-win32-x64-msvc@2.11.4': - optional: true - - '@tauri-apps/cli@2.11.4': - optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.4 - '@tauri-apps/cli-darwin-x64': 2.11.4 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.4 - '@tauri-apps/cli-linux-arm64-musl': 2.11.4 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-gnu': 2.11.4 - '@tauri-apps/cli-linux-x64-musl': 2.11.4 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.4 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.4 - '@tauri-apps/cli-win32-x64-msvc': 2.11.4 - '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3(supports-color@8.1.1)