From 1650ae908024801ec371aff79dbf37b18e32eb5a Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 11:10:39 +1200 Subject: [PATCH 1/2] Add power-automate-cloud-flow plugin (JSON LSP for cloud-flow files) Second marketplace plugin, mirroring dataverse-customization-xml's setup/LSP/test model but for Power Automate solution cloud flows. - Live schema validation of unpacked Workflows/*.json (and *.flow.json) via vscode-json-language-server, using a bundled draft-07 clientdata/WDL wrapper schema. Structure only: connector inputs are left loose because OpenApiConnection isn't in the public Logic Apps schema. - Server is npm ci-installed at setup, pinned in package-lock.json; not committed. Set-LspSchemaPaths.ps1 stamps the machine-local file:// schema URI into .lsp.json (CLAUDE_PLUGIN_ROOT isn't substituted in settings). - Install-Plugin.ps1 self-check drives the real server end-to-end (scripts/lsp-smoke.mjs) over both the push and pull config paths and asserts the schema fires (valid clean, invalid flagged). - Headless/CI structure checks via built-in Test-Json; semantic linting (runAfter/connectionName resolution, hardcoded values) stays in the power-automate-flow-dev skill. - Pester suite (config parity + fixture validation), CI on Windows + Ubuntu. Marketplace, CI, README, llms.txt and CHANGELOG updated for the new plugin. --- .claude-plugin/marketplace.json | 6 + .github/workflows/ci.yml | 13 +- CHANGELOG.md | 14 + README.md | 7 +- llms.txt | 11 + .../.claude-plugin/plugin.json | 13 + plugins/power-automate-cloud-flow/.gitignore | 2 + plugins/power-automate-cloud-flow/.lsp.json | 52 +++ plugins/power-automate-cloud-flow/README.md | 102 +++++ plugins/power-automate-cloud-flow/SKILL.md | 76 ++++ .../commands/setup.md | 14 + .../power-automate-cloud-flow/docs/codex.md | 28 ++ .../docs/debugging.md | 56 +++ .../power-automate-cloud-flow/docs/vscode.md | 37 ++ .../package-lock.json | 365 ++++++++++++++++++ .../power-automate-cloud-flow/package.json | 9 + .../schemas/SOURCE.md | 34 ++ .../schemas/cloud-flow-clientdata.schema.json | 102 +++++ .../scripts/Install-JsonLanguageServer.ps1 | 53 +++ .../scripts/Install-Plugin.ps1 | 41 ++ .../scripts/Set-LspSchemaPaths.ps1 | 101 +++++ .../scripts/lsp-smoke.mjs | 130 +++++++ .../tests/LspConfig.Tests.ps1 | 99 +++++ .../fixtures/invalid/bad-runafter-status.json | 18 + .../fixtures/invalid/missing-definition.json | 6 + .../tests/fixtures/valid/simple-flow.json | 36 ++ 26 files changed, 1422 insertions(+), 3 deletions(-) create mode 100644 plugins/power-automate-cloud-flow/.claude-plugin/plugin.json create mode 100644 plugins/power-automate-cloud-flow/.gitignore create mode 100644 plugins/power-automate-cloud-flow/.lsp.json create mode 100644 plugins/power-automate-cloud-flow/README.md create mode 100644 plugins/power-automate-cloud-flow/SKILL.md create mode 100644 plugins/power-automate-cloud-flow/commands/setup.md create mode 100644 plugins/power-automate-cloud-flow/docs/codex.md create mode 100644 plugins/power-automate-cloud-flow/docs/debugging.md create mode 100644 plugins/power-automate-cloud-flow/docs/vscode.md create mode 100644 plugins/power-automate-cloud-flow/package-lock.json create mode 100644 plugins/power-automate-cloud-flow/package.json create mode 100644 plugins/power-automate-cloud-flow/schemas/SOURCE.md create mode 100644 plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json create mode 100644 plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 create mode 100644 plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 create mode 100644 plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 create mode 100644 plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs create mode 100644 plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 create mode 100644 plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json create mode 100644 plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json create mode 100644 plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 922c1dd..786fe22 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,6 +12,12 @@ "source": "./plugins/dataverse-customization-xml", "description": "Schema-validated hand-editing of Dataverse customization XML (ribbon, sitemap, forms, FetchXML, charts and more) against the official Microsoft XSDs — standalone PowerShell validator plus live lemminx LSP diagnostics.", "category": "development" + }, + { + "name": "power-automate-cloud-flow", + "source": "./plugins/power-automate-cloud-flow", + "description": "Schema-validated hand-editing of unpacked Power Automate solution cloud-flow JSON (Workflows/*.json) against a bundled clientdata/WDL wrapper schema, with live vscode-json-language-server LSP diagnostics.", + "category": "development" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4714a7c..bed5909 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,12 +38,23 @@ jobs: - name: End-to-end setup (fetch schemas + lemminx, stamp paths, self-check) run: ./plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 + # node_modules is not committed; npm ci restores it deterministically from the lockfile. + # Caching it makes the setup step's npm ci a near-no-op on cache hit. + - name: Cache flow LSP node_modules + uses: actions/cache@v4 + with: + path: plugins/power-automate-cloud-flow/node_modules + key: flow-lsp-${{ runner.os }}-${{ hashFiles('plugins/power-automate-cloud-flow/package-lock.json') }} + + - name: End-to-end setup (install JSON language server, stamp paths, LSP self-check) + run: ./plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 + # Version bounds on both modules are deliberate: a new major on the PowerShell Gallery # should not be able to turn CI red without a change in this repo. - name: Pester tests run: | Install-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 6.99.99 -Force -SkipPublisherCheck - Invoke-Pester plugins/dataverse-customization-xml/tests -CI + Invoke-Pester plugins -CI - name: PSScriptAnalyzer run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5dd56..5fcf911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased + +- New plugin `power-automate-cloud-flow`: live schema validation of unpacked Power Automate + solution cloud-flow JSON (`Workflows/*.json`). + - Bundled draft-07 clientdata/WDL wrapper schema (structure only — connector `inputs` left + loose by design, since `OpenApiConnection` isn't in the public Logic Apps schema). + - Live `vscode-json-language-server` LSP diagnostics for Claude Code (and VS Code via + `json.schemas`); the server is `npm ci`-installed at setup, pinned in `package-lock.json`. + - One-shot `Install-Plugin.ps1` whose self-check drives the real LSP end-to-end + (`scripts/lsp-smoke.mjs`) and asserts the schema fires; `/power-automate-cloud-flow:setup`. + - Headless/CI structure checks via PowerShell's built-in `Test-Json` (no bespoke validator). + - Semantic linting (runAfter / connectionName resolution, hard-coded values) stays in the + `power-automate-flow-dev` skill; this plugin is the shape layer. + ## 1.0.1 — 2026-07-16 - Fix: installer no longer reports failure to CI-style hosts after a successful self-check diff --git a/README.md b/README.md index 57feca7..db68857 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,14 @@ Update later with `/plugin marketplace update dataverse-agent-plugins` then | Plugin | What it does | |--------|--------------| | [dataverse-customization-xml](plugins/dataverse-customization-xml/) | Schema-validated hand-editing of Dataverse customization XML (ribbon, sitemap, forms, FetchXML, charts, ISV config and more) against the official Microsoft XSDs. Standalone PowerShell validator + live lemminx LSP diagnostics. Also usable from [Codex](plugins/dataverse-customization-xml/docs/codex.md), [VS Code with no agent](plugins/dataverse-customization-xml/docs/vscode.md), and CI. | +| [power-automate-cloud-flow](plugins/power-automate-cloud-flow/) | Schema-validated hand-editing of unpacked Power Automate solution cloud-flow JSON (`Workflows/*.json`) against a bundled clientdata/WDL wrapper schema. Live `vscode-json-language-server` LSP diagnostics; headless structure checks via built-in `Test-Json`. Also usable from [Codex](plugins/power-automate-cloud-flow/docs/codex.md), [VS Code with no agent](plugins/power-automate-cloud-flow/docs/vscode.md), and CI. | ## Requirements -PowerShell 7+ (`pwsh`) on Windows, macOS or Linux. Plugins fetch what else they need at -setup (Microsoft XSDs, lemminx binary) — nothing bulky or third-party is committed here. +PowerShell 7+ (`pwsh`) on Windows, macOS or Linux. The `power-automate-cloud-flow` plugin also +needs Node.js (for its `npm`-installed JSON language server). Plugins fetch what else they need +at setup (Microsoft XSDs, lemminx binary, the JSON language server) — nothing bulky or +third-party is committed here. ## License diff --git a/llms.txt b/llms.txt index b129dfc..885cdb2 100644 --- a/llms.txt +++ b/llms.txt @@ -9,6 +9,8 @@ ## Docs +### dataverse-customization-xml + - [Repo README](README.md): marketplace overview, install, requirements - [Plugin README](plugins/dataverse-customization-xml/README.md): full setup for every consumer type - [SKILL.md](plugins/dataverse-customization-xml/SKILL.md): the agent skill — validation workflow, root-element→schema table, ribbon-button recipe, gotchas @@ -17,6 +19,15 @@ - [Debugging](plugins/dataverse-customization-xml/docs/debugging.md): setup failures, missing diagnostics, false positives - [Schema source](plugins/dataverse-customization-xml/schemas/SOURCE.md): where the Microsoft XSDs come from, refresh procedure +### power-automate-cloud-flow + +- [Plugin README](plugins/power-automate-cloud-flow/README.md): install + setup for every consumer type +- [SKILL.md](plugins/power-automate-cloud-flow/SKILL.md): the shape-vs-semantics split, what attaches, gotchas +- [Codex guide](plugins/power-automate-cloud-flow/docs/codex.md): the `Test-Json` validation loop from non-Claude agents +- [VS Code guide](plugins/power-automate-cloud-flow/docs/vscode.md): editor-only setup, CI usage +- [Debugging](plugins/power-automate-cloud-flow/docs/debugging.md): setup failures, missing diagnostics +- [Schema source](plugins/power-automate-cloud-flow/schemas/SOURCE.md): what the wrapper schema validates and why + ## Optional - [Design spec](docs/specs/2026-07-15-dataverse-agent-plugins-repo-design.md): why the repo is shaped this way diff --git a/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json b/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json new file mode 100644 index 0000000..a7eb77f --- /dev/null +++ b/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://anthropic.com/claude-code/plugin.schema.json", + "name": "power-automate-cloud-flow", + "version": "1.0.0", + "description": "Schema-validated hand-editing of unpacked Power Automate solution cloud-flow JSON (Workflows/*.json) against a bundled clientdata/WDL wrapper schema, with live vscode-json-language-server LSP diagnostics.", + "author": { + "name": "Connor Parsons" + }, + "homepage": "https://github.com/Cordedmink2/dataverse-agent-plugins", + "skills": [ + "./" + ] +} diff --git a/plugins/power-automate-cloud-flow/.gitignore b/plugins/power-automate-cloud-flow/.gitignore new file mode 100644 index 0000000..46ac48e --- /dev/null +++ b/plugins/power-automate-cloud-flow/.gitignore @@ -0,0 +1,2 @@ +# Installed at setup by scripts/Install-JsonLanguageServer.ps1 (npm ci from the pinned package-lock.json) +node_modules/ diff --git a/plugins/power-automate-cloud-flow/.lsp.json b/plugins/power-automate-cloud-flow/.lsp.json new file mode 100644 index 0000000..6fbb9e3 --- /dev/null +++ b/plugins/power-automate-cloud-flow/.lsp.json @@ -0,0 +1,52 @@ +{ + "json": { + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/node_modules/vscode-langservers-extracted/lib/json-language-server/node/jsonServerMain.js", + "--stdio" + ], + "extensionToLanguage": { + ".json": "json" + }, + "initializationOptions": { + "provideFormatter": true, + "handledSchemaProtocols": [ + "file" + ], + "settings": { + "json": { + "validate": { + "enable": true + }, + "schemas": [ + { + "fileMatch": [ + "**/Workflows/*.json", + "**/Workflows/**/*.json", + "**/*.flow.json" + ], + "url": "schemas/cloud-flow-clientdata.schema.json" + } + ] + } + } + }, + "settings": { + "json": { + "validate": { + "enable": true + }, + "schemas": [ + { + "fileMatch": [ + "**/Workflows/*.json", + "**/Workflows/**/*.json", + "**/*.flow.json" + ], + "url": "schemas/cloud-flow-clientdata.schema.json" + } + ] + } + } + } +} diff --git a/plugins/power-automate-cloud-flow/README.md b/plugins/power-automate-cloud-flow/README.md new file mode 100644 index 0000000..f4991b2 --- /dev/null +++ b/plugins/power-automate-cloud-flow/README.md @@ -0,0 +1,102 @@ +# power-automate-cloud-flow + +A Claude Code plugin for **live schema validation of unpacked Power Automate solution cloud-flow +JSON** — the `Workflows/-.json` files `pac solution unpack` produces (the flow +clientdata: a WDL `definition` plus its `connectionReferences`). Malformed structure shows up as +editor diagnostics *before* `pac solution import`. + +This is the **shape** layer. Cross-node semantics (`runAfter` targets a real sibling, `connectionName` +resolves, hard-coded env values) and the export/pack/import round-trip live in the +`power-automate-flow-dev` skill. See `SKILL.md` for the split. + +## The validation layer + +**LSP (live)** — `vscode-json-language-server` (from `vscode-langservers-extracted`) pushes +diagnostics as you edit, using the bundled draft-07 wrapper schema. Wired for Claude Code (this +plugin's `.lsp.json`) and usable in VS Code (built-in JSON language features via `json.schemas`). + +There is no separate CLI validator script: for headless/CI structure checks, PowerShell's built-in +`Test-Json -SchemaFile schemas/cloud-flow-clientdata.schema.json` validates against the same schema +the LSP loads. + +## Install (Claude Code) + +``` +/plugin marketplace add Cordedmink2/dataverse-agent-plugins +/plugin install power-automate-cloud-flow@dataverse-agent-plugins +/power-automate-cloud-flow:setup +``` + +The setup command installs the pinned JSON language server (`npm ci`), stamps this machine's +absolute schema path into `.lsp.json`, and runs an end-to-end self-check that drives the real server. +It requires PowerShell 7+ (`pwsh`) and Node.js (for `npm`/`node`). Then run `/reload-plugins` (or +restart the session) so the LSP starts with the stamped path. + +## Setup script directly (non-Claude consumers) + +The slash command is a thin wrapper around one idempotent script: + +``` +pwsh scripts/Install-Plugin.ps1 [-UpdateVSCode] +``` + +- `-UpdateVSCode` also writes the `json.schemas` association into your VS Code user settings. + +Run this one script rather than the individual `Install-*`/`Set-*` scripts — a partial manual setup +can leave a running server pointing at a broken relative schema path. + +## What's inside + +| Path | Purpose | +|------|---------| +| `SKILL.md` | The skill: the shape-vs-semantics split, what attaches, gotchas | +| `commands/setup.md` | The `/power-automate-cloud-flow:setup` slash command | +| `scripts/Install-Plugin.ps1` | One-shot setup: server install + path stamping + self-check | +| `scripts/Install-JsonLanguageServer.ps1` | `npm ci` the pinned JSON language server into `node_modules/` | +| `scripts/Set-LspSchemaPaths.ps1` | Stamp this machine's absolute schema `file://` URI into `.lsp.json` (+ VS Code) | +| `scripts/lsp-smoke.mjs` | End-to-end LSP health check (drives the server, asserts the schema fires) | +| `.lsp.json` | Registers the JSON server with Claude Code, with the flow `json.schemas` association | +| `package.json` / `package-lock.json` | Pin the JSON language server version | +| `schemas/cloud-flow-clientdata.schema.json` | The bundled draft-07 wrapper schema | +| `schemas/SOURCE.md` | What the schema validates, what it deliberately doesn't, how to refresh | +| `tests/` | Pester suite (config parity + fixture validation) + valid/invalid fixtures | +| `docs/` | Guides for Codex, VS Code-only, and debugging | + +The JSON language server (`node_modules/`) is **installed at setup, not shipped**. The schema is +hand-maintained and committed. + +## Updating + +``` +/plugin marketplace update dataverse-agent-plugins +/plugin update power-automate-cloud-flow@dataverse-agent-plugins +/power-automate-cloud-flow:setup +``` + +Re-run setup after every update — it re-installs the pinned server and re-stamps the schema path (see +`docs/debugging.md` for why). + +## Other consumers + +- **Codex / any non-Claude agent** — see [`docs/codex.md`](docs/codex.md). +- **VS Code only (no agent)** — see [`docs/vscode.md`](docs/vscode.md). +- **CI / pre-commit** — validate structure with `Test-Json`: + `Get-Content .json -Raw | Test-Json -SchemaFile schemas/cloud-flow-clientdata.schema.json`. + +Something not working? [`docs/debugging.md`](docs/debugging.md). + +## Known caveats + +- **The wrapper is loose on action inputs by design** — `OpenApiConnection` connector actions aren't + in Microsoft's public Logic Apps schema, so a strict `$ref` would produce more false positives than + real findings. Structure is validated; `pac solution check` / a successful import is authoritative. +- **`${CLAUDE_PLUGIN_ROOT}` is only substituted in `.lsp.json` `command`/`args`**, not in nested + `settings` — hence the absolute-path fixup script. + +## License + +MIT (see the repo root `LICENSE`). The bundled schema is original to this repo. The JSON language +server is installed from npm at setup and remains under its own license. + +See `SKILL.md` for the shape-vs-semantics layering and how this plugin pairs with the +`power-automate-flow-dev` skill. diff --git a/plugins/power-automate-cloud-flow/SKILL.md b/plugins/power-automate-cloud-flow/SKILL.md new file mode 100644 index 0000000..a1023e3 --- /dev/null +++ b/plugins/power-automate-cloud-flow/SKILL.md @@ -0,0 +1,76 @@ +--- +name: power-automate-cloud-flow +description: >- + Live schema validation of unpacked Power Automate solution cloud-flow JSON while you hand-edit it. + Use when editing a pac-unpacked solution's Workflows/*.json (the flow clientdata: WDL definition + + connectionReferences) and you want malformed structure to show up as editor diagnostics before + pack/import. Provides a bundled draft-07 wrapper schema and a vscode-json-language-server LSP that + attaches to Workflows/*.json and *.flow.json. This is the SHAPE layer only — cross-node semantic + checks (runAfter targets a real sibling, connectionName resolves, hard-coded env values) and the + export/pack/import round-trip live in the power-automate-flow-dev skill. +--- + +# Power Automate cloud-flow JSON (schema-validated) + +A solution cloud flow, once `pac solution unpack`ed, is `Workflows/-.json`: the flow's +**clientdata** — a Workflow Definition Language (WDL) `definition` plus its `connectionReferences`. +This plugin makes malformed edits to that file **surface as live LSP diagnostics** while you type, +instead of failing at `pac solution import`. + +It is one layer of a two-layer story — do not confuse them: + +| Layer | What it checks | Where | +|-------|----------------|-------| +| **Shape (this plugin)** | JSON well-formedness + the clientdata/WDL wrapper structure: `properties.definition` present, `definition` has `$schema`/`triggers`/`actions`, `runAfter` statuses are the WDL enum. Live, in-editor. | `schemas/cloud-flow-clientdata.schema.json` via `vscode-json-language-server` (`.lsp.json`) | +| **Semantics (the `power-automate-flow-dev` skill)** | Cross-node rules JSON Schema *cannot* express: `runAfter` naming a real sibling, `connectionName` resolving to a declared connection reference, child-invoker connections, hard-coded environment values, condition rows. Plus the export → unpack → edit → pack → import → verify round-trip. | that skill's `flow-lint.ps1` | + +**Rule of thumb:** the LSP catches "this isn't a well-formed flow file." `flow-lint.ps1` catches "this +is well-formed but will import Off / route to the wrong place." Run both before pack/import. + +## Setup (once per machine) + +The JSON language server is a Node package fetched via npm; it is not committed. Run: + +``` +/power-automate-cloud-flow:setup +``` + +or directly `pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"`. That installs the pinned server +(`npm ci`), stamps the machine-local absolute schema path into `.lsp.json` (because +`${CLAUDE_PLUGIN_ROOT}` is not substituted inside LSP `settings`), and runs an end-to-end self-check +that drives the real server and confirms the schema fires. Then `/reload-plugins`. + +## What attaches to what + +The schema associates (see `.lsp.json`) with: + +- `**/Workflows/*.json` and `**/Workflows/**/*.json` — unpacked solution cloud flows +- `**/*.flow.json` — a convenience convention for a standalone flow file + +It deliberately does **not** claim every `*.json` in the workspace. + +## Subagents / headless contexts + +LSP diagnostics only auto-push in the **main interactive session**. A spawned subagent, a workflow +step, or any non-main-session context does NOT receive them. In those contexts validate structure +explicitly with PowerShell's built-in `Test-Json`: + +``` +Get-Content .json -Raw | Test-Json -SchemaFile "${CLAUDE_PLUGIN_ROOT}/schemas/cloud-flow-clientdata.schema.json" +``` + +(and run the `power-automate-flow-dev` skill's `flow-lint.ps1` for the semantic layer). + +## Gotchas + +- **The wrapper is intentionally loose on action inputs.** Power Automate connector actions + (`OpenApiConnection`) are not in Microsoft's public Logic Apps schema, so a strict `$ref` would + drown real errors in false positives. `inputs` is left untyped; structure is what's validated. + `pac solution check` / a successful import remains the authoritative gate. +- **`${CLAUDE_PLUGIN_ROOT}` is not substituted** inside `.lsp.json` `initializationOptions`/`settings` + — only in `command`/`args`. The schema `url` there is an absolute `file://` URI. On a new machine + (or after a plugin update / move), re-run `/power-automate-cloud-flow:setup` — or + `scripts/Set-LspSchemaPaths.ps1` alone — to re-stamp it. +- **The JSON server validates a document only after answering its `workspace/configuration` pull.** + Claude Code and VS Code both handle that; a bare LSP client must too (see `scripts/lsp-smoke.mjs` + for a reference client). diff --git a/plugins/power-automate-cloud-flow/commands/setup.md b/plugins/power-automate-cloud-flow/commands/setup.md new file mode 100644 index 0000000..aa64ff6 --- /dev/null +++ b/plugins/power-automate-cloud-flow/commands/setup.md @@ -0,0 +1,14 @@ +--- +description: Install the JSON language server + stamp the machine-local schema path (run once after install/update) +--- + +Run this plugin's setup script and report the result: + +1. Ask the user whether they also want VS Code's JSON schema association configured + (adds `json.schemas` to their VS Code user settings). +2. Run: `pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` — append ` -UpdateVSCode` + if they said yes. +3. Show the self-check outcome. If setup failed, show the error verbatim and point the user + at `${CLAUDE_PLUGIN_ROOT}/docs/debugging.md`. +4. If setup succeeded, remind the user to run `/reload-plugins` so the JSON LSP starts with the + stamped schema path. diff --git a/plugins/power-automate-cloud-flow/docs/codex.md b/plugins/power-automate-cloud-flow/docs/codex.md new file mode 100644 index 0000000..9cec50c --- /dev/null +++ b/plugins/power-automate-cloud-flow/docs/codex.md @@ -0,0 +1,28 @@ +# Using this plugin with Codex (or any non-Claude agent) + +LSP diagnostics only push into Claude Code's main session, so every other agent must validate +explicitly after each edit. There is no bespoke validator script — PowerShell 7's built-in +`Test-Json` validates against the same bundled schema the LSP loads. + +## Setup + +1. Clone the repo. No server install is needed for the `Test-Json` path (that's only for the LSP), + but PowerShell 7+ (`pwsh`) is required. + +2. Make the skill discoverable. Codex reads `AGENTS.md` — add: + + > When editing an unpacked Power Automate solution cloud flow (`Workflows/*.json` — the flow + > clientdata: WDL `definition` + `connectionReferences`), follow + > `/plugins/power-automate-cloud-flow/SKILL.md`, and after EVERY edit run: + > `Get-Content -Raw | Test-Json -SchemaFile /plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json` + > A schema error means the edit is structurally invalid; fix before pack/import. Then run the + > `power-automate-flow-dev` skill's `flow-lint.ps1` for the semantic layer (runAfter / + > connectionName resolution, hard-coded values). + +3. If your Codex setup supports skill folders, point it at `plugins/power-automate-cloud-flow/` + directly — `SKILL.md` has standard frontmatter. + +## The loop + +edit → `Test-Json -SchemaFile ` (structure) → `flow-lint.ps1` (semantics) → fix until +clean → `pac solution pack` → import. diff --git a/plugins/power-automate-cloud-flow/docs/debugging.md b/plugins/power-automate-cloud-flow/docs/debugging.md new file mode 100644 index 0000000..a2de879 --- /dev/null +++ b/plugins/power-automate-cloud-flow/docs/debugging.md @@ -0,0 +1,56 @@ +# Debugging + +## Setup fails + +- **`npm not found` / `node not found`** — install Node.js (bundles npm) and re-run. The server is + a Node package; there is no offline fallback binary. +- **`npm ci` error** — network unavailable or the registry is down. Retry; if the lockfile is out of + sync with `package.json`, delete `node_modules/` and re-run (the installer falls back to + `npm install` when there is no lockfile). +- **Self-check failed** — the LSP smoke test (`scripts/lsp-smoke.mjs`) drove the real server and a + fixture behaved unexpectedly: almost always the schema and fixtures disagree after a schema edit. + Run `node scripts/lsp-smoke.mjs` directly to see which case failed. + +## No live diagnostics in Claude Code + +1. Did setup run? The `.lsp.json` schema `url` must be an absolute `file://` URI that exists — + committed state is a relative path on purpose and does nothing until stamped. +2. `${CLAUDE_PLUGIN_ROOT}` is only substituted in `.lsp.json` `command`/`args`, NOT in `settings` — + that's why `Set-LspSchemaPaths.ps1` exists. Re-run it (or the whole setup) after moving the plugin. +3. Run `/reload-plugins` (or restart the session) after stamping. +4. The file must match an association glob: `**/Workflows/*.json`, `**/Workflows/**/*.json`, or + `**/*.flow.json`. A flow JSON opened outside a `Workflows/` folder and not named `*.flow.json` + gets no schema. +5. The server validates a document only after answering its `workspace/configuration` pull — Claude + Code handles that; a bare LSP client must too (see `scripts/lsp-smoke.mjs`). +6. Subagents and headless runs NEVER get LSP pushes — that's by design; validate with `Test-Json`. + +## After `/plugin update power-automate-cloud-flow@dataverse-agent-plugins` + +Setup stamps a machine-absolute schema path into the **tracked** `.lsp.json` inside the marketplace +clone, so an update may conflict on that file or reset it to the committed relative path. Re-running +`/power-automate-cloud-flow:setup` after every plugin update is the documented fix — it re-installs +the pinned server and re-stamps the path. + +Contributors working in a clone: the installer dirties `.lsp.json` — don't commit the machine path. +`git update-index --skip-worktree plugins/power-automate-cloud-flow/.lsp.json` keeps it out of your +commits. + +## No diagnostics in VS Code + +- Did you run setup with `-UpdateVSCode`, then restart VS Code? +- Check File > Preferences > Settings (JSON) > `json.schemas` — the `url` is an absolute `file://` + URI and the file must exist. +- A JSONC settings file (comments) makes the updater refuse to write (see [vscode.md](vscode.md)). + +## Validation "errors" that aren't structural + +The wrapper deliberately does not type action `inputs` (connector shapes vary by `operationId` and +aren't in the public Logic Apps schema). So the LSP will NOT flag a wrong connector parameter — that +is expected. For those, and for cross-node semantics (`runAfter`/`connectionName` resolution, +hard-coded env values), use the `power-automate-flow-dev` skill's `flow-lint.ps1`. + +## The final gate + +`pac solution check` / a successful `pac solution import` is authoritative for a packaged flow. The +LSP and `Test-Json` exist so you rarely get that far with a malformed file. diff --git a/plugins/power-automate-cloud-flow/docs/vscode.md b/plugins/power-automate-cloud-flow/docs/vscode.md new file mode 100644 index 0000000..392e382 --- /dev/null +++ b/plugins/power-automate-cloud-flow/docs/vscode.md @@ -0,0 +1,37 @@ +# VS Code-only setup (no agent) + +Live schema validation while hand-editing Power Automate cloud-flow JSON in VS Code. VS Code's +built-in JSON language features do the validation — no extension required. + +Prerequisites: PowerShell 7+ (`pwsh`) and Node.js. The scripts have `#requires -Version 7`; Windows +ships only Windows PowerShell 5.1. + +1. Clone this repo anywhere. +2. Run: + + ``` + pwsh plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 -UpdateVSCode + ``` + + This installs the pinned JSON language server (used by the Claude Code path; VS Code uses its + own built-in one) and writes a `json.schemas` association (absolute `file://` URI) into your VS + Code user settings. +3. Restart VS Code. +4. Open any `Workflows/*.json` (an unpacked solution flow) or a `*.flow.json` file — schema errors + appear as squiggles + Problems entries. Other `*.json` files are untouched. + +For headless/CI structure checks (no editor), use PowerShell's built-in `Test-Json`: + +``` +Get-Content .json -Raw | Test-Json -SchemaFile plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json +``` + +## Settings-update notes + +- If your `settings.json` contains comments (JSONC), the script refuses to modify it (a rewrite + would strip the comments) and prints the exact `json.schemas` entry to add by hand. +- The script writes `settings.json.bak` next to your settings before every write — the `.bak` is + from the **last** run, not the original. +- Only the stable VS Code settings path is auto-detected. VS Code Insiders / VSCodium users: run the + script anyway, then copy the stamped entry from the plugin's `.lsp.json` + (`json.schemas`) into your editor's `settings.json`. diff --git a/plugins/power-automate-cloud-flow/package-lock.json b/plugins/power-automate-cloud-flow/package-lock.json new file mode 100644 index 0000000..94325bb --- /dev/null +++ b/plugins/power-automate-cloud-flow/package-lock.json @@ -0,0 +1,365 @@ +{ + "name": "power-automate-cloud-flow-lsp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "power-automate-cloud-flow-lsp", + "version": "1.0.0", + "dependencies": { + "vscode-langservers-extracted": "4.10.0" + } + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.7.2.tgz", + "integrity": "sha512-WtKRDtJfFEmLrgtu+ODexOHm/6/krRF0k6t+uvkKIKW1Jh9ZIyxZQwJJwB3qhrEgvAxa37zbUg+vn+UyUK/U2w==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "jsonc-parser": "^3.3.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-langservers-extracted": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/vscode-langservers-extracted/-/vscode-langservers-extracted-4.10.0.tgz", + "integrity": "sha512-EFf9uQI4dAKbzMQFjDvVm1xJq1DXAQvBEuEfPGrK/xzfsL5xWTfIuRr90NgfmqwO+IEt6vLZm9EOj6R66xIifg==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "core-js": "^3.20.1", + "jsonc-parser": "^3.2.1", + "regenerator-runtime": "^0.13.9", + "request-light": "^0.7.0", + "semver": "^7.6.1", + "typescript": "^4.0.5", + "vscode-css-languageservice": "^6.2.14", + "vscode-html-languageservice": "^5.2.0", + "vscode-json-languageservice": "^5.3.11", + "vscode-languageserver": "^10.0.0-next.3", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", + "vscode-markdown-languageservice": "^0.5.0-alpha.6", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "bin": { + "vscode-css-language-server": "bin/vscode-css-language-server", + "vscode-eslint-language-server": "bin/vscode-eslint-language-server", + "vscode-html-language-server": "bin/vscode-html-language-server", + "vscode-json-language-server": "bin/vscode-json-language-server", + "vscode-markdown-language-server": "bin/vscode-markdown-language-server" + } + }, + "node_modules/vscode-languageserver": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.1.0.tgz", + "integrity": "sha512-9gEWpXkYGXoqG7pBnE8O8hx/yP7+Aabn4+peQ3KDicQv6qunHSWyLTud3OF0w4S2+HfDD+5HqYKiXQW9HAU6mA==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.18.2" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.1", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, + "node_modules/vscode-markdown-languageservice": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0.tgz", + "integrity": "sha512-+DDXukKWtIHvJtj6tXeLqj8iREnylDQw4yRjY3ldv2J66/oiKRJyLLUy4YhMhfBm9Edjci6VhOSpE84AU3ZFXA==", + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "node-html-parser": "^6.1.5", + "picomatch": "^2.3.1", + "vscode-languageserver-protocol": "^3.17.1", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.7" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + } + } +} diff --git a/plugins/power-automate-cloud-flow/package.json b/plugins/power-automate-cloud-flow/package.json new file mode 100644 index 0000000..63b72b1 --- /dev/null +++ b/plugins/power-automate-cloud-flow/package.json @@ -0,0 +1,9 @@ +{ + "name": "power-automate-cloud-flow-lsp", + "version": "1.0.0", + "private": true, + "description": "Pins the JSON language server used for live diagnostics on Power Automate cloud-flow JSON.", + "dependencies": { + "vscode-langservers-extracted": "4.10.0" + } +} diff --git a/plugins/power-automate-cloud-flow/schemas/SOURCE.md b/plugins/power-automate-cloud-flow/schemas/SOURCE.md new file mode 100644 index 0000000..5326b89 --- /dev/null +++ b/plugins/power-automate-cloud-flow/schemas/SOURCE.md @@ -0,0 +1,34 @@ +# Power Automate cloud-flow clientdata schema + +`cloud-flow-clientdata.schema.json` is a **hand-maintained** JSON Schema (draft-07). It is committed +to the repo — unlike the Dataverse XSDs (Microsoft-copyrighted, downloaded at setup), this schema is +ours to ship. + +## What it validates + +The JSON shape of an unpacked solution cloud flow — the file `pac solution unpack` writes under +`Workflows/-.json`. That file is the flow's `clientdata`: a wrapper around the Workflow +Definition Language (WDL) `definition` plus its `connectionReferences`. + +- `properties.definition` is **required** (a flow with no definition is malformed). +- `definition` requires `$schema`, `triggers`, `actions`. +- `runAfter` statuses are constrained to the WDL enum (`Succeeded` / `Failed` / `Skipped` / `TimedOut`). +- Everything is `additionalProperties: true` — legacy and connector-specific fields are tolerated. + +## What it deliberately does NOT validate + +The strict WDL action vocabulary. Power Automate connector actions (`OpenApiConnection`) are not in +Microsoft's public Logic Apps `workflowdefinition.json` schema, so `$ref`-ing that schema in would +drown real errors in false positives. This wrapper checks **structure**; the authoritative gate for a +packaged flow is `pac solution check` / a successful `pac solution import`. + +Cross-node semantic checks (a `runAfter` naming a real sibling, a `connectionName` resolving to a +declared connection reference, hard-coded environment values) are **inexpressible in JSON Schema** and +live in the `power-automate-flow-dev` skill's `flow-lint.ps1`, not here. This plugin is the live LSP +layer; that skill is the semantic-lint layer. + +## Refreshing + +There is no upstream download. Edit the schema by hand as the flow shape evolves, then run the test +suite (`tests/`) — the fixtures under `tests/fixtures/valid` and `tests/fixtures/invalid` are the +regression gate. diff --git a/plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json b/plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json new file mode 100644 index 0000000..71fb0bc --- /dev/null +++ b/plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/Cordedmink2/dataverse-agent-plugins/power-automate-cloud-flow/cloud-flow-clientdata.schema.json", + "title": "Power Automate solution cloud-flow clientdata", + "description": "Validates the JSON shape of an unpacked solution cloud flow (the file produced under Workflows/ by `pac solution unpack`). This is the flow's `clientdata`: a wrapper around the Workflow Definition Language `definition` plus its `connectionReferences`. The strict WDL action-type vocabulary is NOT enforced here on purpose — Power Automate connector actions (OpenApiConnection) aren't in the public Logic Apps schema, so a strict $ref would drown real errors in false positives. Structure is validated; `pac solution check` remains the authoritative validator for the packaged flow.", + "type": "object", + "additionalProperties": true, + "properties": { + "properties": { + "type": "object", + "additionalProperties": true, + "required": ["definition"], + "properties": { + "connectionReferences": { + "type": "object", + "description": "Map of local action-facing key -> connection-reference metadata. Verified real shape: { runtimeSource, connection: { connectionReferenceLogicalName }, api: { name } }. Each entry binds an action to a connectionreference solution component (solution-aware), not a raw connection. Kept loose — additional/legacy fields are tolerated.", + "additionalProperties": { + "type": "object", + "properties": { + "runtimeSource": { "type": "string", "description": "e.g. \"embedded\"" }, + "connection": { + "type": "object", + "properties": { + "connectionReferenceLogicalName": { "type": "string", "description": "logicalname of the connectionreference row in Dataverse — the ALM-portable link bound at import." }, + "name": { "type": "string", "description": "legacy: direct connection name" }, + "id": { "type": "string" } + } + }, + "api": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "connector, e.g. shared_commondataserviceforapps / shared_sharepointonline" } + } + } + } + } + }, + "definition": { + "type": "object", + "description": "The Workflow Definition Language (WDL) body. Same dialect as Azure Logic Apps.", + "additionalProperties": true, + "required": ["$schema", "triggers", "actions"], + "properties": { + "$schema": { + "type": "string", + "description": "Should be the Logic Apps workflowdefinition schema URL.", + "pattern": "schema\\.management\\.azure\\.com/providers/Microsoft\\.Logic/schemas/.*workflowdefinition\\.json" + }, + "contentVersion": { "type": "string" }, + "parameters": { + "type": "object", + "description": "$connections / $authentication and any flow parameters.", + "additionalProperties": true + }, + "triggers": { + "type": "object", + "description": "One or more triggers. Each has a `type` (e.g. OpenApiConnection, OpenApiConnectionWebhook, Request, Recurrence) and `inputs`.", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": ["type"], + "properties": { + "type": { "type": "string" }, + "kind": { "type": "string" }, + "inputs": { "description": "Trigger inputs — usually an object, but left untyped to avoid false positives." } + }, + "additionalProperties": true + } + }, + "actions": { + "type": "object", + "description": "The action graph. Order is expressed via runAfter, NOT array position. Each action has a `type` and usually `inputs` + `runAfter`.", + "additionalProperties": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "e.g. OpenApiConnection, Compose, Scope, If, Foreach, InitializeVariable, SetVariable, Http, Response, Terminate, Switch." + }, + "inputs": { "description": "Action inputs. Type varies by action — e.g. Compose/Set/Append inputs can be a string, number, array or object — so left untyped to avoid false positives." }, + "runAfter": { + "type": "object", + "description": "Map of upstream-action-name -> array of statuses to run after (Succeeded / Failed / Skipped / TimedOut). Empty {} = runs first.", + "additionalProperties": { + "type": "array", + "items": { "enum": ["Succeeded", "Failed", "Skipped", "TimedOut"] } + } + }, + "runtimeConfiguration": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": true + } + }, + "outputs": { "type": "object", "additionalProperties": true } + } + } + } + }, + "schemaVersion": { "type": "string" } + } +} diff --git a/plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 b/plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 new file mode 100644 index 0000000..f9567c2 --- /dev/null +++ b/plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 @@ -0,0 +1,53 @@ +#requires -Version 7 +<# +.SYNOPSIS + Install the pinned vscode-json-language-server (from vscode-langservers-extracted) into the + plugin's node_modules/ via npm. + +.DESCRIPTION + The server is a Node package (~a few MB), NOT committed to the repo. Run once after + installing/syncing the plugin on a machine. Uses `npm ci` against the committed + package-lock.json for a deterministic, pinned install; falls back to `npm install` if no + lockfile is present yet. Idempotent: skips the install when the pinned server entry is already + present unless -Force. + +.EXAMPLE + pwsh scripts/Install-JsonLanguageServer.ps1 + pwsh scripts/Install-JsonLanguageServer.ps1 -Force # reinstall even if already present +#> +[CmdletBinding()] +param( + # Reinstall even when node_modules already contains the server entry. + [switch]$Force +) + +$ErrorActionPreference = 'Stop' +$pluginRoot = Split-Path $PSScriptRoot -Parent + +# The single load-bearing file: if this exists, the LSP command in .lsp.json can launch. +$serverEntry = Join-Path $pluginRoot 'node_modules' 'vscode-langservers-extracted' 'lib' 'json-language-server' 'node' 'jsonServerMain.js' + +if (-not $Force -and (Test-Path $serverEntry)) { + Write-Host "JSON language server already present (use -Force to reinstall)" -ForegroundColor Green + return +} + +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + throw "npm was not found on PATH. Install Node.js (which bundles npm) and re-run." +} + +Push-Location $pluginRoot +try { + $useCi = Test-Path (Join-Path $pluginRoot 'package-lock.json') + $cmd = if ($useCi) { 'ci' } else { 'install' } + Write-Host "Running 'npm $cmd' in $pluginRoot ..." -ForegroundColor Cyan + # --no-audit/--no-fund keep the output focused; --omit=dev because the server is a runtime dep. + & npm $cmd --no-audit --no-fund --omit=dev + if ($LASTEXITCODE -ne 0) { throw "npm $cmd failed with exit code $LASTEXITCODE." } +} +finally { Pop-Location } + +if (-not (Test-Path $serverEntry)) { + throw "npm completed but the server entry was not found at $serverEntry - package layout changed?" +} +Write-Host "Installed JSON language server ($serverEntry)" -ForegroundColor Green diff --git a/plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 b/plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 new file mode 100644 index 0000000..41e5734 --- /dev/null +++ b/plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 @@ -0,0 +1,41 @@ +#requires -Version 7 +<# +.SYNOPSIS + One-shot plugin setup: install the JSON language server, stamp the machine-local schema path, + run an end-to-end self-check. + +.DESCRIPTION + Idempotent - safe to re-run after /plugin update. The self-check drives the real LSP server + over stdio and confirms the bundled schema fires (valid fixtures clean, invalid fixtures flagged), + so a broken install fails here, not at first real use. + +.EXAMPLE + pwsh scripts/Install-Plugin.ps1 + pwsh scripts/Install-Plugin.ps1 -UpdateVSCode +#> +[CmdletBinding()] +param( + [switch]$UpdateVSCode +) + +$ErrorActionPreference = 'Stop' + +# Child scripts must fail via throw, never exit : an exit through '&' returns to this +# script without stopping it, so the failure would go unnoticed. +Write-Host "== 1/3 JSON language server ==" -ForegroundColor Cyan +& (Join-Path $PSScriptRoot 'Install-JsonLanguageServer.ps1') + +Write-Host "== 2/3 Path stamping ==" -ForegroundColor Cyan +& (Join-Path $PSScriptRoot 'Set-LspSchemaPaths.ps1') -UpdateVSCode:$UpdateVSCode + +Write-Host "== 3/3 Self-check (end-to-end LSP diagnostics) ==" -ForegroundColor Cyan +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + throw "node was not found on PATH. Install Node.js and re-run." +} +$smoke = Join-Path $PSScriptRoot 'lsp-smoke.mjs' +& node $smoke +if ($LASTEXITCODE -ne 0) { throw "Self-check FAILED: LSP smoke test exited $LASTEXITCODE (see output above)." } + +Write-Host "`nSetup complete - self-check passed." -ForegroundColor Green +Write-Host "Claude Code: run /reload-plugins (or restart the session)." +Write-Host "VS Code live validation: install nothing extra (built-in JSON language features); re-run with -UpdateVSCode to add the json.schemas association to your user settings." diff --git a/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 b/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 new file mode 100644 index 0000000..1d108a1 --- /dev/null +++ b/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 @@ -0,0 +1,101 @@ +#requires -Version 7 +<# +.SYNOPSIS + Stamp the machine-local absolute schema path into .lsp.json (and optionally VS Code user + settings) so the JSON language server can load the bundled flow schema. + +.DESCRIPTION + ${CLAUDE_PLUGIN_ROOT} is substituted in .lsp.json 'command'/'args' (so the node server path + stays a variable) but NOT inside 'initializationOptions'/'settings'. The json.schemas 'url' + there must therefore be an absolute file URI for THIS machine. Run after install/update or + after moving the plugin. + +.EXAMPLE + pwsh scripts/Set-LspSchemaPaths.ps1 + pwsh scripts/Set-LspSchemaPaths.ps1 -UpdateVSCode +#> +[CmdletBinding()] +param( + [switch]$UpdateVSCode, + + # Test hook: redirect the VS Code settings.json path so -UpdateVSCode can be exercised + # against a scratch file instead of the real user settings. + [Parameter(DontShow)] + [string]$SettingsPathOverride +) + +$ErrorActionPreference = 'Stop' +$pluginRoot = Split-Path $PSScriptRoot -Parent + +# Source of truth for the schema association (guarded against .lsp.json drift by +# tests/LspConfig.Tests.ps1). One schema, many file-match globs. Do NOT associate every *.json. +$schemaFile = 'cloud-flow-clientdata.schema.json' +$fileMatch = @( + '**/Workflows/*.json' + '**/Workflows/**/*.json' + '**/*.flow.json' +) + +$schemaPath = Join-Path $pluginRoot 'schemas' $schemaFile +if (-not (Test-Path $schemaPath)) { + throw "Schema not found: $schemaPath" +} +# A proper file URI, cross-platform (file:///C:/... on Windows, file:///home/... on POSIX). +$schemaUri = ([uri](Resolve-Path $schemaPath).Path).AbsoluteUri + +$jsonBlock = [ordered]@{ + validate = [ordered]@{ enable = $true } + schemas = @( + [ordered]@{ fileMatch = $fileMatch; url = $schemaUri } + ) +} + +# --- .lsp.json --- +$lspPath = Join-Path $pluginRoot '.lsp.json' +$lsp = Get-Content $lspPath -Raw | ConvertFrom-Json +$lsp.json.initializationOptions.settings.json = $jsonBlock +$lsp.json.settings.json = $jsonBlock +$lsp | ConvertTo-Json -Depth 20 | Set-Content $lspPath -Encoding UTF8 +Write-Host "Updated $lspPath (schema -> $schemaUri)" -ForegroundColor Green + +# --- VS Code user settings.json (optional) --- +if ($UpdateVSCode) { + if ($SettingsPathOverride) { + $settingsPath = $SettingsPathOverride + } + else { + $userDir = if ($IsWindows) { Join-Path $env:APPDATA 'Code\User' } + elseif ($IsMacOS) { "$HOME/Library/Application Support/Code/User" } + else { "$HOME/.config/Code/User" } + $settingsPath = Join-Path $userDir 'settings.json' + } + if (Test-Path $settingsPath) { + $ourEntry = [ordered]@{ fileMatch = $fileMatch; url = $schemaUri } + + # ConvertFrom-Json silently accepts JSONC, so rewriting a commented settings.json would + # strip every comment with no error. Refuse instead and show what to add. Comment tokens + # must follow line-start or whitespace: bare '//' would match URLs (https://...) and bare + # '/*' would match the glob patterns this script itself writes (**/*.json). + $raw = Get-Content $settingsPath -Raw + if ($raw -match '(?m)(^|\s)(//|/\*)') { + Write-Host "$settingsPath contains comments, which this script cannot preserve; not modified." -ForegroundColor Yellow + Write-Host "Add this to json.schemas manually:" -ForegroundColor Yellow + Write-Host ($ourEntry | ConvertTo-Json -Depth 5) + } + else { + $s = $raw | ConvertFrom-Json + # Merge by url: keep the user's other schema associations, replace/append ours. + $kept = @() + if ($s.PSObject.Properties['json.schemas']) { + $kept = @($s.'json.schemas' | Where-Object { $_.url -ne $schemaUri }) + } + $s | Add-Member -NotePropertyName 'json.schemas' -NotePropertyValue ($kept + $ourEntry) -Force + Copy-Item $settingsPath "$settingsPath.bak" -Force + $s | ConvertTo-Json -Depth 20 | Set-Content $settingsPath -Encoding UTF8 + Write-Host "Updated $settingsPath (backup at $settingsPath.bak)" -ForegroundColor Green + } + } + else { Write-Host "VS Code settings.json not found at $settingsPath; skipped." -ForegroundColor Yellow } +} + +Write-Host "`nDone. Reload plugins (/reload-plugins) or restart VS Code to apply." -ForegroundColor Cyan diff --git a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs b/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs new file mode 100644 index 0000000..c3dbe16 --- /dev/null +++ b/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs @@ -0,0 +1,130 @@ +// End-to-end health check for the flow LSP: launches vscode-json-language-server exactly as +// .lsp.json does, feeds it the schema association the plugin ships, opens each test fixture under +// a Workflows/ URI, and asserts the schema actually fires — valid fixtures yield 0 diagnostics, +// invalid fixtures yield >= 1. Exit 0 = healthy, non-zero = broken install/schema/wiring. +// +// It runs TWO scenarios so the check matches Claude Code's real mechanism, which is not fully +// documented for the workspace/configuration pull: +// push - schemas provided ONLY via initializationOptions + workspace/didChangeConfiguration, +// with NO configuration capability advertised and pulls left unanswered. This is what +// the Claude Code docs describe ("settings passed via workspace/didChangeConfiguration"). +// pull - configuration capability advertised and the server's workspace/configuration request +// answered. The alternative path some clients use. +// Both must pass, so the plugin works regardless of which path the host actually uses. +// +// Usage: node scripts/lsp-smoke.mjs +import { spawn } from 'node:child_process'; +import { readFileSync, readdirSync } from 'node:fs'; +import { pathToFileURL, fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; + +const here = dirname(fileURLToPath(import.meta.url)); +const pluginRoot = resolve(here, '..'); +const serverEntry = join(pluginRoot, 'node_modules', 'vscode-langservers-extracted', 'lib', 'json-language-server', 'node', 'jsonServerMain.js'); +const schemaUri = pathToFileURL(join(pluginRoot, 'schemas', 'cloud-flow-clientdata.schema.json')).href; +const fixturesDir = join(pluginRoot, 'tests', 'fixtures'); + +// The schema settings the client hands the server (mirrors .lsp.json settings.json). +const jsonSettings = { + validate: { enable: true }, + schemas: [{ fileMatch: ['**/Workflows/*.json', '**/Workflows/**/*.json', '**/*.flow.json'], url: schemaUri }], +}; +// initializationOptions mirrors .lsp.json: handledSchemaProtocols lets the server load file:// schemas. +const initOptions = { provideFormatter: true, handledSchemaProtocols: ['file'], settings: { json: jsonSettings } }; + +function loadCases() { + const cases = []; + for (const kind of ['valid', 'invalid']) { + const dir = join(fixturesDir, kind); + for (const name of readdirSync(dir).filter((f) => f.endsWith('.json'))) { + cases.push({ kind, name, path: join(dir, name) }); + } + } + return cases; +} + +// Run every fixture through a freshly-spawned server under the given config-delivery mode. +// Returns the number of failures. +function runScenario(mode) { + const answerPulls = mode === 'pull'; + const server = spawn(process.execPath, [serverEntry, '--stdio']); + const diagnostics = new Map(); // uri -> diagnostics[] + let buf = Buffer.alloc(0); + + const send = (msg) => { + const s = JSON.stringify({ jsonrpc: '2.0', ...msg }); + server.stdin.write(`Content-Length: ${Buffer.byteLength(s)}\r\n\r\n${s}`); + }; + const handle = (msg) => { + if (msg.method === 'workspace/configuration') { + // In push mode, simulate a host that does NOT supply config via pull (empty results), + // proving the schema still arrives via initializationOptions + didChangeConfiguration. + const result = msg.params.items.map((it) => (answerPulls && it.section === 'json' ? jsonSettings : {})); + send({ id: msg.id, result }); + return; + } + if (msg.id !== undefined && msg.method) { send({ id: msg.id, result: null }); return; } // ack other server requests + if (msg.method === 'textDocument/publishDiagnostics') { diagnostics.set(msg.params.uri, msg.params.diagnostics || []); } + }; + server.stdout.on('data', (chunk) => { + buf = Buffer.concat([buf, chunk]); + for (;;) { + const headerEnd = buf.indexOf('\r\n\r\n'); + if (headerEnd === -1) break; + const m = buf.slice(0, headerEnd).toString('ascii').match(/Content-Length:\s*(\d+)/i); + const start = headerEnd + 4; + if (!m) { buf = buf.slice(start); continue; } + const len = parseInt(m[1], 10); + if (buf.length < start + len) break; + handle(JSON.parse(buf.slice(start, start + len).toString('utf8'))); + buf = buf.slice(start + len); + } + }); + + const waitFor = (uri, timeoutMs = 4000) => new Promise((res, rej) => { + const started = Date.now(); + const iv = setInterval(() => { + if (diagnostics.has(uri)) { clearInterval(iv); res(diagnostics.get(uri)); } + else if (Date.now() - started > timeoutMs) { clearInterval(iv); rej(new Error(`no diagnostics within ${timeoutMs}ms`)); } + }, 25); + }); + + return (async () => { + const capabilities = answerPulls + ? { workspace: { configuration: true, didChangeConfiguration: {} } } + : { workspace: { didChangeConfiguration: {} } }; // no `configuration` -> push model + send({ id: 1, method: 'initialize', params: { processId: process.pid, rootUri: pathToFileURL(pluginRoot).href, initializationOptions: initOptions, capabilities } }); + await new Promise((r) => setTimeout(r, 300)); + send({ method: 'initialized', params: {} }); + send({ method: 'workspace/didChangeConfiguration', params: { settings: { json: jsonSettings } } }); + + let failures = 0; + console.log(`\n[${mode}]`); + for (const c of loadCases()) { + const text = readFileSync(c.path, 'utf8'); + // Synthetic Workflows/ URI so the schema's fileMatch applies regardless of the fixture's real path. + const uri = pathToFileURL(join(pluginRoot, 'Workflows', `${mode}-${c.kind}-${c.name}`)).href; + send({ method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'json', version: 1, text } } }); + let diags; + try { diags = await waitFor(uri); } + catch (e) { console.error(` ERROR ${c.kind}/${c.name}: ${e.message}`); failures++; continue; } + const ok = c.kind === 'valid' ? diags.length === 0 : diags.length > 0; + const detail = diags.length ? ` (${diags[0].message})` : ''; + console.log(` ${ok ? 'PASS' : 'FAIL'} ${c.kind}/${c.name} -> ${diags.length} diagnostic(s)${ok ? '' : detail}`); + if (!ok) failures++; + } + server.kill(); + return failures; + })(); +} + +async function main() { + if (!loadCases().length) { console.error('No fixtures found under', fixturesDir); process.exit(3); } + let failures = 0; + for (const mode of ['push', 'pull']) failures += await runScenario(mode); + if (failures) { console.error(`\nLSP smoke: ${failures} case(s) failed.`); process.exit(1); } + console.log('\nLSP smoke: all cases passed (push + pull).'); + process.exit(0); +} + +main().catch((e) => { console.error(e); process.exit(2); }); diff --git a/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 b/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 new file mode 100644 index 0000000..4c4f68f --- /dev/null +++ b/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 @@ -0,0 +1,99 @@ +#requires -Version 7 +<# + Two guards: + 1. File-association parity between the $fileMatch / $schemaFile source of truth in + scripts/Set-LspSchemaPaths.ps1 (what setup stamps) and .lsp.json (what ships and what the + JSON language server actually loads). Compares glob list + schema filename, not the full + url: the committed url is a relative path and a machine-local file URI once stamped, but + the filename part is identical in both forms. + 2. The bundled schema actually distinguishes the known-good fixtures from the known-bad ones, + via PowerShell's built-in Test-Json (the same schema the LSP loads). +#> + +Describe 'flow LSP config parity (.lsp.json vs Set-LspSchemaPaths.ps1)' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + + # Extract $fileMatch and $schemaFile from the script without executing it: dot-sourcing + # would rewrite .lsp.json as a side effect. SafeGetValue reads the literals from the AST. + $scriptPath = Join-Path $pluginRoot 'scripts' 'Set-LspSchemaPaths.ps1' + $ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$null, [ref]$null) + + # Collect every string literal on the assignment's right-hand side. Works for both a single + # string ($schemaFile = '...') and a newline- or comma-separated array (@('a' 'b' 'c')). + function Get-AssignmentString([System.Management.Automation.Language.Ast]$Root, [string]$Name) { + $assign = $Root.Find({ + param($node) + $node -is [System.Management.Automation.Language.AssignmentStatementAst] -and + $node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and + $node.Left.VariablePath.UserPath -eq $Name + }, $true) + if (-not $assign) { throw "No `$$Name assignment found in $scriptPath" } + return @($assign.Right.FindAll({ param($n) $n -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) | ForEach-Object { $_.Value }) + } + + $script:expectedGlobs = @(Get-AssignmentString $ast 'fileMatch') | Sort-Object + $script:expectedSchemaFile = @(Get-AssignmentString $ast 'schemaFile')[0] + $script:lsp = Get-Content (Join-Path $pluginRoot '.lsp.json') -Raw | ConvertFrom-Json + + function Get-Assoc($Schemas) { + $entry = @($Schemas)[0] + [pscustomobject]@{ + Globs = @($entry.fileMatch) | Sort-Object + SchemaFile = ($entry.url -split '[\\/]')[-1] + } + } + } + + It 'has a non-empty glob list and a schema file in the script' { + $expectedGlobs.Count | Should -BeGreaterThan 0 + $expectedSchemaFile | Should -Not -BeNullOrEmpty + } + + It 'matches in the initializationOptions settings block' { + $a = Get-Assoc $lsp.json.initializationOptions.settings.json.schemas + $a.Globs | Should -Be $expectedGlobs + $a.SchemaFile | Should -Be $expectedSchemaFile + } + + It 'matches in the workspace settings block' { + $a = Get-Assoc $lsp.json.settings.json.schemas + $a.Globs | Should -Be $expectedGlobs + $a.SchemaFile | Should -Be $expectedSchemaFile + } + + It 'launches the JSON server via node + jsonServerMain.js' { + $lsp.json.command | Should -Be 'node' + ($lsp.json.args -join ' ') | Should -Match 'jsonServerMain\.js' + ($lsp.json.args -join ' ') | Should -Match '--stdio' + } +} + +Describe 'bundled schema distinguishes valid from invalid fixtures' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $script:schema = Join-Path $pluginRoot 'schemas' 'cloud-flow-clientdata.schema.json' + $script:validDir = Join-Path $pluginRoot 'tests' 'fixtures' 'valid' + $script:invalidDir = Join-Path $pluginRoot 'tests' 'fixtures' 'invalid' + + function Test-Fixture([string]$Path) { + # Test-Json throws on a schema violation; treat that as "invalid", a clean $true as "valid". + try { return [bool](Get-Content $Path -Raw | Test-Json -SchemaFile $script:schema -ErrorAction Stop) } + catch { return $false } + } + } + + It 'accepts every valid fixture' { + $files = @(Get-ChildItem $validDir -Filter *.json) + $files.Count | Should -BeGreaterThan 0 + foreach ($f in $files) { Test-Fixture $f.FullName | Should -BeTrue -Because "$($f.Name) should be valid" } + } + + It 'rejects every invalid fixture' { + $files = @(Get-ChildItem $invalidDir -Filter *.json) + $files.Count | Should -BeGreaterThan 0 + foreach ($f in $files) { Test-Fixture $f.FullName | Should -BeFalse -Because "$($f.Name) should be rejected" } + } +} diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json b/plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json new file mode 100644 index 0000000..13ef0eb --- /dev/null +++ b/plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "triggers": { + "Recurrence": { "type": "Recurrence" } + }, + "actions": { + "Second_action": { + "type": "Compose", + "inputs": "x", + "runAfter": { "First_action": ["Done"] } + } + } + } + } +} diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json b/plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json new file mode 100644 index 0000000..3faaa42 --- /dev/null +++ b/plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "connectionReferences": {} + } +} diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json b/plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json new file mode 100644 index 0000000..8595ee1 --- /dev/null +++ b/plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": "1.0.0.0", + "properties": { + "connectionReferences": { + "shared_commondataserviceforapps": { + "runtimeSource": "embedded", + "connection": { "connectionReferenceLogicalName": "new_dataverseconn" }, + "api": { "name": "shared_commondataserviceforapps" } + } + }, + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "$connections": { "type": "Object" }, + "$authentication": { "type": "SecureObject" } + }, + "triggers": { + "When_a_row_is_added": { + "type": "OpenApiConnectionWebhook", + "inputs": { + "host": { "connectionName": "shared_commondataserviceforapps", "operationId": "SubscribeWebhookTrigger", "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps" } + } + } + }, + "actions": { + "Compose_greeting": { + "type": "Compose", + "inputs": "hello", + "runAfter": {} + } + }, + "outputs": {} + } + } +} From 4e1483b15349940abe8738252c7f0e73a016b45c Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 11:17:09 +1200 Subject: [PATCH 2/2] Fix empty schema URI on Linux; smoke-test the stamped .lsp.json Set-LspSchemaPaths built the file URI via [uri].AbsoluteUri, which returns an empty string for a rooted POSIX path on Linux PowerShell (stamped "schema -> "). Build the file:// URI by hand instead, so it is correct on Windows and Linux. Also make lsp-smoke.mjs read the json settings straight from .lsp.json (resolving a relative url to a file URI) rather than recomputing them, so the self-check exercises the actual stamped schema url and would catch a bad stamp directly. --- .../scripts/Set-LspSchemaPaths.ps1 | 7 +++++-- .../scripts/lsp-smoke.mjs | 16 ++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 b/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 index 1d108a1..35bf00c 100644 --- a/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 +++ b/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 @@ -40,8 +40,11 @@ $schemaPath = Join-Path $pluginRoot 'schemas' $schemaFile if (-not (Test-Path $schemaPath)) { throw "Schema not found: $schemaPath" } -# A proper file URI, cross-platform (file:///C:/... on Windows, file:///home/... on POSIX). -$schemaUri = ([uri](Resolve-Path $schemaPath).Path).AbsoluteUri +# Build the file URI by hand (file:///C:/... on Windows, file:///home/... on POSIX). Do NOT use +# [uri].AbsoluteUri: on Linux PowerShell it yields an empty string for a rooted POSIX path. +$abs = (Resolve-Path $schemaPath).Path -replace '\\', '/' +if ($abs -notmatch '^/') { $abs = "/$abs" } # Windows drive path (C:/...) needs the leading slash +$schemaUri = 'file://' + ($abs -replace ' ', '%20') $jsonBlock = [ordered]@{ validate = [ordered]@{ enable = $true } diff --git a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs b/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs index c3dbe16..b75dc30 100644 --- a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs +++ b/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs @@ -21,14 +21,18 @@ import { dirname, join, resolve } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(here, '..'); const serverEntry = join(pluginRoot, 'node_modules', 'vscode-langservers-extracted', 'lib', 'json-language-server', 'node', 'jsonServerMain.js'); -const schemaUri = pathToFileURL(join(pluginRoot, 'schemas', 'cloud-flow-clientdata.schema.json')).href; const fixturesDir = join(pluginRoot, 'tests', 'fixtures'); -// The schema settings the client hands the server (mirrors .lsp.json settings.json). -const jsonSettings = { - validate: { enable: true }, - schemas: [{ fileMatch: ['**/Workflows/*.json', '**/Workflows/**/*.json', '**/*.flow.json'], url: schemaUri }], -}; +// Use the SHIPPED config: read the exact json settings the host will hand the server from +// .lsp.json, so this check exercises the real (stamped) schema url, not a recomputed one. A +// relative url (the committed, un-stamped form) is resolved to a file URI here so a standalone +// run still works before setup has stamped it. +const lsp = JSON.parse(readFileSync(join(pluginRoot, '.lsp.json'), 'utf8')); +const jsonSettings = JSON.parse(JSON.stringify(lsp.json.settings.json)); // deep copy +for (const s of jsonSettings.schemas || []) { + if (!/^(file|https?):\/\//.test(s.url)) s.url = pathToFileURL(resolve(pluginRoot, s.url)).href; +} +if (!jsonSettings.schemas?.[0]?.url) { console.error('.lsp.json has no schema url'); process.exit(3); } // initializationOptions mirrors .lsp.json: handledSchemaProtocols lets the server load file:// schemas. const initOptions = { provideFormatter: true, handledSchemaProtocols: ['file'], settings: { json: jsonSettings } };