sim 2.1.6-preview.84.1 → 2.1.6-preview.89.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 +18 -1
  2. package/dist/index.js +326 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -256,9 +256,26 @@ The main environment variables are:
256
256
  | `SIM_API_KEY` | API key, usually for CI |
257
257
  | `SIM_WORKSPACE` | Workspace to target |
258
258
  | `SIM_OUTPUT` | `table`, `json`, `yaml`, or `text` |
259
- | `SIM_CONFIG_DIR` | Directory containing CLI config and credentials |
259
+ | `SIM_CONFIG_DIR` | Base directory for CLI config, credentials, and the update cache |
260
260
  | `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely |
261
261
  | `SIM_DEBUG` | Print request diagnostics to stderr |
262
+ | `SIM_NO_UPDATE_CHECK` | Turn off the update notice |
263
+
264
+ On eligible interactive invocations, `sim` uses a daily cache before asking
265
+ `registry.npmjs.org` what is published under the `latest` tag and prints one
266
+ line on stderr when a newer version exists. Prerelease installs are skipped
267
+ entirely. The cache lives in `~/.sim` by default and follows `SIM_CONFIG_DIR`;
268
+ without a writable cache, each eligible invocation checks again. Concurrent
269
+ invocations can also perform duplicate checks. The registry request has a
270
+ one-second deadline; the short-lived request process is terminated on expiry.
271
+ Apart from the configured registry URL, it sends only its own version and never
272
+ your Sim API key. If `npm_config_registry` points at a private mirror, its query
273
+ string is preserved, including any query-string credentials. Registry URLs
274
+ containing username/password userinfo are rejected. Set
275
+ `SIM_NO_UPDATE_CHECK=1` to turn it off. Empty or whitespace-only registry values
276
+ use the public default; non-empty malformed or non-HTTP(S) values fail closed.
277
+ The full list of cases where it stays quiet is in the
278
+ [configuration guide](https://docs.sim.ai/cli/configuration).
262
279
 
263
280
  ## Documentation
264
281
 
package/dist/index.js CHANGED
@@ -2310,6 +2310,9 @@ function configPath() {
2310
2310
  function credentialsPath() {
2311
2311
  return process.env.SIM_CREDENTIALS_FILE || join(configDir(), "credentials");
2312
2312
  }
2313
+ function updateCachePath() {
2314
+ return join(configDir(), "update-check.json");
2315
+ }
2313
2316
  // src/config/profile.ts
2314
2317
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2315
2318
  import { dirname } from "node:path";
@@ -16454,6 +16457,328 @@ function attachSecretCommands(program2) {
16454
16457
  secrets.command("set").argument("<name>", "Secret name, as referenced in workflows").description(describeOperation(V2_OPERATIONS.setSecret, "Create or replace a named secret")).addOption(new Option("--scope <scope>", "Secret ownership scope (required)").choices([...SECRET_SCOPES]).makeOptionMandatory()).option("--value <value|@file>", "Secret value. Passing it inline exposes it to shell history and process listings; @path reads it from a file and @- from stdin, verbatim — a trailing newline is part of the value, so write the file with printf rather than echo. Prefix a literal leading @ with a second one").option("--description <description>", "What the secret is for, shown to teammates; workspace scope only. Omit to leave an existing description unchanged").option("--unredacted", `${V2_OPERATIONS.setSecret.body.unredacted.describe} Pass --no-unredacted to restore redaction`).option("--no-unredacted", "Send --unredacted as false").on("option:unredacted", () => redactionSpellings.add("--unredacted")).on("option:no-unredacted", () => redactionSpellings.add("--no-unredacted")).action((name, options, command) => setSecret(name, options, command, redactionSpellings));
16455
16458
  }
16456
16459
 
16460
+ // src/update/check.ts
16461
+ import { spawn as spawn2 } from "node:child_process";
16462
+ import {
16463
+ closeSync,
16464
+ constants as constants2,
16465
+ fstatSync,
16466
+ lstatSync,
16467
+ mkdirSync as mkdirSync2,
16468
+ openSync,
16469
+ readSync as readSync2,
16470
+ renameSync,
16471
+ unlinkSync,
16472
+ writeFileSync as writeFileSync2
16473
+ } from "node:fs";
16474
+ import { dirname as dirname3 } from "node:path";
16475
+ import { fileURLToPath } from "node:url";
16476
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
16477
+ var REGISTRY_TIMEOUT_MS = 1000;
16478
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
16479
+ var PACKAGE_NAME = "sim";
16480
+ var DIST_TAGS_PATH = `-/package/${PACKAGE_NAME}/dist-tags`;
16481
+ var MAX_RESPONSE_BYTES = 64 * 1024;
16482
+ var MAX_CACHE_BYTES = 4 * 1024;
16483
+ var STABLE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
16484
+ function parseStableVersion(version) {
16485
+ const match = STABLE_VERSION_PATTERN.exec(version);
16486
+ if (!match)
16487
+ return null;
16488
+ const parsed = [Number(match[1]), Number(match[2]), Number(match[3])];
16489
+ return parsed.every(Number.isSafeInteger) ? parsed : null;
16490
+ }
16491
+ function isNewerVersion(candidate, current) {
16492
+ if (candidate[0] !== current[0])
16493
+ return candidate[0] > current[0];
16494
+ if (candidate[1] !== current[1])
16495
+ return candidate[1] > current[1];
16496
+ return candidate[2] > current[2];
16497
+ }
16498
+ var CI_VARIABLES = [
16499
+ "CI",
16500
+ "GITHUB_ACTIONS",
16501
+ "JENKINS_URL",
16502
+ "TEAMCITY_VERSION",
16503
+ "BUILDKITE"
16504
+ ];
16505
+ var CACHE_VERSION = 1;
16506
+ var cacheWriteSequence = 0;
16507
+ function isEnabled(value) {
16508
+ if (value === undefined)
16509
+ return false;
16510
+ const normalized = value.trim().toLowerCase();
16511
+ return normalized !== "" && normalized !== "0" && normalized !== "false";
16512
+ }
16513
+ function isProjectLocalInstall(modulePath, cwd) {
16514
+ const normalizedModulePath = normalizeModulePath(modulePath);
16515
+ const nodeModulesIndex = normalizedModulePath.indexOf("/node_modules/");
16516
+ if (nodeModulesIndex < 0)
16517
+ return false;
16518
+ const installRoot = normalizedModulePath.slice(0, nodeModulesIndex);
16519
+ const workingDirectory = normalizeModulePath(cwd).replace(/\/+$/, "");
16520
+ return workingDirectory === installRoot || workingDirectory.startsWith(`${installRoot}/`);
16521
+ }
16522
+ function isUnadvisableInstall(modulePath, env2, cwd) {
16523
+ const normalized = normalizeModulePath(modulePath);
16524
+ return env2.npm_command === "exec" || normalized.includes("/_npx/") || normalized.includes("/packages/sim-cli/") || isProjectLocalInstall(modulePath, cwd);
16525
+ }
16526
+ function normalizeModulePath(modulePath) {
16527
+ return modulePath.replace(/\\/g, "/").toLowerCase();
16528
+ }
16529
+ function registryUrl(env2) {
16530
+ const fallback = new URL(DIST_TAGS_PATH, DEFAULT_REGISTRY);
16531
+ const configured = env2.npm_config_registry?.trim();
16532
+ if (!configured)
16533
+ return fallback;
16534
+ try {
16535
+ const base = new URL(configured);
16536
+ if (base.protocol !== "http:" && base.protocol !== "https:")
16537
+ return null;
16538
+ if (base.username || base.password)
16539
+ return null;
16540
+ base.pathname = `${base.pathname.replace(/\/$/, "")}/${DIST_TAGS_PATH}`;
16541
+ return base;
16542
+ } catch {
16543
+ return null;
16544
+ }
16545
+ }
16546
+ var REGISTRY_REQUEST_SCRIPT = `
16547
+ let input = ''
16548
+ process.stdin.setEncoding('utf8')
16549
+ for await (const chunk of process.stdin) input += chunk
16550
+
16551
+ try {
16552
+ const { url, headers, maxResponseBytes, timeoutMs } = JSON.parse(input)
16553
+ const deadline = setTimeout(() => process.exit(1), timeoutMs)
16554
+ const response = await fetch(url, { headers, redirect: 'error' })
16555
+ const declared = Number(response.headers.get('content-length'))
16556
+
16557
+ if (!response.ok || !response.body || (Number.isFinite(declared) && declared > maxResponseBytes)) {
16558
+ process.exit(1)
16559
+ }
16560
+
16561
+ const reader = response.body.getReader()
16562
+ const chunks = []
16563
+ let seen = 0
16564
+
16565
+ while (true) {
16566
+ const { done, value } = await reader.read()
16567
+ if (done) break
16568
+ seen += value.byteLength
16569
+ if (seen > maxResponseBytes) {
16570
+ process.exit(1)
16571
+ }
16572
+ chunks.push(Buffer.from(value))
16573
+ }
16574
+
16575
+ clearTimeout(deadline)
16576
+ process.stdout.write(Buffer.concat(chunks), () => process.exit(0))
16577
+ } catch {
16578
+ process.exit(1)
16579
+ }
16580
+ `;
16581
+ function registryProcessEnv() {
16582
+ const env2 = { ...process.env };
16583
+ for (const key of Object.keys(env2)) {
16584
+ const normalized = key.toLowerCase();
16585
+ if (normalized === "npm_config_registry" || normalized === "sim_api_key")
16586
+ delete env2[key];
16587
+ }
16588
+ return env2;
16589
+ }
16590
+ function requestRegistry(url, { headers, maxResponseBytes, timeoutMs }) {
16591
+ return new Promise((resolve3, reject) => {
16592
+ const proxyArguments = process.execArgv.filter((argument) => argument === "--use-env-proxy" || argument === "--no-use-env-proxy");
16593
+ const child = spawn2(process.execPath, [...proxyArguments, "--input-type=module", "--eval", REGISTRY_REQUEST_SCRIPT], {
16594
+ env: registryProcessEnv(),
16595
+ killSignal: "SIGKILL",
16596
+ stdio: ["pipe", "pipe", "ignore"],
16597
+ timeout: timeoutMs,
16598
+ windowsHide: true
16599
+ });
16600
+ const chunks = [];
16601
+ let failed = false;
16602
+ let seen = 0;
16603
+ child.stdout.on("data", (chunk) => {
16604
+ seen += chunk.byteLength;
16605
+ if (seen > maxResponseBytes) {
16606
+ failed = true;
16607
+ child.kill("SIGKILL");
16608
+ return;
16609
+ }
16610
+ chunks.push(chunk);
16611
+ });
16612
+ child.stdout.on("error", () => {
16613
+ failed = true;
16614
+ child.kill("SIGKILL");
16615
+ });
16616
+ child.stdin.on("error", () => {});
16617
+ child.once("error", reject);
16618
+ child.once("close", (code) => {
16619
+ resolve3(code === 0 && !failed ? Buffer.concat(chunks).toString("utf8") : null);
16620
+ });
16621
+ child.stdin.end(JSON.stringify({ headers, maxResponseBytes, timeoutMs, url: url.href }));
16622
+ });
16623
+ }
16624
+ async function fetchDistTags(env2, request) {
16625
+ try {
16626
+ const url = registryUrl(env2);
16627
+ if (!url)
16628
+ return null;
16629
+ const text2 = await request(url, {
16630
+ headers: { accept: "application/json", "user-agent": `${PACKAGE_NAME}-cli/${CLI_VERSION}` },
16631
+ maxResponseBytes: MAX_RESPONSE_BYTES,
16632
+ timeoutMs: REGISTRY_TIMEOUT_MS
16633
+ });
16634
+ if (text2 === null || Buffer.byteLength(text2) > MAX_RESPONSE_BYTES)
16635
+ return null;
16636
+ const body = JSON.parse(text2);
16637
+ if (typeof body !== "object" || body === null || Array.isArray(body))
16638
+ return null;
16639
+ const tags = {};
16640
+ for (const [tag, version] of Object.entries(body)) {
16641
+ if (typeof version === "string")
16642
+ tags[tag] = version;
16643
+ }
16644
+ return tags;
16645
+ } catch {
16646
+ return null;
16647
+ }
16648
+ }
16649
+ function readCache(path) {
16650
+ let descriptor = null;
16651
+ try {
16652
+ if (!lstatSync(path).isFile())
16653
+ return null;
16654
+ descriptor = openSync(path, constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW);
16655
+ const descriptorStats = fstatSync(descriptor);
16656
+ if (!descriptorStats.isFile() || descriptorStats.size > MAX_CACHE_BYTES) {
16657
+ return null;
16658
+ }
16659
+ const buffer = Buffer.allocUnsafe(MAX_CACHE_BYTES + 1);
16660
+ let bytesRead = 0;
16661
+ while (bytesRead < buffer.byteLength) {
16662
+ const count = readSync2(descriptor, buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
16663
+ if (count === 0)
16664
+ break;
16665
+ bytesRead += count;
16666
+ }
16667
+ if (bytesRead > MAX_CACHE_BYTES)
16668
+ return null;
16669
+ const parsed = JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
16670
+ if (typeof parsed !== "object" || parsed === null)
16671
+ return null;
16672
+ const entry = parsed;
16673
+ if (entry.version !== CACHE_VERSION)
16674
+ return null;
16675
+ if (typeof entry.checkedAt !== "string" || Number.isNaN(Date.parse(entry.checkedAt)))
16676
+ return null;
16677
+ return {
16678
+ version: CACHE_VERSION,
16679
+ checkedAt: entry.checkedAt
16680
+ };
16681
+ } catch {
16682
+ return null;
16683
+ } finally {
16684
+ if (descriptor !== null) {
16685
+ try {
16686
+ closeSync(descriptor);
16687
+ } catch {}
16688
+ }
16689
+ }
16690
+ }
16691
+ function writeCache(path, entry) {
16692
+ let descriptor = null;
16693
+ let temporaryCreated = false;
16694
+ const temporaryPath = `${path}.${process.pid}.${Date.now()}.${cacheWriteSequence++}.tmp`;
16695
+ try {
16696
+ mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
16697
+ descriptor = openSync(temporaryPath, "wx", 420);
16698
+ temporaryCreated = true;
16699
+ writeFileSync2(descriptor, `${JSON.stringify(entry, null, 2)}
16700
+ `);
16701
+ closeSync(descriptor);
16702
+ descriptor = null;
16703
+ renameSync(temporaryPath, path);
16704
+ temporaryCreated = false;
16705
+ } catch {} finally {
16706
+ if (descriptor !== null) {
16707
+ try {
16708
+ closeSync(descriptor);
16709
+ } catch {}
16710
+ }
16711
+ if (temporaryCreated) {
16712
+ try {
16713
+ unlinkSync(temporaryPath);
16714
+ } catch {}
16715
+ }
16716
+ }
16717
+ }
16718
+ function isFresh(entry, now) {
16719
+ const age = now.getTime() - Date.parse(entry.checkedAt);
16720
+ return age >= 0 && age < CHECK_INTERVAL_MS;
16721
+ }
16722
+ function upgradeCommand(modulePath = fileURLToPath(import.meta.url), env2 = process.env) {
16723
+ const target = `${PACKAGE_NAME}@latest`;
16724
+ const normalized = normalizeModulePath(modulePath);
16725
+ if (normalized.includes(".bun/install/global"))
16726
+ return `bun add -g ${target}`;
16727
+ if (normalized.includes("/pnpm/") || normalized.includes("/.pnpm/")) {
16728
+ return `pnpm add -g ${target}`;
16729
+ }
16730
+ if (normalized.includes("/.yarn/") || normalized.includes("/yarn/")) {
16731
+ return `yarn global add ${target}`;
16732
+ }
16733
+ const agent = env2.npm_config_user_agent ?? "";
16734
+ if (agent.startsWith("pnpm/"))
16735
+ return `pnpm add -g ${target}`;
16736
+ if (agent.startsWith("yarn/"))
16737
+ return `yarn global add ${target}`;
16738
+ if (agent.startsWith("bun/"))
16739
+ return `bun add -g ${target}`;
16740
+ return `npm install -g ${target}`;
16741
+ }
16742
+ async function announceUpdateIfAvailable(options = {}) {
16743
+ try {
16744
+ const env2 = options.env ?? process.env;
16745
+ const isTty = options.isTty ?? process.stderr.isTTY === true;
16746
+ const modulePath = options.modulePath ?? fileURLToPath(import.meta.url);
16747
+ const cwd = options.cwd ?? process.cwd();
16748
+ const now = options.now ?? new Date;
16749
+ if (isEnabled(env2.SIM_NO_UPDATE_CHECK))
16750
+ return;
16751
+ if (!isTty)
16752
+ return;
16753
+ if (CI_VARIABLES.some((variable) => isEnabled(env2[variable])))
16754
+ return;
16755
+ if (isUnadvisableInstall(modulePath, env2, cwd))
16756
+ return;
16757
+ const currentVersion = options.currentVersion ?? CLI_VERSION;
16758
+ const current = parseStableVersion(currentVersion);
16759
+ if (!current)
16760
+ return;
16761
+ const cachePath = updateCachePath();
16762
+ const cached = readCache(cachePath);
16763
+ if (cached && isFresh(cached, now))
16764
+ return;
16765
+ const tags = await fetchDistTags(env2, options.registryRequest ?? requestRegistry);
16766
+ const latest = tags?.latest ?? null;
16767
+ const available = latest ? parseStableVersion(latest) : null;
16768
+ writeCache(cachePath, {
16769
+ version: CACHE_VERSION,
16770
+ checkedAt: now.toISOString()
16771
+ });
16772
+ if (!latest || !available)
16773
+ return;
16774
+ if (!isNewerVersion(available, current))
16775
+ return;
16776
+ const write = options.write ?? ((message) => void process.stderr.write(message));
16777
+ write(`Update available: sim ${currentVersion} → ${latest}. Run: ${upgradeCommand(modulePath, env2)}
16778
+ `);
16779
+ } catch {}
16780
+ }
16781
+
16457
16782
  // src/program.ts
16458
16783
  var PROGRAM_DESCRIPTION = "Talk to the Sim API from your terminal";
16459
16784
  var HELP_EPILOGUE = `
@@ -16533,6 +16858,7 @@ function buildProgram(options = {}) {
16533
16858
  attachProtocolCommands(program2);
16534
16859
  attachSecretCommands(program2);
16535
16860
  program2.addHelpText("after", HELP_EPILOGUE);
16861
+ program2.hook("preAction", () => announceUpdateIfAvailable());
16536
16862
  refuseHelpAfterUnknownCommand(program2);
16537
16863
  assertNoReservedProgramFlags(program2);
16538
16864
  return program2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.1.6-preview.84.1",
3
+ "version": "2.1.6-preview.89.1",
4
4
  "description": "Sim CLI - talk to the Sim API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {