@elmohq/cli 0.2.13 → 0.2.15

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 +1 -0
  2. package/dist/index.js +306 -25
  3. package/package.json +10 -8
package/README.md CHANGED
@@ -60,6 +60,7 @@ For the full self-hosting walkthrough, see the [Elmo docs](https://www.elmohq.co
60
60
  | `elmo init` | Interactive wizard to set up a local Elmo instance |
61
61
  | `elmo compose <args...>` | Run any `docker compose` command against your Elmo project (e.g. `elmo compose up -d`, `elmo compose down`, `elmo compose logs -f`, `elmo compose build`, `elmo compose ps`) |
62
62
  | `elmo edit <env\|compose>` | Change API keys, scrape targets, or the Docker Compose YAML |
63
+ | `elmo upgrade` | Move your deployment to this CLI's version — runs migrations, re-pins image tags, and restarts the stack |
63
64
 
64
65
  Run `elmo --help` or `elmo <command> --help` for the full list of flags.
65
66
 
package/dist/index.js CHANGED
@@ -49,6 +49,51 @@ function formatScrapeTarget(config) {
49
49
  return parts.join(":");
50
50
  }
51
51
  //#endregion
52
+ //#region src/compose-pin.ts
53
+ function renderedByHeader(version) {
54
+ return [`# Rendered by elmo ${version} on ${(/* @__PURE__ */ new Date()).toISOString()}`, "# Run `elmo upgrade` after upgrading the CLI to refresh this file."].join("\n");
55
+ }
56
+ function parseRenderedVersion(contents) {
57
+ const match = contents.match(/^# Rendered by elmo (\S+) on /m);
58
+ return match ? match[1] : null;
59
+ }
60
+ function refreshHeaderVersion(contents, version) {
61
+ if (!/^# Rendered by elmo \S+ on /m.test(contents)) return `${renderedByHeader(version)}\n${contents}`;
62
+ return contents.replace(/^# Rendered by elmo \S+ on .*$/m, `# Rendered by elmo ${version} on ${(/* @__PURE__ */ new Date()).toISOString()}`);
63
+ }
64
+ function repinImages(contents, version) {
65
+ return contents.replace(/(image:\s*elmohq\/elmo-[a-z-]+):\S+/g, `$1:${version}`);
66
+ }
67
+ //#endregion
68
+ //#region src/migrations/planner.ts
69
+ function planMigrations(from, to, all) {
70
+ if (!semver.valid(from)) throw new Error(`Invalid from version: ${from}`);
71
+ if (!semver.valid(to)) throw new Error(`Invalid to version: ${to}`);
72
+ if (semver.gt(from, to)) throw new Error(`Downgrade not supported: ${from} -> ${to}`);
73
+ if (semver.eq(from, to)) return [];
74
+ for (const m of all) {
75
+ if (!semver.valid(m.from)) throw new Error(`Migration has invalid from: ${m.from}`);
76
+ if (!semver.valid(m.to)) throw new Error(`Migration has invalid to: ${m.to}`);
77
+ if (!semver.gt(m.to, m.from)) throw new Error(`Migration ${m.from} -> ${m.to}: 'to' must be greater than 'from'`);
78
+ }
79
+ const applicable = all.filter((m) => semver.gte(m.from, from) && semver.lt(m.from, to)).slice().sort((a, b) => semver.compare(a.from, b.from));
80
+ const seen = /* @__PURE__ */ new Set();
81
+ for (const m of applicable) {
82
+ if (seen.has(m.from)) throw new Error(`Multiple migrations registered with from=${m.from}`);
83
+ seen.add(m.from);
84
+ }
85
+ return applicable;
86
+ }
87
+ async function runMigrations(plan, ctx) {
88
+ for (const migration of plan) {
89
+ ctx.log.step(`Migrating ${migration.from} → ${migration.to}: ${migration.description}`);
90
+ await migration.run(ctx);
91
+ }
92
+ }
93
+ //#endregion
94
+ //#region src/migrations/index.ts
95
+ const MIGRATIONS = [];
96
+ //#endregion
52
97
  //#region src/telemetry.ts
53
98
  const POSTHOG_PUBLIC_KEY = "phc_Jhx9LnI9cTDFHpQmpOzJSDTW127qD9pFU65KRnYym6z";
54
99
  const POSTHOG_HOST = "https://us.i.posthog.com";
@@ -156,6 +201,9 @@ async function main() {
156
201
  program.command("edit").description("change API keys, scrape targets, or the Docker Compose YAML").argument("<env|compose>", "which config file to edit").action(async (target, _opts, cmd) => {
157
202
  await runEdit(target, cmd.optsWithGlobals());
158
203
  });
204
+ program.command("upgrade").description("upgrade your Elmo deployment to this CLI's version").option("--yes", "skip confirmation prompts").action(async (_opts, cmd) => {
205
+ await runUpgrade(cmd.optsWithGlobals(), version);
206
+ });
159
207
  await program.parseAsync(process.argv);
160
208
  }
161
209
  async function withVersionCheck(version, fn) {
@@ -320,7 +368,7 @@ async function runInit(options, version) {
320
368
  postgres_mode: postgresMode,
321
369
  dev_mode: Boolean(options.dev),
322
370
  setup_mode: setupMode,
323
- has_scraper: Boolean(env.BRIGHTDATA_API_TOKEN || env.OLOSTEP_API_KEY),
371
+ has_scraper: Boolean(env.BRIGHTDATA_API_TOKEN || env.OLOSTEP_API_KEY || env.OXYLABS_USERNAME),
324
372
  has_direct_api: hasDirectApiConfigured(env)
325
373
  });
326
374
  if (email) await submitNewsletterSignup(configDir, email);
@@ -329,6 +377,7 @@ async function runInit(options, version) {
329
377
  }
330
378
  const BRIGHTDATA_AFFILIATE = "https://get.brightdata.com/67h1b7h0shcn";
331
379
  const OLOSTEP_AFFILIATE = "https://olostep.com/?ref=elmo";
380
+ const OXYLABS_AFFILIATE = "https://oxylabs.go2cloud.org/aff_c?offer_id=7&aff_id=2263&url_id=32";
332
381
  const PROVIDERS_DOC_URL = "https://docs.elmohq.com/docs/user-guide/providers";
333
382
  const BRIGHTDATA_MODELS = [
334
383
  "chatgpt",
@@ -347,9 +396,20 @@ const OLOSTEP_MODELS = [
347
396
  "gemini",
348
397
  "grok"
349
398
  ];
399
+ const OXYLABS_MODELS = [
400
+ "chatgpt",
401
+ "google-ai-mode",
402
+ "perplexity"
403
+ ];
350
404
  const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
405
+ const DATAFORSEO_MODELS = [
406
+ "google-ai-mode",
407
+ "chatgpt",
408
+ "perplexity",
409
+ "gemini"
410
+ ];
351
411
  const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
352
- const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
412
+ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
353
413
  const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
354
414
  const DEFAULT_MISTRAL_MODEL = "mistral-medium-latest";
355
415
  async function configureProvidersInteractive(env) {
@@ -358,6 +418,7 @@ async function configureProvidersInteractive(env) {
358
418
  "",
359
419
  pc.bold("1. A scraper") + " — to track ChatGPT and Google AI Mode (no public APIs):",
360
420
  ` • ${pc.cyan("BrightData")} — cheap solid option, ~$0.45/mo per prompt`,
421
+ ` • ${pc.cyan("Oxylabs")} — sync realtime API, pay-as-you-go`,
361
422
  ` • ${pc.cyan("Olostep")} — premium option, powers Peec/AirOps, ~$2.25/mo per prompt`,
362
423
  "",
363
424
  pc.bold("2. A direct LLM API") + " — for low-latency tasks (onboarding analysis, sentiment scoring,",
@@ -387,13 +448,20 @@ async function configureProvidersRecommended(env) {
387
448
  const targets = [];
388
449
  const scraper = await p.select({
389
450
  message: "Scraper (tracks ChatGPT + Google AI Mode)",
390
- options: [{
391
- value: "brightdata",
392
- label: "BrightData — ~$0.45/mo per prompt (cheaper)"
393
- }, {
394
- value: "olostep",
395
- label: "Olostep — ~$2.25/mo per prompt (premium)"
396
- }],
451
+ options: [
452
+ {
453
+ value: "brightdata",
454
+ label: "BrightData — ~$0.45/mo per prompt (cheaper)"
455
+ },
456
+ {
457
+ value: "oxylabs",
458
+ label: "Oxylabs — sync realtime API, pay-as-you-go"
459
+ },
460
+ {
461
+ value: "olostep",
462
+ label: "Olostep — ~$2.25/mo per prompt (premium)"
463
+ }
464
+ ],
397
465
  initialValue: "brightdata"
398
466
  });
399
467
  assertNotCancelled(scraper);
@@ -441,6 +509,7 @@ async function configureProvidersCustom(env) {
441
509
  }
442
510
  p.log.step(pc.bold("Step 2 of 2 — Scrapers (optional, but needed to track ChatGPT / Google AI Mode)"));
443
511
  await collectBrightData(env, targets);
512
+ await collectOxylabs(env, targets);
444
513
  await collectOlostep(env, targets);
445
514
  await collectDataForSEO(env, targets);
446
515
  await finalizeScrapeTargets(env, targets);
@@ -457,6 +526,20 @@ async function collectScraperKey(scraper, env) {
457
526
  });
458
527
  assertNotCancelled(key);
459
528
  env.BRIGHTDATA_API_TOKEN = key;
529
+ } else if (scraper === "oxylabs") {
530
+ p.log.info(`Sign up: ${link(pc.cyan(OXYLABS_AFFILIATE), OXYLABS_AFFILIATE)}`);
531
+ const username = await p.text({
532
+ message: "Oxylabs username",
533
+ validate: (v) => !v ? "Required" : void 0
534
+ });
535
+ assertNotCancelled(username);
536
+ env.OXYLABS_USERNAME = username;
537
+ const password = await p.password({
538
+ message: "Oxylabs password",
539
+ validate: (v) => !v ? "Required" : void 0
540
+ });
541
+ assertNotCancelled(password);
542
+ env.OXYLABS_PASSWORD = password;
460
543
  } else {
461
544
  p.log.info(`Sign up: ${link(pc.cyan(OLOSTEP_AFFILIATE), OLOSTEP_AFFILIATE)}`);
462
545
  const key = await p.password({
@@ -540,6 +623,33 @@ async function collectOlostep(env, targets) {
540
623
  targets
541
624
  });
542
625
  }
626
+ async function collectOxylabs(env, targets) {
627
+ const enable = await p.confirm({
628
+ message: `Configure ${pc.bold("Oxylabs")}? (sync realtime API, pay-as-you-go)`,
629
+ initialValue: false
630
+ });
631
+ assertNotCancelled(enable);
632
+ if (!enable) return;
633
+ p.log.info(`Sign up and create Web Scraper API credentials: ${link(pc.cyan(OXYLABS_AFFILIATE), OXYLABS_AFFILIATE)}`);
634
+ const username = await p.text({
635
+ message: "Oxylabs username",
636
+ validate: (v) => !v ? "Required" : void 0
637
+ });
638
+ assertNotCancelled(username);
639
+ env.OXYLABS_USERNAME = username;
640
+ const password = await p.password({
641
+ message: "Oxylabs password",
642
+ validate: (v) => !v ? "Required" : void 0
643
+ });
644
+ assertNotCancelled(password);
645
+ env.OXYLABS_PASSWORD = password;
646
+ await pickScraperTargets({
647
+ providerLabel: "Oxylabs",
648
+ providerId: "oxylabs",
649
+ allModels: OXYLABS_MODELS,
650
+ targets
651
+ });
652
+ }
543
653
  async function pickScraperTargets(args) {
544
654
  const selected = await p.multiselect({
545
655
  message: `LLM Providers to track via ${args.providerLabel}`,
@@ -687,7 +797,7 @@ async function collectOpenRouter(env, targets) {
687
797
  }
688
798
  async function collectDataForSEO(env, targets) {
689
799
  const enable = await p.confirm({
690
- message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode scraping)`,
800
+ message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode + LLM Responses)`,
691
801
  initialValue: false
692
802
  });
693
803
  assertNotCancelled(enable);
@@ -704,13 +814,18 @@ async function collectDataForSEO(env, targets) {
704
814
  });
705
815
  assertNotCancelled(pwd);
706
816
  env.DATAFORSEO_PASSWORD = pwd;
707
- const addTarget = await p.confirm({
708
- message: "Also scrape Google AI Mode via DataForSEO? (google-ai-mode:dataforseo:online)",
709
- initialValue: false
817
+ const selected = await p.multiselect({
818
+ message: "LLM Providers to track via DataForSEO",
819
+ options: DATAFORSEO_MODELS.map((model) => ({
820
+ value: model,
821
+ label: model
822
+ })),
823
+ required: false,
824
+ initialValues: ["google-ai-mode"]
710
825
  });
711
- assertNotCancelled(addTarget);
712
- if (addTarget) targets.push(formatScrapeTarget({
713
- model: "google-ai-mode",
826
+ assertNotCancelled(selected);
827
+ for (const model of selected) targets.push(formatScrapeTarget({
828
+ model,
714
829
  provider: "dataforseo",
715
830
  webSearch: true
716
831
  }));
@@ -817,6 +932,170 @@ async function runEdit(target, options) {
817
932
  });
818
933
  log.info("Restart the stack with `elmo compose up -d` to apply changes.");
819
934
  }
935
+ async function runUpgrade(options, cliVersion) {
936
+ printBanner();
937
+ p.intro(pc.bold("Upgrading Elmo"));
938
+ const latestCli = await fetchLatestCliVersion();
939
+ if (latestCli && semver.valid(cliVersion) && semver.lt(cliVersion, latestCli)) {
940
+ log.warn(`Your CLI (${cliVersion}) is behind the latest published version (${latestCli}).`);
941
+ log.info("Recommended: upgrade the CLI first, then rerun this command:");
942
+ console.log(` ${pc.bold("npm install -g @elmohq/cli@latest")}`);
943
+ const proceed = options.yes ? true : await p.confirm({
944
+ message: `Continue upgrading the stack with CLI ${cliVersion} anyway?`,
945
+ initialValue: false
946
+ });
947
+ assertNotCancelled(proceed);
948
+ if (!proceed) {
949
+ p.cancel("Upgrade cancelled. Upgrade the CLI and rerun `elmo upgrade`.");
950
+ process.exit(0);
951
+ }
952
+ }
953
+ const configDir = await resolveConfigDir(options.dir);
954
+ const composePath = path.join(configDir, "elmo.yaml");
955
+ const detectedVersion = await readRenderedVersion(composePath);
956
+ const fromVersion = detectedVersion ?? cliVersion;
957
+ if (!semver.valid(fromVersion)) throw new Error(`Could not determine the installed version from ${composePath}.`);
958
+ if (semver.gt(fromVersion, cliVersion)) {
959
+ log.warn(`Your deployment (${fromVersion}) is newer than this CLI (${cliVersion}).`);
960
+ log.info("Upgrade the CLI to match, then rerun:");
961
+ console.log(` ${pc.bold("npm install -g @elmohq/cli@latest")}`);
962
+ process.exit(1);
963
+ }
964
+ if (detectedVersion !== null && semver.eq(detectedVersion, cliVersion)) {
965
+ log.success(`Already at ${cliVersion}.`);
966
+ const pull = options.yes ? true : await p.confirm({
967
+ message: "Pull images for this version anyway?",
968
+ initialValue: false
969
+ });
970
+ assertNotCancelled(pull);
971
+ if (pull) {
972
+ assertDockerRunning();
973
+ const wasRunning = await stackHasRunningServices(configDir);
974
+ log.step("Pulling images...");
975
+ await runDockerCompose(configDir, ["pull"]);
976
+ if (wasRunning) {
977
+ log.step("Restarting services...");
978
+ await runDockerCompose(configDir, ["up", "-d"]);
979
+ }
980
+ }
981
+ p.outro(pc.green("Nothing to upgrade."));
982
+ return;
983
+ }
984
+ const plan = detectedVersion === null ? [] : planMigrations(fromVersion, cliVersion, MIGRATIONS);
985
+ if (detectedVersion === null) log.warn(`Couldn't detect the deployment's version — re-pinning images to ${pc.cyan(cliVersion)}.`);
986
+ else log.info(`Upgrading from ${pc.cyan(fromVersion)} → ${pc.cyan(cliVersion)}`);
987
+ if (plan.length === 0) log.step("No migrations to run (docker images will be re-pinned and pulled).");
988
+ else {
989
+ log.step(`Migrations to apply: ${plan.length}`);
990
+ for (const m of plan) console.log(` • ${pc.bold(`${m.from} → ${m.to}`)} ${m.description}`);
991
+ }
992
+ const confirm = options.yes ? true : await p.confirm({
993
+ message: "Proceed with upgrade?",
994
+ initialValue: true
995
+ });
996
+ assertNotCancelled(confirm);
997
+ if (!confirm) {
998
+ p.cancel("Upgrade cancelled.");
999
+ process.exit(0);
1000
+ }
1001
+ assertDockerRunning();
1002
+ const wasRunning = await stackHasRunningServices(configDir);
1003
+ if (wasRunning) {
1004
+ log.step("Stopping services...");
1005
+ await runDockerCompose(configDir, ["down"]);
1006
+ }
1007
+ const ctx = buildMigrationContext(configDir, cliVersion);
1008
+ try {
1009
+ await runMigrations(plan, ctx);
1010
+ } catch (error) {
1011
+ const msg = error instanceof Error ? error.message : String(error);
1012
+ log.error(`Migration failed: ${msg}`);
1013
+ log.info("Your config version was left unchanged. Fix the issue and rerun `elmo upgrade`.");
1014
+ if (wasRunning) log.info("The stack was stopped for the upgrade. Restart with `elmo compose up -d` after fixing.");
1015
+ process.exit(1);
1016
+ }
1017
+ const isDev = await composeUsesBuild(composePath);
1018
+ await repinComposeImages(composePath, cliVersion);
1019
+ await refreshRenderedVersion(path.join(configDir, ".env"), cliVersion);
1020
+ log.success(`Pinned config to ${cliVersion}.`);
1021
+ if (isDev) log.info("Dev mode detected — rebuild with `elmo compose build` to apply the new version.");
1022
+ else {
1023
+ log.step("Pulling images...");
1024
+ await runDockerCompose(configDir, ["pull"]);
1025
+ }
1026
+ if (wasRunning) {
1027
+ log.step("Starting services...");
1028
+ await runDockerCompose(configDir, ["up", "-d"]);
1029
+ const s = p.spinner();
1030
+ s.start("Waiting for services to become healthy...");
1031
+ if (await waitForHealthy(configDir, 18e4)) s.stop("All services healthy!");
1032
+ else {
1033
+ s.stop("Health check timed out.");
1034
+ log.warn("Some services did not report healthy status.");
1035
+ }
1036
+ } else log.info("Stack was stopped before upgrade — leaving it stopped. Start with `elmo compose up -d`.");
1037
+ await trackCliEvent(configDir, "cli_upgrade", {
1038
+ from_version: fromVersion,
1039
+ to_version: cliVersion,
1040
+ migrations_run: plan.length,
1041
+ was_running: wasRunning,
1042
+ dev_mode: isDev
1043
+ });
1044
+ p.outro(pc.green(`Upgraded to ${cliVersion}.`));
1045
+ }
1046
+ async function stackHasRunningServices(configDir) {
1047
+ try {
1048
+ return (await getComposeServices(configDir)).some((s) => s.State?.startsWith("running") ?? false);
1049
+ } catch {
1050
+ return false;
1051
+ }
1052
+ }
1053
+ async function readRenderedVersion(filePath) {
1054
+ try {
1055
+ return parseRenderedVersion(await fs.readFile(filePath, "utf8"));
1056
+ } catch {
1057
+ return null;
1058
+ }
1059
+ }
1060
+ async function composeUsesBuild(composePath) {
1061
+ try {
1062
+ const contents = await fs.readFile(composePath, "utf8");
1063
+ return /^\s*build:/m.test(contents);
1064
+ } catch {
1065
+ return false;
1066
+ }
1067
+ }
1068
+ async function repinComposeImages(composePath, version) {
1069
+ const contents = await fs.readFile(composePath, "utf8");
1070
+ await fs.writeFile(composePath, refreshHeaderVersion(repinImages(contents, version), version), "utf8");
1071
+ }
1072
+ async function refreshRenderedVersion(filePath, version) {
1073
+ try {
1074
+ const contents = await fs.readFile(filePath, "utf8");
1075
+ await fs.writeFile(filePath, refreshHeaderVersion(contents, version), "utf8");
1076
+ } catch {}
1077
+ }
1078
+ function buildMigrationContext(configDir, version) {
1079
+ const envPath = path.join(configDir, ".env");
1080
+ return {
1081
+ configDir,
1082
+ log: {
1083
+ info: (msg) => log.info(msg),
1084
+ warn: (msg) => log.warn(msg),
1085
+ step: (msg) => log.step(msg)
1086
+ },
1087
+ readEnv: async () => {
1088
+ try {
1089
+ return parse(await fs.readFile(envPath, "utf8"));
1090
+ } catch {
1091
+ return {};
1092
+ }
1093
+ },
1094
+ writeEnv: async (env) => {
1095
+ await fs.writeFile(envPath, buildEnvFile(env, version), "utf8");
1096
+ }
1097
+ };
1098
+ }
820
1099
  function buildComposeYaml(options) {
821
1100
  const services = [];
822
1101
  const volumes = /* @__PURE__ */ new Set();
@@ -1037,9 +1316,6 @@ async function writeConfigFiles(configDir, initConfig) {
1037
1316
  await fs.writeFile(envPath, buildEnvFile(initConfig.env, initConfig.version), "utf8");
1038
1317
  await fs.writeFile(composePath, initConfig.composeYaml, "utf8");
1039
1318
  }
1040
- function renderedByHeader(version) {
1041
- return [`# Rendered by elmo ${version} on ${(/* @__PURE__ */ new Date()).toISOString()}`, "# Re-run `elmo init` after upgrading the CLI to refresh this file."].join("\n");
1042
- }
1043
1319
  function buildEnvFile(env, version) {
1044
1320
  const lines = [
1045
1321
  renderedByHeader(version),
@@ -1074,14 +1350,19 @@ async function getPackageVersion() {
1074
1350
  const contents = await fs.readFile(packagePath, "utf8");
1075
1351
  return JSON.parse(contents).version;
1076
1352
  }
1077
- async function maybeNotifyNewVersion(currentVersion) {
1353
+ async function fetchLatestCliVersion() {
1078
1354
  try {
1079
1355
  const response = await fetch("https://registry.npmjs.org/@elmohq/cli/latest");
1080
- if (!response.ok) return;
1081
- const data = await response.json();
1082
- if (!data.version) return;
1083
- if (semver.valid(currentVersion) && semver.lt(currentVersion, data.version)) log.warn(`New CLI version available (${data.version}). Run: npm install -g @elmohq/cli@latest`);
1084
- } catch {}
1356
+ if (!response.ok) return null;
1357
+ return (await response.json()).version ?? null;
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
1362
+ async function maybeNotifyNewVersion(currentVersion) {
1363
+ const latest = await fetchLatestCliVersion();
1364
+ if (!latest) return;
1365
+ if (semver.valid(currentVersion) && semver.lt(currentVersion, latest)) log.warn(`New CLI version available (${latest}). Run: npm install -g @elmohq/cli@latest`);
1085
1366
  }
1086
1367
  main().catch((error) => {
1087
1368
  const msg = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elmohq/cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "license": "MIT",
5
5
  "keywords": [
6
6
  "marketing",
@@ -28,22 +28,24 @@
28
28
  ],
29
29
  "type": "module",
30
30
  "dependencies": {
31
- "@clack/prompts": "^1.5.1",
31
+ "@clack/prompts": "^1.6.0",
32
32
  "commander": "^15.0.0",
33
33
  "dotenv": "17.4.2",
34
34
  "picocolors": "^1.1.1",
35
- "posthog-node": "^5.36.4",
36
- "semver": "^7.8.2"
35
+ "posthog-node": "^5.39.2",
36
+ "semver": "^7.8.5"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.9.2",
39
+ "@types/node": "^26.1.0",
40
40
  "@types/semver": "^7.7.1",
41
- "rolldown": "^1.1.0",
41
+ "rolldown": "^1.1.4",
42
42
  "typescript": "^6.0.3",
43
- "@workspace/config": "0.2.13"
43
+ "vitest": "^4.1.9",
44
+ "@workspace/config": "0.2.15"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "node scripts/build.mjs",
47
- "check-types": "tsc -p tsconfig.json"
48
+ "check-types": "tsc -p tsconfig.json",
49
+ "test": "vitest run"
48
50
  }
49
51
  }