@cdot65/prisma-airs-cli 5.5.0 → 5.6.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/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  - **Unified automation output** — resource reads support `pretty`, `table`, `markdown`, `csv`, `json`, and `yaml`, with pipe-safe stdout; environment deliverables use HTML or Markdown
26
26
  - **Complete pagination** — consistent `--limit`, `--offset`, and `--all` traversal with a configurable safety cap
27
27
  - **`airs doctor`** — one-command diagnostics for environment, credentials, and API connectivity
28
- - **Tenant selection** — `airs tenant create|switch|list|read|delete` selects existing config files without copying secrets or changing read-only mounts
28
+ - **Tenant configuration** — `airs tenant create|set|switch|list|read|delete` supports guided setup with hidden secret prompts, individual setting updates, and existing-file registration without changing read-only mounts
29
29
  - **Profile migration** — `airs runtime profiles backup|restore` exports private JSON/YAML, previews cross-tenant restores, remaps topics and explicit DLP dependencies, and verifies restored policies ([guide](https://cdot65.github.io/prisma-airs-cli/runtime/profile-transfer/))
30
30
  - **`airs config`** — manage the selected config file from the CLI (`list`, `get`, `set`, `unset`, `path`)
31
31
 
@@ -2402,11 +2402,14 @@ async function changeStore(change) {
2402
2402
  await unlink(lockPath);
2403
2403
  }
2404
2404
  }
2405
- async function createTenant(name, configPath) {
2405
+ function validateTenantName(name) {
2406
2406
  if (!NameSchema.safeParse(name).success)
2407
2407
  throw new Error(
2408
2408
  "Tenant name must be 1\u201364 letters, digits, hyphens or underscores, start with a letter/digit, and not be default"
2409
2409
  );
2410
+ }
2411
+ async function createTenant(name, configPath) {
2412
+ validateTenantName(name);
2410
2413
  let path2;
2411
2414
  try {
2412
2415
  path2 = realpathSync(resolve(expandConfigPath(configPath)));
@@ -4186,10 +4189,13 @@ export {
4186
4189
  readBackupFile,
4187
4190
  readBackupDir,
4188
4191
  ConfigSchema,
4192
+ tenantStorePath,
4189
4193
  expandConfigPath,
4190
4194
  defaultTenantConfigPath,
4191
4195
  readTenantStore,
4192
4196
  readTenantConfig,
4197
+ readTenantConfigFile,
4198
+ validateTenantName,
4193
4199
  createTenant,
4194
4200
  switchTenant,
4195
4201
  deleteTenant,
package/dist/cli/index.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  readBackupDir,
24
24
  readBackupFile,
25
25
  readTenantConfig,
26
+ readTenantConfigFile,
26
27
  readTenantStore,
27
28
  renderEnvironmentReportHtml,
28
29
  renderEnvironmentReportMarkdown,
@@ -34,10 +35,12 @@ import {
34
35
  resolveOutputDir,
35
36
  sanitizeFilename,
36
37
  switchTenant,
38
+ tenantStorePath,
39
+ validateTenantName,
37
40
  validateTopic,
38
41
  writeBackupFile,
39
42
  writeReportFile
40
- } from "../chunk-NKAVUBKI.js";
43
+ } from "../chunk-XQOICLSH.js";
41
44
 
42
45
  // src/cli/index.ts
43
46
  import "dotenv/config";
@@ -56,9 +59,9 @@ function installProcessGuards() {
56
59
  }
57
60
 
58
61
  // src/cli/program.ts
59
- import { randomUUID as randomUUID10 } from "crypto";
62
+ import { randomUUID as randomUUID11 } from "crypto";
60
63
  import { readFileSync as readFileSync4 } from "fs";
61
- import { dirname as dirname4, join as join4 } from "path";
64
+ import { dirname as dirname5, join as join5 } from "path";
62
65
  import { fileURLToPath } from "url";
63
66
  import { Command } from "commander";
64
67
 
@@ -3684,19 +3687,19 @@ function installDebugLogger(logPath, options = {}) {
3684
3687
  mkdirSync(dirname(logPath), { recursive: true, mode: 448 });
3685
3688
  writeFileSync(logPath, "", { encoding: "utf8", mode: 384, flag: "wx" });
3686
3689
  const originalFetch = globalThis.fetch;
3687
- globalThis.fetch = async function debugFetch(input, init2) {
3688
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
3690
+ globalThis.fetch = async function debugFetch(input2, init2) {
3691
+ const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
3689
3692
  const rawHeaders = headersToRecord(
3690
- init2?.headers ?? (input instanceof Request ? input.headers : void 0)
3693
+ init2?.headers ?? (input2 instanceof Request ? input2.headers : void 0)
3691
3694
  );
3692
3695
  const inference = Object.keys(rawHeaders).some(
3693
3696
  (name) => name.toLowerCase() === "x-portkey-api-key"
3694
3697
  );
3695
3698
  if (!isAirsUrl(url) && !inference) {
3696
- return originalFetch(input, init2);
3699
+ return originalFetch(input2, init2);
3697
3700
  }
3698
3701
  const dashboard = options.omitBodies || /\/v1\/mgmt\/(dashboard\/|reports\/scancontent(?:\?|$))/.test(url) || /\/aiag\//.test(new URL(url).pathname);
3699
- const method = init2?.method ?? (input instanceof Request ? input.method : "GET");
3702
+ const method = init2?.method ?? (input2 instanceof Request ? input2.method : "GET");
3700
3703
  const reqHeaders = redactHeaders(rawHeaders);
3701
3704
  const loggedUrl = redactUrl(url);
3702
3705
  let reqBody;
@@ -3715,7 +3718,7 @@ function installDebugLogger(logPath, options = {}) {
3715
3718
  let resBody;
3716
3719
  let error;
3717
3720
  try {
3718
- response = await originalFetch(input, init2);
3721
+ response = await originalFetch(input2, init2);
3719
3722
  } catch (err) {
3720
3723
  error = inference ? "Runtime request failed" : "Request failed";
3721
3724
  const entry2 = JSON.stringify({
@@ -6955,8 +6958,8 @@ function buildDefaultCategories(categories) {
6955
6958
  ])
6956
6959
  );
6957
6960
  }
6958
- function parseAttackGoals(input) {
6959
- const trimmed = input.trim();
6961
+ function parseAttackGoals(input2) {
6962
+ const trimmed = input2.trim();
6960
6963
  const raw = trimmed.startsWith("[") ? trimmed : fs2.readFileSync(trimmed, "utf-8");
6961
6964
  let parsed;
6962
6965
  try {
@@ -6978,10 +6981,10 @@ function sliceClientSide(items, opts) {
6978
6981
  const limit = opts.limit !== void 0 ? Number.parseInt(String(opts.limit), 10) : void 0;
6979
6982
  return items.slice(offset, limit === void 0 ? void 0 : offset + limit);
6980
6983
  }
6981
- function parsePositiveInt(input, flag) {
6982
- const n = Number.parseInt(input, 10);
6984
+ function parsePositiveInt(input2, flag) {
6985
+ const n = Number.parseInt(input2, 10);
6983
6986
  if (!Number.isFinite(n) || n <= 0) {
6984
- throw new Error(`${flag}: expected a positive integer, got "${input}"`);
6987
+ throw new Error(`${flag}: expected a positive integer, got "${input2}"`);
6985
6988
  }
6986
6989
  return n;
6987
6990
  }
@@ -7067,10 +7070,10 @@ function resolveScriptB64(opts) {
7067
7070
  }
7068
7071
  throw new Error("one of --script-file or --script-b64 is required");
7069
7072
  }
7070
- function parseAdapterVariables(input) {
7073
+ function parseAdapterVariables(input2) {
7071
7074
  let parsed;
7072
7075
  try {
7073
- parsed = JSON.parse(input);
7076
+ parsed = JSON.parse(input2);
7074
7077
  } catch (err) {
7075
7078
  throw new Error(`--variables: invalid JSON (${err instanceof Error ? err.message : err})`);
7076
7079
  }
@@ -9732,8 +9735,8 @@ async function dataProfiles(api, maxPages2) {
9732
9735
  }
9733
9736
  throw new Error("DLP inventory is incomplete; increase --max-pages");
9734
9737
  }
9735
- async function planRuntimeProfilesRestore(api, input, destinationTsgId, options = {}) {
9736
- const backup = RuntimeProfilesBackupSchema.parse(input);
9738
+ async function planRuntimeProfilesRestore(api, input2, destinationTsgId, options = {}) {
9739
+ const backup = RuntimeProfilesBackupSchema.parse(input2);
9737
9740
  if (!destinationTsgId) throw new Error("Destination TSG ID is required");
9738
9741
  const maxPages2 = options.maxPages ?? 100;
9739
9742
  pagesLimit(maxPages2);
@@ -10109,15 +10112,15 @@ function registerProfileTransferCommands(profiles2) {
10109
10112
  const info = await stat(path3);
10110
10113
  if (!info.isFile() || info.size > MAX_BACKUP_BYTES)
10111
10114
  throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
10112
- let input;
10115
+ let input2;
10113
10116
  try {
10114
10117
  const text2 = await readFile9(path3, "utf8");
10115
10118
  if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
10116
- input = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
10119
+ input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
10117
10120
  } catch {
10118
10121
  throw new CliUsageError("Backup is not valid JSON/YAML; contents are not logged");
10119
10122
  }
10120
- const data = RuntimeProfilesBackupSchema.parse(input);
10123
+ const data = RuntimeProfilesBackupSchema.parse(input2);
10121
10124
  const format = await resolveOutput(restore, opts);
10122
10125
  const { api, tsgId } = await context();
10123
10126
  if (opts.expectTsg && opts.expectTsg !== tsgId)
@@ -10608,26 +10611,26 @@ function registerRuntimeReportCommand(runtime) {
10608
10611
  }
10609
10612
 
10610
10613
  // src/cli/commands/topics-apply.ts
10611
- async function applyTopicToProfile(mgmt, input) {
10614
+ async function applyTopicToProfile(mgmt, input2) {
10612
10615
  const allTopics = await mgmt.listTopics();
10613
- const match = allTopics.find((t) => t.topic_name === input.topicName);
10616
+ const match = allTopics.find((t) => t.topic_name === input2.topicName);
10614
10617
  if (!match || !match.topic_id) {
10615
- throw new Error(`Topic "${input.topicName}" not found. Create it first with "topics create".`);
10618
+ throw new Error(`Topic "${input2.topicName}" not found. Create it first with "topics create".`);
10616
10619
  }
10617
- const existing = await mgmt.getProfileTopics(input.profileName);
10618
- const merged = existing.filter((t) => t.topicName !== input.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
10620
+ const existing = await mgmt.getProfileTopics(input2.profileName);
10621
+ const merged = existing.filter((t) => t.topicName !== input2.topicName).map((t) => ({ topicId: t.topicId, topicName: t.topicName, action: t.action }));
10619
10622
  merged.push({
10620
10623
  topicId: match.topic_id,
10621
10624
  topicName: match.topic_name,
10622
- action: input.intent
10625
+ action: input2.intent
10623
10626
  });
10624
- const guardrailAction = input.intent === "block" ? "allow" : "block";
10625
- await mgmt.assignTopicsToProfile(input.profileName, merged, guardrailAction);
10627
+ const guardrailAction = input2.intent === "block" ? "allow" : "block";
10628
+ await mgmt.assignTopicsToProfile(input2.profileName, merged, guardrailAction);
10626
10629
  return {
10627
10630
  topicId: match.topic_id,
10628
10631
  topicName: match.topic_name,
10629
- profileName: input.profileName,
10630
- intent: input.intent
10632
+ profileName: input2.profileName,
10633
+ intent: input2.intent
10631
10634
  };
10632
10635
  }
10633
10636
  function registerApplyCommand(parent) {
@@ -10672,21 +10675,21 @@ function registerApplyCommand(parent) {
10672
10675
  }
10673
10676
 
10674
10677
  // src/cli/commands/topics-create.ts
10675
- async function createOrUpdateTopic(mgmt, input) {
10676
- const topic = { name: input.name, description: input.description, examples: input.examples };
10678
+ async function createOrUpdateTopic(mgmt, input2) {
10679
+ const topic = { name: input2.name, description: input2.description, examples: input2.examples };
10677
10680
  const errors = validateTopic(topic);
10678
10681
  if (errors.length > 0) {
10679
10682
  throw new Error(errors.map((e) => `${e.field}: ${e.message}`).join("; "));
10680
10683
  }
10681
10684
  const existing = await mgmt.listTopics();
10682
- const match = existing.find((t) => t.topic_name === input.name);
10685
+ const match = existing.find((t) => t.topic_name === input2.name);
10683
10686
  const request = {
10684
- topic_name: input.name,
10685
- description: input.description,
10686
- examples: input.examples
10687
+ topic_name: input2.name,
10688
+ description: input2.description,
10689
+ examples: input2.examples
10687
10690
  };
10688
10691
  if (match && !match.topic_id) {
10689
- throw new Error(`Existing topic '${input.name}' has no topic_id`);
10692
+ throw new Error(`Existing topic '${input2.name}' has no topic_id`);
10690
10693
  }
10691
10694
  const result = match ? await mgmt.updateTopic(match.topic_id, request) : await mgmt.createTopic(request);
10692
10695
  if (!result.topic_id) {
@@ -11843,7 +11846,139 @@ function registerRuntimeCommand(program) {
11843
11846
  registerDlpCommands(runtime);
11844
11847
  }
11845
11848
 
11849
+ // src/config/tenant-settings.ts
11850
+ import { randomUUID as randomUUID10 } from "crypto";
11851
+ import { constants } from "fs";
11852
+ import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
11853
+ import { dirname as dirname4, join as join4 } from "path";
11854
+ var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
11855
+ function validateTenantSettingKey(key) {
11856
+ if (!Object.hasOwn(ConfigSchema.shape, key))
11857
+ throw new Error(`Unknown configuration key. Supported keys: ${TENANT_CONFIG_KEYS.join(", ")}`);
11858
+ }
11859
+ function isTenantSecret(key) {
11860
+ return /(?:key|secret|token|password)$/i.test(key);
11861
+ }
11862
+ function validateCredentials(config) {
11863
+ for (const key of ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"]) {
11864
+ if (typeof config[key] !== "string" || !config[key].trim())
11865
+ throw new Error(
11866
+ "Tenant config requires nonempty mgmtTsgId, mgmtClientId and mgmtClientSecret"
11867
+ );
11868
+ }
11869
+ }
11870
+ async function createManagedTenant(name, config) {
11871
+ validateTenantName(name);
11872
+ for (const key of Object.keys(config)) validateTenantSettingKey(key);
11873
+ const parsed = ConfigSchema.safeParse(config);
11874
+ if (!parsed.success) throw new Error("Invalid tenant configuration");
11875
+ validateCredentials(config);
11876
+ if (readTenantStore().tenants.some((entry) => entry.name === name))
11877
+ throw new Error("Tenant name already exists");
11878
+ const directory = join4(dirname4(tenantStorePath()), "configs");
11879
+ await mkdir3(directory, { recursive: true, mode: 448 });
11880
+ const path3 = join4(directory, `${name}-${randomUUID10()}.json`);
11881
+ const file = await open2(path3, "wx", 384);
11882
+ try {
11883
+ const data = Object.fromEntries(
11884
+ Object.keys(config).map((key) => [key, Reflect.get(parsed.data, key)])
11885
+ );
11886
+ await file.writeFile(`${JSON.stringify(data, null, 2)}
11887
+ `);
11888
+ await file.sync();
11889
+ await file.close();
11890
+ return await createTenant(name, path3);
11891
+ } catch {
11892
+ await file.close();
11893
+ await unlink2(path3);
11894
+ throw new Error("Could not create tenant configuration; registration was not completed");
11895
+ }
11896
+ }
11897
+ async function setTenantSetting(name, key, value) {
11898
+ validateTenantSettingKey(key);
11899
+ const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
11900
+ if (!entry) throw new Error("Tenant not found; register a named tenant first");
11901
+ if (key === "mgmtTsgId" && value !== entry.tsgId)
11902
+ throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
11903
+ const path3 = entry.configPath;
11904
+ const lockPath = `${path3}.lock`;
11905
+ const lock = await open2(lockPath, "wx", 384).catch(() => {
11906
+ throw new Error(
11907
+ "Cannot lock tenant config; it may be read-only or another edit is in progress"
11908
+ );
11909
+ });
11910
+ const temporary = join4(dirname4(path3), `.airs-config-${randomUUID10()}.tmp`);
11911
+ let created = false;
11912
+ try {
11913
+ const stat2 = await lstat6(path3);
11914
+ if (!stat2.isFile() || (stat2.mode & 146) === 0)
11915
+ throw new Error("Tenant config is not a writable regular file");
11916
+ await access(path3, constants.W_OK);
11917
+ const current = readTenantConfigFile(path3, entry.tsgId);
11918
+ const result = ConfigSchema.safeParse({ ...current, [key]: value });
11919
+ if (!result.success) throw new Error("Invalid value for configuration setting");
11920
+ const next = { ...current, [key]: Reflect.get(result.data, key) };
11921
+ validateCredentials(next);
11922
+ const file = await open2(temporary, "wx", 384);
11923
+ created = true;
11924
+ try {
11925
+ await file.writeFile(`${JSON.stringify(next, null, 2)}
11926
+ `);
11927
+ await file.sync();
11928
+ } finally {
11929
+ await file.close();
11930
+ }
11931
+ await rename2(temporary, path3);
11932
+ created = false;
11933
+ } finally {
11934
+ if (created) await unlink2(temporary).catch(() => {
11935
+ });
11936
+ await lock.close();
11937
+ await unlink2(lockPath);
11938
+ }
11939
+ }
11940
+
11941
+ // src/cli/tenant-input.ts
11942
+ import { input, password } from "@inquirer/prompts";
11943
+ async function promptTenantValue(message, secret = false) {
11944
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
11945
+ throw new Error(
11946
+ "Interactive setup requires a terminal. Use --config, or --tsg-id and --client-id with --client-secret-stdin. For tenant set, pass a value or use --stdin."
11947
+ );
11948
+ const prompt = secret ? password : input;
11949
+ return prompt(
11950
+ {
11951
+ message,
11952
+ validate: (value) => Boolean(value.trim()) || "A nonempty value is required"
11953
+ },
11954
+ { input: process.stdin, output: process.stderr }
11955
+ );
11956
+ }
11957
+ async function readTenantStdin() {
11958
+ if (process.stdin.isTTY) throw new Error("--stdin requires piped input");
11959
+ const chunks = [];
11960
+ let size = 0;
11961
+ for await (const chunk of process.stdin) {
11962
+ const buffer = Buffer.from(chunk);
11963
+ size += buffer.length;
11964
+ if (size > 65536) throw new Error("Configuration input exceeds 64 KiB");
11965
+ chunks.push(buffer);
11966
+ }
11967
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
11968
+ if (!value.trim() || /[\r\n\0]/.test(value))
11969
+ throw new Error("Provide one nonempty configuration value on stdin");
11970
+ return value;
11971
+ }
11972
+
11846
11973
  // src/cli/commands/tenant.ts
11974
+ function tenantInputFailure(error) {
11975
+ if (error instanceof Error && error.name === "ExitPromptError") {
11976
+ ui.status("Cancelled; no configuration saved.");
11977
+ process.exitCode = 130;
11978
+ return;
11979
+ }
11980
+ fail(error);
11981
+ }
11847
11982
  var COLUMNS2 = [
11848
11983
  { key: "name", label: "Tenant" },
11849
11984
  { key: "active", label: "Selected" },
@@ -11851,25 +11986,60 @@ var COLUMNS2 = [
11851
11986
  { key: "configPath", label: "Config file" }
11852
11987
  ];
11853
11988
  function registerTenantCommand(program) {
11854
- const tenant = program.command("tenant").description("Select named tenant config files without changing their contents").addHelpText(
11989
+ const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
11855
11990
  "after",
11856
11991
  examples(
11992
+ "airs tenant create development",
11993
+ "airs tenant set development defaultOutput yaml",
11857
11994
  "airs tenant create production --config /secure/production.json",
11858
11995
  "airs tenant switch production",
11859
11996
  "airs tenant read",
11860
11997
  "airs tenant switch default"
11861
11998
  )
11862
11999
  );
11863
- tenant.command("create <name>").description("Register an existing JSON config file by reference; does not activate it").requiredOption("--config <path>", "Existing Prisma AIRS JSON config file").action(async (name, opts) => {
11864
- try {
11865
- const entry = await createTenant(name, opts.config);
11866
- ui.success(
11867
- `Registered ${entry.name} (TSG ${entry.tsgId}); config file unchanged. Use airs tenant switch ${entry.name}.`
11868
- );
11869
- } catch (error) {
11870
- fail(error);
12000
+ tenant.command("create <name>").description(
12001
+ "Create a config with guided prompts, or register an existing JSON file; does not activate it"
12002
+ ).option("--config <path>", "Existing Prisma AIRS JSON config file (no copying or editing)").option("--tsg-id <id>", "Tenant service group ID for a new config").option("--client-id <id>", "OAuth client ID for a new config").option("--client-secret-stdin", "Read the new config OAuth secret from piped stdin").action(
12003
+ async (name, opts) => {
12004
+ try {
12005
+ validateTenantName(name);
12006
+ if (opts.config && (opts.tsgId !== void 0 || opts.clientId !== void 0 || opts.clientSecretStdin))
12007
+ throw new Error("--config cannot be combined with new-config options");
12008
+ if (!opts.config && readTenantStore().tenants.some((entry2) => entry2.name === name))
12009
+ throw new Error("Tenant name already exists");
12010
+ const entry = opts.config ? await createTenant(name, opts.config) : await createManagedTenant(name, {
12011
+ mgmtTsgId: opts.tsgId ?? await promptTenantValue("Tenant service group ID (mgmtTsgId):"),
12012
+ mgmtClientId: opts.clientId ?? await promptTenantValue("OAuth client ID (mgmtClientId):"),
12013
+ mgmtClientSecret: opts.clientSecretStdin ? await readTenantStdin() : await promptTenantValue("OAuth client secret (mgmtClientSecret):", true)
12014
+ });
12015
+ ui.success(
12016
+ `Registered ${entry.name} (TSG ${entry.tsgId}); ${opts.config ? "config file unchanged" : "private config created"}. Use airs tenant switch ${entry.name}.`
12017
+ );
12018
+ } catch (error) {
12019
+ tenantInputFailure(error);
12020
+ }
11871
12021
  }
11872
- });
12022
+ );
12023
+ tenant.command("set <name> <key> [value]").description("Update one named tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
12024
+ async (name, key, value, opts) => {
12025
+ try {
12026
+ validateTenantSettingKey(key);
12027
+ if (!readTenantStore().tenants.some((entry) => entry.name === name))
12028
+ throw new Error("Tenant not found; register a named tenant first");
12029
+ if (opts.stdin && value !== void 0)
12030
+ throw new Error("Use a value or --stdin, not both");
12031
+ if (isTenantSecret(key) && value !== void 0)
12032
+ throw new Error(
12033
+ "Do not pass credentials as arguments. Omit the value for a hidden prompt, or use --stdin."
12034
+ );
12035
+ const setting = opts.stdin ? await readTenantStdin() : value ?? await promptTenantValue(`${key}:`, isTenantSecret(key));
12036
+ await setTenantSetting(name, key, setting);
12037
+ ui.success(`Updated ${key} for tenant ${name}; selection unchanged.`);
12038
+ } catch (error) {
12039
+ tenantInputFailure(error);
12040
+ }
12041
+ }
12042
+ );
11873
12043
  tenant.command("switch <name>").description(
11874
12044
  "Persist the selected tenant; default restores legacy config/environment resolution"
11875
12045
  ).action(async (name) => {
@@ -12005,8 +12175,8 @@ function applySortedHelp(cmd) {
12005
12175
  for (const sub of cmd.commands) applySortedHelp(sub);
12006
12176
  }
12007
12177
  function buildProgram() {
12008
- const here = dirname4(fileURLToPath(import.meta.url));
12009
- const pkg = JSON.parse(readFileSync4(join4(here, "../../package.json"), "utf-8"));
12178
+ const here = dirname5(fileURLToPath(import.meta.url));
12179
+ const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
12010
12180
  const program = new Command();
12011
12181
  program.name("airs").description(
12012
12182
  "CLI and library for Palo Alto Prisma AIRS \u2014 guardrail refinement, AI red teaming, model security scanning, profile audits"
@@ -12035,9 +12205,9 @@ function buildProgram() {
12035
12205
  }
12036
12206
  }
12037
12207
  if (root.debug) {
12038
- const logPath = join4(
12208
+ const logPath = join5(
12039
12209
  process.cwd(),
12040
- `debug-api-${Date.now()}-${randomUUID10().slice(0, 8)}.jsonl`
12210
+ `debug-api-${Date.now()}-${randomUUID11().slice(0, 8)}.jsonl`
12041
12211
  );
12042
12212
  try {
12043
12213
  let ancestor = actionCommand;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  validateTopic,
29
29
  writeBackupFile,
30
30
  writeReportFile
31
- } from "./chunk-NKAVUBKI.js";
31
+ } from "./chunk-XQOICLSH.js";
32
32
  export {
33
33
  AirsScanService,
34
34
  SDK_ASYNC_BATCH_SIZE,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cdot65/prisma-airs-cli",
3
3
  "packageManager": "pnpm@10.6.5",
4
- "version": "5.5.0",
4
+ "version": "5.6.0",
5
5
  "description": "CLI and library for Palo Alto Prisma AIRS — guardrail refinement, AI red teaming, model security scanning, profile audits",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",