Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a9e61f1
Prefactor create-site discovery branch seam
priyanshu92 Jul 12, 2026
f8ac85e
Add create-site template catalog selection
priyanshu92 Jul 12, 2026
c4ef15f
Import selected create-site templates
priyanshu92 Jul 12, 2026
bfe3fc9
Activate imported create-site templates
priyanshu92 Jul 12, 2026
c42d1ca
Apply seed data for create-site templates
priyanshu92 Jul 12, 2026
84c26aa
Handle template reinstall and failures
priyanshu92 Jul 12, 2026
87f6981
Emit create-site template outcome telemetry
priyanshu92 Jul 12, 2026
24ce1df
Document create-site template support
priyanshu92 Jul 12, 2026
c64151a
Keep template telemetry local to Power Pages
priyanshu92 Jul 23, 2026
fa47f6d
Use latest template release by default
priyanshu92 Jul 23, 2026
18d3f12
Support seed data file-column attachments
priyanshu92 Jul 23, 2026
9bfa5ee
Preflight unblock JavaScript for template imports
priyanshu92 Jul 23, 2026
8565ca5
Preflight template language availability
priyanshu92 Jul 24, 2026
685e6e9
Accept template audience arrays
priyanshu92 Jul 28, 2026
1de532e
Fallback template catalog to main
priyanshu92 Jul 28, 2026
510d60e
Bump Power Pages plugin version
priyanshu92 Jul 28, 2026
19cb80c
Render template capabilities browser
priyanshu92 Jul 28, 2026
d0dd04d
Resolve template artifacts under catalog folder
priyanshu92 Jul 28, 2026
f58f277
Remove template browser stats cards
priyanshu92 Jul 28, 2026
b866892
Ask before browsing create-site templates
priyanshu92 Jul 28, 2026
1d00c1d
Show templates as browser tabs
priyanshu92 Jul 28, 2026
202b5d2
Add preview image boundaries
priyanshu92 Jul 28, 2026
3d51a93
Merge remote-tracking branch 'origin/main' into priyanshu92-spa-templ…
priyanshu92 Jul 29, 2026
a8da482
Confirm template target environment
priyanshu92 Jul 29, 2026
5210a2f
Show template preflight tasks
priyanshu92 Jul 29, 2026
7917d1a
Use template-declared Dataverse languages
priyanshu92 Jul 29, 2026
9eaf8db
address PR review feedback
priyanshu92 Jul 29, 2026
e41be6d
Hydrate template seed JSON directly
priyanshu92 Jul 29, 2026
93ea6d9
Emit template telemetry before activation handoff
priyanshu92 Jul 29, 2026
23ff67b
Rename template telemetry to template used
priyanshu92 Jul 29, 2026
7078438
Split create-site path telemetry events
priyanshu92 Jul 29, 2026
4a57752
Use Git refs for template catalog resolution
priyanshu92 Jul 29, 2026
f2dab32
address PR review feedback
priyanshu92 Jul 29, 2026
20429f3
Retry corrupt cached template solution
priyanshu92 Jul 29, 2026
2f28e69
Support Dataverse request body files
priyanshu92 Jul 29, 2026
d928982
Add template customization handoff
priyanshu92 Jul 29, 2026
e99b13b
Support exported template seed data
priyanshu92 Jul 29, 2026
769302b
Show template import progress page
priyanshu92 Jul 29, 2026
255590d
Align template import status page
priyanshu92 Jul 29, 2026
5ab7e74
Keep template import loader logo
priyanshu92 Jul 29, 2026
36d4296
Emphasize template import previews
priyanshu92 Jul 29, 2026
4cf3369
Polish template import carousel
priyanshu92 Jul 29, 2026
1a184e8
Handle invalid preview file URLs
priyanshu92 Jul 29, 2026
aa38cd7
Emit template path telemetry earlier
priyanshu92 Jul 29, 2026
eb366b0
Remove invalid async progress select
priyanshu92 Jul 29, 2026
047c471
Use indeterminate import progress
priyanshu92 Jul 29, 2026
96e2449
Support multi-framework template families
priyanshu92 Jul 30, 2026
328070f
Redirect import status page to live site
priyanshu92 Jul 30, 2026
9924896
address PR review feedback
priyanshu92 Jul 30, 2026
013e079
Run template preflights in parallel
priyanshu92 Jul 30, 2026
4cc473e
Handle existing template site imports
priyanshu92 Jul 30, 2026
5f8e3cd
Show template setup phases
priyanshu92 Jul 30, 2026
31a2372
Add activation phase to template status
priyanshu92 Jul 30, 2026
e78f832
Suppress template telemetry output
priyanshu92 Jul 30, 2026
f088031
Remove template telemetry quiet mode
priyanshu92 Jul 30, 2026
c37adb1
Map template seed lookup ids
priyanshu92 Jul 30, 2026
9bad92d
Fix template browser framework badge
priyanshu92 Jul 30, 2026
1e10b73
address PR review feedback
priyanshu92 Jul 30, 2026
b8bf26e
Validate template browser output
priyanshu92 Aug 3, 2026
98c84df
address PR review feedback
priyanshu92 Aug 3, 2026
0d20287
Keep import status active until redirect
priyanshu92 Aug 3, 2026
fcca2c8
Keep status page waiting after import
priyanshu92 Aug 3, 2026
634365b
Skip template gallery when no match
priyanshu92 Aug 3, 2026
6de1154
Inspect template solution zip without unzip
priyanshu92 Aug 5, 2026
2b9aa40
Move import progress into phase pills
priyanshu92 Aug 5, 2026
78aa621
Harden template import preflights
priyanshu92 Aug 5, 2026
fef669f
Fix Windows browser opener
priyanshu92 Aug 5, 2026
7b3f522
address PR review feedback
priyanshu92 Aug 5, 2026
0f64c51
Parse PAC site active status
priyanshu92 Aug 5, 2026
d22b366
Fix Windows tenant CLI lookup
priyanshu92 Aug 5, 2026
361f01e
Use PAC exe for Windows tenant check
priyanshu92 Aug 5, 2026
50cc3e1
Fix tenant alignment tests on Windows
priyanshu92 Aug 5, 2026
49d4e48
Update completed import phase label
priyanshu92 Aug 6, 2026
1b5bd69
Merge remote-tracking branch 'origin/main' into priyanshu92-spa-templ…
priyanshu92 Aug 11, 2026
0972f2f
Use fixed executables for tenant checks
priyanshu92 Aug 11, 2026
7f6d729
Fallback to active PAC auth environment
priyanshu92 Aug 12, 2026
17c341b
Ask before showing template matches
priyanshu92 Aug 13, 2026
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/power-pages/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "power-pages",
"version": "2.6.3",
"version": "2.7.0",
"description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro. Includes ALM orchestration (plan-alm) with a solution-splitting decision tree, per-solution pipelines, Azure Blob asset advisory, manifest schema v2 for multi-solution deployments, and force-link remediation for cross-host pipeline migrations.",
"author": {
"name": "Microsoft",
Expand Down
2 changes: 1 addition & 1 deletion plugins/power-pages/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "power-pages",
"version": "2.6.3",
"version": "2.7.0",
"description": "Create and deploy Power Pages sites using modern development approaches. Supports code sites (SPAs) with React, Angular, Vue, or Astro. Includes ALM orchestration (plan-alm) with a solution-splitting decision tree, per-solution pipelines, Azure Blob asset advisory, manifest schema v2 for multi-solution deployments, and force-link remediation for cross-host pipeline migrations.",
"author": {
"name": "Microsoft",
Expand Down
7 changes: 5 additions & 2 deletions plugins/power-pages/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Read `PLUGIN_DEVELOPMENT_GUIDE.md` for UX and reliability standards when creatin
- **Dataverse-backed validation** must stay opt-in for local runs only. Do not require live Dataverse connectivity in CI workflows or default test runs; gate it behind explicit local flags such as `--validate-dataverse-relationships`.
- **Azure CLI `--allow-no-subscriptions`** — this flag is only valid on `az login`. Other `az` subcommands (`az account get-access-token`, `az account show`, etc.) reject it as an unrecognized argument and exit 2, so do NOT add it to anything other than `az login`. When the user is not logged in to the Azure CLI, suggest plain `az login` first; only suggest `az login --allow-no-subscriptions` as a fallback if they don't have any associated Azure subscription, since that variant lets subscription-less accounts sign in and still mint AAD-scoped Dataverse/Power Platform tokens via subsequent `az account get-access-token` calls. Reuse the shared `getAuthToken` helper in `scripts/lib/validation-helpers.js` instead of shelling out to `az` directly.
- **Reference docs** shared across skills live in `references/` — reference via `${PLUGIN_ROOT}/references/` paths, don't duplicate.
- **Templates** use `__PLACEHOLDER__` tokens (e.g., `__SITE_NAME__`) replaced during scaffolding. The `gitignore` file is stored without the dot prefix and renamed to `.gitignore` during scaffolding.
- **Local scaffold templates** use `__PLACEHOLDER__` tokens (e.g., `__SITE_NAME__`) replaced during from-scratch scaffolding. The `gitignore` file is stored without the dot prefix and renamed to `.gitignore` during scaffolding.
- **Hooks** are defined centrally in `hooks/hooks.json`, using `PostToolUse` with matcher `Skill` so validation runs when a tracked Power Pages skill completes.
- **ALM split-decision thresholds** are intentionally tighter than the platform hard caps. `scripts/lib/alm-thresholds.js` recommends a split at 75 MB / 4000 components (vs platform caps of 95 MB / 6000), reserving ~20 MB / ~2000-component growth headroom in each split child. Override per-project via `.alm-config.json` if you have a justified reason to push closer to the caps.
- **OAuth credential-style site settings** (ConsumerKey / ClientId / ClientSecret / etc.) are NOT excluded from solutions. `setup-solution` Phase 5 prompts per credential to choose between (a) Secret-typed env var (Key Vault per stage), (b) String-typed env var (plain text per stage), or (c) skip. The site-setting record is added to the solution and routed to an env var so secret values never ship in the solution zip. Plans generated before 2026-05-08 use the older `excluded` bucket — setup-solution's preloadedSettings handler treats those as `credentialNeedsDecision` for backward compatibility.
Expand Down Expand Up @@ -277,7 +277,10 @@ Playwright MCP server for browser automation and live site previews during devel

## Template System

Framework templates use `__PLACEHOLDER__` tokens (e.g., `__SITE_NAME__`, `__PRIMARY_COLOR__`, `__BG_COLOR__`) that get replaced during site scaffolding. The `gitignore` file is stored without the dot prefix to avoid git interference in the plugin repo — it gets renamed to `.gitignore` during scaffolding.
There are two template systems:

- **Local scaffold templates** live under `skills/create-site/assets/{react,vue,angular,astro}/`. These use `__PLACEHOLDER__` tokens (e.g., `__SITE_NAME__`, `__PRIMARY_COLOR__`, `__BG_COLOR__`) that get replaced during from-scratch site scaffolding. The `gitignore` file is stored without the dot prefix to avoid git interference in the plugin repo — it gets renamed to `.gitignore` during scaffolding.
- **Installable SPA templates** live outside this repo in `microsoft/power-pages-samples` under the `templates/` tree. `create-site` reads the central manifest, previews template cards, downloads artifacts from a pinned GitHub SHA, imports the selected template's **unmanaged** solution, applies optional seed data, activates the site, and opens the live URL. These are not `__PLACEHOLDER__` scaffolds; they are curated solution-backed starters.

## Validation Scripts

Expand Down
8 changes: 4 additions & 4 deletions plugins/power-pages/PLUGIN_DEVELOPMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ When a skill deploys a site or audits permissions across many tables, the termin

### The Solution

Every skill must implement **task-based progress tracking** using `TaskCreate` and `TaskUpdate`. Tasks are created upfront at Phase 1, one per phase, and marked `in_progress` / `completed` as the skill progresses.
Every skill must implement **task-based progress tracking** using `TaskCreate` and `TaskUpdate`. Tasks are created upfront at Phase 1, one per phase, and marked `in_progress` / `completed` as the skill progresses. For a skill with mutually exclusive branches, create the path-agnostic tasks upfront, then append the branch-specific phase tasks immediately after the branch is selected so the user never sees permanently skipped work.

### Implementation Pattern

Every skill creates all phase tasks at the start:
Default pattern: every linear skill creates all phase tasks at the start:

```
Phase 1: TaskCreate → subject: "Verify prerequisites", activeForm: "Verifying prerequisites"
Expand All @@ -72,7 +72,7 @@ Each task has three fields:
- `activeForm` — Present continuous (shown in the spinner)
- `description` — Brief explanation of the phase

For skills that process multiple entities (e.g., audit-permissions auditing many tables), create **per-entity sub-tasks** dynamically within the relevant phase.
For skills that process multiple entities (e.g., audit-permissions auditing many tables), create **per-entity sub-tasks** dynamically within the relevant phase. For skills that choose between mutually exclusive paths (for example, a template-backed path vs. a from-scratch path), create the common discovery/selection tasks first and append only the selected path's phases after the routing decision.

### What This Means in Practice

Expand All @@ -83,7 +83,7 @@ For skills that process multiple entities (e.g., audit-permissions auditing many
| `/audit-permissions` | 7 phases (prereqs → discover → analyze → audit → cross-check → report → present) | Per-table tasks in Phase 4 (checklist A-K) |
| `/create-site` | 8 phases (prereqs → gather → plan → scaffold → implement → validate → deploy → summarize) | None |

> **Acceptance criterion:** Every skill must create all phase tasks upfront at Phase 1 start. Each task must have `subject`, `activeForm`, and `description`. Tasks must be marked `in_progress` when starting and `completed` when done.
> **Acceptance criterion:** Every linear skill must create all phase tasks upfront at Phase 1 start. A branching skill must create all path-agnostic tasks upfront and append the selected branch's tasks as soon as the branch is known. Each task must have `subject`, `activeForm`, and `description`. Tasks must be marked `in_progress` when starting and `completed` when done.

---

Expand Down
7 changes: 5 additions & 2 deletions plugins/power-pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ The plugin provides 31 skills that cover the full lifecycle of a Power Pages cod
#### `/create-site`

> "Create a Power Pages site with React for a job board"
>
> "Create a site from a Power Pages SPA template"

Scaffolds a complete code site from a framework template, applies your design direction (fonts, colors, layout), builds out pages and components, and provides a live preview in the browser throughout development.
Starts a complete code site either from a curated SPA template or from a fresh framework scaffold. The template path previews installable templates, imports the selected unmanaged solution into your environment, applies optional seed data, activates the site, and opens the live URL. The from-scratch path applies your design direction (fonts, colors, layout), builds out pages and components, and provides a live preview throughout development.

- Start from a curated SPA template or scaffold from scratch
- Choose from React, Vue, Angular, or Astro
- Real images from Unsplash (no placeholders)
- Live browser preview during development
Expand Down Expand Up @@ -443,7 +446,7 @@ The plugin host must provide an absolute `PLUGIN_ROOT` (GitHub Copilot) or `CLAU
A common end-to-end workflow looks like this:

```
1. /create-site → Scaffold + design + build pages
1. /create-site → Start from a SPA template or scaffold + design + build pages
2. /deploy-site → Upload to Power Pages environment
3. /activate-site → Provision a public URL
4. /setup-datamodel → Create Dataverse tables
Expand Down
21 changes: 19 additions & 2 deletions plugins/power-pages/references/approval-gates.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,16 +468,33 @@ When **removing** a gate, also remove its catalog row in the same PR.

---

### 6.13 `create-site` (5 calls)
### 6.13 `create-site` (20 calls: 13 gates + 7 not-a-gates)

| ID | Kind | Category | Phase | Trigger / question | Cancel leaves |
|---|---|---|---|---|---|
| `create-site:1.purpose` | gate | plan | 1 | Site purpose unclear — multi-question prompt (site name, framework, purpose, audience, location) | nothing |
| `create-site:1.purpose` | gate | plan | 1 | Site name, purpose, or audience unclear — path-agnostic discovery prompt | nothing |
| `create-site:1.5.creation-path` | not-a-gate | - | 1.5 | Route preference before catalog fetch; no project directory, Dataverse write, or durable skill state exists | - |
| `create-site:1.5.template-choice` | not-a-gate | — | 1.5 | Read-only route selection after template preview; only disposable temp preview files exist, with no project directory, Dataverse write, or durable skill state | — |
| `create-site:1.5.from-scratch-setup` | not-a-gate | — | 1.5 | Deferred framework and directory data-gathering before any scaffold files are written | — |
| `create-site:1.5.confirm-environment` | gate | consent | 1.5 | Target environment resolved from PAC/Azure auth — confirm before any environment preflight or import mutation | template-cache |
| `create-site:1.5.unblock-js` | gate | consent | 1.5 | Target environment blocks `.js` in `blockedattachments` — remove only `js` before template import | attachment-block-modified |
| `create-site:1.5.language-requirement` | not-a-gate | - | 1.5 | Route selection after blocking template-declared Dataverse language preflight; no override option exists and no org mutation has happened | - |
| `create-site:1.5.template-import` | gate | consent | 1.5 | Selected template zip downloaded and target env resolved — confirm importing unmanaged solution into the org | template-cache |
| `create-site:1.5.update-installed` | gate | consent | 1.5 | Selected template solution is installed but zip is newer — confirm unmanaged update import | template-cache |
| `create-site:1.5.clone-existing` | gate | consent | 1.5 | Selected template solution is same/older — confirm cloning an existing website instead of re-importing | template-cache |
| `create-site:1.5.clone-source-site` | not-a-gate | — | 1.5 | Disambiguate which existing Website Record ID to clone after clone path is already approved | — |
| `create-site:1.5.customize-template` | not-a-gate | — | 1.5 | Optional post-live route choice to download the imported template source for local customization | — |
| `create-site:1.5.template-download-location` | not-a-gate | — | 1.5 | Download folder selection before running `pac pages download-code-site`; no download runs until a path is selected | — |
| `create-site:1.5.clone-failed` | gate | progress | 1.5 | Clone/download/upload failed — choose retry, from-scratch fallback, or stop | partial-template-clone |
| `create-site:1.5.reinstall-unknown` | gate | consent | 1.5 | Installed-solution detection failed — confirm whether to import anyway, start from scratch, or stop | template-cache |
| `create-site:1.5.import-failed` | gate | progress | 1.5 | Import failed or partial — choose retry, from-scratch fallback, or stop | partial-unmanaged-template-import |
| `create-site:3.requirements` | gate | plan | 3 | *"Which features? / Aesthetic / Mood"* — three sub-prompts sharing this gate; shape the rendered Phase 4 plan | nothing |
| `create-site:4.7.plan-approval` | gate | plan | 4.7 | HTML plan rendered — *"Approve and start building / I'd like to make changes"* | nothing |
| `create-site:7.review` | gate | plan | 7 | Live site ready — *"Would you like any changes?"* | nothing |
| `create-site:8.deploy` | gate | plan | 8 | *"Deploy now (Recommended) / Skip for now"* — invokes `/deploy-site` on Yes | nothing |

Template-path note: when Phase 1.5 selects and installs a template, the workflow stops after activation/live preview and does **not** enter the from-scratch Phase 4 plan approval or Phase 8 deploy prompt. Those gates fire only on the from-scratch branch.

---

### 6.14 `deploy-site` (8 calls)
Expand Down
34 changes: 34 additions & 0 deletions plugins/power-pages/scripts/apply-seed-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node
'use strict';

const { applySeedData } = require('./lib/apply-seed-data');
const { formatJsonResult, runBestEffortJsonCli } = require('./lib/template-cli-args');

// Accepted argv shape:
// --seedDir /tmp/powerpages-templates/<sha>/templates/spa/<id>/seed-data
// --envUrl https://org.crm.dynamics.com
// Missing args are returned as `{ ok:false }` so create-site can continue to
// activation; seeding is best-effort and must never block go-live.
function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--seedDir') args.seedDir = argv[++i];
else if (arg === '--envUrl') args.envUrl = argv[++i];
}
return args;
}

async function run(argv = process.argv.slice(2)) {
const args = parseArgs(argv);
if (!args.seedDir || !args.envUrl) {
return { ok: false, inserted: 0, failed: 1, skipped: 0, errors: [{ scope: 'args', message: 'Usage: apply-seed-data.js --seedDir <dir> --envUrl <url>' }] };
}
return applySeedData(args);
}

if (require.main === module) {
runBestEffortJsonCli(() => run());
}

module.exports = { parseArgs, run, formatJsonResult };
36 changes: 36 additions & 0 deletions plugins/power-pages/scripts/capture-pages-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const { execFileSync } = require('child_process');
const { formatJsonResult } = require('./lib/template-cli-args');

// Accepted argv shape:
// --output /tmp/pac-pages-before.txt
// Missing output is reported as JSON rather than throwing so the skill can stop
// before import if the snapshot cannot be captured.
function parseArgs(argv) {
const args = {};
const idx = argv.indexOf('--output');
if (idx !== -1) args.output = argv[idx + 1];
return args;
}

function capturePagesListVerbose({ output }, deps = {}) {
if (!output) return { ok: false, error: 'Usage: capture-pages-list.js --output <file>' };
const execFile = deps.execFileSync || execFileSync;
const fsImpl = deps.fs || fs;
try {
const stdout = execFile('pac', ['pages', 'list', '-v'], { encoding: 'utf8', timeout: 30000 });
fsImpl.writeFileSync(output, stdout, 'utf8');
return { ok: true, output };
} catch (err) {
return { ok: false, error: err.message };
}
}

if (require.main === module) {
process.stdout.write(formatJsonResult(capturePagesListVerbose(parseArgs(process.argv.slice(2)))));
}

module.exports = { parseArgs, capturePagesListVerbose };
111 changes: 111 additions & 0 deletions plugins/power-pages/scripts/check-available-languages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env node
'use strict';

const { makeRequest } = require('./lib/validation-helpers');
const { formatJsonResult } = require('./lib/template-cli-args');

const DEFAULT_REQUIRED_LOCALE_IDS = [1033];

function parseArgs(argv = process.argv.slice(2)) {
const args = {};
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--envUrl') args.envUrl = argv[++i];
else if (argv[i] === '--token') args.token = argv[++i];
else if (argv[i] === '--requiredLocaleIds') {
args.requiredLocaleIds = argv[++i]
.split(',')
.map((value) => Number(value.trim()))
.filter((value) => Number.isInteger(value));
}
}
return args;
}

function normalizeRequiredLocaleIds(requiredLocaleIds) {
const ids = Array.isArray(requiredLocaleIds) && requiredLocaleIds.length > 0
? requiredLocaleIds
: DEFAULT_REQUIRED_LOCALE_IDS;
return [...new Set(ids.filter((id) => Number.isInteger(id)))];
}

async function checkAvailableLanguages({ envUrl, token, requiredLocaleIds, request = makeRequest } = {}) {
if (!envUrl) {
return { ok: false, error: 'Missing --envUrl.' };
}
if (!token) {
return { ok: false, error: 'Missing --token.' };
}

const normalizedEnvUrl = envUrl.replace(/\/+$/, '');
const res = await request({
url: `${normalizedEnvUrl}/api/data/v9.2/RetrieveAvailableLanguages`,
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
},
timeout: 30000,
});

if (res.error) {
return { ok: false, error: `RetrieveAvailableLanguages request failed: ${res.error}` };
}
if (res.statusCode < 200 || res.statusCode >= 300) {
return {
ok: false,
error: `RetrieveAvailableLanguages returned HTTP ${res.statusCode}: ${(res.body || '').slice(0, 400)}`,
};
}

let payload;
try {
payload = JSON.parse(res.body || '{}');
} catch (err) {
return { ok: false, error: `RetrieveAvailableLanguages returned invalid JSON: ${err.message}` };
}

const requiredIds = normalizeRequiredLocaleIds(requiredLocaleIds);
if (requiredIds.length === 0) {
return { ok: false, error: 'At least one required locale id must be provided.' };
}

// Dataverse returns available language LCIDs as:
// { "LocaleIds": [1033, 1036, ...] }
// See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/retrieveavailablelanguages
// Each template declares the LCIDs its solution was authored against in
// `requiredDataverseLanguages`; import is blocked if any required LCID is absent.
if (!Array.isArray(payload.LocaleIds)) {
return { ok: false, error: 'RetrieveAvailableLanguages response did not include a LocaleIds array.' };
}

const localeIds = payload.LocaleIds.filter((id) => Number.isInteger(id));
const missingLocaleIds = requiredIds.filter((id) => !localeIds.includes(id));
return {
ok: true,
hasRequiredLanguages: missingLocaleIds.length === 0,
requiredLocaleIds: requiredIds,
missingLocaleIds,
localeIds,
};
}

async function main() {
const result = await checkAvailableLanguages(parseArgs());
process.stdout.write(formatJsonResult(result));
process.exit(0);
}

if (require.main === module) {
main().catch((err) => {
process.stdout.write(formatJsonResult({ ok: false, error: err.message }));
process.exit(0);
});
}

module.exports = {
DEFAULT_REQUIRED_LOCALE_IDS,
checkAvailableLanguages,
parseArgs,
};
Loading
Loading