pax8-cta 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -0
- package/README.md +659 -0
- package/demo-data/solutions/ProductQADemo_1_0_0_2_managed.zip +0 -0
- package/dist/commands/analyze.d.ts +28 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +571 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/auth.d.ts +18 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/config.d.ts +34 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +299 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/demo.d.ts +33 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +362 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +1186 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/deployments/helpers.d.ts +68 -0
- package/dist/commands/deployments/helpers.d.ts.map +1 -0
- package/dist/commands/deployments/helpers.js +414 -0
- package/dist/commands/deployments/helpers.js.map +1 -0
- package/dist/commands/deployments/index.d.ts +24 -0
- package/dist/commands/deployments/index.d.ts.map +1 -0
- package/dist/commands/deployments/index.js +47 -0
- package/dist/commands/deployments/index.js.map +1 -0
- package/dist/commands/deployments/list.d.ts +18 -0
- package/dist/commands/deployments/list.d.ts.map +1 -0
- package/dist/commands/deployments/list.js +97 -0
- package/dist/commands/deployments/list.js.map +1 -0
- package/dist/commands/deployments/show.d.ts +18 -0
- package/dist/commands/deployments/show.d.ts.map +1 -0
- package/dist/commands/deployments/show.js +81 -0
- package/dist/commands/deployments/show.js.map +1 -0
- package/dist/commands/deployments/undo.d.ts +18 -0
- package/dist/commands/deployments/undo.d.ts.map +1 -0
- package/dist/commands/deployments/undo.js +295 -0
- package/dist/commands/deployments/undo.js.map +1 -0
- package/dist/commands/export.d.ts +18 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +133 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/import.d.ts +18 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +129 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init-config.d.ts +26 -0
- package/dist/commands/init-config.d.ts.map +1 -0
- package/dist/commands/init-config.js +123 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-validation.d.ts +47 -0
- package/dist/commands/init-validation.d.ts.map +1 -0
- package/dist/commands/init-validation.js +339 -0
- package/dist/commands/init-validation.js.map +1 -0
- package/dist/commands/init-wizard.d.ts +25 -0
- package/dist/commands/init-wizard.d.ts.map +1 -0
- package/dist/commands/init-wizard.js +433 -0
- package/dist/commands/init-wizard.js.map +1 -0
- package/dist/commands/init.d.ts +18 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +46 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/resolve-url.d.ts +18 -0
- package/dist/commands/resolve-url.d.ts.map +1 -0
- package/dist/commands/resolve-url.js +126 -0
- package/dist/commands/resolve-url.js.map +1 -0
- package/dist/commands/setup.d.ts +18 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +239 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/solutions/drift-analysis.d.ts +73 -0
- package/dist/commands/solutions/drift-analysis.d.ts.map +1 -0
- package/dist/commands/solutions/drift-analysis.js +416 -0
- package/dist/commands/solutions/drift-analysis.js.map +1 -0
- package/dist/commands/solutions/drift.d.ts +32 -0
- package/dist/commands/solutions/drift.d.ts.map +1 -0
- package/dist/commands/solutions/drift.js +641 -0
- package/dist/commands/solutions/drift.js.map +1 -0
- package/dist/commands/solutions/fix-planner.d.ts +48 -0
- package/dist/commands/solutions/fix-planner.d.ts.map +1 -0
- package/dist/commands/solutions/fix-planner.js +43 -0
- package/dist/commands/solutions/fix-planner.js.map +1 -0
- package/dist/commands/solutions/helpers.d.ts +35 -0
- package/dist/commands/solutions/helpers.d.ts.map +1 -0
- package/dist/commands/solutions/helpers.js +54 -0
- package/dist/commands/solutions/helpers.js.map +1 -0
- package/dist/commands/solutions/index.d.ts +18 -0
- package/dist/commands/solutions/index.d.ts.map +1 -0
- package/dist/commands/solutions/index.js +30 -0
- package/dist/commands/solutions/index.js.map +1 -0
- package/dist/commands/solutions/list.d.ts +18 -0
- package/dist/commands/solutions/list.d.ts.map +1 -0
- package/dist/commands/solutions/list.js +174 -0
- package/dist/commands/solutions/list.js.map +1 -0
- package/dist/commands/solutions/remove.d.ts +18 -0
- package/dist/commands/solutions/remove.d.ts.map +1 -0
- package/dist/commands/solutions/remove.js +137 -0
- package/dist/commands/solutions/remove.js.map +1 -0
- package/dist/commands/solutions/risk-calculator.d.ts +33 -0
- package/dist/commands/solutions/risk-calculator.d.ts.map +1 -0
- package/dist/commands/solutions/risk-calculator.js +79 -0
- package/dist/commands/solutions/risk-calculator.js.map +1 -0
- package/dist/commands/solutions/show.d.ts +18 -0
- package/dist/commands/solutions/show.d.ts.map +1 -0
- package/dist/commands/solutions/show.js +165 -0
- package/dist/commands/solutions/show.js.map +1 -0
- package/dist/commands/status.d.ts +18 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +573 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/telemetry.d.ts +18 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +85 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/tenants/health.d.ts +18 -0
- package/dist/commands/tenants/health.d.ts.map +1 -0
- package/dist/commands/tenants/health.js +172 -0
- package/dist/commands/tenants/health.js.map +1 -0
- package/dist/commands/tenants/helpers.d.ts +44 -0
- package/dist/commands/tenants/helpers.d.ts.map +1 -0
- package/dist/commands/tenants/helpers.js +72 -0
- package/dist/commands/tenants/helpers.js.map +1 -0
- package/dist/commands/tenants/index.d.ts +19 -0
- package/dist/commands/tenants/index.d.ts.map +1 -0
- package/dist/commands/tenants/index.js +39 -0
- package/dist/commands/tenants/index.js.map +1 -0
- package/dist/commands/tenants/inspect.d.ts +18 -0
- package/dist/commands/tenants/inspect.d.ts.map +1 -0
- package/dist/commands/tenants/inspect.js +176 -0
- package/dist/commands/tenants/inspect.js.map +1 -0
- package/dist/commands/tenants/list.d.ts +18 -0
- package/dist/commands/tenants/list.d.ts.map +1 -0
- package/dist/commands/tenants/list.js +144 -0
- package/dist/commands/tenants/list.js.map +1 -0
- package/dist/commands/tenants/manage.d.ts +20 -0
- package/dist/commands/tenants/manage.d.ts.map +1 -0
- package/dist/commands/tenants/manage.js +206 -0
- package/dist/commands/tenants/manage.js.map +1 -0
- package/dist/commands/tenants/show.d.ts +18 -0
- package/dist/commands/tenants/show.d.ts.map +1 -0
- package/dist/commands/tenants/show.js +191 -0
- package/dist/commands/tenants/show.js.map +1 -0
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +536 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +258 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth.d.ts +51 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +153 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/banner.d.ts +19 -0
- package/dist/lib/banner.d.ts.map +1 -0
- package/dist/lib/banner.js +78 -0
- package/dist/lib/banner.js.map +1 -0
- package/dist/lib/command-wrapper.d.ts +56 -0
- package/dist/lib/command-wrapper.d.ts.map +1 -0
- package/dist/lib/command-wrapper.js +71 -0
- package/dist/lib/command-wrapper.js.map +1 -0
- package/dist/lib/credentials.d.ts +56 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +146 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/demo-banner.d.ts +15 -0
- package/dist/lib/demo-banner.d.ts.map +1 -0
- package/dist/lib/demo-banner.js +33 -0
- package/dist/lib/demo-banner.js.map +1 -0
- package/dist/lib/error-handler.d.ts +51 -0
- package/dist/lib/error-handler.d.ts.map +1 -0
- package/dist/lib/error-handler.js +458 -0
- package/dist/lib/error-handler.js.map +1 -0
- package/dist/lib/errors.d.ts +61 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +168 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/formatters.d.ts +55 -0
- package/dist/lib/formatters.d.ts.map +1 -0
- package/dist/lib/formatters.js +163 -0
- package/dist/lib/formatters.js.map +1 -0
- package/dist/lib/graph-client.d.ts +74 -0
- package/dist/lib/graph-client.d.ts.map +1 -0
- package/dist/lib/graph-client.js +231 -0
- package/dist/lib/graph-client.js.map +1 -0
- package/dist/lib/input.d.ts +22 -0
- package/dist/lib/input.d.ts.map +1 -0
- package/dist/lib/input.js +120 -0
- package/dist/lib/input.js.map +1 -0
- package/dist/lib/interactive-wizard.d.ts +26 -0
- package/dist/lib/interactive-wizard.d.ts.map +1 -0
- package/dist/lib/interactive-wizard.js +550 -0
- package/dist/lib/interactive-wizard.js.map +1 -0
- package/dist/lib/oss-surface.d.ts +21 -0
- package/dist/lib/oss-surface.d.ts.map +1 -0
- package/dist/lib/oss-surface.js +29 -0
- package/dist/lib/oss-surface.js.map +1 -0
- package/dist/lib/output.d.ts +74 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +156 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/picker.d.ts +75 -0
- package/dist/lib/picker.d.ts.map +1 -0
- package/dist/lib/picker.js +115 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/lib/repl.d.ts +19 -0
- package/dist/lib/repl.d.ts.map +1 -0
- package/dist/lib/repl.js +158 -0
- package/dist/lib/repl.js.map +1 -0
- package/dist/lib/spinner.d.ts +41 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +126 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/telemetry.d.ts +96 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +367 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Pax8, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { resolve } from "node:path";
|
|
19
|
+
import chalk from "chalk";
|
|
20
|
+
import Table from "cli-table3";
|
|
21
|
+
import { createSpinner, isQuietMode } from "../../lib/spinner.js";
|
|
22
|
+
import { DEMO_TENANTS, getDemoVersionDriftSummary, getDemoTenantVersionStatus, loadConfig, TokenManager, DataverseClient, VersionChecker, DriftAnalyzer, getDemoUnmanagedCustomizations, getDemoCustomizationSummary, } from "@pax8-cta/core";
|
|
23
|
+
import { withDemoMode } from "../../lib/command-wrapper.js";
|
|
24
|
+
import { handleCommandError } from "../../lib/errors.js";
|
|
25
|
+
import { getClientSecretWithFallback } from "../../lib/credentials.js";
|
|
26
|
+
import { isInteractivePrompt, printRunningCommand } from "../../lib/picker.js";
|
|
27
|
+
import { question } from "../../lib/input.js";
|
|
28
|
+
import { resolveFormat } from "../../lib/output.js";
|
|
29
|
+
import { riskLevelValue, formatRiskLevel } from "./risk-calculator.js";
|
|
30
|
+
import { buildDriftFixPlan } from "./fix-planner.js";
|
|
31
|
+
import { buildAfterActionHint, buildSummary, displayCustomizationDetails, displayCustomizationFleetSummary, displayFleetRiskAnalysis, displayFleetSummary, displayTenantRiskAnalysis, displayTenantStatus, generateDemoDeployHistory, selectOutdated, } from "./drift-analysis.js";
|
|
32
|
+
import { showDemoBanner } from "../../lib/demo-banner.js";
|
|
33
|
+
function buildDriftRows(analyses) {
|
|
34
|
+
return analyses.map((a) => {
|
|
35
|
+
const topFactor = a.factors.length > 0
|
|
36
|
+
? a.factors.filter((f) => f.level !== "low").sort((x, y) => y.weight - x.weight)[0]
|
|
37
|
+
?.description || "Minor risk"
|
|
38
|
+
: "-";
|
|
39
|
+
return {
|
|
40
|
+
tenantName: a.tenantName,
|
|
41
|
+
tenantId: a.tenantId,
|
|
42
|
+
score: a.riskScore,
|
|
43
|
+
risk: a.riskLevel,
|
|
44
|
+
recommendation: a.recommendation,
|
|
45
|
+
topFactor,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Apply the same filter + ordering used by `displayFleetRiskAnalysis` so the
|
|
51
|
+
* JSON envelope's `tenants[]` matches what the human-readable table shows.
|
|
52
|
+
*/
|
|
53
|
+
function shapeFleetForOutput(fleet, riskFilter) {
|
|
54
|
+
let analyses = fleet.tenants;
|
|
55
|
+
if (typeof riskFilter === "string") {
|
|
56
|
+
const level = riskFilter.toLowerCase();
|
|
57
|
+
analyses = analyses.filter((a) => a.riskLevel === level);
|
|
58
|
+
}
|
|
59
|
+
const recOrder = {
|
|
60
|
+
do_not_update: 0,
|
|
61
|
+
update_risky: 1,
|
|
62
|
+
review_recommended: 2,
|
|
63
|
+
safe_to_update: 3,
|
|
64
|
+
current: 4,
|
|
65
|
+
};
|
|
66
|
+
return [...analyses].sort((a, b) => (recOrder[a.recommendation] ?? 5) - (recOrder[b.recommendation] ?? 5));
|
|
67
|
+
}
|
|
68
|
+
function emitFleetRiskJson(fleet, riskFilter) {
|
|
69
|
+
const ordered = shapeFleetForOutput(fleet, riskFilter);
|
|
70
|
+
const envelope = {
|
|
71
|
+
tenants: buildDriftRows(ordered),
|
|
72
|
+
summary: fleet.summary,
|
|
73
|
+
};
|
|
74
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
75
|
+
}
|
|
76
|
+
const driftAnalyzer = new DriftAnalyzer();
|
|
77
|
+
/**
|
|
78
|
+
* Build a fresh `drift` Command instance.
|
|
79
|
+
*
|
|
80
|
+
* The same handler is registered under two parents — `solutions drift`
|
|
81
|
+
* (legacy) and `tenants drift` (the canonical name now that the output is
|
|
82
|
+
* primarily per-tenant; see issue #422 for the deeper restructure). Each
|
|
83
|
+
* parent gets its own Command instance because Commander tracks parent
|
|
84
|
+
* pointers per-Command.
|
|
85
|
+
*/
|
|
86
|
+
export function createDriftCommand() {
|
|
87
|
+
return new Command("drift")
|
|
88
|
+
.description("Compare solution versions across tenants to find outdated deployments")
|
|
89
|
+
.option("-a, --agent <name>", "Check specific agent only")
|
|
90
|
+
.option("-t, --tenant <name>", "Check specific tenant only")
|
|
91
|
+
.option("--outdated", "Show only outdated tenants")
|
|
92
|
+
.option("--risk [level]", "Show risk analysis (optionally filter by: low, medium, high)")
|
|
93
|
+
.option("--json", "Output as JSON")
|
|
94
|
+
.option("--quiet", "Suppress all output (exit code only)")
|
|
95
|
+
.option("-c, --config <path>", "Path to config file", "./config/tenants.yaml")
|
|
96
|
+
.option("--fix", "Deploy current version to outdated tenants (risk-gated)")
|
|
97
|
+
.option("--max-risk <level>", "Maximum risk level to auto-fix: low, medium, or high (default: low)", "low")
|
|
98
|
+
.option("--force", "Fix all tenants regardless of risk (same as --max-risk high)")
|
|
99
|
+
.option("--dry-run", "Show what would be fixed without executing")
|
|
100
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
101
|
+
.addHelpText("after", `
|
|
102
|
+
Examples:
|
|
103
|
+
solutions drift Show fleet-wide version drift summary
|
|
104
|
+
solutions drift -t Pax8CTA-Test2 Check drift for a specific tenant
|
|
105
|
+
solutions drift --outdated Show only outdated tenants
|
|
106
|
+
solutions drift --risk Show drift with risk scores
|
|
107
|
+
solutions drift --risk high Show only high-risk tenants
|
|
108
|
+
solutions drift --fix Fix low-risk outdated tenants
|
|
109
|
+
solutions drift --fix --force Fix all outdated tenants
|
|
110
|
+
`)
|
|
111
|
+
.action(async (options, cmd) => {
|
|
112
|
+
// Merge local opts with globals so root-level `--json` / `--quiet`
|
|
113
|
+
// (`pax8-cta --json solutions drift ...`) reach this command. Then
|
|
114
|
+
// resolve the effective output format up-front so every branch below
|
|
115
|
+
// can branch on a single `fmt` value (issue #401).
|
|
116
|
+
const merged = { ...options, ...cmd.optsWithGlobals() };
|
|
117
|
+
const fmt = resolveFormat({
|
|
118
|
+
json: !!merged.json,
|
|
119
|
+
quiet: !!merged.quiet,
|
|
120
|
+
});
|
|
121
|
+
// Back-compat shim for the after-action helper, which only inspects
|
|
122
|
+
// `options.json`. Treat `fmt === "json"` as "JSON requested" so piped
|
|
123
|
+
// (non-TTY) callers also suppress the picker chrome.
|
|
124
|
+
const jsonOutput = fmt === "json" || fmt === "quiet";
|
|
125
|
+
options = { ...options, json: jsonOutput };
|
|
126
|
+
const spinner = createSpinner("Checking version drift...").start();
|
|
127
|
+
try {
|
|
128
|
+
await withDemoMode(async () => {
|
|
129
|
+
spinner.stop();
|
|
130
|
+
if (fmt === "table") {
|
|
131
|
+
showDemoBanner();
|
|
132
|
+
}
|
|
133
|
+
const enabledTenants = DEMO_TENANTS.filter((t) => t.enabled);
|
|
134
|
+
// --fix mode: deploy current version to outdated tenants
|
|
135
|
+
if (options.fix) {
|
|
136
|
+
const maxRisk = options.force ? "high" : options.maxRisk;
|
|
137
|
+
if (!["low", "medium", "high"].includes(maxRisk)) {
|
|
138
|
+
console.log(chalk.red(`Invalid --max-risk value: '${maxRisk}'. Use low, medium, or high.`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
// Build tenant version statuses
|
|
142
|
+
let enabledTenants = DEMO_TENANTS.filter((t) => t.enabled);
|
|
143
|
+
// Filter to specific tenant if requested
|
|
144
|
+
if (options.tenant) {
|
|
145
|
+
enabledTenants = enabledTenants.filter((t) => t.name.toLowerCase().includes(options.tenant.toLowerCase()) ||
|
|
146
|
+
t.tenantId.toLowerCase().includes(options.tenant.toLowerCase()));
|
|
147
|
+
if (enabledTenants.length === 0) {
|
|
148
|
+
console.log(chalk.red(`Tenant '${options.tenant}' not found`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const tenantStatuses = enabledTenants.map((tenant) => ({
|
|
153
|
+
tenant,
|
|
154
|
+
status: getDemoTenantVersionStatus(tenant.tenantId),
|
|
155
|
+
}));
|
|
156
|
+
const plan = buildDriftFixPlan(tenantStatuses);
|
|
157
|
+
if (plan.length === 0) {
|
|
158
|
+
console.log(chalk.green("All tenants are up to date. Nothing to fix."));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const maxRiskValue = riskLevelValue(maxRisk);
|
|
162
|
+
// Categorize entries
|
|
163
|
+
const willFix = plan.filter((e) => riskLevelValue(e.risk) <= maxRiskValue);
|
|
164
|
+
const willSkip = plan.filter((e) => riskLevelValue(e.risk) > maxRiskValue);
|
|
165
|
+
// Display the fix plan
|
|
166
|
+
console.log(chalk.bold("Drift Fix Plan:"));
|
|
167
|
+
for (const entry of plan) {
|
|
168
|
+
const riskVal = riskLevelValue(entry.risk);
|
|
169
|
+
const included = riskVal <= maxRiskValue;
|
|
170
|
+
const versions = entry.outdatedSolutions
|
|
171
|
+
.map((s) => `${s.deployedVersion || "none"} -> ${s.expectedVersion}`)
|
|
172
|
+
.join(", ");
|
|
173
|
+
if (included) {
|
|
174
|
+
if (entry.risk === "low") {
|
|
175
|
+
console.log(chalk.green(` ✓ ${entry.tenantName} ${versions} (low risk -- safe)`));
|
|
176
|
+
}
|
|
177
|
+
else if (entry.risk === "medium") {
|
|
178
|
+
console.log(chalk.yellow(` ⚠ ${entry.tenantName} ${versions} (medium risk -- included)`));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(chalk.red(` ✗ ${entry.tenantName} ${versions} (high risk -- included)`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (entry.risk === "medium") {
|
|
186
|
+
console.log(chalk.yellow(` ⚠ ${entry.tenantName} ${versions} (medium risk -- SKIPPED)`));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(chalk.red(` ✗ ${entry.tenantName} ${versions} (high risk -- SKIPPED)`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
if (willFix.length === 0) {
|
|
195
|
+
console.log(chalk.yellow(`No tenants within --max-risk=${maxRisk} threshold. Use --max-risk medium or --force to include higher-risk tenants.`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log(`Will update ${willFix.length} of ${plan.length} outdated tenant${plan.length !== 1 ? "s" : ""}.` +
|
|
199
|
+
(willSkip.length > 0
|
|
200
|
+
? ` ${willSkip.length} skipped (risk above ${maxRisk}).`
|
|
201
|
+
: ""));
|
|
202
|
+
if (fmt === "json") {
|
|
203
|
+
console.log(JSON.stringify({
|
|
204
|
+
plan,
|
|
205
|
+
willFix: willFix.map((e) => e.tenantName),
|
|
206
|
+
willSkip: willSkip.map((e) => ({ tenantName: e.tenantName, risk: e.risk })),
|
|
207
|
+
maxRisk,
|
|
208
|
+
dryRun: !!options.dryRun,
|
|
209
|
+
}, null, 2));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (options.dryRun) {
|
|
213
|
+
console.log(chalk.gray("\n--dry-run: No changes were made."));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Confirmation prompt (skip if --yes)
|
|
217
|
+
if (!options.yes) {
|
|
218
|
+
// In demo mode tests, we skip the interactive prompt.
|
|
219
|
+
// The readline import is deferred to avoid issues in test environments.
|
|
220
|
+
const readline = await import("node:readline");
|
|
221
|
+
const rl = readline.createInterface({
|
|
222
|
+
input: process.stdin,
|
|
223
|
+
output: process.stdout,
|
|
224
|
+
});
|
|
225
|
+
const answer = await new Promise((resolve) => {
|
|
226
|
+
rl.question("Continue? [y/N] ", (ans) => {
|
|
227
|
+
rl.close();
|
|
228
|
+
resolve(ans.trim().toLowerCase());
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
if (answer !== "y" && answer !== "yes") {
|
|
232
|
+
console.log(chalk.gray("Aborted."));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Execute fixes (simulated in demo mode)
|
|
237
|
+
console.log();
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const entry of willFix) {
|
|
240
|
+
const fixSpinner = createSpinner(`Updating ${entry.tenantName}...`).start();
|
|
241
|
+
// Simulate deployment delay
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
243
|
+
// In demo mode, simulate success
|
|
244
|
+
fixSpinner.succeed(`${entry.tenantName} updated successfully`);
|
|
245
|
+
results.push({
|
|
246
|
+
tenantName: entry.tenantName,
|
|
247
|
+
tenantId: entry.tenantId,
|
|
248
|
+
status: "updated",
|
|
249
|
+
risk: entry.risk,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Add skipped entries to results
|
|
253
|
+
for (const entry of willSkip) {
|
|
254
|
+
results.push({
|
|
255
|
+
tenantName: entry.tenantName,
|
|
256
|
+
tenantId: entry.tenantId,
|
|
257
|
+
status: "skipped_risk",
|
|
258
|
+
risk: entry.risk,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// Summary
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(chalk.bold("Results:"));
|
|
264
|
+
const updated = results.filter((r) => r.status === "updated").length;
|
|
265
|
+
const skippedRisk = results.filter((r) => r.status === "skipped_risk").length;
|
|
266
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
267
|
+
console.log(chalk.green(` Updated: ${updated}`));
|
|
268
|
+
if (skippedRisk > 0) {
|
|
269
|
+
console.log(chalk.yellow(` Skipped (risk): ${skippedRisk}`));
|
|
270
|
+
}
|
|
271
|
+
if (failed > 0) {
|
|
272
|
+
console.log(chalk.red(` Failed: ${failed}`));
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Single tenant mode
|
|
277
|
+
if (options.tenant) {
|
|
278
|
+
const tenant = enabledTenants.find((t) => t.name.toLowerCase().includes(options.tenant.toLowerCase()) ||
|
|
279
|
+
t.tenantId.toLowerCase().includes(options.tenant.toLowerCase()));
|
|
280
|
+
if (!tenant) {
|
|
281
|
+
console.log(chalk.red(`Tenant '${options.tenant}' not found`));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const status = getDemoTenantVersionStatus(tenant.tenantId);
|
|
285
|
+
if (!status) {
|
|
286
|
+
console.log(chalk.red(`Could not get version status for tenant`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
// Also get unmanaged customizations for this tenant
|
|
290
|
+
const customizationResult = getDemoUnmanagedCustomizations(tenant.tenantId, "CustomerServiceAgent");
|
|
291
|
+
if (options.risk !== undefined) {
|
|
292
|
+
const history = generateDemoDeployHistory(tenant.tenantId);
|
|
293
|
+
const analysis = driftAnalyzer.analyzeTenant(tenant, status, history);
|
|
294
|
+
if (fmt === "json") {
|
|
295
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (fmt === "quiet")
|
|
299
|
+
return;
|
|
300
|
+
displayTenantRiskAnalysis(analysis);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (fmt === "json") {
|
|
304
|
+
console.log(JSON.stringify({ ...status, customizations: customizationResult }, null, 2));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (fmt === "quiet")
|
|
308
|
+
return;
|
|
309
|
+
displayTenantStatus(status);
|
|
310
|
+
// Show unmanaged customizations section
|
|
311
|
+
displayCustomizationDetails(customizationResult);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Fleet-wide view
|
|
315
|
+
if (options.risk !== undefined) {
|
|
316
|
+
const statuses = enabledTenants.map((t) => getDemoTenantVersionStatus(t.tenantId));
|
|
317
|
+
const histories = new Map();
|
|
318
|
+
for (const t of enabledTenants) {
|
|
319
|
+
histories.set(t.tenantId, generateDemoDeployHistory(t.tenantId));
|
|
320
|
+
}
|
|
321
|
+
const fleetAnalysis = driftAnalyzer.analyzeFleet(enabledTenants, statuses, histories);
|
|
322
|
+
if (fmt === "json") {
|
|
323
|
+
emitFleetRiskJson(fleetAnalysis, options.risk);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (fmt === "quiet")
|
|
327
|
+
return;
|
|
328
|
+
displayFleetRiskAnalysis(fleetAnalysis, options.risk);
|
|
329
|
+
await afterDriftReport(fleetAnalysis, { ...options, json: jsonOutput },
|
|
330
|
+
/* isDemo */ true);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Fleet-wide summary (no risk)
|
|
334
|
+
const summary = getDemoVersionDriftSummary();
|
|
335
|
+
const customizationSummary = getDemoCustomizationSummary("CustomerServiceAgent");
|
|
336
|
+
if (fmt === "json") {
|
|
337
|
+
console.log(JSON.stringify({ ...summary, customizations: customizationSummary }, null, 2));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (fmt === "quiet")
|
|
341
|
+
return;
|
|
342
|
+
console.log(chalk.bold("Version Drift Summary"));
|
|
343
|
+
console.log("━".repeat(60));
|
|
344
|
+
console.log();
|
|
345
|
+
// Fleet overview
|
|
346
|
+
const currentPct = Math.round((summary.currentTenants / summary.totalTenants) * 100);
|
|
347
|
+
console.log(`Tenants: ${summary.totalTenants} total`);
|
|
348
|
+
console.log(` ${chalk.green("✓")} Current: ${summary.currentTenants} (${currentPct}%)`);
|
|
349
|
+
console.log(` ${chalk.yellow("⚠")} Outdated: ${summary.outdatedTenants}`);
|
|
350
|
+
if (summary.unknownTenants > 0) {
|
|
351
|
+
console.log(` ${chalk.gray("?")} Unknown: ${summary.unknownTenants}`);
|
|
352
|
+
}
|
|
353
|
+
console.log();
|
|
354
|
+
// Per-solution breakdown
|
|
355
|
+
console.log(chalk.bold("Per-Agent Status"));
|
|
356
|
+
console.log("─".repeat(60));
|
|
357
|
+
const solutionTable = new Table({
|
|
358
|
+
head: ["Agent", "Version", "Current", "Outdated", "Not Deployed"],
|
|
359
|
+
style: { head: ["cyan"] },
|
|
360
|
+
});
|
|
361
|
+
let filteredSummary = summary.solutionSummary;
|
|
362
|
+
if (options.agent) {
|
|
363
|
+
filteredSummary = filteredSummary.filter((s) => s.uniqueName.toLowerCase().includes(options.agent.toLowerCase()) ||
|
|
364
|
+
s.friendlyName.toLowerCase().includes(options.agent.toLowerCase()));
|
|
365
|
+
}
|
|
366
|
+
filteredSummary.forEach((sol) => {
|
|
367
|
+
solutionTable.push([
|
|
368
|
+
sol.uniqueName,
|
|
369
|
+
sol.expectedVersion,
|
|
370
|
+
chalk.green(sol.tenantsAtVersion.toString()),
|
|
371
|
+
sol.tenantsBehind > 0 ? chalk.yellow(sol.tenantsBehind.toString()) : "0",
|
|
372
|
+
sol.tenantsNotDeployed > 0 ? chalk.gray(sol.tenantsNotDeployed.toString()) : "0",
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
375
|
+
console.log(solutionTable.toString());
|
|
376
|
+
// Show outdated tenants if requested
|
|
377
|
+
if (options.outdated) {
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(chalk.bold("Outdated Tenants"));
|
|
380
|
+
console.log("─".repeat(60));
|
|
381
|
+
const outdatedTable = new Table({
|
|
382
|
+
head: ["Tenant", "Agent", "Deployed", "Expected"],
|
|
383
|
+
style: { head: ["cyan"] },
|
|
384
|
+
});
|
|
385
|
+
enabledTenants.forEach((tenant) => {
|
|
386
|
+
const status = getDemoTenantVersionStatus(tenant.tenantId);
|
|
387
|
+
if (!status)
|
|
388
|
+
return;
|
|
389
|
+
status.solutions
|
|
390
|
+
.filter((s) => s.status === "outdated")
|
|
391
|
+
.forEach((sol) => {
|
|
392
|
+
outdatedTable.push([
|
|
393
|
+
tenant.name,
|
|
394
|
+
sol.uniqueName,
|
|
395
|
+
chalk.yellow(sol.deployedVersion || "-"),
|
|
396
|
+
sol.expectedVersion,
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
console.log(outdatedTable.toString());
|
|
401
|
+
}
|
|
402
|
+
// Show unmanaged customizations fleet summary
|
|
403
|
+
displayCustomizationFleetSummary(customizationSummary);
|
|
404
|
+
console.log();
|
|
405
|
+
console.log(chalk.gray("Tip: Use --risk to see risk scores and update recommendations"));
|
|
406
|
+
}, async () => {
|
|
407
|
+
// Production mode — query real environments
|
|
408
|
+
const configPath = resolve(process.cwd(), options.config);
|
|
409
|
+
const config = await loadConfig(configPath);
|
|
410
|
+
const clientSecret = await getClientSecretWithFallback();
|
|
411
|
+
// Get expected solutions from source environment
|
|
412
|
+
const sourceTokenManager = new TokenManager({
|
|
413
|
+
tenantId: config.source?.tenantId || config.partner.tenantId,
|
|
414
|
+
clientId: config.partner.clientId,
|
|
415
|
+
clientSecret,
|
|
416
|
+
});
|
|
417
|
+
const sourceClient = new DataverseClient({
|
|
418
|
+
environmentUrl: config.source?.environmentUrl || config.tenants[0]?.environmentUrl,
|
|
419
|
+
tokenManager: sourceTokenManager,
|
|
420
|
+
clientId: config.partner.clientId,
|
|
421
|
+
});
|
|
422
|
+
spinner.text = "Querying source environment for solution versions...";
|
|
423
|
+
const sourceSolutions = await sourceClient.querySolutions();
|
|
424
|
+
// Filter to non-system solutions (visible, non-default)
|
|
425
|
+
const expectedSolutions = sourceSolutions
|
|
426
|
+
.filter((s) => s.uniquename !== "Default" &&
|
|
427
|
+
s.uniquename !== "Active" &&
|
|
428
|
+
!s.uniquename.startsWith("msdyn_") &&
|
|
429
|
+
!s.uniquename.startsWith("msft_") &&
|
|
430
|
+
!s.uniquename.startsWith("mspcat_"))
|
|
431
|
+
.map((s) => ({
|
|
432
|
+
uniqueName: s.uniquename,
|
|
433
|
+
friendlyName: s.friendlyname,
|
|
434
|
+
version: s.version,
|
|
435
|
+
}));
|
|
436
|
+
if (expectedSolutions.length === 0) {
|
|
437
|
+
spinner.warn("No custom solutions found in source environment");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Determine which tenants to check
|
|
441
|
+
let tenants = config.tenants.filter((t) => t.enabled);
|
|
442
|
+
if (options.tenant) {
|
|
443
|
+
const match = tenants.find((t) => t.name.toLowerCase().includes(options.tenant.toLowerCase()) ||
|
|
444
|
+
t.tenantId.toLowerCase().includes(options.tenant.toLowerCase()));
|
|
445
|
+
if (!match) {
|
|
446
|
+
spinner.fail(chalk.red(`Tenant '${options.tenant}' not found`));
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
tenants = [match];
|
|
450
|
+
}
|
|
451
|
+
spinner.text = `Checking ${tenants.length} tenant(s) for version drift...`;
|
|
452
|
+
// Check each tenant
|
|
453
|
+
const checker = new VersionChecker();
|
|
454
|
+
const statuses = [];
|
|
455
|
+
for (const tenant of tenants) {
|
|
456
|
+
spinner.text = `Checking ${tenant.name}...`;
|
|
457
|
+
const tm = new TokenManager({
|
|
458
|
+
tenantId: tenant.tenantId,
|
|
459
|
+
clientId: config.partner.clientId,
|
|
460
|
+
clientSecret,
|
|
461
|
+
});
|
|
462
|
+
const status = await checker.checkTenantVersions(tenant, expectedSolutions, tm, true);
|
|
463
|
+
statuses.push(status);
|
|
464
|
+
}
|
|
465
|
+
spinner.stop();
|
|
466
|
+
// Risk analysis mode
|
|
467
|
+
if (options.risk !== undefined) {
|
|
468
|
+
// Build deployment history from Dataverse solution history
|
|
469
|
+
const histories = new Map();
|
|
470
|
+
// TODO (#258): Query real deployment history per tenant for richer risk scoring.
|
|
471
|
+
// For now, risk scoring works with version drift + tags (no deploy history).
|
|
472
|
+
if (options.tenant && statuses.length === 1) {
|
|
473
|
+
const analysis = driftAnalyzer.analyzeTenant(tenants[0], statuses[0], histories.get(tenants[0].tenantId));
|
|
474
|
+
if (fmt === "json") {
|
|
475
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (fmt === "quiet")
|
|
479
|
+
return;
|
|
480
|
+
displayTenantRiskAnalysis(analysis);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const fleetAnalysis = driftAnalyzer.analyzeFleet(tenants, statuses, histories);
|
|
484
|
+
if (fmt === "json") {
|
|
485
|
+
emitFleetRiskJson(fleetAnalysis, options.risk);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (fmt === "quiet")
|
|
489
|
+
return;
|
|
490
|
+
displayFleetRiskAnalysis(fleetAnalysis, options.risk);
|
|
491
|
+
await afterDriftReport(fleetAnalysis, { ...options, json: jsonOutput },
|
|
492
|
+
/* isDemo */ false);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Single tenant mode (no risk)
|
|
496
|
+
if (options.tenant && statuses.length === 1) {
|
|
497
|
+
const status = statuses[0];
|
|
498
|
+
if (fmt === "json") {
|
|
499
|
+
console.log(JSON.stringify(status, null, 2));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (fmt === "quiet")
|
|
503
|
+
return;
|
|
504
|
+
displayTenantStatus(status);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Fleet-wide summary (no risk)
|
|
508
|
+
const summary = buildSummary(statuses, expectedSolutions);
|
|
509
|
+
if (fmt === "json") {
|
|
510
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (fmt === "quiet")
|
|
514
|
+
return;
|
|
515
|
+
displayFleetSummary(summary, options, tenants, statuses, checker, expectedSolutions);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
handleCommandError(error, spinner, "Failed to check version drift");
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Pick a stable label for the solution we'd suggest the user act on. Drift
|
|
525
|
+
* is per-fleet today, so when the user didn't scope with `-a/--agent` we try
|
|
526
|
+
* to learn it from the analysis (the riskiest tenant's first outdated
|
|
527
|
+
* solution) and otherwise fall back to `<solution>` so the printed hint is
|
|
528
|
+
* still readable as a template.
|
|
529
|
+
*/
|
|
530
|
+
function resolveSolutionLabel(fleet, options) {
|
|
531
|
+
if (options.agent && options.agent.trim().length > 0) {
|
|
532
|
+
return options.agent;
|
|
533
|
+
}
|
|
534
|
+
for (const t of fleet.tenants) {
|
|
535
|
+
const first = t.outdatedSolutions[0];
|
|
536
|
+
if (first?.uniqueName)
|
|
537
|
+
return first.uniqueName;
|
|
538
|
+
}
|
|
539
|
+
return "<solution>";
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Sort outdated tenants for display: highest risk score first, ties broken
|
|
543
|
+
* alphabetically so the picker is deterministic across runs.
|
|
544
|
+
*/
|
|
545
|
+
function sortOutdated(outdated) {
|
|
546
|
+
return [...outdated].sort((a, b) => {
|
|
547
|
+
if (b.riskScore !== a.riskScore)
|
|
548
|
+
return b.riskScore - a.riskScore;
|
|
549
|
+
return a.tenantName.localeCompare(b.tenantName);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function maxVersionDrift(t) {
|
|
553
|
+
if (t.outdatedSolutions.length === 0)
|
|
554
|
+
return 0;
|
|
555
|
+
return Math.max(...t.outdatedSolutions.map((s) => Math.abs(s.versionDrift)));
|
|
556
|
+
}
|
|
557
|
+
async function afterDriftReport(fleet, options, isDemo) {
|
|
558
|
+
// --json and --quiet suppress all post-report chrome: callers rely on a
|
|
559
|
+
// clean machine-parseable stream (or no stream at all).
|
|
560
|
+
if (options.json)
|
|
561
|
+
return;
|
|
562
|
+
if (isQuietMode())
|
|
563
|
+
return;
|
|
564
|
+
const outdated = selectOutdated(fleet);
|
|
565
|
+
const solutionLabel = resolveSolutionLabel(fleet, options);
|
|
566
|
+
// Hint always renders (including the "fleet is current" case) — it's the
|
|
567
|
+
// discoverable nudge at the bottom of the report.
|
|
568
|
+
const hint = buildAfterActionHint(outdated, solutionLabel);
|
|
569
|
+
console.log();
|
|
570
|
+
console.log(chalk.gray(hint));
|
|
571
|
+
// Picker only fires when we have something to act on, the caller is a real
|
|
572
|
+
// human in a TTY, and they didn't already pass `--fix` (we don't want to
|
|
573
|
+
// double-prompt for an explicit fix run).
|
|
574
|
+
if (outdated.length === 0)
|
|
575
|
+
return;
|
|
576
|
+
if (options.fix)
|
|
577
|
+
return;
|
|
578
|
+
if (!isInteractivePrompt({ json: options.json }))
|
|
579
|
+
return;
|
|
580
|
+
await promptDriftPicker(sortOutdated(outdated), solutionLabel, isDemo);
|
|
581
|
+
}
|
|
582
|
+
async function promptDriftPicker(outdated, solutionLabel, isDemo) {
|
|
583
|
+
console.log();
|
|
584
|
+
console.log(chalk.cyan("Update an outdated tenant now? Pick:"));
|
|
585
|
+
// Pad tenant names so the [risk — N versions behind] hint lines up.
|
|
586
|
+
const widest = outdated.reduce((m, t) => Math.max(m, t.tenantName.length), 0);
|
|
587
|
+
outdated.forEach((t, i) => {
|
|
588
|
+
const drift = maxVersionDrift(t);
|
|
589
|
+
const driftLabel = drift === 1 ? "1 version behind" : `${drift} versions behind`;
|
|
590
|
+
const riskLabel = formatRiskLevel(t.riskLevel).padEnd(4);
|
|
591
|
+
const padded = t.tenantName.padEnd(widest);
|
|
592
|
+
console.log(` ${i + 1}) ${padded} ${chalk.gray(`[${riskLabel} — ${driftLabel}]`)}`);
|
|
593
|
+
});
|
|
594
|
+
console.log(` ${chalk.bold("F")}) fix all outdated (runs solutions drift --fix)`);
|
|
595
|
+
console.log(chalk.gray(" 0) skip"));
|
|
596
|
+
const answer = (await question(chalk.cyan("> "))).trim();
|
|
597
|
+
if (answer === "" || answer === "0")
|
|
598
|
+
return;
|
|
599
|
+
if (answer.toLowerCase() === "f") {
|
|
600
|
+
printRunningCommand(["solutions", "drift", "--fix"]);
|
|
601
|
+
await runDriftFix(isDemo);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const choice = parseInt(answer, 10);
|
|
605
|
+
if (!Number.isInteger(choice) || choice < 1 || choice > outdated.length)
|
|
606
|
+
return;
|
|
607
|
+
const target = outdated[choice - 1];
|
|
608
|
+
printRunningCommand(["deploy", solutionLabel, "--tenant", target.tenantName]);
|
|
609
|
+
await runDeploy(solutionLabel, target.tenantName, isDemo);
|
|
610
|
+
}
|
|
611
|
+
function spawnSelf(args, isDemo) {
|
|
612
|
+
// Mirrors `runDeploy` in analyze.ts: re-invoke the same CLI binary as a
|
|
613
|
+
// child process so the spawned command gets its own commander parse and
|
|
614
|
+
// we don't tangle commander state with the active drift run. stdin is
|
|
615
|
+
// ignored — neither deploy nor `drift --fix` need user input in this
|
|
616
|
+
// post-report flow (and `--fix` defaults to confirm-prompted, but the user
|
|
617
|
+
// can re-run with `--yes` themselves if they want non-interactive).
|
|
618
|
+
return new Promise((resolveSpawn, reject) => {
|
|
619
|
+
const isBundled = !process.argv[1] || process.argv[1] === process.execPath;
|
|
620
|
+
const spawnArgs = isBundled ? args : [process.argv[1], ...args];
|
|
621
|
+
const env = { ...process.env };
|
|
622
|
+
if (isDemo)
|
|
623
|
+
env.DEMO_MODE = "true";
|
|
624
|
+
const proc = spawn(process.execPath, spawnArgs, {
|
|
625
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
626
|
+
env,
|
|
627
|
+
});
|
|
628
|
+
proc.on("close", () => resolveSpawn());
|
|
629
|
+
proc.on("error", reject);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
async function runDeploy(solution, tenantName, isDemo) {
|
|
633
|
+
return spawnSelf(["deploy", solution, "--tenant", tenantName], isDemo);
|
|
634
|
+
}
|
|
635
|
+
async function runDriftFix(isDemo) {
|
|
636
|
+
return spawnSelf(["solutions", "drift", "--fix"], isDemo);
|
|
637
|
+
}
|
|
638
|
+
export { buildAfterActionHint } from "./drift-analysis.js";
|
|
639
|
+
export { calculateDriftRisk } from "./risk-calculator.js";
|
|
640
|
+
export { buildDriftFixPlan } from "./fix-planner.js";
|
|
641
|
+
//# sourceMappingURL=drift.js.map
|