From 8ba38b3d0cb96c16ffb2b40625d21d8ecb49f748 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 11:48:10 +1200 Subject: [PATCH 01/14] Add retrospective design spec for power-automate-cloud-flow plugin --- ...power-automate-cloud-flow-plugin-design.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md diff --git a/docs/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md b/docs/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md new file mode 100644 index 0000000..b8ffc66 --- /dev/null +++ b/docs/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md @@ -0,0 +1,141 @@ +# power-automate-cloud-flow — plugin design (retrospective) + +**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. From e2a1aade18f3d927f343ba07147b143915e3fe07 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 12:00:57 +1200 Subject: [PATCH 02/14] Add design spec for LSP plugin rename + no-restamp restructure Reframe both marketplace plugins as LSP + tiny no-description setup skill, rename dataverse-customization-xml -> dataverse-xml-lsp and power-automate-cloud-flow -> cloud-flow-json-lsp, eliminate the per-machine schema-path re-stamp (launcher shim, hosted-URL fallback), move guidance to docs/guide.md, and bump both to 2.0.0 (breaking rename). The XML plugin keeps its existing CLI validator as the non-LSP/CI/wrapper-file channel (no hook, no LSP overlap); a flow validator and any hook are deferred. --- ...026-07-21-lsp-only-plugin-rename-design.md | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md 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..422ae7c --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-lsp-only-plugin-rename-design.md @@ -0,0 +1,210 @@ +# 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/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/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). From edf2e8dac312f9fbf16c237b0b083e2a5ace174e Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 13:20:09 +1200 Subject: [PATCH 03/14] Adopt docs/superpowers layout for specs and plans Move the existing design specs from docs/specs/ into docs/superpowers/specs/ (matching where the LSP-restructure spec and plan live), and update the llms.txt link and the plan/spec cross-references to the new paths. --- ...026-07-21-lsp-plugin-rename-restructure.md | 366 ++++++++++++++++++ ...sp-plugin-rename-restructure.md.tasks.json | 54 +++ ...-15-dataverse-agent-plugins-repo-design.md | 0 ...026-07-21-lsp-only-plugin-rename-design.md | 4 +- ...power-automate-cloud-flow-plugin-design.md | 0 llms.txt | 2 +- 6 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md create mode 100644 docs/superpowers/plans/2026-07-21-lsp-plugin-rename-restructure.md.tasks.json rename docs/{ => superpowers}/specs/2026-07-15-dataverse-agent-plugins-repo-design.md (100%) rename docs/{ => superpowers}/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md (100%) 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..99e4cc9 --- /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": "pending", + "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": "pending", + "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": "pending", + "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": "pending", + "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": "pending", + "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": "pending", + "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": "pending", + "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-21T12:00: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 100% 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 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 index 422ae7c..de4f8f2 100644 --- 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 @@ -56,7 +56,7 @@ path stamping to repeat on update, and no loss of the XML validator's unique cap (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/specs/`:** old names updated throughout. +- **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`. @@ -93,7 +93,7 @@ plugins/dataverse-xml-lsp/ plugins/cloud-flow-json-lsp/ 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/specs/*.md` (old names updated throughout), and each plugin's `README.md` + +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. diff --git a/docs/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 similarity index 100% rename from docs/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md rename to docs/superpowers/specs/2026-07-21-power-automate-cloud-flow-plugin-design.md diff --git a/llms.txt b/llms.txt index 885cdb2..5512ee7 100644 --- a/llms.txt +++ b/llms.txt @@ -30,5 +30,5 @@ ## 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) From faea9f7f0ad78ca536273c3474550d294e70233c Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 13:26:37 +1200 Subject: [PATCH 04/14] Record launcher-shim spike result (Option 1 vs 2 decision) --- ...026-07-21-lsp-only-plugin-rename-design.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) 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 index de4f8f2..1156d64 100644 --- 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 @@ -208,3 +208,55 @@ Parked for a later, additive change once the LSP core is solid: - 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. From 8029d9091b66902648981464361be3167a13c607 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 13:37:13 +1200 Subject: [PATCH 05/14] Rename plugins to dataverse-xml-lsp and cloud-flow-json-lsp (2.0.0) Rename both marketplace plugins to lead with their LSP identity: dataverse-customization-xml becomes dataverse-xml-lsp and power-automate-cloud-flow becomes cloud-flow-json-lsp. Move the plugin directories (history preserved), bump both plugin.json to 2.0.0 with LSP-first descriptions, and update marketplace.json name, source and descriptions. Update the flow package.json/package-lock.json name and the flow schema $id. Update CI cache paths/keys and every doc path, install/update command, and slug reference across the repo READMEs, llms.txt, and per-plugin docs. Drop the per-plugin commands/setup.md slash command; docs now point at the setup-skill invocation. --- .claude-plugin/marketplace.json | 12 +++---- .github/workflows/ci.yml | 12 +++---- README.md | 12 +++---- llms.txt | 34 +++++++++---------- .../.claude-plugin/plugin.json | 13 +++++++ .../.gitignore | 0 .../.lsp.json | 0 .../README.md | 11 +++--- .../SKILL.md | 6 ++-- .../docs/codex.md | 6 ++-- .../docs/debugging.md | 6 ++-- .../docs/vscode.md | 4 +-- .../package-lock.json | 4 +-- .../package.json | 2 +- .../schemas/SOURCE.md | 0 .../schemas/cloud-flow-clientdata.schema.json | 2 +- .../scripts/Install-JsonLanguageServer.ps1 | 0 .../scripts/Install-Plugin.ps1 | 0 .../scripts/Set-LspSchemaPaths.ps1 | 0 .../scripts/lsp-smoke.mjs | 0 .../tests/LspConfig.Tests.ps1 | 0 .../fixtures/invalid/bad-runafter-status.json | 0 .../fixtures/invalid/missing-definition.json | 0 .../tests/fixtures/valid/simple-flow.json | 0 .../.claude-plugin/plugin.json | 13 ------- .../commands/setup.md | 14 -------- .../.claude-plugin/plugin.json | 13 +++++++ .../.gitignore | 0 .../.lsp.json | 0 .../README.md | 11 +++--- .../SKILL.md | 6 ++-- .../docs/codex.md | 8 ++--- .../docs/debugging.md | 6 ++-- .../docs/vscode.md | 4 +-- .../schemas/SOURCE.md | 0 .../scripts/Get-Lemminx.ps1 | 0 .../scripts/Get-Schemas.ps1 | 0 .../scripts/Install-Plugin.ps1 | 0 .../scripts/Set-LspSchemaPaths.ps1 | 0 .../scripts/Validate-DataverseXml.ps1 | 0 .../tests/LspConfig.Tests.ps1 | 0 .../tests/Validate-DataverseXml.Tests.ps1 | 0 .../tests/fixtures/invalid/datadefinition.xml | 0 .../tests/fixtures/invalid/fetch.xml | 0 .../tests/fixtures/invalid/form.xml | 0 .../tests/fixtures/invalid/forms-wrapper.xml | 0 .../tests/fixtures/invalid/importexport.xml | 0 .../tests/fixtures/invalid/isvconfig.xml | 0 .../tests/fixtures/invalid/parameterxml.xml | 0 .../tests/fixtures/invalid/ribbon.xml | 0 .../tests/fixtures/invalid/savedquery.xml | 0 .../tests/fixtures/invalid/sitemap.xml | 0 .../tests/fixtures/invalid/viewers.xml | 0 .../tests/fixtures/valid/datadefinition.xml | 0 .../tests/fixtures/valid/fetch.xml | 0 .../tests/fixtures/valid/form.xml | 0 .../tests/fixtures/valid/forms-wrapper.xml | 0 .../tests/fixtures/valid/importexport.xml | 0 .../tests/fixtures/valid/isvconfig.xml | 0 .../tests/fixtures/valid/parameterxml.xml | 0 .../tests/fixtures/valid/ribbon.xml | 0 .../tests/fixtures/valid/savedquery.xml | 0 .../tests/fixtures/valid/sitemap.xml | 0 .../tests/fixtures/valid/viewers.xml | 0 .../fixtures/valid/visualization-escaped.xml | 0 .../fixtures/valid/visualization-wrapper.xml | 0 .../versions.json | 0 .../.claude-plugin/plugin.json | 13 ------- .../commands/setup.md | 14 -------- 69 files changed, 98 insertions(+), 128 deletions(-) create mode 100644 plugins/cloud-flow-json-lsp/.claude-plugin/plugin.json rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/.gitignore (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/.lsp.json (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/README.md (93%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/SKILL.md (96%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/docs/codex.md (86%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/docs/debugging.md (91%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/docs/vscode.md (87%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/package-lock.json (99%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/package.json (83%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/schemas/SOURCE.md (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/schemas/cloud-flow-clientdata.schema.json (98%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/scripts/Install-JsonLanguageServer.ps1 (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/scripts/Install-Plugin.ps1 (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/scripts/Set-LspSchemaPaths.ps1 (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/scripts/lsp-smoke.mjs (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/tests/LspConfig.Tests.ps1 (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/tests/fixtures/invalid/bad-runafter-status.json (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/tests/fixtures/invalid/missing-definition.json (100%) rename plugins/{power-automate-cloud-flow => cloud-flow-json-lsp}/tests/fixtures/valid/simple-flow.json (100%) delete mode 100644 plugins/dataverse-customization-xml/.claude-plugin/plugin.json delete mode 100644 plugins/dataverse-customization-xml/commands/setup.md create mode 100644 plugins/dataverse-xml-lsp/.claude-plugin/plugin.json rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/.gitignore (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/.lsp.json (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/README.md (93%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/SKILL.md (97%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/docs/codex.md (70%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/docs/debugging.md (92%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/docs/vscode.md (92%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/schemas/SOURCE.md (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/scripts/Get-Lemminx.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/scripts/Get-Schemas.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/scripts/Install-Plugin.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/scripts/Set-LspSchemaPaths.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/scripts/Validate-DataverseXml.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/LspConfig.Tests.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/Validate-DataverseXml.Tests.ps1 (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/datadefinition.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/fetch.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/form.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/forms-wrapper.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/importexport.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/isvconfig.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/parameterxml.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/ribbon.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/savedquery.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/sitemap.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/invalid/viewers.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/datadefinition.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/fetch.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/form.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/forms-wrapper.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/importexport.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/isvconfig.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/parameterxml.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/ribbon.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/savedquery.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/sitemap.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/viewers.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/visualization-escaped.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/tests/fixtures/valid/visualization-wrapper.xml (100%) rename plugins/{dataverse-customization-xml => dataverse-xml-lsp}/versions.json (100%) delete mode 100644 plugins/power-automate-cloud-flow/.claude-plugin/plugin.json delete mode 100644 plugins/power-automate-cloud-flow/commands/setup.md 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..fa53b54 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 + 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 + 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/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/llms.txt b/llms.txt index 5512ee7..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,24 +9,24 @@ ## 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 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/power-automate-cloud-flow/.lsp.json b/plugins/cloud-flow-json-lsp/.lsp.json similarity index 100% rename from plugins/power-automate-cloud-flow/.lsp.json rename to plugins/cloud-flow-json-lsp/.lsp.json diff --git a/plugins/power-automate-cloud-flow/README.md b/plugins/cloud-flow-json-lsp/README.md similarity index 93% rename from plugins/power-automate-cloud-flow/README.md rename to plugins/cloud-flow-json-lsp/README.md index f4991b2..c5b1f59 100644 --- a/plugins/power-automate-cloud-flow/README.md +++ b/plugins/cloud-flow-json-lsp/README.md @@ -1,4 +1,4 @@ -# 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 @@ -23,8 +23,8 @@ 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 @@ -50,7 +50,6 @@ can leave a running server pointing at a broken relative schema path. | Path | Purpose | |------|---------| | `SKILL.md` | The skill: the shape-vs-semantics split, what attaches, gotchas | -| `commands/setup.md` | The `/power-automate-cloud-flow:setup` slash command | | `scripts/Install-Plugin.ps1` | One-shot setup: server install + path stamping + self-check | | `scripts/Install-JsonLanguageServer.ps1` | `npm ci` the pinned JSON language server into `node_modules/` | | `scripts/Set-LspSchemaPaths.ps1` | Stamp this machine's absolute schema `file://` URI into `.lsp.json` (+ VS Code) | @@ -69,8 +68,8 @@ 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 diff --git a/plugins/power-automate-cloud-flow/SKILL.md b/plugins/cloud-flow-json-lsp/SKILL.md similarity index 96% rename from plugins/power-automate-cloud-flow/SKILL.md rename to plugins/cloud-flow-json-lsp/SKILL.md index a1023e3..e027efa 100644 --- a/plugins/power-automate-cloud-flow/SKILL.md +++ b/plugins/cloud-flow-json-lsp/SKILL.md @@ -1,5 +1,5 @@ --- -name: power-automate-cloud-flow +name: cloud-flow-json-lsp 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 + @@ -32,7 +32,7 @@ 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 @@ -69,7 +69,7 @@ Get-Content .json -Raw | Test-Json -SchemaFile "${CLAUDE_PLUGIN_ROOT}/sche `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 + (or after a plugin update / move), re-run `/cloud-flow-json-lsp:cloud-flow-json-lsp-setup` — or `scripts/Set-LspSchemaPaths.ps1` alone — to re-stamp it. - **The JSON server validates a document only after answering its `workspace/configuration` pull.** Claude Code and VS Code both handle that; a bare LSP client must too (see `scripts/lsp-smoke.mjs` diff --git a/plugins/power-automate-cloud-flow/docs/codex.md b/plugins/cloud-flow-json-lsp/docs/codex.md similarity index 86% rename from plugins/power-automate-cloud-flow/docs/codex.md rename to plugins/cloud-flow-json-lsp/docs/codex.md index 9cec50c..40a15f2 100644 --- a/plugins/power-automate-cloud-flow/docs/codex.md +++ b/plugins/cloud-flow-json-lsp/docs/codex.md @@ -13,13 +13,13 @@ 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/SKILL.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/` +3. If your Codex setup supports skill folders, point it at `plugins/cloud-flow-json-lsp/` directly — `SKILL.md` has standard frontmatter. ## The loop diff --git a/plugins/power-automate-cloud-flow/docs/debugging.md b/plugins/cloud-flow-json-lsp/docs/debugging.md similarity index 91% rename from plugins/power-automate-cloud-flow/docs/debugging.md rename to plugins/cloud-flow-json-lsp/docs/debugging.md index a2de879..95fd4dc 100644 --- a/plugins/power-automate-cloud-flow/docs/debugging.md +++ b/plugins/cloud-flow-json-lsp/docs/debugging.md @@ -25,15 +25,15 @@ 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` +## After `/plugin update cloud-flow-json-lsp@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 +`/cloud-flow-json-lsp:cloud-flow-json-lsp-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 +`git update-index --skip-worktree plugins/cloud-flow-json-lsp/.lsp.json` keeps it out of your commits. ## No diagnostics in VS Code diff --git a/plugins/power-automate-cloud-flow/docs/vscode.md b/plugins/cloud-flow-json-lsp/docs/vscode.md similarity index 87% rename from plugins/power-automate-cloud-flow/docs/vscode.md rename to plugins/cloud-flow-json-lsp/docs/vscode.md index 392e382..983a5e7 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 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 100% rename from plugins/power-automate-cloud-flow/scripts/Install-Plugin.ps1 rename to plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 diff --git a/plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 b/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 similarity index 100% rename from plugins/power-automate-cloud-flow/scripts/Set-LspSchemaPaths.ps1 rename to plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 diff --git a/plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs b/plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs similarity index 100% rename from plugins/power-automate-cloud-flow/scripts/lsp-smoke.mjs rename to plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs diff --git a/plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 similarity index 100% rename from plugins/power-automate-cloud-flow/tests/LspConfig.Tests.ps1 rename to plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 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/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-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-customization-xml/.lsp.json b/plugins/dataverse-xml-lsp/.lsp.json similarity index 100% rename from plugins/dataverse-customization-xml/.lsp.json rename to plugins/dataverse-xml-lsp/.lsp.json diff --git a/plugins/dataverse-customization-xml/README.md b/plugins/dataverse-xml-lsp/README.md similarity index 93% rename from plugins/dataverse-customization-xml/README.md rename to plugins/dataverse-xml-lsp/README.md index 8f73c65..4f261fc 100644 --- a/plugins/dataverse-customization-xml/README.md +++ b/plugins/dataverse-xml-lsp/README.md @@ -1,4 +1,4 @@ -# 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 @@ -16,8 +16,8 @@ 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 @@ -46,7 +46,6 @@ can leave a running lemminx pointing at broken relative schema paths. | 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 | | `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//` | @@ -66,8 +65,8 @@ 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 diff --git a/plugins/dataverse-customization-xml/SKILL.md b/plugins/dataverse-xml-lsp/SKILL.md similarity index 97% rename from plugins/dataverse-customization-xml/SKILL.md rename to plugins/dataverse-xml-lsp/SKILL.md index 1891056..b963e03 100644 --- a/plugins/dataverse-customization-xml/SKILL.md +++ b/plugins/dataverse-xml-lsp/SKILL.md @@ -1,5 +1,5 @@ --- -name: dataverse-customization-xml +name: dataverse-xml-lsp 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 @@ -15,7 +15,7 @@ description: >- 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. @@ -131,7 +131,7 @@ pack/import mechanics. 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 + machine (or after a plugin update), run `/dataverse-xml-lsp:dataverse-xml-lsp-setup` — or `scripts/Set-LspSchemaPaths.ps1` alone — to re-stamp them (and VS Code settings). - 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 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..a2da0b4 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/SKILL.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/` directly — SKILL.md has standard frontmatter. ## The loop diff --git a/plugins/dataverse-customization-xml/docs/debugging.md b/plugins/dataverse-xml-lsp/docs/debugging.md similarity index 92% rename from plugins/dataverse-customization-xml/docs/debugging.md rename to plugins/dataverse-xml-lsp/docs/debugging.md index 7d303d5..29e6e55 100644 --- a/plugins/dataverse-customization-xml/docs/debugging.md +++ b/plugins/dataverse-xml-lsp/docs/debugging.md @@ -22,15 +22,15 @@ 5. 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` +## After `/plugin update dataverse-xml-lsp@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 +Re-running `/dataverse-xml-lsp:dataverse-xml-lsp-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` +commit the machine paths. `git update-index --skip-worktree plugins/dataverse-xml-lsp/.lsp.json` is one way to keep it out of your commits. ## No diagnostics in VS Code diff --git a/plugins/dataverse-customization-xml/docs/vscode.md b/plugins/dataverse-xml-lsp/docs/vscode.md similarity index 92% rename from plugins/dataverse-customization-xml/docs/vscode.md rename to plugins/dataverse-xml-lsp/docs/vscode.md index d354cc3..cc3a0f9 100644 --- a/plugins/dataverse-customization-xml/docs/vscode.md +++ b/plugins/dataverse-xml-lsp/docs/vscode.md @@ -9,7 +9,7 @@ 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 ``` This fetches the Microsoft XSDs and writes `xml.fileAssociations` (absolute paths) into @@ -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 diff --git a/plugins/dataverse-customization-xml/schemas/SOURCE.md b/plugins/dataverse-xml-lsp/schemas/SOURCE.md similarity index 100% rename from plugins/dataverse-customization-xml/schemas/SOURCE.md rename to plugins/dataverse-xml-lsp/schemas/SOURCE.md diff --git a/plugins/dataverse-customization-xml/scripts/Get-Lemminx.ps1 b/plugins/dataverse-xml-lsp/scripts/Get-Lemminx.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/scripts/Get-Lemminx.ps1 rename to plugins/dataverse-xml-lsp/scripts/Get-Lemminx.ps1 diff --git a/plugins/dataverse-customization-xml/scripts/Get-Schemas.ps1 b/plugins/dataverse-xml-lsp/scripts/Get-Schemas.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/scripts/Get-Schemas.ps1 rename to plugins/dataverse-xml-lsp/scripts/Get-Schemas.ps1 diff --git a/plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 b/plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/scripts/Install-Plugin.ps1 rename to plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 diff --git a/plugins/dataverse-customization-xml/scripts/Set-LspSchemaPaths.ps1 b/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/scripts/Set-LspSchemaPaths.ps1 rename to plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 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-customization-xml/tests/LspConfig.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 similarity index 100% rename from plugins/dataverse-customization-xml/tests/LspConfig.Tests.ps1 rename to plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 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/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. From b8b46d219a43a27b90826d2eba05bd7a9de27112 Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 13:51:39 +1200 Subject: [PATCH 06/14] Resolve LSP schema at launch via shim; drop per-machine stamping Both plugins now launch their language server through a small node launcher shim (scripts/lsp-launch.mjs) referenced from .lsp.json via ${CLAUDE_PLUGIN_ROOT}, which Claude Code substitutes in command/args. The shim spawns the real server (vscode-json-language-server / the platform-specific lemminx binary), proxies LSP stdio, and injects the bundled schema association at runtime - computing an absolute file:// path from the passed plugin root. The association is injected in three places (forwarded initialize, every workspace/didChangeConfiguration, and by answering the server's workspace/configuration pull) so an empty client config push cannot clear it. The committed .lsp.json for both plugins is now fully portable: no machine-local absolute path, no per-OS lemminx binary name. Install-Plugin.ps1 no longer stamps .lsp.json; the heavy install and self-check stay. Set-LspSchemaPaths.ps1 is reduced to its VS Code editor-association branch (a separate consumer). LspConfig tests now assert the portable shim shape and that no machine path is present; the JSON self-check (lsp-smoke.mjs) drives the shim itself. --- plugins/cloud-flow-json-lsp/.lsp.json | 43 +---- .../scripts/Install-Plugin.ps1 | 16 +- .../scripts/Set-LspSchemaPaths.ps1 | 100 +++++------- .../scripts/lsp-launch.mjs | 85 ++++++++++ .../cloud-flow-json-lsp/scripts/lsp-smoke.mjs | 65 +++----- .../tests/LspConfig.Tests.ps1 | 76 +++------ plugins/dataverse-xml-lsp/.lsp.json | 89 +---------- .../scripts/Install-Plugin.ps1 | 21 ++- .../scripts/Set-LspSchemaPaths.ps1 | 150 ++++++++---------- .../dataverse-xml-lsp/scripts/lsp-launch.mjs | 103 ++++++++++++ .../tests/LspConfig.Tests.ps1 | 69 ++++---- 11 files changed, 395 insertions(+), 422 deletions(-) create mode 100644 plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs create mode 100644 plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs diff --git a/plugins/cloud-flow-json-lsp/.lsp.json b/plugins/cloud-flow-json-lsp/.lsp.json index 6fbb9e3..cd7d7a4 100644 --- a/plugins/cloud-flow-json-lsp/.lsp.json +++ b/plugins/cloud-flow-json-lsp/.lsp.json @@ -2,51 +2,12 @@ "json": { "command": "node", "args": [ - "${CLAUDE_PLUGIN_ROOT}/node_modules/vscode-langservers-extracted/lib/json-language-server/node/jsonServerMain.js", + "${CLAUDE_PLUGIN_ROOT}/scripts/lsp-launch.mjs", + "${CLAUDE_PLUGIN_ROOT}", "--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/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 b/plugins/cloud-flow-json-lsp/scripts/Install-Plugin.ps1 index 41e5734..f013a89 100644 --- a/plugins/cloud-flow-json-lsp/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 index 35bf00c..cebfe6d 100644 --- a/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 +++ b/plugins/cloud-flow-json-lsp/scripts/Set-LspSchemaPaths.ps1 @@ -1,17 +1,15 @@ #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. + Wire the bundled flow schema into VS Code user settings (optional editor integration). .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. + 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 pwsh scripts/Set-LspSchemaPaths.ps1 -UpdateVSCode #> [CmdletBinding()] @@ -27,8 +25,13 @@ param( $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. +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' @@ -46,59 +49,42 @@ $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 } - ) +# --- 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 } -# --- .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 + # 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 { - $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 + $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 } } +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 +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..4407623 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/scripts/lsp-launch.mjs @@ -0,0 +1,85 @@ +// 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 { 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'); +const schemaUrl = pathToFileURL(join(pluginRoot, 'schemas', 'cloud-flow-clientdata.schema.json')).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('exit', (c) => process.exit(c ?? 0)); diff --git a/plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs b/plugins/cloud-flow-json-lsp/scripts/lsp-smoke.mjs index b75dc30..0218e51 100644 --- a/plugins/cloud-flow-json-lsp/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/LspConfig.Tests.ps1 b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 index 4c4f68f..f183535 100644 --- a/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 +++ b/plugins/cloud-flow-json-lsp/tests/LspConfig.Tests.ps1 @@ -1,72 +1,42 @@ #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. + 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 parity (.lsp.json vs Set-LspSchemaPaths.ps1)' { +Describe 'flow LSP config is portable (.lsp.json launches the shim)' { 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] - } - } + $script:lspPath = Join-Path $pluginRoot '.lsp.json' + $script:raw = Get-Content $script:lspPath -Raw + $script:lsp = $script:raw | ConvertFrom-Json } - 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 '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 '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 'keeps the extension-to-language wiring' { + $lsp.json.extensionToLanguage.'.json' | Should -Be 'json' } - 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' + 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]:[\\/]|/)' } + } } } diff --git a/plugins/dataverse-xml-lsp/.lsp.json b/plugins/dataverse-xml-lsp/.lsp.json index d58180a..e9bc3ec 100644 --- a/plugins/dataverse-xml-lsp/.lsp.json +++ b/plugins/dataverse-xml-lsp/.lsp.json @@ -1,91 +1,14 @@ { "xml": { - "command": "${CLAUDE_PLUGIN_ROOT}/bin/lemminx-win32.exe", + "command": "node", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/scripts/lsp-launch.mjs", + "${CLAUDE_PLUGIN_ROOT}", + "--stdio" + ], "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-xml-lsp/scripts/Install-Plugin.ps1 b/plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 index 0023c54..a6065d6 100644 --- a/plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 +++ b/plugins/dataverse-xml-lsp/scripts/Install-Plugin.ps1 @@ -1,12 +1,14 @@ #requires -Version 7 <# .SYNOPSIS - One-shot plugin setup: fetch schemas + lemminx, stamp machine paths, run a self-check. + One-shot plugin setup: fetch schemas + lemminx and run a self-check. .DESCRIPTION - Idempotent - safe to re-run after /plugin update or a schema version bump. - The self-check validates a known-good and a known-bad fixture so a broken install fails - here, not at first real use. + Idempotent - safe to re-run after /plugin update or a schema version bump. Claude Code resolves + schemas and the lemminx binary at launch via scripts/lsp-launch.mjs, so no machine-local path + is ever stamped into .lsp.json. The self-check validates a known-good and a known-bad fixture + so a broken install fails here, not at first real use. Pass -UpdateVSCode to also wire the + separate VS Code editor associations. .EXAMPLE pwsh scripts/Install-Plugin.ps1 @@ -37,14 +39,9 @@ else { & (Join-Path $PSScriptRoot 'Get-Lemminx.ps1') @lemminxArgs } -Write-Host "== 3/4 Path stamping ==" -ForegroundColor Cyan -$binDir = Join-Path $pluginRoot 'bin' -if ($SkipLemminx -and -not (Get-ChildItem $binDir -Filter 'lemminx*' -File -ErrorAction SilentlyContinue)) { - Write-Host "Skipped (no lemminx binary; validator-only setup)." -} -else { - & (Join-Path $PSScriptRoot 'Set-LspSchemaPaths.ps1') -UpdateVSCode:$UpdateVSCode -} +Write-Host "== 3/4 VS Code association (optional) ==" -ForegroundColor Cyan +if ($UpdateVSCode) { & (Join-Path $PSScriptRoot 'Set-LspSchemaPaths.ps1') -UpdateVSCode } +else { Write-Host "Skipped (Claude Code resolves schemas at launch via the shim; pass -UpdateVSCode to wire VS Code)." } Write-Host "== 4/4 Self-check ==" -ForegroundColor Cyan $validator = Join-Path $PSScriptRoot 'Validate-DataverseXml.ps1' diff --git a/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 b/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 index f9281c0..3124ee1 100644 --- a/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 +++ b/plugins/dataverse-xml-lsp/scripts/Set-LspSchemaPaths.ps1 @@ -1,17 +1,16 @@ #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). + Wire the bundled Dataverse XSDs into VS Code user settings (optional editor integration). .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. + Claude Code no longer needs any stamped path or per-OS binary name: the launcher shim + (scripts/lsp-launch.mjs) discovers the lemminx binary and resolves the absolute XSD systemIds + 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 machine-local file paths. .EXAMPLE - pwsh scripts/Set-LspSchemaPaths.ps1 pwsh scripts/Set-LspSchemaPaths.ps1 -UpdateVSCode #> [CmdletBinding()] @@ -29,6 +28,11 @@ param( $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 } @@ -36,7 +40,8 @@ $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. +# the validator script extracts and validates the inner instead. 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' @@ -47,98 +52,69 @@ $assoc = [ordered]@{ '**/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. +# 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 '\\', '/' -$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." +# --- VS Code user settings.json --- +if ($SettingsPathOverride) { + $settingsPath = $SettingsPathOverride } -$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 +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' } -$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)" } - } +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) + # 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' } - 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 + 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 } } +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 +Write-Host "`nDone. Restart VS Code to apply." -ForegroundColor Cyan 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..d2abe26 --- /dev/null +++ b/plugins/dataverse-xml-lsp/scripts/lsp-launch.mjs @@ -0,0 +1,103 @@ +// 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 { 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). +const version = JSON.parse(readFileSync(join(pluginRoot, 'versions.json'), 'utf8')).schemaVersion; +const schemaDir = join(pluginRoot, 'schemas', version); + +// 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 have no association on purpose (see Set-LspSchemaPaths.ps1). +const assoc = { + '**/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', +}; +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('exit', (c) => process.exit(c ?? 0)); diff --git a/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 b/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 index 8549c4d..5e94479 100644 --- a/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 +++ b/plugins/dataverse-xml-lsp/tests/LspConfig.Tests.ps1 @@ -1,57 +1,42 @@ #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. + 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 'lemminx file association parity (.lsp.json vs Set-LspSchemaPaths.ps1)' { +Describe 'XML LSP config is portable (.lsp.json launches the shim)' { 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 - } + $script:lspPath = Join-Path $pluginRoot '.lsp.json' + $script:raw = Get-Content $script:lspPath -Raw + $script:lsp = $script:raw | ConvertFrom-Json } - It 'has a non-empty association map in the script' { - $expectedPairs.Count | Should -BeGreaterThan 0 + 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 'matches in the initializationOptions settings block' { - Get-PairSet $lsp.xml.initializationOptions.settings.xml.fileAssociations | - Should -Be $expectedPairs + It 'keeps the extension-to-language wiring' { + $lsp.xml.extensionToLanguage.'.xml' | Should -Be 'xml' + $lsp.xml.extensionToLanguage.'.fetchxml' | Should -Be 'xml' } - It 'matches in the workspace settings block' { - Get-PairSet $lsp.xml.settings.xml.fileAssociations | - Should -Be $expectedPairs + 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]:[\\/]|/)' } + } } } From 243f0012dbeac4e4906908a9b7b76da5ceb2f92f Mon Sep 17 00:00:00 2001 From: Connor Parsons Date: Tue, 21 Jul 2026 13:56:42 +1200 Subject: [PATCH 07/14] Move plugin editing guidance into docs/guide.md --- plugins/cloud-flow-json-lsp/README.md | 2 + plugins/cloud-flow-json-lsp/docs/codex.md | 6 +- plugins/cloud-flow-json-lsp/docs/guide.md | 68 +++++++++++ plugins/dataverse-xml-lsp/README.md | 2 + plugins/dataverse-xml-lsp/docs/codex.md | 4 +- plugins/dataverse-xml-lsp/docs/guide.md | 136 ++++++++++++++++++++++ 6 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 plugins/cloud-flow-json-lsp/docs/guide.md create mode 100644 plugins/dataverse-xml-lsp/docs/guide.md diff --git a/plugins/cloud-flow-json-lsp/README.md b/plugins/cloud-flow-json-lsp/README.md index c5b1f59..493386e 100644 --- a/plugins/cloud-flow-json-lsp/README.md +++ b/plugins/cloud-flow-json-lsp/README.md @@ -5,6 +5,8 @@ JSON** — the `Workflows/-.json` files `pac solution unpack` produc 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. diff --git a/plugins/cloud-flow-json-lsp/docs/codex.md b/plugins/cloud-flow-json-lsp/docs/codex.md index 40a15f2..705325f 100644 --- a/plugins/cloud-flow-json-lsp/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/cloud-flow-json-lsp/SKILL.md`, and after EVERY edit run: + > `/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/cloud-flow-json-lsp/` - 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/cloud-flow-json-lsp/docs/guide.md b/plugins/cloud-flow-json-lsp/docs/guide.md new file mode 100644 index 0000000..a4ebd28 --- /dev/null +++ b/plugins/cloud-flow-json-lsp/docs/guide.md @@ -0,0 +1,68 @@ +# 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`. +This plugin makes malformed edits to that file **surface as live LSP diagnostics** while you type, +instead of failing at `pac solution import`. + +It is one layer of a two-layer story — do not confuse them: + +| Layer | What it checks | Where | +|-------|----------------|-------| +| **Shape (this plugin)** | JSON well-formedness + the clientdata/WDL wrapper structure: `properties.definition` present, `definition` has `$schema`/`triggers`/`actions`, `runAfter` statuses are the WDL enum. Live, in-editor. | `schemas/cloud-flow-clientdata.schema.json` via `vscode-json-language-server` (`.lsp.json`) | +| **Semantics (the `power-automate-flow-dev` skill)** | Cross-node rules JSON Schema *cannot* express: `runAfter` naming a real sibling, `connectionName` resolving to a declared connection reference, child-invoker connections, hard-coded environment values, condition rows. Plus the export → unpack → edit → pack → import → verify round-trip. | that skill's `flow-lint.ps1` | + +**Rule of thumb:** the LSP catches "this isn't a well-formed flow file." `flow-lint.ps1` catches "this +is well-formed but will import Off / route to the wrong place." Run both before pack/import. + +## Setup (once per machine) + +The JSON language server is a Node package fetched via npm; it is not committed. Run: + +``` +/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`. + +## What attaches to what + +The schema associates (see `.lsp.json`) with: + +- `**/Workflows/*.json` and `**/Workflows/**/*.json` — unpacked solution cloud flows +- `**/*.flow.json` — a convenience convention for a standalone flow file + +It deliberately does **not** claim every `*.json` in the workspace. + +## Subagents / headless contexts + +LSP diagnostics only auto-push in the **main interactive session**. A spawned subagent, a workflow +step, or any non-main-session context does NOT receive them. In those contexts validate structure +explicitly with PowerShell's built-in `Test-Json`: + +``` +Get-Content .json -Raw | Test-Json -SchemaFile "${CLAUDE_PLUGIN_ROOT}/schemas/cloud-flow-clientdata.schema.json" +``` + +(and run the `power-automate-flow-dev` skill's `flow-lint.ps1` for the semantic layer). + +## Gotchas + +- **The wrapper is intentionally loose on action inputs.** Power Automate connector actions + (`OpenApiConnection`) are not in Microsoft's public Logic Apps schema, so a strict `$ref` would + drown real errors in false positives. `inputs` is left untyped; structure is what's validated. + `pac solution check` / a successful import remains the authoritative gate. +- **`${CLAUDE_PLUGIN_ROOT}` is not substituted** inside `.lsp.json` `initializationOptions`/`settings` + — only in `command`/`args`. The schema `url` there is an absolute `file://` URI. On a new machine + (or after a plugin update / move), re-run `/cloud-flow-json-lsp:cloud-flow-json-lsp-setup` — or + `scripts/Set-LspSchemaPaths.ps1` alone — to re-stamp it. +- **The JSON server validates a document only after answering its `workspace/configuration` pull.** + Claude Code and VS Code both handle that; a bare LSP client must too (see `scripts/lsp-smoke.mjs` + for a reference client). diff --git a/plugins/dataverse-xml-lsp/README.md b/plugins/dataverse-xml-lsp/README.md index 4f261fc..4e56d69 100644 --- a/plugins/dataverse-xml-lsp/README.md +++ b/plugins/dataverse-xml-lsp/README.md @@ -5,6 +5,8 @@ customization XML** — ribbon buttons (`RibbonDiffXml`), app navigation (`SiteM (`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 diff --git a/plugins/dataverse-xml-lsp/docs/codex.md b/plugins/dataverse-xml-lsp/docs/codex.md index a2da0b4..1649c5e 100644 --- a/plugins/dataverse-xml-lsp/docs/codex.md +++ b/plugins/dataverse-xml-lsp/docs/codex.md @@ -18,12 +18,12 @@ script after each edit. > When editing Dataverse customization XML (RibbonDiff.xml, SiteMap, FormXml, > Customizations.xml, FetchXML, charts), follow - > `/plugins/dataverse-xml-lsp/SKILL.md`, and after EVERY edit run: + > `/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-xml-lsp/` 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-xml-lsp/docs/guide.md b/plugins/dataverse-xml-lsp/docs/guide.md new file mode 100644 index 0000000..e38b40e --- /dev/null +++ b/plugins/dataverse-xml-lsp/docs/guide.md @@ -0,0 +1,136 @@ +# 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-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. +2. **lemminx LSP** — live diagnostics while editing (this plugin's `.lsp.json`, and VS Code's + RedHat XML extension via `xml.fileAssociations`). Diagnostics push into context on file edit. + +> **Subagents / headless contexts must use layer 1 (the script).** LSP diagnostics only auto-push +> in the main interactive session — a spawned subagent (Agent tool), a workflow step, or any +> non-main-session context does NOT receive them. In those contexts, always run +> `Validate-DataverseXml.ps1` explicitly after editing; don't assume squiggles appeared. The +> script is self-contained (pwsh + the fetched XSDs) and needs no LSP. + +## Always validate after editing + +``` +pwsh /scripts/Validate-DataverseXml.ps1 [more paths / globs] +``` + +It picks the schema by the file's **root element**: + +| Root element | Schema | Notes | +|---------------------|-------------------------------|-------| +| `RibbonDiffXml` | `RibbonCore.xsd` | Per-entity `RibbonDiff.xml` or app ribbon — **authoritative** | +| `SiteMap` | `SiteMap.xsd` | App navigation — authoritative | +| `form` / `forms` | `FormXml.xsd` | Forms; pac's `` wrapper → each inner `systemform/form` validated — indicative (schema lags modern form attrs) | +| `fetch` | `Fetch.xsd` | FetchXML queries (`.fetchxml` files, query fragments) — authoritative | +| `savedquery` | `Fetch.xsd` | pac `SavedQueries/*.xml` — authoritative | +| `visualization` / `datadefinition` | `VisualizationDataDescription.xsd` | Charts; the `` wrapper's inner `datadescription/datadefinition` is validated (escaped inner XML handled — see gotchas) | +| `configuration` | `isv.config.xsd` | Legacy ISV config | +| `importexportxml` (lowercase) | `ParameterXml.xsd` | Configuration-migration parameter XML — note the case difference from `ImportExportXml` | +| `viewers` | `reports.config.xsd` | Report viewers config | +| `ImportExportXml` | `CustomizationsSolution.xsd` | Whole `Customizations.xml` — **indicative only** (see caveat) | + +Unknown root elements **fail loud** (exit 1) and list the supported roots — use +`-Schema ` to force one. + +Non-zero exit = validation failed. Fix and re-run until clean, then pack/import. + +## Adding a ribbon (command-bar) button — the common task + +In a pac-unpacked solution, edit the entity's `Entities//RibbonDiff.xml`. Fill the +empty `` and ``. Minimal valid shape (Microsoft-verified): + +```xml + + + + +