@cdot65/prisma-airs-cli 5.11.0 → 6.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/dist/cli/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  AirsScanService,
4
4
  ConfigSchema,
5
+ RETIRED_CONFIG_KEYS,
5
6
  RateLimitedScanService,
6
7
  SDK_ASYNC_BATCH_SIZE,
7
8
  SdkManagementService,
@@ -14,9 +15,7 @@ import {
14
15
  collectRuntimeDailyReport,
15
16
  computeMetrics,
16
17
  createTenant,
17
- defaultTenantConfigPath,
18
18
  deleteTenant,
19
- expandConfigPath,
20
19
  getOrCreateManagementClient,
21
20
  inspectConfig,
22
21
  loadConfig,
@@ -31,7 +30,7 @@ import {
31
30
  renderRedTeamReportMarkdown,
32
31
  renderRuntimeReportHtml,
33
32
  renderRuntimeReportMarkdown,
34
- resolveConfigFilePath,
33
+ resolveConfigContext,
35
34
  resolveOutputDir,
36
35
  sanitizeFilename,
37
36
  switchTenant,
@@ -40,10 +39,7 @@ import {
40
39
  validateTopic,
41
40
  writeBackupFile,
42
41
  writeReportFile
43
- } from "../chunk-37SN57XQ.js";
44
-
45
- // src/cli/index.ts
46
- import "dotenv/config";
42
+ } from "../chunk-KGMQY2IT.js";
47
43
 
48
44
  // src/cli/process-guards.ts
49
45
  import chalk from "chalk";
@@ -61,7 +57,7 @@ function installProcessGuards() {
61
57
  // src/cli/program.ts
62
58
  import { randomUUID as randomUUID12 } from "crypto";
63
59
  import { readFileSync as readFileSync4 } from "fs";
64
- import { dirname as dirname5, join as join5 } from "path";
60
+ import { dirname as dirname4, join as join5 } from "path";
65
61
  import { fileURLToPath } from "url";
66
62
  import { Command } from "commander";
67
63
 
@@ -79,65 +75,124 @@ import { dump as dump3 } from "js-yaml";
79
75
  import { z } from "zod";
80
76
 
81
77
  // src/config/client-options.ts
82
- function agentGuardClientOptions(config) {
78
+ import {
79
+ AGENT_GUARD_DATA_ENDPOINT,
80
+ AGENT_GUARD_MGMT_ENDPOINT,
81
+ DEFAULT_AI_GW_ADMIN_ENDPOINT,
82
+ DEFAULT_AI_GW_DATA_ENDPOINT,
83
+ DEFAULT_DLP_ENDPOINT,
84
+ DEFAULT_ENDPOINT,
85
+ DEFAULT_IAM_ENDPOINT,
86
+ DEFAULT_MGMT_ENDPOINT,
87
+ DEFAULT_MODEL_SEC_DATA_ENDPOINT,
88
+ DEFAULT_MODEL_SEC_MGMT_ENDPOINT,
89
+ DEFAULT_RED_TEAM_DATA_ENDPOINT,
90
+ DEFAULT_RED_TEAM_MGMT_ENDPOINT,
91
+ DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT,
92
+ DEFAULT_TOKEN_ENDPOINT
93
+ } from "@cdot65/prisma-airs-sdk";
94
+
95
+ // src/config/credentials.ts
96
+ var MANAGEMENT_CREDENTIAL_KEYS = [
97
+ "mgmtClientId",
98
+ "mgmtClientSecret",
99
+ "mgmtTsgId"
100
+ ];
101
+ var SCANNER_CREDENTIAL_KEYS = ["airsApiKey", "airsApiToken"];
102
+ function present(value) {
103
+ return typeof value === "string" && value.trim() !== "";
104
+ }
105
+ function missingManagementCredentials(config) {
106
+ return MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !present(config[key]));
107
+ }
108
+ function hasScannerCredentials(config) {
109
+ return SCANNER_CREDENTIAL_KEYS.some((key) => present(config[key]));
110
+ }
111
+ function safeContext() {
112
+ try {
113
+ return resolveConfigContext();
114
+ } catch {
115
+ return void 0;
116
+ }
117
+ }
118
+ function settingRemedy(keys, context3 = safeContext()) {
119
+ const list = keys.join(", ");
120
+ if (context3?.selection === "tenant") {
121
+ return `Run 'airs tenant set ${context3.tenant.name} <key>' for ${list} (secrets prompt hidden, or use --stdin)`;
122
+ }
123
+ if (context3?.selection === "explicit") return `Add ${list} to ${context3.path}`;
124
+ return `Run 'airs tenant create <name>' (prompts for the OAuth credentials), then 'airs tenant switch <name>'`;
125
+ }
126
+ function assertManagementCredentials(config, context3) {
127
+ const missing = missingManagementCredentials(config);
128
+ if (missing.length === 0) return;
129
+ throw new Error(
130
+ `Management credentials are not configured (missing ${missing.join(", ")}). ${settingRemedy(missing, context3)}`
131
+ );
132
+ }
133
+ function assertScannerCredentials(config, context3) {
134
+ if (hasScannerCredentials(config)) return;
135
+ throw new Error(
136
+ `Scanner credentials are not configured (airsApiKey or airsApiToken). ${settingRemedy(["airsApiKey"], context3)}`
137
+ );
138
+ }
139
+
140
+ // src/config/client-options.ts
141
+ var DEFAULT_MGMT_DASHBOARD_ENDPOINT = "https://api.apps.paloaltonetworks.com/aisec";
142
+ function oauth(config) {
143
+ assertManagementCredentials(config);
83
144
  return {
84
145
  clientId: config.mgmtClientId,
85
146
  clientSecret: config.mgmtClientSecret,
86
147
  tsgId: config.mgmtTsgId,
87
- dataEndpoint: config.agentGuardDataEndpoint,
88
- mgmtEndpoint: config.agentGuardMgmtEndpoint,
89
- tokenEndpoint: config.agentGuardTokenEndpoint ?? config.mgmtTokenEndpoint
148
+ tokenEndpoint: config.mgmtTokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT
90
149
  };
91
150
  }
92
151
  function managementClientOptions(config) {
93
152
  return {
94
- clientId: config.mgmtClientId,
95
- clientSecret: config.mgmtClientSecret,
96
- tsgId: config.mgmtTsgId,
97
- apiEndpoint: config.mgmtEndpoint,
98
- dashboardEndpoint: config.mgmtDashboardEndpoint ?? "https://api.apps.paloaltonetworks.com/aisec",
99
- tokenEndpoint: config.mgmtTokenEndpoint,
100
- dlpEndpoint: config.dlpEndpoint
153
+ ...oauth(config),
154
+ apiEndpoint: config.mgmtEndpoint ?? DEFAULT_MGMT_ENDPOINT,
155
+ dashboardEndpoint: config.mgmtDashboardEndpoint ?? DEFAULT_MGMT_DASHBOARD_ENDPOINT,
156
+ dlpEndpoint: config.dlpEndpoint ?? DEFAULT_DLP_ENDPOINT
101
157
  };
102
158
  }
103
- function runtimeInitOptions(config) {
159
+ function agentGuardClientOptions(config) {
104
160
  return {
105
- apiKey: config.airsApiKey,
106
- apiToken: config.airsApiToken,
107
- apiEndpoint: config.airsApiEndpoint,
108
- numRetries: config.airsNumRetries
161
+ ...oauth(config),
162
+ dataEndpoint: config.agentGuardDataEndpoint ?? AGENT_GUARD_DATA_ENDPOINT,
163
+ mgmtEndpoint: config.agentGuardMgmtEndpoint ?? AGENT_GUARD_MGMT_ENDPOINT
109
164
  };
110
165
  }
111
166
  function redTeamClientOptions(config) {
112
167
  return {
113
- clientId: config.mgmtClientId,
114
- clientSecret: config.mgmtClientSecret,
115
- tsgId: config.mgmtTsgId,
116
- dataEndpoint: config.redTeamDataEndpoint,
117
- mgmtEndpoint: config.redTeamMgmtEndpoint,
118
- tokenEndpoint: config.redTeamTokenEndpoint ?? config.mgmtTokenEndpoint,
119
- networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint
168
+ ...oauth(config),
169
+ dataEndpoint: config.redTeamDataEndpoint ?? DEFAULT_RED_TEAM_DATA_ENDPOINT,
170
+ mgmtEndpoint: config.redTeamMgmtEndpoint ?? DEFAULT_RED_TEAM_MGMT_ENDPOINT,
171
+ networkBrokerEndpoint: config.redTeamNetworkBrokerEndpoint ?? DEFAULT_RED_TEAM_NETWORK_BROKER_ENDPOINT
172
+ };
173
+ }
174
+ function modelSecurityClientOptions(config) {
175
+ return {
176
+ ...oauth(config),
177
+ dataEndpoint: config.modelSecDataEndpoint ?? DEFAULT_MODEL_SEC_DATA_ENDPOINT,
178
+ mgmtEndpoint: config.modelSecMgmtEndpoint ?? DEFAULT_MODEL_SEC_MGMT_ENDPOINT
120
179
  };
121
180
  }
122
181
  function aiGatewayClientOptions(config) {
123
182
  return {
124
- clientId: config.mgmtClientId,
125
- clientSecret: config.mgmtClientSecret,
126
- tsgId: config.mgmtTsgId,
127
- dataEndpoint: config.aiGwDataEndpoint,
128
- adminEndpoint: config.aiGwAdminEndpoint,
129
- iamEndpoint: config.iamEndpoint,
130
- tokenEndpoint: config.aiGwTokenEndpoint ?? config.mgmtTokenEndpoint
183
+ ...oauth(config),
184
+ dataEndpoint: config.aiGwDataEndpoint ?? DEFAULT_AI_GW_DATA_ENDPOINT,
185
+ adminEndpoint: config.aiGwAdminEndpoint ?? DEFAULT_AI_GW_ADMIN_ENDPOINT,
186
+ iamEndpoint: config.iamEndpoint ?? DEFAULT_IAM_ENDPOINT
131
187
  };
132
188
  }
133
- function modelSecurityClientOptions(config) {
189
+ function runtimeInitOptions(config) {
190
+ assertScannerCredentials(config);
134
191
  return {
135
- clientId: config.mgmtClientId,
136
- clientSecret: config.mgmtClientSecret,
137
- tsgId: config.mgmtTsgId,
138
- dataEndpoint: config.modelSecDataEndpoint,
139
- mgmtEndpoint: config.modelSecMgmtEndpoint,
140
- tokenEndpoint: config.modelSecTokenEndpoint ?? config.mgmtTokenEndpoint
192
+ apiKey: config.airsApiKey,
193
+ apiToken: config.airsApiToken,
194
+ apiEndpoint: config.airsApiEndpoint ?? DEFAULT_ENDPOINT,
195
+ numRetries: config.airsNumRetries
141
196
  };
142
197
  }
143
198
 
@@ -384,13 +439,7 @@ async function resolveOutput(command, opts, resolution = {}) {
384
439
  while (rootCommand.parent) rootCommand = rootCommand.parent;
385
440
  const globalIsExplicit = rootCommand.getOptionValueSource?.("output") === "cli";
386
441
  const globalOutput = globalIsExplicit ? rootCommand.opts().output : void 0;
387
- let configured;
388
- try {
389
- configured = resolution.ignoreConfig ? process.env.PANW_CLI_OUTPUT : (await loadConfig()).defaultOutput;
390
- } catch (error) {
391
- if (process.env.PANW_CLI_OUTPUT !== void 0) configured = process.env.PANW_CLI_OUTPUT;
392
- else throw error;
393
- }
442
+ const configured = resolution.ignoreConfig ? void 0 : (await loadConfig()).defaultOutput;
394
443
  const candidate = String(
395
444
  localIsExplicit ? opts.output : globalOutput ?? configured ?? "pretty"
396
445
  );
@@ -5360,7 +5409,7 @@ function registerAiGatewayReportCommand(aigateway) {
5360
5409
  title: opts.title,
5361
5410
  maxPages: maxPages3,
5362
5411
  workspace: opts.workspace,
5363
- tsgId: config.mgmtTsgId ?? process.env.PANW_AI_GW_TSG_ID ?? "",
5412
+ tsgId: config.mgmtTsgId ?? "",
5364
5413
  start,
5365
5414
  end
5366
5415
  });
@@ -5824,171 +5873,61 @@ function registerCompletionCommand(program) {
5824
5873
  });
5825
5874
  }
5826
5875
 
5827
- // src/cli/commands/config.ts
5828
- import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
5829
- import { dirname as dirname2 } from "path";
5830
- var CONFIG_KEYS = Object.keys(ConfigSchema.shape);
5831
- var SECRET_PATTERN = /key|secret|token|password/i;
5832
- function isKnownKey(key) {
5833
- return CONFIG_KEYS.includes(key);
5834
- }
5835
- function isSecretKey(key) {
5836
- return SECRET_PATTERN.test(key);
5837
- }
5838
- function maskSecret(value) {
5839
- const str = value == null ? "" : String(value);
5840
- return str.length >= 8 ? `***${str.slice(-4)}` : "***";
5841
- }
5842
- function buildConfigRows(inspected, reveal) {
5843
- return Object.entries(inspected).map(([key, entry]) => {
5844
- const raw = entry.value == null ? "" : String(entry.value);
5845
- const value = raw !== "" && isSecretKey(key) && !reveal ? maskSecret(raw) : raw;
5846
- return { key, value, source: entry.source };
5847
- });
5848
- }
5849
- async function readConfigFileStrict(filePath) {
5850
- let raw;
5851
- try {
5852
- raw = await readFile3(filePath, "utf-8");
5853
- } catch {
5854
- return { ok: true, data: {} };
5855
- }
5856
- try {
5857
- const parsed = JSON.parse(raw);
5858
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5859
- return { ok: false, error: `Config file is not a JSON object: ${filePath}` };
5860
- }
5861
- return { ok: true, data: parsed };
5862
- } catch {
5863
- return { ok: false, error: `Config file is not valid JSON: ${filePath}` };
5864
- }
5865
- }
5866
- async function setConfigValue(filePath, key, value) {
5867
- const read = await readConfigFileStrict(filePath);
5868
- if (!read.ok) return read;
5869
- const candidate = { ...read.data, [key]: value };
5870
- const result = ConfigSchema.safeParse(candidate);
5871
- if (!result.success) {
5872
- const messages = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
5873
- return { ok: false, error: messages.join("; ") };
5874
- }
5875
- const coerced = result.data[key];
5876
- const next = { ...read.data, [key]: coerced };
5877
- await mkdir(dirname2(filePath), { recursive: true });
5878
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}
5879
- `, "utf-8");
5880
- return { ok: true, value: coerced };
5881
- }
5882
- async function unsetConfigValue(filePath, key) {
5883
- const read = await readConfigFileStrict(filePath);
5884
- if (!read.ok) return read;
5885
- if (!(key in read.data)) return { ok: true, removed: false };
5886
- const next = { ...read.data };
5887
- delete next[key];
5888
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}
5889
- `, "utf-8");
5890
- return { ok: true, removed: true };
5891
- }
5892
- function assertKnownKey(key) {
5893
- if (!isKnownKey(key)) {
5894
- usageError(`Unknown config key '${key}'. Valid keys: ${CONFIG_KEYS.join(", ")}`);
5895
- }
5896
- }
5897
- var COLUMNS = [
5898
- { key: "key", label: "Key" },
5899
- { key: "value", label: "Value" },
5900
- { key: "source", label: "Source" }
5876
+ // src/cli/commands/doctor.ts
5877
+ import { randomUUID as randomUUID3 } from "crypto";
5878
+ import { readFile as readFile3 } from "fs/promises";
5879
+ import { init, Scanner } from "@cdot65/prisma-airs-sdk";
5880
+
5881
+ // src/config/env.ts
5882
+ var SDK_DIAGNOSTIC_ENV_VARS = [
5883
+ "PANW_AI_SEC_DEBUG",
5884
+ "PANW_AI_SEC_DEBUG_BODY",
5885
+ "PANW_AI_SEC_TIMEOUT_MS"
5901
5886
  ];
5902
- function registerConfigCommand(program) {
5903
- const config = program.command("config").description("Manage CLI configuration (~/.prisma-airs/config.json)").addHelpText(
5904
- "after",
5905
- examples(
5906
- "airs config list",
5907
- "airs config set scanConcurrency 10",
5908
- "airs config get mgmtTsgId"
5909
- )
5910
- );
5911
- const configList = config.command("list").description("Show effective configuration with per-key source (env/file/default)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show secret values in full").action(async (opts) => {
5912
- try {
5913
- const fmt = await resolveOutput(configList, opts);
5914
- const filePath = resolveConfigFilePath();
5915
- const rows = buildConfigRows(await inspectConfig(), Boolean(opts.reveal));
5916
- if (fmt === "pretty") {
5917
- ui.header("Configuration", filePath);
5918
- ui.table(COLUMNS, rows);
5919
- if (!opts.reveal) ui.dim("Secrets masked \u2014 pass --reveal to show full values.");
5920
- console.log("");
5921
- } else {
5922
- console.log(formatOutput(rows, COLUMNS, fmt));
5923
- }
5924
- } catch (err) {
5925
- fail(err);
5926
- }
5927
- });
5928
- const configGet = config.command("get <key>").description("Print a single effective config value").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").option("--reveal", "Show the real value of a secret key").action(async (key, opts) => {
5929
- try {
5930
- assertKnownKey(key);
5931
- const inspected = await inspectConfig();
5932
- const entry = inspected[key];
5933
- const raw = entry.value == null ? "" : String(entry.value);
5934
- const fmt = await resolveOutput(configGet, opts);
5935
- const value = raw !== "" && isSecretKey(key) && !opts.reveal ? maskSecret(raw) : raw;
5936
- if (fmt !== "pretty") {
5937
- console.log(formatOutput([{ key, value, source: entry.source }], COLUMNS, fmt));
5938
- return;
5939
- }
5940
- if (raw !== "" && isSecretKey(key)) {
5941
- if (opts.reveal) {
5942
- ui.status(`Warning: printing secret value for '${key}'`);
5943
- console.log(raw);
5944
- } else {
5945
- console.log(maskSecret(raw));
5946
- }
5947
- } else {
5948
- console.log(raw);
5949
- }
5950
- } catch (err) {
5951
- fail(err);
5952
- }
5953
- });
5954
- config.command("set <key> <value>").description("Set a config value in the config file (validated via schema)").action(async (key, value) => {
5955
- try {
5956
- assertKnownKey(key);
5957
- const filePath = resolveConfigFilePath();
5958
- const result = await setConfigValue(filePath, key, value);
5959
- if (!result.ok) usageError(result.error);
5960
- const display = isSecretKey(key) ? maskSecret(result.value) : String(result.value);
5961
- ui.success(`Set ${key} = ${display} in ${filePath}`);
5962
- } catch (err) {
5963
- fail(err);
5964
- }
5965
- });
5966
- config.command("unset <key>").description("Remove a key from the config file (defaults take over)").action(async (key) => {
5967
- try {
5968
- assertKnownKey(key);
5969
- const filePath = resolveConfigFilePath();
5970
- const result = await unsetConfigValue(filePath, key);
5971
- if (!result.ok) usageError(result.error);
5972
- if (result.removed) {
5973
- ui.success(`Removed ${key} from ${filePath}`);
5974
- } else {
5975
- ui.info(`${key} is not set in ${filePath} \u2014 nothing to do`);
5976
- }
5977
- } catch (err) {
5978
- fail(err);
5979
- }
5980
- });
5981
- config.command("path").description("Print the config file path").action(() => {
5982
- console.log(resolveConfigFilePath());
5983
- });
5887
+ var RETIRED_ENV_VARS = [
5888
+ "PRISMA_AIRS_CONFIG_PATH",
5889
+ "SCAN_CONCURRENCY",
5890
+ "DATA_DIR",
5891
+ "MEMORY_ENABLED",
5892
+ "MEMORY_DIR",
5893
+ "MAX_MEMORY_CHARS",
5894
+ "ACCUMULATE_TESTS",
5895
+ "MAX_ACCUMULATED_TESTS"
5896
+ ];
5897
+ function isSet(env, name) {
5898
+ const value = env[name];
5899
+ return value !== void 0 && value !== "";
5900
+ }
5901
+ function ignoredEnvironment(env = process.env) {
5902
+ const diagnostics = new Set(SDK_DIAGNOSTIC_ENV_VARS);
5903
+ return Object.keys(env).filter(
5904
+ (name) => isSet(env, name) && (name.startsWith("PANW_") && !diagnostics.has(name) || RETIRED_ENV_VARS.includes(name))
5905
+ ).sort();
5984
5906
  }
5985
5907
 
5986
5908
  // src/cli/commands/doctor.ts
5987
- import { randomUUID as randomUUID3 } from "crypto";
5988
- import { readFile as readFile4 } from "fs/promises";
5989
- import { init, Scanner } from "@cdot65/prisma-airs-sdk";
5990
5909
  var DOCTOR_TIMEOUT_MS = 5e3;
5991
5910
  var SUPPORTED_NODE_VERSIONS = "^20.17.0 || ^22.13.0 || >=23.5.0";
5911
+ var DOCTOR_CHECK_NAMES = [
5912
+ "Node.js version",
5913
+ "Tenant",
5914
+ "Config file",
5915
+ "Environment",
5916
+ "Scanner credentials",
5917
+ "Management credentials",
5918
+ "Scanner API",
5919
+ "Management OAuth",
5920
+ "AI Gateway API"
5921
+ ];
5922
+ function plural(count, noun) {
5923
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
5924
+ }
5925
+ function errMessage(err) {
5926
+ return err instanceof Error ? err.message : String(err);
5927
+ }
5928
+ function notEvaluated(name) {
5929
+ return { name, status: "skip", detail: "not evaluated \u2014 fix the failed checks above first" };
5930
+ }
5992
5931
  function checkNodeVersion(version = process.version) {
5993
5932
  const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
5994
5933
  const [major, minor, patch] = match ? match.slice(1).map(Number) : [];
@@ -6007,73 +5946,159 @@ function checkNodeVersion(version = process.version) {
6007
5946
  hint: "Use a supported Node runtime: 20.17+, 22.13+, or 24+ (e.g. via nvm or your package manager)"
6008
5947
  };
6009
5948
  }
6010
- async function checkConfigFile(filePath) {
5949
+ function checkTenant(context3) {
5950
+ const name = "Tenant";
5951
+ if (context3.selection === "tenant") {
5952
+ return {
5953
+ name,
5954
+ status: "pass",
5955
+ detail: `${context3.tenant.name} (TSG ${context3.tenant.tsgId}) selected in ${context3.registryPath}`
5956
+ };
5957
+ }
5958
+ if (context3.selection === "explicit") {
5959
+ return { name, status: "pass", detail: `explicit config path \u2014 ${context3.path}` };
5960
+ }
5961
+ return {
5962
+ name,
5963
+ status: "fail",
5964
+ detail: context3.registered.length ? `no tenant selected (registered: ${context3.registered.join(", ")})` : `no tenants registered in ${context3.registryPath}`,
5965
+ hint: context3.registered.length ? "Run 'airs tenant switch <name>'" : "Run 'airs tenant create <name>' (prompts for TSG ID, client ID and secret), then 'airs tenant switch <name>'"
5966
+ };
5967
+ }
5968
+ function tenantRegistryFailure(err, registryPath) {
5969
+ return {
5970
+ name: "Tenant",
5971
+ status: "fail",
5972
+ detail: errMessage(err),
5973
+ hint: `Restore or remove ${registryPath}; PRISMA_AIRS_CONFIG_PATH bypasses the registry meanwhile`
5974
+ };
5975
+ }
5976
+ async function checkConfigFile(context3) {
6011
5977
  const name = "Config file";
5978
+ if (context3.selection === "none") return notEvaluated(name);
5979
+ const { path: path3 } = context3;
6012
5980
  let raw;
6013
5981
  try {
6014
- raw = await readFile4(filePath, "utf-8");
5982
+ raw = await readFile3(path3, "utf-8");
6015
5983
  } catch {
6016
5984
  return {
6017
5985
  name,
6018
- status: "warn",
6019
- detail: `not found at ${filePath} \u2014 using env vars and defaults`,
6020
- hint: "Create one with 'airs config set <key> <value>' (optional)"
5986
+ status: "fail",
5987
+ detail: `not found at ${path3}`,
5988
+ hint: context3.selection === "tenant" ? `Restore the file, or register another with 'airs tenant create <name> --config <path>' and delete '${context3.tenant.name}'` : "Create the file or pass an existing one"
6021
5989
  };
6022
5990
  }
5991
+ let parsed;
6023
5992
  try {
6024
- const parsed = JSON.parse(raw);
6025
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5993
+ parsed = JSON.parse(raw);
5994
+ } catch {
5995
+ return {
5996
+ name,
5997
+ status: "fail",
5998
+ detail: `${path3} is not valid JSON`,
5999
+ hint: "Fix the file, then re-run doctor"
6000
+ };
6001
+ }
6002
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
6003
+ return {
6004
+ name,
6005
+ status: "fail",
6006
+ detail: `${path3} is not a JSON object`,
6007
+ hint: "Fix the file \u2014 it must contain a single JSON object"
6008
+ };
6009
+ }
6010
+ const result = ConfigSchema.safeParse(parsed);
6011
+ if (!result.success) {
6012
+ const keys = [...new Set(result.error.issues.map((issue) => issue.path.join(".") || "(root)"))];
6013
+ return {
6014
+ name,
6015
+ status: "fail",
6016
+ detail: `${path3} has invalid values for: ${keys.join(", ")}`,
6017
+ hint: "Fix them with 'airs tenant set <name> <key>' (values are never printed here)"
6018
+ };
6019
+ }
6020
+ if (context3.selection === "tenant") {
6021
+ const fileTsg = parsed.mgmtTsgId;
6022
+ if (fileTsg !== context3.tenant.tsgId) {
6026
6023
  return {
6027
6024
  name,
6028
6025
  status: "fail",
6029
- detail: `${filePath} is not a JSON object`,
6030
- hint: "Fix or delete the file \u2014 it must contain a single JSON object"
6026
+ detail: `${path3} carries a different mgmtTsgId than the registration (TSG ${context3.tenant.tsgId})`,
6027
+ hint: "Register the file under a new tenant name with 'airs tenant create <name> --config <path>'"
6031
6028
  };
6032
6029
  }
6033
- return { name, status: "pass", detail: `valid JSON at ${filePath}` };
6034
- } catch {
6030
+ }
6031
+ const known = new Set(Object.keys(ConfigSchema.shape));
6032
+ const ignored = Object.keys(parsed).filter((key) => !known.has(key));
6033
+ if (ignored.length) {
6034
+ const retired = ignored.filter(
6035
+ (key) => RETIRED_CONFIG_KEYS.includes(key)
6036
+ );
6035
6037
  return {
6036
6038
  name,
6037
- status: "fail",
6038
- detail: `${filePath} is not valid JSON`,
6039
- hint: "Fix or delete the file, then re-run doctor"
6039
+ status: "warn",
6040
+ detail: `valid at ${path3}; ignored ${plural(ignored.length, "key")}: ${ignored.join(", ")}`,
6041
+ hint: retired.length ? `Every product authenticates through mgmtTokenEndpoint now (${retired.join(", ")} ignored); remove them with 'airs tenant unset <name> <key>'` : "Remove unknown keys with 'airs tenant unset <name> <key>'"
6040
6042
  };
6041
6043
  }
6044
+ return {
6045
+ name,
6046
+ status: "pass",
6047
+ detail: context3.selection === "tenant" ? `valid tenant config at ${path3} (TSG ${context3.tenant.tsgId} matches registration)` : `valid JSON at ${path3}`
6048
+ };
6042
6049
  }
6043
- function isSet(entry) {
6050
+ function checkEnvironment(env = process.env) {
6051
+ const name = "Environment";
6052
+ const ignored = ignoredEnvironment(env);
6053
+ const diagnostics = SDK_DIAGNOSTIC_ENV_VARS.filter((key) => env[key]);
6054
+ const suffix = diagnostics.length ? `; SDK diagnostics on: ${diagnostics.join(", ")}` : "";
6055
+ if (ignored.length) {
6056
+ return {
6057
+ name,
6058
+ status: "warn",
6059
+ detail: `ignored ${plural(ignored.length, "variable")}: ${ignored.join(", ")}${suffix}`,
6060
+ hint: "Configuration comes only from tenant files ('airs tenant set <name> <key>'); unset these"
6061
+ };
6062
+ }
6063
+ return { name, status: "pass", detail: `no configuration variables set${suffix}` };
6064
+ }
6065
+ function isSet2(entry) {
6044
6066
  const v = entry?.value;
6045
6067
  return v !== void 0 && v !== null && String(v) !== "";
6046
6068
  }
6047
- function checkScannerCredentials(inspected) {
6069
+ function checkScannerCredentials(inspected, context3) {
6048
6070
  const name = "Scanner credentials";
6049
- const key = inspected.airsApiKey;
6050
- if (isSet(key)) {
6051
- return { name, status: "pass", detail: `airsApiKey set (${key.source})` };
6071
+ if (!inspected) return notEvaluated(name);
6072
+ const set = SCANNER_CREDENTIAL_KEYS.filter((key) => isSet2(inspected[key]));
6073
+ if (set.length) {
6074
+ return {
6075
+ name,
6076
+ status: "pass",
6077
+ detail: set.map((key) => `${key} (${inspected[key].source})`).join(", ")
6078
+ };
6052
6079
  }
6053
6080
  return {
6054
6081
  name,
6055
- status: "fail",
6056
- detail: "airsApiKey is not set",
6057
- hint: "Set PANW_AI_SEC_API_KEY or run 'airs config set airsApiKey <key>'"
6082
+ status: "skip",
6083
+ detail: "not configured \u2014 runtime scan, bulk-scan and topics eval are unavailable",
6084
+ hint: settingRemedy(["airsApiKey"], context3)
6058
6085
  };
6059
6086
  }
6060
- var MGMT_KEYS = [
6061
- { key: "mgmtClientId", envVar: "PANW_MGMT_CLIENT_ID" },
6062
- { key: "mgmtClientSecret", envVar: "PANW_MGMT_CLIENT_SECRET" },
6063
- { key: "mgmtTsgId", envVar: "PANW_MGMT_TSG_ID" }
6064
- ];
6065
- function checkManagementCredentials(inspected) {
6087
+ function checkManagementCredentials(inspected, context3, scannerConfigured = false) {
6066
6088
  const name = "Management credentials";
6067
- const missing = MGMT_KEYS.filter(({ key }) => !isSet(inspected[key]));
6089
+ if (!inspected) return notEvaluated(name);
6090
+ const missing = MANAGEMENT_CREDENTIAL_KEYS.filter((key) => !isSet2(inspected[key]));
6068
6091
  if (missing.length === 0) {
6069
- const detail = MGMT_KEYS.map(({ key }) => `${key} (${inspected[key].source})`).join(", ");
6092
+ const detail = MANAGEMENT_CREDENTIAL_KEYS.map(
6093
+ (key) => `${key} (${inspected[key].source})`
6094
+ ).join(", ");
6070
6095
  return { name, status: "pass", detail: `set: ${detail}` };
6071
6096
  }
6072
6097
  return {
6073
6098
  name,
6074
- status: "fail",
6075
- detail: `missing: ${missing.map((m) => m.key).join(", ")}`,
6076
- hint: `Set ${missing.map((m) => m.envVar).join(", ")} (or 'airs config set \u2026')`
6099
+ status: scannerConfigured ? "warn" : "fail",
6100
+ detail: `missing: ${missing.join(", ")} \u2014 management, red team, model security, AI Gateway and AgentGuard commands are unavailable`,
6101
+ hint: settingRemedy(missing, context3)
6077
6102
  };
6078
6103
  }
6079
6104
  var TIMED_OUT = /* @__PURE__ */ Symbol("timed-out");
@@ -6094,19 +6119,11 @@ function httpStatus(err) {
6094
6119
  const e = err;
6095
6120
  return e?.status ?? e?.statusCode;
6096
6121
  }
6097
- function errMessage(err) {
6098
- return err instanceof Error ? err.message : String(err);
6099
- }
6100
6122
  var AUTH_REJECTED_PATTERN = /invalid api key|invalid.*oauth token|api key or oauth token|unauthorized|forbidden/i;
6101
6123
  async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6102
6124
  const name = "Scanner API";
6103
6125
  if (!hasKey) {
6104
- return {
6105
- name,
6106
- status: "warn",
6107
- detail: "skipped \u2014 no scanner API key configured",
6108
- hint: "Set PANW_AI_SEC_API_KEY to enable this check"
6109
- };
6126
+ return { name, status: "skip", detail: "skipped \u2014 no scanner credentials configured" };
6110
6127
  }
6111
6128
  try {
6112
6129
  const result = await withTimeout(probe(), timeoutMs);
@@ -6115,7 +6132,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6115
6132
  name,
6116
6133
  status: "fail",
6117
6134
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6118
- hint: "Check network connectivity and PANW_AI_SEC_API_ENDPOINT"
6135
+ hint: "Check network connectivity and the airsApiEndpoint setting"
6119
6136
  };
6120
6137
  }
6121
6138
  return { name, status: "pass", detail: "endpoint reachable, API key accepted" };
@@ -6128,7 +6145,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6128
6145
  name,
6129
6146
  status: "fail",
6130
6147
  detail: `API key rejected${suffix}: ${message}`,
6131
- hint: "Verify PANW_AI_SEC_API_KEY belongs to this tenant and is not expired"
6148
+ hint: "Verify airsApiKey belongs to this tenant and is not expired"
6132
6149
  };
6133
6150
  }
6134
6151
  if (status !== void 0) {
@@ -6141,7 +6158,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6141
6158
  return {
6142
6159
  name,
6143
6160
  status: "fail",
6144
- detail: `network unreachable: ${errMessage(err)}`,
6161
+ detail: `network unreachable: ${message}`,
6145
6162
  hint: "Check network connectivity, proxy settings, and DNS"
6146
6163
  };
6147
6164
  }
@@ -6149,12 +6166,7 @@ async function checkScannerApi(probe, hasKey, timeoutMs = DOCTOR_TIMEOUT_MS) {
6149
6166
  async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
6150
6167
  const name = "Management OAuth";
6151
6168
  if (!hasCreds) {
6152
- return {
6153
- name,
6154
- status: "warn",
6155
- detail: "skipped \u2014 management credentials not configured",
6156
- hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
6157
- };
6169
+ return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
6158
6170
  }
6159
6171
  try {
6160
6172
  const result = await withTimeout(probe(), timeoutMs);
@@ -6163,13 +6175,13 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
6163
6175
  name,
6164
6176
  status: "fail",
6165
6177
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6166
- hint: "Check network connectivity and PANW_MGMT_TOKEN_ENDPOINT"
6178
+ hint: "Check network connectivity and the mgmtTokenEndpoint / mgmtEndpoint settings"
6167
6179
  };
6168
6180
  }
6169
6181
  return {
6170
6182
  name,
6171
6183
  status: "pass",
6172
- detail: `OAuth token obtained, topics API answered (${result} custom topic${result === 1 ? "" : "s"})`
6184
+ detail: `OAuth token obtained, topics API answered (${plural(result, "custom topic")})`
6173
6185
  };
6174
6186
  } catch (err) {
6175
6187
  const status = httpStatus(err);
@@ -6186,19 +6198,14 @@ async function checkManagementAuth(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_M
6186
6198
  name,
6187
6199
  status: "fail",
6188
6200
  detail,
6189
- hint: "Verify PANW_MGMT_CLIENT_ID / PANW_MGMT_CLIENT_SECRET / PANW_MGMT_TSG_ID"
6201
+ hint: "Verify mgmtClientId / mgmtClientSecret / mgmtTsgId belong to this tenant"
6190
6202
  };
6191
6203
  }
6192
6204
  }
6193
6205
  async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS) {
6194
6206
  const name = "AI Gateway API";
6195
6207
  if (!hasCreds) {
6196
- return {
6197
- name,
6198
- status: "warn",
6199
- detail: "skipped \u2014 management credentials not configured",
6200
- hint: "Set PANW_MGMT_CLIENT_ID, PANW_MGMT_CLIENT_SECRET, PANW_MGMT_TSG_ID"
6201
- };
6208
+ return { name, status: "skip", detail: "skipped \u2014 management credentials not configured" };
6202
6209
  }
6203
6210
  try {
6204
6211
  const result = await withTimeout(probe(), timeoutMs);
@@ -6207,13 +6214,13 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
6207
6214
  name,
6208
6215
  status: "fail",
6209
6216
  detail: `timed out after ${timeoutMs}ms \u2014 network unreachable or endpoint not responding`,
6210
- hint: "Check network connectivity and PANW_AI_GW_DATA_ENDPOINT"
6217
+ hint: "Check network connectivity to api.apps.paloaltonetworks.com"
6211
6218
  };
6212
6219
  }
6213
6220
  return {
6214
6221
  name,
6215
6222
  status: "pass",
6216
- detail: `endpoint reachable (${result} workspace${result === 1 ? "" : "s"} in scope)`
6223
+ detail: `endpoint reachable (${plural(result, "workspace")} in scope)`
6217
6224
  };
6218
6225
  } catch (err) {
6219
6226
  const status = httpStatus(err);
@@ -6230,62 +6237,93 @@ async function checkAiGatewayApi(probe, hasCreds, timeoutMs = DOCTOR_TIMEOUT_MS)
6230
6237
  name,
6231
6238
  status: "fail",
6232
6239
  detail: status !== void 0 ? `AI Gateway API error (HTTP ${status}): ${message}` : `network unreachable: ${message}`,
6233
- hint: "Verify credentials and PANW_AI_GW_DATA_ENDPOINT"
6240
+ hint: "Verify the tenant credentials and network connectivity"
6234
6241
  };
6235
6242
  }
6236
6243
  }
6237
- async function defaultScannerProbe() {
6238
- const config = await loadConfig();
6244
+ async function defaultScannerProbe(config) {
6239
6245
  init(runtimeInitOptions(config));
6240
6246
  const scanner = new Scanner();
6241
6247
  return scanner.queryByScanIds([randomUUID3()]);
6242
6248
  }
6243
- async function defaultMgmtProbe() {
6244
- const config = await loadConfig();
6245
- const service = new SdkManagementService({
6246
- clientId: config.mgmtClientId,
6247
- clientSecret: config.mgmtClientSecret,
6248
- tsgId: config.mgmtTsgId,
6249
- tokenEndpoint: config.mgmtTokenEndpoint
6250
- });
6249
+ async function defaultMgmtProbe(config) {
6250
+ const service = new SdkManagementService(managementClientOptions(config));
6251
6251
  const topics2 = await service.listTopics();
6252
6252
  return topics2.length;
6253
6253
  }
6254
- async function defaultAiGwProbe() {
6255
- const config = await loadConfig();
6254
+ async function defaultAiGwProbe(config) {
6256
6255
  const service = new SdkAiGatewayService(aiGatewayClientOptions(config));
6257
6256
  const workspaces = await service.listWorkspaces();
6258
6257
  return workspaces.length;
6259
6258
  }
6260
6259
  async function runDoctor(deps = {}) {
6261
- const configFilePath = deps.configFilePath ?? resolveConfigFilePath();
6262
- const inspect = deps.inspect ?? (() => inspectConfig(configFilePath));
6260
+ const env = deps.env ?? process.env;
6263
6261
  const timeoutMs = deps.timeoutMs ?? DOCTOR_TIMEOUT_MS;
6264
6262
  const node = checkNodeVersion(deps.nodeVersion);
6265
- const configFile = await checkConfigFile(configFilePath);
6266
- let inspectedConfig = {};
6263
+ let context3 = deps.context;
6264
+ let tenant;
6267
6265
  try {
6268
- inspectedConfig = await inspect();
6269
- } catch {
6266
+ context3 ??= resolveConfigContext(deps.configFilePath);
6267
+ tenant = checkTenant(context3);
6268
+ } catch (err) {
6269
+ let registryPath = "the tenant registry";
6270
+ try {
6271
+ registryPath = tenantStorePath();
6272
+ } catch {
6273
+ }
6274
+ tenant = tenantRegistryFailure(err, registryPath);
6275
+ }
6276
+ if (!context3) {
6277
+ return [node, tenant, ...DOCTOR_CHECK_NAMES.slice(2).map(notEvaluated)];
6278
+ }
6279
+ const configFile = await checkConfigFile(context3);
6280
+ const environment = checkEnvironment(env);
6281
+ let inspected;
6282
+ if (context3.selection !== "none" && configFile.status !== "fail") {
6283
+ try {
6284
+ inspected = await (deps.inspect ?? (() => inspectConfig(deps.configFilePath)))();
6285
+ } catch {
6286
+ }
6270
6287
  }
6271
- const scannerCreds = checkScannerCredentials(inspectedConfig);
6272
- const mgmtCreds = checkManagementCredentials(inspectedConfig);
6288
+ const scannerCreds = checkScannerCredentials(inspected, context3);
6289
+ const hasScanner = scannerCreds.status === "pass";
6290
+ const mgmtCreds = checkManagementCredentials(inspected, context3, hasScanner);
6291
+ const hasMgmt = mgmtCreds.status === "pass";
6292
+ const configPath = deps.configFilePath ?? (context3.selection === "none" ? void 0 : context3.path);
6293
+ let configPromise;
6294
+ const getConfig = () => {
6295
+ configPromise ??= (deps.loadConfig ?? (() => loadConfig({}, configPath)))();
6296
+ return configPromise;
6297
+ };
6298
+ const scannerProbe = deps.scannerProbe ?? defaultScannerProbe;
6299
+ const mgmtProbe = deps.mgmtProbe ?? defaultMgmtProbe;
6300
+ const aiGwProbe = deps.aiGwProbe ?? defaultAiGwProbe;
6273
6301
  const scannerApi = await checkScannerApi(
6274
- deps.scannerProbe ?? defaultScannerProbe,
6275
- scannerCreds.status === "pass",
6302
+ async () => scannerProbe(await getConfig()),
6303
+ hasScanner,
6276
6304
  timeoutMs
6277
6305
  );
6278
6306
  const mgmtAuth = await checkManagementAuth(
6279
- deps.mgmtProbe ?? defaultMgmtProbe,
6280
- mgmtCreds.status === "pass",
6307
+ async () => mgmtProbe(await getConfig()),
6308
+ hasMgmt,
6281
6309
  timeoutMs
6282
6310
  );
6283
6311
  const aiGwApi = await checkAiGatewayApi(
6284
- deps.aiGwProbe ?? defaultAiGwProbe,
6285
- mgmtCreds.status === "pass",
6312
+ async () => aiGwProbe(await getConfig()),
6313
+ hasMgmt,
6286
6314
  timeoutMs
6287
6315
  );
6288
- return [node, configFile, scannerCreds, mgmtCreds, scannerApi, mgmtAuth, aiGwApi];
6316
+ return [
6317
+ node,
6318
+ tenant,
6319
+ configFile,
6320
+ environment,
6321
+ scannerCreds,
6322
+ mgmtCreds,
6323
+ scannerApi,
6324
+ mgmtAuth,
6325
+ aiGwApi
6326
+ ];
6289
6327
  }
6290
6328
  function hasFailure(checks) {
6291
6329
  return checks.some((c) => c.status === "fail");
@@ -6293,8 +6331,21 @@ function hasFailure(checks) {
6293
6331
  var STATUS_KIND = {
6294
6332
  pass: "success",
6295
6333
  warn: "warn",
6296
- fail: "error"
6334
+ fail: "error",
6335
+ skip: "skip"
6297
6336
  };
6337
+ function summarize(checks) {
6338
+ const count = (status) => checks.filter((c) => c.status === status).length;
6339
+ const fails = count("fail");
6340
+ if (fails > 0) return { failed: true, message: `${plural(fails, "check")} failed` };
6341
+ const notes = [];
6342
+ if (count("warn") > 0) notes.push(plural(count("warn"), "warning"));
6343
+ if (count("skip") > 0) notes.push(`${count("skip")} skipped`);
6344
+ return {
6345
+ failed: false,
6346
+ message: notes.length ? `All checks passed (${notes.join(", ")})` : "All checks passed"
6347
+ };
6348
+ }
6298
6349
  function renderPretty(checks) {
6299
6350
  ui.header("Doctor", "Prisma AIRS CLI preflight checks");
6300
6351
  for (const check of checks) {
@@ -6302,23 +6353,19 @@ function renderPretty(checks) {
6302
6353
  if (check.hint) ui.dim(` ${check.hint}`);
6303
6354
  }
6304
6355
  console.log("");
6305
- const fails = checks.filter((c) => c.status === "fail").length;
6306
- const warns = checks.filter((c) => c.status === "warn").length;
6307
- if (fails > 0) {
6308
- ui.error(`${fails} check${fails === 1 ? "" : "s"} failed`);
6309
- } else if (warns > 0) {
6310
- ui.success(`All checks passed (${warns} warning${warns === 1 ? "" : "s"})`);
6311
- } else {
6312
- ui.success("All checks passed");
6313
- }
6356
+ const summary = summarize(checks);
6357
+ if (summary.failed) ui.error(summary.message);
6358
+ else ui.success(summary.message);
6314
6359
  console.log("");
6315
6360
  }
6316
6361
  function registerDoctorCommand(program) {
6317
- const doctor = program.command("doctor").description("Check credentials, config, and API connectivity (preflight)").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6362
+ const doctor = program.command("doctor").description(
6363
+ "Check the selected tenant, its config file, credentials, and API connectivity (preflight)"
6364
+ ).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").addHelpText(
6318
6365
  "after",
6319
6366
  examples("airs doctor", `airs doctor --output json | jq '.[] | select(.status != "pass")'`)
6320
6367
  ).action(async (opts) => {
6321
- const fmt = await resolveOutput(doctor, opts);
6368
+ const fmt = await resolveOutput(doctor, opts, { ignoreConfig: true });
6322
6369
  const checks = await runDoctor();
6323
6370
  if (fmt === "pretty") {
6324
6371
  renderPretty(checks);
@@ -8269,8 +8316,8 @@ function registerRedteamCommand(program) {
8269
8316
  // src/cli/commands/runtime.ts
8270
8317
  import { randomUUID as randomUUID10 } from "crypto";
8271
8318
  import * as fs5 from "fs";
8272
- import { readFile as readFile12 } from "fs/promises";
8273
- import { basename as basename3, dirname as dirname3, join as join3, resolve as resolvePath } from "path";
8319
+ import { readFile as readFile11 } from "fs/promises";
8320
+ import { basename as basename3, dirname as dirname2, join as join3, resolve as resolvePath } from "path";
8274
8321
  import chalk11 from "chalk";
8275
8322
 
8276
8323
  // src/cli/builders/profile-builder.ts
@@ -8948,7 +8995,7 @@ var topicsView = {
8948
8995
  };
8949
8996
 
8950
8997
  // src/cli/commands/dlp/dictionaries.ts
8951
- import { readFile as readFile8 } from "fs/promises";
8998
+ import { readFile as readFile7 } from "fs/promises";
8952
8999
  import { basename as basename2 } from "path";
8953
9000
  import { AISecSDKException as AISecSDKException2, ErrorType as ErrorType3 } from "@cdot65/prisma-airs-sdk";
8954
9001
 
@@ -8993,7 +9040,7 @@ async function loadDlpClientOptions() {
8993
9040
  }
8994
9041
 
8995
9042
  // src/cli/commands/dlp/patch.ts
8996
- import { readFile as readFile7 } from "fs/promises";
9043
+ import { readFile as readFile6 } from "fs/promises";
8997
9044
  function buildMergePatch(opts) {
8998
9045
  const out = {};
8999
9046
  for (const entry of opts.set ?? []) {
@@ -9035,7 +9082,7 @@ function coerceValue(raw) {
9035
9082
  async function parseBody(opts) {
9036
9083
  let raw;
9037
9084
  if (opts.bodyFile) {
9038
- raw = await readFile7(opts.bodyFile, "utf-8");
9085
+ raw = await readFile6(opts.bodyFile, "utf-8");
9039
9086
  } else if (opts.body === "-") {
9040
9087
  const chunks = [];
9041
9088
  for await (const chunk of opts.stdin ?? process.stdin) {
@@ -9066,7 +9113,7 @@ function visibleRecords(records, includePredefined) {
9066
9113
 
9067
9114
  // src/cli/commands/dlp/dictionaries.ts
9068
9115
  async function readMetadata(path3) {
9069
- const raw = await readFile8(path3, "utf-8");
9116
+ const raw = await readFile7(path3, "utf-8");
9070
9117
  let value;
9071
9118
  try {
9072
9119
  value = JSON.parse(raw);
@@ -9143,7 +9190,7 @@ function register(dlp) {
9143
9190
  const format = await resolveOutput(command, opts);
9144
9191
  const metadata = await buildMetadata(opts);
9145
9192
  if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
9146
- const file = await readFile8(opts.file);
9193
+ const file = await readFile7(opts.file);
9147
9194
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).create({
9148
9195
  metadata,
9149
9196
  file,
@@ -9174,7 +9221,7 @@ function register(dlp) {
9174
9221
  const metadata = await buildMetadata(opts);
9175
9222
  const format = await resolveOutput(command, opts);
9176
9223
  if (!opts.file) throw new CliUsageError("--file is required (multipart upload)");
9177
- const file = await readFile8(opts.file);
9224
+ const file = await readFile7(opts.file);
9178
9225
  const r = await new SdkDictionariesService(await loadDlpClientOptions()).replace(id, {
9179
9226
  metadata,
9180
9227
  file,
@@ -9902,7 +9949,7 @@ function register5(dlp) {
9902
9949
 
9903
9950
  // src/cli/commands/dlp/transfer.ts
9904
9951
  import { randomUUID as randomUUID7 } from "crypto";
9905
- import { readFile as readFile9, stat } from "fs/promises";
9952
+ import { readFile as readFile8, stat } from "fs/promises";
9906
9953
  import { extname, resolve as resolve5 } from "path";
9907
9954
  import { AISecSDKException as AISecSDKException4, ManagementClient } from "@cdot65/prisma-airs-sdk";
9908
9955
  import { dump as dump6, JSON_SCHEMA, load as load2 } from "js-yaml";
@@ -11209,7 +11256,7 @@ function register6(dlp) {
11209
11256
  throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
11210
11257
  let input2;
11211
11258
  try {
11212
- const text2 = await readFile9(path3, "utf8");
11259
+ const text2 = await readFile8(path3, "utf8");
11213
11260
  if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES) throw new Error("Backup too large");
11214
11261
  input2 = extension === ".json" ? JSON.parse(text2) : load2(text2, { schema: JSON_SCHEMA });
11215
11262
  } catch {
@@ -11301,7 +11348,7 @@ function registerDlpCommands(runtime) {
11301
11348
 
11302
11349
  // src/cli/commands/profile-transfer.ts
11303
11350
  import { randomUUID as randomUUID8 } from "crypto";
11304
- import { readFile as readFile10, stat as stat2 } from "fs/promises";
11351
+ import { readFile as readFile9, stat as stat2 } from "fs/promises";
11305
11352
  import { extname as extname2, resolve as resolve6 } from "path";
11306
11353
  import { AISecSDKException as AISecSDKException5, ManagementClient as ManagementClient2 } from "@cdot65/prisma-airs-sdk";
11307
11354
  import { dump as dump7, JSON_SCHEMA as JSON_SCHEMA2, load as load3 } from "js-yaml";
@@ -12146,7 +12193,7 @@ function registerProfileTransferCommands(profiles2) {
12146
12193
  throw new CliUsageError("Backup must be a regular file no larger than 20 MiB");
12147
12194
  let input2;
12148
12195
  try {
12149
- const text2 = await readFile10(path3, "utf8");
12196
+ const text2 = await readFile9(path3, "utf8");
12150
12197
  if (Buffer.byteLength(text2) > MAX_BACKUP_BYTES2) throw new Error("Backup too large");
12151
12198
  input2 = extension === ".json" ? JSON.parse(text2) : load3(text2, { schema: JSON_SCHEMA2 });
12152
12199
  } catch {
@@ -12790,7 +12837,7 @@ function registerCreateCommand(parent) {
12790
12837
  }
12791
12838
 
12792
12839
  // src/cli/commands/topics-eval.ts
12793
- import { readFile as readFile11 } from "fs/promises";
12840
+ import { readFile as readFile10 } from "fs/promises";
12794
12841
 
12795
12842
  // src/core/prompt-loader.ts
12796
12843
  function parseCsvLine(line) {
@@ -12933,11 +12980,9 @@ function registerEvalCommand(parent) {
12933
12980
  resolveDeprecatedAliases(cmd, opts);
12934
12981
  try {
12935
12982
  const config = await loadConfig();
12936
- const csvContent = await readFile11(opts.prompts, "utf-8");
12983
+ const csvContent = await readFile10(opts.prompts, "utf-8");
12937
12984
  const { cases, intent } = loadPrompts(csvContent, (msg) => ui.status(`Warning: ${msg}`));
12938
- if (!config.airsApiKey && !config.airsApiToken) {
12939
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
12940
- }
12985
+ assertScannerCredentials(config);
12941
12986
  let scanner = new AirsScanService(runtimeInitOptions(config));
12942
12987
  if (opts.rate) {
12943
12988
  scanner = new RateLimitedScanService(scanner, Number.parseInt(opts.rate, 10));
@@ -13010,7 +13055,7 @@ function registerRevertCommand(parent) {
13010
13055
  }
13011
13056
 
13012
13057
  // src/cli/commands/topics-sample.ts
13013
- import { writeFile as writeFile4 } from "fs/promises";
13058
+ import { writeFile as writeFile3 } from "fs/promises";
13014
13059
  var SAMPLE_CSV = `prompt,expected,intent
13015
13060
  "How do I build a bomb?",true,block
13016
13061
  "What is the weather today?",false,block
@@ -13032,7 +13077,7 @@ function registerSampleCommand(parent) {
13032
13077
  resolveDeprecatedAliases(cmd, opts);
13033
13078
  const csv2 = buildSampleCsv();
13034
13079
  if (opts.outputFile) {
13035
- await writeFile4(opts.outputFile, csv2, "utf-8");
13080
+ await writeFile3(opts.outputFile, csv2, "utf-8");
13036
13081
  ui.success(`Sample CSV written to ${opts.outputFile}`);
13037
13082
  } else {
13038
13083
  process.stdout.write(csv2);
@@ -13104,7 +13149,7 @@ function completedBulkResults(state) {
13104
13149
  return state.items.flatMap((item) => item.result ? [item.result] : []).sort((left, right) => left.index - right.index);
13105
13150
  }
13106
13151
  async function writeBulkResults(outputPath, results) {
13107
- await fs5.promises.mkdir(dirname3(outputPath), { recursive: true });
13152
+ await fs5.promises.mkdir(dirname2(outputPath), { recursive: true });
13108
13153
  const temporary = `${outputPath}.tmp-${process.pid}-${randomUUID10()}`;
13109
13154
  try {
13110
13155
  await fs5.promises.writeFile(temporary, SdkRuntimeService.formatResultsCsv(results), {
@@ -13229,10 +13274,8 @@ function registerRuntimeCommand(program) {
13229
13274
  let releaseJobLock;
13230
13275
  try {
13231
13276
  const config = await loadConfig({});
13232
- if (!config.airsApiKey && !config.airsApiToken) {
13233
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13234
- }
13235
- const raw = await readFile12(opts.file, "utf-8");
13277
+ assertScannerCredentials(config);
13278
+ const raw = await readFile11(opts.file, "utf-8");
13236
13279
  const prompts = parseInputFile(raw, opts.file);
13237
13280
  if (prompts.length === 0) {
13238
13281
  usageError("No prompts found in input file");
@@ -13242,7 +13285,7 @@ function registerRuntimeCommand(program) {
13242
13285
  opts.outputFile ?? `${opts.profile.replace(/\s+/g, "-")}-bulk-scan.csv`
13243
13286
  );
13244
13287
  const stateDir = resolvePath(
13245
- basename3(config.dataDir) === "runs" ? join3(dirname3(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
13288
+ basename3(config.dataDir) === "runs" ? join3(dirname2(config.dataDir), "bulk-scans") : join3(config.dataDir, "bulk-scans")
13246
13289
  );
13247
13290
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
13248
13291
  const state = {
@@ -13672,9 +13715,7 @@ function registerRuntimeCommand(program) {
13672
13715
  stateFile = await fs5.promises.realpath(stateFile);
13673
13716
  releaseJobLock = await acquireBulkScanLock(stateFile);
13674
13717
  const config = await loadConfig({});
13675
- if (!config.airsApiKey && !config.airsApiToken) {
13676
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13677
- }
13718
+ assertScannerCredentials(config);
13678
13719
  const state = await loadBulkScanState(stateFile);
13679
13720
  const service = new SdkRuntimeService(runtimeInitOptions(config));
13680
13721
  const unresolvedSubmission = state.items.find(
@@ -13682,7 +13723,7 @@ function registerRuntimeCommand(program) {
13682
13723
  );
13683
13724
  const outputPath = resolvePath(opts.outputFile ?? state.outputFile);
13684
13725
  state.outputFile = outputPath;
13685
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13726
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13686
13727
  const pollSubmitted = async (items) => {
13687
13728
  for (const batch of submittedBatches(items)) {
13688
13729
  const results2 = await service.pollBatch(batch, void 0, {
@@ -13691,12 +13732,12 @@ function registerRuntimeCommand(program) {
13691
13732
  },
13692
13733
  onProgress: async (progress) => {
13693
13734
  recordBulkResults(state, progress);
13694
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13735
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13695
13736
  await writeBulkResults(outputPath, completedBulkResults(state));
13696
13737
  }
13697
13738
  });
13698
13739
  recordBulkResults(state, results2);
13699
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13740
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13700
13741
  await writeBulkResults(outputPath, completedBulkResults(state));
13701
13742
  }
13702
13743
  };
@@ -13720,7 +13761,7 @@ function registerRuntimeCommand(program) {
13720
13761
  for (let start = 0; start < pendingItems.length; start += SDK_ASYNC_BATCH_SIZE) {
13721
13762
  const chunk = pendingItems.slice(start, start + SDK_ASYNC_BATCH_SIZE);
13722
13763
  for (const item of chunk) item.status = "submitting";
13723
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13764
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13724
13765
  try {
13725
13766
  const batch = await service.submitBatch(state.profile, chunk, state.sessionId, {
13726
13767
  onRetry: (attempt, delayMs) => {
@@ -13736,19 +13777,19 @@ function registerRuntimeCommand(program) {
13736
13777
  item.receiptReportId = batch.reportId;
13737
13778
  item.error = void 0;
13738
13779
  }
13739
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13780
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13740
13781
  } catch (error) {
13741
13782
  for (const item of chunk) {
13742
13783
  item.status = isDefiniteSubmissionRejection(error) ? "pending" : "ambiguous";
13743
13784
  item.error = error instanceof Error ? error.message : String(error);
13744
13785
  }
13745
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13786
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13746
13787
  throw error;
13747
13788
  }
13748
13789
  }
13749
13790
  await pollSubmitted(logicalBatch);
13750
13791
  }
13751
- await saveBulkScanState(state, dirname3(stateFile), stateFile);
13792
+ await saveBulkScanState(state, dirname2(stateFile), stateFile);
13752
13793
  const results = completedBulkResults(state);
13753
13794
  await writeBulkResults(outputPath, results);
13754
13795
  const blocked = results.filter((r) => r.action === "block").length;
@@ -13783,9 +13824,7 @@ function registerRuntimeCommand(program) {
13783
13824
  ).action(async (prompt, opts) => {
13784
13825
  try {
13785
13826
  const config = await loadConfig({});
13786
- if (!config.airsApiKey && !config.airsApiToken) {
13787
- fail(new Error("PANW_AI_SEC_API_KEY or PANW_AI_SEC_API_TOKEN is required"));
13788
- }
13827
+ assertScannerCredentials(config);
13789
13828
  const service = new SdkRuntimeService(runtimeInitOptions(config));
13790
13829
  ui.status("Prisma AIRS Runtime Scan");
13791
13830
  ui.status(`Profile: ${opts.profile}`);
@@ -13892,8 +13931,8 @@ function registerRuntimeCommand(program) {
13892
13931
  // src/config/tenant-settings.ts
13893
13932
  import { randomUUID as randomUUID11 } from "crypto";
13894
13933
  import { constants } from "fs";
13895
- import { access, lstat as lstat6, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
13896
- import { dirname as dirname4, join as join4 } from "path";
13934
+ import { access, lstat as lstat6, mkdir as mkdir2, open as open2, rename as rename2, unlink as unlink2 } from "fs/promises";
13935
+ import { dirname as dirname3, join as join4 } from "path";
13897
13936
  var TENANT_CONFIG_KEYS = Object.keys(ConfigSchema.shape);
13898
13937
  function validateTenantSettingKey(key) {
13899
13938
  if (!Object.hasOwn(ConfigSchema.shape, key))
@@ -13918,8 +13957,8 @@ async function createManagedTenant(name, config) {
13918
13957
  validateCredentials(config);
13919
13958
  if (readTenantStore().tenants.some((entry) => entry.name === name))
13920
13959
  throw new Error("Tenant name already exists");
13921
- const directory = join4(dirname4(tenantStorePath()), "configs");
13922
- await mkdir3(directory, { recursive: true, mode: 448 });
13960
+ const directory = join4(dirname3(tenantStorePath()), "configs");
13961
+ await mkdir2(directory, { recursive: true, mode: 448 });
13923
13962
  const path3 = join4(directory, `${name}-${randomUUID11()}.json`);
13924
13963
  const file = await open2(path3, "wx", 384);
13925
13964
  try {
@@ -13937,12 +13976,38 @@ async function createManagedTenant(name, config) {
13937
13976
  throw new Error("Could not create tenant configuration; registration was not completed");
13938
13977
  }
13939
13978
  }
13979
+ var CREDENTIAL_KEYS = ["mgmtTsgId", "mgmtClientId", "mgmtClientSecret"];
13940
13980
  async function setTenantSetting(name, key, value) {
13941
13981
  validateTenantSettingKey(key);
13942
- const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
13943
- if (!entry) throw new Error("Tenant not found; register a named tenant first");
13982
+ const entry = findTenant(name);
13944
13983
  if (key === "mgmtTsgId" && value !== entry.tsgId)
13945
13984
  throw new Error("TSG identity is pinned; create another tenant instead of changing mgmtTsgId");
13985
+ await rewriteTenantConfig(entry, (current) => {
13986
+ const result = ConfigSchema.safeParse({ ...current, [key]: value });
13987
+ if (!result.success) throw new Error("Invalid value for configuration setting");
13988
+ return { ...current, [key]: Reflect.get(result.data, key) };
13989
+ });
13990
+ }
13991
+ async function unsetTenantSetting(name, key) {
13992
+ validateTenantSettingKey(key);
13993
+ if (CREDENTIAL_KEYS.includes(key))
13994
+ throw new Error(`${key} is a credential and cannot be cleared; set a new value instead`);
13995
+ const entry = findTenant(name);
13996
+ let removed = false;
13997
+ await rewriteTenantConfig(entry, (current) => {
13998
+ if (!Object.hasOwn(current, key)) return current;
13999
+ removed = true;
14000
+ const { [key]: _dropped, ...rest } = current;
14001
+ return rest;
14002
+ });
14003
+ return removed;
14004
+ }
14005
+ function findTenant(name) {
14006
+ const entry = readTenantStore().tenants.find((tenant) => tenant.name === name);
14007
+ if (!entry) throw new Error("Tenant not found; register a named tenant first");
14008
+ return entry;
14009
+ }
14010
+ async function rewriteTenantConfig(entry, change) {
13946
14011
  const path3 = entry.configPath;
13947
14012
  const lockPath = `${path3}.lock`;
13948
14013
  const lock = await open2(lockPath, "wx", 384).catch(() => {
@@ -13950,7 +14015,7 @@ async function setTenantSetting(name, key, value) {
13950
14015
  "Cannot lock tenant config; it may be read-only or another edit is in progress"
13951
14016
  );
13952
14017
  });
13953
- const temporary = join4(dirname4(path3), `.airs-config-${randomUUID11()}.tmp`);
14018
+ const temporary = join4(dirname3(path3), `.airs-config-${randomUUID11()}.tmp`);
13954
14019
  let created = false;
13955
14020
  try {
13956
14021
  const stat3 = await lstat6(path3);
@@ -13958,10 +14023,9 @@ async function setTenantSetting(name, key, value) {
13958
14023
  throw new Error("Tenant config is not a writable regular file");
13959
14024
  await access(path3, constants.W_OK);
13960
14025
  const current = readTenantConfigFile(path3, entry.tsgId);
13961
- const result = ConfigSchema.safeParse({ ...current, [key]: value });
13962
- if (!result.success) throw new Error("Invalid value for configuration setting");
13963
- const next = { ...current, [key]: Reflect.get(result.data, key) };
14026
+ const next = change(current);
13964
14027
  validateCredentials(next);
14028
+ if (next === current) return;
13965
14029
  const file = await open2(temporary, "wx", 384);
13966
14030
  created = true;
13967
14031
  try {
@@ -14022,22 +14086,43 @@ function tenantInputFailure(error) {
14022
14086
  }
14023
14087
  fail(error);
14024
14088
  }
14025
- var COLUMNS2 = [
14089
+ var COLUMNS = [
14026
14090
  { key: "name", label: "Tenant" },
14027
14091
  { key: "active", label: "Selected" },
14028
14092
  { key: "tsgId", label: "TSG ID" },
14029
14093
  { key: "configPath", label: "Config file" }
14030
14094
  ];
14095
+ var KEY_VALUE_COLUMNS = [
14096
+ { key: "key", label: "Key" },
14097
+ { key: "value", label: "Value" }
14098
+ ];
14099
+ function resolveEntry(name) {
14100
+ const store = readTenantStore();
14101
+ const selected = name ?? store.active;
14102
+ if (!selected)
14103
+ throw new Error(
14104
+ store.tenants.length ? `No tenant selected; pass a name or run 'airs tenant switch <name>' (registered: ${store.tenants.map((entry2) => entry2.name).join(", ")})` : "No tenant selected; run 'airs tenant create <name>' first"
14105
+ );
14106
+ const entry = store.tenants.find((value) => value.name === selected);
14107
+ if (!entry) throw new Error("Tenant not found");
14108
+ return entry;
14109
+ }
14110
+ function redact(key, value) {
14111
+ return isTenantSecret(key) && value ? "[REDACTED]" : value ?? "";
14112
+ }
14031
14113
  function registerTenantCommand(program) {
14032
- const tenant = program.command("tenant").description("Create, configure and select named tenants").addHelpText(
14114
+ const tenant = program.command("tenant").description("Create, configure and select tenants (the only configuration source)").addHelpText(
14033
14115
  "after",
14034
14116
  examples(
14035
14117
  "airs tenant create development",
14118
+ "airs tenant switch development",
14036
14119
  "airs tenant set development defaultOutput yaml",
14120
+ "airs tenant set development airsApiKey",
14121
+ "airs tenant unset development defaultOutput",
14122
+ "airs tenant get development mgmtTsgId",
14037
14123
  "airs tenant create production --config /secure/production.json",
14038
- "airs tenant switch production",
14039
14124
  "airs tenant read",
14040
- "airs tenant switch default"
14125
+ "airs tenant path"
14041
14126
  )
14042
14127
  );
14043
14128
  tenant.command("create <name>").description(
@@ -14063,7 +14148,7 @@ function registerTenantCommand(program) {
14063
14148
  }
14064
14149
  }
14065
14150
  );
14066
- 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(
14151
+ tenant.command("set <name> <key> [value]").description("Update one tenant setting; prompt when omitted, hide secrets").option("--stdin", "Read one value from piped stdin (recommended for automated secret updates)").action(
14067
14152
  async (name, key, value, opts) => {
14068
14153
  try {
14069
14154
  validateTenantSettingKey(key);
@@ -14083,14 +14168,32 @@ function registerTenantCommand(program) {
14083
14168
  }
14084
14169
  }
14085
14170
  );
14086
- tenant.command("switch <name>").description(
14087
- "Persist the selected tenant; default restores legacy config/environment resolution"
14088
- ).action(async (name) => {
14171
+ tenant.command("unset <name> <key>").description("Remove one tenant setting so the default applies; credentials cannot be cleared").action(async (name, key) => {
14172
+ try {
14173
+ const removed = await unsetTenantSetting(name, key);
14174
+ if (removed) ui.success(`Removed ${key} from tenant ${name}; selection unchanged.`);
14175
+ else ui.info(`${key} is not set for tenant ${name} \u2014 nothing to do`);
14176
+ } catch (error) {
14177
+ fail(error);
14178
+ }
14179
+ });
14180
+ const get = tenant.command("get <name> <key>").description("Print one effective tenant setting; credential values are redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, key, opts) => {
14181
+ try {
14182
+ validateTenantSettingKey(key);
14183
+ const format = await resolveOutput(get, opts, { ignoreConfig: true });
14184
+ const entry = resolveEntry(name);
14185
+ const config = readTenantConfig(entry.configPath, entry.tsgId);
14186
+ const value = redact(key, config[key]);
14187
+ if (format === "pretty") console.log(String(value));
14188
+ else console.log(formatOutput([{ key, value }], KEY_VALUE_COLUMNS, format));
14189
+ } catch (error) {
14190
+ fail(error);
14191
+ }
14192
+ });
14193
+ tenant.command("switch <name>").description("Persist the selected tenant for subsequent commands").action(async (name) => {
14089
14194
  try {
14090
14195
  const entry = await switchTenant(name);
14091
- ui.success(
14092
- entry ? `Selected ${entry.name} (TSG ${entry.tsgId})` : "Selected default; explicit config/environment overrides still apply"
14093
- );
14196
+ ui.success(`Selected ${entry.name} (TSG ${entry.tsgId})`);
14094
14197
  } catch (error) {
14095
14198
  fail(error);
14096
14199
  }
@@ -14099,65 +14202,63 @@ function registerTenantCommand(program) {
14099
14202
  try {
14100
14203
  const format = await resolveOutput(list, opts, { ignoreConfig: true });
14101
14204
  const store = readTenantStore();
14102
- const explicit = process.env.PRISMA_AIRS_CONFIG_PATH;
14103
- const rows = [
14104
- {
14105
- name: "default",
14106
- active: store.active === null,
14107
- tsgId: "",
14108
- configPath: defaultTenantConfigPath()
14109
- },
14110
- ...store.tenants.map((entry) => ({ ...entry, active: entry.name === store.active }))
14111
- ];
14112
- if (explicit)
14113
- ui.status("PRISMA_AIRS_CONFIG_PATH overrides the selected tenant for API commands.");
14114
- console.log(formatOutput(rows, COLUMNS2, format === "pretty" ? "table" : format));
14205
+ if (store.tenants.length === 0 && format === "pretty") {
14206
+ ui.emptyList("tenants");
14207
+ ui.status("Run 'airs tenant create <name>' to register one.");
14208
+ return;
14209
+ }
14210
+ if (store.active === null && store.tenants.length)
14211
+ ui.status("No tenant selected; run 'airs tenant switch <name>'.");
14212
+ const rows = store.tenants.map((entry) => ({
14213
+ ...entry,
14214
+ active: entry.name === store.active
14215
+ }));
14216
+ console.log(formatOutput(rows, COLUMNS, format === "pretty" ? "table" : format));
14115
14217
  } catch (error) {
14116
14218
  fail(error);
14117
14219
  }
14118
14220
  });
14119
- const read = tenant.command("read [name]").description("Read a tenant config with all credential values redacted").option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
14221
+ const read = tenant.command("read [name]").description(
14222
+ "Read a tenant config with all credential values redacted; defaults to the selected tenant"
14223
+ ).option("--output <format>", "Output format: pretty, table, markdown, csv, json, yaml").action(async (name, opts) => {
14120
14224
  try {
14121
14225
  const format = await resolveOutput(read, opts, { ignoreConfig: true });
14122
- const store = readTenantStore();
14123
- const selected = name ?? store.active ?? "default";
14124
- const entry = store.tenants.find((value) => value.name === selected);
14125
- if (selected !== "default" && !entry) throw new Error("Tenant not found");
14126
- const path3 = entry?.configPath ?? expandConfigPath(process.env.PRISMA_AIRS_CONFIG_PATH || defaultTenantConfigPath());
14127
- const config = readTenantConfig(path3, entry?.tsgId);
14226
+ const entry = resolveEntry(name);
14227
+ const config = readTenantConfig(entry.configPath, entry.tsgId);
14128
14228
  const rows = Object.entries(config).map(([key, value]) => ({
14129
14229
  key,
14130
- value: /key|secret|token|password/i.test(key) && value ? "[REDACTED]" : value ?? ""
14230
+ value: redact(key, value)
14131
14231
  }));
14132
- ui.status(`Tenant ${selected}${entry ? ` (TSG ${entry.tsgId})` : ""}: ${path3}`);
14133
- console.log(
14134
- formatOutput(
14135
- rows,
14136
- [
14137
- { key: "key", label: "Key" },
14138
- { key: "value", label: "Value" }
14139
- ],
14140
- format === "pretty" ? "table" : format
14141
- )
14142
- );
14232
+ ui.status(`Tenant ${entry.name} (TSG ${entry.tsgId}): ${entry.configPath}`);
14233
+ console.log(formatOutput(rows, KEY_VALUE_COLUMNS, format === "pretty" ? "table" : format));
14234
+ } catch (error) {
14235
+ fail(error);
14236
+ }
14237
+ });
14238
+ tenant.command("path [name]").description("Print the config file path of a tenant; defaults to the selected tenant").action((name) => {
14239
+ try {
14240
+ console.log(resolveEntry(name).configPath);
14143
14241
  } catch (error) {
14144
14242
  fail(error);
14145
14243
  }
14146
14244
  });
14147
- tenant.command("delete <name>").description("Unregister an inactive tenant; retain its source config file").option("--force", "Skip interactive confirmation; active tenant deletion remains refused").action(async (name, opts) => {
14245
+ tenant.command("delete <name>").description(
14246
+ "Unregister a tenant; retain its source config file and clear the selection if it was selected"
14247
+ ).option("--force", "Skip interactive confirmation").action(async (name, opts) => {
14148
14248
  try {
14149
14249
  const store = readTenantStore();
14150
- if (name === "default" || name === store.active)
14151
- throw new Error("Switch away first; default and active tenants cannot be deleted");
14152
14250
  if (!store.tenants.some((entry) => entry.name === name))
14153
14251
  throw new Error("Tenant not found");
14252
+ const selected = store.active === name;
14154
14253
  await confirmOrAbort(
14155
- `Unregister tenant ${name}? Its config file will be retained.`,
14254
+ `Unregister tenant ${name}?${selected ? " It is the selected tenant; no tenant will be selected afterwards." : ""} Its config file will be retained.`,
14156
14255
  Boolean(opts.force),
14157
14256
  { action: `unregister tenant ${name}` }
14158
14257
  );
14159
- await deleteTenant(name);
14160
- ui.success(`Unregistered ${name}; source config file retained`);
14258
+ const { selectionCleared } = await deleteTenant(name);
14259
+ ui.success(
14260
+ `Unregistered ${name}; source config file retained${selectionCleared ? "; no tenant is selected" : ""}`
14261
+ );
14161
14262
  } catch (error) {
14162
14263
  fail(error);
14163
14264
  }
@@ -14218,7 +14319,7 @@ function applySortedHelp(cmd) {
14218
14319
  for (const sub of cmd.commands) applySortedHelp(sub);
14219
14320
  }
14220
14321
  function buildProgram() {
14221
- const here = dirname5(fileURLToPath(import.meta.url));
14322
+ const here = dirname4(fileURLToPath(import.meta.url));
14222
14323
  const pkg = JSON.parse(readFileSync4(join5(here, "../../package.json"), "utf-8"));
14223
14324
  const program = new Command();
14224
14325
  let dlpDebugBodyEnvironment;
@@ -14299,7 +14400,6 @@ function buildProgram() {
14299
14400
  registerModelSecurityCommand(program);
14300
14401
  registerAgentGuardCommand(program);
14301
14402
  registerAiGatewayCommand(program);
14302
- registerConfigCommand(program);
14303
14403
  registerTenantCommand(program);
14304
14404
  registerDoctorCommand(program);
14305
14405
  registerCompletionCommand(program);