fullcourtdefense-cli 1.35.2 → 1.35.5

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.
@@ -6945,6 +6945,7 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
6945
6945
  if (/^data:/i.test(token) || /^[A-Za-z0-9+/]{200,}={0,2}$/.test(token)) return false;
6946
6946
  if (/^(?:https?|s3|gs|file):\/\//i.test(token) || /[\\/]{1}[\w.-]+[\\/]/.test(token)) return false;
6947
6947
  if (allowWordSlug && /^[A-Za-z_][\w.-]*\/[A-Za-z_][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(token)) return false;
6948
+ if (allowWordSlug && /[/.]/.test(token) && token.split(/[/.]/).length >= 2 && token.split(/[/.]/).every((part) => /^[A-Za-z][a-z]*(?:[A-Z][a-z0-9]*)*$/.test(part))) return false;
6948
6949
  if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(token) || /^[a-z]+(?:_[a-z0-9]+)+$/.test(token)) return false;
6949
6950
  if (allowWordSlug && /^[A-Z][a-z]+-(?:[A-Z][a-z]+){2,}$/.test(token)) return false;
6950
6951
  if (allowWordSlug && /^[a-z]+(?:-[a-z]+)+(?:-\d+)?$/.test(token)) return false;
@@ -7033,7 +7034,7 @@ function hasSecretLikeValue(value) {
7033
7034
  return detectSecretKind(value) !== "none";
7034
7035
  }
7035
7036
  var PII_EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
7036
- var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]+)$|^(?:no-?reply|noreply|do-?not-?reply|test\d*|example|user\d*|admin|root|foo|bar|baz|someone|somebody|you|me|name|first\.last|john\.doe|jane\.doe|email)@)/i;
7037
+ var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]+|[\w.-]*\.gserviceaccount\.com|[\w.-]*\.svc\.id\.goog)$|^(?:no-?reply|noreply|do-?not-?reply|test\d*|example|user\d*|admin|root|foo|bar|baz|someone|somebody|you|me|name|first\.last|john\.doe|jane\.doe|email)@)/i;
7037
7038
  var PII_PHONE_RE = /(?:^|[^\w.@/:-])(\+?\(?\d{1,4}\)?[\s-]\(?\d{2,4}\)?[\s-]\d{2,4}(?:[\s-]\d{2,4}){0,2})(?![\w.@/:-])/g;
7038
7039
  function hasPiiLikeValue(raw) {
7039
7040
  if (!raw) return false;
@@ -7171,7 +7172,9 @@ function extractUrl(args, _argsText) {
7171
7172
  if (typeof raw !== "string") continue;
7172
7173
  const shellField = ["command", "cmd", "script", "input"].includes(field);
7173
7174
  const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).map(withoutGhMetadataProse).join("\n") : raw;
7174
- const firstUrl = (text) => text.match(/https?:\/\/[^\s"'<>`]+/i)?.[0] || text.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>`]+/i)?.[0];
7175
+ const firstWebUrl = (text) => text.match(/https?:\/\/[^\s"'<>`]+/i)?.[0];
7176
+ const firstStorageUrl = (text) => text.match(/\b(?:s3|gs|az|ftp|sftp|smb):\/\/[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[^\s"'<>`]*)?/i)?.[0];
7177
+ const firstUrl = (text) => firstWebUrl(text) || firstStorageUrl(text);
7175
7178
  const outboundUrls = shellField ? splitShellSegments(value).filter((segment) => /^(?:curl(?:\.exe)?|wget(?:\.exe)?|invoke-restmethod|invoke-webrequest|irm|iwr)$/i.test(shellSegmentLead(segment)) && classifyShellSegment(segment) === "write").map(firstUrl).filter((url) => Boolean(url)) : [];
7176
7179
  const externalOutbound = outboundUrls.find((url) => {
7177
7180
  try {
@@ -7180,7 +7183,8 @@ function extractUrl(args, _argsText) {
7180
7183
  return false;
7181
7184
  }
7182
7185
  });
7183
- const found = externalOutbound || outboundUrls[0] || firstUrl(shellField ? segmentsThatMayReachNetwork(splitShellSegments(value)).join("\n") : value);
7186
+ const networkSegments = shellField ? segmentsThatMayReachNetwork(splitShellSegments(value)) : [value];
7187
+ const found = externalOutbound || outboundUrls[0] || firstWebUrl(networkSegments.join("\n")) || (shellField ? firstStorageUrl(networkSegments.filter((segment, index, all) => storageUriIsDestination(segment, index, all)).join("\n")) : void 0) || firstStorageUrl(networkSegments.join("\n"));
7184
7188
  if (found) return found;
7185
7189
  }
7186
7190
  return "";
@@ -7827,6 +7831,24 @@ function shellSegmentLead(segment) {
7827
7831
  }
7828
7832
  var PS_SESSION_VARIABLE_ASSIGN_RE = /^\s*\$(?:psdefaultparametervalues|psmodulepath|profile|executioncontext|env:(?:path|pathext|psmodulepath))\s*(?:\[[^\]]*\]\s*)?(?:\+?=)/i;
7829
7833
  var PS_VALUE_EXECUTOR_RE = /\b(?:invoke-expression|iex|invoke-command|icm|start-process|saps|invoke-item|ii)\b|&\s*[$(]|\.\s*invoke\s*\(/i;
7834
+ var PS_VALUE_LEAD_RE = /^\$(?:env:)?\w+(?:\.\w+|\[\d+\])*$/;
7835
+ var PS_BLOCK_STAGE_LEADS = /* @__PURE__ */ new Set(["foreach-object", "foreach", "%", "where-object", "where", "?"]);
7836
+ function classifyPipelineStage(stage, depth) {
7837
+ const trimmed = stage.trim();
7838
+ if (!trimmed) return "read";
7839
+ if (isPowerShellReadFormatter(trimmed)) return "read";
7840
+ const lead = shellSegmentLead(trimmed);
7841
+ if (PS_BLOCK_STAGE_LEADS.has(lead) && depth < 3) {
7842
+ const open = trimmed.indexOf("{");
7843
+ const close = trimmed.lastIndexOf("}");
7844
+ if (open >= 0 && close > open) {
7845
+ const rest = maskQuotedSpans(trimmed.slice(0, open) + trimmed.slice(close + 1));
7846
+ if (!/^\s*(?:foreach-object|foreach|%|where-object|where|\?)\s*(?:-process\s*)?\s*$/i.test(rest)) return "SHELL";
7847
+ return classifyShellCommandOperation(trimmed.slice(open + 1, close), depth + 1);
7848
+ }
7849
+ }
7850
+ return classifyShellSegment(trimmed, depth + 1);
7851
+ }
7830
7852
  function isInertValueExpression(raw) {
7831
7853
  const masked = maskQuotedSpans(raw);
7832
7854
  if (/\$\(|`|&\s*[$(\w"']|[|<>]/.test(masked) || PS_VALUE_EXECUTOR_RE.test(masked)) return false;
@@ -7843,8 +7865,12 @@ function classifyShellSegment(segment, depth = 0) {
7843
7865
  if (!raw) return "read";
7844
7866
  if (PS_SESSION_VARIABLE_ASSIGN_RE.test(raw)) return "SHELL";
7845
7867
  const lead = shellSegmentLead(segment);
7846
- if ((/^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
7847
- if (SHELL_NEUTRAL_BUILTINS.has(lead) || /^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) return "read";
7868
+ if (PS_VALUE_LEAD_RE.test(lead) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
7869
+ if (SHELL_NEUTRAL_BUILTINS.has(lead)) return "read";
7870
+ if (PS_VALUE_LEAD_RE.test(lead)) {
7871
+ const stages = splitShellSegments(raw, true).slice(1);
7872
+ return worstShellOp(stages.map((stage) => classifyPipelineStage(stage, depth)), "read");
7873
+ }
7848
7874
  if (isInertValueExpression(raw)) return "read";
7849
7875
  const rawLower = raw.toLowerCase();
7850
7876
  const sqlClient = SHELL_SQL_CLIENTS.has(lead) || /\|\s*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlcmd|sqlite3|mongosh?|clickhouse-client|bq|cockroach|usql)\b/.test(rawLower);
@@ -7896,7 +7922,26 @@ function classifyShellWords(masked, raw, lead) {
7896
7922
  if (lead === "git" && /\bgit\s+(?:status|diff|log|show|blame|clone|fetch|pull|ls-files|remote|branch(?!\s+-[dD])|describe|rev-parse|stash\s+(?:list|show)|tag$)\b/.test(value)) return "read";
7897
7923
  if (lead === "gh" && /\bgh\s+(?:pr|issue|repo|run|release|workflow)\s+(?:view|list|status|checks|diff|download)\b/.test(value)) return "read";
7898
7924
  if (lead === "gh" && /\bgh\s+auth\s+status\b/.test(value) && !/--show-token|\s-t\b/.test(value)) return "read";
7899
- if (/^(?:gcloud\s+run\s+services\s+(?:describe|logs\s+read)|kubectl\s+(?:get|describe|logs|top|config\s+(?:current-context|get-contexts|view(?!\s+--raw)))|docker\s+(?:ps|images|logs|inspect|version|info)|terraform\s+(?:plan|validate|fmt|show|state\s+list|output)|aws\s+(?:s3\s+ls|sts\s+get-caller-identity)|az\s+account\s+show|npm\s+(?:ls|list|view|outdated|--version)|node\s+--version|python3?\s+--version)\b/.test(value)) return "read";
7925
+ if (lead === "gh" && /\bgh\s+api(?=\s|$)/.test(value)) {
7926
+ const method = /(?:\s-x|\s--method)[\s=]+["']?([a-z]+)/i.exec(segment);
7927
+ const hasField = /\s(?:-f|-F|--field|--raw-field|--input)(?:[\s=]|$)/.test(segment);
7928
+ if (method && method[1].toUpperCase() !== "GET" || hasField) return "write";
7929
+ const header = /\s(?:-H|--header)(?:[\s=]|$)/.exec(segment);
7930
+ if (header && /\$\(|`|\$env:|\(\s*[A-Za-z][\w-]*\s/.test(segment.slice(header.index))) return "write";
7931
+ return pipesIntoInterpreter ? "SHELL" : "read";
7932
+ }
7933
+ const gcloudVerb = gcloudVerbWord(value);
7934
+ if (gcloudVerb) {
7935
+ if (GCLOUD_READ_VERB_RE.test(gcloudVerb)) {
7936
+ if (pipesIntoInterpreter) return "SHELL";
7937
+ const redirect = value.match(/(?:^|[^>0-9])>{1,2}\s*([^\s&|;]+)/);
7938
+ if (redirect && !/^\/dev\/null$|^nul$|^\$null$/i.test(redirect[1])) return "write";
7939
+ return "read";
7940
+ }
7941
+ if (gcloudVerb === "delete") return "DELETE";
7942
+ if (GCLOUD_WRITE_VERB_RE.test(gcloudVerb)) return pipesIntoInterpreter ? "SHELL" : "write";
7943
+ }
7944
+ if (/^(?:kubectl\s+(?:get|describe|logs|top|config\s+(?:current-context|get-contexts|view(?!\s+--raw)))|docker\s+(?:ps|images|logs|inspect|version|info)|terraform\s+(?:plan|validate|fmt|show|state\s+list|output)|aws\s+(?:s3\s+ls|sts\s+get-caller-identity)|az\s+account\s+show|npm\s+(?:ls|list|view|outdated|--version)|node\s+--version|python3?\s+--version)\b/.test(value)) return "read";
7900
7945
  if (/\b(rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r|del\s+\/[fqs]|remove-item\b.*\s-recurse\b|rmdir\s+\/s)\b/.test(value)) {
7901
7946
  return isWorkspaceScopedRemoval(segment) ? "write" : "DELETE";
7902
7947
  }
@@ -7927,6 +7972,35 @@ function classifyShellWords(masked, raw, lead) {
7927
7972
  if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
7928
7973
  return "SHELL";
7929
7974
  }
7975
+ var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|versions)$/;
7976
+ var GCLOUD_WRITE_VERB_RE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|import|export|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
7977
+ function gcloudVerbWord(value) {
7978
+ const tokens = value.split("|")[0].trim().split(/\s+/);
7979
+ if (!/^gcloud(?:\.cmd|\.exe)?$/.test(tokens.shift() || "")) return null;
7980
+ let depth = 0;
7981
+ for (const token of tokens) {
7982
+ if (token.startsWith("-")) continue;
7983
+ if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || token === "delete") return token;
7984
+ if (!/^[a-z][a-z0-9-]*$/.test(token) || ++depth > 6) return null;
7985
+ }
7986
+ return null;
7987
+ }
7988
+ var CLOUD_CLI_LEAD_RE = /^(?:gcloud(?:\.cmd|\.exe)?|gsutil|aws|az)$/i;
7989
+ var STORAGE_TRANSFER_RE = /\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|gcloud(?:\.cmd|\.exe)?\s+storage\s+(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\b/i;
7990
+ var PIPES_INTO_INTERPRETER_RE = /\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b|\|\s*(?:pwsh|powershell|python3?|node|perl|ruby)\b/i;
7991
+ function storageUriIsDestination(segment, index = 0, all = []) {
7992
+ const lead = shellSegmentLead(segment);
7993
+ if (!CLOUD_CLI_LEAD_RE.test(lead)) return true;
7994
+ const words = maskQuotedSpans(segment).toLowerCase();
7995
+ if (PIPES_INTO_INTERPRETER_RE.test(words) || /\$\(|`|&\s*[$(]/.test(words) || PS_VALUE_EXECUTOR_RE.test(words)) return true;
7996
+ if (STORAGE_TRANSFER_RE.test(words)) return true;
7997
+ if (/^\s*(?:\$[\w:]+\s*=|[A-Za-z_]\w*=)/.test(segment) && all.slice(index + 1).some((later) => {
7998
+ const laterWords = maskQuotedSpans(later).toLowerCase();
7999
+ return PS_VALUE_EXECUTOR_RE.test(laterWords) || PIPES_INTO_INTERPRETER_RE.test(laterWords) || /(?:^|[\s;&|(])(?:sudo\s+)?(?:ba|z|da|k)?sh\s+-c\b|(?:^|[\s;&|(])(?:pwsh|powershell)(?:\.exe)?\s+-c(?:ommand)?\b|\beval\b|&\s*[$(]/.test(laterWords);
8000
+ })) return true;
8001
+ if (/^gcloud(?:\.cmd|\.exe)?\s+(?:--\S+\s+)*(?:alpha\s+|beta\s+)?[a-z-]+(?:\s+[a-z-]+){0,4}\s+export\b/.test(words.split("|")[0].trim())) return true;
8002
+ return false;
8003
+ }
7930
8004
  var SHELL_BLOCK_STRUCTURE_RE = /^(?:\s|[{}()=\-\d]|\+=|""|\$[\w:.]+|\b(?:try|catch|finally|if|then|else|elseif|elif|fi|do|done|esac|end|exit|return|break|continue)\b)*$/i;
7931
8005
  function classifyShellCommandOperation(command, depth = 0) {
7932
8006
  if (provenPowerShellReadSequence(command)) return "read";
@@ -8386,11 +8460,17 @@ function inferBundledToolContext(toolName, args) {
8386
8460
  context["destination.domain"] = parsed.hostname;
8387
8461
  context["url.risk"] = hostType;
8388
8462
  const requestHasSecret = recordHttpRequestFacts(context, args, detectedUrl);
8389
- if (inbound) {
8463
+ const storageResourceOnly = isCodeExecTool && /^(?:gs|s3|az):\/\//i.test(detectedUrl) && (() => {
8464
+ const segments = splitShellSegments(commandText);
8465
+ const bucket = (detectedUrl.match(/^[a-z0-9]+:\/\/[^/\s"'<>`]+/i) || [detectedUrl])[0].toLowerCase();
8466
+ const carriers = segments.map((segment, index) => [segment, index]).filter(([segment]) => segment.toLowerCase().includes(bucket));
8467
+ return carriers.length > 0 && carriers.every(([segment, index]) => !storageUriIsDestination(segment, index, segments));
8468
+ })();
8469
+ if (inbound || storageResourceOnly) {
8390
8470
  context["source.domain"] = parsed.hostname;
8391
8471
  context["source.type"] = hostType;
8392
8472
  }
8393
- if (!inbound || requestHasSecret) {
8473
+ if (!inbound && !storageResourceOnly || requestHasSecret) {
8394
8474
  context["destination.type"] = hostType;
8395
8475
  }
8396
8476
  } catch {
@@ -16,7 +16,10 @@ export interface NativeDialogView {
16
16
  headline: string;
17
17
  subtitle: string;
18
18
  kind?: string;
19
+ /** The action: the whole script when the explanation carried it, else the short "What" line. */
19
20
  action?: string;
21
+ /** Plain-words line per step of a shell script ("Exports Firestore data … to storage bucket …"). */
22
+ steps: string[];
20
23
  policy?: string;
21
24
  scope?: string;
22
25
  agent?: string;
@@ -12,6 +12,8 @@ function line(value, limit) {
12
12
  const head = Math.ceil((limit - 3) * 0.6);
13
13
  return `${text.slice(0, head)} … ${text.slice(-(limit - head - 3))}`;
14
14
  }
15
+ /** The full script is shown in a scrolling box; this only guards against a runaway payload. */
16
+ const MAX_DIALOG_SCRIPT = 6000;
15
17
  const CEILING_NOTE_RE = /\s*This policy is set to Block[.;]\s*(?:Because it is not marked "Hard block", your workstation requires your approval before continuing\.|on a developer workstation it asks you instead\.)?\s*(?:An admin can (?:review the matching rule and its scope in the console|mark it "Hard block" in the console to stop this without asking)\.)?/;
16
18
  const CEILING_NOTE_SHORT = 'set to Block; on your workstation it asks you first.';
17
19
  /** Parse the `Rule:` line the block explanation writes into the name the console lists and its scope. */
@@ -39,6 +41,20 @@ function promptFacts(reason) {
39
41
  const facts = { agent, ide: field('IDE') };
40
42
  if (rule)
41
43
  Object.assign(facts, ruleParts(rule));
44
+ const script = field('Script');
45
+ if (script)
46
+ facts.script = script.length > MAX_DIALOG_SCRIPT ? `${script.slice(0, MAX_DIALOG_SCRIPT - 1)}…` : script;
47
+ const doesAt = rows.findIndex(row => /^Does:\s*$/.test(row));
48
+ if (doesAt >= 0) {
49
+ const steps = [];
50
+ for (const row of rows.slice(doesAt + 1)) {
51
+ if (!row.startsWith('- '))
52
+ break;
53
+ steps.push(row.slice(2).trim());
54
+ }
55
+ if (steps.length)
56
+ facts.steps = steps;
57
+ }
42
58
  if (what) {
43
59
  // summarizeAction ends with the kind in parentheses: "… (shell command)".
44
60
  const kind = /^(.*)\s\(([a-z][a-z /-]{2,30})\)$/i.exec(what);
@@ -94,7 +110,9 @@ function nativeDialogView(reason, options = {}) {
94
110
  ? 'A security policy paused this action for an admin or manager.'
95
111
  : 'A security policy paused this action. Review the details and choose what to do next.',
96
112
  kind: f.kind,
97
- action: f.action ? line(f.action, 280) : undefined,
113
+ // The whole script when we have it (the box scrolls); the short line otherwise.
114
+ action: f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action ? line(f.action, 280) : undefined,
115
+ steps: (f.steps || []).slice(0, 14).map(step => line(step, 220)),
98
116
  policy: f.policy ? line(f.policy, 160) : undefined,
99
117
  scope: f.scope ? line(f.scope, 220) : undefined,
100
118
  agent: f.agent ? line(f.agent, 80) : undefined,
@@ -117,6 +135,7 @@ function nativeApprovalMessage(reason, options = {}) {
117
135
  f.policy ? `Policy: ${line(f.policy, 160)}` : '',
118
136
  f.scope ? ` ${line(f.scope, 200)}` : '',
119
137
  f.action ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
138
+ ...(f.steps || []).slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
120
139
  f.agent ? `Agent: ${line(f.agent, 80)}` : '',
121
140
  f.ide ? `IDE: ${line(f.ide, 40)}` : '',
122
141
  f.tool ? `Tool: ${line(f.tool, 230)}` : '',
@@ -19,7 +19,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.summarizeAction = summarizeAction;
20
20
  exports.buildBlockExplanation = buildBlockExplanation;
21
21
  exports.oneLineExplanation = oneLineExplanation;
22
+ const commandNarrative_1 = require("./commandNarrative");
22
23
  const MAX_WHAT = 160;
24
+ /** The full script travels to the approval dialog; anything longer is still cut, but far later. */
25
+ const MAX_SCRIPT = 6000;
26
+ /** Newlines flattened so the multi-line explanation grammar (one `Field:` per row) still parses. */
27
+ function scriptLine(command) {
28
+ const flat = command.replace(/\r?\n/g, ' ⏎ ').trim();
29
+ return flat.length > MAX_SCRIPT ? `${flat.slice(0, MAX_SCRIPT - 1)}…` : flat;
30
+ }
23
31
  function str(v) {
24
32
  return typeof v === 'string' && v.trim() ? v : undefined;
25
33
  }
@@ -229,9 +237,18 @@ function buildBlockExplanation(input) {
229
237
  if (input.note)
230
238
  why = `${why} ${input.note}`.trim();
231
239
  const headline = headlineFor(input.outcome, noun);
240
+ // Shell / terminal: say what each step of the script does, in plain words,
241
+ // and carry the whole script when the short "What" line had to cut it.
242
+ const command = (input.event === 'shell' || input.event === 'terminal')
243
+ ? (str(input.toolArgs?.command) || str(input.toolArgs?.cmd) || str(input.toolArgs?.commandLine) || str(input.toolArgs?.script))
244
+ : undefined;
245
+ const steps = command ? (0, commandNarrative_1.describeShellCommandLines)(command) : [];
246
+ const script = command && command.replace(/\s+/g, ' ').trim().length > MAX_WHAT - 1 ? scriptLine(command) : undefined;
232
247
  const userMessage = [
233
248
  headline,
234
249
  `What: ${what}`,
250
+ ...(steps.length ? ['Does:', ...steps.map(step => ` - ${step}`)] : []),
251
+ ...(script ? [`Script: ${script}`] : []),
235
252
  `Rule: ${rule}`,
236
253
  `Why: ${why}`,
237
254
  stepsTitle(input.outcome),
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What a shell script DOES, one plain-words line per step — for the approval
3
+ * dialog and the IDE message. A developer asked to approve
4
+ *
5
+ * $prefix="gs://b/prod-export"; gcloud firestore export $prefix --project acme-prod; gcloud firestore import $prefix --project acme-staging
6
+ *
7
+ * should read "Exports Firestore data from project acme-prod to bucket b ·
8
+ * Imports Firestore data into project acme-staging from bucket b", not a
9
+ * truncated command line.
10
+ *
11
+ * Fixed templates only, keyed on the program and its verb — no model, no
12
+ * scoring, nothing that could decide anything. This module never influences a
13
+ * verdict; it only words one. A program the grammar does not know is reported
14
+ * as exactly that ("Runs `foo bar`"), never guessed. Pure: no I/O.
15
+ */
16
+ export interface CommandStep {
17
+ /** The segment as written (trimmed). */
18
+ segment: string;
19
+ /** Plain-words description of what it does. */
20
+ text: string;
21
+ /** The grammar recognised the program and its verb. */
22
+ known: boolean;
23
+ }
24
+ /** Quote-aware split on `;`, `&&`, `||`, newlines. Pipes stay inside a step. */
25
+ export declare function splitCommandSteps(command: string): string[];
26
+ /** One plain-words line per step, in order. */
27
+ export declare function describeShellCommand(command: string): CommandStep[];
28
+ /** The step lines as the explanation prints them (`- …`). */
29
+ export declare function describeShellCommandLines(command: string): string[];