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.
@@ -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
- ### 0.0.3 (2026-01-13)
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
- * add Azure PIM CLI for role activation and deactivation ([2d10b87](https://github.com/tapanmeena/azp-cli/commit/2d10b87eab7d51a6427f4a70f200521e66e45b98))
11
- * add copilot instructions for azp-cli ([4c9ab89](https://github.com/tapanmeena/azp-cli/commit/4c9ab894110595f2c88ef8b12073ea51b2c468ef))
12
- * enhance CLI with non-interactive activation and deactivation options ([3e6fa12](https://github.com/tapanmeena/azp-cli/commit/3e6fa1253e7f1fa1e29c9a75cb90fa6a3d5484bc))
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
- * revert version number to 0.0.1 in package.json ([0e814da](https://github.com/tapanmeena/azp-cli/commit/0e814da76560c3416602c46bdc9ae7c2b312e6ea))
18
- * update @types/node dependency to version 25.0.6 ([21a03a1](https://github.com/tapanmeena/azp-cli/commit/21a03a12eff9ee372ff2f398dfdab40ae83891b1))
19
- * update import paths to use relative references ([2a287af](https://github.com/tapanmeena/azp-cli/commit/2a287af68d1e0071a8c48af38d46546ccbe184e1))
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 authContext = await (0, auth_1.authenticate)();
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: cmd.subscriptionId ?? "",
44
- roleNames: requestedRoleNames,
45
- durationHours: cmd.durationHours,
46
- justification: cmd.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: cmd.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 authContext = await (0, auth_1.authenticate)();
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: cmd.subscriptionId,
110
- roleNames: requestedRoleNames,
111
- justification: cmd.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: cmd.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