diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 786fe22..d3132cc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,15 +8,15 @@ }, "plugins": [ { - "name": "dataverse-customization-xml", - "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.", + "name": "dataverse-xml-lsp", + "source": "./plugins/dataverse-xml-lsp", + "description": "LSP + CLI validator for Dataverse customization XML: live lemminx diagnostics and a standalone PowerShell validator that check ribbon, sitemap, forms, FetchXML, charts and more against the official Microsoft XSDs.", "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.", + "name": "cloud-flow-json-lsp", + "source": "./plugins/cloud-flow-json-lsp", + "description": "JSON LSP for unpacked Power Automate cloud-flow clientdata: live vscode-json-language-server diagnostics on solution Workflows/*.json against a bundled clientdata/WDL wrapper schema.", "category": "development" } ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bed5909..1832124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,22 +32,22 @@ jobs: - name: Cache lemminx binary uses: actions/cache@v4 with: - path: plugins/dataverse-customization-xml/bin - key: lemminx-${{ runner.os }}-${{ hashFiles('plugins/dataverse-customization-xml/versions.json') }} + path: plugins/dataverse-xml-lsp/bin + key: lemminx-${{ runner.os }}-${{ hashFiles('plugins/dataverse-xml-lsp/versions.json') }} - - name: End-to-end setup (fetch schemas + lemminx, stamp paths, self-check) - run: ./plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 + - name: End-to-end setup (fetch schemas + lemminx, self-check) + run: ./plugins/dataverse-xml-lsp/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') }} + path: plugins/cloud-flow-json-lsp/node_modules + key: flow-lsp-${{ runner.os }}-${{ hashFiles('plugins/cloud-flow-json-lsp/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 + - name: End-to-end setup (install JSON language server, LSP self-check) + run: ./plugins/cloud-flow-json-lsp/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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcf911..684c943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,13 @@ # 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. +## 2.0.0 — 2026-07-21 + +### Changed (breaking) +- Renamed plugins: `dataverse-customization-xml` → `dataverse-xml-lsp`, `power-automate-cloud-flow` → `cloud-flow-json-lsp`. Reinstall under the new id and update your `enabledPlugins` key. +- Each plugin is now an LSP server plus a single manually-run, no-description setup skill (`/:-setup`). The auto-triggering skill and the `/:setup` slash command are removed — editing guidance no longer surfaces automatically; it lives in `docs/guide.md`. +- LSP schema path is resolved at launch (launcher shim), so setup no longer stamps `.lsp.json` and need not be re-run after `/plugin update`. + +The `dataverse-xml-lsp` plugin keeps `Validate-DataverseXml.ps1` for CI/headless and pac wrapper-file (forms/charts) validation; those wrapper files are validator-owned (not LSP-associated). ## 1.0.1 — 2026-07-16 diff --git a/README.md b/README.md index db68857..5e2a7ed 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,22 @@ plugin also works standalone (plain PowerShell scripts) for other agents, VS Cod ## Install (Claude Code) /plugin marketplace add Cordedmink2/dataverse-agent-plugins - /plugin install dataverse-customization-xml@dataverse-agent-plugins - /dataverse-customization-xml:setup + /plugin install dataverse-xml-lsp@dataverse-agent-plugins + /dataverse-xml-lsp:dataverse-xml-lsp-setup Update later with `/plugin marketplace update dataverse-agent-plugins` then -`/plugin update dataverse-customization-xml@dataverse-agent-plugins` (re-run setup after updates). +`/plugin update dataverse-xml-lsp@dataverse-agent-plugins` (re-run setup after updates). ## Plugins | 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. | +| [dataverse-xml-lsp](plugins/dataverse-xml-lsp/) | LSP + CLI validator for Dataverse customization XML (ribbon, sitemap, forms, FetchXML, charts, ISV config and more) against the official Microsoft XSDs. Live lemminx LSP diagnostics + a standalone PowerShell validator. Also usable from [Codex](plugins/dataverse-xml-lsp/docs/codex.md), [VS Code with no agent](plugins/dataverse-xml-lsp/docs/vscode.md), and CI. | +| [cloud-flow-json-lsp](plugins/cloud-flow-json-lsp/) | JSON LSP for unpacked Power Automate cloud-flow clientdata (`Workflows/*.json`), validated 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/cloud-flow-json-lsp/docs/codex.md), [VS Code with no agent](plugins/cloud-flow-json-lsp/docs/vscode.md), and CI. | ## Requirements -PowerShell 7+ (`pwsh`) on Windows, macOS or Linux. The `power-automate-cloud-flow` plugin also +PowerShell 7+ (`pwsh`) on Windows, macOS or Linux. The `cloud-flow-json-lsp` 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. diff --git a/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md b/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md new file mode 100644 index 0000000..80a5330 --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md @@ -0,0 +1,366 @@ +# LSP Plugin Rename + No-Restamp Restructure — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename both marketplace plugins to LSP-first names, replace each auto-triggering skill + setup command with one tiny no-description setup skill, eliminate the per-machine schema-path re-stamp, and raise test coverage — without losing the XML plugin's CLI validator. + +**Architecture:** Each plugin stays `skills: ["./"]` with a tiny `SKILL.md` (name only, no description). The LSP resolves its bundled schema at launch via a launcher shim referenced in `.lsp.json` `command`/`args` (where `${CLAUDE_PLUGIN_ROOT}` substitutes), so no absolute path is stamped. The XML plugin keeps `Validate-DataverseXml.ps1` as the non-LSP/CI channel (no hook). Guidance moves to `docs/guide.md`. + +**Tech Stack:** Claude Code plugins (`.claude-plugin/plugin.json`, `.lsp.json`), PowerShell 7 scripts, Node (`vscode-langservers-extracted` JSON server + a node shim), lemminx (Java) for XML, Pester + PSScriptAnalyzer, GitHub Actions. + +**User decisions (already made):** +- "I want both plugins to just be lsp servers" with "a tiny skill with no description that they have to manually run to independently setup". +- Names: `dataverse-customization-xml` → `dataverse-xml-lsp`; `power-automate-cloud-flow` → `cloud-flow-json-lsp` (asymmetry intentional; `dataverse-json-lsp` rejected). +- "Keep the XML validator"; flow stays pure LSP; no flow validator and no hook in this change (deferred). +- Guidance → `docs/guide.md`; setup slash command removed; old `docs/superpowers/specs/` updated throughout. +- "try and develop option 1 first and test it and if it doesn't work properly, fallback to option 2" — shim first (gate), hosted-URL only if the shim fails testing; exactly one ships. +- Clean rename via `git mv`, bump both to `2.0.0`, CHANGELOG documents the breaking rename, work on branch `refactor/lsp-only-plugin-rename`. +- Scope = the bundled schemas (full scope kept, incl. the two "indicative" XML layers with their caveat). + +**Spec:** `docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md` + +--- + +## File structure (after rename) + +``` +plugins/dataverse-xml-lsp/ plugins/cloud-flow-json-lsp/ + .claude-plugin/plugin.json .claude-plugin/plugin.json (name, version 2.0.0, desc) + .lsp.json .lsp.json (launch via shim, no stamped path) + SKILL.md (name: *-setup, no desc) SKILL.md (name: *-setup, no desc) + scripts/ + Install-Plugin.ps1 (no stamping) Install-Plugin.ps1 (no stamping) + lsp-launch.mjs (NEW shim) lsp-launch.mjs (NEW shim) + Get-Schemas.ps1 Get-Lemminx.ps1 Install-JsonLanguageServer.ps1 lsp-smoke.mjs + Validate-DataverseXml.ps1 (KEPT) + (Set-LspSchemaPaths.ps1 retired for CC path) + schemas/ tests/ schemas/ tests/ package*.json + docs/ guide.md(NEW) codex vscode debugging +``` + +Both delete `commands/setup.md`. The XML plugin keeps `tests/Validate-DataverseXml.Tests.ps1`. + +--- + +### Task 1: Launcher-shim feasibility spike (DECISION GATE) + +**Goal:** Prove whether a small launcher can inject the schema association into each server at runtime (so `.lsp.json` needs no stamped absolute path), and decide Option 1 (shim) vs Option 2 (hosted URL) for the rest of the plan. + +> **USER-ORDERED GATE — NON-SKIPPABLE.** This task was requested by the user in the current conversation ("develop option 1 first and test it and if it doesn't work properly, fallback to option 2"). It MUST NOT be closed by walking around it, by declaring it "verified inline", or by substituting a cheaper check. Close only after every item in `acceptanceCriteria` has been re-validated independently, with output captured. + +**Files:** +- Create (throwaway, under scratchpad — not committed): a prototype shim per server + a driver reusing the existing `plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs` as the client. +- Record the decision at the top of the spec file `docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md` under a new "Spike result" note. + +**Acceptance Criteria:** +- [ ] A node shim spawns `vscode-json-language-server`, receives the plugin root as an argv, and makes the server validate `tests/fixtures/valid/simple-flow.json` clean and `tests/fixtures/invalid/*.json` as errors — driven from a working directory that is NOT the plugin root, with NO absolute path written into any config file. +- [ ] The equivalent is confirmed for lemminx (inject `xml.fileAssociations` `systemId` from the plugin-root argv) against the XML valid/invalid ribbon fixtures. +- [ ] A go/no-go decision is written to the spec: shim works for both → Option 1; otherwise → Option 2 (hosted URL), naming which server failed and why. + +**Verify:** `node --plugin-root --cwd ` prints the invalid fixture's diagnostics and exits non-zero on the bad fixture, zero on the good one — for both servers. + +**Steps:** + +- [ ] **Step 1: Install both servers locally** so the spike can drive real binaries. + +```bash +pwsh plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 +pwsh plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 +``` + +- [ ] **Step 2: Prototype the JSON shim.** In the scratchpad, write a node script that: + reads `--plugin-root`; spawns `node /node_modules/.../jsonServerMain.js --stdio`; + proxies stdio; and injects `json.schemas:[{fileMatch:[...], url:"file:///schemas/cloud-flow-clientdata.schema.json"}]` into the config the server pulls (answer the server's `workspace/configuration` request, and set `initializationOptions.settings.json.schemas` on the `initialize` it forwards). Reuse `lsp-smoke.mjs` as the driving client but launch the shim instead of the server, and run it from a temp cwd. + +- [ ] **Step 3: Confirm JSON validation fires** with no stamped path (good fixture clean, bad fixtures flagged). Capture output. + +- [ ] **Step 4: Prototype the lemminx shim** the same way, injecting `xml.fileAssociations` (systemId = `file:///schemas//`) for the ribbon glob; drive it against `tests/fixtures/valid/ribbon.xml` and `invalid/ribbon.xml`. + +- [ ] **Step 5: Decide and record.** If both work → Option 1 (shim). If either can't inject reliably → Option 2 (hosted URL) for the whole plan. Append a "Spike result (YYYY-MM-DD)" note to the spec stating the decision and evidence. + +- [ ] **Step 6: Commit the decision note** (spike prototypes stay in scratchpad, uncommitted). + +```bash +git add docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md +git commit -m "Record launcher-shim spike result (Option 1 vs 2 decision)" +``` + +--- + +### Task 2: Rename both plugins (atomic) + +**Goal:** Move both plugin directories to the new names and update every reference in one coherent commit, so the tree is never half-renamed. + +**Files:** +- Move: `plugins/dataverse-customization-xml/` → `plugins/dataverse-xml-lsp/`; `plugins/power-automate-cloud-flow/` → `plugins/cloud-flow-json-lsp/` (via `git mv`). +- Modify: `.claude-plugin/marketplace.json`; each `.claude-plugin/plugin.json`; `plugins/cloud-flow-json-lsp/package.json` + `package-lock.json`; `plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json` (`$id`); `.claude/settings.json`; `.github/workflows/ci.yml`; `README.md`; `llms.txt`; `docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md`; `docs/superpowers/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md`; each plugin's `README.md`, `docs/codex.md`, `docs/vscode.md`, `docs/debugging.md`. +- Delete: `plugins/*/commands/setup.md` (both). + +**Acceptance Criteria:** +- [ ] Both plugin dirs exist under the new names; `git mv` preserved history. +- [ ] `marketplace.json` has both new `name`+`source`; both `plugin.json` have new `name`, `"version": "2.0.0"`, and an LSP-first `description`. +- [ ] Flow `package.json`/`package-lock.json` name = `cloud-flow-json-lsp`; flow schema `$id` uses the new path. +- [ ] `.claude/settings.json` key is `dataverse-xml-lsp@dataverse-agent-plugins`. +- [ ] CI paths/cache keys point at the new dirs; `commands/setup.md` gone from both. +- [ ] `grep -ri` for both old slugs (excluding `node_modules`/`.git`) returns only `CHANGELOG.md` 1.0.x lines. + +**Verify:** `grep -rin --exclude-dir=node_modules --exclude-dir=.git -e dataverse-customization-xml -e power-automate-cloud-flow .` → only CHANGELOG 1.0.x hits; then `pwsh -c "Invoke-Pester plugins"` → all pass. + +**Steps:** + +- [ ] **Step 1: Move the directories.** + +```bash +git mv plugins/dataverse-customization-xml plugins/dataverse-xml-lsp +git mv plugins/power-automate-cloud-flow plugins/cloud-flow-json-lsp +git rm plugins/dataverse-xml-lsp/commands/setup.md plugins/cloud-flow-json-lsp/commands/setup.md +``` + +- [ ] **Step 2: Update manifests.** In `.claude-plugin/marketplace.json` set both `name` + `source` (`./plugins/dataverse-xml-lsp`, `./plugins/cloud-flow-json-lsp`) and reword each `description` to lead with "LSP". In each `plugins/*/.claude-plugin/plugin.json` set `name` to the new slug, `"version": "2.0.0"`, and an LSP-first `description`. + +- [ ] **Step 3: Update flow npm identity + schema id.** In `plugins/cloud-flow-json-lsp/package.json` and `package-lock.json` set `name` to `cloud-flow-json-lsp`. In `schemas/cloud-flow-clientdata.schema.json` change `$id` to `https://github.com/Cordedmink2/dataverse-agent-plugins/cloud-flow-json-lsp/cloud-flow-clientdata.schema.json`. + +- [ ] **Step 4: Update repo-level references.** `.claude/settings.json` enabledPlugins key; `.github/workflows/ci.yml` (cache `path:` + `key:` + the `run:` script paths for both plugins); `README.md`; `llms.txt`; both `docs/superpowers/specs/*.md` (replace old slugs/paths/`/…:setup` command refs throughout). + +- [ ] **Step 5: Update per-plugin docs.** In each plugin's `README.md` and `docs/{codex,vscode,debugging}.md`, replace old slug, paths, install/update commands, and `/:setup` references. (The `/:setup` command no longer exists — reword those to the setup-skill invocation, finalized in Task 5.) + +- [ ] **Step 6: Verify + commit.** + +```bash +grep -rin --exclude-dir=node_modules --exclude-dir=.git -e dataverse-customization-xml -e power-automate-cloud-flow . | grep -v CHANGELOG.md +pwsh -c "Invoke-Pester plugins" +git add -A +git commit -m "Rename plugins to dataverse-xml-lsp and cloud-flow-json-lsp (2.0.0)" +``` + +--- + +### Task 3: No-restamp schema resolution (productionize the spike) + +**Goal:** Ship the mechanism chosen in Task 1 so the committed `.lsp.json` resolves its schema on every machine with no stamping, and remove the stamping step from setup. + +**Files:** +- Option 1 (shim, primary): Create `plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs` and `plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs` (hardened from the Task 1 prototype). Modify both `.lsp.json` (`command: "node"`, `args: ["${CLAUDE_PLUGIN_ROOT}/scripts/lsp-launch.mjs", "${CLAUDE_PLUGIN_ROOT}", "--stdio"]`; remove the stamped `settings`/`initializationOptions` schema `url`). Modify both `scripts/Install-Plugin.ps1` (drop the `Set-LspSchemaPaths.ps1` call for the CC path). Retire `Set-LspSchemaPaths.ps1` for the CC path (keep only the `-UpdateVSCode` branch if VS Code stamping is still wanted). +- Option 2 (hosted URL, only if Task 1 said so): host the schema(s); set `handledSchemaProtocols` to include `https`; put the stable URL in `.lsp.json`; same Install-Plugin/Set-LspSchemaPaths simplification. + +**Acceptance Criteria:** +- [ ] `.lsp.json` contains no machine-local absolute path; it uses `${CLAUDE_PLUGIN_ROOT}` (shim) or an `https://` URL (fallback). +- [ ] After copying the plugin to a fresh path and running the LSP, the schema still loads (no re-stamp). +- [ ] `Install-Plugin.ps1` no longer edits `.lsp.json`. + +**Verify:** From a temp copy of the plugin dir, drive the LSP via the shim/URL against a bad fixture and confirm diagnostics fire — no stamping step run. (Shim: `node scripts/lsp-launch.mjs --stdio` fed an LSP init + a bad doc → diagnostics.) + +**Steps:** + +- [ ] **Step 1: Productionize the launcher** from Task 1's prototype into `scripts/lsp-launch.mjs` for each plugin (JSON server for flow, lemminx for XML), reading `process.argv[2]` as the plugin root and injecting the schema association as the spike proved. (Exact injection code = the working prototype from Task 1.) + +- [ ] **Step 2: Point `.lsp.json` at the shim.** Replace `command`/`args` to launch `lsp-launch.mjs` with `${CLAUDE_PLUGIN_ROOT}` as an arg; delete the stamped schema `url` from `settings`/`initializationOptions` (the shim supplies it). Keep the `fileMatch` globs the shim needs (pass them in the shim or keep them in a non-path-bearing settings block). + +- [ ] **Step 3: De-stamp setup.** In `Install-Plugin.ps1`, remove the `Set-LspSchemaPaths.ps1` invocation for the Claude Code path. Retire the CC-path logic in `Set-LspSchemaPaths.ps1` (leave the `-UpdateVSCode` editor-association branch if kept). + +- [ ] **Step 4: Verify from a moved copy.** + +```bash +cp -r plugins/cloud-flow-json-lsp "$TMP/moved-flow" +node "$TMP/moved-flow/scripts/lsp-launch.mjs" "$TMP/moved-flow" --stdio # driven by the smoke client → bad fixture flagged +``` + +- [ ] **Step 5: Commit.** + +```bash +git add plugins/*/scripts/lsp-launch.mjs plugins/*/.lsp.json plugins/*/scripts/Install-Plugin.ps1 plugins/*/scripts/Set-LspSchemaPaths.ps1 +git commit -m "Resolve LSP schema path at launch; drop per-machine stamping" +``` + +--- + +### Task 4: Move guidance to `docs/guide.md` + +**Goal:** Preserve the rich domain guidance the auto-triggering skill carried by moving it into a reference doc, before the skill is shrunk. + +**Files:** +- Create: `plugins/dataverse-xml-lsp/docs/guide.md`, `plugins/cloud-flow-json-lsp/docs/guide.md`. +- Modify: each plugin's `docs/codex.md` (repoint "follow `SKILL.md`" → `docs/guide.md`); each plugin's `README.md` (add a prominent pointer to `docs/guide.md`). + +**Acceptance Criteria:** +- [ ] `docs/guide.md` (XML) contains the validate loop, the ribbon/command-bar recipe, the "rules the XSD doesn't catch" list, gotchas, and schema-refresh section from the old `SKILL.md`. +- [ ] `docs/guide.md` (flow) contains the shape note, attach globs, the ad-hoc `Test-Json` one-liner, and gotchas. +- [ ] Each `guide.md` opens noting it is the manual replacement for the old auto-loaded skill; `codex.md` and `README.md` point at it. + +**Verify:** `grep -l "manual replacement for the old" plugins/*/docs/guide.md` lists both; `grep -r "guide.md" plugins/*/README.md plugins/*/docs/codex.md` shows the pointers. + +**Steps:** + +- [ ] **Step 1: Create `docs/guide.md`** for each plugin by moving the body of the current `SKILL.md` (everything below the frontmatter) into it, with a one-line preamble: `> This is the manual replacement for the old auto-loaded skill; the plugin no longer surfaces this automatically.` + +- [ ] **Step 2: Repoint pointers.** In each `docs/codex.md`, change "follow `SKILL.md`" to "follow `docs/guide.md`". In each `README.md`, add a line: `See docs/guide.md for editing guidance.` + +- [ ] **Step 3: Commit.** + +```bash +git add plugins/*/docs/guide.md plugins/*/docs/codex.md plugins/*/README.md +git commit -m "Move plugin editing guidance into docs/guide.md" +``` + +--- + +### Task 5: Shrink `SKILL.md` to a tiny no-description setup skill + +**Goal:** Replace each auto-triggering skill body with a tiny, no-description skill whose only job is to run setup. + +**Files:** +- Modify (overwrite): `plugins/dataverse-xml-lsp/SKILL.md`, `plugins/cloud-flow-json-lsp/SKILL.md`. + +**Acceptance Criteria:** +- [ ] Frontmatter has `name:` (`dataverse-xml-lsp-setup` / `cloud-flow-json-lsp-setup`) and **no `description:`**. +- [ ] Body is ~4 lines: what the plugin is, the one-time `Install-Plugin.ps1` command (mention `-UpdateVSCode`), the `/reload-plugins` reminder + `docs/debugging.md` on failure, and the `docs/guide.md` pointer. +- [ ] `/reload-plugins` lists the skill with no description and it is manually invocable as `/:-setup`. + +**Verify:** `pwsh -c "Invoke-Pester plugins"` (the Task 6 setup-skill guard passes); manual `/reload-plugins` then `/dataverse-xml-lsp:dataverse-xml-lsp-setup` runs setup. + +**Steps:** + +- [ ] **Step 1: Write the XML `SKILL.md`.** + +```markdown +--- +name: dataverse-xml-lsp-setup +--- + +# dataverse-xml-lsp setup + +This plugin is a lemminx XML LSP for Dataverse customization XML (plus `scripts/Validate-DataverseXml.ps1` for CI/headless). Run once per machine: + +`pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` (add `-UpdateVSCode` to also wire VS Code). Then run `/reload-plugins`. If it fails, see `docs/debugging.md`. Editing guidance: `docs/guide.md`. +``` + +- [ ] **Step 2: Write the flow `SKILL.md`.** + +```markdown +--- +name: cloud-flow-json-lsp-setup +--- + +# cloud-flow-json-lsp setup + +This plugin is a JSON LSP for unpacked Power Automate cloud-flow clientdata (`Workflows/*.json`). Run once per machine: + +`pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` (add `-UpdateVSCode` to also wire VS Code). Then run `/reload-plugins`. If it fails, see `docs/debugging.md`. Editing guidance: `docs/guide.md`. +``` + +- [ ] **Step 3: Commit.** + +```bash +git add plugins/*/SKILL.md +git commit -m "Replace auto-triggering skills with tiny no-description setup skills" +``` + +--- + +### Task 6: Tests — raise to well-tested + +**Goal:** Lock the new behaviors with tests: no-description skill, setup idempotency, XML LSP smoke, and shim resolution. + +**Files:** +- Create: `plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1`, `plugins/cloud-flow-json-lsp/tests/SetupSkill.Tests.ps1`; `plugins/dataverse-xml-lsp/scripts/lsp-smoke-xml.mjs` (or `.ps1`) + a test invoking it. +- Modify: keep `plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1` (unchanged); extend `LspConfig.Tests.ps1` if needed to assert the shim launch shape. + +**Acceptance Criteria:** +- [ ] A test asserts each `SKILL.md` parses, `name` == `-setup`, and there is no `description` key. +- [ ] A test runs `Install-Plugin.ps1` twice and asserts the second run succeeds (idempotent). +- [ ] An XML LSP smoke drives lemminx end-to-end (good fixture clean, bad flagged), parity with `cloud-flow-json-lsp/scripts/lsp-smoke.mjs`. +- [ ] `Validate-DataverseXml.Tests.ps1` still passes; a test asserts the LSP is NOT associated to wrapper roots (``, ``). + +**Verify:** `pwsh -c "Invoke-Pester plugins"` → all green, including the new suites. + +**Steps:** + +- [ ] **Step 1: Setup-skill guard test** (both plugins), e.g. `plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1`: + +```powershell +#requires -Version 7 +Describe 'setup skill frontmatter' { + BeforeAll { + $skill = Join-Path (Split-Path $PSScriptRoot -Parent) 'SKILL.md' + $fm = (Get-Content $skill -Raw) -split '(?m)^---\s*$' | Select-Object -Index 1 + } + It 'names the skill -setup' { $fm | Should -Match 'name:\s*dataverse-xml-lsp-setup' } + It 'has no description key' { $fm | Should -Not -Match '(?m)^\s*description\s*:' } +} +``` + +- [ ] **Step 2: Run it to see it pass** (after Task 5): `pwsh -c "Invoke-Pester plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1"` → PASS. + +- [ ] **Step 3: Idempotency test** — a Pester `It` that runs `Install-Plugin.ps1` twice and asserts `$LASTEXITCODE -eq 0` both times (guard with a skip when `node`/network unavailable so CI stays green). + +- [ ] **Step 4: XML LSP smoke** — adapt `cloud-flow-json-lsp/scripts/lsp-smoke.mjs` to drive lemminx via the shim, assert the invalid ribbon fixture yields diagnostics and the valid one does not; add a Pester wrapper test. + +- [ ] **Step 5: Wrapper-root non-association assertion** — a test that reads `.lsp.json` (and/or `Set-LspSchemaPaths` associations) and asserts no association glob targets forms/chart wrapper files. + +- [ ] **Step 6: Commit.** + +```bash +git add plugins/*/tests/SetupSkill.Tests.ps1 plugins/dataverse-xml-lsp/scripts/lsp-smoke-xml.mjs plugins/*/tests +git commit -m "Add tests: no-description skill, setup idempotency, XML LSP smoke" +``` + +--- + +### Task 7: CHANGELOG + final end-to-end verification (ACCEPTANCE GATE) + +**Goal:** Document the breaking 2.0.0 change and prove the whole restructure works end-to-end. + +> **USER-ORDERED GATE — NON-SKIPPABLE.** This task was requested by the user in the current conversation (the north-star: "really good and well tested"). It MUST NOT be closed by walking around it, by declaring it "verified inline", or by substituting a cheaper check. Close only after every item in `acceptanceCriteria` has been re-validated independently, with output captured. + +**Files:** +- Modify: `CHANGELOG.md` (new top entry). + +**Acceptance Criteria:** +- [ ] CHANGELOG top entry documents: renames (old→new), LSP + tiny no-description setup skill, guidance → `docs/guide.md`, setup command removed, re-stamp eliminated; marked **breaking** with the reinstall + `enabledPlugins` migration note; states the "no auto-guidance" consequence. +- [ ] `Install-Plugin.ps1` self-check passes for both plugins. +- [ ] `Invoke-Pester plugins` and `Invoke-ScriptAnalyzer -Path plugins -Recurse` are green. +- [ ] `/reload-plugins` loads both with no load error; LSPs attach via the shim with no stamped path; each setup skill lists with no description and is invocable. + +**Verify:** the four commands below all succeed; `/reload-plugins` shows both plugins + LSPs with no error. + +**Steps:** + +- [ ] **Step 1: Write the CHANGELOG entry** at the top of `CHANGELOG.md`: + +```markdown +## 2.0.0 + +### Changed (breaking) +- Renamed plugins: `dataverse-customization-xml` → `dataverse-xml-lsp`, `power-automate-cloud-flow` → `cloud-flow-json-lsp`. Reinstall under the new id and update your `enabledPlugins` key. +- Each plugin is now an LSP server plus a single manually-run, no-description setup skill (`/:-setup`). The auto-triggering skill and the `/:setup` slash command are removed — editing guidance no longer surfaces automatically; it lives in `docs/guide.md`. +- LSP schema path is resolved at launch, so setup no longer stamps `.lsp.json` and need not be re-run after `/plugin update`. + +The XML plugin keeps `Validate-DataverseXml.ps1` for CI/headless/wrapper-file validation. +``` + +- [ ] **Step 2: Run the full verification.** + +```bash +pwsh plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 +pwsh plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 +pwsh -c "Invoke-Pester plugins" +pwsh -c "Invoke-ScriptAnalyzer -Path plugins -Recurse" +``` +Expected: both installs report "self-check passed"; Pester all pass; ScriptAnalyzer clean. + +- [ ] **Step 3: Reload and eyeball.** `/reload-plugins` → both plugins load, no load error, LSP servers attach; `/:-setup` is listed without a description and runs. + +- [ ] **Step 4: Commit.** + +```bash +git add CHANGELOG.md +git commit -m "Document 2.0.0 breaking restructure in CHANGELOG" +``` + +--- + +## Self-review + +- **Spec coverage:** rename (T2), no-restamp shim/URL (T1 decision, T3), tiny no-description skill (T5), guidance→guide.md (T4), XML validator kept (T2/T6 keep the script+test), tests incl. no-desc guard + idempotency + XML smoke + wrapper non-association (T6), versioning + CHANGELOG (T2 version, T7 changelog), verification incl. grep + reload (T2, T7). All spec sections map to a task. +- **Placeholders:** the only deferred code is `lsp-launch.mjs`'s exact injection body, which is intentionally the *output of the Task 1 spike* (can't be finalized before the spike proves it) — Task 3 productionizes that verified prototype. Every other step carries real commands/code. +- **Type/name consistency:** skill names `dataverse-xml-lsp-setup` / `cloud-flow-json-lsp-setup`, shim `scripts/lsp-launch.mjs`, guide `docs/guide.md` used consistently across tasks. diff --git a/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md.tasks.json b/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md.tasks.json new file mode 100644 index 0000000..4b5cb49 --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md.tasks.json @@ -0,0 +1,54 @@ +{ + "planPath": "docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md", + "tasks": [ + { + "id": 6, + "subject": "Task 1: Launcher-shim feasibility spike (gate)", + "status": "completed", + "description": "**Goal:** Prove whether a small launcher can inject the schema association into each server at runtime (so `.lsp.json` needs no stamped absolute path), and decide Option 1 (shim) vs Option 2 (hosted URL).\n\n**USER-ORDERED GATE — NON-SKIPPABLE.** Requested by the user (\"develop option 1 first and test it and if it doesn't work properly, fallback to option 2\"). Close only after every acceptanceCriteria item is re-validated independently, with output captured.\n\n**Files:** throwaway prototype in scratchpad (node shim per server + driver reusing lsp-smoke.mjs); record decision in docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md.\n\n**Acceptance Criteria:**\n- [ ] JSON shim injects schema from argv, validates from non-plugin cwd, no stamped path\n- [ ] lemminx shim confirmed the same way against XML ribbon fixtures\n- [ ] Option 1 vs 2 decision recorded in the spec with evidence\n\n**Verify:** node --plugin-root --cwd flags bad fixture (non-zero), passes good (zero), for both servers.\n\n```json:metadata\n{\"files\": [\"docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md\"], \"verifyCommand\": \"node --plugin-root --cwd \", \"acceptanceCriteria\": [\"JSON shim injects schema from argv, validates from non-plugin cwd, no stamped path\", \"lemminx shim confirmed the same way\", \"Option 1 vs 2 decision recorded in the spec with evidence\"], \"userGate\": true, \"tags\": [\"user-gate\"], \"gateScope\": \"both-servers\", \"modelTier\": \"standard\"}\n```" + }, + { + "id": 7, + "subject": "Task 2: Rename both plugins (atomic)", + "status": "completed", + "blockedBy": [6], + "description": "**Goal:** Move both plugin directories to the new names and update every reference in one coherent commit.\n\n**Files:** git mv both dirs (→ dataverse-xml-lsp, cloud-flow-json-lsp); modify marketplace.json, both plugin.json, flow package.json + package-lock.json, flow schema $id, .claude/settings.json, .github/workflows/ci.yml, README.md, llms.txt, both docs/superpowers/specs/*.md, each plugin's README + docs/{codex,vscode,debugging}.md; delete both commands/setup.md.\n\n**Acceptance Criteria:**\n- [ ] dirs moved via git mv (history preserved)\n- [ ] manifests + version 2.0.0 + LSP-first descriptions updated\n- [ ] flow npm name + schema $id updated\n- [ ] settings key + CI paths updated; commands/ deleted\n- [ ] grep old slugs returns only CHANGELOG 1.0.x\n\n**Verify:** grep -rin --exclude-dir=node_modules --exclude-dir=.git -e dataverse-customization-xml -e power-automate-cloud-flow . → only CHANGELOG 1.0.x; then pwsh -c \"Invoke-Pester plugins\" → pass.\n\n```json:metadata\n{\"files\": [\".claude-plugin/marketplace.json\", \"plugins/dataverse-xml-lsp/.claude-plugin/plugin.json\", \"plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json\", \"plugins/cloud-flow-json-lsp/package.json\", \"plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json\", \".claude/settings.json\", \".github/workflows/ci.yml\", \"README.md\", \"llms.txt\"], \"verifyCommand\": \"grep -rin --exclude-dir=node_modules --exclude-dir=.git -e dataverse-customization-xml -e power-automate-cloud-flow . | grep -v CHANGELOG.md; pwsh -c \\\"Invoke-Pester plugins\\\"\", \"acceptanceCriteria\": [\"dirs moved via git mv\", \"manifests + version 2.0.0 + descriptions updated\", \"flow npm name + schema $id updated\", \"settings key + CI paths updated, commands/ deleted\", \"grep old slugs returns only CHANGELOG 1.0.x\"], \"modelTier\": \"standard\"}\n```" + }, + { + "id": 8, + "subject": "Task 3: No-restamp schema resolution", + "status": "completed", + "blockedBy": [6, 7], + "description": "**Goal:** Ship the mechanism chosen in Task 1 so the committed .lsp.json resolves its schema on every machine with no stamping, and remove the stamping step from setup.\n\n**Files:** Option 1 (shim): create scripts/lsp-launch.mjs in both plugins; modify both .lsp.json (launch via shim with ${CLAUDE_PLUGIN_ROOT} arg; remove stamped url); modify both Install-Plugin.ps1 (drop Set-LspSchemaPaths CC call); retire Set-LspSchemaPaths CC path. Option 2 (fallback): hosted https schema URL + handledSchemaProtocols.\n\n**Acceptance Criteria:**\n- [ ] .lsp.json has no machine-local absolute path\n- [ ] schema loads from a moved copy with no re-stamp\n- [ ] Install-Plugin.ps1 no longer edits .lsp.json\n\n**Verify:** from a temp copy, node scripts/lsp-launch.mjs --stdio driven by the smoke client flags the bad fixture, no stamping run.\n\n```json:metadata\n{\"files\": [\"plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs\", \"plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs\", \"plugins/dataverse-xml-lsp/.lsp.json\", \"plugins/cloud-flow-json-lsp/.lsp.json\", \"plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1\", \"plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1\"], \"verifyCommand\": \"node plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs --stdio (driven by smoke client from a moved copy)\", \"acceptanceCriteria\": [\".lsp.json has no machine-local absolute path\", \"schema loads from a moved copy with no re-stamp\", \"Install-Plugin.ps1 no longer edits .lsp.json\"], \"modelTier\": \"standard\"}\n```" + }, + { + "id": 9, + "subject": "Task 4: Move guidance to docs/guide.md", + "status": "completed", + "blockedBy": [7], + "description": "**Goal:** Preserve the domain guidance the auto-triggering skill carried by moving it into a reference doc, before the skill is shrunk.\n\n**Files:** create plugins/*/docs/guide.md; modify each docs/codex.md (repoint to guide.md) and README.md (pointer).\n\n**Acceptance Criteria:**\n- [ ] XML guide.md has validate loop, ribbon recipe, XSD-caveats list, gotchas, schema-refresh\n- [ ] flow guide.md has shape note, attach globs, Test-Json one-liner, gotchas\n- [ ] each guide.md opens as the manual replacement note; codex.md + README point at it\n\n**Verify:** grep -l \"manual replacement for the old\" plugins/*/docs/guide.md lists both; grep -r \"guide.md\" plugins/*/README.md plugins/*/docs/codex.md shows pointers.\n\n```json:metadata\n{\"files\": [\"plugins/dataverse-xml-lsp/docs/guide.md\", \"plugins/cloud-flow-json-lsp/docs/guide.md\", \"plugins/dataverse-xml-lsp/docs/codex.md\", \"plugins/cloud-flow-json-lsp/docs/codex.md\", \"plugins/dataverse-xml-lsp/README.md\", \"plugins/cloud-flow-json-lsp/README.md\"], \"verifyCommand\": \"grep -l \\\"manual replacement for the old\\\" plugins/*/docs/guide.md; grep -r \\\"guide.md\\\" plugins/*/README.md plugins/*/docs/codex.md\", \"acceptanceCriteria\": [\"XML guide.md has the full moved guidance\", \"flow guide.md has the moved guidance\", \"guide preamble present; codex.md + README.md point at guide.md\"], \"modelTier\": \"mechanical\"}\n```" + }, + { + "id": 10, + "subject": "Task 5: Shrink SKILL.md to tiny setup skill", + "status": "completed", + "blockedBy": [9], + "description": "**Goal:** Replace each auto-triggering skill body with a tiny, no-description skill whose only job is to run setup.\n\n**Files:** overwrite plugins/dataverse-xml-lsp/SKILL.md and plugins/cloud-flow-json-lsp/SKILL.md.\n\n**Acceptance Criteria:**\n- [ ] frontmatter has name: -setup and NO description:\n- [ ] body ~4 lines: what it is + install cmd (mention -UpdateVSCode) + reload/debugging + guide pointer\n- [ ] reload lists the skill without a description, invocable as /:-setup\n\n**Verify:** pwsh -c \"Invoke-Pester plugins\" (Task 6 guard passes); manual /reload-plugins then /dataverse-xml-lsp:dataverse-xml-lsp-setup runs setup.\n\n```json:metadata\n{\"files\": [\"plugins/dataverse-xml-lsp/SKILL.md\", \"plugins/cloud-flow-json-lsp/SKILL.md\"], \"verifyCommand\": \"pwsh -c \\\"Invoke-Pester plugins\\\"\", \"acceptanceCriteria\": [\"frontmatter name is -setup with NO description\", \"body ~4 lines: what it is + install cmd + reload/debugging + guide pointer\", \"reload lists skill without description, invocable as /:-setup\"], \"modelTier\": \"mechanical\"}\n```" + }, + { + "id": 11, + "subject": "Task 6: Tests — raise to well-tested", + "status": "completed", + "blockedBy": [8, 10], + "description": "**Goal:** Lock the new behaviors with tests: no-description skill, setup idempotency, XML LSP smoke, shim resolution.\n\n**Files:** create plugins/*/tests/SetupSkill.Tests.ps1 and plugins/dataverse-xml-lsp/scripts/lsp-smoke-xml.mjs + test; keep Validate-DataverseXml.Tests.ps1; extend LspConfig.Tests.ps1 for shim shape if needed.\n\n**Acceptance Criteria:**\n- [ ] no-description skill guard passes both plugins\n- [ ] idempotency test (Install-Plugin twice) passes (skip-guard when node/network unavailable)\n- [ ] XML lemminx LSP smoke flags bad fixture, passes good\n- [ ] validator test still passes; LSP not associated to wrapper roots (, )\n\n**Verify:** pwsh -c \"Invoke-Pester plugins\" → all green.\n\n```json:metadata\n{\"files\": [\"plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1\", \"plugins/cloud-flow-json-lsp/tests/SetupSkill.Tests.ps1\", \"plugins/dataverse-xml-lsp/scripts/lsp-smoke-xml.mjs\"], \"verifyCommand\": \"pwsh -c \\\"Invoke-Pester plugins\\\"\", \"acceptanceCriteria\": [\"no-description skill guard passes both plugins\", \"idempotency test passes\", \"XML lemminx LSP smoke flags bad fixture, passes good\", \"validator test still passes; LSP not associated to wrapper roots\"], \"modelTier\": \"standard\"}\n```" + }, + { + "id": 12, + "subject": "Task 7: CHANGELOG + final verification (gate)", + "status": "completed", + "blockedBy": [11], + "description": "**Goal:** Document the breaking 2.0.0 change and prove the whole restructure works end-to-end.\n\n**USER-ORDERED GATE — NON-SKIPPABLE.** Requested by the user (north-star: \"really good and well tested\"). Close only after every acceptanceCriteria item is re-validated independently, with output captured.\n\n**Files:** modify CHANGELOG.md (new top entry).\n\n**Acceptance Criteria:**\n- [ ] CHANGELOG 2.0.0 breaking entry: renames, LSP + no-description setup skill, guidance → docs/guide.md, setup command removed, re-stamp eliminated; reinstall + enabledPlugins migration note; no-auto-guidance consequence\n- [ ] both Install-Plugin self-checks pass\n- [ ] Invoke-Pester + ScriptAnalyzer green\n- [ ] reload loads both, LSPs attach with no stamped path, setup skills listed without description + invocable\n\n**Verify:** pwsh plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1; pwsh plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1; pwsh -c \"Invoke-Pester plugins\"; pwsh -c \"Invoke-ScriptAnalyzer -Path plugins -Recurse\".\n\n```json:metadata\n{\"files\": [\"CHANGELOG.md\"], \"verifyCommand\": \"pwsh plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1; pwsh plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1; pwsh -c \\\"Invoke-Pester plugins\\\"; pwsh -c \\\"Invoke-ScriptAnalyzer -Path plugins -Recurse\\\"\", \"acceptanceCriteria\": [\"CHANGELOG 2.0.0 breaking entry with migration + consequence note\", \"both Install-Plugin self-checks pass\", \"Invoke-Pester + ScriptAnalyzer green\", \"reload loads both, LSPs attach with no stamped path, setup skills listed without description + invocable\"], \"userGate\": true, \"tags\": [\"user-gate\"], \"gateScope\": \"all\", \"modelTier\": \"standard\"}\n```" + } + ], + "lastUpdated": "2026-07-21T13:15:00+12:00" +} diff --git a/docs/specs/2026-07-15-dataverse-agent-plugins-repo-design.md b/docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md similarity index 97% rename from docs/specs/2026-07-15-dataverse-agent-plugins-repo-design.md rename to docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md index 8094562..2cbc522 100644 --- a/docs/specs/2026-07-15-dataverse-agent-plugins-repo-design.md +++ b/docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md @@ -1,5 +1,7 @@ # dataverse-agent-plugins — public repo design +> **Historical — superseded by [`2026-07-21-lsp-only-plugin-rename-design.md`](2026-07-21-lsp-only-plugin-rename-design.md) (v2.0.0).** Kept as a dated record. The plugin names (`dataverse-customization-xml`, `power-automate-cloud-flow`) and the `/:setup` command described here changed in the 2.0.0 LSP restructure. + **Date:** 2026-07-15 **Status:** Approved design, pre-implementation **Author:** Connor Parsons diff --git a/docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md b/docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md new file mode 100644 index 0000000..1156d64 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md @@ -0,0 +1,262 @@ +# LSP plugins: rename + tiny setup skill + no-restamp (XML keeps its validator) — design + +Date: 2026-07-21 + +## Context + +The `dataverse-agent-plugins` marketplace ships two plugins: +`dataverse-customization-xml` (a lemminx XML LSP for Dataverse customization XML, plus a standalone +`Validate-DataverseXml.ps1`) and `power-automate-cloud-flow` (a `vscode-json-language-server` JSON +LSP for unpacked cloud-flow clientdata). Each currently carries a large **auto-triggering** root +skill — a `SKILL.md` whose long `description` is full of trigger phrases so it activates on its own — +plus a `/:setup` slash command that stamps a machine-local absolute schema path into +`.lsp.json` (needed because `${CLAUDE_PLUGIN_ROOT}` is not substituted inside `.lsp.json` `settings`). +That stamp must be re-run after every `/plugin update` or repo move. + +The goal is that both plugins are **really good, well-tested, useful LSPs that are easy to set up and +update.** Concretely: + +- Each plugin's shape is **an LSP server + one tiny, no-description setup skill.** The old + auto-triggering skill and the setup slash command are removed; the domain guidance the skill + carried is preserved as reference docs. +- **Eliminate the re-stamp step** so updates are effectively zero-touch. +- **Raise test coverage** so the LSP path is provably correct. +- Rename the plugins to names that say plainly what they are. + +**The XML plugin keeps its existing `Validate-DataverseXml.ps1`** — the LSP structurally cannot do +what it does (validate in headless/subagent/CI contexts the LSP never reaches, and validate the pac +**wrapper files** — ``, `` incl. escaped inner XML — by extracting the inner +fragment). It is kept scoped as the **non-LSP channel** with **no auto-hook**: the LSP owns +interactive validation, the validator owns CI/headless + wrapper files, so they never overlap or +disagree (both read the same schemas). A bespoke **flow** validator and any validation **hook** are +deliberately deferred (YAGNI) — see §10. + +Outcome: two lean, well-tested LSP plugins, set up once by a manually-run skill, with no per-machine +path stamping to repeat on update, and no loss of the XML validator's unique capabilities. + +## Decisions (locked with the user) + +- **Names:** `dataverse-customization-xml` → `dataverse-xml-lsp`; + `power-automate-cloud-flow` → `cloud-flow-json-lsp`. Asymmetry intentional (broad XSD-set LSP vs. + single cloud-flow schema); a forced-symmetric `dataverse-json-lsp` was rejected as overpromising. +- **Scope = the bundled schemas.** Full scope retained — XML keeps the whole official XSD set (~10 + root types); JSON keeps its single self-contained cloud-flow wrapper. +- **Shape = LSP + setup skill.** Plus: the XML plugin **keeps** `Validate-DataverseXml.ps1` and its + Pester test as the non-LSP channel. The flow plugin stays pure LSP (built-in `Test-Json` one-liner + documented for ad-hoc headless checks). No flow validator, no hook, in this change. +- **No overlap.** No auto-hook. The LSP is not associated to the wrapper roots the validator owns + (charts already excluded; ensure forms are validator-owned), so the LSP and validator never both + claim a file and cannot disagree. +- **Setup skill shape:** keep `skills: ["./"]`; the root `SKILL.md` becomes a tiny skill named + `-setup` (`dataverse-xml-lsp-setup` / `cloud-flow-json-lsp-setup`) with **no `description`** + — confirmed supported: it still loads and is invocable as `/:-setup`, and with no + description Claude has nothing to auto-match, so it never auto-triggers. +- **No re-stamp:** resolve the schema path at LSP launch instead of stamping it. Build **Option 1 + (launcher shim)** first and test it; if it doesn't work properly, abandon it and build **Option 2 + (hosted schema URL)** instead (§4). Exactly one mechanism ships. +- **Guidance:** moved into a new `docs/guide.md` per plugin (not README, not deleted). +- **Setup slash command:** removed; the tiny skill is the sole setup entry point. +- **Old design specs under `docs/superpowers/specs/`:** old names updated throughout. +- **Execution/versioning:** clean rename via `git mv` (preserve history), bump both to `2.0.0`, + CHANGELOG documents the breaking rename + restructure, work on a new branch off `main`. + +## 1. End state (per plugin) + +``` +plugins/dataverse-xml-lsp/ plugins/cloud-flow-json-lsp/ + .claude-plugin/plugin.json .claude-plugin/plugin.json + .lsp.json (launches via shim, §4) .lsp.json (launches via shim, §4) + SKILL.md ← tiny, NO description SKILL.md ← tiny, NO description + (name: dataverse-xml-lsp-setup) (name: cloud-flow-json-lsp-setup) + scripts/Validate-DataverseXml.ps1 ← KEPT scripts/ (lsp-smoke.mjs, install) + scripts/ schemas/ tests/ schemas/ tests/ package*.json + docs/ + guide.md ← NEW (moved guidance) + codex.md vscode.md debugging.md + REMOVED: commands/setup.md REMOVED: commands/setup.md + RETIRED: scripts/Set-LspSchemaPaths.ps1 (Claude Code path; see §4) +``` + +`skills: ["./"]` stays. The XML plugin keeps `tests/Validate-DataverseXml.Tests.ps1`. + +## 2. Rename mapping + +| Thing | Old → New | +|---|---| +| dir + `plugin.json` name + marketplace `name`/`source` | `dataverse-customization-xml` → **`dataverse-xml-lsp`** | +| dir + `plugin.json` name + marketplace `name`/`source` | `power-automate-cloud-flow` → **`cloud-flow-json-lsp`** | +| `SKILL.md` `name:` (differs from plugin name — has `-setup`) | → **`dataverse-xml-lsp-setup`** / **`cloud-flow-json-lsp-setup`** | +| flow `package.json` / `package-lock.json` name | `power-automate-cloud-flow-lsp` → **`cloud-flow-json-lsp`** | +| flow schema `$id` URL (`schemas/cloud-flow-clientdata.schema.json`) | `.../power-automate-cloud-flow/...` → `.../cloud-flow-json-lsp/...` | +| `.claude/settings.json` `enabledPlugins` key | `dataverse-customization-xml@dataverse-agent-plugins` → `dataverse-xml-lsp@dataverse-agent-plugins` | + +Reference rewrites (from the full pre-work inventory): `.claude-plugin/marketplace.json` (`name` + +`source` + LSP-first `description`), each `plugin.json` `description`, `.github/workflows/ci.yml` +(all `plugins//…` paths + cache keys), root `README.md`, `llms.txt`, `CHANGELOG.md` (new top +entry only), both `docs/superpowers/specs/*.md` (old names updated throughout), and each plugin's `README.md` + +`docs/{codex,vscode,debugging}.md`. `.lsp.json` config keys (`"xml"`/`"json"`) and test fixtures are +untouched by the rename. + +## 3. The tiny setup skill (`SKILL.md`) + +Frontmatter carries `name:` (`-setup`) only — **no `description:`**. Body is ~4 lines and +folds in what `commands/setup.md` did, minus path stamping (§4 removes it): + +- State the plugin is an LSP server (XML: plus the `Validate-DataverseXml.ps1` CLI for CI/headless). +- One-time per machine: `pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` (append + `-UpdateVSCode` to also wire the VS Code editor path); ask about `-UpdateVSCode` before running. +- On success remind to run `/reload-plugins`; on failure point at `docs/debugging.md`. +- Editing guidance lives in `docs/guide.md`. + +`Install-Plugin.ps1` keeps the **heavy per-machine install** (flow: `npm ci`; XML: fetch XSDs + +lemminx binary) and the **end-to-end self-check** (XML's stays validator-based since the validator is +kept; flow's stays the `lsp-smoke.mjs` LSP drive), and stays idempotent. Its **path-stamping step is +removed** once §4 lands — the script no longer edits `.lsp.json`, so it need not run again after an +update. + +## 4. No-restamp: schema path resolved at launch (Option 1 primary, Option 2 fallback) + +**Problem.** `${CLAUDE_PLUGIN_ROOT}` is substituted in `.lsp.json` `command`/`args`/`env`/ +`workspaceFolder` but **not** in `settings`/`initializationOptions`, where the schema `url` lives, so +today the absolute path is stamped per machine and re-stamped after each update/move. Relative paths +there resolve against the workspace (not the plugin), and the pac-generated documents can't carry +`$schema`/`schemaLocation`. + +**Option 1 — launcher shim (build and test first).** Launch the server via a small wrapper +referenced in `command`/`args` (where `${CLAUDE_PLUGIN_ROOT}` *does* substitute). The shim receives +the plugin root as an argument, spawns the real language server, and injects the absolute schema +association at runtime — for the JSON server by supplying the `json.schemas` config it pulls via +`workspace/configuration`; for lemminx by supplying `xml.fileAssociations` `systemId`. The committed +`.lsp.json` is then fully portable: no per-machine state, no network, no re-stamp. Target design. + +**Option 2 — hosted schema URL (fallback, only if the shim fails testing).** The flow schema is +self-contained (0 external `$ref`s), so it can be referenced by a stable `https://` URL with +`handledSchemaProtocols: ["file","https"]`. For the XML plugin this requires hosting the XSD set +(its `xs:include`s resolve relative to the schema URL). Trade-off: first-use network fetch (cached) +and coupling to a live URL. + +**Strategy:** implement and test Option 1 on both servers. If it works, ship it; Option 2 is never +built. If it doesn't, **replace** it with Option 2. Exactly one mechanism ships. Either way, +per-machine stamping for the Claude Code LSP is eliminated and `Set-LspSchemaPaths.ps1` is retired +for that path. (The XML validator resolves its XSDs from the local `schemas/` dir independently of +this and is unaffected. VS Code's own association via `-UpdateVSCode` is a separate, optional editor +consumer, out of scope for the shim.) + +## 5. Guidance relocation → `docs/guide.md` + +The current rich `SKILL.md` bodies move (near-verbatim) into a new `docs/guide.md` per plugin — XML: +the two-layer (LSP live + validator) loop, the ribbon recipe, "rules the XSD doesn't catch", +gotchas, schema refresh; flow: the shape note, attach globs, the ad-hoc `Test-Json` one-liner for +headless checks, gotchas. `docs/codex.md`'s "follow `SKILL.md`" pointers are repointed to +`docs/guide.md` (XML `codex.md` keeps the validator as the story for non-Claude agents). Each plugin +`README.md` keeps a prominent pointer to `docs/guide.md`, and `docs/guide.md` opens by noting it is +the manual replacement for the old auto-loaded skill. + +## 6. Versioning, changelog, branch + +- Both `plugin.json` versions → **2.0.0**. +- New top CHANGELOG entry: plugins renamed (old → new); restructured to LSP + a single manually-run + no-description setup skill; guidance moved to `docs/guide.md`; setup slash command removed; + re-stamp eliminated. (XML validator retained — no behavior loss there.) Called out as **breaking** + — reinstall under the new plugin id and update the `enabledPlugins` key — and states the §9 + consequence so the change isn't silent. +- All work on a new branch off `main` (e.g. `refactor/lsp-only-plugin-rename`). + +## 7. Testing (raise to "well-tested") + +On top of the existing Pester suites (which cover `.lsp.json`↔script parity and schema good/bad +fixtures, and resolve paths via `$PSScriptRoot` so the rename doesn't break them): + +- **Setup skill guard** — each root `SKILL.md` parses, has `name: -setup`, and has **no + `description`** (locks the no-auto-trigger behavior). +- **Idempotency** — `Install-Plugin.ps1` run twice is clean and the self-check still passes. +- **Launcher shim** (if Option 1 ships) — drive the shim and confirm the server loads the schema and + flags a bad fixture from a working dir that is NOT the plugin root (proving no stamping needed). +- **XML LSP smoke** — add a lemminx end-to-end smoke (parity with the flow `lsp-smoke.mjs`) so the + actual XML LSP diagnostic path is covered, not only the validator. +- **Keep** `Validate-DataverseXml.Tests.ps1` (validator retained). Confirm the LSP is not associated + to the wrapper roots the validator owns (no LSP/validator disagreement on ``/``). + +## 8. Verification (end-to-end) + +1. `pwsh …/Install-Plugin.ps1` for both — heavy install + self-check pass. +2. `Invoke-Pester plugins` and `Invoke-ScriptAnalyzer -Path plugins -Recurse` — green (incl. §7). +3. CI: updated `.github/workflows/ci.yml` paths resolve and the workflow runs. +4. `/reload-plugins`: both plugins load with **no load error**; the LSP servers attach via the shim + with no stamped path; the tiny setup skill lists **without a description** and is manually + invocable; invoking it installs cleanly. +5. `grep -ri` for both old slugs (excluding `node_modules`/`.git`) returns only the intentional + `CHANGELOG.md` 1.0.x historical mentions. + +## 9. Consequences (accepted trade-off) + +- **No auto-guidance.** With the fat auto-triggering skill gone, editing a matching file no longer + pulls the recipe or gotchas into context; what remains automatic is the LSP's **live diagnostics** + (main session only) and — for XML — whatever the user runs via `Validate-DataverseXml.ps1`. + Guidance is opt-in via `docs/guide.md`, kept discoverable through each README's pointer, and the + CHANGELOG notes the change so it isn't silent. +- **Validation capability is otherwise unchanged:** the XML validator still covers headless/CI and + the pac wrapper files; the flow plugin still has the built-in `Test-Json` one-liner for ad-hoc + headless checks. + +## 10. Deferred (candidate follow-ups, not in this change) + +Parked for a later, additive change once the LSP core is solid: + +- A bespoke **flow** validator CLI (a thin `Test-Json` wrapper: globs, exit codes, readable errors) + for symmetry and CI ergonomics. +- A `PostToolUse` **hook** that auto-runs a validator on matching edits — pending an empirical check + that such hooks fire in subagents/headless, and mindful of main-session overlap with the LSP + (which is exactly why it's deferred, not adopted, here). + +## Spike result (2026-07-21) + +**Decision: GO — Option 1 (launcher shim) for BOTH servers.** No hosted-URL fallback needed. + +A throwaway node shim was prototyped for each server. It takes the plugin root as an argv +(the value Claude Code substitutes into `.lsp.json` `command`/`args` from `${CLAUDE_PLUGIN_ROOT}`), +spawns the real server, proxies LSP stdio, and injects the schema association at runtime — computing +the absolute `file://` schema path from the passed root. Nothing is stamped into any committed file. + +The injection is applied in three places (the third turned out to be essential): the forwarded +`initialize` (`initializationOptions.settings`), any client `workspace/didChangeConfiguration` +(re-inserting the schema so an empty client push can't clear it), and by directly answering the +server's `workspace/configuration` pull. It was driven by a stand-in client (based on +`scripts/lsp-smoke.mjs`) launched from a **non-plugin cwd**; the client itself supplied **no** schema +and answered any config pull with `{}`, so a firing schema can only have come from the shim. + +### JSON (vscode-json-language-server) — PASS + +``` +[json] driven from cwd=…\scratchpad + PASS valid Workflows/case-valid-simple-flow.json -> 0 diagnostic(s) + PASS invalid Workflows/case-invalid-missing-definition.json -> 1 diagnostic(s) | first: Missing property "definition". + PASS invalid Workflows/case-invalid-bad-runafter-status.json -> 1 diagnostic(s) | first: Value is not accepted. Valid values: "Succeeded", "Failed", "Skipped", "TimedOut". + client-side config pull seen (should be false): false + +json shim: all cases PASSED +``` + +### XML (lemminx native binary `lemminx-win32.exe`) — PASS + +``` +[xml] driven from cwd=…\scratchpad + PASS valid v/RibbonDiff.xml -> 0 diagnostic(s) + PASS invalid i/RibbonDiff.xml -> 1 diagnostic(s) | first: Element name 'Actions' is invalid. + One of the following is expected: + - EnableRules + Error indicated by: + {the schema} + client-side config pull seen (should be false): false + +xml shim: all cases PASSED +``` + +The XML error (`{the schema}` / "expected: EnableRules") is XSD-derived, proving `RibbonCore.xsd` +attached via the shim-injected `xml.fileAssociations` (absolute `systemId` computed at runtime). +Both results were stable across repeated runs. lemminx is a self-contained native image here — no +separate Java runtime was needed. + +**Consequence for the design:** the committed `.lsp.json` for both plugins can carry portable, +relative schema references (or none) with no machine-local path; the shim removes the post-update +re-stamping step entirely. The setup skill's stamping responsibility can therefore be dropped. diff --git a/docs/superpowers/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md b/docs/superpowers/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md new file mode 100644 index 0000000..1e5968a --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md @@ -0,0 +1,143 @@ +# power-automate-cloud-flow — plugin design (retrospective) + +> **Historical — superseded by [`2026-07-21-lsp-only-plugin-rename-design.md`](2026-07-21-lsp-only-plugin-rename-design.md) (v2.0.0).** Kept as a dated record. This plugin was renamed to `cloud-flow-json-lsp` and its `/…:setup` command removed in the 2.0.0 LSP restructure. + +**Date:** 2026-07-21 +**Status:** Retrospective spec — documents shipped v1.0.0 (commit `fff8ff0`). Written after the fact to +capture intended scope and to audit the build for sprawl. +**Author:** Connor Parsons + +## Summary + +`power-automate-cloud-flow` is the second plugin in the `dataverse-agent-plugins` marketplace. It +provides **live JSON-schema validation of unpacked Power Automate solution cloud-flow files** — the +`Workflows/-.json` that `pac solution unpack` writes. That file is the flow's +*clientdata*: a Workflow Definition Language (WDL) `definition` plus its `connectionReferences`. +Malformed structure surfaces as editor diagnostics **while you type**, instead of failing later at +`pac solution import`. + +It is deliberately **one layer of a two-layer story**: + +- **Shape (this plugin):** JSON well-formedness and the clientdata/WDL wrapper structure — + `properties.definition` present; `definition` has `$schema`/`triggers`/`actions`; `runAfter` + statuses are the WDL enum. Expressible in JSON Schema, enforced live via an LSP. +- **Semantics (the `power-automate-flow-dev` skill):** cross-node rules JSON Schema *cannot* express + (`runAfter` naming a real sibling, `connectionName` resolving, hard-coded env values) plus the + export → pack → import → verify round-trip. Enforced by that skill's `flow-lint.ps1`. + +The scope boundary between these two layers is the single most important design decision, and it is +what keeps the plugin small. This plugin never tries to do semantics. + +## Decisions (reconstructed from the build) + +| Question | Decision | +|---|---| +| What is validated | JSON **shape** of unpacked cloud-flow clientdata only — not connector semantics, not the round-trip | +| Validation mechanism (live) | `vscode-json-language-server` (from `vscode-langservers-extracted`, pinned `4.10.0`) via `.lsp.json` | +| Validation mechanism (headless/CI) | PowerShell built-in `Test-Json -SchemaFile ` against the **same** bundled schema — no bespoke CLI validator | +| Schema authorship | Hand-maintained draft-07 wrapper, **committed** to the repo (original work; contrast the Dataverse XSDs which are Microsoft-copyrighted and downloaded) | +| Action `inputs` strictness | **Left untyped on purpose** — `OpenApiConnection` connector actions aren't in the public Logic Apps schema, so a strict `$ref` would drown real errors in false positives | +| File association | `**/Workflows/*.json`, `**/Workflows/**/*.json`, `**/*.flow.json` only — deliberately **not** every `*.json` | +| Language server distribution | Installed at setup via `npm ci` (pinned lockfile), **not committed** — `node_modules/` is gitignored | +| Setup shape | One idempotent entry script (`Install-Plugin.ps1`) → install server → stamp path → end-to-end self-check | +| Why a path-stamping step exists | `${CLAUDE_PLUGIN_ROOT}` is substituted in `.lsp.json` `command`/`args` but **not** in nested `settings`, so the schema `url` must be stamped to a machine-absolute `file://` URI | +| Authoritative gate | `pac solution check` / a successful `pac solution import` — the plugin exists so you rarely reach that with a malformed file | +| Platform | PowerShell 7+ (`pwsh`) cross-platform; Node.js for the server | + +## Architecture + +### Components + +| Unit | Single purpose | +|------|----------------| +| `schemas/cloud-flow-clientdata.schema.json` | The draft-07 wrapper schema — the one source of validation truth, loaded by both the LSP and `Test-Json` | +| `.lsp.json` | Registers the JSON server with Claude Code and carries the `json.schemas` file-match association | +| `scripts/Install-Plugin.ps1` | One-shot orchestrator: install → stamp → self-check (3 steps) | +| `scripts/Install-JsonLanguageServer.ps1` | `npm ci` the pinned server into `node_modules/`; idempotent, `-Force` to reinstall | +| `scripts/Set-LspSchemaPaths.ps1` | Stamp the machine-absolute `file://` schema URI into `.lsp.json` (and optionally VS Code settings) | +| `scripts/lsp-smoke.mjs` | End-to-end health check: drives the real server over both push and pull config-delivery models and asserts valid fixtures are clean / invalid fixtures flag | +| `commands/setup.md` | The `/power-automate-cloud-flow:setup` slash command — thin wrapper over `Install-Plugin.ps1` | +| `tests/LspConfig.Tests.ps1` | Pester guards: config parity (`.lsp.json` ↔ the script's source-of-truth globs, read via AST) + fixtures distinguish valid/invalid | +| `tests/fixtures/{valid,invalid}/` | Regression corpus for the schema | +| `SKILL.md` / `README.md` / `schemas/SOURCE.md` / `docs/*` | Skill entry point, human README, schema rationale, and consumer/debugging guides | + +### Data flow + +**Setup (once per machine, re-run after every update):** + +``` +/power-automate-cloud-flow:setup + → Install-Plugin.ps1 + 1. Install-JsonLanguageServer.ps1 (npm ci → node_modules/) + 2. Set-LspSchemaPaths.ps1 (stamp absolute file:// URI into .lsp.json) + 3. lsp-smoke.mjs (drive real server, assert schema fires; fail loud here) + → /reload-plugins +``` + +**Validation (main interactive session):** edit `Workflows/*.json` → LSP pushes diagnostics live +against the stamped schema. + +**Validation (subagent / headless / CI):** no LSP push by design → +`Get-Content -Raw | Test-Json -SchemaFile ` against the same schema. + +### Why the setup is a self-checking three-step, not a one-liner + +The failure mode being defended against is a **running server pointing at a broken relative schema +path** — silent non-validation. The install/stamp/self-check sequence makes a broken install fail at +setup time (loud), consistent with the repo owner's "fail loud, don't mask" principle. The smoke test +running *both* push and pull config-delivery models hedges against Claude Code's under-documented +`workspace/configuration` behavior. + +## Sprawl & quality assessment + +The concern that prompted this spec was that the plugin might have been built without a design and +grown sprawl. Honest verdict: **the code is tight; the prose is redundant.** + +### What is well-scoped (no action needed) + +- **Single source of validation truth.** One schema serves both the live LSP and the headless + `Test-Json` path. There is no second, drifting validator. This is the strongest design choice. +- **The shape/semantics boundary is explicit and consistently honored.** Nothing in this plugin + attempts cross-node semantics; it points at `power-automate-flow-dev` for that. Scope creep is + actively resisted. +- **Every script has one job**, and the entry script composes them. File count (~20) is proportionate + to the job; no file is doing two unrelated things. +- **Tests guard the two things that actually rot:** config parity between `.lsp.json` and the stamping + script's source-of-truth globs (via AST read, not execution — a genuinely careful touch), and that + the schema still separates the valid/invalid corpus. +- **`node_modules/` is installed, not shipped;** the schema is committed. Correct call on both. + +### Sprawl found: documentation redundancy (the real finding) + +Three facts are each restated in four-to-six places: + +| Repeated fact | Appears in | +|---|---| +| Shape-vs-semantics split / "use `flow-lint.ps1` for semantics" | `SKILL.md`, `README.md`, `SOURCE.md`, `docs/debugging.md`, `docs/codex.md`, `docs/vscode.md` | +| `${CLAUDE_PLUGIN_ROOT}` not substituted in `settings` → why stamping exists | `SKILL.md`, `README.md`, `Set-LspSchemaPaths.ps1` header, `docs/debugging.md` | +| "`inputs` left untyped on purpose (false positives)" | `SKILL.md`, `README.md`, `SOURCE.md`, `docs/debugging.md` | + +Some repetition is legitimate — a debugging doc *should* restate a gotcha as a troubleshooting step, +and `SKILL.md` (agent-facing) vs `README.md` (human-facing) serve different readers. But the current +spread means a change to any of these three facts must be hand-propagated to 4–6 files, and drift is +likely. This is maintenance sprawl, not feature sprawl. + +**Recommended (optional) cleanup:** +- Make `SKILL.md` the single canonical statement of the shape/semantics boundary and the two design + caveats; have `README.md` and the `docs/*` files link to it rather than re-explain. +- Keep the troubleshooting *symptoms* in `docs/debugging.md` but point to the canonical *why*. + +### Minor observations (not sprawl, noted for completeness) + +- `.lsp.json` carries the `json` settings block **twice** (`initializationOptions.settings` and + `settings`) to cover both config-delivery models. This is intentional and guarded by a parity test, + but it is duplicated state — the smoke test's two-scenario design is what justifies keeping both. +- The stamping step dirties a **tracked** file (`.lsp.json`) with a machine path. This is documented + (`docs/debugging.md` notes `git update-index --skip-worktree`), but it remains an inherent friction + of the "substitute in `command`/`args` but not `settings`" limitation, not a defect in the plugin. + +### Bottom line + +No decomposition needed and no feature to cut. The plugin does one thing and its scope boundary is +sound. The only worthwhile follow-up is consolidating the repeated prose so the three load-bearing +facts live in one place each. diff --git a/llms.txt b/llms.txt index 885cdb2..e1ec6c4 100644 --- a/llms.txt +++ b/llms.txt @@ -1,7 +1,7 @@ # dataverse-agent-plugins > Claude Code plugin marketplace for Microsoft Dataverse development. Its founding plugin, -> dataverse-customization-xml, validates hand-edited Dataverse customization XML (ribbon, +> dataverse-xml-lsp, validates hand-edited Dataverse customization XML (ribbon, > sitemap, forms, FetchXML, charts, ISV config) against the official Microsoft XSDs via a > standalone PowerShell 7 validator and lemminx LSP diagnostics. XSDs and the lemminx binary > are fetched at setup, not committed. Install in Claude Code: @@ -9,26 +9,26 @@ ## Docs -### dataverse-customization-xml +### dataverse-xml-lsp - [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 -- [Codex guide](plugins/dataverse-customization-xml/docs/codex.md): using the validator loop from non-Claude agents -- [VS Code guide](plugins/dataverse-customization-xml/docs/vscode.md): editor-only setup, CI usage -- [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 +- [Plugin README](plugins/dataverse-xml-lsp/README.md): full setup for every consumer type +- [SKILL.md](plugins/dataverse-xml-lsp/SKILL.md): the agent skill — validation workflow, root-element→schema table, ribbon-button recipe, gotchas +- [Codex guide](plugins/dataverse-xml-lsp/docs/codex.md): using the validator loop from non-Claude agents +- [VS Code guide](plugins/dataverse-xml-lsp/docs/vscode.md): editor-only setup, CI usage +- [Debugging](plugins/dataverse-xml-lsp/docs/debugging.md): setup failures, missing diagnostics, false positives +- [Schema source](plugins/dataverse-xml-lsp/schemas/SOURCE.md): where the Microsoft XSDs come from, refresh procedure + +### cloud-flow-json-lsp + +- [Plugin README](plugins/cloud-flow-json-lsp/README.md): install + setup for every consumer type +- [SKILL.md](plugins/cloud-flow-json-lsp/SKILL.md): the shape-vs-semantics split, what attaches, gotchas +- [Codex guide](plugins/cloud-flow-json-lsp/docs/codex.md): the `Test-Json` validation loop from non-Claude agents +- [VS Code guide](plugins/cloud-flow-json-lsp/docs/vscode.md): editor-only setup, CI usage +- [Debugging](plugins/cloud-flow-json-lsp/docs/debugging.md): setup failures, missing diagnostics +- [Schema source](plugins/cloud-flow-json-lsp/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 +- [Design spec](docs/superpowers/specs/2026-07-15-dataverse-agent-plugins-repo-design.md): why the repo is shaped this way - [Changelog](CHANGELOG.md) diff --git a/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json b/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json new file mode 100644 index 0000000..b88014a --- /dev/null +++ b/plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://anthropic.com/claude-code/plugin.schema.json", + "name": "cloud-flow-json-lsp", + "version": "2.0.0", + "description": "JSON LSP for unpacked Power Automate cloud-flow clientdata: live vscode-json-language-server diagnostics on solution Workflows/*.json against a bundled clientdata/WDL wrapper schema.", + "author": { + "name": "Connor Parsons" + }, + "homepage": "https://github.com/Cordedmink2/dataverse-agent-plugins", + "skills": [ + "./" + ] +} diff --git a/plugins/power-automate-cloud-flow/.gitignore b/plugins/cloud-flow-json-lsp/.gitignore similarity index 100% rename from plugins/power-automate-cloud-flow/.gitignore rename to plugins/cloud-flow-json-lsp/.gitignore diff --git a/plugins/cloud-flow-json-lsp/.lsp.json b/plugins/cloud-flow-json-lsp/.lsp.json new file mode 100644 index 0000000..cd7d7a4 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/.lsp.json @@ -0,0 +1,13 @@ +{ + "json": { + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/scripts/lsp-launch.mjs", + "${CLAUDE_PLUGIN_ROOT}", + "--stdio" + ], + "extensionToLanguage": { + ".json": "json" + } + } +} diff --git a/plugins/power-automate-cloud-flow/README.md b/plugins/cloud-flow-json-lsp/README.md similarity index 62% rename from plugins/power-automate-cloud-flow/README.md rename to plugins/cloud-flow-json-lsp/README.md index f4991b2..c37cea2 100644 --- a/plugins/power-automate-cloud-flow/README.md +++ b/plugins/cloud-flow-json-lsp/README.md @@ -1,13 +1,15 @@ -# power-automate-cloud-flow +# cloud-flow-json-lsp 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`. +**See [`docs/guide.md`](docs/guide.md) for editing guidance.** + 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. +`power-automate-flow-dev` skill. See [`docs/guide.md`](docs/guide.md) for the split. ## The validation layer @@ -23,14 +25,14 @@ the LSP loads. ``` /plugin marketplace add Cordedmink2/dataverse-agent-plugins -/plugin install power-automate-cloud-flow@dataverse-agent-plugins -/power-automate-cloud-flow:setup +/plugin install cloud-flow-json-lsp@dataverse-agent-plugins +/cloud-flow-json-lsp:cloud-flow-json-lsp-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. +The setup command installs the pinned JSON language server (`npm ci`) 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. The launcher +shim resolves the bundled schema at runtime, so nothing machine-specific is stamped into `.lsp.json`. ## Setup script directly (non-Claude consumers) @@ -42,20 +44,21 @@ 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. +Run this one script rather than the individual `Install-*` scripts — a partial manual setup can +leave `node_modules/` or the schema missing, so the launcher shim throws at startup instead of +validating silently against nothing. ## 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 | +| `SKILL.md` | Tiny no-description setup skill (runs `Install-Plugin.ps1`) | +| `scripts/Install-Plugin.ps1` | One-shot setup: server install + 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-launch.mjs` | Launcher shim — spawns the JSON server and injects the bundled schema association at runtime | +| `scripts/Set-LspSchemaPaths.ps1` | Write the `json.schemas` association into VS Code user settings (`-UpdateVSCode`; Claude Code uses the shim instead) | | `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 | +| `.lsp.json` | Launches the JSON server via the shim, which resolves the bundled schema at runtime — no stamped path | | `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 | @@ -69,12 +72,13 @@ hand-maintained and committed. ``` /plugin marketplace update dataverse-agent-plugins -/plugin update power-automate-cloud-flow@dataverse-agent-plugins -/power-automate-cloud-flow:setup +/plugin update cloud-flow-json-lsp@dataverse-agent-plugins +/cloud-flow-json-lsp:cloud-flow-json-lsp-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). +The launcher shim resolves the bundled schema at runtime, so `.lsp.json` is portable — you do +**not** need to re-run setup after a plugin update or a repo move. Re-run it only when an update +bumps the pinned server version and you need the new `node_modules/` installed. ## Other consumers @@ -91,12 +95,13 @@ Something not working? [`docs/debugging.md`](docs/debugging.md). 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. + `settings` — which is why the launcher shim (`scripts/lsp-launch.mjs`), handed the plugin root + as an argv, injects the schema association at runtime instead of `.lsp.json` carrying it. ## 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. +See [`docs/guide.md`](docs/guide.md) for the shape-vs-semantics layering and how this plugin pairs +with the `power-automate-flow-dev` skill. diff --git a/plugins/cloud-flow-json-lsp/SKILL.md b/plugins/cloud-flow-json-lsp/SKILL.md new file mode 100644 index 0000000..42cdb25 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/SKILL.md @@ -0,0 +1,9 @@ +--- +name: cloud-flow-json-lsp-setup +--- + +# cloud-flow-json-lsp setup + +This plugin is a JSON LSP for unpacked Power Automate cloud-flow clientdata (`Workflows/*.json`). Run once per machine: + +`pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` (add `-UpdateVSCode` to also wire VS Code). Then run `/reload-plugins`. If it fails, see `docs/debugging.md`. Editing guidance: `docs/guide.md`. diff --git a/plugins/power-automate-cloud-flow/docs/codex.md b/plugins/cloud-flow-json-lsp/docs/codex.md similarity index 78% rename from plugins/power-automate-cloud-flow/docs/codex.md rename to plugins/cloud-flow-json-lsp/docs/codex.md index 9cec50c..705325f 100644 --- a/plugins/power-automate-cloud-flow/docs/codex.md +++ b/plugins/cloud-flow-json-lsp/docs/codex.md @@ -13,14 +13,14 @@ explicitly after each edit. There is no bespoke validator script — PowerShell > 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` + > `/plugins/cloud-flow-json-lsp/docs/guide.md`, and after EVERY edit run: + > `Get-Content -Raw | Test-Json -SchemaFile /plugins/cloud-flow-json-lsp/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. +3. If your Codex setup supports skill folders, point it at + `plugins/cloud-flow-json-lsp/docs/guide.md` directly for the full editing guidance. ## The loop diff --git a/plugins/power-automate-cloud-flow/docs/debugging.md b/plugins/cloud-flow-json-lsp/docs/debugging.md similarity index 60% rename from plugins/power-automate-cloud-flow/docs/debugging.md rename to plugins/cloud-flow-json-lsp/docs/debugging.md index a2de879..3ec7fe6 100644 --- a/plugins/power-automate-cloud-flow/docs/debugging.md +++ b/plugins/cloud-flow-json-lsp/docs/debugging.md @@ -13,28 +13,21 @@ ## 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 +Diagnostics work straight from the committed `.lsp.json`: it launches the shim +(`scripts/lsp-launch.mjs`), which resolves the bundled schema's absolute `file://` URI at runtime +from the plugin root — no stamped path, so a plugin update or repo move does not break it. If +squiggles don't appear: + +1. Are Node and the pinned server installed? `.lsp.json` runs `node scripts/lsp-launch.mjs`; the + shim throws loudly if `node_modules/` or the schema are missing. Run + `pwsh scripts/Install-Plugin.ps1` to (re)install them. +2. Run `/reload-plugins` (or restart the session). +3. 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 +4. 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. +5. Subagents and headless runs NEVER get LSP pushes — that's by design; validate with `Test-Json`. ## No diagnostics in VS Code diff --git a/plugins/power-automate-cloud-flow/SKILL.md b/plugins/cloud-flow-json-lsp/docs/guide.md similarity index 70% rename from plugins/power-automate-cloud-flow/SKILL.md rename to plugins/cloud-flow-json-lsp/docs/guide.md index a1023e3..65ebc66 100644 --- a/plugins/power-automate-cloud-flow/SKILL.md +++ b/plugins/cloud-flow-json-lsp/docs/guide.md @@ -1,16 +1,8 @@ ---- -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) +# cloud-flow-json-lsp — editing guide + +> This is the manual replacement for the old auto-loaded skill; the plugin no longer surfaces this automatically. + +## 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`. @@ -32,13 +24,13 @@ is well-formed but will import Off / route to the wrong place." Run both before The JSON language server is a Node package fetched via npm; it is not committed. Run: ``` -/power-automate-cloud-flow:setup +/cloud-flow-json-lsp:cloud-flow-json-lsp-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`. +(`npm ci`) and runs an end-to-end self-check that drives the real server and confirms the schema +fires. The launcher shim (`scripts/lsp-launch.mjs`) resolves the bundled schema's absolute `file://` +URI at runtime, so nothing is stamped into `.lsp.json`. Then `/reload-plugins`. ## What attaches to what @@ -68,9 +60,10 @@ Get-Content .json -Raw | Test-Json -SchemaFile "${CLAUDE_PLUGIN_ROOT}/sche 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. + — only in `command`/`args`. That's why the launcher shim (`scripts/lsp-launch.mjs`), handed the + plugin root as an argv, injects the schema association at runtime, computing an absolute `file://` + URI. `.lsp.json` carries no path, so it stays portable — no re-stamp is needed after a plugin + update or a repo move. - **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/docs/vscode.md b/plugins/cloud-flow-json-lsp/docs/vscode.md similarity index 73% rename from plugins/power-automate-cloud-flow/docs/vscode.md rename to plugins/cloud-flow-json-lsp/docs/vscode.md index 392e382..a3cc789 100644 --- a/plugins/power-automate-cloud-flow/docs/vscode.md +++ b/plugins/cloud-flow-json-lsp/docs/vscode.md @@ -10,7 +10,7 @@ ships only Windows PowerShell 5.1. 2. Run: ``` - pwsh plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 -UpdateVSCode + pwsh plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 -UpdateVSCode ``` This installs the pinned JSON language server (used by the Claude Code path; VS Code uses its @@ -23,7 +23,7 @@ ships only Windows PowerShell 5.1. 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 +Get-Content .json -Raw | Test-Json -SchemaFile plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json ``` ## Settings-update notes @@ -33,5 +33,6 @@ Get-Content .json -Raw | Test-Json -SchemaFile plugins/power-automate-clou - 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`. + script anyway (it targets the stable settings path), then copy the `json.schemas` entry it wrote + into your editor's own `settings.json`. That entry comes from `Set-LspSchemaPaths.ps1`, not from + `.lsp.json` (which carries no schema entry — the Claude Code launcher shim injects it at runtime). diff --git a/plugins/power-automate-cloud-flow/package-lock.json b/plugins/cloud-flow-json-lsp/package-lock.json similarity index 99% rename from plugins/power-automate-cloud-flow/package-lock.json rename to plugins/cloud-flow-json-lsp/package-lock.json index 94325bb..ce51183 100644 --- a/plugins/power-automate-cloud-flow/package-lock.json +++ b/plugins/cloud-flow-json-lsp/package-lock.json @@ -1,11 +1,11 @@ { - "name": "power-automate-cloud-flow-lsp", + "name": "cloud-flow-json-lsp", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "power-automate-cloud-flow-lsp", + "name": "cloud-flow-json-lsp", "version": "1.0.0", "dependencies": { "vscode-langservers-extracted": "4.10.0" diff --git a/plugins/power-automate-cloud-flow/package.json b/plugins/cloud-flow-json-lsp/package.json similarity index 83% rename from plugins/power-automate-cloud-flow/package.json rename to plugins/cloud-flow-json-lsp/package.json index 63b72b1..87a6ca9 100644 --- a/plugins/power-automate-cloud-flow/package.json +++ b/plugins/cloud-flow-json-lsp/package.json @@ -1,5 +1,5 @@ { - "name": "power-automate-cloud-flow-lsp", + "name": "cloud-flow-json-lsp", "version": "1.0.0", "private": true, "description": "Pins the JSON language server used for live diagnostics on Power Automate cloud-flow JSON.", diff --git a/plugins/power-automate-cloud-flow/schemas/SOURCE.md b/plugins/cloud-flow-json-lsp/schemas/SOURCE.md similarity index 100% rename from plugins/power-automate-cloud-flow/schemas/SOURCE.md rename to plugins/cloud-flow-json-lsp/schemas/SOURCE.md diff --git a/plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json b/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json similarity index 98% rename from plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json rename to plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json index 71fb0bc..5e7cae3 100644 --- a/plugins/power-automate-cloud-flow/schemas/cloud-flow-clientdata.schema.json +++ b/plugins/cloud-flow-json-lsp/schemas/cloud-flow-clientdata.schema.json @@ -1,6 +1,6 @@ { "$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", + "$id": "https://github.com/Cordedmink2/dataverse-agent-plugins/cloud-flow-json-lsp/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", diff --git a/plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 b/plugins/cloud-flow-json-lsp/scripts/Install-JsonLanguageServer.ps1 similarity index 100% rename from plugins/power-automate-cloud-flow/scripts/Install-JsonLanguageServer.ps1 rename to plugins/cloud-flow-json-lsp/scripts/Install-JsonLanguageServer.ps1 diff --git a/plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 b/plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 similarity index 59% rename from plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 rename to plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 index 41e5734..f013a89 100644 --- a/plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 +++ b/plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 @@ -1,13 +1,14 @@ #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. + One-shot plugin setup: install the JSON language server and 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. + Idempotent - safe to re-run after /plugin update. Claude Code resolves the bundled schema at + launch via scripts/lsp-launch.mjs, so no machine-local path is ever stamped into .lsp.json. + The self-check drives that shim over stdio and confirms the schema fires (valid fixtures clean, + invalid fixtures flagged), so a broken install fails here, not at first real use. + Pass -UpdateVSCode to also wire the separate VS Code editor association. .EXAMPLE pwsh scripts/Install-Plugin.ps1 @@ -25,8 +26,9 @@ $ErrorActionPreference = 'Stop' 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 "== 2/3 VS Code association (optional) ==" -ForegroundColor Cyan +if ($UpdateVSCode) { & (Join-Path $PSScriptRoot 'Set-LspSchemaPaths.ps1') -UpdateVSCode } +else { Write-Host "Skipped (Claude Code resolves the schema at launch via the shim; pass -UpdateVSCode to wire VS Code)." } Write-Host "== 3/3 Self-check (end-to-end LSP diagnostics) ==" -ForegroundColor Cyan if (-not (Get-Command node -ErrorAction SilentlyContinue)) { diff --git a/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 b/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 new file mode 100644 index 0000000..cebfe6d --- /dev/null +++ b/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 @@ -0,0 +1,90 @@ +#requires -Version 7 +<# +.SYNOPSIS + Wire the bundled flow schema into VS Code user settings (optional editor integration). + +.DESCRIPTION + Claude Code no longer needs any stamped path: the launcher shim (scripts/lsp-launch.mjs) + resolves the absolute schema url at launch from ${CLAUDE_PLUGIN_ROOT}, so .lsp.json stays + portable and is never edited. This script now only serves the separate VS Code consumer, whose + settings.json cannot reference ${CLAUDE_PLUGIN_ROOT} and so needs a machine-local file URI. + +.EXAMPLE + 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 + +if (-not $UpdateVSCode) { + Write-Host "Nothing to do: Claude Code resolves the schema at launch via the shim. Pass -UpdateVSCode to wire the VS Code editor path." -ForegroundColor Yellow + return +} + +# The schema association VS Code needs: one schema, many file-match globs. Do NOT associate +# every *.json. Kept in sync with the globs the shim (scripts/lsp-launch.mjs) injects. +$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" +} +# 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') + +# --- VS Code user settings.json --- +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. Restart VS Code to apply." -ForegroundColor Cyan diff --git a/plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs b/plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs new file mode 100644 index 0000000..3fa8864 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs @@ -0,0 +1,94 @@ +// Launcher shim for the cloud-flow JSON language server. +// +// Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} in .lsp.json command/args (NOT in settings), so +// the plugin root arrives here as an argv. We spawn the real json-language-server, proxy LSP +// stdio, and inject the bundled schema association at runtime — computing an absolute file:// url +// from the passed root. Nothing machine-local is ever written to a committed file. +// +// The schema must be injected in THREE places or the server silently stops validating: +// 1. the forwarded `initialize` (initializationOptions.settings.json), +// 2. every client workspace/didChangeConfiguration (an empty client push otherwise CLEARS it), +// 3. by directly answering the server's workspace/configuration pull. +// +// Usage: node lsp-launch.mjs [--stdio] +import { spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const pluginRoot = process.argv[2]; +if (!pluginRoot) throw new Error('lsp-launch: missing plugin root argument'); + +const serverEntry = join(pluginRoot, 'node_modules', 'vscode-langservers-extracted', 'lib', 'json-language-server', 'node', 'jsonServerMain.js'); + +// Fail loud if the bundled schema is missing (e.g. a partial install): the server would otherwise +// launch and silently validate nothing. +const schemaPath = join(pluginRoot, 'schemas', 'cloud-flow-clientdata.schema.json'); +if (!existsSync(schemaPath)) throw new Error(`lsp-launch: schema not found: ${schemaPath}. Run scripts/Install-Plugin.ps1.`); +const schemaUrl = pathToFileURL(schemaPath).href; + +const jsonSettings = { + validate: { enable: true }, + schemas: [{ + fileMatch: ['**/Workflows/*.json', '**/Workflows/**/*.json', '**/*.flow.json'], + url: schemaUrl, + }], +}; + +// --- LSP stdio framing --- +function writeMsg(stream, obj) { + const s = JSON.stringify(obj); + stream.write(`Content-Length: ${Buffer.byteLength(s)}\r\n\r\n${s}`); +} +function makeParser(onMsg) { + let buf = Buffer.alloc(0); + return (chunk) => { + buf = Buffer.concat([buf, chunk]); + for (;;) { + const he = buf.indexOf('\r\n\r\n'); + if (he === -1) break; + const m = buf.slice(0, he).toString('ascii').match(/Content-Length:\s*(\d+)/i); + const start = he + 4; + if (!m) { buf = buf.slice(start); continue; } + const len = parseInt(m[1], 10); + if (buf.length < start + len) break; + onMsg(JSON.parse(buf.slice(start, start + len).toString('utf8'))); + buf = buf.slice(start + len); + } + }; +} + +const server = spawn(process.execPath, [serverEntry, '--stdio'], { stdio: ['pipe', 'pipe', 'inherit'] }); + +const fromClient = makeParser((msg) => { + if (msg.method === 'initialize') { + msg.params = msg.params || {}; + const io = msg.params.initializationOptions = msg.params.initializationOptions || {}; + io.provideFormatter = true; + io.handledSchemaProtocols = ['file']; + io.settings = io.settings || {}; + io.settings.json = jsonSettings; + } + if (msg.method === 'workspace/didChangeConfiguration') { + msg.params = msg.params || {}; + msg.params.settings = msg.params.settings || {}; + msg.params.settings.json = jsonSettings; + } + writeMsg(server.stdin, msg); +}); + +const fromServer = makeParser((msg) => { + if (msg.method === 'workspace/configuration' && msg.id !== undefined) { + const result = (msg.params.items || []).map((it) => (it.section === 'json' ? jsonSettings : {})); + writeMsg(server.stdin, { jsonrpc: '2.0', id: msg.id, result }); + return; // consumed by the shim; never reaches the client + } + writeMsg(process.stdout, msg); +}); + +process.stdin.on('data', fromClient); +server.stdout.on('data', fromServer); +server.on('error', (err) => { console.error(`lsp-launch: failed to spawn ${serverEntry}: ${err.message}`); process.exit(1); }); +// Exit on 'close' (fires after stdio has fully drained), not 'exit', so the last forwarded +// message is not truncated. +server.on('close', (c) => process.exit(c ?? 0)); diff --git a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs b/plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs similarity index 56% rename from plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs rename to plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs index b75dc30..0218e51 100644 --- a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs +++ b/plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs @@ -1,16 +1,17 @@ -// 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. +// End-to-end health check for the flow LSP. Launches the plugin exactly as .lsp.json does — via +// the launcher shim (scripts/lsp-launch.mjs), which spawns vscode-json-language-server and injects +// the bundled schema at runtime. 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. +// The client here supplies NO schema of its own and answers any workspace/configuration pull that +// reaches it with {} — so a firing schema can only have come from the shim. That both exercises +// the real launch path and proves the shim resolves the schema without any stamped config. +// +// It runs TWO scenarios so the check matches whichever config-delivery path the host uses: +// push - no `configuration` capability advertised; pulls left unanswered by the client. +// pull - `configuration` capability advertised; any pull reaching the client answered with {}. +// Both must pass. // // Usage: node scripts/lsp-smoke.mjs import { spawn } from 'node:child_process'; @@ -20,22 +21,9 @@ 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 shim = join(here, 'lsp-launch.mjs'); const fixturesDir = join(pluginRoot, 'tests', 'fixtures'); -// 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 } }; - function loadCases() { const cases = []; for (const kind of ['valid', 'invalid']) { @@ -47,11 +35,11 @@ function loadCases() { return cases; } -// Run every fixture through a freshly-spawned server under the given config-delivery mode. +// Run every fixture through a freshly-spawned shim 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 advertiseConfig = mode === 'pull'; + const server = spawn(process.execPath, [shim, pluginRoot, '--stdio']); const diagnostics = new Map(); // uri -> diagnostics[] let buf = Buffer.alloc(0); @@ -60,13 +48,9 @@ function runScenario(mode) { 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; - } + // The shim should intercept workspace/configuration; if one still reaches us, answer {} so the + // schema can only have come from the shim. + if (msg.method === 'workspace/configuration') { send({ id: msg.id, result: msg.params.items.map(() => ({})) }); 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 || []); } }; @@ -94,13 +78,14 @@ function runScenario(mode) { }); return (async () => { - const capabilities = answerPulls + const capabilities = advertiseConfig ? { 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 } }); + : { workspace: { didChangeConfiguration: {} } }; + // Deliberately supply NO schema: no initializationOptions.settings, empty didChangeConfiguration. + send({ id: 1, method: 'initialize', params: { processId: process.pid, rootUri: pathToFileURL(pluginRoot).href, capabilities } }); await new Promise((r) => setTimeout(r, 300)); send({ method: 'initialized', params: {} }); - send({ method: 'workspace/didChangeConfiguration', params: { settings: { json: jsonSettings } } }); + send({ method: 'workspace/didChangeConfiguration', params: { settings: {} } }); let failures = 0; console.log(`\n[${mode}]`); diff --git a/plugins/cloud-flow-json-lsp/tests/Install.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/Install.Tests.ps1 new file mode 100644 index 0000000..d5e5f38 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/Install.Tests.ps1 @@ -0,0 +1,34 @@ +#requires -Version 7 +<# + Install-Plugin.ps1 is documented as idempotent (safe to re-run after /plugin update). This + proves it: run it TWICE and require the second run to exit 0. + + The run exercises the real install (JSON server + end-to-end smoke self-check), so it is guarded + to SKIP cleanly when the environment can't support it without network work: + - node absent -> the shim/self-check can't launch, + - server entry absent -> Install-JsonLanguageServer would run `npm ci` (network). + When both are present the run is warm and network-free. +#> + +Describe 'Install-Plugin is idempotent (cloud-flow-json-lsp)' { + + BeforeAll { + $script:pluginRoot = Split-Path $PSScriptRoot -Parent + $script:install = Join-Path $pluginRoot 'scripts' 'Install-Plugin.ps1' + $script:pwshExe = [Environment]::ProcessPath # this host is pwsh 7 (#requires); reuse it + + $serverEntry = Join-Path $pluginRoot 'node_modules' 'vscode-langservers-extracted' 'lib' 'json-language-server' 'node' 'jsonServerMain.js' + $script:haveServer = Test-Path $serverEntry + $script:haveNode = [bool](Get-Command node -ErrorAction SilentlyContinue) + } + + It 'exits 0 when run twice in a row' { + if (-not $haveNode) { Set-ItResult -Skipped -Because 'node is not on PATH'; return } + if (-not $haveServer) { Set-ItResult -Skipped -Because 'JSON language server is not installed (node_modules); a run would `npm ci`'; return } + + & $pwshExe -NoProfile -File $install *> $null + $LASTEXITCODE | Should -Be 0 -Because 'first install run should succeed' + $out = & $pwshExe -NoProfile -File $install 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "second (idempotent) run should also succeed. Output:`n$($out -join "`n")" + } +} diff --git a/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 new file mode 100644 index 0000000..f183535 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 @@ -0,0 +1,69 @@ +#requires -Version 7 +<# + Two guards: + 1. .lsp.json launches the portable launcher shim (scripts/lsp-launch.mjs) via node with + ${CLAUDE_PLUGIN_ROOT} args, and carries NO machine-local absolute path. The shim resolves + the schema at launch, so nothing is stamped per machine. + 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 is portable (.lsp.json launches the shim)' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $script:lspPath = Join-Path $pluginRoot '.lsp.json' + $script:raw = Get-Content $script:lspPath -Raw + $script:lsp = $script:raw | ConvertFrom-Json + } + + It 'launches the shim via node with ${CLAUDE_PLUGIN_ROOT} args' { + $lsp.json.command | Should -Be 'node' + $argstr = $lsp.json.args -join ' ' + $argstr | Should -Match 'lsp-launch\.mjs' + $argstr | Should -Match '\$\{CLAUDE_PLUGIN_ROOT\}' + $argstr | Should -Match '--stdio' + } + + It 'keeps the extension-to-language wiring' { + $lsp.json.extensionToLanguage.'.json' | Should -Be 'json' + } + + It 'contains no machine-local absolute path' { + # No stamped file URI, and no bare absolute path (Windows drive or POSIX root) outside the + # ${CLAUDE_PLUGIN_ROOT} variable. Everything machine-specific is resolved at launch. + $raw | Should -Not -Match 'file://' + $raw | Should -Not -Match '[A-Za-z]:[\\/]' + foreach ($a in $lsp.json.args) { + if ($a -notmatch '\$\{CLAUDE_PLUGIN_ROOT\}') { $a | Should -Not -Match '^([A-Za-z]:[\\/]|/)' } + } + } +} + +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/cloud-flow-json-lsp/tests/SetupSkill.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/SetupSkill.Tests.ps1 new file mode 100644 index 0000000..6c1a5f9 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/tests/SetupSkill.Tests.ps1 @@ -0,0 +1,46 @@ +#requires -Version 7 +<# + Locks the setup skill's no-auto-trigger behavior. The root SKILL.md must: + - parse (have a YAML frontmatter block between the first two --- lines), + - carry name: cloud-flow-json-lsp-setup, + - carry NO description: key (a description would let Claude auto-invoke the skill; setup is + run once by hand, never triggered from context). +#> + +Describe 'setup skill frontmatter (cloud-flow-json-lsp)' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $script:skillPath = Join-Path $pluginRoot 'SKILL.md' + $script:lines = Get-Content $script:skillPath + + # Parse the YAML frontmatter: the block between the first two --- lines. Fail loud if the + # file has no such block (a bare key/value list is all these skills carry). + $fence = @() + for ($i = 0; $i -lt $script:lines.Count; $i++) { + if ($script:lines[$i].Trim() -eq '---') { $fence += $i } + if ($fence.Count -eq 2) { break } + } + $script:frontmatter = @{} + if ($fence.Count -eq 2) { + foreach ($line in $script:lines[($fence[0] + 1)..($fence[1] - 1)]) { + if ($line -match '^\s*([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$') { + $script:frontmatter[$Matches[1]] = $Matches[2] + } + } + } + $script:fenceCount = $fence.Count + } + + It 'has a parseable YAML frontmatter block' { + $fenceCount | Should -Be 2 -Because 'SKILL.md must open with a --- ... --- frontmatter block' + } + + It 'name is cloud-flow-json-lsp-setup' { + $frontmatter['name'] | Should -Be 'cloud-flow-json-lsp-setup' + } + + It 'has NO description key (no auto-trigger)' { + $frontmatter.ContainsKey('description') | Should -BeFalse -Because 'a description would let the skill auto-trigger; setup is run by hand' + } +} diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json b/plugins/cloud-flow-json-lsp/tests/fixtures/invalid/bad-runafter-status.json similarity index 100% rename from plugins/power-automate-cloud-flow/tests/fixtures/invalid/bad-runafter-status.json rename to plugins/cloud-flow-json-lsp/tests/fixtures/invalid/bad-runafter-status.json diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json b/plugins/cloud-flow-json-lsp/tests/fixtures/invalid/missing-definition.json similarity index 100% rename from plugins/power-automate-cloud-flow/tests/fixtures/invalid/missing-definition.json rename to plugins/cloud-flow-json-lsp/tests/fixtures/invalid/missing-definition.json diff --git a/plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json b/plugins/cloud-flow-json-lsp/tests/fixtures/valid/simple-flow.json similarity index 100% rename from plugins/power-automate-cloud-flow/tests/fixtures/valid/simple-flow.json rename to plugins/cloud-flow-json-lsp/tests/fixtures/valid/simple-flow.json diff --git a/plugins/dataverse-customization-xml/.claude-plugin/plugin.json b/plugins/dataverse-customization-xml/.claude-plugin/plugin.json deleted file mode 100644 index 67defc7..0000000 --- a/plugins/dataverse-customization-xml/.claude-plugin/plugin.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://anthropic.com/claude-code/plugin.schema.json", - "name": "dataverse-customization-xml", - "version": "1.0.1", - "description": "Schema-validated hand-editing of Dataverse customization XML (ribbon, sitemap, forms, FetchXML, charts and more) against the official XSDs, with lemminx LSP diagnostics and a standalone validator.", - "author": { - "name": "Connor Parsons" - }, - "homepage": "https://github.com/Cordedmink2/dataverse-agent-plugins", - "skills": [ - "./" - ] -} diff --git a/plugins/dataverse-customization-xml/.lsp.json b/plugins/dataverse-customization-xml/.lsp.json deleted file mode 100644 index d58180a..0000000 --- a/plugins/dataverse-customization-xml/.lsp.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "xml": { - "command": "${CLAUDE_PLUGIN_ROOT}/bin/lemminx-win32.exe", - "extensionToLanguage": { - ".xml": "xml", - ".fetchxml": "xml" - }, - "initializationOptions": { - "settings": { - "xml": { - "validation": { - "enabled": true, - "schema": { - "enabled": "always" - } - }, - "fileAssociations": [ - { - "pattern": "**/RibbonDiff.xml", - "systemId": "schemas/9.0.0.2090/RibbonCore.xsd" - }, - { - "pattern": "**/[Cc]ustomizations.xml", - "systemId": "schemas/9.0.0.2090/CustomizationsSolution.xsd" - }, - { - "pattern": "**/SiteMap*.xml", - "systemId": "schemas/9.0.0.2090/SiteMap.xsd" - }, - { - "pattern": "**/FormXml/**/*.xml", - "systemId": "schemas/9.0.0.2090/FormXml.xsd" - }, - { - "pattern": "**/SavedQueries/**/*.xml", - "systemId": "schemas/9.0.0.2090/Fetch.xsd" - }, - { - "pattern": "**/*.fetchxml", - "systemId": "schemas/9.0.0.2090/Fetch.xsd" - }, - { - "pattern": "**/isv.config.xml", - "systemId": "schemas/9.0.0.2090/isv.config.xsd" - } - ] - } - } - }, - "settings": { - "xml": { - "validation": { - "enabled": true, - "schema": { - "enabled": "always" - } - }, - "fileAssociations": [ - { - "pattern": "**/RibbonDiff.xml", - "systemId": "schemas/9.0.0.2090/RibbonCore.xsd" - }, - { - "pattern": "**/[Cc]ustomizations.xml", - "systemId": "schemas/9.0.0.2090/CustomizationsSolution.xsd" - }, - { - "pattern": "**/SiteMap*.xml", - "systemId": "schemas/9.0.0.2090/SiteMap.xsd" - }, - { - "pattern": "**/FormXml/**/*.xml", - "systemId": "schemas/9.0.0.2090/FormXml.xsd" - }, - { - "pattern": "**/SavedQueries/**/*.xml", - "systemId": "schemas/9.0.0.2090/Fetch.xsd" - }, - { - "pattern": "**/*.fetchxml", - "systemId": "schemas/9.0.0.2090/Fetch.xsd" - }, - { - "pattern": "**/isv.config.xml", - "systemId": "schemas/9.0.0.2090/isv.config.xsd" - } - ] - } - } - } -} diff --git a/plugins/dataverse-customization-xml/commands/setup.md b/plugins/dataverse-customization-xml/commands/setup.md deleted file mode 100644 index 8f28fd0..0000000 --- a/plugins/dataverse-customization-xml/commands/setup.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: Fetch the Microsoft XSDs + lemminx binary and stamp machine-local paths (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 XML extension configured - (adds `xml.fileAssociations` 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 lemminx LSP starts - with the stamped paths. diff --git a/plugins/dataverse-customization-xml/scripts/Set-LspSchemaPaths.ps1 b/plugins/dataverse-customization-xml/scripts/Set-LspSchemaPaths.ps1 deleted file mode 100644 index f9281c0..0000000 --- a/plugins/dataverse-customization-xml/scripts/Set-LspSchemaPaths.ps1 +++ /dev/null @@ -1,144 +0,0 @@ -#requires -Version 7 -<# -.SYNOPSIS - Stamp machine-local absolute schema paths and the per-OS lemminx binary into .lsp.json - (and optionally VS Code user settings). - -.DESCRIPTION - ${CLAUDE_PLUGIN_ROOT} is NOT substituted inside .lsp.json initializationOptions/settings, - so the lemminx fileAssociation systemIds must be absolute paths for THIS machine. The - 'command' field keeps the ${CLAUDE_PLUGIN_ROOT} variable (it IS substituted there) but the - binary filename is OS-specific. Run after install/update or after changing schema version. - -.EXAMPLE - pwsh scripts/Set-LspSchemaPaths.ps1 - pwsh scripts/Set-LspSchemaPaths.ps1 -UpdateVSCode -#> -[CmdletBinding()] -param( - # Defaults to the schemaVersion pinned in versions.json. - [string]$SchemaVersion, - [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 - -if (-not $SchemaVersion) { - $SchemaVersion = (Get-Content (Join-Path $pluginRoot 'versions.json') -Raw | ConvertFrom-Json).schemaVersion -} -$schemaDir = Join-Path $pluginRoot 'schemas' $SchemaVersion - -# pattern -> schema filename. Charts have no association: their wrapper root -# is not declared in the XSD, so whole-file LSP validation would only produce a false error; -# the validator script extracts and validates the inner instead. -$assoc = [ordered]@{ - '**/RibbonDiff.xml' = 'RibbonCore.xsd' - '**/[Cc]ustomizations.xml' = 'CustomizationsSolution.xsd' - '**/SiteMap*.xml' = 'SiteMap.xsd' - '**/FormXml/**/*.xml' = 'FormXml.xsd' - '**/SavedQueries/**/*.xml' = 'Fetch.xsd' - '**/*.fetchxml' = 'Fetch.xsd' - '**/isv.config.xml' = 'isv.config.xsd' -} - -# Every XSD about to be stamped must actually exist - a partial extraction or wrong -# -SchemaVersion would otherwise stamp paths to nonexistent files. -$missingXsds = @($assoc.Values | Sort-Object -Unique | - Where-Object { -not (Test-Path (Join-Path $schemaDir $_)) }) -if ($missingXsds.Count -gt 0) { - throw "Missing XSD(s) in ${schemaDir}: $($missingXsds -join ', '). Run scripts/Get-Schemas.ps1 first." -} -$schemaDirFwd = ((Resolve-Path $schemaDir).Path) -replace '\\', '/' -$fileAssociations = @($assoc.GetEnumerator() | ForEach-Object { - [ordered]@{ pattern = $_.Key; systemId = "$schemaDirFwd/$($_.Value)" } - }) - -# lemminx binary name must match whatever Get-Lemminx.ps1 installed on this OS. Get-Lemminx.ps1 -# discovers the binary's real name inside the vsix rather than hardcoding it (per-OS names are -# e.g. lemminx-win32.exe, lemminx-linux-x86_64, lemminx-osx-x86_64, lemminx-osx-aarch_64), so -# find it here the same way instead of guessing. -$binDir = Join-Path $pluginRoot 'bin' -$binaries = @(Get-ChildItem $binDir -Filter 'lemminx*' -File -ErrorAction SilentlyContinue) -if ($binaries.Count -ne 1) { - throw "Expected exactly one lemminx binary in $binDir, found $($binaries.Count). Run scripts/Get-Lemminx.ps1 first." -} -$exeName = $binaries[0].Name - -# --- .lsp.json --- -$lspPath = Join-Path $pluginRoot '.lsp.json' -$lsp = Get-Content $lspPath -Raw | ConvertFrom-Json -$xmlBlock = [ordered]@{ - validation = [ordered]@{ enabled = $true; schema = [ordered]@{ enabled = 'always' } } - fileAssociations = $fileAssociations -} -$lsp.xml.command = '${CLAUDE_PLUGIN_ROOT}/bin/' + $exeName -$lsp.xml.initializationOptions.settings.xml = $xmlBlock -$lsp.xml.settings.xml = $xmlBlock -$lsp | ConvertTo-Json -Depth 20 | Set-Content $lspPath -Encoding UTF8 -Write-Host "Updated $lspPath" -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) { - # RedHat XML globs have no char classes -> expand [Cc] into two entries. - $vscodeAssoc = @() - foreach ($kv in $assoc.GetEnumerator()) { - $pattern = $kv.Key -replace '\[Cc\]', 'C' - $vscodeAssoc += [ordered]@{ pattern = $pattern; systemId = "$schemaDirFwd/$($kv.Value)" } - if ($kv.Key -match '\[Cc\]') { - $vscodeAssoc += [ordered]@{ pattern = ($kv.Key -replace '\[Cc\]', 'c'); systemId = "$schemaDirFwd/$($kv.Value)" } - } - } - - # 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 - # (**/*.xml). - $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 these settings manually:" -ForegroundColor Yellow - $manual = [ordered]@{ - 'xml.fileAssociations' = $vscodeAssoc - 'xml.validation.enabled' = $true - 'xml.validation.schema.enabled' = 'always' - } - Write-Host ($manual | ConvertTo-Json -Depth 5) - } - else { - $s = $raw | ConvertFrom-Json - # Merge by pattern: keep the user's associations for patterns that are not ours - # (e.g. their own pom.xml mapping), replace/append ours. - $ourPatterns = @($vscodeAssoc | ForEach-Object { $_.pattern }) - $kept = @() - if ($s.PSObject.Properties['xml.fileAssociations']) { - $kept = @($s.'xml.fileAssociations' | Where-Object { $_.pattern -notin $ourPatterns }) - } - $s | Add-Member -NotePropertyName 'xml.fileAssociations' -NotePropertyValue ($kept + $vscodeAssoc) -Force - $s | Add-Member -NotePropertyName 'xml.validation.enabled' -NotePropertyValue $true -Force - $s | Add-Member -NotePropertyName 'xml.validation.schema.enabled' -NotePropertyValue 'always' -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/dataverse-customization-xml/tests/LspConfig.Tests.ps1 b/plugins/dataverse-customization-xml/tests/LspConfig.Tests.ps1 deleted file mode 100644 index 8549c4d..0000000 --- a/plugins/dataverse-customization-xml/tests/LspConfig.Tests.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -#requires -Version 7 -<# - Guards the two sources of truth for lemminx file associations against drift: - the $assoc map in scripts/Set-LspSchemaPaths.ps1 (what setup stamps) and .lsp.json - (what ships and what lemminx actually loads). Compares pattern -> schema-filename - pairs, not full systemId paths: the systemId is a relative path in the committed - file and a machine-local absolute path once setup has stamped it, but the filename - part is identical in both forms. -#> - -Describe 'lemminx file association parity (.lsp.json vs Set-LspSchemaPaths.ps1)' { - - BeforeAll { - $pluginRoot = Split-Path $PSScriptRoot -Parent - - # Extract $assoc from the script without executing it: dot-sourcing would rewrite - # .lsp.json as a side effect. SafeGetValue evaluates the hashtable literal from - # the AST with no code execution. - $scriptPath = Join-Path $pluginRoot 'scripts' 'Set-LspSchemaPaths.ps1' - $ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$null, [ref]$null) - $assocAssignment = $ast.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 'assoc' - }, $true) - if (-not $assocAssignment) { throw "No `$assoc assignment found in $scriptPath" } - $hashAst = $assocAssignment.Find({ param($node) $node -is [System.Management.Automation.Language.HashtableAst] }, $true) - if (-not $hashAst) { throw "The `$assoc assignment in $scriptPath is no longer a hashtable literal; update this test's extraction." } - $assoc = $hashAst.SafeGetValue() - - # Script scope: these are read by the It blocks below, which run in their own scope. - $script:expectedPairs = @($assoc.GetEnumerator() | ForEach-Object { '{0} -> {1}' -f $_.Key, $_.Value }) | Sort-Object - - $script:lsp = Get-Content (Join-Path $pluginRoot '.lsp.json') -Raw | ConvertFrom-Json - - function Get-PairSet($FileAssociations) { - @($FileAssociations | ForEach-Object { - '{0} -> {1}' -f $_.pattern, ($_.systemId -split '/')[-1] - }) | Sort-Object - } - } - - It 'has a non-empty association map in the script' { - $expectedPairs.Count | Should -BeGreaterThan 0 - } - - It 'matches in the initializationOptions settings block' { - Get-PairSet $lsp.xml.initializationOptions.settings.xml.fileAssociations | - Should -Be $expectedPairs - } - - It 'matches in the workspace settings block' { - Get-PairSet $lsp.xml.settings.xml.fileAssociations | - Should -Be $expectedPairs - } -} diff --git a/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json b/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json new file mode 100644 index 0000000..c4e77c6 --- /dev/null +++ b/plugins/dataverse-xml-lsp/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://anthropic.com/claude-code/plugin.schema.json", + "name": "dataverse-xml-lsp", + "version": "2.0.0", + "description": "LSP + CLI validator for Dataverse customization XML: live lemminx diagnostics and a standalone PowerShell validator checking ribbon, sitemap, forms, FetchXML, charts and more against the official XSDs.", + "author": { + "name": "Connor Parsons" + }, + "homepage": "https://github.com/Cordedmink2/dataverse-agent-plugins", + "skills": [ + "./" + ] +} diff --git a/plugins/dataverse-customization-xml/.gitignore b/plugins/dataverse-xml-lsp/.gitignore similarity index 100% rename from plugins/dataverse-customization-xml/.gitignore rename to plugins/dataverse-xml-lsp/.gitignore diff --git a/plugins/dataverse-xml-lsp/.lsp.json b/plugins/dataverse-xml-lsp/.lsp.json new file mode 100644 index 0000000..e9bc3ec --- /dev/null +++ b/plugins/dataverse-xml-lsp/.lsp.json @@ -0,0 +1,14 @@ +{ + "xml": { + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/scripts/lsp-launch.mjs", + "${CLAUDE_PLUGIN_ROOT}", + "--stdio" + ], + "extensionToLanguage": { + ".xml": "xml", + ".fetchxml": "xml" + } + } +} diff --git a/plugins/dataverse-customization-xml/README.md b/plugins/dataverse-xml-lsp/README.md similarity index 63% rename from plugins/dataverse-customization-xml/README.md rename to plugins/dataverse-xml-lsp/README.md index 8f73c65..93345a5 100644 --- a/plugins/dataverse-customization-xml/README.md +++ b/plugins/dataverse-xml-lsp/README.md @@ -1,10 +1,12 @@ -# dataverse-customization-xml +# dataverse-xml-lsp A Claude Code plugin for **schema-validated hand-editing of Dataverse / model-driven-app customization XML** — ribbon buttons (`RibbonDiffXml`), app navigation (`SiteMap`), forms (`FormXml`), FetchXML, charts, and the whole `Customizations.xml`. Malformed edits fail loud *before* `pac solution import`. +**See [`docs/guide.md`](docs/guide.md) for editing guidance.** + ## Two validation layers 1. **Script (backbone)** — `pwsh scripts/Validate-DataverseXml.ps1 `. Works from any @@ -16,14 +18,15 @@ customization XML** — ribbon buttons (`RibbonDiffXml`), app navigation (`SiteM ``` /plugin marketplace add Cordedmink2/dataverse-agent-plugins -/plugin install dataverse-customization-xml@dataverse-agent-plugins -/dataverse-customization-xml:setup +/plugin install dataverse-xml-lsp@dataverse-agent-plugins +/dataverse-xml-lsp:dataverse-xml-lsp-setup ``` -The setup command fetches the Microsoft XSDs and the lemminx binary, stamps this machine's -absolute paths, and runs a self-check. It requires PowerShell 7+ (`pwsh`) — Windows ships -only Windows PowerShell 5.1, and the scripts have `#requires -Version 7`. Then run -`/reload-plugins` (or restart the session) so the LSP starts with the stamped paths. +The setup command fetches the Microsoft XSDs and the lemminx binary and runs a self-check. It +requires PowerShell 7+ (`pwsh`) — Windows ships only Windows PowerShell 5.1, and the scripts +have `#requires -Version 7`. Then run `/reload-plugins` (or restart the session) so the LSP +starts. The launcher shim resolves the bundled schema at runtime, so nothing machine-specific +is stamped into `.lsp.json`. ## Setup script directly (non-Claude consumers) @@ -38,21 +41,22 @@ pwsh scripts/Install-Plugin.ps1 [-UpdateVSCode] [-SkipLemminx] [-TargetPlatform agents). - `-TargetPlatform` overrides the auto-detected OS/arch for the lemminx download. -Run this one script rather than the individual `Get-*`/`Set-*` scripts — a partial manual setup -can leave a running lemminx pointing at broken relative schema paths. +Run this one script rather than the individual `Get-*` scripts — a partial manual setup can +leave the XSDs or the lemminx binary missing, so the launcher shim throws at startup instead of +validating silently against nothing. ## What's inside | Path | Purpose | |------|---------| -| `SKILL.md` | The skill: workflow, root→schema table, ribbon-button recipe, gotchas | -| `commands/setup.md` | The `/dataverse-customization-xml:setup` slash command | -| `scripts/Install-Plugin.ps1` | One-shot setup: schemas + lemminx + path stamping + self-check | +| `SKILL.md` | Tiny no-description setup skill (runs `Install-Plugin.ps1`) | +| `scripts/Install-Plugin.ps1` | One-shot setup: schemas + lemminx + self-check | | `scripts/Validate-DataverseXml.ps1` | Standalone validator — root-element → XSD, line/col errors, non-zero exit | | `scripts/Get-Schemas.ps1` | Download the official Microsoft XSDs into `schemas//` | | `scripts/Get-Lemminx.ps1` | Download the native lemminx binary into `bin/` (SHA256-verified) | -| `scripts/Set-LspSchemaPaths.ps1` | Stamp this machine's absolute schema paths into `.lsp.json` (+ VS Code) | -| `.lsp.json` | Registers lemminx with Claude Code, with XSD `fileAssociations` | +| `scripts/lsp-launch.mjs` | Launcher shim — spawns lemminx and injects the bundled XSD associations at runtime | +| `scripts/Set-LspSchemaPaths.ps1` | Write the XSD associations into VS Code user settings (`-UpdateVSCode`; Claude Code uses the shim instead) | +| `.lsp.json` | Launches lemminx via the shim, which resolves the bundled XSDs at runtime — no stamped paths | | `versions.json` | Pinned schema version/URL and lemminx (vscode-xml) version | | `schemas/SOURCE.md` | Where the XSDs come from and how to bump the version | | `tests/` | Pester suite + valid/invalid fixtures for every mapped root | @@ -66,13 +70,14 @@ platform-specific. ``` /plugin marketplace update dataverse-agent-plugins -/plugin update dataverse-customization-xml@dataverse-agent-plugins -/dataverse-customization-xml:setup +/plugin update dataverse-xml-lsp@dataverse-agent-plugins +/dataverse-xml-lsp:dataverse-xml-lsp-setup ``` -Re-run setup after every update — it re-stamps paths and re-checks the fetched assets (see -`docs/debugging.md` for why). Schema-version bumps are driven by `versions.json`; the procedure -is in `schemas/SOURCE.md`. +The launcher shim resolves the bundled schema at runtime, so `.lsp.json` is portable — you do +**not** need to re-run setup after a plugin update or a repo move. Re-run it only when an update +bumps the pinned schema/lemminx version and you need the new assets fetched. Schema-version bumps +are driven by `versions.json`; the procedure is in `schemas/SOURCE.md`. ## Other consumers @@ -90,7 +95,8 @@ Something not working? [`docs/debugging.md`](docs/debugging.md). SiteMap fragment validation is authoritative; FormXml is indicative (the schema lags modern form attributes like `headerdensity`, `contenttype`). - **`${CLAUDE_PLUGIN_ROOT}` is only substituted in `.lsp.json` `command`/`args`**, not in nested - `settings` — hence the absolute-path fixup script. + `settings` — which is why the launcher shim (`scripts/lsp-launch.mjs`), handed the plugin root + as an argv, injects the XSD associations at runtime instead of `.lsp.json` carrying them. ## License @@ -98,4 +104,4 @@ The plugin is MIT-licensed (see the repo root `LICENSE`). The Microsoft XSDs are from Microsoft at setup time and remain subject to Microsoft's terms — they are not redistributed in this repo. -See `SKILL.md` for the ribbon-button recipe and the full edit → validate → pack → import loop. +See [`docs/guide.md`](docs/guide.md) for the ribbon-button recipe and the full edit → validate → pack → import loop. diff --git a/plugins/dataverse-xml-lsp/SKILL.md b/plugins/dataverse-xml-lsp/SKILL.md new file mode 100644 index 0000000..76167d7 --- /dev/null +++ b/plugins/dataverse-xml-lsp/SKILL.md @@ -0,0 +1,9 @@ +--- +name: dataverse-xml-lsp-setup +--- + +# dataverse-xml-lsp setup + +This plugin is a lemminx XML LSP for Dataverse customization XML (plus `scripts/Validate-DataverseXml.ps1` for CI/headless). Run once per machine: + +`pwsh "${CLAUDE_PLUGIN_ROOT}/scripts/Install-Plugin.ps1"` (add `-UpdateVSCode` to also wire VS Code). Then run `/reload-plugins`. If it fails, see `docs/debugging.md`. Editing guidance: `docs/guide.md`. diff --git a/plugins/dataverse-customization-xml/docs/codex.md b/plugins/dataverse-xml-lsp/docs/codex.md similarity index 70% rename from plugins/dataverse-customization-xml/docs/codex.md rename to plugins/dataverse-xml-lsp/docs/codex.md index b523376..1649c5e 100644 --- a/plugins/dataverse-customization-xml/docs/codex.md +++ b/plugins/dataverse-xml-lsp/docs/codex.md @@ -9,7 +9,7 @@ script after each edit. 1. Clone the repo and run setup (no LSP needed, so skip the binary): ``` - pwsh plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 -SkipLemminx + pwsh plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 -SkipLemminx ``` This fetches the Microsoft XSDs and runs a self-check; requires PowerShell 7+ (`pwsh`). @@ -18,12 +18,12 @@ script after each edit. > When editing Dataverse customization XML (RibbonDiff.xml, SiteMap, FormXml, > Customizations.xml, FetchXML, charts), follow - > `/plugins/dataverse-customization-xml/SKILL.md`, and after EVERY edit run: - > `pwsh /plugins/dataverse-customization-xml/scripts/Validate-DataverseXml.ps1 ` + > `/plugins/dataverse-xml-lsp/docs/guide.md`, and after EVERY edit run: + > `pwsh /plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 ` > Non-zero exit = the edit is invalid; fix before pack/import. 3. If your Codex setup supports skill folders, point it at - `plugins/dataverse-customization-xml/` directly — SKILL.md has standard frontmatter. + `plugins/dataverse-xml-lsp/docs/guide.md` directly for the full editing guidance. ## The loop diff --git a/plugins/dataverse-customization-xml/docs/debugging.md b/plugins/dataverse-xml-lsp/docs/debugging.md similarity index 56% rename from plugins/dataverse-customization-xml/docs/debugging.md rename to plugins/dataverse-xml-lsp/docs/debugging.md index 7d303d5..4deab76 100644 --- a/plugins/dataverse-customization-xml/docs/debugging.md +++ b/plugins/dataverse-xml-lsp/docs/debugging.md @@ -12,27 +12,18 @@ ## No live diagnostics in Claude Code -1. Did setup run? `.lsp.json` systemIds must be absolute paths that exist — committed state - is relative-on-purpose and does nothing. -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. lemminx only validates files inside a real workspace folder. -5. Subagents and headless runs NEVER get LSP pushes — that's by design; they must run the +Diagnostics work straight from the committed `.lsp.json`: it launches the shim +(`scripts/lsp-launch.mjs`), which resolves the bundled XSDs at runtime from the plugin root — no +stamped paths, so a plugin update or repo move does not break it. If squiggles don't appear: + +1. Is Node installed and on `PATH`? `.lsp.json` runs `node scripts/lsp-launch.mjs`; the shim + throws loudly if the XSDs or the lemminx binary are missing. Run + `pwsh scripts/Install-Plugin.ps1` to (re)fetch them. +2. Run `/reload-plugins` (or restart the session). +3. lemminx only validates files inside a real workspace folder. +4. Subagents and headless runs NEVER get LSP pushes — that's by design; they must run the validator script. -## After `/plugin update dataverse-customization-xml@dataverse-agent-plugins` - -Setup stamps machine-absolute paths into the **tracked** `.lsp.json` inside the marketplace -clone, so an update may conflict on that file or reset it to the committed relative paths. -Re-running `/dataverse-customization-xml:setup` after every plugin update is the documented -fix — it re-stamps the paths and re-verifies the fetched assets. - -Contributors working in a clone of this repo: the installer dirties `.lsp.json` — don't -commit the machine paths. `git update-index --skip-worktree plugins/dataverse-customization-xml/.lsp.json` -is one way to keep it out of your commits. - ## No diagnostics in VS Code - Is `redhat.vscode-xml` installed? Did you restart after `-UpdateVSCode`? diff --git a/plugins/dataverse-customization-xml/SKILL.md b/plugins/dataverse-xml-lsp/docs/guide.md similarity index 86% rename from plugins/dataverse-customization-xml/SKILL.md rename to plugins/dataverse-xml-lsp/docs/guide.md index 1891056..0f3038b 100644 --- a/plugins/dataverse-customization-xml/SKILL.md +++ b/plugins/dataverse-xml-lsp/docs/guide.md @@ -1,21 +1,13 @@ ---- -name: dataverse-customization-xml -description: >- - Hand-edit Dataverse / model-driven-app customization XML against the official Microsoft XSDs, - with schema validation before pack/import. Use whenever editing a pac-unpacked solution's - RibbonDiff.xml, Other/Customizations.xml, SiteMap XML, or FormXml — especially adding a - ribbon / command-bar button ("add a ribbon button", "command bar button", "RibbonDiffXml", - "custom action", "CommandDefinition", "JavaScriptFunction"), editing app navigation - (SiteMap), or validating any Dataverse customization XML fragment. Provides a standalone - PowerShell validator (Validate-DataverseXml.ps1) and live lemminx LSP diagnostics. ---- - -# Dataverse customization XML (schema-validated) +# dataverse-xml-lsp — editing guide + +> This is the manual replacement for the old auto-loaded skill; the plugin no longer surfaces this automatically. + +## Dataverse customization XML (schema-validated) Edit Dataverse customization XML so malformed edits **fail loud before `pac solution import`**, not silently at import time. Two validation layers share the same official Microsoft XSD set (`schemas/9.0.0.2090/`, fetched at setup — if the validator exits 2 with "Schema directory not -found", run `/dataverse-customization-xml:setup` or `pwsh scripts/Install-Plugin.ps1`): +found", run `/dataverse-xml-lsp:dataverse-xml-lsp-setup` or `pwsh scripts/Install-Plugin.ps1`): 1. **`scripts/Validate-DataverseXml.ps1`** — the backbone. Run it after every edit and before pack/import. Tool-agnostic (any shell, CI, Codex). No Java, no network. @@ -130,9 +122,9 @@ pack/import mechanics. - **The ultimate structural gate is `pac solution pack`** — it runs SolutionPackager's own validation. If it packs, the solution is structurally sound for import. - **`${CLAUDE_PLUGIN_ROOT}` is not substituted** inside `.lsp.json` `initializationOptions`/ - `settings` — only in `command`/`args`. Schema `systemId`s there use absolute paths. On a new - machine (or after a plugin update), run `/dataverse-customization-xml:setup` — or - `scripts/Set-LspSchemaPaths.ps1` alone — to re-stamp them (and VS Code settings). + `settings` — only in `command`/`args`. That's why the launcher shim (`scripts/lsp-launch.mjs`), + handed the plugin root as an argv, injects the XSD associations at runtime. `.lsp.json` carries no + paths, so it stays portable — no re-stamp is needed after a plugin update or a repo move. - lemminx validates a document only inside a real workspace and after answering its `workspace/configuration` pull — Claude Code and VS Code both handle that; a bare LSP client must too. @@ -140,5 +132,6 @@ pack/import mechanics. ## Refreshing the schemas To move to a newer schema release: update `schemaVersion` (and `schemasZipUrl` if Microsoft -publishes a new link) in `versions.json`, run `pwsh scripts/Get-Schemas.ps1 -Force`, run -`pwsh scripts/Set-LspSchemaPaths.ps1`, then run the test suite. Details in `schemas/SOURCE.md`. +publishes a new link) in `versions.json`, run `pwsh scripts/Get-Schemas.ps1 -Force`, then run the +test suite. The launcher shim reads `schemaVersion` from `versions.json` at launch, so it picks up +the new schema dir with no re-stamp. Details in `schemas/SOURCE.md`. diff --git a/plugins/dataverse-customization-xml/docs/vscode.md b/plugins/dataverse-xml-lsp/docs/vscode.md similarity index 61% rename from plugins/dataverse-customization-xml/docs/vscode.md rename to plugins/dataverse-xml-lsp/docs/vscode.md index d354cc3..27a3496 100644 --- a/plugins/dataverse-customization-xml/docs/vscode.md +++ b/plugins/dataverse-xml-lsp/docs/vscode.md @@ -9,13 +9,13 @@ scripts have `#requires -Version 7`. 2. Run: ``` - pwsh plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 -UpdateVSCode + pwsh plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 -UpdateVSCode -SkipLemminx ``` This fetches the Microsoft XSDs and writes `xml.fileAssociations` (absolute paths) into - your VS Code user settings. It also downloads the lemminx binary (~47 MB) — VS Code - doesn't use it (the XML extension ships its own server), but the path-stamping step needs - it present, so don't pass `-SkipLemminx` here. + your VS Code user settings. VS Code uses the RedHat extension's own server, not this + plugin's lemminx binary or launcher shim, so `-SkipLemminx` avoids the ~47 MB download (and + the Node requirement) here — the VS Code association only needs the XSDs. 3. Install the **XML** extension by Red Hat (`redhat.vscode-xml`) and restart VS Code. 4. Open any `RibbonDiff.xml` / `SiteMap*.xml` / `Customizations.xml` / `*.fetchxml` / `FormXml/**/*.xml` / `SavedQueries/**/*.xml` / `isv.config.xml` — schema errors appear as @@ -25,7 +25,7 @@ scripts have `#requires -Version 7`. The standalone validator also works as a plain CLI for pre-commit hooks or CI: ``` -pwsh plugins/dataverse-customization-xml/scripts/Validate-DataverseXml.ps1 +pwsh plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 ``` ## Settings-update notes @@ -38,9 +38,10 @@ pwsh plugins/dataverse-customization-xml/scripts/Validate-DataverseXml.ps1 +[CmdletBinding()] +param( + # Defaults to the schemaVersion pinned in versions.json. + [string]$SchemaVersion, + [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 + +if (-not $UpdateVSCode) { + Write-Host "Nothing to do: Claude Code resolves schemas at launch via the shim. Pass -UpdateVSCode to wire the VS Code editor path." -ForegroundColor Yellow + return +} + +if (-not $SchemaVersion) { + $SchemaVersion = (Get-Content (Join-Path $pluginRoot 'versions.json') -Raw | ConvertFrom-Json).schemaVersion +} +$schemaDir = Join-Path $pluginRoot 'schemas' $SchemaVersion + +# pattern -> schema filename. Charts () AND forms () have no association on +# purpose: they are pac WRAPPER files whose root the XSD doesn't declare, so whole-file validation +# would only produce a false error on the wrapper root. The validator (Validate-DataverseXml.ps1) +# owns them via per-fragment extraction (systemform/form, datadescription/datadefinition). Kept in +# sync with the associations the shim (scripts/lsp-launch.mjs) injects for Claude Code. +$assoc = [ordered]@{ + '**/RibbonDiff.xml' = 'RibbonCore.xsd' + '**/[Cc]ustomizations.xml' = 'CustomizationsSolution.xsd' + '**/SiteMap*.xml' = 'SiteMap.xsd' + '**/SavedQueries/**/*.xml' = 'Fetch.xsd' + '**/*.fetchxml' = 'Fetch.xsd' + '**/isv.config.xml' = 'isv.config.xsd' +} + +# Every XSD about to be wired must actually exist - a partial extraction or wrong +# -SchemaVersion would otherwise point VS Code at nonexistent files. +$missingXsds = @($assoc.Values | Sort-Object -Unique | + Where-Object { -not (Test-Path (Join-Path $schemaDir $_)) }) +if ($missingXsds.Count -gt 0) { + throw "Missing XSD(s) in ${schemaDir}: $($missingXsds -join ', '). Run scripts/Get-Schemas.ps1 first." +} +$schemaDirFwd = ((Resolve-Path $schemaDir).Path) -replace '\\', '/' + +# --- VS Code user settings.json --- +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) { + # RedHat XML globs have no char classes -> expand [Cc] into two entries. + $vscodeAssoc = @() + foreach ($kv in $assoc.GetEnumerator()) { + $pattern = $kv.Key -replace '\[Cc\]', 'C' + $vscodeAssoc += [ordered]@{ pattern = $pattern; systemId = "$schemaDirFwd/$($kv.Value)" } + if ($kv.Key -match '\[Cc\]') { + $vscodeAssoc += [ordered]@{ pattern = ($kv.Key -replace '\[Cc\]', 'c'); systemId = "$schemaDirFwd/$($kv.Value)" } + } + } + + # 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 + # (**/*.xml). + $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 these settings manually:" -ForegroundColor Yellow + $manual = [ordered]@{ + 'xml.fileAssociations' = $vscodeAssoc + 'xml.validation.enabled' = $true + 'xml.validation.schema.enabled' = 'always' + } + Write-Host ($manual | ConvertTo-Json -Depth 5) + } + else { + $s = $raw | ConvertFrom-Json + # Merge by pattern: keep the user's associations for patterns that are not ours + # (e.g. their own pom.xml mapping), replace/append ours. + $ourPatterns = @($vscodeAssoc | ForEach-Object { $_.pattern }) + $kept = @() + if ($s.PSObject.Properties['xml.fileAssociations']) { + $kept = @($s.'xml.fileAssociations' | Where-Object { $_.pattern -notin $ourPatterns }) + } + $s | Add-Member -NotePropertyName 'xml.fileAssociations' -NotePropertyValue ($kept + $vscodeAssoc) -Force + $s | Add-Member -NotePropertyName 'xml.validation.enabled' -NotePropertyValue $true -Force + $s | Add-Member -NotePropertyName 'xml.validation.schema.enabled' -NotePropertyValue 'always' -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. Restart VS Code to apply." -ForegroundColor Cyan diff --git a/plugins/dataverse-customization-xml/scripts/Validate-DataverseXml.ps1 b/plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/scripts/Validate-DataverseXml.ps1 rename to plugins/dataverse-xml-lsp/scripts/Validate-DataverseXml.ps1 diff --git a/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs b/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs new file mode 100644 index 0000000..2cd1102 --- /dev/null +++ b/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs @@ -0,0 +1,116 @@ +// Launcher shim for the lemminx (XML) native language server. +// +// Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} in .lsp.json command/args (NOT in settings), so +// the plugin root arrives here as an argv. We spawn the platform-specific lemminx binary, proxy +// LSP stdio, and inject xml.fileAssociations (each systemId an absolute file:// url to the right +// XSD) at runtime. Nothing machine-local is ever written to a committed file. +// +// The associations must be injected in THREE places or lemminx silently stops validating: +// 1. the forwarded `initialize` (initializationOptions.settings.xml), +// 2. every client workspace/didChangeConfiguration (an empty client push otherwise CLEARS it), +// 3. by directly answering the server's workspace/configuration pull. +// +// Usage: node lsp-launch.mjs [--stdio] +import { spawn } from 'node:child_process'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const pluginRoot = process.argv[2]; +if (!pluginRoot) throw new Error('lsp-launch: missing plugin root argument'); + +// Schema version is pinned in versions.json (same source Get-Schemas.ps1 extracts to). Fail loud +// if the schema dir is missing (e.g. a bad version bump or partial fetch): lemminx would otherwise +// launch and silently validate nothing. +const version = JSON.parse(readFileSync(join(pluginRoot, 'versions.json'), 'utf8')).schemaVersion; +const schemaDir = join(pluginRoot, 'schemas', version); +if (!existsSync(schemaDir)) throw new Error(`lsp-launch: schema dir not found: ${schemaDir}. Run scripts/Get-Schemas.ps1.`); + +// lemminx ships under a per-OS binary name (lemminx-win32.exe, lemminx-linux-x86_64, +// lemminx-osx-aarch_64, ...). Get-Lemminx.ps1 installs exactly one; discover it the same way +// rather than guessing the platform suffix here. +const binDir = join(pluginRoot, 'bin'); +const bins = readdirSync(binDir).filter((f) => f.startsWith('lemminx')); +if (bins.length !== 1) throw new Error(`lsp-launch: expected exactly one lemminx binary in ${binDir}, found ${bins.length}. Run scripts/Get-Lemminx.ps1.`); +const serverExe = join(binDir, bins[0]); + +// pattern -> XSD filename. Charts () AND forms () have no association on +// purpose: they are pac WRAPPER files whose root the XSD doesn't declare, so lemminx would +// false-positive on the wrapper root. The validator (Validate-DataverseXml.ps1) owns them, via +// per-fragment extraction (systemform/form, datadescription/datadefinition). +const assoc = { + '**/RibbonDiff.xml': 'RibbonCore.xsd', + '**/[Cc]ustomizations.xml': 'CustomizationsSolution.xsd', + '**/SiteMap*.xml': 'SiteMap.xsd', + '**/SavedQueries/**/*.xml': 'Fetch.xsd', + '**/*.fetchxml': 'Fetch.xsd', + '**/isv.config.xml': 'isv.config.xsd', +}; +// Every XSD an association points at must exist - a partial extraction would otherwise leave +// lemminx unable to load a schema with no visible error. +const missingXsds = [...new Set(Object.values(assoc))].filter((xsd) => !existsSync(join(schemaDir, xsd))); +if (missingXsds.length) throw new Error(`lsp-launch: missing XSD(s) in ${schemaDir}: ${missingXsds.join(', ')}. Run scripts/Get-Schemas.ps1.`); + +const xmlSettings = { + validation: { enabled: true, schema: { enabled: 'always' } }, + fileAssociations: Object.entries(assoc).map(([pattern, xsd]) => ({ + pattern, + systemId: pathToFileURL(join(schemaDir, xsd)).href, + })), +}; + +// --- LSP stdio framing --- +function writeMsg(stream, obj) { + const s = JSON.stringify(obj); + stream.write(`Content-Length: ${Buffer.byteLength(s)}\r\n\r\n${s}`); +} +function makeParser(onMsg) { + let buf = Buffer.alloc(0); + return (chunk) => { + buf = Buffer.concat([buf, chunk]); + for (;;) { + const he = buf.indexOf('\r\n\r\n'); + if (he === -1) break; + const m = buf.slice(0, he).toString('ascii').match(/Content-Length:\s*(\d+)/i); + const start = he + 4; + if (!m) { buf = buf.slice(start); continue; } + const len = parseInt(m[1], 10); + if (buf.length < start + len) break; + onMsg(JSON.parse(buf.slice(start, start + len).toString('utf8'))); + buf = buf.slice(start + len); + } + }; +} + +const server = spawn(serverExe, [], { stdio: ['pipe', 'pipe', 'inherit'] }); + +const fromClient = makeParser((msg) => { + if (msg.method === 'initialize') { + msg.params = msg.params || {}; + const io = msg.params.initializationOptions = msg.params.initializationOptions || {}; + io.settings = io.settings || {}; + io.settings.xml = xmlSettings; + } + if (msg.method === 'workspace/didChangeConfiguration') { + msg.params = msg.params || {}; + msg.params.settings = msg.params.settings || {}; + msg.params.settings.xml = xmlSettings; + } + writeMsg(server.stdin, msg); +}); + +const fromServer = makeParser((msg) => { + if (msg.method === 'workspace/configuration' && msg.id !== undefined) { + const result = (msg.params.items || []).map((it) => (!it.section || it.section === 'xml' ? xmlSettings : {})); + writeMsg(server.stdin, { jsonrpc: '2.0', id: msg.id, result }); + return; // consumed by the shim; never reaches the client + } + writeMsg(process.stdout, msg); +}); + +process.stdin.on('data', fromClient); +server.stdout.on('data', fromServer); +server.on('error', (err) => { console.error(`lsp-launch: failed to spawn ${serverExe}: ${err.message}`); process.exit(1); }); +// Exit on 'close' (fires after stdio has fully drained), not 'exit', so the last forwarded +// message is not truncated. +server.on('close', (c) => process.exit(c ?? 0)); diff --git a/plugins/dataverse-xml-lsp/scripts/lsp-smoke.mjs b/plugins/dataverse-xml-lsp/scripts/lsp-smoke.mjs new file mode 100644 index 0000000..ea432ad --- /dev/null +++ b/plugins/dataverse-xml-lsp/scripts/lsp-smoke.mjs @@ -0,0 +1,132 @@ +// End-to-end health check for the XML LSP. Launches the plugin exactly as .lsp.json does — via +// the launcher shim (scripts/lsp-launch.mjs), which spawns the lemminx binary and injects the +// xml.fileAssociations at runtime. Opens a ribbon fixture under a **/RibbonDiff.xml URI and +// asserts the schema actually fires: the valid fixture yields 0 diagnostics, the invalid one >= 1. +// Exit 0 = healthy, non-zero = broken install/schema/wiring. +// +// The client here supplies NO schema of its own and answers any workspace/configuration pull that +// reaches it with {} — so a firing schema can only have come from the shim. That both exercises +// the real launch path and proves the shim resolves the associations without any stamped config. +// +// It runs TWO scenarios so the check matches whichever config-delivery path the host uses: +// push - no `configuration` capability advertised; pulls left unanswered by the client. +// pull - `configuration` capability advertised; any pull reaching the client answered with {}. +// Both must pass. +// +// Usage: node scripts/lsp-smoke.mjs +import { spawn } from 'node:child_process'; +import { readFileSync } 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 shim = join(here, 'lsp-launch.mjs'); +const fixturesDir = join(pluginRoot, 'tests', 'fixtures'); + +// Ribbon is the fully-authoritative fragment (RibbonCore.xsd). The synthetic URI name RibbonDiff.xml +// makes the shim's **/RibbonDiff.xml association apply regardless of the fixture's real filename. +const cases = [ + { kind: 'valid', path: join(fixturesDir, 'valid', 'ribbon.xml') }, + { kind: 'invalid', path: join(fixturesDir, 'invalid', 'ribbon.xml') }, +]; + +// Run every case through a freshly-spawned shim under the given config-delivery mode. +// Returns the number of failures. +function runScenario(mode) { + const advertiseConfig = mode === 'pull'; + const server = spawn(process.execPath, [shim, pluginRoot, '--stdio']); + const diagnostics = new Map(); // uri -> diagnostics[] + const lastUpdate = new Map(); // uri -> timestamp of most recent publish + 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) => { + // The shim should intercept workspace/configuration; if one still reaches us, answer {} so the + // associations can only have come from the shim. + if (msg.method === 'workspace/configuration') { send({ id: msg.id, result: (msg.params.items || []).map(() => ({})) }); 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 || []); + lastUpdate.set(msg.params.uri, Date.now()); + } + }; + 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); + } + }); + + // lemminx publishes a transient "No grammar constraints" diagnostic the instant a doc opens, then + // RE-publishes after the injected fileAssociations settle (schema applied). We must read the + // settled result, not that first transient: wait until at least one publish has arrived AND no + // further publish has landed for `quietMs`. Fail loud if none ever arrives. + const waitSettled = (uri, quietMs = 1200, timeoutMs = 20000) => new Promise((res, rej) => { + const started = Date.now(); + const iv = setInterval(() => { + const seen = diagnostics.has(uri); + if (seen && Date.now() - lastUpdate.get(uri) >= quietMs) { clearInterval(iv); res(diagnostics.get(uri)); } + else if (Date.now() - started > timeoutMs) { + clearInterval(iv); + if (seen) res(diagnostics.get(uri)); else rej(new Error(`no diagnostics within ${timeoutMs}ms`)); + } + }, 25); + }); + + return (async () => { + const capabilities = advertiseConfig + ? { workspace: { configuration: true, didChangeConfiguration: {} } } + : { workspace: { didChangeConfiguration: {} } }; + // Deliberately supply NO schema: no initializationOptions.settings, empty didChangeConfiguration. + send({ id: 1, method: 'initialize', params: { processId: process.pid, rootUri: pathToFileURL(pluginRoot).href, capabilities } }); + await new Promise((r) => setTimeout(r, 500)); + send({ method: 'initialized', params: {} }); + send({ method: 'workspace/didChangeConfiguration', params: { settings: {} } }); + // Let the shim-injected config land before opening any doc. lemminx validates a doc at open + // time; if it opens before the fileAssociations are in effect it reports "No grammar + // constraints" and does not necessarily re-fire. A host configures the server, then opens files. + await new Promise((r) => setTimeout(r, 1500)); + + let failures = 0; + console.log(`\n[${mode}]`); + for (const c of cases) { + const text = readFileSync(c.path, 'utf8'); + // Synthetic URI whose FILENAME is exactly RibbonDiff.xml so the shim's **/RibbonDiff.xml + // association applies regardless of the fixture's real filename. The glob matches on the last + // path segment, so uniqueness comes from the parent dir (mode/kind), never the filename. + const uri = pathToFileURL(join(pluginRoot, 'src', mode, c.kind, 'RibbonDiff.xml')).href; + send({ method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'xml', version: 1, text } } }); + let diags; + try { diags = await waitSettled(uri); } + catch (e) { console.error(` ERROR ${c.kind}/ribbon.xml: ${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}/ribbon.xml -> ${diags.length} diagnostic(s)${ok ? '' : detail}`); + if (!ok) failures++; + } + server.kill(); + return failures; + })(); +} + +async function main() { + let failures = 0; + for (const mode of ['push', 'pull']) failures += await runScenario(mode); + if (failures) { console.error(`\nXML LSP smoke: ${failures} case(s) failed.`); process.exit(1); } + console.log('\nXML LSP smoke: all cases passed (push + pull).'); + process.exit(0); +} + +main().catch((e) => { console.error(e); process.exit(2); }); diff --git a/plugins/dataverse-xml-lsp/tests/Install.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Install.Tests.ps1 new file mode 100644 index 0000000..1ce2d4e --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/Install.Tests.ps1 @@ -0,0 +1,39 @@ +#requires -Version 7 +<# + Install-Plugin.ps1 is documented as idempotent (safe to re-run after /plugin update or a schema + version bump). This proves it: run it TWICE and require the second run to exit 0. + + The run exercises the real install (schema + lemminx resolution + self-check), so it is guarded + to SKIP cleanly when the environment can't support it without network work: + - node absent -> the shim/self-check can't launch, + - lemminx binary absent -> Get-Lemminx would download ~47 MB, + - schema set absent -> Get-Schemas would download Microsoft's Schemas.zip. + When all three are present the run is warm and network-free. +#> + +Describe 'Install-Plugin is idempotent (dataverse-xml-lsp)' { + + BeforeAll { + $script:pluginRoot = Split-Path $PSScriptRoot -Parent + $script:install = Join-Path $pluginRoot 'scripts' 'Install-Plugin.ps1' + $script:pwshExe = [Environment]::ProcessPath # this host is pwsh 7 (#requires); reuse it + + $binDir = Join-Path $pluginRoot 'bin' + $script:haveLemminx = (Test-Path $binDir) -and @(Get-ChildItem $binDir -Filter 'lemminx*' -ErrorAction SilentlyContinue).Count -ge 1 + $ver = (Get-Content (Join-Path $pluginRoot 'versions.json') -Raw | ConvertFrom-Json).schemaVersion + $schemaDir = Join-Path $pluginRoot 'schemas' $ver + $script:haveSchemas = (Test-Path $schemaDir) -and @(Get-ChildItem $schemaDir -Filter '*.xsd' -ErrorAction SilentlyContinue).Count -ge 1 + $script:haveNode = [bool](Get-Command node -ErrorAction SilentlyContinue) + } + + It 'exits 0 when run twice in a row' { + if (-not $haveNode) { Set-ItResult -Skipped -Because 'node is not on PATH'; return } + if (-not $haveLemminx) { Set-ItResult -Skipped -Because 'lemminx binary is not installed (bin/ empty); a run would download it'; return } + if (-not $haveSchemas) { Set-ItResult -Skipped -Because 'schema set is not present; a run would download Schemas.zip'; return } + + & $pwshExe -NoProfile -File $install *> $null + $LASTEXITCODE | Should -Be 0 -Because 'first install run should succeed' + $out = & $pwshExe -NoProfile -File $install 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "second (idempotent) run should also succeed. Output:`n$($out -join "`n")" + } +} diff --git a/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 new file mode 100644 index 0000000..c753460 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/LspAssociations.Tests.ps1 @@ -0,0 +1,119 @@ +#requires -Version 7 +<# + The XML LSP and the validator must not both own the same files. The validator owns pac's + WRAPPER files - (form definitions) and (charts) - because it extracts + and validates the inner fragment (systemform/form, datadescription/datadefinition). lemminx + cannot do that: pointed at a wrapper file it validates the WHOLE document against a schema whose + root is the inner element, so it false-positives on the wrapper root + (e.g. "cvc-elt.1.a: Cannot find the declaration of element 'forms'"). + + The design (docs/superpowers/specs) states the LSP is not associated to the wrapper roots the + validator owns. This reads the association map straight out of the shim (scripts/lsp-launch.mjs) + and asserts no association glob targets a forms or chart wrapper file. +#> + +Describe 'XML LSP is not associated to validator-owned wrapper roots' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $shim = Join-Path $pluginRoot 'scripts' 'lsp-launch.mjs' + $raw = Get-Content $shim -Raw + + # Pull `const assoc = { 'glob': 'File.xsd', ... };` out of the shim and parse the pairs. + $block = [regex]::Match($raw, 'const\s+assoc\s*=\s*\{(.*?)\};', 'Singleline') + $script:assoc = @{} + foreach ($m in [regex]::Matches($block.Groups[1].Value, "'([^']+)'\s*:\s*'([^']+)'")) { + $script:assoc[$m.Groups[1].Value] = $m.Groups[2].Value + } + + # Minimal glob -> regex (supports **, *, and character classes like [Cc]); paths use '/'. + function Test-GlobMatch([string]$Glob, [string]$Path) { + $rx = [regex]::Escape($Glob) + $rx = $rx -replace '\\\*\\\*/', '(?:.*/)?' # **/ -> any (or no) leading dirs + $rx = $rx -replace '\\\*\\\*', '.*' # ** -> anything + $rx = $rx -replace '\\\*', '[^/]*' # * -> anything but a separator + $rx = $rx -replace '\\\[', '[' + $rx = $rx -replace '\\\]', ']' + return [regex]::IsMatch($Path, "^$rx$") + } + $script:matcher = ${function:Test-GlobMatch} + + # Representative pac-unpacked wrapper file paths (root / ). + $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' + } + + It 'parses a non-empty association map from the shim' { + $assoc.Count | Should -BeGreaterThan 0 + } + + It 'does not associate the visualization/chart wrapper (validator-owned)' { + # Charts are validator-only: no association may point at the chart schema. + $assoc.Values | Should -Not -Contain 'VisualizationDataDescription.xsd' + } + + It 'does not associate pac forms wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $assoc.Keys) { + if (& $matcher $glob $formsWrapper) { $hit += "$glob -> $($assoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "no LSP association may match a pac wrapper file ($formsWrapper); lemminx would false-positive on the root. Matched: $($hit -join ', ')" + } +} + +Describe 'VS Code association map is not associated to validator-owned wrapper roots' { + # The RedHat XML extension is a SEPARATE consumer wired by Set-LspSchemaPaths.ps1 -UpdateVSCode. + # It must exclude the same forms/chart wrapper roots the shim does, or -UpdateVSCode would + # re-introduce the / false-positive lemminx avoids. + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $stampScript = Join-Path $pluginRoot 'scripts' 'Set-LspSchemaPaths.ps1' + + # Parse the `$assoc = [ordered]@{ ... }` hashtable straight out of the script via the AST + # (robust to formatting), then read the literal glob -> XSD string pairs. + $ast = [System.Management.Automation.Language.Parser]::ParseFile($stampScript, [ref]$null, [ref]$null) + $assign = $ast.Find({ + param($n) + $n -is [System.Management.Automation.Language.AssignmentStatementAst] -and + $n.Left.Extent.Text -eq '$assoc' + }, $true) + $hashAst = $assign.Right.Find({ + param($n) $n -is [System.Management.Automation.Language.HashtableAst] + }, $true) + $script:vsAssoc = @{} + foreach ($pair in $hashAst.KeyValuePairs) { + $k = $pair.Item1.Extent.Text.Trim("'`"") + $v = $pair.Item2.Extent.Text.Trim("'`"") + $script:vsAssoc[$k] = $v + } + + # Minimal glob -> regex (supports **, *, and character classes like [Cc]); paths use '/'. + function Test-GlobMatch([string]$Glob, [string]$Path) { + $rx = [regex]::Escape($Glob) + $rx = $rx -replace '\\\*\\\*/', '(?:.*/)?' + $rx = $rx -replace '\\\*\\\*', '.*' + $rx = $rx -replace '\\\*', '[^/]*' + $rx = $rx -replace '\\\[', '[' + $rx = $rx -replace '\\\]', ']' + return [regex]::IsMatch($Path, "^$rx$") + } + $script:matcher = ${function:Test-GlobMatch} + $script:formsWrapper = 'src/Entities/account/FormXml/main/00000000-0000-0000-0000-000000000000.xml' + } + + It 'parses a non-empty association map from Set-LspSchemaPaths.ps1' { + $vsAssoc.Count | Should -BeGreaterThan 0 + } + + It 'does not associate the visualization/chart wrapper (validator-owned)' { + $vsAssoc.Values | Should -Not -Contain 'VisualizationDataDescription.xsd' + } + + It 'does not associate pac forms wrapper files (validator-owned)' { + $hit = @() + foreach ($glob in $vsAssoc.Keys) { + if (& $matcher $glob $formsWrapper) { $hit += "$glob -> $($vsAssoc[$glob])" } + } + $hit | Should -BeNullOrEmpty -Because "no VS Code association may match a pac wrapper file ($formsWrapper); the RedHat XML extension would false-positive on the root. Matched: $($hit -join ', ')" + } +} diff --git a/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 new file mode 100644 index 0000000..5e94479 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 @@ -0,0 +1,42 @@ +#requires -Version 7 +<# + Guards that .lsp.json is portable: it launches the launcher shim (scripts/lsp-launch.mjs) via + node with ${CLAUDE_PLUGIN_ROOT} args and carries NO machine-local absolute path or per-OS + lemminx binary name. The shim discovers the binary and resolves the XSD systemIds at launch, so + nothing is stamped per machine. (The XSD associations themselves are exercised end-to-end by + the validator and its fixtures in Validate-DataverseXml.Tests.ps1.) +#> + +Describe 'XML LSP config is portable (.lsp.json launches the shim)' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $script:lspPath = Join-Path $pluginRoot '.lsp.json' + $script:raw = Get-Content $script:lspPath -Raw + $script:lsp = $script:raw | ConvertFrom-Json + } + + It 'launches the shim via node with ${CLAUDE_PLUGIN_ROOT} args' { + $lsp.xml.command | Should -Be 'node' + $argstr = $lsp.xml.args -join ' ' + $argstr | Should -Match 'lsp-launch\.mjs' + $argstr | Should -Match '\$\{CLAUDE_PLUGIN_ROOT\}' + $argstr | Should -Match '--stdio' + } + + It 'keeps the extension-to-language wiring' { + $lsp.xml.extensionToLanguage.'.xml' | Should -Be 'xml' + $lsp.xml.extensionToLanguage.'.fetchxml' | Should -Be 'xml' + } + + It 'contains no machine-local absolute path or lemminx binary name' { + # No stamped file URI, no bare absolute path (Windows drive or POSIX root) outside the + # ${CLAUDE_PLUGIN_ROOT} variable, and no per-OS lemminx binary name. All resolved at launch. + $raw | Should -Not -Match 'file://' + $raw | Should -Not -Match '[A-Za-z]:[\\/]' + $raw | Should -Not -Match 'lemminx' + foreach ($a in $lsp.xml.args) { + if ($a -notmatch '\$\{CLAUDE_PLUGIN_ROOT\}') { $a | Should -Not -Match '^([A-Za-z]:[\\/]|/)' } + } + } +} diff --git a/plugins/dataverse-xml-lsp/tests/LspSmoke.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspSmoke.Tests.ps1 new file mode 100644 index 0000000..45dda75 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/LspSmoke.Tests.ps1 @@ -0,0 +1,30 @@ +#requires -Version 7 +<# + Drives the XML LSP end-to-end through the launcher shim (scripts/lsp-launch.mjs) via node, the + same path Claude Code uses. scripts/lsp-smoke.mjs acts as an LSP client that supplies NO schema + of its own (and answers any workspace/configuration pull with {}), opens the ribbon fixtures, + and asserts the shim-injected fileAssociations fire: valid ribbon -> 0 diagnostics, invalid -> + >= 1. This is the only test that exercises node + the shim + real lemminx together (the + validator self-check bypasses them). + + Guarded to SKIP when node or the lemminx binary is absent, so a validator-only CI stays green. +#> + +Describe 'XML LSP smoke (node + shim + lemminx)' { + + BeforeAll { + $script:pluginRoot = Split-Path $PSScriptRoot -Parent + $script:smoke = Join-Path $pluginRoot 'scripts' 'lsp-smoke.mjs' + $binDir = Join-Path $pluginRoot 'bin' + $script:haveLemminx = (Test-Path $binDir) -and @(Get-ChildItem $binDir -Filter 'lemminx*' -ErrorAction SilentlyContinue).Count -ge 1 + $script:haveNode = [bool](Get-Command node -ErrorAction SilentlyContinue) + } + + It 'valid ribbon -> 0 diagnostics, invalid -> >= 1 (schema fires via the shim)' { + if (-not $haveNode) { Set-ItResult -Skipped -Because 'node is not on PATH'; return } + if (-not $haveLemminx) { Set-ItResult -Skipped -Because 'lemminx binary is not installed (bin/ empty)'; return } + + $out = & node $smoke 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "the shim-driven smoke should pass. Output:`n$($out -join "`n")" + } +} diff --git a/plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1 new file mode 100644 index 0000000..0bdcc50 --- /dev/null +++ b/plugins/dataverse-xml-lsp/tests/SetupSkill.Tests.ps1 @@ -0,0 +1,46 @@ +#requires -Version 7 +<# + Locks the setup skill's no-auto-trigger behavior. The root SKILL.md must: + - parse (have a YAML frontmatter block between the first two --- lines), + - carry name: dataverse-xml-lsp-setup, + - carry NO description: key (a description would let Claude auto-invoke the skill; setup is + run once by hand, never triggered from context). +#> + +Describe 'setup skill frontmatter (dataverse-xml-lsp)' { + + BeforeAll { + $pluginRoot = Split-Path $PSScriptRoot -Parent + $script:skillPath = Join-Path $pluginRoot 'SKILL.md' + $script:lines = Get-Content $script:skillPath + + # Parse the YAML frontmatter: the block between the first two --- lines. Fail loud if the + # file has no such block (a bare key/value list is all these skills carry). + $fence = @() + for ($i = 0; $i -lt $script:lines.Count; $i++) { + if ($script:lines[$i].Trim() -eq '---') { $fence += $i } + if ($fence.Count -eq 2) { break } + } + $script:frontmatter = @{} + if ($fence.Count -eq 2) { + foreach ($line in $script:lines[($fence[0] + 1)..($fence[1] - 1)]) { + if ($line -match '^\s*([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$') { + $script:frontmatter[$Matches[1]] = $Matches[2] + } + } + } + $script:fenceCount = $fence.Count + } + + It 'has a parseable YAML frontmatter block' { + $fenceCount | Should -Be 2 -Because 'SKILL.md must open with a --- ... --- frontmatter block' + } + + It 'name is dataverse-xml-lsp-setup' { + $frontmatter['name'] | Should -Be 'dataverse-xml-lsp-setup' + } + + It 'has NO description key (no auto-trigger)' { + $frontmatter.ContainsKey('description') | Should -BeFalse -Because 'a description would let the skill auto-trigger; setup is run by hand' + } +} diff --git a/plugins/dataverse-customization-xml/tests/Validate-DataverseXml.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/tests/Validate-DataverseXml.Tests.ps1 rename to plugins/dataverse-xml-lsp/tests/Validate-DataverseXml.Tests.ps1 diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/datadefinition.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/datadefinition.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/datadefinition.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/datadefinition.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/fetch.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/fetch.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/fetch.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/fetch.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/form.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/form.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/form.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/form.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/forms-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/forms-wrapper.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/forms-wrapper.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/forms-wrapper.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/importexport.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/importexport.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/importexport.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/importexport.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/isvconfig.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/isvconfig.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/isvconfig.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/isvconfig.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/parameterxml.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/parameterxml.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/parameterxml.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/parameterxml.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/ribbon.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/ribbon.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/ribbon.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/ribbon.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/savedquery.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedquery.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/savedquery.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/savedquery.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/sitemap.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/sitemap.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/sitemap.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/sitemap.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/invalid/viewers.xml b/plugins/dataverse-xml-lsp/tests/fixtures/invalid/viewers.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/invalid/viewers.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/invalid/viewers.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/datadefinition.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/datadefinition.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/datadefinition.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/datadefinition.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/fetch.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/fetch.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/fetch.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/fetch.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/form.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/form.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/form.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/form.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/forms-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/forms-wrapper.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/forms-wrapper.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/forms-wrapper.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/importexport.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/importexport.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/importexport.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/importexport.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/isvconfig.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/isvconfig.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/isvconfig.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/isvconfig.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/parameterxml.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/parameterxml.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/parameterxml.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/parameterxml.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/ribbon.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/ribbon.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/ribbon.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/ribbon.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/savedquery.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/savedquery.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/savedquery.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/savedquery.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/sitemap.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/sitemap.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/sitemap.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/sitemap.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/viewers.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/viewers.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/viewers.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/viewers.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/visualization-escaped.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/visualization-escaped.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/visualization-escaped.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/visualization-escaped.xml diff --git a/plugins/dataverse-customization-xml/tests/fixtures/valid/visualization-wrapper.xml b/plugins/dataverse-xml-lsp/tests/fixtures/valid/visualization-wrapper.xml similarity index 100% rename from plugins/dataverse-customization-xml/tests/fixtures/valid/visualization-wrapper.xml rename to plugins/dataverse-xml-lsp/tests/fixtures/valid/visualization-wrapper.xml diff --git a/plugins/dataverse-customization-xml/versions.json b/plugins/dataverse-xml-lsp/versions.json similarity index 100% rename from plugins/dataverse-customization-xml/versions.json rename to plugins/dataverse-xml-lsp/versions.json diff --git a/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json b/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json deleted file mode 100644 index a7eb77f..0000000 --- a/plugins/power-automate-cloud-flow/.claude-plugin/plugin.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$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/.lsp.json b/plugins/power-automate-cloud-flow/.lsp.json deleted file mode 100644 index 6fbb9e3..0000000 --- a/plugins/power-automate-cloud-flow/.lsp.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "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/commands/setup.md b/plugins/power-automate-cloud-flow/commands/setup.md deleted file mode 100644 index aa64ff6..0000000 --- a/plugins/power-automate-cloud-flow/commands/setup.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -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/scripts/Set-LspSchemaPaths.ps1 b/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 deleted file mode 100644 index 35bf00c..0000000 --- a/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -#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" -} -# 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 } - 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/tests/LspConfig.Tests.ps1 b/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 deleted file mode 100644 index 4c4f68f..0000000 --- a/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 +++ /dev/null @@ -1,99 +0,0 @@ -#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" } - } -}