@s-berdia/opencode-router 1.0.0 → 1.1.0

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.
package/README.md CHANGED
@@ -66,6 +66,10 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
66
66
 
67
67
  The router now selects models automatically. You do not have to do more.
68
68
 
69
+ OpenCode does not update plugins automatically. When a new version of the router is
70
+ available, the router shows a note. Then type `/routing-update` and restart OpenCode.
71
+ See [Update the router](./USAGE.md#12-update-the-router).
72
+
69
73
  ## Commands
70
74
 
71
75
  ### In OpenCode
@@ -76,6 +80,7 @@ The router now selects models automatically. You do not have to do more.
76
80
  | `/routing-status` | Show the model in use for each agent (after failover) and its 5-hour quota use |
77
81
  | `/routing-plan go-plus` | Save a plan profile (`go`, `go-plus`, or `free`) and apply it immediately |
78
82
  | `/routing-eval` | Send one test prompt to the `build` models and write a report |
83
+ | `/routing-update` | Look for a newer version and remove the old copy that OpenCode keeps. Then restart OpenCode |
79
84
 
80
85
  ### In a terminal
81
86
 
@@ -88,6 +93,7 @@ npx @s-berdia/opencode-router status # models in use and quota u
88
93
  npx @s-berdia/opencode-router plan go-plus # save the plan profile and rebuild
89
94
  npx @s-berdia/opencode-router cat # catalog file, date, model count
90
95
  npx @s-berdia/opencode-router install --scope global # write slash-command files
96
+ npx @s-berdia/opencode-router@latest update # remove OpenCode's old copy
91
97
  npx @s-berdia/opencode-router --help # all commands and options
92
98
  ```
93
99
 
@@ -140,6 +146,7 @@ or a value that is not valid, the router ignores it and shows a note in the scor
140
146
  | `blend` | `{ "input": 0.75, "output": 0.25, "cacheRead": 0.1 }` | Token mix used to calculate the cost of one million tokens |
141
147
  | `enduranceScale` | `{ "minTokensPer5h": 0.25, "maxTokensPer5h": 40 }` | Millions of tokens per 5 hours that give an endurance score of 0 and 100 |
142
148
  | `feeds` | `{ "enabled": false }` | Optional benchmark feed: `{ "enabled": true, "url": "https://...", "timeoutMs": 5000 }` |
149
+ | `updateCheck` | `true` | Once each 24 hours, ask npm for the newest router version. `false` stops the check |
143
150
 
144
151
  The [usage guide](./USAGE.md#6-change-the-router-settings) has examples for each key.
145
152
 
@@ -172,6 +179,7 @@ score = w_intelligence * intelligence
172
179
  | `~/.config/opencode/router/routing-table.json` | The last routing table |
173
180
  | `~/.config/opencode/router/last-apply.json` | The result of the last live apply |
174
181
  | `~/.config/opencode/router/eval-report.md` | The report of the last `/routing-eval` |
182
+ | `~/.config/opencode/router/update-check.json` | The time of the last update check and the newest version on npm |
175
183
  | `opencode.json` or `opencode.jsonc` (global, or the project file with `--scope project`) | `model`, `small_model`, and `agent.<role>.model` and `variant`. Comments and all other keys stay. A file with a JSON error is not changed |
176
184
 
177
185
  If `XDG_CONFIG_HOME` is set, the router uses `$XDG_CONFIG_HOME/opencode` in place of
@@ -183,6 +191,10 @@ Models with `-free` in their name can collect your prompts for training. The sco
183
191
  marks them `free*`. To keep your main work off these models, set
184
192
  `"privacy": { "freeModelsOk": false }`. To never use them, set `"denyModels": ["*-free"]`.
185
193
 
194
+ Once each 24 hours, the router sends one request to `registry.npmjs.org` to get the
195
+ newest version number of the router. It sends no other data. To stop this, set
196
+ `"updateCheck": false`.
197
+
186
198
  ## Development
187
199
 
188
200
  ```bash
package/USAGE.md CHANGED
@@ -16,7 +16,8 @@ Do the steps in sequence. Each command has a label that tells you which shell to
16
16
  9. [Files and folders](#9-files-and-folders)
17
17
  10. [Troubleshooting](#10-troubleshooting)
18
18
  11. [Known limits](#11-known-limits)
19
- 12. [Remove the router](#12-remove-the-router)
19
+ 12. [Update the router](#12-update-the-router)
20
+ 13. [Remove the router](#13-remove-the-router)
20
21
 
21
22
  ## 1. Before you start
22
23
 
@@ -119,8 +120,8 @@ Use this option to test changes to the code.
119
120
 
120
121
  1. In the OpenCode prompt, type `/` and look at the list of commands. Make sure that
121
122
  these commands are in the list: `/refresh-routing`, `/routing-status`,
122
- `/routing-plan`, and `/routing-eval`. If they are not in the list, go to
123
- [Troubleshooting](#10-troubleshooting).
123
+ `/routing-plan`, `/routing-eval`, and `/routing-update`. If they are not in the list,
124
+ go to [Troubleshooting](#10-troubleshooting).
124
125
  2. Type this command and push Enter:
125
126
 
126
127
  ```text
@@ -311,6 +312,19 @@ models of that family.
311
312
  }
312
313
  ```
313
314
 
315
+ ### Stop the update check
316
+
317
+ Once each 24 hours, the router asks npm for the newest version number of the router. It
318
+ sends one request to `registry.npmjs.org` and no other data. When a newer version is
319
+ available, the router shows a toast in OpenCode and a line under the status and the
320
+ scorecard. To stop the check:
321
+
322
+ ```json
323
+ {
324
+ "updateCheck": false
325
+ }
326
+ ```
327
+
314
328
  ### Advanced cost settings
315
329
 
316
330
  | Key | Default | Meaning |
@@ -356,6 +370,7 @@ These commands work in PowerShell, CMD, Git Bash, macOS, and Linux.
356
370
  | `opencode-router plan go-plus --scope global` | Save the plan in the global settings, then calculate and write the routing table |
357
371
  | `opencode-router cat` | Show the catalog path, its date, and the number of models |
358
372
  | `opencode-router install --scope global` | Write the slash commands as files in `~/.config/opencode/command/`. If `router.config.json` does not exist, it also writes an empty one |
373
+ | `opencode-router update` | Ask npm for the newest version. If the copy that OpenCode keeps is older, remove it. See [Update the router](#12-update-the-router) |
359
374
 
360
375
  The terminal tool reads the global `router.config.json` and the project file of the
361
376
  current folder, in the same way as the plugin.
@@ -436,6 +451,7 @@ The router writes these files:
436
451
  | `~/.config/opencode/router/routing-table.json` | Plugin | The last routing table |
437
452
  | `~/.config/opencode/router/last-apply.json` | Plugin | The result of the last live apply |
438
453
  | `~/.config/opencode/router/eval-report.md` | `/routing-eval` | The test report |
454
+ | `~/.config/opencode/router/update-check.json` | Plugin, `status`, `refresh` | The time of the last update check and the newest version on npm |
439
455
  | `~/.config/opencode/opencode.json` | `/refresh-routing`, `/routing-plan` | `model`, `small_model`, and `agent.<role>.model` and `variant` |
440
456
  | `~/.config/opencode/router.config.json` | `/routing-plan`, `install` (only if missing) | Your router settings |
441
457
 
@@ -466,7 +482,7 @@ Do not commit the `router/` folder. It contains your usage data.
466
482
 
467
483
  ## 11. Known limits
468
484
 
469
- These are the limits of version 1.0.0.
485
+ These are the limits of version 1.1.0.
470
486
 
471
487
  1. The router sets the model of the `plan`, `build`, `general`, and `explore` agents at
472
488
  each message. If you select a different model by hand, the router replaces it. To
@@ -483,7 +499,59 @@ These are the limits of version 1.0.0.
483
499
  rate-limit error for a full session puts only the model of that session into a
484
500
  cooldown.
485
501
 
486
- ## 12. Remove the router
502
+ ## 12. Update the router
503
+
504
+ OpenCode downloads the plugin one time and then uses its own copy. It does not download
505
+ a newer version automatically. The router looks for a newer version once each 24 hours
506
+ and tells you when one is available.
507
+
508
+ ### With the slash command
509
+
510
+ 1. In the OpenCode prompt, type this command and push Enter:
511
+
512
+ ```text
513
+ /routing-update
514
+ ```
515
+
516
+ 2. Read the result:
517
+ - `OpenCode has opencode-router <version>, the newest version.`: you do not have to
518
+ do more.
519
+ - `Removed the cached copy: ...`: go to step 3.
520
+ - `opencode.json pins an older version ...`: do the steps in
521
+ [With a fixed version](#with-a-fixed-version).
522
+ 3. Close OpenCode. Start OpenCode again. OpenCode downloads the newest version when it
523
+ starts.
524
+ 4. Type `/routing-status`. If the line `opencode-router <version> is available` does not
525
+ show, the update is complete.
526
+
527
+ Between steps 2 and 3, the router slash commands do not work, because the files that they
528
+ use are removed. Restart OpenCode immediately.
529
+
530
+ ### From a terminal
531
+
532
+ If the slash commands do not work, close OpenCode and run this command. Then start
533
+ OpenCode again.
534
+
535
+ ```bash
536
+ npx @s-berdia/opencode-router@latest update
537
+ ```
538
+
539
+ This command works in PowerShell, CMD, Git Bash, macOS, and Linux.
540
+
541
+ ### With a fixed version
542
+
543
+ To select the version yourself, write it in the `plugin` list:
544
+
545
+ ```json
546
+ {
547
+ "plugin": ["@s-berdia/opencode-router@1.1.0"]
548
+ }
549
+ ```
550
+
551
+ To update, change the number to the new version and restart OpenCode. The newest version
552
+ is on <https://www.npmjs.com/package/@s-berdia/opencode-router>.
553
+
554
+ ## 13. Remove the router
487
555
 
488
556
  1. Open `opencode.json` (see [step 2](#2-install-the-plugin)).
489
557
  2. Remove `"@s-berdia/opencode-router"` from the `plugin` list.
@@ -509,5 +577,6 @@ These are the limits of version 1.0.0.
509
577
  ```
510
578
 
511
579
  6. If you ran `install`, delete these files from `~/.config/opencode/command/`:
512
- `refresh-routing.md`, `routing-status.md`, `routing-plan.md`, and `routing-eval.md`.
580
+ `refresh-routing.md`, `routing-status.md`, `routing-plan.md`, `routing-eval.md`, and
581
+ `routing-update.md`.
513
582
  7. Restart OpenCode.
package/dist/bin.js CHANGED
@@ -822,12 +822,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
822
822
  }
823
823
  return result;
824
824
  }
825
- function findNodeAtLocation(root, path5) {
825
+ function findNodeAtLocation(root, path6) {
826
826
  if (!root) {
827
827
  return void 0;
828
828
  }
829
829
  let node = root;
830
- for (let segment of path5) {
830
+ for (let segment of path6) {
831
831
  if (typeof segment === "string") {
832
832
  if (node.type !== "object" || !Array.isArray(node.children)) {
833
833
  return void 0;
@@ -1181,14 +1181,14 @@ function getNodeType(value) {
1181
1181
 
1182
1182
  // node_modules/jsonc-parser/lib/esm/impl/edit.js
1183
1183
  function setProperty(text, originalPath, value, options) {
1184
- const path5 = originalPath.slice();
1184
+ const path6 = originalPath.slice();
1185
1185
  const errors = [];
1186
1186
  const root = parseTree(text, errors);
1187
1187
  let parent = void 0;
1188
1188
  let lastSegment = void 0;
1189
- while (path5.length > 0) {
1190
- lastSegment = path5.pop();
1191
- parent = findNodeAtLocation(root, path5);
1189
+ while (path6.length > 0) {
1190
+ lastSegment = path6.pop();
1191
+ parent = findNodeAtLocation(root, path6);
1192
1192
  if (parent === void 0 && value !== void 0) {
1193
1193
  if (typeof lastSegment === "string") {
1194
1194
  value = { [lastSegment]: value };
@@ -1413,8 +1413,8 @@ function printParseErrorCode(code) {
1413
1413
  }
1414
1414
  return "<unknown ParseErrorCode>";
1415
1415
  }
1416
- function modify(text, path5, value, options) {
1417
- return setProperty(text, path5, value, options);
1416
+ function modify(text, path6, value, options) {
1417
+ return setProperty(text, path6, value, options);
1418
1418
  }
1419
1419
  function applyEdits(text, edits) {
1420
1420
  let sortedEdits = edits.slice(0).sort((a, b) => {
@@ -1443,10 +1443,13 @@ function globalConfigDir() {
1443
1443
  const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
1444
1444
  return path.join(base, "opencode");
1445
1445
  }
1446
- function catalogCacheFile() {
1446
+ function opencodeCacheDir() {
1447
1447
  const xdg = process.env.XDG_CACHE_HOME;
1448
1448
  const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
1449
- return path.join(base, "opencode", "models.json");
1449
+ return path.join(base, "opencode");
1450
+ }
1451
+ function catalogCacheFile() {
1452
+ return path.join(opencodeCacheDir(), "models.json");
1450
1453
  }
1451
1454
  function globalRouterConfigFile() {
1452
1455
  return path.join(globalConfigDir(), "router.config.json");
@@ -1814,6 +1817,7 @@ function sanitizeUserConfig(raw, file, notes) {
1814
1817
  if (threshold !== void 0 && !(typeof threshold === "number" && threshold > 0 && threshold <= 1)) {
1815
1818
  drop("softThreshold", "must be a number above 0 and at most 1");
1816
1819
  }
1820
+ if (config.updateCheck !== void 0 && typeof config.updateCheck !== "boolean") drop("updateCheck", "must be true or false");
1817
1821
  for (const key of ["denyModels", "allowModels"]) {
1818
1822
  const list = config[key];
1819
1823
  if (list !== void 0 && !(Array.isArray(list) && list.every((item) => typeof item === "string"))) {
@@ -2542,8 +2546,10 @@ var SLASH_COMMANDS = [
2542
2546
  { name: "refresh-routing", description: "Recompute and apply OpenCode model routing", args: "refresh" },
2543
2547
  { name: "routing-status", description: "Show the model in use per role and its 5-hour quota use", args: "status" },
2544
2548
  { name: "routing-plan", description: "Switch the routing plan (go | go-plus | free) and rebuild", args: "plan $1" },
2545
- { name: "routing-eval", description: "Run the router scoring stress test", args: "eval" }
2549
+ { name: "routing-eval", description: "Run the router scoring stress test", args: "eval" },
2550
+ { name: "routing-update", description: "Update opencode-router to the newest version", args: "update" }
2546
2551
  ];
2552
+ var OUTPUT_BEGIN = "---BEGIN ROUTER OUTPUT---";
2547
2553
  function cliScriptPath() {
2548
2554
  return fileURLToPath(new URL("./bin.js", import.meta.url)).replace(/\\/g, "/");
2549
2555
  }
@@ -2556,7 +2562,7 @@ function commandTemplate(args) {
2556
2562
  "2. Present the text between the markers to the user exactly as written.",
2557
2563
  "3. Do not summarize, reformat, shorten, or reorganize it.",
2558
2564
  "",
2559
- "---BEGIN ROUTER OUTPUT---",
2565
+ OUTPUT_BEGIN,
2560
2566
  `!\`node "${cliScriptPath()}" ${args} --scope global\``,
2561
2567
  "---END ROUTER OUTPUT---"
2562
2568
  ].join("\n");
@@ -2593,6 +2599,9 @@ function ledgerFile() {
2593
2599
  function evalReportFile() {
2594
2600
  return path4.join(stateDir(), "eval-report.md");
2595
2601
  }
2602
+ function updateCheckFile() {
2603
+ return path4.join(stateDir(), "update-check.json");
2604
+ }
2596
2605
  function applyMarkerFile() {
2597
2606
  return path4.join(stateDir(), "last-apply.json");
2598
2607
  }
@@ -2739,6 +2748,97 @@ async function runEval(opts = {}) {
2739
2748
  return 0;
2740
2749
  }
2741
2750
 
2751
+ // src/update.ts
2752
+ import fs6 from "fs";
2753
+ import path5 from "path";
2754
+ import { fileURLToPath as fileURLToPath2 } from "url";
2755
+ var PACKAGE_NAME = "@s-berdia/opencode-router";
2756
+ var LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2f")}/latest`;
2757
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2758
+ var TIMEOUT_MS = 3e3;
2759
+ function parseVersion(version) {
2760
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
2761
+ return match ? match.slice(1).map(Number) : null;
2762
+ }
2763
+ function isNewerVersion(a, b) {
2764
+ const left = parseVersion(a);
2765
+ const right = parseVersion(b);
2766
+ if (!left || !right) return false;
2767
+ for (let i = 0; i < 3; i++) {
2768
+ if (left[i] !== right[i]) return left[i] > right[i];
2769
+ }
2770
+ return false;
2771
+ }
2772
+ function installedVersion() {
2773
+ const file = fileURLToPath2(new URL("../package.json", import.meta.url));
2774
+ return readJson(file)?.version ?? "0.0.0";
2775
+ }
2776
+ async function fetchLatestVersion() {
2777
+ const controller = new AbortController();
2778
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
2779
+ try {
2780
+ const res = await fetch(LATEST_URL, { signal: controller.signal, headers: { accept: "application/json" } });
2781
+ if (!res.ok) return void 0;
2782
+ const body = await res.json();
2783
+ return typeof body.version === "string" ? body.version : void 0;
2784
+ } catch {
2785
+ return void 0;
2786
+ } finally {
2787
+ clearTimeout(timeout);
2788
+ }
2789
+ }
2790
+ async function checkForUpdate(opts = {}) {
2791
+ try {
2792
+ if (loadUserConfig(opts.projectDir).config.updateCheck === false) return null;
2793
+ const current = opts.current ?? installedVersion();
2794
+ const now = opts.now ?? Date.now();
2795
+ const state = readJson(updateCheckFile());
2796
+ let latest = state?.latest;
2797
+ const fresh = state !== void 0 && now >= state.checkedAt && now - state.checkedAt < CHECK_INTERVAL_MS;
2798
+ if (!fresh) {
2799
+ latest = await fetchLatestVersion() ?? latest;
2800
+ writeJson(updateCheckFile(), { checkedAt: now, latest });
2801
+ }
2802
+ return latest && isNewerVersion(latest, current) ? { current, latest } : null;
2803
+ } catch {
2804
+ return null;
2805
+ }
2806
+ }
2807
+ function updateNotice(update) {
2808
+ return `opencode-router ${update.latest} is available (installed: ${update.current}). To update, type /routing-update in OpenCode.`;
2809
+ }
2810
+ function clearOutdatedCopies(latest) {
2811
+ const [scope, name] = PACKAGE_NAME.split("/");
2812
+ const scopeDir = path5.join(opencodeCacheDir(), "packages", scope);
2813
+ const result = { removed: [], pinned: [], current: [], failed: [] };
2814
+ let entries = [];
2815
+ try {
2816
+ entries = fs6.readdirSync(scopeDir);
2817
+ } catch {
2818
+ return result;
2819
+ }
2820
+ for (const entry of entries) {
2821
+ const dir = path5.join(scopeDir, entry);
2822
+ if (entry === name || entry === `${name}@latest`) {
2823
+ const manifest = path5.join(dir, "node_modules", scope, name, "package.json");
2824
+ const version = readJson(manifest)?.version ?? "0.0.0";
2825
+ if (!isNewerVersion(latest, version)) {
2826
+ result.current.push(dir);
2827
+ continue;
2828
+ }
2829
+ try {
2830
+ fs6.rmSync(dir, { recursive: true, force: true, maxRetries: 3 });
2831
+ result.removed.push(dir);
2832
+ } catch (error) {
2833
+ result.failed.push({ dir, error: error instanceof Error ? error.message : String(error) });
2834
+ }
2835
+ } else if (entry.startsWith(`${name}@`) && isNewerVersion(latest, entry.slice(name.length + 1))) {
2836
+ result.pinned.push(entry);
2837
+ }
2838
+ }
2839
+ return result;
2840
+ }
2841
+
2742
2842
  // src/cli.ts
2743
2843
  var HELP = `opencode-router - dynamic model routing for OpenCode Go / Go Plus / Free
2744
2844
 
@@ -2749,6 +2849,7 @@ Usage:
2749
2849
  opencode-router install [--scope] Write slash-command files and a starter config
2750
2850
  opencode-router cat Show catalog metadata (path, date, model count)
2751
2851
  opencode-router eval Run the scoring stress test
2852
+ opencode-router update Look for a newer version and clear OpenCode's cached copy
2752
2853
 
2753
2854
  Options:
2754
2855
  --plan <go|go-plus|free> Plan profile for this run
@@ -2813,6 +2914,12 @@ function parseCommandLine(argv, cwd) {
2813
2914
  function build(opts) {
2814
2915
  return buildRoutingTable({ plan: opts.plan, catalogFile: opts.catalog, projectDir: opts.cwd });
2815
2916
  }
2917
+ async function writeUpdateNotice(opts) {
2918
+ const update = await checkForUpdate({ projectDir: opts.cwd });
2919
+ if (update) process.stdout.write(`
2920
+ ${updateNotice(update)}
2921
+ `);
2922
+ }
2816
2923
  async function cmdRefresh(opts) {
2817
2924
  const { table } = await build(opts);
2818
2925
  if (opts.json) {
@@ -2844,6 +2951,7 @@ Applied: ${detail}
2844
2951
  "A running OpenCode session recomputes routing on its next message (within 15 s). Restart to reload the UI display now.\n"
2845
2952
  );
2846
2953
  }
2954
+ await writeUpdateNotice(opts);
2847
2955
  }
2848
2956
  return 0;
2849
2957
  }
@@ -2857,6 +2965,7 @@ async function cmdStatus(opts) {
2857
2965
  process.stdout.write(JSON.stringify({ table: result.table, rows, usage }, null, 2) + "\n");
2858
2966
  } else {
2859
2967
  process.stdout.write(formatStatus(result.table, rows) + "\n");
2968
+ await writeUpdateNotice(opts);
2860
2969
  }
2861
2970
  return 0;
2862
2971
  }
@@ -2900,6 +3009,44 @@ function cmdInstall(opts) {
2900
3009
  );
2901
3010
  return 0;
2902
3011
  }
3012
+ async function cmdUpdate() {
3013
+ const latest = await fetchLatestVersion();
3014
+ if (!latest) {
3015
+ process.stderr.write("error: could not reach the npm registry. Try again later.\n");
3016
+ return 1;
3017
+ }
3018
+ const { removed, pinned, current, failed } = clearOutdatedCopies(latest);
3019
+ if (failed.length > 0) {
3020
+ for (const { dir, error } of failed) process.stderr.write(`error: could not remove ${dir} (${error})
3021
+ `);
3022
+ process.stderr.write(`Close OpenCode, then run this command in a terminal: npx ${PACKAGE_NAME}@latest update
3023
+ `);
3024
+ return 1;
3025
+ }
3026
+ for (const dir of removed) process.stdout.write(`Removed the cached copy: ${dir}
3027
+ `);
3028
+ if (removed.length > 0) {
3029
+ process.stdout.write(`Restart OpenCode. It then installs opencode-router ${latest}.
3030
+ `);
3031
+ }
3032
+ if (pinned.length > 0) {
3033
+ process.stdout.write(
3034
+ `opencode.json pins an older version (${pinned.join(", ")}). Change the plugin entry to "${PACKAGE_NAME}@${latest}", then restart OpenCode.
3035
+ `
3036
+ );
3037
+ }
3038
+ if (removed.length > 0 || pinned.length > 0) return 0;
3039
+ if (current.length > 0) {
3040
+ process.stdout.write(`OpenCode has opencode-router ${latest}, the newest version.
3041
+ `);
3042
+ } else {
3043
+ process.stdout.write(
3044
+ `OpenCode has no cached copy of opencode-router. The newest version is ${latest}. If you use a local build, run git pull and npm run build. If your OpenCode has the command "opencode plugin update", run it.
3045
+ `
3046
+ );
3047
+ }
3048
+ return 0;
3049
+ }
2903
3050
  function usageError(error) {
2904
3051
  process.stderr.write(`error: ${error.message}
2905
3052
 
@@ -2927,6 +3074,8 @@ async function main(argv, cwd = process.cwd()) {
2927
3074
  return cmdInstall(opts);
2928
3075
  case "eval":
2929
3076
  return await runEval({ plan: opts.plan, catalog: opts.catalog, json: opts.json, projectDir: opts.cwd });
3077
+ case "update":
3078
+ return await cmdUpdate();
2930
3079
  default:
2931
3080
  throw new UsageError(`unknown command "${command}"`);
2932
3081
  }