Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/power-pages/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ skills/
assets/alm-plan-template.html ← HTML template with __PLACEHOLDER__ tokens for the ALM plan document
scripts/render-alm-plan.js ← Renders alm-plan-template.html from planData JSON (stages diagram, checklist, risks)
scripts/validate-plan-alm.js ← Validates docs/alm-plan.html exists and is > 500 bytes; gracefully exits 0 if not a plan-alm session
setup-prerequisites/
SKILL.md ← Machine setup skill — checks/installs Node, Git, .NET SDK, PAC CLI, Azure CLI and signs both CLIs in
scripts/detect-prerequisites.js ← Read-only probe; emits JSON status + an `actions` list the workflow works through
scripts/install-prerequisite.js ← Installs one tool via winget (Windows) / Homebrew (macOS) / dotnet tool (PAC, all platforms)
```

## ALM intent routing — `plan-alm` is the front door
Expand Down
19 changes: 18 additions & 1 deletion plugins/power-pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ This keeps hook behavior in one place and avoids relying on skill-frontmatter ho
| Prerequisite | Required for | Install |
|---|---|---|
| [Node.js](https://nodejs.org/) (LTS) | All skills | `winget install OpenJS.NodeJS.LTS` |
| [Git](https://git-scm.com/downloads) | Commits made by most skills | `winget install Git.Git` |
| [PAC CLI](https://learn.microsoft.com/power-platform/developer/cli/introduction) | Deploy, activate, data model | `dotnet tool install -g Microsoft.PowerApps.CLI.Tool` |
| [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) | Data model, sample data, activation | `winget install Microsoft.AzureCLI` |

Installing from the marketplace copies the skill files and nothing else, so none of these tools come with it. Run [`/setup-prerequisites`](#setup-prerequisites) to check what is on your machine, install what is missing, and sign the two CLIs in.

## Skills

The plugin provides 31 skills that cover the full lifecycle of a Power Pages code site — scaffolding, deployment, data modeling, backend integration, authentication, ALM and CI/CD, security review, testing, and auditing. Each skill is invoked conversationally — just describe what you want to do.
The plugin provides 32 skills that cover the full lifecycle of a Power Pages code site — scaffolding, deployment, data modeling, backend integration, authentication, ALM and CI/CD, security review, testing, and auditing. Each skill is invoked conversationally — just describe what you want to do.

### Site scaffolding and deployment

Expand Down Expand Up @@ -390,6 +393,19 @@ Adds search engine optimization artifacts: `robots.txt`, `sitemap.xml`, and meta

### Support

#### `/setup-prerequisites`

> "Set up the plugin" · "Check my setup" · "pac is not recognized"

Gets a machine ready to use the plugin. Checks Node.js, Git, the .NET SDK, the Power Platform CLI, and the Azure CLI, installs whatever is missing, and signs both CLIs in.

- Reports each tool's version and sign-in state before changing anything
- Asks before every install, one tool at a time, showing the exact command
- Installs via winget on Windows and Homebrew on macOS; hands back commands on Linux
- Offers a PAC CLI update when a newer version is published
- Warns when the two CLIs are signed into different tenants
- A failed install never stops the run — everything outstanding lands in the final summary

#### `/report-issue`

> "Report a bug with the create-site skill"
Expand Down Expand Up @@ -441,6 +457,7 @@ The plugin ships with two MCP servers configured in `.mcp.json` — they start a
A common end-to-end workflow looks like this:

```
0. /setup-prerequisites → Install the CLIs and sign in (first run only)
1. /create-site → Scaffold + design + build pages
2. /deploy-site → Upload to Power Pages environment
3. /activate-site → Provision a public URL
Expand Down
12 changes: 12 additions & 0 deletions plugins/power-pages/references/approval-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,17 @@ New skill (Power Pages source & dependency security scan). Runs local static ana

---

### 6.31 `setup-prerequisites` (2 gate IDs)

New skill (machine setup for marketplace installs). Checks Node.js, Git, the .NET SDK, the PAC CLI, and the Azure CLI, installs what is missing, and signs both CLIs in. Both gates are `consent` — each one authorizes a change to the user's machine (a package install, or a credential prompt that writes a CLI profile) rather than a change to the project.

| ID | Kind | Category | Phase | Trigger / question | Cancel leaves |
|---|---|---|---|---|---|
| `setup-prerequisites:2.install-consent` | gate | consent | 2 | *"Install <tool> using <command>? / Skip"* — fires once per `install`/`update` action returned by `detect-prerequisites.js`. Skipped entirely when nothing is missing. | nothing — the tool stays as it was and the run continues with the next one |
| `setup-prerequisites:3.signin-consent` | gate | consent | 3 | *"Sign in to <CLI> now? / Skip"* — fires once per `signin` action, and again after an install that added a CLI. | nothing — no CLI profile is created; the outstanding sign-in is listed in the Phase 4 summary |

---

### Cross-plugin shared skills — out of catalog scope

`report-issue` — Its prompts are cross-plugin, not power-pages-specific, so they are not catalogued here. If the shared workflow is ever governed by per-plugin approval-gate linting, add a `report-issue:*` section to this catalog.
Expand Down Expand Up @@ -728,6 +739,7 @@ These need explicit confirmation from the reviewer before SKILL.md edits land. R
- §6.13–§6.24 added — full catalog rows for `create-site`, `deploy-site`, `add-server-logic`, `add-cloud-flow`, `setup-auth`, `integrate-webapi`, `setup-datamodel`, `add-sample-data`, `add-seo`, `create-webroles`, `audit-permissions`, `integrate-backend` (45 gates + 9 not-a-gates).
- §6.24a–§6.28 added — security skills introduced by PR #151 (`manage-firewall`, `manage-headers`, `scan-site`, `security-review`). The new skills use a runtime-loop prompt pattern; §6.24a documents the marker convention for that pattern. 3 gates + 2 not-a-gates.
- §6.30 added — `scan-code` (Power Pages source & dependency security scan). 3 `plan` gates (`scan-code:1.agent-review-fallback`, `scan-code:2.scope-choice`, `scan-code:2.depth-choice`); no not-a-gates.
- §6.31 added — `setup-prerequisites` (machine setup for marketplace installs). 2 `consent` gates (`setup-prerequisites:2.install-consent`, `setup-prerequisites:3.signin-consent`); no not-a-gates.
- Markers added to all non-ALM SKILL.md files (HTML comment + 🚦 block per gate; `not-a-gate` comment per data-gathering prompt or meta-mention).
- `scripts/lint-skills-alm.js` warn-only branch removed — all skills now hard-fail.
- `AGENTS.md` Key Patterns updated — Approval Gate convention applies plugin-wide; new skills must extend §6 in the same PR they introduce a prompt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ If the tracking script creates or updates site setting YAML files, include those
| deploy-pipeline | DeployPipeline | Site/AI/Skills/DeployPipeline |
| ensure-pipelines-host | EnsurePipelinesHost | Site/AI/Skills/EnsurePipelinesHost |
| force-link-environment | ForceLinkEnvironment | Site/AI/Skills/ForceLinkEnvironment |
| setup-prerequisites | SetupPrerequisites | Site/AI/Skills/SetupPrerequisites |

## YAML Format

Expand Down
230 changes: 230 additions & 0 deletions plugins/power-pages/scripts/tests/detect-prerequisites.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
const test = require('node:test');
const assert = require('node:assert/strict');

const {
buildProbeInvocation,
parsePacVersion,
parseAzVersion,
parseDotnetVersion,
parseGitVersion,
parsePacAuthWho,
parseAzAccountShow,
compareVersions,
latestStableVersion,
buildActions,
tenantMismatch,
} = require('../../skills/setup-prerequisites/scripts/detect-prerequisites');

// Captured from `pac help` on 1.51.1 — the version carries a `+<git-sha>` build
// suffix that must not leak into the comparison against NuGet's plain semver.
const PAC_HELP = `
Microsoft PowerPlatform CLI

Version: 1.51.1+g8a2ec33

Usage: pac [command] [options]
`;

// Captured from `pac auth who` on 2.9.3. Note the "Tenant Id" casing, and that
// the banner opens with a "Connected as" line before the label/value block.
const PAC_AUTH_WHO = `
Connected as user@contoso.com

Type: User
Cloud: Public
Tenant Id: 72f988bf-86f1-41af-91ab-2d7cd011db47
Tenant Country: IN
User: user@contoso.com
Environment Id: 11111111-1111-1111-1111-111111111111
Organization Friendly Name: Contoso Dev
`;

function statusFixture(overrides = {}) {
return Object.assign(
{
node: { available: true, version: '22.11.0' },
git: { available: true, version: '2.53.0' },
dotnet: { available: true, version: '10.0.102' },
pac: { available: true, version: '1.51.1', updateAvailable: false },
az: { available: true, version: '2.77.0' },
pacAuth: { signedIn: true, tenantId: 'tenant-a' },
azAuth: { signedIn: true, tenantId: 'tenant-a' },
},
overrides
);
}

// The Azure CLI installs only `az.cmd` on Windows, which Node refuses to spawn
// directly, so every Windows probe goes through `cmd.exe /c`.
test('buildProbeInvocation routes Windows probes through cmd.exe', () => {
assert.deepEqual(buildProbeInvocation('az', ['version', '-o', 'tsv'], 'win32'), {
file: 'cmd.exe',
args: ['/c', 'az', 'version', '-o', 'tsv'],
});
});

test('buildProbeInvocation runs the command directly off Windows', () => {
for (const platform of ['darwin', 'linux']) {
assert.deepEqual(buildProbeInvocation('az', ['account', 'show'], platform), {
file: 'az',
args: ['account', 'show'],
});
}
});

test('parsePacVersion strips the build-metadata suffix', () => { assert.equal(parsePacVersion(PAC_HELP), '1.51.1');
});
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated

test('parsePacVersion returns null for missing or unparseable output', () => {
assert.equal(parsePacVersion(null), null);
assert.equal(parsePacVersion('command not found: pac'), null);
});

test('parseAzVersion reads the version from tsv output', () => {
assert.equal(parseAzVersion('2.77.0\t2.77.0\t\t\n'), '2.77.0');
assert.equal(parseAzVersion(null), null);
});

test('parseDotnetVersion handles stable and preview SDKs', () => {
assert.equal(parseDotnetVersion('10.0.102\n'), '10.0.102');
assert.equal(parseDotnetVersion('9.0.100-preview.1.24101.2\n'), '9.0.100-preview.1.24101.2');
assert.equal(parseDotnetVersion(null), null);
});

test('parseGitVersion reads upstream and Apple-shipped builds', () => {
assert.equal(parseGitVersion('git version 2.53.0\n'), '2.53.0');
assert.equal(parseGitVersion('git version 2.39.5 (Apple Git-154)\n'), '2.39.5');
assert.equal(parseGitVersion('git version 2.51.0.windows.1\n'), '2.51.0');
assert.equal(parseGitVersion(null), null);
assert.equal(parseGitVersion('command not found: git'), null);
});

test('parsePacAuthWho extracts tenant, user, and cloud', () => {
const auth = parsePacAuthWho(PAC_AUTH_WHO);
assert.equal(auth.signedIn, true);
assert.equal(auth.tenantId, '72f988bf-86f1-41af-91ab-2d7cd011db47');
assert.equal(auth.user, 'user@contoso.com');
assert.equal(auth.cloud, 'Public');
});

test('parsePacAuthWho accepts the older "Tenant ID" casing', () => {
const auth = parsePacAuthWho('Tenant ID: t-1\nUser: u@c.com');
assert.equal(auth.tenantId, 't-1');
});

test('parsePacAuthWho reports signed out when no profile exists', () => {
assert.deepEqual(parsePacAuthWho('No profiles were found on this computer.'), {
signedIn: false,
tenantId: null,
user: null,
cloud: null,
});
assert.equal(parsePacAuthWho(null).signedIn, false);
assert.equal(parsePacAuthWho('some unrelated banner text').signedIn, false);
});

test('parseAzAccountShow extracts tenant, user, and subscription', () => {
const account = parseAzAccountShow(
JSON.stringify({ tenantId: 'tenant-a', user: { name: 'user@contoso.com' }, name: 'Pay-As-You-Go' })
);
assert.deepEqual(account, {
signedIn: true,
tenantId: 'tenant-a',
user: 'user@contoso.com',
subscription: 'Pay-As-You-Go',
});
});

test('parseAzAccountShow treats a subscription-less account as signed in', () => {
const account = parseAzAccountShow(JSON.stringify({ tenantId: 'tenant-a', user: { name: 'u@c.com' } }));
assert.equal(account.signedIn, true);
assert.equal(account.subscription, null);
});

test('parseAzAccountShow reports signed out for null, non-JSON, and empty payloads', () => {
assert.equal(parseAzAccountShow(null).signedIn, false);
assert.equal(parseAzAccountShow("Please run 'az login' to setup account.").signedIn, false);
assert.equal(parseAzAccountShow('{}').signedIn, false);
assert.equal(parseAzAccountShow('null').signedIn, false);
});

test('compareVersions orders versions and ignores prerelease suffixes', () => {
assert.equal(compareVersions('1.51.1', '1.52.0'), 1);
assert.equal(compareVersions('1.52.0', '1.51.1'), -1);
assert.equal(compareVersions('1.51.1', '1.51.1'), 0);
assert.equal(compareVersions('1.51.1-preview', '1.51.1'), 0);
assert.equal(compareVersions('1.51', '1.51.0'), 0);
assert.equal(compareVersions(null, '1.51.1'), 0);
});

test('latestStableVersion picks the last stable entry', () => {
assert.equal(latestStableVersion(['1.50.1', '1.51.1', '1.52.1']), '1.52.1');
assert.equal(latestStableVersion(['1.50.1', '1.52.0-preview']), '1.50.1');
assert.equal(latestStableVersion([]), null);
assert.equal(latestStableVersion(undefined), null);
});

test('buildActions is empty when everything is present and signed in', () => {
assert.deepEqual(buildActions(statusFixture()), []);
});

test('buildActions reports one install per missing tool', () => {
const actions = buildActions(
statusFixture({
git: { available: false, version: null },
dotnet: { available: false, version: null },
az: { available: false, version: null },
azAuth: { signedIn: false, tenantId: null },
})
);
assert.deepEqual(actions, [
{ tool: 'git', kind: 'install' },
{ tool: 'dotnet', kind: 'install' },
{ tool: 'az', kind: 'install' },
]);
});

test('buildActions does not ask a missing CLI to sign in', () => {
const actions = buildActions(
statusFixture({
pac: { available: false, version: null, updateAvailable: false },
pacAuth: { signedIn: false, tenantId: null },
})
);
assert.deepEqual(actions, [{ tool: 'pac', kind: 'install' }]);
});

test('buildActions offers a pac update only when pac is installed', () => {
const actions = buildActions(
statusFixture({ pac: { available: true, version: '1.50.1', updateAvailable: true } })
);
assert.deepEqual(actions, [{ tool: 'pac', kind: 'update' }]);
});

test('buildActions asks a signed-out but installed CLI to sign in', () => {
const actions = buildActions(statusFixture({ azAuth: { signedIn: false, tenantId: null } }));
assert.deepEqual(actions, [{ tool: 'az', kind: 'signin' }]);
});

test('tenantMismatch flags differing tenants', () => {
const mismatch = tenantMismatch(
{ signedIn: true, tenantId: 'tenant-a' },
{ signedIn: true, tenantId: 'tenant-b' }
);
assert.deepEqual(mismatch, { pacTenantId: 'tenant-a', azTenantId: 'tenant-b' });
});

test('tenantMismatch ignores casing differences', () => {
assert.equal(
tenantMismatch({ signedIn: true, tenantId: 'TENANT-A' }, { signedIn: true, tenantId: 'tenant-a' }),
null
);
});

test('tenantMismatch stays silent when either side is signed out or unknown', () => {
assert.equal(tenantMismatch({ signedIn: false }, { signedIn: true, tenantId: 'tenant-b' }), null);
assert.equal(
tenantMismatch({ signedIn: true, tenantId: null }, { signedIn: true, tenantId: 'tenant-b' }),
null
);
});
Loading
Loading