sim 2.1.6-preview.83.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 +447 -3
  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";
@@ -8263,6 +8266,22 @@ var V2_OPERATIONS = {
8263
8266
  }
8264
8267
  }
8265
8268
  },
8269
+ editFileContent: {
8270
+ method: "PATCH",
8271
+ path: "/api/v2/files/[fileId]/content",
8272
+ pathParams: ["fileId"],
8273
+ pathParamDocs: { fileId: "File identifier." },
8274
+ responseMode: "json",
8275
+ summary: "Edit File Content",
8276
+ body: {
8277
+ workspaceId: { kind: "string", required: true, describe: "Workspace that owns the file." },
8278
+ edit: {
8279
+ kind: "unknown",
8280
+ required: true,
8281
+ describe: "One exact or anchor-based edit: search_replace, replace_between, insert_after, or delete_between."
8282
+ }
8283
+ }
8284
+ },
8266
8285
  executeTool: {
8267
8286
  method: "POST",
8268
8287
  path: "/api/v2/tools/[toolId]/execute",
@@ -9263,6 +9282,28 @@ var V2_OPERATIONS = {
9263
9282
  values: ["active", "archived"],
9264
9283
  default: "active",
9265
9284
  describe: "Which lifecycle set to list: `active` (default) returns live folders only; `archived` returns folders a recursive delete soft-deleted, which is how a caller finds a path to hand to the folder restore. Authorization is identical for both."
9285
+ },
9286
+ recursive: {
9287
+ kind: "enum",
9288
+ values: [
9289
+ "true",
9290
+ "1",
9291
+ "yes",
9292
+ "on",
9293
+ "y",
9294
+ "enabled",
9295
+ "false",
9296
+ "0",
9297
+ "no",
9298
+ "off",
9299
+ "n",
9300
+ "disabled"
9301
+ ],
9302
+ describe: "Whether parentPath includes every descendant instead of direct children only."
9303
+ },
9304
+ depth: {
9305
+ kind: "integer",
9306
+ describe: "Deepest level below parentPath to include when recursive is true."
9266
9307
  }
9267
9308
  }
9268
9309
  },
@@ -10440,6 +10481,14 @@ var V2_OPERATIONS = {
10440
10481
  maxBytes: {
10441
10482
  kind: "integer",
10442
10483
  describe: "Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit."
10484
+ },
10485
+ offset: {
10486
+ kind: "integer",
10487
+ describe: "First line to return, 1-based. Absent starts at the first line."
10488
+ },
10489
+ limit: {
10490
+ kind: "integer",
10491
+ describe: "How many lines to return from `offset`. Absent reads to the end."
10443
10492
  }
10444
10493
  }
10445
10494
  },
@@ -10770,6 +10819,50 @@ var V2_OPERATIONS = {
10770
10819
  workspaceId: { kind: "string", required: true, describe: "Unique workspace identifier." }
10771
10820
  }
10772
10821
  },
10822
+ searchFileContent: {
10823
+ method: "GET",
10824
+ path: "/api/v2/files/search",
10825
+ pathParams: [],
10826
+ responseMode: "json",
10827
+ summary: "Search File Content",
10828
+ query: {
10829
+ workspaceId: { kind: "string", required: true, describe: "Workspace to search." },
10830
+ query: {
10831
+ kind: "string",
10832
+ required: true,
10833
+ describe: "Regular expression, or exact text when `mode` is `exact`."
10834
+ },
10835
+ mode: {
10836
+ kind: "enum",
10837
+ values: ["exact", "regex"],
10838
+ default: "regex",
10839
+ describe: "How `query` is read."
10840
+ },
10841
+ maxResults: { kind: "integer", default: 50, describe: "Maximum matching lines to return." },
10842
+ folderPaths: {
10843
+ kind: "string",
10844
+ describe: "Folders the search is confined to, comma-separated. Absent searches the whole workspace. The scope also narrows `indexStatus`, so `complete` describes the folders searched rather than the workspace."
10845
+ },
10846
+ includeSubfolders: {
10847
+ kind: "enum",
10848
+ values: [
10849
+ "true",
10850
+ "1",
10851
+ "yes",
10852
+ "on",
10853
+ "y",
10854
+ "enabled",
10855
+ "false",
10856
+ "0",
10857
+ "no",
10858
+ "off",
10859
+ "n",
10860
+ "disabled"
10861
+ ],
10862
+ describe: "Whether the scope descends into nested folders. Absent means yes. The listed spellings are the whole accepted vocabulary and are case-sensitive; any other value is rejected."
10863
+ }
10864
+ }
10865
+ },
10773
10866
  searchKnowledge: {
10774
10867
  method: "POST",
10775
10868
  path: "/api/v2/knowledge/search",
@@ -12019,6 +12112,7 @@ var TABLE_SORT_HELP = 'Ordered sort keys: [{"field":"createdAt","direction":"des
12019
12112
  var KNOWLEDGE_TAG_DEFINITIONS_HELP = 'Tag definitions: [{"tagSlot":"tag1","displayName":"category","fieldType":"text"}]';
12020
12113
  var CUSTOM_TOOL_SCHEMA_HELP = 'OpenAI function schema: {"type":"function","function":{"name":"...","parameters":{"type":"object","properties":{}}}}';
12021
12114
  var DISPATCH_ROW_LIMIT_HELP = "Stop after this many eligible rows have run (1-1,000,000). Omit for an unbounded run";
12115
+ var FILE_EDIT_HELP = 'One edit object: {"mode":"search_replace","search":"old","content":"new","replaceAll":false}, {"mode":"replace_between","beforeAnchor":"start line","afterAnchor":"end line","content":"new"}, {"mode":"insert_after","anchor":"line","content":"new"}, or {"mode":"delete_between","startAnchor":"first line deleted","endAnchor":"ending line kept"}. Anchored modes also accept occurrence starting at 1';
12022
12116
  var WORKFLOW_OPERATIONS_HELP = 'Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also extract_from_subflow, whose params carry {"subflowId":"<loop-id>"}, and insert_into_subflow, which creates a block and so takes an add’s params plus that subflowId';
12023
12117
  var WORKFLOW_SET_BLOCK_ENABLED_HELP = 'Blocks to enable or disable, applied after --operations: [{"block_id":"<uuid>","enabled":false}]. Disabling a loop or parallel cascades to its unlocked descendants; enabling a block whose container is disabled is declined';
12024
12118
  var WORKFLOW_VARIABLE_OPERATIONS_HELP = 'Variable changes to apply in order, keyed by operation: [{"operation":"add","name":"my_var","type":"string","value":"hello"},{"operation":"edit","name":"my_var","value":"updated"},{"operation":"delete","name":"my_var"}]';
@@ -12399,6 +12493,25 @@ var CLI_CONTRACT = {
12399
12493
  flags: { folderPaths: FOLDER_PATHS_FLAG },
12400
12494
  confirm: "This deletes every listed table and all of their rows."
12401
12495
  },
12496
+ searchFileContent: {
12497
+ flags: {
12498
+ folderPaths: {
12499
+ ...FOLDER_PATHS_FLAG,
12500
+ describe: "Folders to search, by path as shown in the app; omit to search the whole workspace"
12501
+ },
12502
+ includeSubfolders: {
12503
+ boolean: true,
12504
+ negatable: true,
12505
+ describe: "Whether each folder scope includes nested folders; on by default"
12506
+ }
12507
+ },
12508
+ itemsPath: "results",
12509
+ columns: [
12510
+ { header: "file", path: "fileId" },
12511
+ { header: "line", path: "lineNumber" },
12512
+ { header: "text" }
12513
+ ]
12514
+ },
12402
12515
  searchKnowledge: {
12403
12516
  flags: {
12404
12517
  knowledgeBaseIds: { name: "kb", list: true, describe: "Knowledge base ID (repeatable)" },
@@ -12818,6 +12931,13 @@ var CLI_CONTRACT = {
12818
12931
  folderPaths: FOLDER_PATHS_FLAG
12819
12932
  }
12820
12933
  },
12934
+ editFileContent: {
12935
+ command: "files edit",
12936
+ describe: "Apply one exact or anchor-based edit to a text file",
12937
+ flags: {
12938
+ edit: { describe: FILE_EDIT_HELP }
12939
+ }
12940
+ },
12821
12941
  updateFileContent: {
12822
12942
  command: "files set-content",
12823
12943
  describe: "Replace a file’s contents",
@@ -13531,7 +13651,7 @@ function withoutWireVocabulary(documented) {
13531
13651
  return documented.replace(WIRE_VOCABULARY_SENTENCE, " ").trim();
13532
13652
  }
13533
13653
  var NON_PAGINATED_LIMIT_HINT = " (caps a --filter match only; omit it to act on every match, and note 0 is not accepted)";
13534
- function addFieldOption(command, operation, field, descriptor, slot, paginates) {
13654
+ function addFieldOption(command, operation, field, descriptor, slot, paginates, capsAFilter) {
13535
13655
  if (field === PROFILE_INJECTED_FIELD || field === "cursor")
13536
13656
  return;
13537
13657
  const flag = flagSpecFor(operation, field);
@@ -13543,7 +13663,7 @@ function addFieldOption(command, operation, field, descriptor, slot, paginates)
13543
13663
  command.option("--limit <n>", "Maximum items to return (0 for everything)", String(DEFAULT_LIMIT));
13544
13664
  return;
13545
13665
  }
13546
- const documented = `${describeField(flag, descriptor, name, field)}${field === "limit" && (descriptor.kind === "number" || descriptor.kind === "integer") ? NON_PAGINATED_LIMIT_HINT : ""}`;
13666
+ const documented = `${describeField(flag, descriptor, name, field)}${capsAFilter && field === "limit" && (descriptor.kind === "number" || descriptor.kind === "integer") ? NON_PAGINATED_LIMIT_HINT : ""}`;
13547
13667
  if (descriptor.kind === "boolean" || flag.boolean) {
13548
13668
  const booleanDoc = withoutWireVocabulary(documented);
13549
13669
  if (descriptor.required) {
@@ -13591,13 +13711,14 @@ function addOperationOptions(command, operation, commandSpec, operationSpec) {
13591
13711
  command.addOption(new Option(`${short}--${name} <${flag.placeholder ?? "value"}>`, `${flag.describe ?? operationSpec.pathParamDocs?.[param] ?? `Set ${name.replaceAll("-", " ")}`} (required)`).makeOptionMandatory());
13592
13712
  }
13593
13713
  const paginates = cursorSlot(operationSpec) !== null;
13714
+ const capsAFilter = ["query", "body", "headers"].some((slot) => operationSpec[slot] !== undefined && ("filter" in operationSpec[slot]));
13594
13715
  for (const slot of ["query", "body", "headers"]) {
13595
13716
  for (const [field, descriptor] of Object.entries(operationSpec[slot] ?? {})) {
13596
13717
  if (commandSpec.requestFields && !commandSpec.requestFields.includes(field))
13597
13718
  continue;
13598
13719
  if (commandSpec.positionals?.includes(field))
13599
13720
  continue;
13600
- addFieldOption(command, operation, field, descriptor, slot, paginates);
13721
+ addFieldOption(command, operation, field, descriptor, slot, paginates, capsAFilter);
13601
13722
  }
13602
13723
  }
13603
13724
  if (commandSpec.allWorkspaces) {
@@ -16336,6 +16457,328 @@ function attachSecretCommands(program2) {
16336
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));
16337
16458
  }
16338
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
+
16339
16782
  // src/program.ts
16340
16783
  var PROGRAM_DESCRIPTION = "Talk to the Sim API from your terminal";
16341
16784
  var HELP_EPILOGUE = `
@@ -16415,6 +16858,7 @@ function buildProgram(options = {}) {
16415
16858
  attachProtocolCommands(program2);
16416
16859
  attachSecretCommands(program2);
16417
16860
  program2.addHelpText("after", HELP_EPILOGUE);
16861
+ program2.hook("preAction", () => announceUpdateIfAvailable());
16418
16862
  refuseHelpAfterUnknownCommand(program2);
16419
16863
  assertNoReservedProgramFlags(program2);
16420
16864
  return program2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sim",
3
- "version": "2.1.6-preview.83.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": {