azp-cli 0.0.3 → 1.0.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/.github/copilot-instructions.md +8 -0
- package/CHANGELOG.md +20 -7
- package/README.md +64 -0
- package/dist/index.js +414 -15
- package/dist/index.js.map +1 -1
- package/dist/presets-cli.d.ts +29 -0
- package/dist/presets-cli.d.ts.map +1 -0
- package/dist/presets-cli.js +397 -0
- package/dist/presets-cli.js.map +1 -0
- package/dist/presets.d.ts +52 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +208 -0
- package/dist/presets.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +499 -18
- package/src/presets-cli.ts +500 -0
- package/src/presets.ts +253 -0
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- This is a Node.js/TypeScript terminal CLI for Azure PIM role activation/deactivation.
|
|
6
6
|
- Entry point: [src/index.ts](src/index.ts) (Commander commands; default is `activate`).
|
|
7
7
|
- Interactive flows: [src/cli.ts](src/cli.ts) (Inquirer menus + loops; calls Azure operations).
|
|
8
|
+
- Presets (config + merge logic): [src/presets.ts](src/presets.ts) (JSON file in user config dir; template expansion).
|
|
9
|
+
- Presets (interactive wizards): [src/presets-cli.ts](src/presets-cli.ts) (Inquirer-based add/edit flows; can query Azure for subscriptions/roles).
|
|
8
10
|
- Auth: [src/auth.ts](src/auth.ts) (Azure CLI credential + Microsoft Graph `/me` lookup).
|
|
9
11
|
- Azure PIM operations: [src/azure-pim.ts](src/azure-pim.ts) (ARM AuthorizationManagementClient schedule APIs).
|
|
10
12
|
- Terminal UX helpers: [src/ui.ts](src/ui.ts) (chalk formatting + single global ora spinner).
|
|
@@ -13,6 +15,10 @@
|
|
|
13
15
|
|
|
14
16
|
- `authenticate()` (AzureCliCredential) → Graph `/me` → returns `AuthContext` with `credential`, `userId`, `userPrincipalName`.
|
|
15
17
|
- `showMainMenu(authContext)` → activation/deactivation flows.
|
|
18
|
+
- Presets:
|
|
19
|
+
- CLI `--preset <name>` (and optional defaults) load from `presets.json` via `loadPresets()`.
|
|
20
|
+
- Effective one-shot options are resolved in `src/index.ts` with precedence: CLI flags > preset values > defaults > code defaults.
|
|
21
|
+
- `justification` templates are expanded at runtime using `AuthContext` (e.g., `${date}`, `${datetime}`, `${userPrincipalName}`).
|
|
16
22
|
- Subscriptions fetched via `SubscriptionClient` (`fetchSubscriptions`).
|
|
17
23
|
- Eligible roles via `roleEligibilitySchedules.listForScope("/subscriptions/{id}", { filter: "asTarget()" })`.
|
|
18
24
|
- Activate via `roleAssignmentScheduleRequests.create(..., { requestType: "SelfActivate", linkedRoleEligibilityScheduleId, scheduleInfo, justification })`.
|
|
@@ -32,12 +38,14 @@
|
|
|
32
38
|
- Use `startSpinner/succeedSpinner/failSpinner` and `logInfo/logSuccess/logWarning/logError` from [src/ui.ts](src/ui.ts).
|
|
33
39
|
- `ui.ts` maintains a single global spinner; stop/replace it instead of starting multiple spinners.
|
|
34
40
|
- Keep Azure calls in [src/azure-pim.ts](src/azure-pim.ts); keep prompt/control-flow in [src/cli.ts](src/cli.ts).
|
|
41
|
+
- Keep preset persistence and schema validation in [src/presets.ts](src/presets.ts); keep preset wizards/prompts in [src/presets-cli.ts](src/presets-cli.ts).
|
|
35
42
|
- Inquirer patterns used here:
|
|
36
43
|
- `type: "select"` for single-choice menus, `type: "checkbox"` for multi-select, `type: "confirm"` for final confirmation.
|
|
37
44
|
- Back navigation uses sentinel values like `"__BACK__"`.
|
|
38
45
|
- Errors:
|
|
39
46
|
- `src/index.ts` has top-level error handling and special-cases auth/Azure CLI errors (e.g., messages containing `AADSTS` or `AzureCliCredential`).
|
|
40
47
|
- In `azure-pim.ts`, 403/`AuthorizationFailed` returns an empty list (warn) instead of failing the whole flow.
|
|
48
|
+
- For presets, prefer clear “file path + next step” messages (e.g., mention `AZP_PRESETS_PATH` override or `azp preset list`).
|
|
41
49
|
|
|
42
50
|
## Integration points / prerequisites
|
|
43
51
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,21 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## [1.0.0](https://github.com/tapanmeena/azp-cli/compare/v0.0.4...v1.0.0) (2026-01-13)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add presets CLI for managing activation and deactivation presets ([67a50c5](https://github.com/tapanmeena/azp-cli/commit/67a50c545126bd8f9eed8f3f7ba24987a51f854a))
|
|
6
11
|
|
|
12
|
+
### [0.0.4](https://github.com/tapanmeena/azp-cli/compare/v0.0.3...v0.0.4) (2026-01-13)
|
|
7
13
|
|
|
8
14
|
### Features
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
- add reusable presets for activation/deactivation (stored in user config dir)
|
|
17
|
+
- add preset management commands: `preset list|show|add|edit|remove`
|
|
18
|
+
- support justification templates (`${date}`, `${datetime}`, `${userPrincipalName}`)
|
|
19
|
+
|
|
20
|
+
### 0.0.3 (2026-01-13)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
13
23
|
|
|
24
|
+
- add Azure PIM CLI for role activation and deactivation ([2d10b87](https://github.com/tapanmeena/azp-cli/commit/2d10b87eab7d51a6427f4a70f200521e66e45b98))
|
|
25
|
+
- add copilot instructions for azp-cli ([4c9ab89](https://github.com/tapanmeena/azp-cli/commit/4c9ab894110595f2c88ef8b12073ea51b2c468ef))
|
|
26
|
+
- enhance CLI with non-interactive activation and deactivation options ([3e6fa12](https://github.com/tapanmeena/azp-cli/commit/3e6fa1253e7f1fa1e29c9a75cb90fa6a3d5484bc))
|
|
14
27
|
|
|
15
28
|
### Bug Fixes
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
- revert version number to 0.0.1 in package.json ([0e814da](https://github.com/tapanmeena/azp-cli/commit/0e814da76560c3416602c46bdc9ae7c2b312e6ea))
|
|
31
|
+
- update @types/node dependency to version 25.0.6 ([21a03a1](https://github.com/tapanmeena/azp-cli/commit/21a03a12eff9ee372ff2f398dfdab40ae83891b1))
|
|
32
|
+
- update import paths to use relative references ([2a287af](https://github.com/tapanmeena/azp-cli/commit/2a287af68d1e0071a8c48af38d46546ccbe184e1))
|
|
20
33
|
|
|
21
34
|
## [0.0.2] - 2026-01-13
|
|
22
35
|
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ node dist/index.js
|
|
|
76
76
|
| ------------ | ----- | -------------------------------------- |
|
|
77
77
|
| `activate` | `a` | Activate a role in Azure PIM (default) |
|
|
78
78
|
| `deactivate` | `d` | Deactivate a role in Azure PIM |
|
|
79
|
+
| `preset` | - | Manage reusable presets |
|
|
79
80
|
| `help` | - | Display help information |
|
|
80
81
|
|
|
81
82
|
### One-command (non-interactive) activation
|
|
@@ -111,6 +112,69 @@ azp activate --no-interactive --dry-run \
|
|
|
111
112
|
--output json
|
|
112
113
|
```
|
|
113
114
|
|
|
115
|
+
## Presets
|
|
116
|
+
|
|
117
|
+
Presets let you save your daily activation/deactivation routines (subscription + role names + duration + justification) and reuse them with `--preset <name>`.
|
|
118
|
+
|
|
119
|
+
### Presets file location
|
|
120
|
+
|
|
121
|
+
By default, presets are stored in a per-user config file:
|
|
122
|
+
|
|
123
|
+
- macOS/Linux: `~/.config/azp-cli/presets.json` (or `$XDG_CONFIG_HOME/azp-cli/presets.json`)
|
|
124
|
+
- Windows: `%APPDATA%\azp-cli\presets.json`
|
|
125
|
+
|
|
126
|
+
Override the location with:
|
|
127
|
+
|
|
128
|
+
- `AZP_PRESETS_PATH=/path/to/presets.json`
|
|
129
|
+
|
|
130
|
+
### Preset contents
|
|
131
|
+
|
|
132
|
+
A preset can define one or both blocks:
|
|
133
|
+
|
|
134
|
+
- `activate`: `subscriptionId`, `roleNames[]`, `durationHours`, `justification`, `allowMultiple`
|
|
135
|
+
- `deactivate`: `subscriptionId` (optional), `roleNames[]`, `justification`, `allowMultiple`
|
|
136
|
+
|
|
137
|
+
`justification` supports simple templates:
|
|
138
|
+
|
|
139
|
+
- `${date}` → `YYYY-MM-DD`
|
|
140
|
+
- `${datetime}` → ISO timestamp
|
|
141
|
+
- `${userPrincipalName}` → resolved from Microsoft Graph `/me`
|
|
142
|
+
|
|
143
|
+
### Common workflows
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Create a preset (interactive wizard)
|
|
147
|
+
azp preset add daily-ops
|
|
148
|
+
|
|
149
|
+
# Edit a preset (interactive wizard)
|
|
150
|
+
azp preset edit daily-ops
|
|
151
|
+
|
|
152
|
+
# You can also re-run add to overwrite an existing preset
|
|
153
|
+
azp preset add daily-ops
|
|
154
|
+
|
|
155
|
+
# List presets
|
|
156
|
+
azp preset list
|
|
157
|
+
|
|
158
|
+
# Show one preset
|
|
159
|
+
azp preset show daily-ops
|
|
160
|
+
|
|
161
|
+
# Use a preset (flags still override preset values)
|
|
162
|
+
azp activate --preset daily-ops --yes
|
|
163
|
+
|
|
164
|
+
# Non-interactive run using the preset
|
|
165
|
+
azp activate --preset daily-ops --no-interactive --yes --output json
|
|
166
|
+
|
|
167
|
+
# Deactivate using a preset
|
|
168
|
+
azp deactivate --preset daily-ops --no-interactive --yes
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Defaults
|
|
172
|
+
|
|
173
|
+
When you create a preset via `azp preset add`, you can optionally set it as the default for `activate` and/or `deactivate`.
|
|
174
|
+
|
|
175
|
+
- Default presets are applied automatically when you run one-shot flows and you haven’t explicitly provided the required flags.
|
|
176
|
+
- Example: after setting a default activate preset, `azp activate --no-interactive --yes` can work without specifying `--subscription-id`/`--role-name`.
|
|
177
|
+
|
|
114
178
|
### Example Session
|
|
115
179
|
|
|
116
180
|
```
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
3
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
7
|
const commander_1 = require("commander");
|
|
8
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
5
9
|
const package_json_1 = require("../package.json");
|
|
6
10
|
const auth_1 = require("./auth");
|
|
7
11
|
const cli_1 = require("./cli");
|
|
12
|
+
const presets_cli_1 = require("./presets-cli");
|
|
13
|
+
const presets_1 = require("./presets");
|
|
8
14
|
const ui_1 = require("./ui");
|
|
15
|
+
const getOptionValueSource = (command, optionName) => {
|
|
16
|
+
const fn = command.getOptionValueSource;
|
|
17
|
+
if (typeof fn === "function")
|
|
18
|
+
return fn.call(command, optionName);
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|
|
21
|
+
const resolveValue = (command, optionName, cliValue, presetValue) => {
|
|
22
|
+
const source = getOptionValueSource(command, optionName);
|
|
23
|
+
if (source === "cli")
|
|
24
|
+
return cliValue;
|
|
25
|
+
if (presetValue !== undefined)
|
|
26
|
+
return presetValue;
|
|
27
|
+
return cliValue;
|
|
28
|
+
};
|
|
9
29
|
const program = new commander_1.Command();
|
|
10
30
|
program.name("azp-cli").description("Azure PIM CLI - A CLI tool for Azure Privilege Identity Management (PIM)").version(package_json_1.version);
|
|
11
31
|
program
|
|
@@ -20,34 +40,64 @@ program
|
|
|
20
40
|
}, [])
|
|
21
41
|
.option("--duration-hours <n>", "Duration hours (1-8)", (value) => Number.parseInt(value, 10))
|
|
22
42
|
.option("--justification <text>", "Justification for activation")
|
|
43
|
+
.option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
|
|
23
44
|
.option("--no-interactive", "Do not prompt; require flags to be unambiguous")
|
|
24
45
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
25
46
|
.option("--allow-multiple", "Allow activating multiple eligible matches for a role name")
|
|
26
47
|
.option("--dry-run", "Resolve targets and print summary without submitting activation requests")
|
|
27
48
|
.option("--output <text|json>", "Output format", "text")
|
|
28
49
|
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
29
|
-
.action(async (cmd) => {
|
|
50
|
+
.action(async (cmd, command) => {
|
|
30
51
|
try {
|
|
31
52
|
const output = (cmd.output ?? "text");
|
|
32
53
|
const quiet = Boolean(cmd.quiet || output === "json");
|
|
33
54
|
(0, ui_1.configureUi)({ quiet });
|
|
34
55
|
(0, ui_1.showHeader)();
|
|
35
|
-
const
|
|
56
|
+
const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
|
|
36
57
|
const requestedRoleNames = cmd.roleName ?? [];
|
|
37
|
-
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
|
|
58
|
+
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
|
|
59
|
+
const authContext = await (0, auth_1.authenticate)();
|
|
38
60
|
if (!wantsOneShot) {
|
|
39
61
|
await (0, cli_1.showMainMenu)(authContext);
|
|
40
62
|
return;
|
|
41
63
|
}
|
|
64
|
+
const presets = await (0, presets_1.loadPresets)();
|
|
65
|
+
const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
|
|
66
|
+
const hasSubscriptionFromCli = getOptionValueSource(command, "subscriptionId") === "cli";
|
|
67
|
+
const defaultPresetName = presets.data.defaults?.activatePreset;
|
|
68
|
+
const shouldUseDefaultPreset = !explicitPresetName && Boolean(defaultPresetName) && (!hasSubscriptionFromCli || !hasRoleNamesFromCli);
|
|
69
|
+
const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
|
|
70
|
+
if (explicitPresetName) {
|
|
71
|
+
const entry = (0, presets_1.getPreset)(presets.data, explicitPresetName);
|
|
72
|
+
if (!entry) {
|
|
73
|
+
const names = (0, presets_1.listPresetNames)(presets.data);
|
|
74
|
+
throw new Error(`Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`);
|
|
75
|
+
}
|
|
76
|
+
if (!entry.activate) {
|
|
77
|
+
throw new Error(`Preset "${explicitPresetName}" does not define an activate block.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const presetOptions = (0, presets_1.resolveActivatePresetOptions)(presets.data, presetNameToUse);
|
|
81
|
+
const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId ?? "", presetOptions.subscriptionId);
|
|
82
|
+
const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
|
|
83
|
+
const effectiveDurationHours = resolveValue(command, "durationHours", cmd.durationHours, presetOptions.durationHours);
|
|
84
|
+
const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
|
|
85
|
+
const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
|
|
86
|
+
const effectiveJustification = effectiveJustificationRaw
|
|
87
|
+
? (0, presets_1.expandTemplate)(effectiveJustificationRaw, {
|
|
88
|
+
userId: authContext.userId,
|
|
89
|
+
userPrincipalName: authContext.userPrincipalName,
|
|
90
|
+
})
|
|
91
|
+
: undefined;
|
|
42
92
|
const result = await (0, cli_1.activateOnce)(authContext, {
|
|
43
|
-
subscriptionId:
|
|
44
|
-
roleNames:
|
|
45
|
-
durationHours:
|
|
46
|
-
justification:
|
|
93
|
+
subscriptionId: effectiveSubscriptionId,
|
|
94
|
+
roleNames: effectiveRoleNames,
|
|
95
|
+
durationHours: effectiveDurationHours,
|
|
96
|
+
justification: effectiveJustification,
|
|
47
97
|
dryRun: cmd.dryRun,
|
|
48
98
|
noInteractive: cmd.noInteractive,
|
|
49
99
|
yes: cmd.yes,
|
|
50
|
-
allowMultiple:
|
|
100
|
+
allowMultiple: effectiveAllowMultiple,
|
|
51
101
|
});
|
|
52
102
|
if (output === "json") {
|
|
53
103
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -86,33 +136,61 @@ program
|
|
|
86
136
|
return list;
|
|
87
137
|
}, [])
|
|
88
138
|
.option("--justification <text>", "Justification for deactivation")
|
|
139
|
+
.option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
|
|
89
140
|
.option("--no-interactive", "Do not prompt; require flags to be unambiguous")
|
|
90
141
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
91
142
|
.option("--allow-multiple", "Allow deactivating multiple active matches for a role name")
|
|
92
143
|
.option("--dry-run", "Resolve targets and print summary without submitting deactivation requests")
|
|
93
144
|
.option("--output <text|json>", "Output format", "text")
|
|
94
145
|
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
95
|
-
.action(async (cmd) => {
|
|
146
|
+
.action(async (cmd, command) => {
|
|
96
147
|
try {
|
|
97
148
|
const output = (cmd.output ?? "text");
|
|
98
149
|
const quiet = Boolean(cmd.quiet || output === "json");
|
|
99
150
|
(0, ui_1.configureUi)({ quiet });
|
|
100
151
|
(0, ui_1.showHeader)();
|
|
101
|
-
const
|
|
152
|
+
const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
|
|
102
153
|
const requestedRoleNames = cmd.roleName ?? [];
|
|
103
|
-
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
|
|
154
|
+
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
|
|
155
|
+
const authContext = await (0, auth_1.authenticate)();
|
|
104
156
|
if (!wantsOneShot) {
|
|
105
157
|
await (0, cli_1.showMainMenu)(authContext);
|
|
106
158
|
return;
|
|
107
159
|
}
|
|
160
|
+
const presets = await (0, presets_1.loadPresets)();
|
|
161
|
+
const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
|
|
162
|
+
const defaultPresetName = presets.data.defaults?.deactivatePreset;
|
|
163
|
+
const shouldUseDefaultPreset = !explicitPresetName && Boolean(defaultPresetName) && !hasRoleNamesFromCli;
|
|
164
|
+
const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
|
|
165
|
+
if (explicitPresetName) {
|
|
166
|
+
const entry = (0, presets_1.getPreset)(presets.data, explicitPresetName);
|
|
167
|
+
if (!entry) {
|
|
168
|
+
const names = (0, presets_1.listPresetNames)(presets.data);
|
|
169
|
+
throw new Error(`Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`);
|
|
170
|
+
}
|
|
171
|
+
if (!entry.deactivate) {
|
|
172
|
+
throw new Error(`Preset "${explicitPresetName}" does not define a deactivate block.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const presetOptions = (0, presets_1.resolveDeactivatePresetOptions)(presets.data, presetNameToUse);
|
|
176
|
+
const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId, presetOptions.subscriptionId);
|
|
177
|
+
const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
|
|
178
|
+
const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
|
|
179
|
+
const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
|
|
180
|
+
const effectiveJustification = effectiveJustificationRaw
|
|
181
|
+
? (0, presets_1.expandTemplate)(effectiveJustificationRaw, {
|
|
182
|
+
userId: authContext.userId,
|
|
183
|
+
userPrincipalName: authContext.userPrincipalName,
|
|
184
|
+
})
|
|
185
|
+
: undefined;
|
|
108
186
|
const result = await (0, cli_1.deactivateOnce)(authContext, {
|
|
109
|
-
subscriptionId:
|
|
110
|
-
roleNames:
|
|
111
|
-
justification:
|
|
187
|
+
subscriptionId: effectiveSubscriptionId,
|
|
188
|
+
roleNames: effectiveRoleNames,
|
|
189
|
+
justification: effectiveJustification,
|
|
112
190
|
dryRun: cmd.dryRun,
|
|
113
191
|
noInteractive: cmd.noInteractive,
|
|
114
192
|
yes: cmd.yes,
|
|
115
|
-
allowMultiple:
|
|
193
|
+
allowMultiple: effectiveAllowMultiple,
|
|
116
194
|
});
|
|
117
195
|
if (output === "json") {
|
|
118
196
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -147,5 +225,326 @@ program
|
|
|
147
225
|
(0, ui_1.showHeader)();
|
|
148
226
|
program.outputHelp();
|
|
149
227
|
});
|
|
228
|
+
const presetCommand = program
|
|
229
|
+
.command("preset")
|
|
230
|
+
.description("Manage azp-cli presets")
|
|
231
|
+
.addHelpText("after", `\nPresets file location:\n ${(0, presets_1.getDefaultPresetsFilePath)()}\n\nYou can override via environment variable AZP_PRESETS_PATH.\n`);
|
|
232
|
+
presetCommand
|
|
233
|
+
.command("list")
|
|
234
|
+
.description("List available presets")
|
|
235
|
+
.option("--output <text|json>", "Output format", "text")
|
|
236
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
237
|
+
.action(async (cmd) => {
|
|
238
|
+
try {
|
|
239
|
+
const output = (cmd.output ?? "text");
|
|
240
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
241
|
+
(0, ui_1.configureUi)({ quiet });
|
|
242
|
+
(0, ui_1.showHeader)();
|
|
243
|
+
const loaded = await (0, presets_1.loadPresets)();
|
|
244
|
+
const names = (0, presets_1.listPresetNames)(loaded.data);
|
|
245
|
+
if (output === "json") {
|
|
246
|
+
process.stdout.write(`${JSON.stringify({
|
|
247
|
+
ok: true,
|
|
248
|
+
filePath: loaded.filePath,
|
|
249
|
+
defaults: loaded.data.defaults ?? {},
|
|
250
|
+
presets: names,
|
|
251
|
+
}, null, 2)}\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
(0, ui_1.logInfo)(`Presets file: ${loaded.filePath}`);
|
|
255
|
+
if (!loaded.exists) {
|
|
256
|
+
(0, ui_1.logDim)("(File does not exist yet; use 'azp preset add' to create one.)");
|
|
257
|
+
}
|
|
258
|
+
(0, ui_1.logBlank)();
|
|
259
|
+
if (names.length === 0) {
|
|
260
|
+
(0, ui_1.logWarning)("No presets found.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const defaultActivate = loaded.data.defaults?.activatePreset;
|
|
264
|
+
const defaultDeactivate = loaded.data.defaults?.deactivatePreset;
|
|
265
|
+
for (const name of names) {
|
|
266
|
+
const tags = [];
|
|
267
|
+
if (defaultActivate === name)
|
|
268
|
+
tags.push("default:activate");
|
|
269
|
+
if (defaultDeactivate === name)
|
|
270
|
+
tags.push("default:deactivate");
|
|
271
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
272
|
+
(0, ui_1.logInfo)(`${name}${suffix}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
277
|
+
const output = (cmd.output ?? "text");
|
|
278
|
+
if (output === "json") {
|
|
279
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
(0, ui_1.logBlank)();
|
|
283
|
+
(0, ui_1.logError)(`An error occurred: ${errorMessage}`);
|
|
284
|
+
(0, ui_1.logBlank)();
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
presetCommand
|
|
289
|
+
.command("show")
|
|
290
|
+
.description("Show a preset")
|
|
291
|
+
.argument("<name>", "Preset name")
|
|
292
|
+
.option("--output <text|json>", "Output format", "text")
|
|
293
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
294
|
+
.action(async (name, cmd) => {
|
|
295
|
+
try {
|
|
296
|
+
const output = (cmd.output ?? "text");
|
|
297
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
298
|
+
(0, ui_1.configureUi)({ quiet });
|
|
299
|
+
(0, ui_1.showHeader)();
|
|
300
|
+
const loaded = await (0, presets_1.loadPresets)();
|
|
301
|
+
const entry = (0, presets_1.getPreset)(loaded.data, name);
|
|
302
|
+
if (!entry) {
|
|
303
|
+
const names = (0, presets_1.listPresetNames)(loaded.data);
|
|
304
|
+
throw new Error(`Preset not found: "${name}". Presets file: ${loaded.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`);
|
|
305
|
+
}
|
|
306
|
+
if (output === "json") {
|
|
307
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name, preset: entry }, null, 2)}\n`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
(0, ui_1.logInfo)(`Preset: ${name}`);
|
|
311
|
+
if (entry.description)
|
|
312
|
+
(0, ui_1.logDim)(entry.description);
|
|
313
|
+
(0, ui_1.logBlank)();
|
|
314
|
+
if (entry.activate) {
|
|
315
|
+
(0, ui_1.logInfo)("activate:");
|
|
316
|
+
(0, ui_1.logDim)(` subscriptionId: ${entry.activate.subscriptionId ?? "(unset)"}`);
|
|
317
|
+
(0, ui_1.logDim)(` roleNames: ${(entry.activate.roleNames ?? []).join(", ") || "(unset)"}`);
|
|
318
|
+
(0, ui_1.logDim)(` durationHours: ${entry.activate.durationHours ?? "(unset)"}`);
|
|
319
|
+
(0, ui_1.logDim)(` justification: ${entry.activate.justification ?? "(unset)"}`);
|
|
320
|
+
(0, ui_1.logDim)(` allowMultiple: ${entry.activate.allowMultiple ?? "(unset)"}`);
|
|
321
|
+
(0, ui_1.logBlank)();
|
|
322
|
+
}
|
|
323
|
+
if (entry.deactivate) {
|
|
324
|
+
(0, ui_1.logInfo)("deactivate:");
|
|
325
|
+
(0, ui_1.logDim)(` subscriptionId: ${entry.deactivate.subscriptionId ?? "(unset)"}`);
|
|
326
|
+
(0, ui_1.logDim)(` roleNames: ${(entry.deactivate.roleNames ?? []).join(", ") || "(unset)"}`);
|
|
327
|
+
(0, ui_1.logDim)(` justification: ${entry.deactivate.justification ?? "(unset)"}`);
|
|
328
|
+
(0, ui_1.logDim)(` allowMultiple: ${entry.deactivate.allowMultiple ?? "(unset)"}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
333
|
+
const output = (cmd.output ?? "text");
|
|
334
|
+
if (output === "json") {
|
|
335
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
(0, ui_1.logBlank)();
|
|
339
|
+
(0, ui_1.logError)(`An error occurred: ${errorMessage}`);
|
|
340
|
+
(0, ui_1.logBlank)();
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
presetCommand
|
|
345
|
+
.command("add")
|
|
346
|
+
.description("Add a preset (interactive wizard)")
|
|
347
|
+
.argument("[name]", "Preset name")
|
|
348
|
+
.option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
|
|
349
|
+
.option("--no-from-azure", "Create preset without querying Azure")
|
|
350
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
351
|
+
.option("--output <text|json>", "Output format", "text")
|
|
352
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
353
|
+
.action(async (name, cmd, command) => {
|
|
354
|
+
try {
|
|
355
|
+
const output = (cmd.output ?? "text");
|
|
356
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
357
|
+
(0, ui_1.configureUi)({ quiet });
|
|
358
|
+
(0, ui_1.showHeader)();
|
|
359
|
+
const loaded = await (0, presets_1.loadPresets)();
|
|
360
|
+
const existingNames = (0, presets_1.listPresetNames)(loaded.data);
|
|
361
|
+
if (name && (0, presets_1.getPreset)(loaded.data, name)) {
|
|
362
|
+
if (!cmd.yes) {
|
|
363
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
364
|
+
{
|
|
365
|
+
type: "confirm",
|
|
366
|
+
name: "overwrite",
|
|
367
|
+
message: `Preset "${name}" already exists. Overwrite?`,
|
|
368
|
+
default: false,
|
|
369
|
+
},
|
|
370
|
+
]);
|
|
371
|
+
if (!overwrite) {
|
|
372
|
+
throw new Error("Aborted.");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const fromAzureSource = getOptionValueSource(command, "fromAzure");
|
|
377
|
+
const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
|
|
378
|
+
const namesForValidation = name && (0, presets_1.getPreset)(loaded.data, name) ? existingNames.filter((n) => n !== name) : existingNames;
|
|
379
|
+
const wizard = await (0, presets_cli_1.runPresetAddWizard)({
|
|
380
|
+
name,
|
|
381
|
+
fromAzure: wizardFromAzure,
|
|
382
|
+
existingNames: namesForValidation,
|
|
383
|
+
});
|
|
384
|
+
if (!cmd.yes) {
|
|
385
|
+
const { confirmSave } = await inquirer_1.default.prompt([
|
|
386
|
+
{
|
|
387
|
+
type: "confirm",
|
|
388
|
+
name: "confirmSave",
|
|
389
|
+
message: "Save this preset?",
|
|
390
|
+
default: true,
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
if (!confirmSave) {
|
|
394
|
+
throw new Error("Aborted.");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
let next = (0, presets_1.upsertPreset)(loaded.data, wizard.name, wizard.entry);
|
|
398
|
+
if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
|
|
399
|
+
next = (0, presets_1.setDefaultPresetName)(next, "activate", wizard.name);
|
|
400
|
+
}
|
|
401
|
+
if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
|
|
402
|
+
next = (0, presets_1.setDefaultPresetName)(next, "deactivate", wizard.name);
|
|
403
|
+
}
|
|
404
|
+
await (0, presets_1.savePresets)(loaded.filePath, next);
|
|
405
|
+
if (output === "json") {
|
|
406
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name: wizard.name }, null, 2)}\n`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
(0, ui_1.logSuccess)(`Preset saved: ${wizard.name}`);
|
|
410
|
+
(0, ui_1.logDim)(`File: ${loaded.filePath}`);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
414
|
+
const output = (cmd.output ?? "text");
|
|
415
|
+
if (output === "json") {
|
|
416
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
(0, ui_1.logBlank)();
|
|
420
|
+
(0, ui_1.logError)(`An error occurred: ${errorMessage}`);
|
|
421
|
+
(0, ui_1.logBlank)();
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
presetCommand
|
|
426
|
+
.command("edit")
|
|
427
|
+
.description("Edit an existing preset (interactive wizard)")
|
|
428
|
+
.argument("<name>", "Preset name")
|
|
429
|
+
.option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
|
|
430
|
+
.option("--no-from-azure", "Edit preset without querying Azure")
|
|
431
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
432
|
+
.option("--output <text|json>", "Output format", "text")
|
|
433
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
434
|
+
.action(async (name, cmd, command) => {
|
|
435
|
+
try {
|
|
436
|
+
const output = (cmd.output ?? "text");
|
|
437
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
438
|
+
(0, ui_1.configureUi)({ quiet });
|
|
439
|
+
(0, ui_1.showHeader)();
|
|
440
|
+
const loaded = await (0, presets_1.loadPresets)();
|
|
441
|
+
const existing = (0, presets_1.getPreset)(loaded.data, name);
|
|
442
|
+
if (!existing) {
|
|
443
|
+
throw new Error(`Preset not found: "${name}"`);
|
|
444
|
+
}
|
|
445
|
+
const fromAzureSource = getOptionValueSource(command, "fromAzure");
|
|
446
|
+
const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
|
|
447
|
+
const wizard = await (0, presets_cli_1.runPresetEditWizard)({
|
|
448
|
+
name,
|
|
449
|
+
existingEntry: existing,
|
|
450
|
+
fromAzure: wizardFromAzure,
|
|
451
|
+
});
|
|
452
|
+
if (!cmd.yes) {
|
|
453
|
+
const { confirmSave } = await inquirer_1.default.prompt([
|
|
454
|
+
{
|
|
455
|
+
type: "confirm",
|
|
456
|
+
name: "confirmSave",
|
|
457
|
+
message: `Save changes to preset "${name}"?`,
|
|
458
|
+
default: true,
|
|
459
|
+
},
|
|
460
|
+
]);
|
|
461
|
+
if (!confirmSave) {
|
|
462
|
+
throw new Error("Aborted.");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
let next = (0, presets_1.upsertPreset)(loaded.data, name, wizard.entry);
|
|
466
|
+
if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
|
|
467
|
+
next = (0, presets_1.setDefaultPresetName)(next, "activate", name);
|
|
468
|
+
}
|
|
469
|
+
if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
|
|
470
|
+
next = (0, presets_1.setDefaultPresetName)(next, "deactivate", name);
|
|
471
|
+
}
|
|
472
|
+
await (0, presets_1.savePresets)(loaded.filePath, next);
|
|
473
|
+
if (output === "json") {
|
|
474
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
(0, ui_1.logSuccess)(`Preset updated: ${name}`);
|
|
478
|
+
(0, ui_1.logDim)(`File: ${loaded.filePath}`);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
482
|
+
const output = (cmd.output ?? "text");
|
|
483
|
+
if (output === "json") {
|
|
484
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
(0, ui_1.logBlank)();
|
|
488
|
+
(0, ui_1.logError)(`An error occurred: ${errorMessage}`);
|
|
489
|
+
(0, ui_1.logBlank)();
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
presetCommand
|
|
494
|
+
.command("remove")
|
|
495
|
+
.description("Remove a preset")
|
|
496
|
+
.argument("<name>", "Preset name")
|
|
497
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
498
|
+
.option("--output <text|json>", "Output format", "text")
|
|
499
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
500
|
+
.action(async (name, cmd) => {
|
|
501
|
+
try {
|
|
502
|
+
const output = (cmd.output ?? "text");
|
|
503
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
504
|
+
(0, ui_1.configureUi)({ quiet });
|
|
505
|
+
(0, ui_1.showHeader)();
|
|
506
|
+
const loaded = await (0, presets_1.loadPresets)();
|
|
507
|
+
const entry = (0, presets_1.getPreset)(loaded.data, name);
|
|
508
|
+
if (!entry) {
|
|
509
|
+
throw new Error(`Preset not found: "${name}"`);
|
|
510
|
+
}
|
|
511
|
+
if (!cmd.yes) {
|
|
512
|
+
const { confirmRemove } = await inquirer_1.default.prompt([
|
|
513
|
+
{
|
|
514
|
+
type: "confirm",
|
|
515
|
+
name: "confirmRemove",
|
|
516
|
+
message: `Remove preset "${name}"?`,
|
|
517
|
+
default: false,
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
if (!confirmRemove) {
|
|
521
|
+
throw new Error("Aborted.");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
let next = (0, presets_1.removePreset)(loaded.data, name);
|
|
525
|
+
if (next.defaults?.activatePreset === name)
|
|
526
|
+
next = (0, presets_1.setDefaultPresetName)(next, "activate", undefined);
|
|
527
|
+
if (next.defaults?.deactivatePreset === name)
|
|
528
|
+
next = (0, presets_1.setDefaultPresetName)(next, "deactivate", undefined);
|
|
529
|
+
await (0, presets_1.savePresets)(loaded.filePath, next);
|
|
530
|
+
if (output === "json") {
|
|
531
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
(0, ui_1.logSuccess)(`Preset removed: ${name}`);
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
538
|
+
const output = (cmd.output ?? "text");
|
|
539
|
+
if (output === "json") {
|
|
540
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
(0, ui_1.logBlank)();
|
|
544
|
+
(0, ui_1.logError)(`An error occurred: ${errorMessage}`);
|
|
545
|
+
(0, ui_1.logBlank)();
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
150
549
|
program.parse();
|
|
151
550
|
//# sourceMappingURL=index.js.map
|