azp-cli 0.0.3 → 0.0.4
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 +14 -8
- 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
package/src/index.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import inquirer from "inquirer";
|
|
4
5
|
import { version } from "../package.json";
|
|
5
6
|
import { authenticate } from "./auth";
|
|
6
7
|
import { activateOnce, deactivateOnce, showMainMenu } from "./cli";
|
|
7
|
-
import {
|
|
8
|
+
import { runPresetAddWizard, runPresetEditWizard } from "./presets-cli";
|
|
9
|
+
import {
|
|
10
|
+
expandTemplate,
|
|
11
|
+
getDefaultPresetsFilePath,
|
|
12
|
+
getPreset,
|
|
13
|
+
listPresetNames,
|
|
14
|
+
loadPresets,
|
|
15
|
+
removePreset,
|
|
16
|
+
savePresets,
|
|
17
|
+
resolveActivatePresetOptions,
|
|
18
|
+
resolveDeactivatePresetOptions,
|
|
19
|
+
setDefaultPresetName,
|
|
20
|
+
upsertPreset,
|
|
21
|
+
} from "./presets";
|
|
22
|
+
import { configureUi, logBlank, logDim, logError, logInfo, logSuccess, logWarning, showHeader } from "./ui";
|
|
8
23
|
|
|
9
24
|
type OutputFormat = "text" | "json";
|
|
10
25
|
|
|
@@ -13,6 +28,7 @@ type ActivateCommandOptions = {
|
|
|
13
28
|
roleName?: string[];
|
|
14
29
|
durationHours?: number;
|
|
15
30
|
justification?: string;
|
|
31
|
+
preset?: string;
|
|
16
32
|
noInteractive?: boolean;
|
|
17
33
|
yes?: boolean;
|
|
18
34
|
allowMultiple?: boolean;
|
|
@@ -25,6 +41,7 @@ type DeactivateCommandOptions = {
|
|
|
25
41
|
subscriptionId?: string;
|
|
26
42
|
roleName?: string[];
|
|
27
43
|
justification?: string;
|
|
44
|
+
preset?: string;
|
|
28
45
|
noInteractive?: boolean;
|
|
29
46
|
yes?: boolean;
|
|
30
47
|
allowMultiple?: boolean;
|
|
@@ -33,6 +50,26 @@ type DeactivateCommandOptions = {
|
|
|
33
50
|
quiet?: boolean;
|
|
34
51
|
};
|
|
35
52
|
|
|
53
|
+
type PresetCommandOptions = {
|
|
54
|
+
output?: OutputFormat;
|
|
55
|
+
quiet?: boolean;
|
|
56
|
+
fromAzure?: boolean;
|
|
57
|
+
yes?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const getOptionValueSource = (command: Command, optionName: string): string | undefined => {
|
|
61
|
+
const fn = (command as any).getOptionValueSource;
|
|
62
|
+
if (typeof fn === "function") return fn.call(command, optionName);
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const resolveValue = <T>(command: Command, optionName: string, cliValue: T, presetValue: T | undefined): T => {
|
|
67
|
+
const source = getOptionValueSource(command, optionName);
|
|
68
|
+
if (source === "cli") return cliValue;
|
|
69
|
+
if (presetValue !== undefined) return presetValue;
|
|
70
|
+
return cliValue;
|
|
71
|
+
};
|
|
72
|
+
|
|
36
73
|
const program = new Command();
|
|
37
74
|
|
|
38
75
|
program.name("azp-cli").description("Azure PIM CLI - A CLI tool for Azure Privilege Identity Management (PIM)").version(version);
|
|
@@ -54,13 +91,14 @@ program
|
|
|
54
91
|
)
|
|
55
92
|
.option("--duration-hours <n>", "Duration hours (1-8)", (value: string) => Number.parseInt(value, 10))
|
|
56
93
|
.option("--justification <text>", "Justification for activation")
|
|
94
|
+
.option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
|
|
57
95
|
.option("--no-interactive", "Do not prompt; require flags to be unambiguous")
|
|
58
96
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
59
97
|
.option("--allow-multiple", "Allow activating multiple eligible matches for a role name")
|
|
60
98
|
.option("--dry-run", "Resolve targets and print summary without submitting activation requests")
|
|
61
99
|
.option("--output <text|json>", "Output format", "text")
|
|
62
100
|
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
63
|
-
.action(async (cmd: ActivateCommandOptions) => {
|
|
101
|
+
.action(async (cmd: ActivateCommandOptions, command: Command) => {
|
|
64
102
|
try {
|
|
65
103
|
const output = (cmd.output ?? "text") as OutputFormat;
|
|
66
104
|
const quiet = Boolean(cmd.quiet || output === "json");
|
|
@@ -69,26 +107,65 @@ program
|
|
|
69
107
|
// Show header (text mode only)
|
|
70
108
|
showHeader();
|
|
71
109
|
|
|
72
|
-
|
|
73
|
-
const authContext = await authenticate();
|
|
110
|
+
const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
|
|
74
111
|
|
|
75
112
|
const requestedRoleNames = cmd.roleName ?? [];
|
|
76
|
-
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
|
|
113
|
+
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
|
|
114
|
+
|
|
115
|
+
// Authenticate (required for both interactive and one-shot flows)
|
|
116
|
+
const authContext = await authenticate();
|
|
77
117
|
|
|
78
118
|
if (!wantsOneShot) {
|
|
79
119
|
await showMainMenu(authContext);
|
|
80
120
|
return;
|
|
81
121
|
}
|
|
82
122
|
|
|
123
|
+
const presets = await loadPresets();
|
|
124
|
+
const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
|
|
125
|
+
const hasSubscriptionFromCli = getOptionValueSource(command, "subscriptionId") === "cli";
|
|
126
|
+
|
|
127
|
+
const defaultPresetName = presets.data.defaults?.activatePreset;
|
|
128
|
+
const shouldUseDefaultPreset =
|
|
129
|
+
!explicitPresetName && Boolean(defaultPresetName) && (!hasSubscriptionFromCli || !hasRoleNamesFromCli);
|
|
130
|
+
|
|
131
|
+
const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
|
|
132
|
+
|
|
133
|
+
if (explicitPresetName) {
|
|
134
|
+
const entry = getPreset(presets.data, explicitPresetName);
|
|
135
|
+
if (!entry) {
|
|
136
|
+
const names = listPresetNames(presets.data);
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!entry.activate) {
|
|
142
|
+
throw new Error(`Preset "${explicitPresetName}" does not define an activate block.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const presetOptions = resolveActivatePresetOptions(presets.data, presetNameToUse);
|
|
147
|
+
|
|
148
|
+
const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId ?? "", presetOptions.subscriptionId);
|
|
149
|
+
const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
|
|
150
|
+
const effectiveDurationHours = resolveValue(command, "durationHours", cmd.durationHours, presetOptions.durationHours);
|
|
151
|
+
const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
|
|
152
|
+
const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
|
|
153
|
+
const effectiveJustification = effectiveJustificationRaw
|
|
154
|
+
? expandTemplate(effectiveJustificationRaw, {
|
|
155
|
+
userId: authContext.userId,
|
|
156
|
+
userPrincipalName: authContext.userPrincipalName,
|
|
157
|
+
})
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
83
160
|
const result = await activateOnce(authContext, {
|
|
84
|
-
subscriptionId:
|
|
85
|
-
roleNames:
|
|
86
|
-
durationHours:
|
|
87
|
-
justification:
|
|
161
|
+
subscriptionId: effectiveSubscriptionId,
|
|
162
|
+
roleNames: effectiveRoleNames,
|
|
163
|
+
durationHours: effectiveDurationHours,
|
|
164
|
+
justification: effectiveJustification,
|
|
88
165
|
dryRun: cmd.dryRun,
|
|
89
166
|
noInteractive: cmd.noInteractive,
|
|
90
167
|
yes: cmd.yes,
|
|
91
|
-
allowMultiple:
|
|
168
|
+
allowMultiple: effectiveAllowMultiple,
|
|
92
169
|
});
|
|
93
170
|
|
|
94
171
|
if (output === "json") {
|
|
@@ -138,13 +215,14 @@ program
|
|
|
138
215
|
[]
|
|
139
216
|
)
|
|
140
217
|
.option("--justification <text>", "Justification for deactivation")
|
|
218
|
+
.option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
|
|
141
219
|
.option("--no-interactive", "Do not prompt; require flags to be unambiguous")
|
|
142
220
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
143
221
|
.option("--allow-multiple", "Allow deactivating multiple active matches for a role name")
|
|
144
222
|
.option("--dry-run", "Resolve targets and print summary without submitting deactivation requests")
|
|
145
223
|
.option("--output <text|json>", "Output format", "text")
|
|
146
224
|
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
147
|
-
.action(async (cmd: DeactivateCommandOptions) => {
|
|
225
|
+
.action(async (cmd: DeactivateCommandOptions, command: Command) => {
|
|
148
226
|
try {
|
|
149
227
|
const output = (cmd.output ?? "text") as OutputFormat;
|
|
150
228
|
const quiet = Boolean(cmd.quiet || output === "json");
|
|
@@ -153,25 +231,60 @@ program
|
|
|
153
231
|
// Show header (text mode only)
|
|
154
232
|
showHeader();
|
|
155
233
|
|
|
156
|
-
|
|
157
|
-
const authContext = await authenticate();
|
|
234
|
+
const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
|
|
158
235
|
|
|
159
236
|
const requestedRoleNames = cmd.roleName ?? [];
|
|
160
|
-
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
|
|
237
|
+
const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
|
|
238
|
+
|
|
239
|
+
// Authenticate (required for both interactive and one-shot flows)
|
|
240
|
+
const authContext = await authenticate();
|
|
161
241
|
|
|
162
242
|
if (!wantsOneShot) {
|
|
163
243
|
await showMainMenu(authContext);
|
|
164
244
|
return;
|
|
165
245
|
}
|
|
166
246
|
|
|
247
|
+
const presets = await loadPresets();
|
|
248
|
+
const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
|
|
249
|
+
|
|
250
|
+
const defaultPresetName = presets.data.defaults?.deactivatePreset;
|
|
251
|
+
const shouldUseDefaultPreset = !explicitPresetName && Boolean(defaultPresetName) && !hasRoleNamesFromCli;
|
|
252
|
+
const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
|
|
253
|
+
|
|
254
|
+
if (explicitPresetName) {
|
|
255
|
+
const entry = getPreset(presets.data, explicitPresetName);
|
|
256
|
+
if (!entry) {
|
|
257
|
+
const names = listPresetNames(presets.data);
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (!entry.deactivate) {
|
|
263
|
+
throw new Error(`Preset "${explicitPresetName}" does not define a deactivate block.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const presetOptions = resolveDeactivatePresetOptions(presets.data, presetNameToUse);
|
|
268
|
+
|
|
269
|
+
const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId, presetOptions.subscriptionId);
|
|
270
|
+
const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
|
|
271
|
+
const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
|
|
272
|
+
const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
|
|
273
|
+
const effectiveJustification = effectiveJustificationRaw
|
|
274
|
+
? expandTemplate(effectiveJustificationRaw, {
|
|
275
|
+
userId: authContext.userId,
|
|
276
|
+
userPrincipalName: authContext.userPrincipalName,
|
|
277
|
+
})
|
|
278
|
+
: undefined;
|
|
279
|
+
|
|
167
280
|
const result = await deactivateOnce(authContext, {
|
|
168
|
-
subscriptionId:
|
|
169
|
-
roleNames:
|
|
170
|
-
justification:
|
|
281
|
+
subscriptionId: effectiveSubscriptionId,
|
|
282
|
+
roleNames: effectiveRoleNames,
|
|
283
|
+
justification: effectiveJustification,
|
|
171
284
|
dryRun: cmd.dryRun,
|
|
172
285
|
noInteractive: cmd.noInteractive,
|
|
173
286
|
yes: cmd.yes,
|
|
174
|
-
allowMultiple:
|
|
287
|
+
allowMultiple: effectiveAllowMultiple,
|
|
175
288
|
});
|
|
176
289
|
|
|
177
290
|
if (output === "json") {
|
|
@@ -213,4 +326,372 @@ program
|
|
|
213
326
|
program.outputHelp();
|
|
214
327
|
});
|
|
215
328
|
|
|
329
|
+
const presetCommand = program
|
|
330
|
+
.command("preset")
|
|
331
|
+
.description("Manage azp-cli presets")
|
|
332
|
+
.addHelpText(
|
|
333
|
+
"after",
|
|
334
|
+
`\nPresets file location:\n ${getDefaultPresetsFilePath()}\n\nYou can override via environment variable AZP_PRESETS_PATH.\n`
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
presetCommand
|
|
338
|
+
.command("list")
|
|
339
|
+
.description("List available presets")
|
|
340
|
+
.option("--output <text|json>", "Output format", "text")
|
|
341
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
342
|
+
.action(async (cmd: PresetCommandOptions) => {
|
|
343
|
+
try {
|
|
344
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
345
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
346
|
+
configureUi({ quiet });
|
|
347
|
+
|
|
348
|
+
showHeader();
|
|
349
|
+
|
|
350
|
+
const loaded = await loadPresets();
|
|
351
|
+
const names = listPresetNames(loaded.data);
|
|
352
|
+
|
|
353
|
+
if (output === "json") {
|
|
354
|
+
process.stdout.write(
|
|
355
|
+
`${JSON.stringify(
|
|
356
|
+
{
|
|
357
|
+
ok: true,
|
|
358
|
+
filePath: loaded.filePath,
|
|
359
|
+
defaults: loaded.data.defaults ?? {},
|
|
360
|
+
presets: names,
|
|
361
|
+
},
|
|
362
|
+
null,
|
|
363
|
+
2
|
|
364
|
+
)}\n`
|
|
365
|
+
);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
logInfo(`Presets file: ${loaded.filePath}`);
|
|
370
|
+
if (!loaded.exists) {
|
|
371
|
+
logDim("(File does not exist yet; use 'azp preset add' to create one.)");
|
|
372
|
+
}
|
|
373
|
+
logBlank();
|
|
374
|
+
|
|
375
|
+
if (names.length === 0) {
|
|
376
|
+
logWarning("No presets found.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const defaultActivate = loaded.data.defaults?.activatePreset;
|
|
381
|
+
const defaultDeactivate = loaded.data.defaults?.deactivatePreset;
|
|
382
|
+
for (const name of names) {
|
|
383
|
+
const tags: string[] = [];
|
|
384
|
+
if (defaultActivate === name) tags.push("default:activate");
|
|
385
|
+
if (defaultDeactivate === name) tags.push("default:deactivate");
|
|
386
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
387
|
+
logInfo(`${name}${suffix}`);
|
|
388
|
+
}
|
|
389
|
+
} catch (error: any) {
|
|
390
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
391
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
392
|
+
if (output === "json") {
|
|
393
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
logBlank();
|
|
397
|
+
logError(`An error occurred: ${errorMessage}`);
|
|
398
|
+
logBlank();
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
presetCommand
|
|
404
|
+
.command("show")
|
|
405
|
+
.description("Show a preset")
|
|
406
|
+
.argument("<name>", "Preset name")
|
|
407
|
+
.option("--output <text|json>", "Output format", "text")
|
|
408
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
409
|
+
.action(async (name: string, cmd: PresetCommandOptions) => {
|
|
410
|
+
try {
|
|
411
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
412
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
413
|
+
configureUi({ quiet });
|
|
414
|
+
|
|
415
|
+
showHeader();
|
|
416
|
+
|
|
417
|
+
const loaded = await loadPresets();
|
|
418
|
+
const entry = getPreset(loaded.data, name);
|
|
419
|
+
if (!entry) {
|
|
420
|
+
const names = listPresetNames(loaded.data);
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Preset not found: "${name}". Presets file: ${loaded.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (output === "json") {
|
|
427
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name, preset: entry }, null, 2)}\n`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
logInfo(`Preset: ${name}`);
|
|
432
|
+
if (entry.description) logDim(entry.description);
|
|
433
|
+
logBlank();
|
|
434
|
+
|
|
435
|
+
if (entry.activate) {
|
|
436
|
+
logInfo("activate:");
|
|
437
|
+
logDim(` subscriptionId: ${entry.activate.subscriptionId ?? "(unset)"}`);
|
|
438
|
+
logDim(` roleNames: ${(entry.activate.roleNames ?? []).join(", ") || "(unset)"}`);
|
|
439
|
+
logDim(` durationHours: ${entry.activate.durationHours ?? "(unset)"}`);
|
|
440
|
+
logDim(` justification: ${entry.activate.justification ?? "(unset)"}`);
|
|
441
|
+
logDim(` allowMultiple: ${entry.activate.allowMultiple ?? "(unset)"}`);
|
|
442
|
+
logBlank();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (entry.deactivate) {
|
|
446
|
+
logInfo("deactivate:");
|
|
447
|
+
logDim(` subscriptionId: ${entry.deactivate.subscriptionId ?? "(unset)"}`);
|
|
448
|
+
logDim(` roleNames: ${(entry.deactivate.roleNames ?? []).join(", ") || "(unset)"}`);
|
|
449
|
+
logDim(` justification: ${entry.deactivate.justification ?? "(unset)"}`);
|
|
450
|
+
logDim(` allowMultiple: ${entry.deactivate.allowMultiple ?? "(unset)"}`);
|
|
451
|
+
}
|
|
452
|
+
} catch (error: any) {
|
|
453
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
454
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
455
|
+
if (output === "json") {
|
|
456
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
logBlank();
|
|
460
|
+
logError(`An error occurred: ${errorMessage}`);
|
|
461
|
+
logBlank();
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
presetCommand
|
|
467
|
+
.command("add")
|
|
468
|
+
.description("Add a preset (interactive wizard)")
|
|
469
|
+
.argument("[name]", "Preset name")
|
|
470
|
+
.option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
|
|
471
|
+
.option("--no-from-azure", "Create preset without querying Azure")
|
|
472
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
473
|
+
.option("--output <text|json>", "Output format", "text")
|
|
474
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
475
|
+
.action(async (name: string | undefined, cmd: PresetCommandOptions, command: Command) => {
|
|
476
|
+
try {
|
|
477
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
478
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
479
|
+
configureUi({ quiet });
|
|
480
|
+
|
|
481
|
+
showHeader();
|
|
482
|
+
|
|
483
|
+
const loaded = await loadPresets();
|
|
484
|
+
const existingNames = listPresetNames(loaded.data);
|
|
485
|
+
|
|
486
|
+
if (name && getPreset(loaded.data, name)) {
|
|
487
|
+
if (!cmd.yes) {
|
|
488
|
+
const { overwrite } = await inquirer.prompt<{ overwrite: boolean }>([
|
|
489
|
+
{
|
|
490
|
+
type: "confirm",
|
|
491
|
+
name: "overwrite",
|
|
492
|
+
message: `Preset "${name}" already exists. Overwrite?`,
|
|
493
|
+
default: false,
|
|
494
|
+
},
|
|
495
|
+
]);
|
|
496
|
+
if (!overwrite) {
|
|
497
|
+
throw new Error("Aborted.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const fromAzureSource = getOptionValueSource(command, "fromAzure");
|
|
503
|
+
const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
|
|
504
|
+
|
|
505
|
+
const namesForValidation = name && getPreset(loaded.data, name) ? existingNames.filter((n) => n !== name) : existingNames;
|
|
506
|
+
|
|
507
|
+
const wizard = await runPresetAddWizard({
|
|
508
|
+
name,
|
|
509
|
+
fromAzure: wizardFromAzure,
|
|
510
|
+
existingNames: namesForValidation,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (!cmd.yes) {
|
|
514
|
+
const { confirmSave } = await inquirer.prompt<{ confirmSave: boolean }>([
|
|
515
|
+
{
|
|
516
|
+
type: "confirm",
|
|
517
|
+
name: "confirmSave",
|
|
518
|
+
message: "Save this preset?",
|
|
519
|
+
default: true,
|
|
520
|
+
},
|
|
521
|
+
]);
|
|
522
|
+
if (!confirmSave) {
|
|
523
|
+
throw new Error("Aborted.");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let next = upsertPreset(loaded.data, wizard.name, wizard.entry);
|
|
528
|
+
|
|
529
|
+
if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
|
|
530
|
+
next = setDefaultPresetName(next, "activate", wizard.name);
|
|
531
|
+
}
|
|
532
|
+
if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
|
|
533
|
+
next = setDefaultPresetName(next, "deactivate", wizard.name);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
await savePresets(loaded.filePath, next);
|
|
537
|
+
|
|
538
|
+
if (output === "json") {
|
|
539
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name: wizard.name }, null, 2)}\n`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
logSuccess(`Preset saved: ${wizard.name}`);
|
|
544
|
+
logDim(`File: ${loaded.filePath}`);
|
|
545
|
+
} catch (error: any) {
|
|
546
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
547
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
548
|
+
if (output === "json") {
|
|
549
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
logBlank();
|
|
553
|
+
logError(`An error occurred: ${errorMessage}`);
|
|
554
|
+
logBlank();
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
presetCommand
|
|
560
|
+
.command("edit")
|
|
561
|
+
.description("Edit an existing preset (interactive wizard)")
|
|
562
|
+
.argument("<name>", "Preset name")
|
|
563
|
+
.option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
|
|
564
|
+
.option("--no-from-azure", "Edit preset without querying Azure")
|
|
565
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
566
|
+
.option("--output <text|json>", "Output format", "text")
|
|
567
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
568
|
+
.action(async (name: string, cmd: PresetCommandOptions, command: Command) => {
|
|
569
|
+
try {
|
|
570
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
571
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
572
|
+
configureUi({ quiet });
|
|
573
|
+
|
|
574
|
+
showHeader();
|
|
575
|
+
|
|
576
|
+
const loaded = await loadPresets();
|
|
577
|
+
const existing = getPreset(loaded.data, name);
|
|
578
|
+
if (!existing) {
|
|
579
|
+
throw new Error(`Preset not found: "${name}"`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const fromAzureSource = getOptionValueSource(command, "fromAzure");
|
|
583
|
+
const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
|
|
584
|
+
|
|
585
|
+
const wizard = await runPresetEditWizard({
|
|
586
|
+
name,
|
|
587
|
+
existingEntry: existing,
|
|
588
|
+
fromAzure: wizardFromAzure,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (!cmd.yes) {
|
|
592
|
+
const { confirmSave } = await inquirer.prompt<{ confirmSave: boolean }>([
|
|
593
|
+
{
|
|
594
|
+
type: "confirm",
|
|
595
|
+
name: "confirmSave",
|
|
596
|
+
message: `Save changes to preset "${name}"?`,
|
|
597
|
+
default: true,
|
|
598
|
+
},
|
|
599
|
+
]);
|
|
600
|
+
if (!confirmSave) {
|
|
601
|
+
throw new Error("Aborted.");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let next = upsertPreset(loaded.data, name, wizard.entry);
|
|
606
|
+
if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
|
|
607
|
+
next = setDefaultPresetName(next, "activate", name);
|
|
608
|
+
}
|
|
609
|
+
if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
|
|
610
|
+
next = setDefaultPresetName(next, "deactivate", name);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
await savePresets(loaded.filePath, next);
|
|
614
|
+
|
|
615
|
+
if (output === "json") {
|
|
616
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
logSuccess(`Preset updated: ${name}`);
|
|
621
|
+
logDim(`File: ${loaded.filePath}`);
|
|
622
|
+
} catch (error: any) {
|
|
623
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
624
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
625
|
+
if (output === "json") {
|
|
626
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
logBlank();
|
|
630
|
+
logError(`An error occurred: ${errorMessage}`);
|
|
631
|
+
logBlank();
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
presetCommand
|
|
637
|
+
.command("remove")
|
|
638
|
+
.description("Remove a preset")
|
|
639
|
+
.argument("<name>", "Preset name")
|
|
640
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
641
|
+
.option("--output <text|json>", "Output format", "text")
|
|
642
|
+
.option("--quiet", "Suppress non-essential output (recommended with --output json)")
|
|
643
|
+
.action(async (name: string, cmd: PresetCommandOptions) => {
|
|
644
|
+
try {
|
|
645
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
646
|
+
const quiet = Boolean(cmd.quiet || output === "json");
|
|
647
|
+
configureUi({ quiet });
|
|
648
|
+
|
|
649
|
+
showHeader();
|
|
650
|
+
|
|
651
|
+
const loaded = await loadPresets();
|
|
652
|
+
const entry = getPreset(loaded.data, name);
|
|
653
|
+
if (!entry) {
|
|
654
|
+
throw new Error(`Preset not found: "${name}"`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!cmd.yes) {
|
|
658
|
+
const { confirmRemove } = await inquirer.prompt<{ confirmRemove: boolean }>([
|
|
659
|
+
{
|
|
660
|
+
type: "confirm",
|
|
661
|
+
name: "confirmRemove",
|
|
662
|
+
message: `Remove preset "${name}"?`,
|
|
663
|
+
default: false,
|
|
664
|
+
},
|
|
665
|
+
]);
|
|
666
|
+
if (!confirmRemove) {
|
|
667
|
+
throw new Error("Aborted.");
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let next = removePreset(loaded.data, name);
|
|
672
|
+
if (next.defaults?.activatePreset === name) next = setDefaultPresetName(next, "activate", undefined);
|
|
673
|
+
if (next.defaults?.deactivatePreset === name) next = setDefaultPresetName(next, "deactivate", undefined);
|
|
674
|
+
|
|
675
|
+
await savePresets(loaded.filePath, next);
|
|
676
|
+
|
|
677
|
+
if (output === "json") {
|
|
678
|
+
process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
logSuccess(`Preset removed: ${name}`);
|
|
683
|
+
} catch (error: any) {
|
|
684
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
685
|
+
const output = (cmd.output ?? "text") as OutputFormat;
|
|
686
|
+
if (output === "json") {
|
|
687
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
logBlank();
|
|
691
|
+
logError(`An error occurred: ${errorMessage}`);
|
|
692
|
+
logBlank();
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
216
697
|
program.parse();
|