|
| 1 | +#!/usr/bin/env node |
| 2 | +'use strict'; |
| 3 | + |
| 4 | +// list-environments.js — enumerate the Dataverse environments the signed-in PAC |
| 5 | +// user can access, as JSON, for ENV_LIST pre-fill (plan-alm / setup-pipeline / |
| 6 | +// ensure-pipelines-host prompt the user with known env URLs). |
| 7 | +// |
| 8 | +// Why this exists: |
| 9 | +// The skills historically ran `pac env list --output json`. That is INVALID on |
| 10 | +// current PAC CLI (verified on 2.8.1): `pac env list` accepts only `--filter` |
| 11 | +// and errors with "An unknown argument --output was passed", so the JSON |
| 12 | +// pre-fill silently never worked. `pac env list` DOES emit a plain table with |
| 13 | +// an "Environment URL" column, so this helper runs the plain command and parses |
| 14 | +// that table into JSON. (`pac admin list --json` also yields JSON but is |
| 15 | +// admin-only and enumerates the WHOLE tenant — wrong scope for a per-user |
| 16 | +// pre-fill — so we deliberately parse `pac env list` instead.) |
| 17 | +// |
| 18 | +// Usage: |
| 19 | +// node list-environments.js -> prints JSON array to stdout |
| 20 | +// |
| 21 | +// Output (JSON array; empty [] when PAC is unauthenticated / the command fails — |
| 22 | +// the pre-fill is best-effort and callers degrade gracefully to manual entry): |
| 23 | +// [ { "displayName": "...", "environmentId": "...", "environmentUrl": "https://…", |
| 24 | +// "uniqueName": "...", "active": true|false }, ... ] |
| 25 | +// |
| 26 | +// Exit 0 always (callers parse stdout; [] means "no pre-fill available"). |
| 27 | + |
| 28 | +const { execSync } = require('child_process'); |
| 29 | + |
| 30 | +// Parse the plain `pac env list` table. Pure + exported for unit testing. |
| 31 | +// Example real output (PAC 2.8.1) — note the header row, the "Connected as" banner |
| 32 | +// line, and that the active env is flagged with `*` in the leading "Active" column: |
| 33 | +// |
| 34 | +// Connected as admin@contoso.onmicrosoft.com |
| 35 | +// Active Display Name Environment ID Environment URL Unique Name |
| 36 | +// * Contoso Dev d664a1f5-5c5b-efbf-9cc9-c1923c437109 https://contosodev.crm.dynamics.com/ unq78bd16d6e4baf01189f56045bd003 |
| 37 | +// Contoso Prod e8ccb697-db78-e2d6-b721-ef23eedbc302 https://contosoprod.crm4.dynamics.com/ unqe4574a3ea1bff01195c56045bd03c |
| 38 | +// |
| 39 | +// Display names contain spaces and variable padding, so we anchor on the three |
| 40 | +// unambiguous tokens that always appear in order — the 36-char environment GUID, |
| 41 | +// the https URL, and the trailing unique name — and treat everything before the |
| 42 | +// GUID as `[activeMarker] + displayName`. |
| 43 | +function parseEnvList(stdout) { |
| 44 | + if (!stdout || typeof stdout !== 'string') return []; |
| 45 | + const rows = []; |
| 46 | + for (const rawLine of stdout.split(/\r?\n/)) { |
| 47 | + const line = rawLine.replace(/\s+$/, ''); |
| 48 | + if (!line.trim()) continue; |
| 49 | + // Skip the "Connected as ..." banner and the column header row. |
| 50 | + if (/^Connected as\b/i.test(line.trim())) continue; |
| 51 | + if (/^Active\s+Display Name\b/i.test(line.trim())) continue; |
| 52 | + |
| 53 | + // prefix = (optional `*` active marker) + display name; then GUID, URL, uniqueName. |
| 54 | + const m = line.match( |
| 55 | + /^(.*?)\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+(https:\/\/\S+)\s+(\S+)\s*$/i, |
| 56 | + ); |
| 57 | + if (!m) continue; |
| 58 | + const prefix = m[1]; |
| 59 | + // The active env is flagged with a leading `*` in the "Active" column. |
| 60 | + const active = /^\s*\*/.test(prefix); |
| 61 | + const displayName = prefix.replace(/^\s*\*?\s*/, '').trim(); |
| 62 | + rows.push({ |
| 63 | + displayName, |
| 64 | + environmentId: m[2], |
| 65 | + environmentUrl: m[3].replace(/\/+$/, ''), |
| 66 | + uniqueName: m[4], |
| 67 | + active, |
| 68 | + }); |
| 69 | + } |
| 70 | + return rows; |
| 71 | +} |
| 72 | + |
| 73 | +function listEnvironments() { |
| 74 | + let stdout = ''; |
| 75 | + try { |
| 76 | + stdout = execSync('pac env list', { encoding: 'utf8', timeout: 20000 }); |
| 77 | + } catch (e) { |
| 78 | + // Best-effort: an unauthenticated / failing PAC CLI yields no pre-fill, not an |
| 79 | + // error — callers (plan-alm Phase 1, setup-pipeline) fall back to manual entry. |
| 80 | + // `pac` writes its table to stdout even on some non-zero exits, so try to parse |
| 81 | + // whatever was captured before giving up. |
| 82 | + stdout = (e && (e.stdout || '')) || ''; |
| 83 | + } |
| 84 | + return parseEnvList(stdout); |
| 85 | +} |
| 86 | + |
| 87 | +if (require.main === module) { |
| 88 | + // Never throw to the caller — emit [] on any failure so the consumer always |
| 89 | + // receives parseable JSON. |
| 90 | + let result = []; |
| 91 | + try { result = listEnvironments(); } catch { result = []; } |
| 92 | + process.stdout.write(JSON.stringify(result) + '\n'); |
| 93 | + process.exit(0); |
| 94 | +} |
| 95 | + |
| 96 | +module.exports = { parseEnvList, listEnvironments }; |
0 commit comments