sim 2.0.0-preview.16.1 → 2.0.0-preview.18.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 +5 -0
  2. package/dist/index.js +146 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,6 +57,11 @@ Each setting resolves independently, first match wins:
57
57
  | --- | --- |
58
58
  | 1 | Command-line flag (`--endpoint`, `--workspace`, `--output`) |
59
59
  | 2 | Environment (`SIM_ENDPOINT`, `SIM_API_KEY`, `SIM_WORKSPACE`, `SIM_OUTPUT`) |
60
+
61
+ `SIM_TIMEOUT_SECONDS` bounds each request (default `3600`, `0` waits
62
+ indefinitely) and `SIM_DEBUG=1` traces requests to stderr. Node ignores
63
+ `HTTPS_PROXY` unless `NODE_USE_ENV_PROXY=1` is also set, on Node 22.21+ or
64
+ 24.5+; the CLI warns when a proxy is configured but will not be used.
60
65
  | 3 | `~/.sim/config` / `~/.sim/credentials` for the selected profile |
61
66
  | 4 | Built-in default (`https://www.sim.ai`, `table`) |
62
67
 
package/dist/index.js CHANGED
@@ -2539,6 +2539,53 @@ function readPackageVersion() {
2539
2539
  var CLI_VERSION = readPackageVersion();
2540
2540
  var USER_AGENT = `sim-cli/${CLI_VERSION} node/${process.versions.node} (${process.platform}; ${process.arch})`;
2541
2541
 
2542
+ // src/http/environment.ts
2543
+ var reported = new Set;
2544
+ function once(key, message) {
2545
+ if (reported.has(key))
2546
+ return;
2547
+ reported.add(key);
2548
+ process.stderr.write(`warning: ${message}
2549
+ `);
2550
+ }
2551
+ var PROXY_VARIABLES = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
2552
+ var PROXY_SUPPORT = { 22: 21, 24: 5 };
2553
+ var FIRST_SUPPORTED_MAJOR = 24;
2554
+ function runtimeCanProxy(version) {
2555
+ const [major, minor] = version.replace(/^v/, "").split(".").map(Number);
2556
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
2557
+ return false;
2558
+ const firstSupportedMinor = PROXY_SUPPORT[major];
2559
+ if (firstSupportedMinor !== undefined)
2560
+ return minor >= firstSupportedMinor;
2561
+ return major > FIRST_SUPPORTED_MAJOR;
2562
+ }
2563
+ function warnIfProxyIgnored(env2 = process.env, version = process.version) {
2564
+ const variable = PROXY_VARIABLES.find((name) => env2[name]);
2565
+ if (!variable)
2566
+ return;
2567
+ if (env2.NODE_USE_ENV_PROXY && runtimeCanProxy(version))
2568
+ return;
2569
+ once("proxy", runtimeCanProxy(version) ? `${variable} is set but Node only uses it when NODE_USE_ENV_PROXY=1. Re-run with NODE_USE_ENV_PROXY=1 to route through the proxy.` : `${variable} is set but Node ${version} cannot use it. Upgrade to Node 22.21 or 24.5 and set NODE_USE_ENV_PROXY=1 to route through the proxy.`);
2570
+ }
2571
+ var LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
2572
+ function isLoopback(hostname) {
2573
+ return LOOPBACK.has(hostname) || hostname.endsWith(".localhost");
2574
+ }
2575
+ function warnIfKeyOverCleartext(endpoint, hasApiKey) {
2576
+ if (!hasApiKey)
2577
+ return;
2578
+ let url;
2579
+ try {
2580
+ url = new URL(endpoint);
2581
+ } catch {
2582
+ return;
2583
+ }
2584
+ if (url.protocol !== "http:" || isLoopback(url.hostname))
2585
+ return;
2586
+ once("cleartext", `sending your API key to ${url.host} over http. Anything on the path can read it — use https unless this network is trusted.`);
2587
+ }
2588
+
2542
2589
  // src/http/client.ts
2543
2590
  class SimApiError extends Error {
2544
2591
  status;
@@ -2623,6 +2670,51 @@ function dropUnionBranchNoise(issues) {
2623
2670
  const kept = issues.filter((issue) => !issues.some((other) => rejectsAKeyThatValidated(issue, other)));
2624
2671
  return kept.length > 0 ? kept : issues;
2625
2672
  }
2673
+ var DEFAULT_TIMEOUT_SECONDS = 3600;
2674
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
2675
+ var RAISE_TIMEOUT_HINT = "Raise SIM_TIMEOUT_SECONDS, or set it to 0 to wait indefinitely.";
2676
+ function isRequestTimeout(error) {
2677
+ return error instanceof DOMException && error.name === "TimeoutError";
2678
+ }
2679
+ function resolveTimeoutMs(env2 = process.env) {
2680
+ const raw = env2.SIM_TIMEOUT_SECONDS;
2681
+ if (raw === undefined || raw.trim() === "")
2682
+ return DEFAULT_TIMEOUT_SECONDS * 1000;
2683
+ const seconds = Number(raw);
2684
+ if (!Number.isFinite(seconds) || seconds < 0) {
2685
+ throw new SimApiError(`Invalid SIM_TIMEOUT_SECONDS "${raw}". Use a non-negative number of seconds, or 0 to disable.`, 0);
2686
+ }
2687
+ const ms = seconds === 0 ? 0 : Math.max(1, Math.round(seconds * 1000));
2688
+ if (ms > MAX_TIMEOUT_MS) {
2689
+ throw new SimApiError(`SIM_TIMEOUT_SECONDS "${raw}" is longer than Node can wait (${Math.floor(MAX_TIMEOUT_MS / 1000)}s). Use 0 to wait indefinitely.`, 0);
2690
+ }
2691
+ return ms;
2692
+ }
2693
+ function combineSignals(caller, timeout) {
2694
+ if (!caller)
2695
+ return timeout;
2696
+ if (!timeout)
2697
+ return caller;
2698
+ if (typeof AbortSignal.any === "function")
2699
+ return AbortSignal.any([caller, timeout]);
2700
+ const controller = new AbortController;
2701
+ for (const signal of [caller, timeout]) {
2702
+ if (signal.aborted) {
2703
+ controller.abort(signal.reason);
2704
+ break;
2705
+ }
2706
+ signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
2707
+ }
2708
+ return controller.signal;
2709
+ }
2710
+ function debugEnabled(env2 = process.env) {
2711
+ const raw = env2.SIM_DEBUG;
2712
+ return raw !== undefined && raw !== "" && raw !== "0" && raw.toLowerCase() !== "false";
2713
+ }
2714
+ function traceRequest(method, url, status, startedAt) {
2715
+ process.stderr.write(`${source_default.dim(`[sim] ${method} ${url} → ${status} ${Math.round(performance.now() - startedAt)}ms`)}
2716
+ `);
2717
+ }
2626
2718
  function formatApiErrorDetails(details) {
2627
2719
  const issues = [];
2628
2720
  const seen = new Set;
@@ -2706,10 +2798,18 @@ class SimClient {
2706
2798
  const apiKey = this.resolveApiKey(options.auth);
2707
2799
  const url = buildUrl(this.profile.endpoint, path, options.query);
2708
2800
  const hasBody = options.body !== undefined;
2801
+ const method = options.method ?? "GET";
2802
+ warnIfProxyIgnored();
2803
+ warnIfKeyOverCleartext(this.profile.endpoint, Boolean(apiKey));
2804
+ const timeoutMs = resolveTimeoutMs();
2805
+ const timeout = timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
2806
+ const signal = combineSignals(options.signal, timeout);
2807
+ const trace = debugEnabled();
2808
+ const startedAt = performance.now();
2709
2809
  let response;
2710
2810
  try {
2711
2811
  response = await fetch(url, {
2712
- method: options.method ?? "GET",
2812
+ method,
2713
2813
  headers: {
2714
2814
  ...apiKey ? { "x-api-key": apiKey } : {},
2715
2815
  accept: "application/json",
@@ -2718,15 +2818,22 @@ class SimClient {
2718
2818
  ...options.headers
2719
2819
  },
2720
2820
  body: hasBody ? JSON.stringify(options.body) : undefined,
2721
- signal: options.signal,
2821
+ signal,
2722
2822
  redirect: "manual"
2723
2823
  });
2724
2824
  } catch (cause) {
2825
+ if (trace)
2826
+ traceRequest(method, url, "failed", startedAt);
2725
2827
  if (options.signal?.aborted) {
2726
2828
  throw new SimApiError("Request cancelled.", 0);
2727
2829
  }
2830
+ if (timeout?.aborted) {
2831
+ throw new SimApiError(`${url} did not answer within ${timeoutMs / 1000}s. ${RAISE_TIMEOUT_HINT}`, 0);
2832
+ }
2728
2833
  throw new SimApiError(`Could not reach ${this.profile.endpoint}: ${cause.message}`, 0);
2729
2834
  }
2835
+ if (trace)
2836
+ traceRequest(method, url, response.status, startedAt);
2730
2837
  if (REDIRECT_STATUSES.has(response.status))
2731
2838
  throw this.toRedirectError(url, path, response);
2732
2839
  if (!response.ok) {
@@ -2768,16 +2875,16 @@ function redirectEndpoint(endpoint, requestPath, target) {
2768
2875
  return suggested === endpoint.replace(/\/+$/, "") ? null : suggested;
2769
2876
  }
2770
2877
  function pageProgress() {
2771
- let reported = false;
2878
+ let reported2 = false;
2772
2879
  return {
2773
2880
  advance: (fetched) => {
2774
2881
  if (!process.stderr.isTTY)
2775
2882
  return;
2776
- reported = true;
2883
+ reported2 = true;
2777
2884
  process.stderr.write(`\r${source_default.dim(`fetched ${fetched}…`)}\x1B[K`);
2778
2885
  },
2779
2886
  finish: () => {
2780
- if (reported)
2887
+ if (reported2)
2781
2888
  process.stderr.write("\r\x1B[K");
2782
2889
  }
2783
2890
  };
@@ -8939,6 +9046,10 @@ var V2_OPERATIONS = {
8939
9046
  kind: "string",
8940
9047
  required: true,
8941
9048
  describe: "Write-only secret value. It is never returned."
9049
+ },
9050
+ description: {
9051
+ kind: "string",
9052
+ describe: "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one."
8942
9053
  }
8943
9054
  }
8944
9055
  },
@@ -9996,7 +10107,8 @@ var CLI_CONTRACT = {
9996
10107
  { header: "name" },
9997
10108
  { header: "scope" },
9998
10109
  { header: "role" },
9999
- { header: "updated", path: "updatedAt", format: "timestamp" }
10110
+ { header: "updated", path: "updatedAt", format: "timestamp" },
10111
+ { header: "description" }
10000
10112
  ]
10001
10113
  },
10002
10114
  getWorkspace: {
@@ -11059,7 +11171,7 @@ function attachCredentialCommands(program2) {
11059
11171
  }
11060
11172
 
11061
11173
  // src/commands/protocol/files-get.ts
11062
- import { once } from "node:events";
11174
+ import { once as once2 } from "node:events";
11063
11175
  import { createWriteStream } from "node:fs";
11064
11176
  import { link, lstat, mkdtemp, readlink, rename, rm } from "node:fs/promises";
11065
11177
  import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
@@ -11074,6 +11186,9 @@ function printProtocolResult(format, result) {
11074
11186
 
11075
11187
  // src/commands/protocol/files-get.ts
11076
11188
  function writeFailure(path, error) {
11189
+ if (isRequestTimeout(error)) {
11190
+ return new SimApiError(`Downloading ${path} timed out. ${RAISE_TIMEOUT_HINT}`, 0);
11191
+ }
11077
11192
  const code = error.code;
11078
11193
  if (code === "EEXIST") {
11079
11194
  return new SimApiError(`${path} already exists. Pass --force to overwrite it, or choose another output path.`, 0);
@@ -11164,7 +11279,7 @@ async function streamToStdout(body, output = process.stdout) {
11164
11279
  if (done)
11165
11280
  return;
11166
11281
  if (!output.write(value))
11167
- await once(output, "drain");
11282
+ await once2(output, "drain");
11168
11283
  }
11169
11284
  } finally {
11170
11285
  reader.releaseLock();
@@ -11608,17 +11723,17 @@ function jsonFlag(raw, flagName, kind) {
11608
11723
  }
11609
11724
  async function watchImport(client, workspaceId, job) {
11610
11725
  let current = job;
11611
- let reported = -1;
11726
+ let reported2 = -1;
11612
11727
  while (!IMPORT_SETTLED.has(current.status)) {
11613
11728
  await sleep2(IMPORT_POLL_MS);
11614
11729
  const next = await client.request(`/api/v2/tables/imports/${encodeURIComponent(current.id)}`, { query: { workspaceId } });
11615
11730
  current = next.data;
11616
- if (process.stderr.isTTY && current.rowsProcessed !== reported) {
11617
- reported = current.rowsProcessed;
11618
- process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported} rows`)}\x1B[K`);
11731
+ if (process.stderr.isTTY && current.rowsProcessed !== reported2) {
11732
+ reported2 = current.rowsProcessed;
11733
+ process.stderr.write(`\r${source_default.dim(`${current.status}… ${reported2} rows`)}\x1B[K`);
11619
11734
  }
11620
11735
  }
11621
- if (process.stderr.isTTY && reported >= 0)
11736
+ if (process.stderr.isTTY && reported2 >= 0)
11622
11737
  process.stderr.write("\r\x1B[K");
11623
11738
  return current;
11624
11739
  }
@@ -11834,7 +11949,8 @@ var SECRET_RESULT = {
11834
11949
  { header: "name" },
11835
11950
  { header: "scope" },
11836
11951
  { header: "role" },
11837
- { header: "updated", path: "updatedAt", format: "timestamp" }
11952
+ { header: "updated", path: "updatedAt", format: "timestamp" },
11953
+ { header: "description" }
11838
11954
  ]
11839
11955
  };
11840
11956
  function validateSecretValue(value) {
@@ -11845,7 +11961,16 @@ function validateSecretValue(value) {
11845
11961
  }
11846
11962
  return value;
11847
11963
  }
11964
+ function validateDescriptionScope(description, scope) {
11965
+ if (description === undefined)
11966
+ return;
11967
+ if (scope === "personal") {
11968
+ throw new SimApiError("--description is only supported for a workspace secret.", 0);
11969
+ }
11970
+ return description;
11971
+ }
11848
11972
  async function setSecret(name, options, command) {
11973
+ const description = validateDescriptionScope(options.description, options.scope);
11849
11974
  const value = validateSecretValue(options.value ?? await promptSecret());
11850
11975
  const { client, profile } = clientFrom(command);
11851
11976
  const operation = V2_OPERATIONS.setSecret;
@@ -11854,7 +11979,8 @@ async function setSecret(name, options, command) {
11854
11979
  body: {
11855
11980
  workspaceId: client.requireWorkspace(),
11856
11981
  scope: options.scope,
11857
- value
11982
+ value,
11983
+ description
11858
11984
  }
11859
11985
  });
11860
11986
  renderResult("setSecret", profile.output, response.data, SECRET_RESULT);
@@ -11863,7 +11989,7 @@ function attachSecretCommands(program2) {
11863
11989
  const secrets = program2.commands.find((command) => command.name() === "secrets");
11864
11990
  if (!secrets)
11865
11991
  throw new Error("The generated secrets command group is missing");
11866
- 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));
11992
+ 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").option("--description <description>", "What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged").action((name, options, command) => setSecret(name, options, command));
11867
11993
  }
11868
11994
 
11869
11995
  // src/runtime/renamed.ts
@@ -12232,6 +12358,10 @@ async function main() {
12232
12358
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
12233
12359
  process.exit(1);
12234
12360
  }
12361
+ if (isRequestTimeout(error)) {
12362
+ console.error(source_default.red(`Error: the request timed out. ${RAISE_TIMEOUT_HINT}`));
12363
+ process.exit(1);
12364
+ }
12235
12365
  if (error instanceof SimApiError) {
12236
12366
  console.error(source_default.red(`Error: ${sanitize(error.message)}`));
12237
12367
  if (error.code)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.0.0-preview.16.1",
3
+ "version": "2.0.0-preview.18.1",
4
4
  "description": "Sim CLI - talk to the Sim API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {