sim 2.0.0-preview.10.1 → 2.0.0-preview.11.1

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.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/index.js +662 -278
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2466,8 +2466,18 @@ function deleteProfile(profile) {
2466
2466
  writeIni(credentialsPath(), credentialsDoc, true);
2467
2467
  return { config, credentials };
2468
2468
  }
2469
- function normalizeEndpoint(endpoint) {
2470
- return endpoint.replace(/\/+$/, "");
2469
+ function normalizeEndpoint(endpoint, source) {
2470
+ const trimmed = endpoint.replace(/\/+$/, "");
2471
+ let parsed;
2472
+ try {
2473
+ parsed = new URL(trimmed);
2474
+ } catch {
2475
+ throw new ProfileConfigError(`Invalid endpoint "${endpoint}" from ${source}. Use an absolute URL, e.g. ${DEFAULT_ENDPOINT} or http://localhost:3000`);
2476
+ }
2477
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2478
+ throw new ProfileConfigError(`Unsupported endpoint scheme "${parsed.protocol.replace(/:$/, "")}" from ${source}. Use http or https, e.g. ${DEFAULT_ENDPOINT}`);
2479
+ }
2480
+ return trimmed;
2471
2481
  }
2472
2482
  function resolve(candidates, fallback, fallbackSource) {
2473
2483
  for (const [source, value] of candidates) {
@@ -2505,7 +2515,7 @@ function resolveProfile(overrides = {}) {
2505
2515
  }
2506
2516
  return {
2507
2517
  name,
2508
- endpoint: normalizeEndpoint(endpoint.value),
2518
+ endpoint: normalizeEndpoint(endpoint.value, endpoint.source),
2509
2519
  apiKey: apiKey.value,
2510
2520
  workspaceId: workspaceId.value,
2511
2521
  output: output.value,
@@ -2539,13 +2549,25 @@ function buildUrl(endpoint, path, query) {
2539
2549
  }
2540
2550
  return url.toString();
2541
2551
  }
2542
- function toApiError(status, raw) {
2552
+ var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
2553
+ var MARKUP_PREFIX = /^\s*<(?:!doctype|html|\?xml)/i;
2554
+ var WORKSPACE_KEY_REFUSAL = "WORKSPACE_KEY_OPERATION_NOT_PERMITTED";
2555
+ function toNonJsonError(url, status, contentType, raw) {
2556
+ const type = contentType?.split(";")[0]?.trim().toLowerCase();
2557
+ const isMarkup = type === "text/html" || type === "application/xhtml+xml" || MARKUP_PREFIX.test(raw);
2558
+ const kind = isMarkup ? "HTML" : type && type !== "application/json" ? type : "a non-JSON response";
2559
+ const text = raw.trim();
2560
+ const keepSnippet = !isMarkup && text.length > 0 && text.length <= 200;
2561
+ return new SimApiError(`${url} returned ${kind}, not JSON (HTTP ${status}) — check your endpoint.${keepSnippet ? ` Response: ${truncate(text, 200)}` : ""}`, status);
2562
+ }
2563
+ function toApiError(url, status, contentType, raw) {
2543
2564
  let parsed;
2544
2565
  try {
2545
2566
  parsed = JSON.parse(raw);
2546
2567
  } catch {
2547
- const text = raw.trim();
2548
- return new SimApiError(text ? truncate(text, 300) : `Request failed with status ${status}`, status);
2568
+ if (!raw.trim())
2569
+ return new SimApiError(`Request failed with status ${status}`, status);
2570
+ return toNonJsonError(url, status, contentType, raw);
2549
2571
  }
2550
2572
  const body = parsed;
2551
2573
  if (body.error && typeof body.error === "object") {
@@ -2561,8 +2583,33 @@ function toApiError(status, raw) {
2561
2583
  function truncate(value, max) {
2562
2584
  return value.length <= max ? value : `${value.slice(0, max)}…`;
2563
2585
  }
2586
+ function namesWorkspaceKeyRefusal(error) {
2587
+ if (error.code === WORKSPACE_KEY_REFUSAL)
2588
+ return true;
2589
+ const details = error.details;
2590
+ if (!details || typeof details !== "object")
2591
+ return false;
2592
+ return details.code === WORKSPACE_KEY_REFUSAL;
2593
+ }
2594
+ function isStrictPrefix(path, other) {
2595
+ if (path.length >= other.length)
2596
+ return false;
2597
+ return path.every((segment, index) => segment === other[index]);
2598
+ }
2599
+ function rejectsAKeyThatValidated(issue, other) {
2600
+ if (!issue.unrecognizedKeys || !isStrictPrefix(issue.path, other.path))
2601
+ return false;
2602
+ return issue.unrecognizedKeys.includes(other.path[issue.path.length]);
2603
+ }
2604
+ function dropUnionBranchNoise(issues) {
2605
+ if (issues.length < 2)
2606
+ return issues;
2607
+ const kept = issues.filter((issue) => !issues.some((other) => rejectsAKeyThatValidated(issue, other)));
2608
+ return kept.length > 0 ? kept : issues;
2609
+ }
2564
2610
  function formatApiErrorDetails(details) {
2565
- const issues = new Set;
2611
+ const issues = [];
2612
+ const seen = new Set;
2566
2613
  const visit = (value, parentPath = []) => {
2567
2614
  if (Array.isArray(value)) {
2568
2615
  value.forEach((item) => visit(item, parentPath));
@@ -2580,15 +2627,27 @@ function formatApiErrorDetails(details) {
2580
2627
  }
2581
2628
  if (typeof issue.message !== "string" || issue.message === "Invalid input")
2582
2629
  return;
2583
- issues.add(`${path.length > 0 ? path.join(".") : "request"}: ${issue.message}`);
2630
+ const line = `${path.join(".")}: ${issue.message}`;
2631
+ if (seen.has(line))
2632
+ return;
2633
+ seen.add(line);
2634
+ issues.push({
2635
+ path,
2636
+ message: issue.message,
2637
+ unrecognizedKeys: issue.code === "unrecognized_keys" && Array.isArray(issue.keys) ? issue.keys.map(String) : null
2638
+ });
2584
2639
  };
2585
2640
  visit(details);
2586
- if (issues.size === 0)
2641
+ if (issues.length === 0)
2587
2642
  return [` details: ${truncate(JSON.stringify(details), 1000)}`];
2588
- const visible = [...issues].slice(0, 8);
2589
- const lines = [" details:", ...visible.map((issue) => ` ${issue}`)];
2590
- if (issues.size > visible.length)
2591
- lines.push(` … ${issues.size - visible.length} more issues`);
2643
+ const kept = dropUnionBranchNoise(issues);
2644
+ const visible = kept.slice(0, 8);
2645
+ const lines = [
2646
+ " details:",
2647
+ ...visible.map((issue) => ` ${issue.path.length > 0 ? issue.path.join(".") : "request"}: ${issue.message}`)
2648
+ ];
2649
+ if (kept.length > visible.length)
2650
+ lines.push(` … ${kept.length - visible.length} more issues`);
2592
2651
  return lines;
2593
2652
  }
2594
2653
 
@@ -2614,6 +2673,20 @@ class SimClient {
2614
2673
  return workspaceId;
2615
2674
  }
2616
2675
  async requestRaw(path, options = {}) {
2676
+ return (await this.send(path, options)).response;
2677
+ }
2678
+ async request(path, options = {}) {
2679
+ const { response, url } = await this.send(path, options);
2680
+ const raw = await response.text();
2681
+ if (!raw)
2682
+ return;
2683
+ try {
2684
+ return JSON.parse(raw);
2685
+ } catch {
2686
+ throw toNonJsonError(url, response.status, response.headers.get("content-type"), raw);
2687
+ }
2688
+ }
2689
+ async send(path, options) {
2617
2690
  const apiKey = this.resolveApiKey(options.auth);
2618
2691
  const url = buildUrl(this.profile.endpoint, path, options.query);
2619
2692
  const hasBody = options.body !== undefined;
@@ -2628,7 +2701,8 @@ class SimClient {
2628
2701
  ...options.headers
2629
2702
  },
2630
2703
  body: hasBody ? JSON.stringify(options.body) : undefined,
2631
- signal: options.signal
2704
+ signal: options.signal,
2705
+ redirect: "manual"
2632
2706
  });
2633
2707
  } catch (cause) {
2634
2708
  if (options.signal?.aborted) {
@@ -2636,43 +2710,87 @@ class SimClient {
2636
2710
  }
2637
2711
  throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
2638
2712
  }
2713
+ if (REDIRECT_STATUSES.has(response.status))
2714
+ throw this.toRedirectError(url, path, response);
2639
2715
  if (!response.ok) {
2640
2716
  const raw = await response.text();
2641
- const error = toApiError(response.status, raw);
2717
+ const error = toApiError(url, response.status, response.headers.get("content-type"), raw);
2642
2718
  if (response.status === 401) {
2643
2719
  error.message = `${error.message} — run: sim login --profile ${this.profile.name}`;
2644
2720
  }
2721
+ if (namesWorkspaceKeyRefusal(error)) {
2722
+ error.message = `${error.message} — this operation needs a personal API key: sim login --profile ${this.profile.name}`;
2723
+ }
2645
2724
  throw error;
2646
2725
  }
2647
- return response;
2726
+ return { response, url };
2648
2727
  }
2649
- async request(path, options = {}) {
2650
- const response = await this.requestRaw(path, options);
2651
- const raw = await response.text();
2652
- if (!raw)
2653
- return;
2654
- return JSON.parse(raw);
2728
+ toRedirectError(url, path, response) {
2729
+ const location = response.headers.get("location")?.trim();
2730
+ let target = null;
2731
+ if (location) {
2732
+ try {
2733
+ target = new URL(location, url);
2734
+ } catch {
2735
+ target = null;
2736
+ }
2737
+ }
2738
+ if (!target) {
2739
+ return new SimApiError(`${url} answered HTTP ${response.status} with no usable redirect target. Check the endpoint for profile "${this.profile.name}".`, response.status);
2740
+ }
2741
+ const suggested = redirectEndpoint(this.profile.endpoint, path, target);
2742
+ if (!suggested) {
2743
+ return new SimApiError(`${url} redirected to ${target.href}. The CLI does not follow redirects, because a redirect can drop the request body and turn a write into a silent no-op.`, response.status);
2744
+ }
2745
+ return new SimApiError(`Endpoint redirected to ${suggested}. Run: sim configure --profile ${this.profile.name} --set-endpoint ${suggested}`, response.status);
2655
2746
  }
2656
2747
  }
2748
+ function redirectEndpoint(endpoint, requestPath, target) {
2749
+ const prefix = target.pathname.endsWith(requestPath) ? target.pathname.slice(0, target.pathname.length - requestPath.length) : "";
2750
+ const suggested = `${target.origin}${prefix}`.replace(/\/+$/, "");
2751
+ return suggested === endpoint.replace(/\/+$/, "") ? null : suggested;
2752
+ }
2753
+ function pageProgress() {
2754
+ let reported = false;
2755
+ return {
2756
+ advance: (fetched) => {
2757
+ if (!process.stderr.isTTY)
2758
+ return;
2759
+ reported = true;
2760
+ process.stderr.write(`\r${source_default.dim(`fetched ${fetched}…`)}\x1B[K`);
2761
+ },
2762
+ finish: () => {
2763
+ if (reported)
2764
+ process.stderr.write("\r\x1B[K");
2765
+ }
2766
+ };
2767
+ }
2657
2768
  async function requestAllPages(client, path, options) {
2658
2769
  const { query, pageSize, limit: requestedLimit, ...requestOptions } = options;
2659
2770
  const limit = requestedLimit ?? Number.POSITIVE_INFINITY;
2660
2771
  if (limit <= 0)
2661
2772
  return [];
2662
2773
  const items = [];
2774
+ const progress = pageProgress();
2663
2775
  let cursor = null;
2664
- do {
2665
- const page = await client.request(path, {
2666
- ...requestOptions,
2667
- query: {
2668
- ...query,
2669
- limit: Math.min(pageSize, limit - items.length),
2670
- cursor
2671
- }
2672
- });
2673
- items.push(...page.data);
2674
- cursor = page.nextCursor;
2675
- } while (cursor && items.length < limit);
2776
+ try {
2777
+ do {
2778
+ const page = await client.request(path, {
2779
+ ...requestOptions,
2780
+ query: {
2781
+ ...query,
2782
+ limit: Math.min(pageSize, limit - items.length),
2783
+ cursor
2784
+ }
2785
+ });
2786
+ items.push(...page.data);
2787
+ cursor = page.nextCursor;
2788
+ if (cursor && items.length < limit)
2789
+ progress.advance(items.length);
2790
+ } while (cursor && items.length < limit);
2791
+ } finally {
2792
+ progress.finish();
2793
+ }
2676
2794
  return items.slice(0, limit);
2677
2795
  }
2678
2796
  function resolvePath(template, params = {}) {
@@ -5894,7 +6012,7 @@ function duration(ms) {
5894
6012
  if (ms === null || ms === undefined)
5895
6013
  return EMPTY;
5896
6014
  if (ms < 1000)
5897
- return `${ms}ms`;
6015
+ return `${Math.round(ms)}ms`;
5898
6016
  if (ms < 60000)
5899
6017
  return `${(ms / 1000).toFixed(1)}s`;
5900
6018
  return `${Math.floor(ms / 60000)}m${Math.round(ms % 60000 / 1000)}s`;
@@ -5914,17 +6032,18 @@ function oneLine(value) {
5914
6032
  return value.replace(/\s*[\r\n\t]+\s*/g, " ");
5915
6033
  }
5916
6034
  var MAX_CELL_WIDTH = 60;
5917
- function clampCell(value) {
5918
- if (visibleWidth(value) <= MAX_CELL_WIDTH || value !== value.replace(ANSI_PATTERN, "")) {
6035
+ var MAX_RECORD_WIDTH = 160;
6036
+ function clamp(value, width) {
6037
+ if (visibleWidth(value) <= width || value !== value.replace(ANSI_PATTERN, "")) {
5919
6038
  return value;
5920
6039
  }
5921
- return `${value.slice(0, MAX_CELL_WIDTH - 1)}…`;
6040
+ return `${value.slice(0, width - 1)}…`;
5922
6041
  }
5923
6042
  function renderTable(rows, columns) {
5924
6043
  if (rows.length === 0)
5925
6044
  return source_default.dim("No results.");
5926
6045
  const headers = columns.map((column) => sanitize(column.header));
5927
- const cells = rows.map((row) => columns.map((column) => clampCell(oneLine(column.value(row)))));
6046
+ const cells = rows.map((row) => columns.map((column) => clamp(oneLine(column.value(row)), MAX_CELL_WIDTH)));
5928
6047
  const widths = columns.map((_column, index) => Math.max(visibleWidth(headers[index]), ...cells.map((line) => visibleWidth(line[index]))));
5929
6048
  const header = headers.map((label, index) => source_default.dim(pad(label.toUpperCase(), widths[index]))).join(" ").trimEnd();
5930
6049
  const body = cells.map((line) => line.map((cell, index) => pad(cell, widths[index])).join(" ").trimEnd());
@@ -5970,7 +6089,7 @@ function printRecord(format, fields, raw) {
5970
6089
  }
5971
6090
  const width = Math.max(...safeFields.map(([label]) => visibleWidth(label)));
5972
6091
  for (const [label, value] of safeFields) {
5973
- console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${oneLine(value)}`);
6092
+ console.log(`${source_default.dim(pad(`${label}:`, width + 1))} ${clamp(oneLine(value), MAX_RECORD_WIDTH)}`);
5974
6093
  }
5975
6094
  }
5976
6095
 
@@ -6036,6 +6155,24 @@ function buildApprovalUrl(endpoint, auth, scope, workspaceId) {
6036
6155
  url.searchParams.set("workspace", workspaceId);
6037
6156
  return url.toString();
6038
6157
  }
6158
+ var POLL_PATH = "/api/cli/auth/poll";
6159
+ function toRedirectError(endpoint, response) {
6160
+ const location = response.headers.get("location")?.trim();
6161
+ let target = null;
6162
+ if (location) {
6163
+ try {
6164
+ target = new URL(location, endpoint);
6165
+ } catch {
6166
+ target = null;
6167
+ }
6168
+ }
6169
+ if (!target) {
6170
+ return new SimApiError(`${endpoint} answered the login poll with HTTP ${response.status} and no usable redirect target. Check the endpoint.`, response.status);
6171
+ }
6172
+ const refusal = `${endpoint} redirected the login poll to ${target.href}. The CLI does not follow redirects, because a redirect drops the request body and would carry the login secret to another origin.`;
6173
+ const suggested = redirectEndpoint(endpoint, POLL_PATH, target);
6174
+ return new SimApiError(suggested ? `${refusal} Re-run with --endpoint ${suggested}, or run: sim configure --set-endpoint ${suggested}` : refusal, response.status);
6175
+ }
6039
6176
  async function pollForKey(endpoint, auth, signal) {
6040
6177
  const deadline = Date.now() + POLL_TIMEOUT_MS;
6041
6178
  while (Date.now() < deadline) {
@@ -6043,16 +6180,19 @@ async function pollForKey(endpoint, auth, signal) {
6043
6180
  throw new SimApiError("Login cancelled.", 0);
6044
6181
  let response = null;
6045
6182
  try {
6046
- response = await fetch(new URL("/api/cli/auth/poll", endpoint), {
6183
+ response = await fetch(new URL(POLL_PATH, endpoint), {
6047
6184
  method: "POST",
6048
6185
  headers: { "content-type": "application/json", accept: "application/json" },
6049
6186
  body: JSON.stringify({ request: auth.request, verifier: auth.pollSecret }),
6050
- signal
6187
+ signal,
6188
+ redirect: "manual"
6051
6189
  });
6052
6190
  } catch {
6053
6191
  response = null;
6054
6192
  }
6055
6193
  if (response) {
6194
+ if (REDIRECT_STATUSES.has(response.status))
6195
+ throw toRedirectError(endpoint, response);
6056
6196
  const raw = await response.text();
6057
6197
  if (!response.ok) {
6058
6198
  if (!RETRYABLE_POLL_STATUSES.has(response.status)) {
@@ -6106,193 +6246,6 @@ function clientFrom(command) {
6106
6246
  return { client: new SimClient(profile), profile };
6107
6247
  }
6108
6248
 
6109
- // src/commands/auth.ts
6110
- function openBrowser(url) {
6111
- const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
6112
- try {
6113
- const child = spawn(command, args, { stdio: "ignore", detached: true });
6114
- child.on("error", () => {});
6115
- child.unref();
6116
- } catch {}
6117
- }
6118
- function presentAuthentication(source) {
6119
- switch (source) {
6120
- case "flag":
6121
- return { authenticated: true, source: "flag" };
6122
- case "env":
6123
- return { authenticated: true, source: "env" };
6124
- case "credentials":
6125
- return { authenticated: true, source: "credentials" };
6126
- case "unset":
6127
- return { authenticated: false, source: "unset" };
6128
- case "config":
6129
- case "default":
6130
- throw new SimApiError(`Unexpected API key source "${source}".`, 0);
6131
- }
6132
- }
6133
- async function confirmProfileOverwrite(profileName) {
6134
- if (!process.stdin.isTTY) {
6135
- throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
6136
- }
6137
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
6138
- try {
6139
- const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
6140
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
6141
- } finally {
6142
- prompt.close();
6143
- }
6144
- }
6145
- function loginCommand() {
6146
- return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
6147
- const profile = profileFrom(command);
6148
- if (options.scope !== "platform" && options.scope !== "copilot") {
6149
- throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
6150
- }
6151
- const scope = options.scope;
6152
- if (readCredentialsProfile(profile.name).api_key && !options.yes) {
6153
- const confirmed = await confirmProfileOverwrite(profile.name);
6154
- if (!confirmed) {
6155
- console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
6156
- return;
6157
- }
6158
- }
6159
- const auth = createAuthRequest();
6160
- const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
6161
- console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
6162
- console.log(`
6163
- Pairing code: ${source_default.bold(auth.pairing)}`);
6164
- console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
6165
- `));
6166
- console.log(url);
6167
- if (options.browser)
6168
- openBrowser(url);
6169
- console.log(source_default.dim(`
6170
- Waiting for approval…`));
6171
- const key = await pollForKey(profile.endpoint, auth);
6172
- if (key.scope !== scope) {
6173
- throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
6174
- }
6175
- writeCredentialsProfile(profile.name, key.apiKey);
6176
- const settings = {
6177
- endpoint: profile.endpoint,
6178
- workspace: key.workspaceId ?? null
6179
- };
6180
- writeConfigProfile(profile.name, settings);
6181
- console.log(source_default.green(`
6182
- ✓ Logged in. Key stored in ${credentialsPath()}`));
6183
- if (key.workspaceBound && key.workspaceId) {
6184
- console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
6185
- } else if (key.workspaceId) {
6186
- console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
6187
- } else {
6188
- console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
6189
- }
6190
- });
6191
- }
6192
- function logoutCommand() {
6193
- return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
6194
- const profile = profileFrom(command);
6195
- if (options.all) {
6196
- const removed = deleteProfile(profile.name);
6197
- if (!removed.config && !removed.credentials) {
6198
- console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
6199
- return;
6200
- }
6201
- console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
6202
- return;
6203
- }
6204
- if (!readCredentialsProfile(profile.name).api_key) {
6205
- console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
6206
- return;
6207
- }
6208
- writeCredentialsProfile(profile.name, null);
6209
- console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
6210
- console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
6211
- });
6212
- }
6213
- function whoamiCommand() {
6214
- return new Command("whoami").description("Show the resolved profile and where each setting came from").action((_options, command) => {
6215
- const profile = profileFrom(command);
6216
- const { sources } = profile;
6217
- const authentication = presentAuthentication(sources.apiKey);
6218
- const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
6219
- printRecord(profile.output, [
6220
- ["Profile", profile.name],
6221
- ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
6222
- [
6223
- "API key",
6224
- authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
6225
- ],
6226
- ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
6227
- ["Output", annotate(profile.output, sources.output)]
6228
- ], {
6229
- profile: profile.name,
6230
- endpoint: profile.endpoint,
6231
- workspaceId: profile.workspaceId,
6232
- output: profile.output,
6233
- authenticated: authentication.authenticated,
6234
- sources: {
6235
- endpoint: sources.endpoint,
6236
- authentication: authentication.source,
6237
- workspaceId: sources.workspaceId,
6238
- output: sources.output
6239
- }
6240
- });
6241
- });
6242
- }
6243
- function profilesCommand() {
6244
- return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
6245
- const profiles = listProfiles();
6246
- if (profiles.length === 0) {
6247
- console.log(source_default.dim("No profiles yet. Run: sim login"));
6248
- return;
6249
- }
6250
- const active = profileFrom(command).name;
6251
- for (const name of profiles) {
6252
- const marker = name === active ? source_default.green("*") : " ";
6253
- const hasKey = Boolean(readCredentialsProfile(name).api_key);
6254
- console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
6255
- }
6256
- });
6257
- }
6258
-
6259
- // src/commands/configure.ts
6260
- function configureCommand() {
6261
- return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
6262
- const profile = profileFrom(command);
6263
- const updates = {};
6264
- if (options.setEndpoint)
6265
- updates.endpoint = options.setEndpoint.replace(/\/+$/, "");
6266
- if (options.setWorkspace)
6267
- updates.workspace = options.setWorkspace;
6268
- if (options.setOutput) {
6269
- if (!OUTPUT_FORMATS.includes(options.setOutput)) {
6270
- throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
6271
- }
6272
- updates.output = options.setOutput;
6273
- }
6274
- for (const key of options.unset ?? []) {
6275
- if (!["endpoint", "workspace", "output"].includes(key)) {
6276
- throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
6277
- }
6278
- updates[key] = null;
6279
- }
6280
- if (Object.keys(updates).length === 0) {
6281
- const current = readConfigProfile(profile.name);
6282
- if (Object.keys(current).length === 0) {
6283
- console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
6284
- return;
6285
- }
6286
- for (const [key, value] of Object.entries(current)) {
6287
- console.log(`${source_default.dim(`${key}:`)} ${value}`);
6288
- }
6289
- return;
6290
- }
6291
- writeConfigProfile(profile.name, updates);
6292
- console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
6293
- });
6294
- }
6295
-
6296
6249
  // src/generated/v2-api.ts
6297
6250
  var V2_OPERATIONS = {
6298
6251
  abortFileUpload: {
@@ -9380,6 +9333,261 @@ var V2_OPERATIONS = {
9380
9333
  }
9381
9334
  };
9382
9335
 
9336
+ // src/commands/auth.ts
9337
+ function openBrowser(url) {
9338
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : [process.platform === "darwin" ? "open" : "xdg-open", [url]];
9339
+ try {
9340
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
9341
+ child.on("error", () => {});
9342
+ child.unref();
9343
+ } catch {}
9344
+ }
9345
+ function presentAuthentication(source) {
9346
+ switch (source) {
9347
+ case "flag":
9348
+ return { authenticated: true, source: "flag" };
9349
+ case "env":
9350
+ return { authenticated: true, source: "env" };
9351
+ case "credentials":
9352
+ return { authenticated: true, source: "credentials" };
9353
+ case "unset":
9354
+ return { authenticated: false, source: "unset" };
9355
+ case "config":
9356
+ case "default":
9357
+ throw new SimApiError(`Unexpected API key source "${source}".`, 0);
9358
+ }
9359
+ }
9360
+ async function confirmProfileOverwrite(profileName) {
9361
+ if (!process.stdin.isTTY) {
9362
+ throw new SimApiError(`Profile "${profileName}" already exists. Re-run with --yes to overwrite it.`, 0);
9363
+ }
9364
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
9365
+ try {
9366
+ const answer = await prompt.question(`Profile "${profileName}" already exists. Replace its API key and login defaults? (y/N) `);
9367
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
9368
+ } finally {
9369
+ prompt.close();
9370
+ }
9371
+ }
9372
+ function loginCommand() {
9373
+ return new Command("login").description("Authorize this terminal and store an API key for the profile").option("--scope <scope>", "Key space to mint from: platform or copilot", "platform").option("--no-browser", "Print the URL instead of opening a browser").option("-y, --yes", "Overwrite an existing profile without prompting").action(async (options, command) => {
9374
+ const profile = profileFrom(command);
9375
+ if (options.scope !== "platform" && options.scope !== "copilot") {
9376
+ throw new SimApiError(`Unknown scope "${options.scope}". Use platform or copilot.`, 0);
9377
+ }
9378
+ const scope = options.scope;
9379
+ if (readCredentialsProfile(profile.name).api_key && !options.yes) {
9380
+ const confirmed = await confirmProfileOverwrite(profile.name);
9381
+ if (!confirmed) {
9382
+ console.log(source_default.dim("Login cancelled; the existing profile was not changed."));
9383
+ return;
9384
+ }
9385
+ }
9386
+ const auth = createAuthRequest();
9387
+ const url = buildApprovalUrl(profile.endpoint, auth, scope, profile.workspaceId ?? undefined);
9388
+ console.log(`Signing in to ${source_default.bold(profile.endpoint)} as profile ${source_default.bold(profile.name)}`);
9389
+ console.log(`
9390
+ Pairing code: ${source_default.bold(auth.pairing)}`);
9391
+ console.log(source_default.dim(`Confirm this code matches what the browser shows before approving.
9392
+ `));
9393
+ console.log(url);
9394
+ if (options.browser)
9395
+ openBrowser(url);
9396
+ console.log(source_default.dim(`
9397
+ Waiting for approval…`));
9398
+ const key = await pollForKey(profile.endpoint, auth);
9399
+ if (key.scope !== scope) {
9400
+ throw new SimApiError(`Server issued a ${key.scope} key but this profile needs a ${scope} key. Update the Sim deployment, or run: sim login --scope ${key.scope}`, 0);
9401
+ }
9402
+ writeCredentialsProfile(profile.name, key.apiKey);
9403
+ const settings = {
9404
+ endpoint: profile.endpoint,
9405
+ workspace: key.workspaceId ?? null
9406
+ };
9407
+ writeConfigProfile(profile.name, settings);
9408
+ console.log(source_default.green(`
9409
+ ✓ Logged in. Key stored in ${credentialsPath()}`));
9410
+ if (key.workspaceBound && key.workspaceId) {
9411
+ console.log(source_default.dim(` Workspace-scoped key — it can only reach ${key.workspaceId}.`));
9412
+ } else if (key.workspaceId) {
9413
+ console.log(source_default.dim(` Personal key, defaulting to ${key.workspaceId}. Override per command with --workspace.`));
9414
+ } else {
9415
+ console.log(source_default.dim(" Personal key with no default workspace. Set one with: sim configure --set-workspace <id>"));
9416
+ }
9417
+ });
9418
+ }
9419
+ function logoutCommand() {
9420
+ return new Command("logout").description("Remove the profile's stored API key").option("--all", "Remove the profile entirely, including its settings").action((options, command) => {
9421
+ const profile = profileFrom(command);
9422
+ if (options.all) {
9423
+ const removed = deleteProfile(profile.name);
9424
+ if (!removed.config && !removed.credentials) {
9425
+ console.log(source_default.dim(`Nothing stored for profile "${profile.name}".`));
9426
+ return;
9427
+ }
9428
+ console.log(source_default.green(`✓ Removed profile "${profile.name}".`));
9429
+ return;
9430
+ }
9431
+ if (!readCredentialsProfile(profile.name).api_key) {
9432
+ console.log(source_default.dim(`No stored key for profile "${profile.name}".`));
9433
+ return;
9434
+ }
9435
+ writeCredentialsProfile(profile.name, null);
9436
+ console.log(source_default.green(`✓ Removed the stored key for profile "${profile.name}".`));
9437
+ console.log(source_default.dim(" The key itself is still active — revoke it in Settings → API keys."));
9438
+ });
9439
+ }
9440
+ var CREDENTIAL_VERDICT_STATUSES = new Set([401, 403, 404]);
9441
+ var WHOAMI_EXIT_CODES = {
9442
+ verified: 0,
9443
+ disabled: 0,
9444
+ unauthenticated: 1,
9445
+ rejected: 1,
9446
+ unreachable: 2,
9447
+ "no-workspace": 2
9448
+ };
9449
+ async function verifyProfile(client, profile) {
9450
+ if (!profile.apiKey) {
9451
+ return {
9452
+ status: "unauthenticated",
9453
+ workspace: null,
9454
+ detail: `no API key — run: sim login --profile ${profile.name}`
9455
+ };
9456
+ }
9457
+ if (!profile.workspaceId) {
9458
+ return {
9459
+ status: "no-workspace",
9460
+ workspace: null,
9461
+ detail: `no workspace to check against — run: sim configure --profile ${profile.name} --set-workspace <id>`
9462
+ };
9463
+ }
9464
+ const operation = V2_OPERATIONS.getWorkspace;
9465
+ try {
9466
+ const response = await client.request(resolvePath(operation.path, { workspaceId: profile.workspaceId }), { method: operation.method });
9467
+ const { id, name, memberCount } = response.data;
9468
+ return { status: "verified", workspace: { id, name, memberCount }, detail: null };
9469
+ } catch (error) {
9470
+ if (!(error instanceof SimApiError))
9471
+ throw error;
9472
+ return {
9473
+ status: CREDENTIAL_VERDICT_STATUSES.has(error.status) ? "rejected" : "unreachable",
9474
+ workspace: null,
9475
+ detail: error.message
9476
+ };
9477
+ }
9478
+ }
9479
+ function presentVerification(verification) {
9480
+ if (verification.status === "verified") {
9481
+ const { name, memberCount } = verification.workspace;
9482
+ const members = `${memberCount} ${memberCount === 1 ? "member" : "members"}`;
9483
+ return `${source_default.green("✓")} ${safeOneLine(name)} · ${members}`;
9484
+ }
9485
+ const detail = safeOneLine(verification.detail);
9486
+ switch (verification.status) {
9487
+ case "rejected":
9488
+ return `${source_default.red("✗")} ${detail}`;
9489
+ case "unauthenticated":
9490
+ return source_default.yellow(`not logged in — ${detail}`);
9491
+ case "disabled":
9492
+ return source_default.dim(detail);
9493
+ default:
9494
+ return source_default.yellow(`could not check — ${detail}`);
9495
+ }
9496
+ }
9497
+ function whoamiCommand() {
9498
+ return new Command("whoami").description("Show the resolved profile, where each setting came from, and whether it works").option("--no-verify", "Skip the API check and only print the resolved settings").action(async (options, command) => {
9499
+ const { client, profile } = clientFrom(command);
9500
+ const { sources } = profile;
9501
+ const authentication = presentAuthentication(sources.apiKey);
9502
+ const verification = options.verify ? await verifyProfile(client, profile) : { status: "disabled", workspace: null, detail: "not checked (--no-verify)" };
9503
+ const annotate = (value, source) => source === "unset" ? source_default.dim("not set") : `${value} ${source_default.dim(`(${source})`)}`;
9504
+ printRecord(profile.output, [
9505
+ ["Profile", profile.name],
9506
+ ["Endpoint", annotate(profile.endpoint, sources.endpoint)],
9507
+ [
9508
+ "API key",
9509
+ authentication.authenticated ? annotate("configured", authentication.source) : source_default.yellow("not logged in")
9510
+ ],
9511
+ ["Workspace", annotate(profile.workspaceId ?? "", sources.workspaceId)],
9512
+ ["Output", annotate(profile.output, sources.output)],
9513
+ ["Verified", presentVerification(verification)]
9514
+ ], {
9515
+ profile: profile.name,
9516
+ endpoint: profile.endpoint,
9517
+ workspaceId: profile.workspaceId,
9518
+ output: profile.output,
9519
+ authenticated: authentication.authenticated,
9520
+ sources: {
9521
+ endpoint: sources.endpoint,
9522
+ authentication: authentication.source,
9523
+ workspaceId: sources.workspaceId,
9524
+ output: sources.output
9525
+ },
9526
+ verification: {
9527
+ status: verification.status,
9528
+ workspace: verification.workspace,
9529
+ detail: verification.detail
9530
+ }
9531
+ });
9532
+ const exitCode = WHOAMI_EXIT_CODES[verification.status];
9533
+ if (exitCode !== 0)
9534
+ process.exitCode = exitCode;
9535
+ });
9536
+ }
9537
+ function profilesCommand() {
9538
+ return new Command("profiles").alias("profile").description("List the profiles defined in the config and credentials files").action((_options, command) => {
9539
+ const profiles = listProfiles();
9540
+ if (profiles.length === 0) {
9541
+ console.log(source_default.dim("No profiles yet. Run: sim login"));
9542
+ return;
9543
+ }
9544
+ const active = profileFrom(command).name;
9545
+ for (const name of profiles) {
9546
+ const marker = name === active ? source_default.green("*") : " ";
9547
+ const hasKey = Boolean(readCredentialsProfile(name).api_key);
9548
+ console.log(`${marker} ${name}${hasKey ? "" : source_default.dim(" (no key)")}`);
9549
+ }
9550
+ });
9551
+ }
9552
+
9553
+ // src/commands/configure.ts
9554
+ function configureCommand() {
9555
+ return new Command("configure").description("Set a profile's endpoint, default workspace, or output format").option("--set-endpoint <url>", "Sim deployment to talk to").option("--set-workspace <id>", "Default workspace for workspace-scoped commands").option("--set-output <format>", `Default output format (${OUTPUT_FORMATS.join(" | ")})`).option("--unset <key...>", "Remove settings (endpoint, workspace, output)").action((options, command) => {
9556
+ const profile = profileFrom(command);
9557
+ const updates = {};
9558
+ if (options.setEndpoint) {
9559
+ updates.endpoint = normalizeEndpoint(options.setEndpoint, "--set-endpoint");
9560
+ }
9561
+ if (options.setWorkspace)
9562
+ updates.workspace = options.setWorkspace;
9563
+ if (options.setOutput) {
9564
+ if (!OUTPUT_FORMATS.includes(options.setOutput)) {
9565
+ throw new SimApiError(`Unknown output format "${options.setOutput}". Use one of: ${OUTPUT_FORMATS.join(", ")}`, 0);
9566
+ }
9567
+ updates.output = options.setOutput;
9568
+ }
9569
+ for (const key of options.unset ?? []) {
9570
+ if (!["endpoint", "workspace", "output"].includes(key)) {
9571
+ throw new SimApiError(`Cannot unset "${key}". Use endpoint, workspace, or output.`, 0);
9572
+ }
9573
+ updates[key] = null;
9574
+ }
9575
+ if (Object.keys(updates).length === 0) {
9576
+ const current = readConfigProfile(profile.name);
9577
+ if (Object.keys(current).length === 0) {
9578
+ console.log(source_default.dim(`No settings stored for profile "${profile.name}".`));
9579
+ return;
9580
+ }
9581
+ for (const [key, value] of Object.entries(current)) {
9582
+ console.log(`${source_default.dim(`${key}:`)} ${value}`);
9583
+ }
9584
+ return;
9585
+ }
9586
+ writeConfigProfile(profile.name, updates);
9587
+ console.log(source_default.green(`✓ Updated profile "${profile.name}" in ${configPath()}`));
9588
+ });
9589
+ }
9590
+
9383
9591
  // src/runtime/request.ts
9384
9592
  import { existsSync as existsSync2, readFileSync as readFileSync2, readSync } from "node:fs";
9385
9593
 
@@ -9389,7 +9597,8 @@ var TABLE_FILTER_HELP = 'Predicate: {"all":[{"field":"status","op":"eq","value":
9389
9597
  var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"desc"}] (direction: asc or desc)';
9390
9598
  var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
9391
9599
  var FOLDER_PATH_INPUT = {
9392
- describe: "Folder path; the leading / is optional"
9600
+ describe: "Folder path as shown in the app; the leading / is optional",
9601
+ folderPath: true
9393
9602
  };
9394
9603
  var FOLDER_PATH_FLAG = {
9395
9604
  ...FOLDER_PATH_INPUT,
@@ -9399,7 +9608,7 @@ var FOLDER_DELETE_FLAGS = {
9399
9608
  path: FOLDER_PATH_INPUT,
9400
9609
  recursive: { boolean: true, describe: "Delete the folder and its descendants" }
9401
9610
  };
9402
- var KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS = { id: "knowledgeBaseId" };
9611
+ var KNOWLEDGE_BASE_PATH_ARGUMENT = { id: "knowledgeBaseId" };
9403
9612
  var WORKFLOW_RUN_SCOPE = {
9404
9613
  id: {
9405
9614
  name: "workflow",
@@ -9407,10 +9616,11 @@ var WORKFLOW_RUN_SCOPE = {
9407
9616
  describe: "Workflow ID"
9408
9617
  }
9409
9618
  };
9619
+ var FOLDER_COLUMN = { header: "folder", path: "folderPath", format: "folder-path" };
9410
9620
  var FOLDER_LIST_COLUMNS = [
9411
- { header: "path" },
9621
+ { header: "path", format: "folder-path" },
9412
9622
  { header: "name" },
9413
- { header: "parent", path: "parentPath" },
9623
+ { header: "parent", path: "parentPath", format: "folder-path" },
9414
9624
  { header: "updated", path: "updatedAt", format: "timestamp" }
9415
9625
  ];
9416
9626
  function moveResource(command, resource) {
@@ -9480,7 +9690,7 @@ var CLI_CONTRACT = {
9480
9690
  bulkUpdateKnowledgeDocuments: {
9481
9691
  command: "knowledge documents batch-update",
9482
9692
  describe: "Enable or disable every matching document",
9483
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9693
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9484
9694
  flags: {
9485
9695
  documentIds: { name: "document", list: true },
9486
9696
  selectAll: { boolean: true, describe: "Apply to every document in the knowledge base" }
@@ -9490,6 +9700,11 @@ var CLI_CONTRACT = {
9490
9700
  command: "workflows undeploy",
9491
9701
  describe: "Take a workflow out of deployment"
9492
9702
  },
9703
+ getWorkflowDeployment: {
9704
+ command: "workflows deployment status",
9705
+ renamedFrom: ["workflows deployment list"],
9706
+ describe: "Show a workflow’s current deployment"
9707
+ },
9493
9708
  setSecret: { hidden: true },
9494
9709
  deleteTable: { confirm: "This deletes the table and all of its rows." },
9495
9710
  deleteTableRow: { confirm: "This deletes the row." },
@@ -9499,7 +9714,7 @@ var CLI_CONTRACT = {
9499
9714
  },
9500
9715
  deleteKnowledgeBase: { confirm: "This deletes the knowledge base and every document in it." },
9501
9716
  deleteKnowledgeDocument: {
9502
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9717
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9503
9718
  confirm: "This deletes the document and its embeddings."
9504
9719
  },
9505
9720
  deleteFile: { confirm: "This archives the file." },
@@ -9529,7 +9744,10 @@ var CLI_CONTRACT = {
9529
9744
  workflowIds: { name: "workflow", list: true },
9530
9745
  folderPaths: { ...FOLDER_PATH_FLAG, list: true },
9531
9746
  triggers: { name: "trigger", list: true },
9532
- details: { describe: "Response detail level" },
9747
+ details: {
9748
+ requestDefault: "full",
9749
+ describe: "Response detail level; full is requested by default to name each run’s workflow"
9750
+ },
9533
9751
  includeTraceSpans: {
9534
9752
  boolean: true,
9535
9753
  describe: "Include trace spans in JSON or YAML output (implies full detail)"
@@ -9656,7 +9874,7 @@ var CLI_CONTRACT = {
9656
9874
  columns: [
9657
9875
  { header: "id" },
9658
9876
  { header: "name" },
9659
- { header: "folder", path: "folderPath" },
9877
+ FOLDER_COLUMN,
9660
9878
  { header: "rows", path: "rowCount" },
9661
9879
  { header: "updated", path: "updatedAt", format: "timestamp" }
9662
9880
  ]
@@ -9666,7 +9884,7 @@ var CLI_CONTRACT = {
9666
9884
  columns: [
9667
9885
  { header: "id" },
9668
9886
  { header: "name" },
9669
- { header: "folder", path: "folderPath" },
9887
+ FOLDER_COLUMN,
9670
9888
  { header: "deployed", path: "isDeployed", format: "bool" },
9671
9889
  { header: "runs", path: "runCount" },
9672
9890
  { header: "last run", path: "lastRunAt", format: "timestamp" }
@@ -9677,7 +9895,7 @@ var CLI_CONTRACT = {
9677
9895
  columns: [
9678
9896
  { header: "id" },
9679
9897
  { header: "name" },
9680
- { header: "folder", path: "folderPath" },
9898
+ FOLDER_COLUMN,
9681
9899
  { header: "size", format: "bytes" },
9682
9900
  { header: "type" },
9683
9901
  { header: "uploaded by", path: "uploadedByEmail" },
@@ -9690,15 +9908,17 @@ var CLI_CONTRACT = {
9690
9908
  columns: [
9691
9909
  { header: "id" },
9692
9910
  { header: "name" },
9693
- { header: "folder", path: "folderPath" },
9911
+ FOLDER_COLUMN,
9694
9912
  { header: "docs", path: "docCount" },
9695
9913
  { header: "tokens", path: "tokenCount" },
9696
9914
  { header: "model", path: "embeddingModel" }
9697
9915
  ]
9698
9916
  },
9699
- getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS },
9917
+ getKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
9918
+ updateKnowledgeDocument: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
9919
+ listKnowledgeTags: { pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT },
9700
9920
  listKnowledgeDocuments: {
9701
- pathArgumentNames: KNOWLEDGE_DOCUMENT_PATH_ARGUMENTS,
9921
+ pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT,
9702
9922
  columns: [
9703
9923
  { header: "id" },
9704
9924
  { header: "filename" },
@@ -9813,7 +10033,7 @@ var CLI_CONTRACT = {
9813
10033
  { header: "name" },
9814
10034
  { header: "size", format: "bytes" },
9815
10035
  { header: "type" },
9816
- { header: "folder", path: "folderPath" },
10036
+ FOLDER_COLUMN,
9817
10037
  { header: "uploaded by", path: "uploadedByEmail" },
9818
10038
  { header: "uploaded", path: "uploadedAt", format: "timestamp" },
9819
10039
  { header: "updated", path: "updatedAt", format: "timestamp" },
@@ -9840,6 +10060,11 @@ var CLI_CONTRACT = {
9840
10060
  command: "files rename",
9841
10061
  describe: "Rename a file"
9842
10062
  },
10063
+ restoreFile: {
10064
+ command: "files restore",
10065
+ renamedFrom: ["files restore create"],
10066
+ describe: "Restore an archived file"
10067
+ },
9843
10068
  updateFileContent: {
9844
10069
  command: "files set-content",
9845
10070
  describe: "Replace a file’s contents",
@@ -9992,13 +10217,26 @@ var CLI_CONTRACT = {
9992
10217
  command: "tables rows find",
9993
10218
  describe: "Find rows matching a predicate",
9994
10219
  flags: {
9995
- q: { describe: "Value to find" },
10220
+ q: { name: "query", renamedFrom: ["q"], describe: "Value to find" },
9996
10221
  predicate: { name: "filter", json: true, describe: TABLE_FILTER_HELP },
9997
10222
  sort: { json: true, describe: TABLE_SORT_HELP }
9998
10223
  },
9999
10224
  itemsPath: "matches",
10000
10225
  columns: [{ header: "ordinal" }, { header: "row", path: "rowId" }, { header: "column" }]
10001
10226
  },
10227
+ queryRowsCount: {
10228
+ command: "tables rows count",
10229
+ renamedFrom: ["tables count create"],
10230
+ describe: "Count rows matching a filter",
10231
+ flags: {
10232
+ predicate: {
10233
+ name: "filter",
10234
+ renamedFrom: ["predicate"],
10235
+ json: true,
10236
+ describe: TABLE_FILTER_HELP
10237
+ }
10238
+ }
10239
+ },
10002
10240
  runTableColumn: {
10003
10241
  command: "tables columns run",
10004
10242
  describe: "Run a column’s workflow",
@@ -10251,6 +10489,26 @@ function readListValues(raw, flagName) {
10251
10489
  return trimmed;
10252
10490
  });
10253
10491
  }
10492
+ var PERCENT_ESCAPE = /%[0-9A-Fa-f]{2}/;
10493
+ var SUB_DELIMITERS = /[!'()*]/g;
10494
+ function encodeFolderPathSegment(name) {
10495
+ if (name === ".")
10496
+ return "%2E";
10497
+ if (name === "..")
10498
+ return "%2E%2E";
10499
+ return encodeURIComponent(name).replace(SUB_DELIMITERS, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
10500
+ }
10501
+ function encodeFolderPath(value) {
10502
+ return value.split("/").map((segment) => {
10503
+ if (!PERCENT_ESCAPE.test(segment))
10504
+ return encodeFolderPathSegment(segment);
10505
+ try {
10506
+ return encodeFolderPathSegment(decodeURIComponent(segment));
10507
+ } catch {
10508
+ return encodeFolderPathSegment(segment);
10509
+ }
10510
+ }).join("/");
10511
+ }
10254
10512
  function pathHint(raw) {
10255
10513
  if (raw.startsWith("@") || /^\s*[[{"\-\d]|^\s*(true|false|null)/.test(raw))
10256
10514
  return "";
@@ -10260,7 +10518,7 @@ function coerce(raw, field, flag, flagName) {
10260
10518
  if (raw === undefined)
10261
10519
  return;
10262
10520
  if (flag.list) {
10263
- const values = readListValues(raw, flagName);
10521
+ const values = readListValues(raw, flagName).map((value) => flag.folderPath ? encodeFolderPath(value) : value);
10264
10522
  return field.kind === "string" ? values.join(",") : values;
10265
10523
  }
10266
10524
  if (takesJson(field, flag)) {
@@ -10285,6 +10543,8 @@ function coerce(raw, field, flag, flagName) {
10285
10543
  if (choices && !choices.includes(String(raw))) {
10286
10544
  throw new SimApiError(`--${flagName} must be one of: ${choices.join(", ")}`, 0);
10287
10545
  }
10546
+ if (flag.folderPath && typeof raw === "string")
10547
+ return encodeFolderPath(raw);
10288
10548
  return raw;
10289
10549
  }
10290
10550
  function asQueryValue(value) {
@@ -10325,7 +10585,8 @@ function buildRequest(operation, positional, flags, workspaceId) {
10325
10585
  continue;
10326
10586
  const flagName = flagNameFor(operation, field);
10327
10587
  const omitProfileWorkspace = commandSpec.allWorkspaces && flags.allWorkspaces === true;
10328
- const raw = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10588
+ const provided = field === PROFILE_INJECTED_FIELD ? omitProfileWorkspace ? undefined : workspaceId : flags[camel(flagName)];
10589
+ const raw = provided ?? flag.requestDefault;
10329
10590
  const value = coerce(raw ?? undefined, descriptor, flag, flagName);
10330
10591
  if (value === undefined) {
10331
10592
  if (descriptor.required) {
@@ -10498,6 +10759,15 @@ function countTraceSpans(value) {
10498
10759
  function at(row, path) {
10499
10760
  return path.split(".").reduce((value, key) => value && typeof value === "object" ? value[key] : undefined, row);
10500
10761
  }
10762
+ function decodeFolderPath(value) {
10763
+ return value.split("/").map((segment) => {
10764
+ try {
10765
+ return decodeURIComponent(segment);
10766
+ } catch {
10767
+ return segment;
10768
+ }
10769
+ }).join("/");
10770
+ }
10501
10771
  function renderCell(value, format, options = {}) {
10502
10772
  switch (format) {
10503
10773
  case "timestamp":
@@ -10512,6 +10782,8 @@ function renderCell(value, format, options = {}) {
10512
10782
  return typeof value === "number" ? `$${value.toFixed(4)}` : text(null);
10513
10783
  case "count":
10514
10784
  return Array.isArray(value) ? String(value.length) : text(null);
10785
+ case "folder-path":
10786
+ return typeof value === "string" ? text(decodeFolderPath(value)) : text(value);
10515
10787
  case "trace-count": {
10516
10788
  const count = countTraceSpans(value);
10517
10789
  return `${count} ${count === 1 ? "span" : "spans"}${options.expandedTrace ? "" : " (use --trace)"}`;
@@ -10522,10 +10794,42 @@ function renderCell(value, format, options = {}) {
10522
10794
  return sanitize(typeof value === "object" ? JSON.stringify(value) : String(value));
10523
10795
  }
10524
10796
  }
10525
- var NESTED_CELL_WIDTH = 160;
10526
- function recordCell(value) {
10527
- const rendered = renderCell(value, "auto");
10528
- return rendered.length > NESTED_CELL_WIDTH ? `${rendered.slice(0, NESTED_CELL_WIDTH)}…` : rendered;
10797
+ var TIMESTAMP_KEY = /(?:At|Date)$/;
10798
+ var DURATION_KEY = /Ms$|^duration/;
10799
+ var BYTES_KEY = /^size$|(?:Size|Bytes)$/;
10800
+ var BOOL_KEY = /^(?:is|has)[A-Z]/;
10801
+ var RATIO_KEY = /^(?:similarity|score)$|(?:Similarity|Score)$/;
10802
+ var FOLDER_PATH_KEY = /^(?:path|parentPath|folderPath)$/;
10803
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
10804
+ var RATIO_PRECISION = 4;
10805
+ function inferFormat(key, value) {
10806
+ if (typeof value === "boolean")
10807
+ return BOOL_KEY.test(key) ? "bool" : null;
10808
+ if (typeof value === "string") {
10809
+ if (FOLDER_PATH_KEY.test(key))
10810
+ return "folder-path";
10811
+ return TIMESTAMP_KEY.test(key) && ISO_TIMESTAMP.test(value) && !Number.isNaN(Date.parse(value)) ? "timestamp" : null;
10812
+ }
10813
+ if (typeof value !== "number" || !Number.isFinite(value))
10814
+ return null;
10815
+ if (DURATION_KEY.test(key))
10816
+ return "duration";
10817
+ if (BYTES_KEY.test(key))
10818
+ return "bytes";
10819
+ return null;
10820
+ }
10821
+ function inferredCell(key, value) {
10822
+ if (typeof value === "number" && Number.isFinite(value) && RATIO_KEY.test(key)) {
10823
+ return value.toFixed(RATIO_PRECISION);
10824
+ }
10825
+ return renderCell(value, inferFormat(key, value) ?? "auto");
10826
+ }
10827
+ function humanizeKey(key) {
10828
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
10829
+ }
10830
+ function inferHeader(key, format) {
10831
+ const trimmed = format === "duration" || format === "bytes" ? key.replace(/(?:Ms|Bytes)$/, "") : format === "bool" ? key.replace(/^is(?=[A-Z])/, "") : key;
10832
+ return humanizeKey(trimmed || key);
10529
10833
  }
10530
10834
  function columnsFrom(specs) {
10531
10835
  return specs.map((spec) => ({
@@ -10534,9 +10838,9 @@ function columnsFrom(specs) {
10534
10838
  }));
10535
10839
  }
10536
10840
  function fieldsFrom(data, specs, options = {}) {
10537
- return specs.flatMap((spec) => {
10841
+ return specs.map((spec) => {
10538
10842
  const value = at(data, spec.path ?? spec.header);
10539
- return value === undefined ? [] : [[spec.header, renderCell(value, spec.format, options)]];
10843
+ return [spec.header, value === undefined ? text(null) : renderCell(value, spec.format, options)];
10540
10844
  });
10541
10845
  }
10542
10846
  function inferColumns(rows, expand) {
@@ -10551,7 +10855,7 @@ function inferColumns(rows, expand) {
10551
10855
  if (value !== null && typeof value === "object")
10552
10856
  continue;
10553
10857
  seen.add(key);
10554
- paths.push({ path: key, header: key });
10858
+ paths.push({ path: key, key, header: inferHeader(key, inferFormat(key, value)), owned: true });
10555
10859
  }
10556
10860
  }
10557
10861
  if (expand) {
@@ -10564,13 +10868,18 @@ function inferColumns(rows, expand) {
10564
10868
  if (nested.has(key))
10565
10869
  continue;
10566
10870
  nested.add(key);
10567
- paths.push({ path: `${expand}.${key}`, header: seen.has(key) ? `${expand}.${key}` : key });
10871
+ paths.push({
10872
+ path: `${expand}.${key}`,
10873
+ key,
10874
+ header: seen.has(key) ? `${expand}.${key}` : key,
10875
+ owned: false
10876
+ });
10568
10877
  }
10569
10878
  }
10570
10879
  }
10571
- return paths.map(({ path, header }) => ({
10880
+ return paths.map(({ path, key, header, owned }) => ({
10572
10881
  header: sanitize(header),
10573
- value: (row) => renderCell(at(row, path), "auto")
10882
+ value: (row) => owned ? inferredCell(key, at(row, path)) : renderCell(at(row, path), "auto")
10574
10883
  }));
10575
10884
  }
10576
10885
  function unwrapResource(data) {
@@ -10603,7 +10912,10 @@ function renderResult(operation, format, raw, spec, options = {}) {
10603
10912
  printList(format, data, spec.columns ? columnsFrom(spec.columns) : inferColumns(data, spec.expand));
10604
10913
  return;
10605
10914
  }
10606
- const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [key, recordCell(value)]) : [];
10915
+ const fields = spec.fields ? fieldsFrom(data, spec.fields, options) : data && typeof data === "object" ? Object.entries(data).map(([key, value]) => [
10916
+ inferHeader(key, inferFormat(key, value)),
10917
+ inferredCell(key, value)
10918
+ ]) : [];
10607
10919
  printRecord(format, fields, data);
10608
10920
  if (spec.expandedTrace && options.expandedTrace) {
10609
10921
  const traceSpans = at(data, "traceSpans");
@@ -11001,7 +11313,7 @@ async function finishUploadSession(client, workspaceId, session, path) {
11001
11313
 
11002
11314
  // src/commands/protocol/files-upload.ts
11003
11315
  function attachFileUpload(files) {
11004
- files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Destination folder path (defaults to /)").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11316
+ files.command("upload").argument("<path>", "Local file to upload").description("Upload a file to the workspace").option("--folder <path>", "Folder path as shown in the app; defaults to the root folder").option("--name <name>", "Store it under a different name").action(async (path, options, command) => {
11005
11317
  const { client, profile } = clientFrom(command);
11006
11318
  const workspaceId = client.requireWorkspace();
11007
11319
  const { name, size } = await localFile(path, options.name);
@@ -11012,7 +11324,7 @@ function attachFileUpload(files) {
11012
11324
  name,
11013
11325
  contentType: contentTypeFor(name),
11014
11326
  size,
11015
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
11327
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11016
11328
  }
11017
11329
  });
11018
11330
  const { session, uploadToken, transfer } = created.data;
@@ -11114,15 +11426,22 @@ function addFieldOption(command, operation, field, descriptor) {
11114
11426
  const placeholder = takesList ? "<value...>" : wantsJson ? "<json|@file>" : "<value>";
11115
11427
  const choices = flag.choices ?? descriptor.values;
11116
11428
  const describe = `${documented}${takesList ? " (space-separated, or @path / @- with one value per line)" : wantsJson ? " (JSON, or @path / @- to read a file or stdin)" : ""}${descriptor.required ? " (required)" : ""}`;
11429
+ const renamedFrom = flag.renamedFrom ?? [];
11117
11430
  const option = new Option(`${short}--${name} ${placeholder}`, describe);
11118
11431
  if (choices && !takesList)
11119
11432
  option.choices([...choices]);
11120
11433
  if (descriptor.default !== undefined && field !== "limit") {
11121
11434
  option.default(undefined, String(descriptor.default));
11122
11435
  }
11123
- if (descriptor.required)
11436
+ if (descriptor.required && renamedFrom.length === 0)
11124
11437
  option.makeOptionMandatory();
11125
11438
  command.addOption(option);
11439
+ for (const previous of renamedFrom) {
11440
+ const retired = new Option(`--${previous} ${placeholder}`).hideHelp();
11441
+ if (choices && !takesList)
11442
+ retired.choices([...choices]);
11443
+ command.addOption(retired);
11444
+ }
11126
11445
  }
11127
11446
  function addOperationOptions(command, operation, commandSpec, operationSpec) {
11128
11447
  for (const param of operationSpec.pathParams) {
@@ -11158,7 +11477,7 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
11158
11477
  }
11159
11478
  }
11160
11479
  if (commandSpec.confirm) {
11161
- command.option("-y, --yes", "Skip the confirmation");
11480
+ command.option("-y, --yes", "Confirm this destructive operation (required)");
11162
11481
  }
11163
11482
  }
11164
11483
 
@@ -11166,8 +11485,11 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
11166
11485
  var COLUMNS = [
11167
11486
  { header: "kind", value: (entry) => text(entry.kind) },
11168
11487
  { header: "name", value: (entry) => text(entry.name) },
11169
- { header: "ref", value: (entry) => text(entry.ref) },
11170
- { header: "folder", value: (entry) => text(entry.folderPath) },
11488
+ {
11489
+ header: "ref",
11490
+ value: (entry) => text(entry.kind === "folder" ? decodeFolderPath(entry.ref) : entry.ref)
11491
+ },
11492
+ { header: "folder", value: (entry) => text(decodeFolderPath(entry.folderPath)) },
11171
11493
  { header: "updated", value: (entry) => timestamp2(entry.updatedAt) }
11172
11494
  ];
11173
11495
  function operationPath(operation) {
@@ -11218,7 +11540,7 @@ function attachResourceDirectoryCommands(group, config) {
11218
11540
  throw new SimApiError("--limit must be a non-negative integer", 0);
11219
11541
  }
11220
11542
  const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit;
11221
- const folderPath = path ?? "/";
11543
+ const folderPath = encodeFolderPath(path ?? "/");
11222
11544
  const { client, profile } = clientFrom(command);
11223
11545
  const workspaceId = client.requireWorkspace();
11224
11546
  const [folders, resources] = await Promise.all([
@@ -11233,7 +11555,7 @@ function attachResourceDirectoryCommands(group, config) {
11233
11555
  const operation = V2_OPERATIONS[config.createFolder];
11234
11556
  const result = await client.request(operation.path, {
11235
11557
  method: operation.method,
11236
- body: { workspaceId: client.requireWorkspace(), path }
11558
+ body: { workspaceId: client.requireWorkspace(), path: encodeFolderPath(path) }
11237
11559
  });
11238
11560
  renderResult(config.createFolder, profile.output, result.data ?? result, {});
11239
11561
  });
@@ -11287,7 +11609,7 @@ function validateTargetOptions(options) {
11287
11609
  return intoExisting;
11288
11610
  }
11289
11611
  function attachTableImport(tables) {
11290
- tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
11612
+ tables.command("import").argument("[path]", "Local CSV file to import; omit when using --file-id").description("Import a CSV, into a new table by default").option("--name <name>", "Identifier for the new table: letters, numbers, and underscores; defaults to the sanitized file name").option("--table-id <id>", "Import into this existing table instead of creating one").addOption(new Option("--mode <append|replace>", "How to write into --table-id (default: append)").choices(["append", "replace"])).option("--folder <path>", "Folder path for the new table, as shown in the app").option("--file-id <id>", "Import a file already in the workspace instead of a local path").option("--mapping <json|@file>", "Column mapping (--table-id only)").option("--create-columns <json|@file>", "Columns to create (--table-id only)").option("--timezone <iana>", "Timezone for date parsing, e.g. America/New_York").option("--no-wait", "Return once the import is queued instead of watching it").action(async (path, options, command) => {
11291
11613
  const { client, profile } = clientFrom(command);
11292
11614
  const workspaceId = client.requireWorkspace();
11293
11615
  if (Boolean(path) === Boolean(options.fileId)) {
@@ -11312,7 +11634,7 @@ function attachTableImport(tables) {
11312
11634
  target = {
11313
11635
  type: "new",
11314
11636
  name,
11315
- ...options.folder !== undefined ? { folderPath: options.folder } : {}
11637
+ ...options.folder !== undefined ? { folderPath: encodeFolderPath(options.folder) } : {}
11316
11638
  };
11317
11639
  }
11318
11640
  const started = await client.request(V2_OPERATIONS.createTableImport.path, {
@@ -11513,6 +11835,23 @@ function attachSecretCommands(program2) {
11513
11835
  secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description("Create or replace a named secret").addOption(new Option("--scope <scope>", "Secret ownership scope").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value>", "Secret value; visible to shell history when supplied directly").action((name, options, command) => setSecret(name, options, command));
11514
11836
  }
11515
11837
 
11838
+ // src/runtime/renamed.ts
11839
+ var warned = new Set;
11840
+ function warn(kind, from, to) {
11841
+ const key = `${kind}:${from}`;
11842
+ if (warned.has(key))
11843
+ return;
11844
+ warned.add(key);
11845
+ process.stderr.write(`warning: ${kind} "${from}" has been renamed to "${to}". The old name still works.
11846
+ `);
11847
+ }
11848
+ function warnRenamedCommand(from, to) {
11849
+ warn("command", `sim ${from}`, `sim ${to}`);
11850
+ }
11851
+ function warnRenamedFlag(from, to) {
11852
+ warn("flag", `--${from}`, `--${to}`);
11853
+ }
11854
+
11516
11855
  // src/runtime/execute.ts
11517
11856
  function cursorSlot(operationSpec) {
11518
11857
  if (operationSpec.query && "cursor" in operationSpec.query)
@@ -11521,6 +11860,23 @@ function cursorSlot(operationSpec) {
11521
11860
  return "body";
11522
11861
  return null;
11523
11862
  }
11863
+ function foldRenamedFlags(operation, commandSpec, flags) {
11864
+ for (const [field, flag] of Object.entries(commandSpec.flags ?? {})) {
11865
+ if (!flag.renamedFrom?.length)
11866
+ continue;
11867
+ const current = flagNameFor(operation, field);
11868
+ for (const previous of flag.renamedFrom) {
11869
+ const supplied = flags[camel(previous)];
11870
+ if (supplied === undefined)
11871
+ continue;
11872
+ if (flags[camel(current)] !== undefined) {
11873
+ throw new SimApiError(`--${previous} is the former name of --${current}; pass one, not both`, 0);
11874
+ }
11875
+ warnRenamedFlag(previous, current);
11876
+ flags[camel(current)] = supplied;
11877
+ }
11878
+ }
11879
+ }
11524
11880
  async function executeOperation(operation, commandSpec, operationSpec, invocation) {
11525
11881
  const host = invocation[invocation.length - 1];
11526
11882
  const inheritedFlags = host.optsWithGlobals();
@@ -11535,6 +11891,7 @@ async function executeOperation(operation, commandSpec, operationSpec, invocatio
11535
11891
  for (const [index, field] of (commandSpec.positionals ?? []).entries()) {
11536
11892
  requestFlags[camel(flagNameFor(operation, field))] = invocation[pathPositionalCount + index];
11537
11893
  }
11894
+ foldRenamedFlags(operation, commandSpec, requestFlags);
11538
11895
  if (commandSpec.confirm && !requestFlags.yes) {
11539
11896
  throw new SimApiError(`${commandSpec.confirm} Re-run with --yes to confirm.`, 0);
11540
11897
  }
@@ -11555,16 +11912,23 @@ async function executeOperation(operation, commandSpec, operationSpec, invocatio
11555
11912
  const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT);
11556
11913
  const pageLimit = "limit" in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {};
11557
11914
  const rows = [];
11915
+ const progress = pageProgress();
11558
11916
  let cursor = null;
11559
- do {
11560
- const page = await client.request(request.path, {
11561
- method: operationSpec.method,
11562
- query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
11563
- body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
11564
- });
11565
- rows.push(...page.data);
11566
- cursor = page.nextCursor;
11567
- } while (cursor && rows.length < limit);
11917
+ try {
11918
+ do {
11919
+ const page = await client.request(request.path, {
11920
+ method: operationSpec.method,
11921
+ query: paging === "query" ? { ...request.query, ...pageLimit, cursor } : request.query,
11922
+ body: paging === "body" ? { ...request.body ?? {}, ...pageLimit, ...cursor ? { cursor } : {} } : request.body
11923
+ });
11924
+ rows.push(...page.data);
11925
+ cursor = page.nextCursor;
11926
+ if (cursor && rows.length < limit)
11927
+ progress.advance(rows.length);
11928
+ } while (cursor && rows.length < limit);
11929
+ } finally {
11930
+ progress.finish();
11931
+ }
11568
11932
  renderPage(profile.output, Number.isFinite(limit) ? rows.slice(0, limit) : rows, commandSpec);
11569
11933
  return;
11570
11934
  }
@@ -11693,6 +12057,19 @@ function configureOperation(command, operation, spec) {
11693
12057
  function buildLeaf(operation, spec, leafName) {
11694
12058
  return addMissingArgumentExample(configureOperation(new Command(leafName), operation, spec));
11695
12059
  }
12060
+ function addRenamedCommand(groups, operation, spec, from, to) {
12061
+ const segments = from.split(" ");
12062
+ const [groupName, ...rest] = segments;
12063
+ if (rest.length === 0)
12064
+ throw new Error(`${operation}.renamedFrom "${from}" must include a verb`);
12065
+ let parent = groupFor(groups, groupName);
12066
+ for (const segment of rest.slice(0, -1)) {
12067
+ parent = nestedGroup(parent, segment, { hidden: true });
12068
+ }
12069
+ const leaf = buildLeaf(operation, spec, rest[rest.length - 1]);
12070
+ leaf.hook("preAction", () => warnRenamedCommand(from, to));
12071
+ parent.addCommand(leaf, { hidden: true });
12072
+ }
11696
12073
  function groupFor(groups, name) {
11697
12074
  const existing = groups.get(name);
11698
12075
  if (existing)
@@ -11708,12 +12085,12 @@ function resourceLabel(name) {
11708
12085
  const label = name.endsWith("s") ? name.slice(0, -1) : name;
11709
12086
  return label.replaceAll("-", " ");
11710
12087
  }
11711
- function nestedGroup(parent, name) {
12088
+ function nestedGroup(parent, name, options = {}) {
11712
12089
  const existing = parent.commands.find((candidate) => candidate.name() === name);
11713
12090
  if (existing)
11714
12091
  return existing;
11715
12092
  const created = new Command(name).description(`Manage ${resourceLabel(parent.name())} ${name.replaceAll("-", " ")}`);
11716
- parent.addCommand(created);
12093
+ parent.addCommand(created, { hidden: options.hidden });
11717
12094
  return created;
11718
12095
  }
11719
12096
  function addLeafCommand(groups, operation, spec, segments) {
@@ -11742,6 +12119,7 @@ function variantCommandSpec(spec, variant) {
11742
12119
  }
11743
12120
  function buildGeneratedCommands() {
11744
12121
  const groups = new Map;
12122
+ const renamed = [];
11745
12123
  for (const operation of Object.keys(V2_OPERATIONS)) {
11746
12124
  const spec = CLI_CONTRACT[operation] ?? {};
11747
12125
  const operationSpec = V2_OPERATIONS[operation];
@@ -11764,6 +12142,12 @@ function buildGeneratedCommands() {
11764
12142
  for (const variant of spec.variants ?? []) {
11765
12143
  addLeafCommand(groups, operation, variantCommandSpec(spec, variant), variant.command.split(" "));
11766
12144
  }
12145
+ for (const from of spec.renamedFrom ?? []) {
12146
+ renamed.push({ operation, spec, from, to: segments.join(" ") });
12147
+ }
12148
+ }
12149
+ for (const { operation, spec, from, to } of renamed) {
12150
+ addRenamedCommand(groups, operation, spec, from, to);
11767
12151
  }
11768
12152
  return [...groups.values()].sort((a, b) => a.name().localeCompare(b.name()));
11769
12153
  }