diff --git a/.github/workflows/build-all-platforms.yml b/.github/workflows/build-all-platforms.yml new file mode 100644 index 0000000..f8cb28e --- /dev/null +++ b/.github/workflows/build-all-platforms.yml @@ -0,0 +1,111 @@ +name: Build All Platforms + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g., v0.1.2). Leave empty to use current ref." + required: false + type: string + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build (${{ matrix.platform }}) + strategy: + fail-fast: false + matrix: + include: + # Windows + - platform: windows-latest + target: x86_64-pc-windows-msvc + args: "" + + # macOS Intel + - platform: macos-13 + target: x86_64-apple-darwin + args: "--target x86_64-apple-darwin" + + # macOS Apple Silicon + - platform: macos-14 + target: aarch64-apple-darwin + args: "--target aarch64-apple-darwin" + + # Linux + - platform: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + args: "" + + runs-on: ${{ matrix.platform }} + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set release tag + shell: bash + run: | + set -euo pipefail + TAG_INPUT="${{ inputs.tag }}" + if [ -n "$TAG_INPUT" ]; then + if [[ "$TAG_INPUT" == v* ]]; then + TAG="$TAG_INPUT" + else + TAG="v$TAG_INPUT" + fi + else + TAG="${GITHUB_REF_NAME}" + fi + echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" + + # Linux dependencies + - name: Install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.27.0 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0.5.17 + env: + CI: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tagName: ${{ env.RELEASE_TAG }} + releaseName: OpenWork ${{ env.RELEASE_TAG }} + releaseBody: | + ## Downloads + - **Windows**: `.exe` or `.msi` installer + - **macOS**: `.dmg` disk image + - **Linux**: `.AppImage` or `.deb` package + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} diff --git a/AGENTS.md b/AGENTS.md index 16f47aa..7634e06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,17 +97,14 @@ This captures OpenWork’s preferred reactivity + UI state patterns (avoid globa ## Skill: Trigger a Release -OpenWork releases are built by GitHub Actions (`Release App`). A release is triggered by pushing a `v*` tag (e.g. `v0.1.6`). +OpenWork releases are built by GitHub Actions (`Release App`) and publish signed + notarized macOS DMGs to the GitHub Release for a tag. ### Standard release (recommended) -1. Ensure `main` is green and up to date. -2. Bump versions (keep these in sync): - - `apps/openwork/package.json` (`version`) - - `apps/openwork/src-tauri/tauri.conf.json` (`version`) - - `apps/openwork/src-tauri/Cargo.toml` (`version`) -3. Merge the version bump to `main`. -4. Create and push a tag: +1. Bump versions (at minimum `apps/openwork/package.json`, and keep Tauri/Rust versions in sync). +2. Merge to `main`. +3. Create and push a version tag: + - `git tag vX.Y.Z` - `git push origin vX.Y.Z` @@ -121,7 +118,7 @@ If the workflow needs to be re-run for an existing tag (e.g. notarization retry) ### Verify -- Runs: `gh run list --repo different-ai/openwork --workflow "Release App" --limit 5` -- Release: `gh release view vX.Y.Z --repo different-ai/openwork` +- `gh run list --repo different-ai/openwork --workflow "Release App" --limit 5` +- `gh release view vX.Y.Z --repo different-ai/openwork` -Confirm the DMG assets are attached and versioned correctly. \ No newline at end of file +Confirm the DMG assets are attached and versioned correctly. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0ef701f..de4d522 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2120,7 +2120,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openwork" -version = "0.1.8" +version = "0.1.9" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/gen/schemas/windows-schema.json b/src-tauri/gen/schemas/windows-schema.json new file mode 100644 index 0000000..5aa9e87 --- /dev/null +++ b/src-tauri/gen/schemas/windows-schema.json @@ -0,0 +1,2310 @@ +{ + "$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`", + "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`" + }, + { + "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 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 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-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-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_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_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-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-internal-toggle-maximize`" + }, + { + "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 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 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 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." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope." + }, + { + "description": "Enables the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope." + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope." + }, + { + "description": "Denies the confirm command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope." + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save 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/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5888332..b17720a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,14 +1,14 @@ use std::{ - collections::hash_map::DefaultHasher, - env, - ffi::OsStr, - fs, - hash::{Hash, Hasher}, - net::TcpListener, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, + collections::hash_map::DefaultHasher, + env, + ffi::OsStr, + fs, + hash::{Hash, Hasher}, + net::TcpListener, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, }; use serde::{Deserialize, Serialize}; @@ -18,320 +18,319 @@ use tauri::{Manager, State}; const MACOS_APP_SUPPORT_DIR: &str = "Library/Application Support"; fn candidate_xdg_data_dirs() -> Vec { - let mut candidates = Vec::new(); - let Some(home) = home_dir() else { - return candidates; - }; + let mut candidates = Vec::new(); + let Some(home) = home_dir() else { + return candidates; + }; - candidates.push(home.join(".local").join("share")); - candidates.push(home.join(".config")); + candidates.push(home.join(".local").join("share")); + candidates.push(home.join(".config")); - #[cfg(target_os = "macos")] - { - candidates.push(home.join(MACOS_APP_SUPPORT_DIR)); - } + #[cfg(target_os = "macos")] + { + candidates.push(home.join(MACOS_APP_SUPPORT_DIR)); + } - candidates + candidates } fn candidate_xdg_config_dirs() -> Vec { - let mut candidates = Vec::new(); - let Some(home) = home_dir() else { - return candidates; - }; + let mut candidates = Vec::new(); + let Some(home) = home_dir() else { + return candidates; + }; - candidates.push(home.join(".config")); + candidates.push(home.join(".config")); - #[cfg(target_os = "macos")] - { - candidates.push(home.join(MACOS_APP_SUPPORT_DIR)); - } + #[cfg(target_os = "macos")] + { + candidates.push(home.join(MACOS_APP_SUPPORT_DIR)); + } - candidates + candidates } fn maybe_infer_xdg_home( - var_name: &str, - candidates: Vec, - relative_marker: &Path, + var_name: &str, + candidates: Vec, + relative_marker: &Path, ) -> Option { - if env::var_os(var_name).is_some() { - return None; - } + if env::var_os(var_name).is_some() { + return None; + } - for base in candidates { - if base.join(relative_marker).is_file() { - return Some(base.to_string_lossy().to_string()); + for base in candidates { + if base.join(relative_marker).is_file() { + return Some(base.to_string_lossy().to_string()); + } } - } - None + None } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkspaceOpenworkConfig { - pub version: u32, - pub workspace: Option, - #[serde(default, alias = "authorizedRoots")] - pub authorized_roots: Vec, + pub version: u32, + pub workspace: Option, + #[serde(default, alias = "authorizedRoots")] + pub authorized_roots: Vec, } impl Default for WorkspaceOpenworkConfig { - fn default() -> Self { - Self { - version: 1, - workspace: None, - authorized_roots: Vec::new(), + fn default() -> Self { + Self { + version: 1, + workspace: None, + authorized_roots: Vec::new(), + } } - } } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkspaceOpenworkWorkspace { - pub name: Option, - #[serde(default, alias = "createdAt")] - pub created_at: Option, - #[serde(default, alias = "preset")] - pub preset: Option, + pub name: Option, + #[serde(default, alias = "createdAt")] + pub created_at: Option, + #[serde(default, alias = "preset")] + pub preset: Option, } impl WorkspaceOpenworkConfig { - fn new(workspace_path: &str, preset: &str) -> Self { - let root = PathBuf::from(workspace_path); - let inferred_name = root - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("Workspace") - .to_string(); - - Self { - version: 1, - workspace: Some(WorkspaceOpenworkWorkspace { - name: Some(inferred_name), - created_at: Some(now_ms()), - preset: Some(preset.to_string()), - }), - authorized_roots: vec![workspace_path.to_string()], + fn new(workspace_path: &str, preset: &str) -> Self { + let root = PathBuf::from(workspace_path); + let inferred_name = root + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("Workspace") + .to_string(); + + Self { + version: 1, + workspace: Some(WorkspaceOpenworkWorkspace { + name: Some(inferred_name), + created_at: Some(now_ms()), + preset: Some(preset.to_string()), + }), + authorized_roots: vec![workspace_path.to_string()], + } } - } } #[derive(Default)] struct EngineManager { - inner: Mutex, + inner: Mutex, } #[derive(Default)] struct EngineState { - child: Option, - project_dir: Option, - hostname: Option, - port: Option, - base_url: Option, - last_stdout: Option, - last_stderr: Option, + child: Option, + project_dir: Option, + hostname: Option, + port: Option, + base_url: Option, + last_stdout: Option, + last_stderr: Option, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct EngineInfo { - pub running: bool, - pub base_url: Option, - pub project_dir: Option, - pub hostname: Option, - pub port: Option, - pub pid: Option, - pub last_stdout: Option, - pub last_stderr: Option, + pub running: bool, + pub base_url: Option, + pub project_dir: Option, + pub hostname: Option, + pub port: Option, + pub pid: Option, + pub last_stdout: Option, + pub last_stderr: Option, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct EngineDoctorResult { - pub found: bool, - pub in_path: bool, - pub resolved_path: Option, - pub version: Option, - pub supports_serve: bool, - pub notes: Vec, - pub serve_help_status: Option, - pub serve_help_stdout: Option, - pub serve_help_stderr: Option, + pub found: bool, + pub in_path: bool, + pub resolved_path: Option, + pub version: Option, + pub supports_serve: bool, + pub notes: Vec, + pub serve_help_status: Option, + pub serve_help_stdout: Option, + pub serve_help_stderr: Option, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ExecResult { - pub ok: bool, - pub status: i32, - pub stdout: String, - pub stderr: String, + pub ok: bool, + pub status: i32, + pub stdout: String, + pub stderr: String, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct OpencodeConfigFile { - pub path: String, - pub exists: bool, - pub content: Option, + pub path: String, + pub exists: bool, + pub content: Option, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct UpdaterEnvironment { - pub supported: bool, - pub reason: Option, - pub executable_path: Option, - pub app_bundle_path: Option, + pub supported: bool, + pub reason: Option, + pub executable_path: Option, + pub app_bundle_path: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkspaceInfo { - pub id: String, - pub name: String, - pub path: String, - pub preset: String, + pub id: String, + pub name: String, + pub path: String, + pub preset: String, } #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkspaceList { - pub active_id: String, - pub workspaces: Vec, + pub active_id: String, + pub workspaces: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] struct WorkspaceStateV1 { - active_id: String, - workspaces: Vec, + active_id: String, + workspaces: Vec, } impl Default for WorkspaceStateV1 { - fn default() -> Self { - Self { - active_id: "starter".to_string(), - workspaces: Vec::new(), + fn default() -> Self { + Self { + active_id: "starter".to_string(), + workspaces: Vec::new(), + } } - } } fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) } fn stable_workspace_id(path: &str) -> String { - let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); - format!("ws_{:x}", hasher.finish()) + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + format!("ws_{:x}", hasher.finish()) } fn openwork_state_paths(app: &tauri::AppHandle) -> Result<(PathBuf, PathBuf), String> { - let app_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; + let app_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; - let state_dir = app_dir.join("state"); - let file_path = state_dir.join("workspaces.json"); + let state_dir = app_dir.join("state"); + let file_path = state_dir.join("workspaces.json"); - Ok((state_dir, file_path)) + Ok((state_dir, file_path)) } fn load_workspace_state(app: &tauri::AppHandle) -> Result { - let (_dir, path) = openwork_state_paths(app)?; + let (_dir, path) = openwork_state_paths(app)?; - if !path.exists() { - return Ok(WorkspaceStateV1::default()); - } + if !path.exists() { + return Ok(WorkspaceStateV1::default()); + } - let raw = fs::read_to_string(&path) - .map_err(|e| format!("Failed to read {}: {e}", path.display()))?; - serde_json::from_str::(&raw) - .map_err(|e| format!("Failed to parse {}: {e}", path.display())) + let raw = + fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {e}", path.display()))?; + serde_json::from_str::(&raw) + .map_err(|e| format!("Failed to parse {}: {e}", path.display())) } fn save_workspace_state(app: &tauri::AppHandle, state: &WorkspaceStateV1) -> Result<(), String> { - let (dir, path) = openwork_state_paths(app)?; - fs::create_dir_all(&dir).map_err(|e| format!("Failed to create {}: {e}", dir.display()))?; + let (dir, path) = openwork_state_paths(app)?; + fs::create_dir_all(&dir).map_err(|e| format!("Failed to create {}: {e}", dir.display()))?; - let json = serde_json::to_string_pretty(state).map_err(|e| e.to_string())?; - fs::write(&path, json).map_err(|e| format!("Failed to write {}: {e}", path.display()))?; - Ok(()) + let json = serde_json::to_string_pretty(state).map_err(|e| e.to_string())?; + fs::write(&path, json).map_err(|e| format!("Failed to write {}: {e}", path.display()))?; + Ok(()) } fn ensure_starter_workspace(app: &tauri::AppHandle) -> Result { - let app_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; + let app_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; - let starter_path = app_dir.join("workspaces").join("starter"); + let starter_path = app_dir.join("workspaces").join("starter"); - fs::create_dir_all(&starter_path) - .map_err(|e| format!("Failed to create starter workspace: {e}"))?; + fs::create_dir_all(&starter_path) + .map_err(|e| format!("Failed to create starter workspace: {e}"))?; - let id = "starter".to_string(); + let id = "starter".to_string(); - Ok(WorkspaceInfo { - id, - name: "Starter Workspace".to_string(), - path: starter_path.to_string_lossy().to_string(), - preset: "starter".to_string(), - }) + Ok(WorkspaceInfo { + id, + name: "Starter Workspace".to_string(), + path: starter_path.to_string_lossy().to_string(), + preset: "starter".to_string(), + }) } - fn merge_plugins(existing: Vec, required: &[&str]) -> Vec { - let mut next = existing; - for plugin in required { - if !next.iter().any(|p| p == plugin) { - next.push(plugin.to_string()); + let mut next = existing; + for plugin in required { + if !next.iter().any(|p| p == plugin) { + next.push(plugin.to_string()); + } } - } - next + next } fn sanitize_template_id(raw: &str) -> Option { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return None; - } + let trimmed = raw.trim(); + if trimmed.is_empty() { + return None; + } - let mut out = String::new(); - for ch in trimmed.chars() { - if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' { - out.push(ch); + let mut out = String::new(); + for ch in trimmed.chars() { + if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' { + out.push(ch); + } } - } - if out.is_empty() { - return None; - } + if out.is_empty() { + return None; + } - Some(out) + Some(out) } fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), String> { - let root = PathBuf::from(workspace_path); + let root = PathBuf::from(workspace_path); - let skill_root = root.join(".opencode").join("skill"); - fs::create_dir_all(&skill_root) - .map_err(|e| format!("Failed to create .opencode/skill: {e}"))?; + let skill_root = root.join(".opencode").join("skill"); + fs::create_dir_all(&skill_root) + .map_err(|e| format!("Failed to create .opencode/skill: {e}"))?; - // Seed workspace onboarding skill (required by onboarding PRD). - let guide_dir = skill_root.join("workspace_guide"); - if !guide_dir.exists() { - fs::create_dir_all(&guide_dir) - .map_err(|e| format!("Failed to create {}: {e}", guide_dir.display()))?; + // Seed workspace onboarding skill (required by onboarding PRD). + let guide_dir = skill_root.join("workspace_guide"); + if !guide_dir.exists() { + fs::create_dir_all(&guide_dir) + .map_err(|e| format!("Failed to create {}: {e}", guide_dir.display()))?; - let doc = r#"# Workspace Guide + let doc = r#"# Workspace Guide This workspace is a real folder with local configuration. @@ -350,21 +349,21 @@ This workspace is a real folder with local configuration. Be concise and practical."#; - fs::write(guide_dir.join("SKILL.md"), doc) - .map_err(|e| format!("Failed to write SKILL.md: {e}"))?; - } - - let templates_dir = root.join(".openwork").join("templates"); - fs::create_dir_all(&templates_dir) - .map_err(|e| format!("Failed to create .openwork/templates: {e}"))?; - - // Seed starter templates if the workspace is empty. - if fs::read_dir(&templates_dir) - .map_err(|e| format!("Failed to read {}: {e}", templates_dir.display()))? - .next() - .is_none() - { - let defaults = vec![ + fs::write(guide_dir.join("SKILL.md"), doc) + .map_err(|e| format!("Failed to write SKILL.md: {e}"))?; + } + + let templates_dir = root.join(".openwork").join("templates"); + fs::create_dir_all(&templates_dir) + .map_err(|e| format!("Failed to create .openwork/templates: {e}"))?; + + // Seed starter templates if the workspace is empty. + if fs::read_dir(&templates_dir) + .map_err(|e| format!("Failed to read {}: {e}", templates_dir.display()))? + .next() + .is_none() + { + let defaults = vec![ WorkspaceTemplate { id: "tmpl_understand_workspace".to_string(), title: "Understand this workspace".to_string(), @@ -395,1020 +394,1084 @@ Be concise and practical."#; }, ]; - for template in defaults { - let file_path = templates_dir.join(format!("{}.json", template.id)); - fs::write( - &file_path, - serde_json::to_string_pretty(&template).map_err(|e| e.to_string())?, - ) - .map_err(|e| format!("Failed to write {}: {e}", file_path.display()))?; - } - } - - let config_path = root.join("opencode.json"); - let mut config: serde_json::Value = if config_path.exists() { - let raw = fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read {}: {e}", config_path.display()))?; - serde_json::from_str(&raw).unwrap_or_else(|_| serde_json::json!({})) - } else { - serde_json::json!({ - "$schema": "https://opencode.ai/config.json" - }) - }; + for template in defaults { + let file_path = templates_dir.join(format!("{}.json", template.id)); + fs::write( + &file_path, + serde_json::to_string_pretty(&template).map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Failed to write {}: {e}", file_path.display()))?; + } + } - if !config.is_object() { - config = serde_json::json!({ - "$schema": "https://opencode.ai/config.json" - }); - } - - let required_plugins: Vec<&str> = match preset { - "starter" => vec!["opencode-scheduler"], - "automation" => vec!["opencode-scheduler"], - _ => vec![], - }; - - if !required_plugins.is_empty() { - let plugins_value = config - .get("plugin") - .cloned() - .unwrap_or_else(|| serde_json::json!([])); - - let existing_plugins: Vec = match plugins_value { - serde_json::Value::Array(arr) => arr - .into_iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(), - serde_json::Value::String(s) => vec![s], - _ => vec![], + let config_path = root.join("opencode.json"); + let mut config: serde_json::Value = if config_path.exists() { + let raw = fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read {}: {e}", config_path.display()))?; + serde_json::from_str(&raw).unwrap_or_else(|_| serde_json::json!({})) + } else { + serde_json::json!({ + "$schema": "https://opencode.ai/config.json" + }) }; - let merged = merge_plugins(existing_plugins, &required_plugins); - if let Some(obj) = config.as_object_mut() { - obj.insert( - "plugin".to_string(), - serde_json::Value::Array(merged.into_iter().map(serde_json::Value::String).collect()), - ); + if !config.is_object() { + config = serde_json::json!({ + "$schema": "https://opencode.ai/config.json" + }); } - } - - fs::write(&config_path, serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?) - .map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?; - let openwork_path = root.join(".opencode").join("openwork.json"); - if !openwork_path.exists() { - let openwork = WorkspaceOpenworkConfig::new(workspace_path, preset); + let required_plugins: Vec<&str> = match preset { + "starter" => vec!["opencode-scheduler"], + "automation" => vec!["opencode-scheduler"], + _ => vec![], + }; - fs::create_dir_all(openwork_path.parent().unwrap()) - .map_err(|e| format!("Failed to create {}: {e}", openwork_path.display()))?; + if !required_plugins.is_empty() { + let plugins_value = config + .get("plugin") + .cloned() + .unwrap_or_else(|| serde_json::json!([])); + + let existing_plugins: Vec = match plugins_value { + serde_json::Value::Array(arr) => arr + .into_iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect(), + serde_json::Value::String(s) => vec![s], + _ => vec![], + }; + + let merged = merge_plugins(existing_plugins, &required_plugins); + if let Some(obj) = config.as_object_mut() { + obj.insert( + "plugin".to_string(), + serde_json::Value::Array( + merged.into_iter().map(serde_json::Value::String).collect(), + ), + ); + } + } fs::write( - &openwork_path, - serde_json::to_string_pretty(&openwork).map_err(|e| e.to_string())?, + &config_path, + serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?, ) - .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; - } + .map_err(|e| format!("Failed to write {}: {e}", config_path.display()))?; + + let openwork_path = root.join(".opencode").join("openwork.json"); + if !openwork_path.exists() { + let openwork = WorkspaceOpenworkConfig::new(workspace_path, preset); + + fs::create_dir_all(openwork_path.parent().unwrap()) + .map_err(|e| format!("Failed to create {}: {e}", openwork_path.display()))?; + + fs::write( + &openwork_path, + serde_json::to_string_pretty(&openwork).map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; + } - Ok(()) + Ok(()) } #[tauri::command] fn workspace_bootstrap(app: tauri::AppHandle) -> Result { - let mut state = load_workspace_state(&app)?; + let mut state = load_workspace_state(&app)?; - // Ensure starter workspace always exists. - let starter = ensure_starter_workspace(&app)?; - ensure_workspace_files(&starter.path, &starter.preset)?; + // Ensure starter workspace always exists. + let starter = ensure_starter_workspace(&app)?; + ensure_workspace_files(&starter.path, &starter.preset)?; - if !state.workspaces.iter().any(|w| w.id == starter.id) { - state.workspaces.push(starter.clone()); - } + if !state.workspaces.iter().any(|w| w.id == starter.id) { + state.workspaces.push(starter.clone()); + } - if state.active_id.trim().is_empty() { - state.active_id = starter.id.clone(); - } + if state.active_id.trim().is_empty() { + state.active_id = starter.id.clone(); + } - if !state.workspaces.iter().any(|w| w.id == state.active_id) { - state.active_id = starter.id.clone(); - } + if !state.workspaces.iter().any(|w| w.id == state.active_id) { + state.active_id = starter.id.clone(); + } - save_workspace_state(&app, &state)?; + save_workspace_state(&app, &state)?; - Ok(WorkspaceList { - active_id: state.active_id, - workspaces: state.workspaces, - }) + Ok(WorkspaceList { + active_id: state.active_id, + workspaces: state.workspaces, + }) } #[tauri::command] -fn workspace_set_active(app: tauri::AppHandle, workspace_id: String) -> Result { - let mut state = load_workspace_state(&app)?; - let id = workspace_id.trim(); +fn workspace_set_active( + app: tauri::AppHandle, + workspace_id: String, +) -> Result { + let mut state = load_workspace_state(&app)?; + let id = workspace_id.trim(); - if id.is_empty() { - return Err("workspaceId is required".to_string()); - } + if id.is_empty() { + return Err("workspaceId is required".to_string()); + } - if !state.workspaces.iter().any(|w| w.id == id) { - return Err("Unknown workspaceId".to_string()); - } + if !state.workspaces.iter().any(|w| w.id == id) { + return Err("Unknown workspaceId".to_string()); + } - state.active_id = id.to_string(); - save_workspace_state(&app, &state)?; + state.active_id = id.to_string(); + save_workspace_state(&app, &state)?; - Ok(WorkspaceList { - active_id: state.active_id, - workspaces: state.workspaces, - }) + Ok(WorkspaceList { + active_id: state.active_id, + workspaces: state.workspaces, + }) } #[tauri::command] fn workspace_create( - app: tauri::AppHandle, - folder_path: String, - name: String, - preset: String, + app: tauri::AppHandle, + folder_path: String, + name: String, + preset: String, ) -> Result { - let folder = folder_path.trim().to_string(); - if folder.is_empty() { - return Err("folderPath is required".to_string()); - } + let folder = folder_path.trim().to_string(); + if folder.is_empty() { + return Err("folderPath is required".to_string()); + } - let workspace_name = name.trim().to_string(); - if workspace_name.is_empty() { - return Err("name is required".to_string()); - } + let workspace_name = name.trim().to_string(); + if workspace_name.is_empty() { + return Err("name is required".to_string()); + } - let preset = preset.trim().to_string(); - let preset = if preset.is_empty() { "starter".to_string() } else { preset }; + let preset = preset.trim().to_string(); + let preset = if preset.is_empty() { + "starter".to_string() + } else { + preset + }; - fs::create_dir_all(&folder) - .map_err(|e| format!("Failed to create workspace folder: {e}"))?; + fs::create_dir_all(&folder).map_err(|e| format!("Failed to create workspace folder: {e}"))?; - let id = stable_workspace_id(&folder); + let id = stable_workspace_id(&folder); - ensure_workspace_files(&folder, &preset)?; + ensure_workspace_files(&folder, &preset)?; - let mut state = load_workspace_state(&app)?; + let mut state = load_workspace_state(&app)?; - // Replace existing entry with same id. - state.workspaces.retain(|w| w.id != id); - state.workspaces.push(WorkspaceInfo { - id: id.clone(), - name: workspace_name, - path: folder, - preset, - }); + // Replace existing entry with same id. + state.workspaces.retain(|w| w.id != id); + state.workspaces.push(WorkspaceInfo { + id: id.clone(), + name: workspace_name, + path: folder, + preset, + }); - state.active_id = id; - save_workspace_state(&app, &state)?; + state.active_id = id; + save_workspace_state(&app, &state)?; - Ok(WorkspaceList { - active_id: state.active_id, - workspaces: state.workspaces, - }) + Ok(WorkspaceList { + active_id: state.active_id, + workspaces: state.workspaces, + }) } #[tauri::command] fn workspace_add_authorized_root( - _app: tauri::AppHandle, - workspace_path: String, - folder_path: String, + _app: tauri::AppHandle, + workspace_path: String, + folder_path: String, ) -> Result { - let workspace_path = workspace_path.trim().to_string(); - let folder_path = folder_path.trim().to_string(); - - if workspace_path.is_empty() { - return Err("workspacePath is required".to_string()); - } - if folder_path.is_empty() { - return Err("folderPath is required".to_string()); - } - - let openwork_path = PathBuf::from(&workspace_path) - .join(".opencode") - .join("openwork.json"); - - if let Some(parent) = openwork_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create {}: {e}", parent.display()))?; - } - - let mut config: WorkspaceOpenworkConfig = if openwork_path.exists() { - let raw = fs::read_to_string(&openwork_path) - .map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?; - serde_json::from_str(&raw).unwrap_or_default() - } else { - let mut cfg = WorkspaceOpenworkConfig::default(); - if !cfg.authorized_roots.iter().any(|p| p == &workspace_path) { - cfg.authorized_roots.push(workspace_path.clone()); - } - cfg - }; - - if !config.authorized_roots.iter().any(|p| p == &folder_path) { - config.authorized_roots.push(folder_path); - } - - fs::write( - &openwork_path, - serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?, - ) - .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; - - Ok(ExecResult { - ok: true, - status: 0, - stdout: "Updated authorizedRoots".to_string(), - stderr: String::new(), - }) + let workspace_path = workspace_path.trim().to_string(); + let folder_path = folder_path.trim().to_string(); + + if workspace_path.is_empty() { + return Err("workspacePath is required".to_string()); + } + if folder_path.is_empty() { + return Err("folderPath is required".to_string()); + } + + let openwork_path = PathBuf::from(&workspace_path) + .join(".opencode") + .join("openwork.json"); + + if let Some(parent) = openwork_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create {}: {e}", parent.display()))?; + } + + let mut config: WorkspaceOpenworkConfig = if openwork_path.exists() { + let raw = fs::read_to_string(&openwork_path) + .map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?; + serde_json::from_str(&raw).unwrap_or_default() + } else { + let mut cfg = WorkspaceOpenworkConfig::default(); + if !cfg.authorized_roots.iter().any(|p| p == &workspace_path) { + cfg.authorized_roots.push(workspace_path.clone()); + } + cfg + }; + + if !config.authorized_roots.iter().any(|p| p == &folder_path) { + config.authorized_roots.push(folder_path); + } + + fs::write( + &openwork_path, + serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; + + Ok(ExecResult { + ok: true, + status: 0, + stdout: "Updated authorizedRoots".to_string(), + stderr: String::new(), + }) } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct WorkspaceTemplate { - pub id: String, - pub title: String, - pub description: String, - pub prompt: String, - #[serde(default)] - pub created_at: u64, + pub id: String, + pub title: String, + pub description: String, + pub prompt: String, + #[serde(default)] + pub created_at: u64, } #[tauri::command] fn workspace_template_write( - _app: tauri::AppHandle, - workspace_path: String, - template: WorkspaceTemplate, + _app: tauri::AppHandle, + workspace_path: String, + template: WorkspaceTemplate, ) -> Result { - let workspace_path = workspace_path.trim().to_string(); - if workspace_path.is_empty() { - return Err("workspacePath is required".to_string()); - } - - let Some(template_id) = sanitize_template_id(&template.id) else { - return Err("template.id is required".to_string()); - }; - - let templates_dir = PathBuf::from(&workspace_path) - .join(".openwork") - .join("templates"); - - fs::create_dir_all(&templates_dir) - .map_err(|e| format!("Failed to create {}: {e}", templates_dir.display()))?; - - let payload = WorkspaceTemplate { - id: template_id.clone(), - title: template.title, - description: template.description, - prompt: template.prompt, - created_at: if template.created_at > 0 { template.created_at } else { now_ms() }, - }; - - let file_path = templates_dir.join(format!("{}.json", template_id)); - fs::write( - &file_path, - serde_json::to_string_pretty(&payload).map_err(|e| e.to_string())?, - ) - .map_err(|e| format!("Failed to write {}: {e}", file_path.display()))?; - - Ok(ExecResult { - ok: true, - status: 0, - stdout: format!("Wrote {}", file_path.display()), - stderr: String::new(), - }) + let workspace_path = workspace_path.trim().to_string(); + if workspace_path.is_empty() { + return Err("workspacePath is required".to_string()); + } + + let Some(template_id) = sanitize_template_id(&template.id) else { + return Err("template.id is required".to_string()); + }; + + let templates_dir = PathBuf::from(&workspace_path) + .join(".openwork") + .join("templates"); + + fs::create_dir_all(&templates_dir) + .map_err(|e| format!("Failed to create {}: {e}", templates_dir.display()))?; + + let payload = WorkspaceTemplate { + id: template_id.clone(), + title: template.title, + description: template.description, + prompt: template.prompt, + created_at: if template.created_at > 0 { + template.created_at + } else { + now_ms() + }, + }; + + let file_path = templates_dir.join(format!("{}.json", template_id)); + fs::write( + &file_path, + serde_json::to_string_pretty(&payload).map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Failed to write {}: {e}", file_path.display()))?; + + Ok(ExecResult { + ok: true, + status: 0, + stdout: format!("Wrote {}", file_path.display()), + stderr: String::new(), + }) } #[tauri::command] fn workspace_openwork_read( - _app: tauri::AppHandle, - workspace_path: String, + _app: tauri::AppHandle, + workspace_path: String, ) -> Result { - let workspace_path = workspace_path.trim().to_string(); - if workspace_path.is_empty() { - return Err("workspacePath is required".to_string()); - } - - let openwork_path = PathBuf::from(&workspace_path) - .join(".opencode") - .join("openwork.json"); - - if !openwork_path.exists() { - let mut cfg = WorkspaceOpenworkConfig::default(); - cfg.authorized_roots.push(workspace_path); - return Ok(cfg); - } - - let raw = fs::read_to_string(&openwork_path) - .map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?; - - serde_json::from_str::(&raw).map_err(|e| { - format!( - "Failed to parse {}: {e}", - openwork_path.display() - ) - }) + let workspace_path = workspace_path.trim().to_string(); + if workspace_path.is_empty() { + return Err("workspacePath is required".to_string()); + } + + let openwork_path = PathBuf::from(&workspace_path) + .join(".opencode") + .join("openwork.json"); + + if !openwork_path.exists() { + let mut cfg = WorkspaceOpenworkConfig::default(); + cfg.authorized_roots.push(workspace_path); + return Ok(cfg); + } + + let raw = fs::read_to_string(&openwork_path) + .map_err(|e| format!("Failed to read {}: {e}", openwork_path.display()))?; + + serde_json::from_str::(&raw) + .map_err(|e| format!("Failed to parse {}: {e}", openwork_path.display())) } #[tauri::command] fn workspace_openwork_write( - _app: tauri::AppHandle, - workspace_path: String, - config: WorkspaceOpenworkConfig, + _app: tauri::AppHandle, + workspace_path: String, + config: WorkspaceOpenworkConfig, ) -> Result { - let workspace_path = workspace_path.trim().to_string(); - if workspace_path.is_empty() { - return Err("workspacePath is required".to_string()); - } - - let openwork_path = PathBuf::from(&workspace_path) - .join(".opencode") - .join("openwork.json"); - - if let Some(parent) = openwork_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create {}: {e}", parent.display()))?; - } - - fs::write( - &openwork_path, - serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?, - ) - .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; - - Ok(ExecResult { - ok: true, - status: 0, - stdout: format!("Wrote {}", openwork_path.display()), - stderr: String::new(), - }) + let workspace_path = workspace_path.trim().to_string(); + if workspace_path.is_empty() { + return Err("workspacePath is required".to_string()); + } + + let openwork_path = PathBuf::from(&workspace_path) + .join(".opencode") + .join("openwork.json"); + + if let Some(parent) = openwork_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create {}: {e}", parent.display()))?; + } + + fs::write( + &openwork_path, + serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?, + ) + .map_err(|e| format!("Failed to write {}: {e}", openwork_path.display()))?; + + Ok(ExecResult { + ok: true, + status: 0, + stdout: format!("Wrote {}", openwork_path.display()), + stderr: String::new(), + }) } #[tauri::command] fn workspace_template_delete( - _app: tauri::AppHandle, - workspace_path: String, - template_id: String, + _app: tauri::AppHandle, + workspace_path: String, + template_id: String, ) -> Result { - let workspace_path = workspace_path.trim().to_string(); - if workspace_path.is_empty() { - return Err("workspacePath is required".to_string()); - } - - let Some(template_id) = sanitize_template_id(&template_id) else { - return Err("templateId is required".to_string()); - }; - - let file_path = PathBuf::from(&workspace_path) - .join(".openwork") - .join("templates") - .join(format!("{}.json", template_id)); - - if file_path.exists() { - fs::remove_file(&file_path) - .map_err(|e| format!("Failed to delete {}: {e}", file_path.display()))?; - } - - Ok(ExecResult { - ok: true, - status: 0, - stdout: format!("Deleted {}", file_path.display()), - stderr: String::new(), - }) + let workspace_path = workspace_path.trim().to_string(); + if workspace_path.is_empty() { + return Err("workspacePath is required".to_string()); + } + + let Some(template_id) = sanitize_template_id(&template_id) else { + return Err("templateId is required".to_string()); + }; + + let file_path = PathBuf::from(&workspace_path) + .join(".openwork") + .join("templates") + .join(format!("{}.json", template_id)); + + if file_path.exists() { + fs::remove_file(&file_path) + .map_err(|e| format!("Failed to delete {}: {e}", file_path.display()))?; + } + + Ok(ExecResult { + ok: true, + status: 0, + stdout: format!("Deleted {}", file_path.display()), + stderr: String::new(), + }) } fn find_free_port() -> Result { - let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| e.to_string())?; - let port = listener.local_addr().map_err(|e| e.to_string())?.port(); - Ok(port) + let listener = TcpListener::bind(("127.0.0.1", 0)).map_err(|e| e.to_string())?; + let port = listener.local_addr().map_err(|e| e.to_string())?.port(); + Ok(port) } #[cfg(windows)] -const OPENCODE_EXECUTABLE: &str = "opencode.exe"; +const OPENCODE_EXECUTABLES: &[&str] = &["opencode.exe", "opencode.cmd", "opencode.bat"]; #[cfg(not(windows))] -const OPENCODE_EXECUTABLE: &str = "opencode"; +const OPENCODE_EXECUTABLES: &[&str] = &["opencode"]; fn home_dir() -> Option { - if let Ok(home) = env::var("HOME") { - if !home.trim().is_empty() { - return Some(PathBuf::from(home)); + if let Ok(home) = env::var("HOME") { + if !home.trim().is_empty() { + return Some(PathBuf::from(home)); + } } - } - if let Ok(profile) = env::var("USERPROFILE") { - if !profile.trim().is_empty() { - return Some(PathBuf::from(profile)); + if let Ok(profile) = env::var("USERPROFILE") { + if !profile.trim().is_empty() { + return Some(PathBuf::from(profile)); + } } - } - None + None } fn path_entries() -> Vec { - let mut entries = Vec::new(); - let Some(path) = env::var_os("PATH") else { - return entries; - }; + let mut entries = Vec::new(); + let Some(path) = env::var_os("PATH") else { + return entries; + }; - entries.extend(env::split_paths(&path)); - entries + entries.extend(env::split_paths(&path)); + entries } -fn resolve_in_path(name: &str) -> Option { - for dir in path_entries() { - let candidate = dir.join(name); - if candidate.is_file() { - return Some(candidate); +fn resolve_in_path(names: &[&str]) -> Option { + for dir in path_entries() { + for name in names { + let candidate = dir.join(name); + if candidate.is_file() { + return Some(candidate); + } + } } - } - None + None } fn candidate_opencode_paths() -> Vec { - let mut candidates = Vec::new(); + let mut candidates = Vec::new(); + + if let Some(home) = home_dir() { + for exe in OPENCODE_EXECUTABLES { + candidates.push(home.join(".opencode").join("bin").join(exe)); + } + } + + // Windows npm global install location + #[cfg(windows)] + { + if let Ok(appdata) = env::var("APPDATA") { + for exe in OPENCODE_EXECUTABLES { + candidates.push(PathBuf::from(&appdata).join("npm").join(exe)); + } + } + } - if let Some(home) = home_dir() { - candidates.push(home.join(".opencode").join("bin").join(OPENCODE_EXECUTABLE)); - } + // Homebrew default paths (macOS/Linux). + for exe in OPENCODE_EXECUTABLES { + candidates.push(PathBuf::from("/opt/homebrew/bin").join(exe)); + candidates.push(PathBuf::from("/usr/local/bin").join(exe)); + candidates.push(PathBuf::from("/usr/bin").join(exe)); + } - // Homebrew default paths. - candidates.push(PathBuf::from("/opt/homebrew/bin").join(OPENCODE_EXECUTABLE)); - candidates.push(PathBuf::from("/usr/local/bin").join(OPENCODE_EXECUTABLE)); + candidates +} - // Common Linux paths. - candidates.push(PathBuf::from("/usr/bin").join(OPENCODE_EXECUTABLE)); - candidates.push(PathBuf::from("/usr/local/bin").join(OPENCODE_EXECUTABLE)); +/// Creates a Command for running opencode. +/// On Windows, .cmd and .bat files must be invoked via cmd.exe /C. +fn create_opencode_command(program: &OsStr) -> Command { + #[cfg(windows)] + { + let program_str = program.to_string_lossy(); + if program_str.ends_with(".cmd") || program_str.ends_with(".bat") { + let mut cmd = Command::new("cmd.exe"); + cmd.arg("/C").arg(program); + return cmd; + } + } - candidates + Command::new(program) } fn opencode_version(program: &OsStr) -> Option { - let output = Command::new(program).arg("--version").output().ok()?; - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let mut command = create_opencode_command(program); + command.arg("--version"); + + let output = command.output().ok()?; + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - if !stdout.is_empty() { - return Some(stdout); - } - if !stderr.is_empty() { - return Some(stderr); - } + if !stdout.is_empty() { + return Some(stdout); + } + if !stderr.is_empty() { + return Some(stderr); + } - None + None } fn truncate_output(input: &str, max_chars: usize) -> String { - if input.len() <= max_chars { - return input.to_string(); - } + if input.len() <= max_chars { + return input.to_string(); + } - // Keep tail to preserve error context. - input.chars().skip(input.chars().count() - max_chars).collect() + // Keep tail to preserve error context. + input + .chars() + .skip(input.chars().count() - max_chars) + .collect() } fn opencode_serve_help(program: &OsStr) -> (bool, Option, Option, Option) { - match Command::new(program).arg("serve").arg("--help").output() { - Ok(output) => { - let status = output.status.code(); - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - let ok = output.status.success(); - - let stdout = if stdout.is_empty() { - None - } else { - Some(truncate_output(&stdout, 4000)) - }; - let stderr = if stderr.is_empty() { - None - } else { - Some(truncate_output(&stderr, 4000)) - }; - - (ok, status, stdout, stderr) + let mut command = create_opencode_command(program); + command.arg("serve").arg("--help"); + + match command.output() { + Ok(output) => { + let status = output.status.code(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let ok = output.status.success(); + + let stdout = if stdout.is_empty() { + None + } else { + Some(truncate_output(&stdout, 4000)) + }; + let stderr = if stderr.is_empty() { + None + } else { + Some(truncate_output(&stderr, 4000)) + }; + + (ok, status, stdout, stderr) + } + Err(_) => (false, None, None, None), } - Err(_) => (false, None, None, None), - } } fn resolve_opencode_executable() -> (Option, bool, Vec) { - let mut notes = Vec::new(); + let mut notes = Vec::new(); + + // Prefer explicit override. + if let Ok(custom) = env::var("OPENCODE_BIN_PATH") { + let custom = custom.trim(); + if !custom.is_empty() { + let candidate = PathBuf::from(custom); + if candidate.is_file() { + notes.push(format!("Using OPENCODE_BIN_PATH: {}", candidate.display())); + return (Some(candidate), false, notes); + } + notes.push(format!( + "OPENCODE_BIN_PATH set but missing: {}", + candidate.display() + )); + } + } - // Prefer explicit override. - if let Ok(custom) = env::var("OPENCODE_BIN_PATH") { - let custom = custom.trim(); - if !custom.is_empty() { - let candidate = PathBuf::from(custom); - if candidate.is_file() { - notes.push(format!("Using OPENCODE_BIN_PATH: {}", candidate.display())); - return (Some(candidate), false, notes); - } - notes.push(format!("OPENCODE_BIN_PATH set but missing: {}", candidate.display())); + if let Some(path) = resolve_in_path(OPENCODE_EXECUTABLES) { + notes.push(format!("Found in PATH: {}", path.display())); + return (Some(path), true, notes); } - } - if let Some(path) = resolve_in_path(OPENCODE_EXECUTABLE) { - notes.push(format!("Found in PATH: {}", path.display())); - return (Some(path), true, notes); - } + notes.push("Not found on PATH".to_string()); - notes.push("Not found on PATH".to_string()); + for candidate in candidate_opencode_paths() { + if candidate.is_file() { + notes.push(format!("Found at {}", candidate.display())); + return (Some(candidate), false, notes); + } - for candidate in candidate_opencode_paths() { - if candidate.is_file() { - notes.push(format!("Found at {}", candidate.display())); - return (Some(candidate), false, notes); + notes.push(format!("Missing: {}", candidate.display())); } - notes.push(format!("Missing: {}", candidate.display())); - } - - (None, false, notes) + (None, false, notes) } fn run_capture_optional(command: &mut Command) -> Result, String> { - match command.output() { - Ok(output) => { - let status = output.status.code().unwrap_or(-1); - Ok(Some(ExecResult { - ok: output.status.success(), - status, - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - })) - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(format!( - "Failed to run {}: {e}", - command.get_program().to_string_lossy() - )), - } + match command.output() { + Ok(output) => { + let status = output.status.code().unwrap_or(-1); + Ok(Some(ExecResult { + ok: output.status.success(), + status, + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + })) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(format!( + "Failed to run {}: {e}", + command.get_program().to_string_lossy() + )), + } } fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> { - if !src.is_dir() { - return Err(format!("Source is not a directory: {}", src.display())); - } + if !src.is_dir() { + return Err(format!("Source is not a directory: {}", src.display())); + } - fs::create_dir_all(dest).map_err(|e| format!("Failed to create dir {}: {e}", dest.display()))?; + fs::create_dir_all(dest) + .map_err(|e| format!("Failed to create dir {}: {e}", dest.display()))?; - for entry in fs::read_dir(src).map_err(|e| format!("Failed to read dir {}: {e}", src.display()))? { - let entry = entry.map_err(|e| e.to_string())?; - let file_type = entry.file_type().map_err(|e| e.to_string())?; + for entry in + fs::read_dir(src).map_err(|e| format!("Failed to read dir {}: {e}", src.display()))? + { + let entry = entry.map_err(|e| e.to_string())?; + let file_type = entry.file_type().map_err(|e| e.to_string())?; - let from = entry.path(); - let to = dest.join(entry.file_name()); + let from = entry.path(); + let to = dest.join(entry.file_name()); - if file_type.is_dir() { - copy_dir_recursive(&from, &to)?; - continue; - } + if file_type.is_dir() { + copy_dir_recursive(&from, &to)?; + continue; + } - if file_type.is_file() { - fs::copy(&from, &to) - .map_err(|e| format!("Failed to copy {} -> {}: {e}", from.display(), to.display()))?; - continue; - } + if file_type.is_file() { + fs::copy(&from, &to).map_err(|e| { + format!("Failed to copy {} -> {}: {e}", from.display(), to.display()) + })?; + continue; + } - // Skip symlinks and other non-regular entries. - } + // Skip symlinks and other non-regular entries. + } - Ok(()) + Ok(()) } fn is_mac_dmg_or_translocated(path: &Path) -> bool { - let path_str = path.to_string_lossy(); - path_str.contains("/Volumes/") || path_str.contains("AppTranslocation") + let path_str = path.to_string_lossy(); + path_str.contains("/Volumes/") || path_str.contains("AppTranslocation") } #[tauri::command] fn updater_environment(_app: tauri::AppHandle) -> UpdaterEnvironment { - let executable_path = std::env::current_exe().ok(); - - let app_bundle_path = executable_path - .as_ref() - .and_then(|exe| exe.parent()) - .and_then(|p| p.parent()) - .and_then(|p| p.parent()) - .map(|p| p.to_path_buf()); - - let mut supported = true; - let mut reason: Option = None; - - if let Some(exe) = executable_path.as_ref() { - if is_mac_dmg_or_translocated(exe) { - supported = false; - reason = Some( + let executable_path = std::env::current_exe().ok(); + + let app_bundle_path = executable_path + .as_ref() + .and_then(|exe| exe.parent()) + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .map(|p| p.to_path_buf()); + + let mut supported = true; + let mut reason: Option = None; + + if let Some(exe) = executable_path.as_ref() { + if is_mac_dmg_or_translocated(exe) { + supported = false; + reason = Some( "OpenWork is running from a mounted disk image. Install it to Applications to enable updates." .to_string(), ); + } } - } - if supported { - if let Some(bundle) = app_bundle_path.as_ref() { - if is_mac_dmg_or_translocated(bundle) { - supported = false; - reason = Some( + if supported { + if let Some(bundle) = app_bundle_path.as_ref() { + if is_mac_dmg_or_translocated(bundle) { + supported = false; + reason = Some( "OpenWork is running from a mounted disk image. Install it to Applications to enable updates." .to_string(), ); - } + } + } } - } - UpdaterEnvironment { - supported, - reason, - executable_path: executable_path.map(|p| p.to_string_lossy().to_string()), - app_bundle_path: app_bundle_path.map(|p| p.to_string_lossy().to_string()), - } + UpdaterEnvironment { + supported, + reason, + executable_path: executable_path.map(|p| p.to_string_lossy().to_string()), + app_bundle_path: app_bundle_path.map(|p| p.to_string_lossy().to_string()), + } } #[tauri::command] fn reset_openwork_state(app: tauri::AppHandle, mode: String) -> Result<(), String> { - let mode = mode.trim(); - if mode != "onboarding" && mode != "all" { - return Err("mode must be 'onboarding' or 'all'".to_string()); - } + let mode = mode.trim(); + if mode != "onboarding" && mode != "all" { + return Err("mode must be 'onboarding' or 'all'".to_string()); + } - let cache_dir = app - .path() - .app_cache_dir() - .map_err(|e| format!("Failed to resolve app cache dir: {e}"))?; + let cache_dir = app + .path() + .app_cache_dir() + .map_err(|e| format!("Failed to resolve app cache dir: {e}"))?; - if cache_dir.exists() { - fs::remove_dir_all(&cache_dir) - .map_err(|e| format!("Failed to remove cache dir {}: {e}", cache_dir.display()))?; - } + if cache_dir.exists() { + fs::remove_dir_all(&cache_dir) + .map_err(|e| format!("Failed to remove cache dir {}: {e}", cache_dir.display()))?; + } - if mode == "all" { - let data_dir = app - .path() - .app_data_dir() - .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; + if mode == "all" { + let data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; - if data_dir.exists() { - fs::remove_dir_all(&data_dir) - .map_err(|e| format!("Failed to remove data dir {}: {e}", data_dir.display()))?; + if data_dir.exists() { + fs::remove_dir_all(&data_dir) + .map_err(|e| format!("Failed to remove data dir {}: {e}", data_dir.display()))?; + } } - } - Ok(()) + Ok(()) } fn resolve_opencode_config_path(scope: &str, project_dir: &str) -> Result { - match scope { - "project" => { - if project_dir.trim().is_empty() { - return Err("projectDir is required".to_string()); - } - Ok(PathBuf::from(project_dir).join("opencode.json")) - } - "global" => { - let base = if let Ok(dir) = env::var("XDG_CONFIG_HOME") { - PathBuf::from(dir) - } else if let Ok(home) = env::var("HOME") { - PathBuf::from(home).join(".config") - } else { - return Err("Unable to resolve config directory".to_string()); - }; - - Ok(base.join("opencode").join("opencode.json")) + match scope { + "project" => { + if project_dir.trim().is_empty() { + return Err("projectDir is required".to_string()); + } + Ok(PathBuf::from(project_dir).join("opencode.json")) + } + "global" => { + let base = if let Ok(dir) = env::var("XDG_CONFIG_HOME") { + PathBuf::from(dir) + } else if let Ok(appdata) = env::var("APPDATA") { + // Windows: use %APPDATA% + PathBuf::from(appdata) + } else if let Ok(home) = env::var("HOME") { + PathBuf::from(home).join(".config") + } else if let Ok(userprofile) = env::var("USERPROFILE") { + // Windows fallback: use %USERPROFILE%\.config + PathBuf::from(userprofile).join(".config") + } else { + return Err("Unable to resolve config directory".to_string()); + }; + + Ok(base.join("opencode").join("opencode.json")) + } + _ => Err("scope must be 'project' or 'global'".to_string()), } - _ => Err("scope must be 'project' or 'global'".to_string()), - } } impl EngineManager { - fn snapshot_locked(state: &mut EngineState) -> EngineInfo { - let (running, pid) = match state.child.as_mut() { - None => (false, None), - Some(child) => match child.try_wait() { - Ok(Some(_status)) => { - // Process exited. - state.child = None; - (false, None) + fn snapshot_locked(state: &mut EngineState) -> EngineInfo { + let (running, pid) = match state.child.as_mut() { + None => (false, None), + Some(child) => match child.try_wait() { + Ok(Some(_status)) => { + // Process exited. + state.child = None; + (false, None) + } + Ok(None) => (true, Some(child.id())), + Err(_) => (true, Some(child.id())), + }, + }; + + EngineInfo { + running, + base_url: state.base_url.clone(), + project_dir: state.project_dir.clone(), + hostname: state.hostname.clone(), + port: state.port, + pid, + last_stdout: state.last_stdout.clone(), + last_stderr: state.last_stderr.clone(), } - Ok(None) => (true, Some(child.id())), - Err(_) => (true, Some(child.id())), - }, - }; + } - EngineInfo { - running, - base_url: state.base_url.clone(), - project_dir: state.project_dir.clone(), - hostname: state.hostname.clone(), - port: state.port, - pid, - last_stdout: state.last_stdout.clone(), - last_stderr: state.last_stderr.clone(), - } - } - - fn stop_locked(state: &mut EngineState) { - if let Some(mut child) = state.child.take() { - let _ = child.kill(); - let _ = child.wait(); - } - state.base_url = None; - state.project_dir = None; - state.hostname = None; - state.port = None; - state.last_stdout = None; - state.last_stderr = None; - } + fn stop_locked(state: &mut EngineState) { + if let Some(mut child) = state.child.take() { + let _ = child.kill(); + let _ = child.wait(); + } + state.base_url = None; + state.project_dir = None; + state.hostname = None; + state.port = None; + state.last_stdout = None; + state.last_stderr = None; + } } #[tauri::command] fn engine_info(manager: State) -> EngineInfo { - let mut state = manager.inner.lock().expect("engine mutex poisoned"); - EngineManager::snapshot_locked(&mut state) + let mut state = manager.inner.lock().expect("engine mutex poisoned"); + EngineManager::snapshot_locked(&mut state) } #[tauri::command] fn engine_stop(manager: State) -> EngineInfo { - let mut state = manager.inner.lock().expect("engine mutex poisoned"); - EngineManager::stop_locked(&mut state); - EngineManager::snapshot_locked(&mut state) + let mut state = manager.inner.lock().expect("engine mutex poisoned"); + EngineManager::stop_locked(&mut state); + EngineManager::snapshot_locked(&mut state) } fn resolve_sidecar_candidate(prefer_sidecar: bool) -> (Option, Vec) { - if !prefer_sidecar { - return (None, Vec::new()); - } + if !prefer_sidecar { + return (None, Vec::new()); + } - let mut notes = Vec::new(); + let mut notes = Vec::new(); - #[cfg(not(windows))] - { - // Best-effort: if we eventually bundle a binary, it will likely live here (dev) or be - // injected during bundling. - let candidate = PathBuf::from("src-tauri/sidecars").join(OPENCODE_EXECUTABLE); - if candidate.is_file() { - notes.push(format!("Using bundled sidecar: {}", candidate.display())); - return (Some(candidate), notes); - } + #[cfg(not(windows))] + { + // Best-effort: if we eventually bundle a binary, it will likely live here (dev) or be + // injected during bundling. + let candidate = PathBuf::from("src-tauri/sidecars").join(OPENCODE_EXECUTABLES[0]); + if candidate.is_file() { + notes.push(format!("Using bundled sidecar: {}", candidate.display())); + return (Some(candidate), notes); + } - notes.push(format!("Sidecar requested but missing: {}", candidate.display())); - return (None, notes); - } + notes.push(format!( + "Sidecar requested but missing: {}", + candidate.display() + )); + return (None, notes); + } - #[cfg(windows)] - { - notes.push("Sidecar requested but unsupported on Windows".to_string()); - (None, notes) - } + #[cfg(windows)] + { + notes.push("Sidecar requested but unsupported on Windows".to_string()); + (None, notes) + } } #[tauri::command] fn engine_doctor(prefer_sidecar: Option) -> EngineDoctorResult { - let prefer_sidecar = prefer_sidecar.unwrap_or(false); - - let (sidecar, mut notes) = resolve_sidecar_candidate(prefer_sidecar); - let (resolved, in_path, more_notes) = match sidecar { - Some(path) => (Some(path), false, Vec::new()), - None => resolve_opencode_executable(), - }; - - notes.extend(more_notes); - - let (version, supports_serve, serve_help_status, serve_help_stdout, serve_help_stderr) = - match resolved.as_ref() { - Some(path) => { - let (ok, status, stdout, stderr) = opencode_serve_help(path.as_os_str()); - ( - opencode_version(path.as_os_str()), - ok, - status, - stdout, - stderr, - ) - } - None => (None, false, None, None, None), + let prefer_sidecar = prefer_sidecar.unwrap_or(false); + + let (sidecar, mut notes) = resolve_sidecar_candidate(prefer_sidecar); + let (resolved, in_path, more_notes) = match sidecar { + Some(path) => (Some(path), false, Vec::new()), + None => resolve_opencode_executable(), }; - EngineDoctorResult { - found: resolved.is_some(), - in_path, - resolved_path: resolved.map(|path| path.to_string_lossy().to_string()), - version, - supports_serve, - notes, - serve_help_status, - serve_help_stdout, - serve_help_stderr, - } + notes.extend(more_notes); + + let (version, supports_serve, serve_help_status, serve_help_stdout, serve_help_stderr) = + match resolved.as_ref() { + Some(path) => { + let (ok, status, stdout, stderr) = opencode_serve_help(path.as_os_str()); + ( + opencode_version(path.as_os_str()), + ok, + status, + stdout, + stderr, + ) + } + None => (None, false, None, None, None), + }; + + EngineDoctorResult { + found: resolved.is_some(), + in_path, + resolved_path: resolved.map(|path| path.to_string_lossy().to_string()), + version, + supports_serve, + notes, + serve_help_status, + serve_help_stdout, + serve_help_stderr, + } } #[tauri::command] fn engine_install() -> Result { - #[cfg(windows)] - { - return Ok(ExecResult { + #[cfg(windows)] + { + return Ok(ExecResult { ok: false, status: -1, stdout: String::new(), stderr: "Guided install is not supported on Windows yet. Install OpenCode via Scoop/Chocolatey or https://opencode.ai/install, then restart OpenWork.".to_string(), }); - } - - #[cfg(not(windows))] - { - let install_dir = home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".opencode") - .join("bin"); - - let output = Command::new("bash") - .arg("-lc") - .arg("curl -fsSL https://opencode.ai/install | bash") - .env("OPENCODE_INSTALL_DIR", install_dir) - .output() - .map_err(|e| format!("Failed to run installer: {e}"))?; - - let status = output.status.code().unwrap_or(-1); - Ok(ExecResult { - ok: output.status.success(), - status, - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - }) - } + } + + #[cfg(not(windows))] + { + let install_dir = home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".opencode") + .join("bin"); + + let output = Command::new("bash") + .arg("-lc") + .arg("curl -fsSL https://opencode.ai/install | bash") + .env("OPENCODE_INSTALL_DIR", install_dir) + .output() + .map_err(|e| format!("Failed to run installer: {e}"))?; + + let status = output.status.code().unwrap_or(-1); + Ok(ExecResult { + ok: output.status.success(), + status, + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }) + } } #[tauri::command] fn engine_start( - manager: State, - project_dir: String, - prefer_sidecar: Option, + manager: State, + project_dir: String, + prefer_sidecar: Option, ) -> Result { - let project_dir = project_dir.trim().to_string(); - if project_dir.is_empty() { - return Err("projectDir is required".to_string()); - } - - let hostname = "127.0.0.1".to_string(); - let port = find_free_port()?; - - let mut state = manager.inner.lock().expect("engine mutex poisoned"); - - // Stop any existing engine first. - EngineManager::stop_locked(&mut state); - - let mut notes = Vec::new(); - - let (resolved_sidecar, mut sidecar_notes) = - resolve_sidecar_candidate(prefer_sidecar.unwrap_or(false)); - - notes.append(&mut sidecar_notes); - - let (program, _in_path, more_notes) = match resolved_sidecar { - Some(path) => (Some(path), false, Vec::new()), - None => resolve_opencode_executable(), - }; - - notes.extend(more_notes); - let Some(program) = program else { - let notes_text = notes.join("\n"); - return Err(format!( - "OpenCode CLI not found.\n\nInstall with:\n- brew install anomalyco/tap/opencode\n- curl -fsSL https://opencode.ai/install | bash\n\nNotes:\n{notes_text}" - )); - }; - - let mut command = Command::new(&program); - command - .arg("serve") - .arg("--hostname") - .arg(&hostname) - .arg("--port") - .arg(port.to_string()) - // Allow the Vite dev server origin, plus common Tauri origins. - .arg("--cors") - .arg("http://localhost:5173") - .arg("--cors") - .arg("tauri://localhost") - .arg("--cors") - .arg("http://tauri.localhost") - .current_dir(&project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - // Best-effort: restore env parity with terminal installs. - // If the GUI process doesn't have XDG_* vars but the user's OpenCode auth/config lives under - // a common XDG location, infer and set them for the engine process. - if let Some(xdg_data_home) = maybe_infer_xdg_home( - "XDG_DATA_HOME", - candidate_xdg_data_dirs(), - Path::new("opencode/auth.json"), - ) { - command.env("XDG_DATA_HOME", xdg_data_home); - } - - // Help OpenCode find global config/plugins in GUI contexts. - if let Some(xdg_config_home) = maybe_infer_xdg_home( - "XDG_CONFIG_HOME", - candidate_xdg_config_dirs(), - Path::new("opencode/opencode.json"), - ) { - command.env("XDG_CONFIG_HOME", xdg_config_home); - } - - // Tag requests and logs to make debugging easier. - command.env("OPENCODE_CLIENT", "openwork"); - - // Inherit the current environment (Command already does) but also pass through an explicit - // marker for UI-driven launches so we can key off it in engine logs. - command.env("OPENWORK", "1"); - - let child = command - .spawn() - .map_err(|e| format!("Failed to start opencode: {e}"))?; - - state.last_stdout = None; - state.last_stderr = None; - - state.child = Some(child); - state.project_dir = Some(project_dir); - state.hostname = Some(hostname.clone()); - state.port = Some(port); - state.base_url = Some(format!("http://{hostname}:{port}")); - - Ok(EngineManager::snapshot_locked(&mut state)) + let project_dir = project_dir.trim().to_string(); + if project_dir.is_empty() { + return Err("projectDir is required".to_string()); + } + + let hostname = "127.0.0.1".to_string(); + let port = find_free_port()?; + + let mut state = manager.inner.lock().expect("engine mutex poisoned"); + + // Stop any existing engine first. + EngineManager::stop_locked(&mut state); + + let mut notes = Vec::new(); + + let (resolved_sidecar, mut sidecar_notes) = + resolve_sidecar_candidate(prefer_sidecar.unwrap_or(false)); + + notes.append(&mut sidecar_notes); + + let (program, _in_path, more_notes) = match resolved_sidecar { + Some(path) => (Some(path), false, Vec::new()), + None => resolve_opencode_executable(), + }; + + notes.extend(more_notes); + let Some(program) = program else { + let notes_text = notes.join("\n"); + #[cfg(windows)] + let install_msg = "OpenCode CLI not found.\n\nInstall with:\n- npm install -g opencode-ai\n- Or download from https://opencode.ai/install\n\nAfter installing, restart OpenWork."; + #[cfg(not(windows))] + let install_msg = "OpenCode CLI not found.\n\nInstall with:\n- brew install anomalyco/tap/opencode\n- curl -fsSL https://opencode.ai/install | bash"; + return Err(format!("{install_msg}\n\nNotes:\n{notes_text}")); + }; + + let mut command = create_opencode_command(program.as_os_str()); + command + .arg("serve") + .arg("--hostname") + .arg(&hostname) + .arg("--port") + .arg(port.to_string()) + // Allow the Vite dev server origin, plus common Tauri origins. + .arg("--cors") + .arg("http://localhost:5173") + .arg("--cors") + .arg("tauri://localhost") + .arg("--cors") + .arg("http://tauri.localhost") + .current_dir(&project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // Best-effort: restore env parity with terminal installs. + // If the GUI process doesn't have XDG_* vars but the user's OpenCode auth/config lives under + // a common XDG location, infer and set them for the engine process. + if let Some(xdg_data_home) = maybe_infer_xdg_home( + "XDG_DATA_HOME", + candidate_xdg_data_dirs(), + Path::new("opencode/auth.json"), + ) { + command.env("XDG_DATA_HOME", xdg_data_home); + } + + // Help OpenCode find global config/plugins in GUI contexts. + if let Some(xdg_config_home) = maybe_infer_xdg_home( + "XDG_CONFIG_HOME", + candidate_xdg_config_dirs(), + Path::new("opencode/opencode.json"), + ) { + command.env("XDG_CONFIG_HOME", xdg_config_home); + } + + // Tag requests and logs to make debugging easier. + command.env("OPENCODE_CLIENT", "openwork"); + + // Inherit the current environment (Command already does) but also pass through an explicit + // marker for UI-driven launches so we can key off it in engine logs. + command.env("OPENWORK", "1"); + + let child = command + .spawn() + .map_err(|e| format!("Failed to start opencode: {e}"))?; + + state.last_stdout = None; + state.last_stderr = None; + + state.child = Some(child); + state.project_dir = Some(project_dir); + state.hostname = Some(hostname.clone()); + state.port = Some(port); + state.base_url = Some(format!("http://{hostname}:{port}")); + + Ok(EngineManager::snapshot_locked(&mut state)) } #[tauri::command] fn opkg_install(project_dir: String, package: String) -> Result { - let project_dir = project_dir.trim().to_string(); - if project_dir.is_empty() { - return Err("projectDir is required".to_string()); - } - - let package = package.trim().to_string(); - if package.is_empty() { - return Err("package is required".to_string()); - } - - let mut opkg = Command::new("opkg"); - opkg - .arg("install") - .arg(&package) - .current_dir(&project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(result) = run_capture_optional(&mut opkg)? { - return Ok(result); - } - - let mut openpackage = Command::new("openpackage"); - openpackage - .arg("install") - .arg(&package) - .current_dir(&project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(result) = run_capture_optional(&mut openpackage)? { - return Ok(result); - } - - let mut pnpm = Command::new("pnpm"); - pnpm - .arg("dlx") - .arg("opkg") - .arg("install") - .arg(&package) - .current_dir(&project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(result) = run_capture_optional(&mut pnpm)? { - return Ok(result); - } - - let mut npx = Command::new("npx"); - npx - .arg("opkg") - .arg("install") - .arg(&package) - .current_dir(&project_dir) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(result) = run_capture_optional(&mut npx)? { - return Ok(result); - } - - Ok(ExecResult { + let project_dir = project_dir.trim().to_string(); + if project_dir.is_empty() { + return Err("projectDir is required".to_string()); + } + + let package = package.trim().to_string(); + if package.is_empty() { + return Err("package is required".to_string()); + } + + let mut opkg = Command::new("opkg"); + opkg.arg("install") + .arg(&package) + .current_dir(&project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + if let Some(result) = run_capture_optional(&mut opkg)? { + return Ok(result); + } + + let mut openpackage = Command::new("openpackage"); + openpackage + .arg("install") + .arg(&package) + .current_dir(&project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + if let Some(result) = run_capture_optional(&mut openpackage)? { + return Ok(result); + } + + let mut pnpm = Command::new("pnpm"); + pnpm.arg("dlx") + .arg("opkg") + .arg("install") + .arg(&package) + .current_dir(&project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + if let Some(result) = run_capture_optional(&mut pnpm)? { + return Ok(result); + } + + let mut npx = Command::new("npx"); + npx.arg("opkg") + .arg("install") + .arg(&package) + .current_dir(&project_dir) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + if let Some(result) = run_capture_optional(&mut npx)? { + return Ok(result); + } + + Ok(ExecResult { ok: false, status: -1, stdout: String::new(), @@ -1417,120 +1480,130 @@ fn opkg_install(project_dir: String, package: String) -> Result Result { - let project_dir = project_dir.trim().to_string(); - if project_dir.is_empty() { - return Err("projectDir is required".to_string()); - } - - let source_dir = source_dir.trim().to_string(); - if source_dir.is_empty() { - return Err("sourceDir is required".to_string()); - } - - let src = PathBuf::from(&source_dir); - let name = src - .file_name() - .and_then(|s| s.to_str()) - .ok_or_else(|| "Failed to infer skill name from directory".to_string())?; - - let dest = PathBuf::from(&project_dir) - .join(".opencode") - .join("skill") - .join(name); - - if dest.exists() { - if overwrite { - fs::remove_dir_all(&dest) - .map_err(|e| format!("Failed to remove existing skill dir {}: {e}", dest.display()))?; - } else { - return Err(format!("Skill already exists at {}", dest.display())); +fn import_skill( + project_dir: String, + source_dir: String, + overwrite: bool, +) -> Result { + let project_dir = project_dir.trim().to_string(); + if project_dir.is_empty() { + return Err("projectDir is required".to_string()); } - } - copy_dir_recursive(&src, &dest)?; + let source_dir = source_dir.trim().to_string(); + if source_dir.is_empty() { + return Err("sourceDir is required".to_string()); + } - Ok(ExecResult { - ok: true, - status: 0, - stdout: format!("Imported skill to {}", dest.display()), - stderr: String::new(), - }) + let src = PathBuf::from(&source_dir); + let name = src + .file_name() + .and_then(|s| s.to_str()) + .ok_or_else(|| "Failed to infer skill name from directory".to_string())?; + + let dest = PathBuf::from(&project_dir) + .join(".opencode") + .join("skill") + .join(name); + + if dest.exists() { + if overwrite { + fs::remove_dir_all(&dest).map_err(|e| { + format!( + "Failed to remove existing skill dir {}: {e}", + dest.display() + ) + })?; + } else { + return Err(format!("Skill already exists at {}", dest.display())); + } + } + + copy_dir_recursive(&src, &dest)?; + + Ok(ExecResult { + ok: true, + status: 0, + stdout: format!("Imported skill to {}", dest.display()), + stderr: String::new(), + }) } #[tauri::command] fn read_opencode_config(scope: String, project_dir: String) -> Result { - let path = resolve_opencode_config_path(scope.trim(), &project_dir)?; - let exists = path.exists(); + let path = resolve_opencode_config_path(scope.trim(), &project_dir)?; + let exists = path.exists(); - let content = if exists { - Some(fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {e}", path.display()))?) - } else { - None - }; + let content = if exists { + Some( + fs::read_to_string(&path) + .map_err(|e| format!("Failed to read {}: {e}", path.display()))?, + ) + } else { + None + }; - Ok(OpencodeConfigFile { - path: path.to_string_lossy().to_string(), - exists, - content, - }) + Ok(OpencodeConfigFile { + path: path.to_string_lossy().to_string(), + exists, + content, + }) } #[tauri::command] fn write_opencode_config( - scope: String, - project_dir: String, - content: String, + scope: String, + project_dir: String, + content: String, ) -> Result { - let path = resolve_opencode_config_path(scope.trim(), &project_dir)?; + let path = resolve_opencode_config_path(scope.trim(), &project_dir)?; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create config dir {}: {e}", parent.display()))?; - } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create config dir {}: {e}", parent.display()))?; + } - fs::write(&path, content) - .map_err(|e| format!("Failed to write {}: {e}", path.display()))?; + fs::write(&path, content).map_err(|e| format!("Failed to write {}: {e}", path.display()))?; - Ok(ExecResult { - ok: true, - status: 0, - stdout: format!("Wrote {}", path.display()), - stderr: String::new(), - }) + Ok(ExecResult { + ok: true, + status: 0, + stdout: format!("Wrote {}", path.display()), + stderr: String::new(), + }) } pub fn run() { - let builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init()); - - #[cfg(desktop)] - let builder = builder - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_updater::Builder::new().build()); - - builder - .manage(EngineManager::default()) - .invoke_handler(tauri::generate_handler![ - engine_start, - engine_stop, - engine_info, - engine_doctor, - engine_install, - workspace_bootstrap, - workspace_set_active, - workspace_create, - workspace_add_authorized_root, - workspace_template_write, - workspace_template_delete, - workspace_openwork_read, - workspace_openwork_write, - opkg_install, - import_skill, - read_opencode_config, - write_opencode_config, - updater_environment, - reset_openwork_state - ]) - .run(tauri::generate_context!()) - .expect("error while running OpenWork"); + let builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init()); + + #[cfg(desktop)] + let builder = builder + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_updater::Builder::new().build()); + + builder + .manage(EngineManager::default()) + .invoke_handler(tauri::generate_handler![ + engine_start, + engine_stop, + engine_info, + engine_doctor, + engine_install, + workspace_bootstrap, + workspace_set_active, + workspace_create, + workspace_add_authorized_root, + workspace_template_write, + workspace_template_delete, + workspace_openwork_read, + workspace_openwork_write, + opkg_install, + import_skill, + read_opencode_config, + write_opencode_config, + updater_environment, + reset_openwork_state + ]) + .run(tauri::generate_context!()) + .expect("error while running OpenWork"); }