@elmohq/cli 0.2.0 → 0.2.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 (2) hide show
  1. package/dist/index.js +301 -170
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -37,11 +37,11 @@ function printBanner() {
37
37
  }
38
38
  // ── Logging ──────────────────────────────────────────────────────────────────
39
39
  const log = {
40
- info: (msg) => isCI() ? console.log(msg) : p.log.info(msg),
41
- warn: (msg) => isCI() ? console.warn(pc.yellow(msg)) : p.log.warn(msg),
42
- error: (msg) => isCI() ? console.error(pc.red(msg)) : p.log.error(msg),
43
- success: (msg) => isCI() ? console.log(pc.green(msg)) : p.log.success(msg),
44
- step: (msg) => isCI() ? console.log(msg) : p.log.step(msg),
40
+ info: (msg) => p.log.info(msg),
41
+ warn: (msg) => p.log.warn(msg),
42
+ error: (msg) => p.log.error(msg),
43
+ success: (msg) => p.log.success(msg),
44
+ step: (msg) => p.log.step(msg),
45
45
  };
46
46
  // ── Helpers ──────────────────────────────────────────────────────────────────
47
47
  function assertNotCancelled(value) {
@@ -50,9 +50,6 @@ function assertNotCancelled(value) {
50
50
  process.exit(0);
51
51
  }
52
52
  }
53
- function isCI() {
54
- return Boolean(process.env.ELMO_CI);
55
- }
56
53
  function generateSecret(bytes = 32) {
57
54
  return crypto.randomBytes(bytes).toString("base64url");
58
55
  }
@@ -147,13 +144,6 @@ async function withVersionCheck(version, fn) {
147
144
  // ── Command: init ────────────────────────────────────────────────────────────
148
145
  async function runInit(options, version) {
149
146
  printBanner();
150
- if (isCI()) {
151
- await runInitCI(options, version);
152
- return;
153
- }
154
- await runInitInteractive(options, version);
155
- }
156
- async function runInitInteractive(options, version) {
157
147
  p.intro(pc.bold("Setting up Elmo"));
158
148
  const cwd = process.cwd();
159
149
  // ── Resolve config directory ─────────────────────────────────────────
@@ -250,69 +240,10 @@ async function runInitInteractive(options, version) {
250
240
  env.DATABASE_URL = LOCAL_DATABASE_URL;
251
241
  }
252
242
  // ── AI providers ─────────────────────────────────────────────────────
253
- const setOpenai = await p.confirm({
254
- message: "Set OpenAI credentials?",
255
- initialValue: true,
256
- });
257
- assertNotCancelled(setOpenai);
258
- if (setOpenai) {
259
- const key = await p.text({
260
- message: "OPENAI_API_KEY",
261
- validate: (v) => (!v ? "Required" : undefined),
262
- });
263
- assertNotCancelled(key);
264
- env.OPENAI_API_KEY = key;
265
- }
266
- const setAnthropic = await p.confirm({
267
- message: "Set Anthropic credentials?",
268
- initialValue: true,
269
- });
270
- assertNotCancelled(setAnthropic);
271
- if (setAnthropic) {
272
- const key = await p.text({
273
- message: "ANTHROPIC_API_KEY",
274
- validate: (v) => (!v ? "Required" : undefined),
275
- });
276
- assertNotCancelled(key);
277
- env.ANTHROPIC_API_KEY = key;
278
- }
279
- const setDataforseo = await p.confirm({
280
- message: "Set DataForSEO credentials?",
281
- initialValue: false,
282
- });
283
- assertNotCancelled(setDataforseo);
284
- if (setDataforseo) {
285
- const login = await p.text({
286
- message: "DATAFORSEO_LOGIN",
287
- validate: (v) => (!v ? "Required" : undefined),
288
- });
289
- assertNotCancelled(login);
290
- env.DATAFORSEO_LOGIN = login;
291
- const pwd = await p.text({
292
- message: "DATAFORSEO_PASSWORD",
293
- validate: (v) => (!v ? "Required" : undefined),
294
- });
295
- assertNotCancelled(pwd);
296
- env.DATAFORSEO_PASSWORD = pwd;
297
- }
298
- // ── Scrape targets ──────────────────────────────────────────────────
299
- // Default based on which API keys were provided
300
- const defaultTargets = [
301
- env.OPENAI_API_KEY ? "chatgpt:openai-api:gpt-5-mini:online" : null,
302
- env.ANTHROPIC_API_KEY ? "claude:anthropic-api:claude-sonnet-4" : null,
303
- env.DATAFORSEO_LOGIN ? "google-ai-mode:dataforseo:online" : null,
304
- ].filter(Boolean).join(",");
305
- const scrapeTargets = await p.text({
306
- message: "SCRAPE_TARGETS (model:provider[:version][:online], comma-separated)",
307
- initialValue: defaultTargets || undefined,
308
- placeholder: "chatgpt:openai-api:gpt-5-mini:online,claude:anthropic-api:claude-sonnet-4,google-ai-mode:dataforseo:online",
309
- validate: (v) => (!v ? "Required" : undefined),
310
- });
311
- assertNotCancelled(scrapeTargets);
312
- env.SCRAPE_TARGETS = scrapeTargets;
243
+ await configureProvidersInteractive(env);
313
244
  // ── Product updates ─────────────────────────────────────────────────
314
245
  const updatesEmail = await p.text({
315
- message: "Enter your email to receive product updates (optional)",
246
+ message: "Enter your work email to receive product updates (optional)",
316
247
  placeholder: "you@example.com",
317
248
  });
318
249
  const email = p.isCancel(updatesEmail) ? undefined : updatesEmail || undefined;
@@ -338,7 +269,7 @@ async function runInitInteractive(options, version) {
338
269
  repoRoot,
339
270
  });
340
271
  p.log.success(`Config written to ${configDir}`);
341
- p.log.warn("Your .env contains secrets — do not commit it to version control.");
272
+ p.log.warn("Your generated .env file contains secrets — do not commit it to version control.");
342
273
  if (options.dev) {
343
274
  p.log.info("Dev mode enabled. Run `elmo compose build` before starting.");
344
275
  }
@@ -365,90 +296,302 @@ async function runInitInteractive(options, version) {
365
296
  p.log.message(`If you find Elmo useful, star us on GitHub!\n ${link(pc.cyan("https://github.com/elmohq/elmo"), "https://github.com/elmohq/elmo")}`);
366
297
  p.outro(pc.green("Setup complete!"));
367
298
  }
368
- async function runInitCI(options, version) {
369
- const cwd = process.cwd();
370
- // Resolve config directory — --dir flag or ELMO_CONFIG_DIR or cwd
371
- const dirArg = options.dir ?? process.env.ELMO_CONFIG_DIR ?? cwd;
372
- const configDir = path.resolve(cwd, dirArg);
373
- // Resolve docker directory for dev mode
374
- let dockerDir;
375
- let repoRoot;
376
- if (options.dev) {
377
- const explicitDockerDir = options.dockerDir ?? process.env.ELMO_DOCKER_DIR;
378
- dockerDir = await resolveDockerDirAuto(cwd, explicitDockerDir);
379
- repoRoot = path.resolve(dockerDir, "..");
299
+ // ── Provider Configuration ───────────────────────────────────────────────────
300
+ const BRIGHTDATA_AFFILIATE = "https://get.brightdata.com/67h1b7h0shcn";
301
+ const OLOSTEP_AFFILIATE = "https://olostep.com/?ref=elmo";
302
+ const PROVIDERS_DOC_URL = "https://docs.elmohq.com/docs/deployment/providers";
303
+ // Surfaces each scraper can track — the first two are the "recommended starter" set.
304
+ const BRIGHTDATA_MODELS = [
305
+ "chatgpt",
306
+ "google-ai-mode",
307
+ "perplexity",
308
+ "copilot",
309
+ "gemini",
310
+ "grok",
311
+ ];
312
+ const OLOSTEP_MODELS = [
313
+ "chatgpt",
314
+ "google-ai-mode",
315
+ "google-ai-overview",
316
+ "perplexity",
317
+ "copilot",
318
+ "gemini",
319
+ "grok",
320
+ ];
321
+ const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
322
+ const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
323
+ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
324
+ const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
325
+ async function configureProvidersInteractive(env) {
326
+ p.note([
327
+ "Elmo tracks which AI answer engines mention your brand. Most of",
328
+ "that traffic comes from ChatGPT and Google AI Mode — neither has",
329
+ "a public API, so tracking them requires a scraper.",
330
+ "",
331
+ pc.bold("Recommended scrapers:"),
332
+ ` • ${pc.cyan("BrightData")} — cheap solid option, ~$0.45/mo per prompt`,
333
+ ` • ${pc.cyan("Olostep")} — premium option, powers Peec/AirOps, ~$2.25/mo per prompt`,
334
+ "",
335
+ "Pricing assumes Elmo's default cadence (5 runs/day × 2 surfaces).",
336
+ "Configure any combination below — every target is opt-in.",
337
+ ].join("\n"), "AI visibility providers");
338
+ const targets = [];
339
+ const brightDataTookDefaults = await collectBrightData(env, targets);
340
+ if (brightDataTookDefaults) {
341
+ await finalizeScrapeTargets(env, targets, { skipEdit: true });
342
+ return;
380
343
  }
381
- else {
382
- repoRoot = cwd;
344
+ const olostepTookDefaults = await collectOlostep(env, targets);
345
+ if (olostepTookDefaults) {
346
+ await finalizeScrapeTargets(env, targets, { skipEdit: true });
347
+ return;
383
348
  }
384
- // Build env from defaults and env vars
385
- const postgresMode = process.env.ELMO_POSTGRES_MODE ?? "docker";
386
- const env = {};
387
- env.DEPLOYMENT_MODE = "local";
388
- env.VITE_DEPLOYMENT_MODE = "local";
389
- env.BETTER_AUTH_SECRET = generateSecret();
390
- env.DEFAULT_ORG_ID = DEFAULT_ORG_ID;
391
- env.DEFAULT_ORG_NAME = DEFAULT_ORG_NAME;
392
- env.APP_NAME = DEFAULT_APP_NAME;
393
- env.APP_ICON = DEFAULT_APP_ICON;
394
- env.APP_URL = DEFAULT_APP_URL;
395
- env.VITE_APP_NAME = DEFAULT_APP_NAME;
396
- env.VITE_APP_ICON = DEFAULT_APP_ICON;
397
- env.VITE_APP_URL = DEFAULT_APP_URL;
398
- if (postgresMode === "external") {
399
- env.DATABASE_URL = process.env.ELMO_DATABASE_URL ?? "";
349
+ await collectAnthropic(env, targets);
350
+ await collectOpenAI(env, targets);
351
+ await collectOpenRouter(env, targets);
352
+ await collectDataForSEO(env, targets);
353
+ await finalizeScrapeTargets(env, targets);
354
+ }
355
+ async function collectBrightData(env, targets) {
356
+ const enable = await p.confirm({
357
+ message: `Configure ${pc.bold("BrightData")}? (recommended scraper — ~$0.45/mo per prompt)`,
358
+ initialValue: true,
359
+ });
360
+ assertNotCancelled(enable);
361
+ if (!enable)
362
+ return false;
363
+ p.log.info(`Sign up and generate an API token: ${link(pc.cyan(BRIGHTDATA_AFFILIATE), BRIGHTDATA_AFFILIATE)}`);
364
+ const key = await p.password({
365
+ message: "BrightData API token",
366
+ validate: (v) => (!v ? "Required" : undefined),
367
+ });
368
+ assertNotCancelled(key);
369
+ env.BRIGHTDATA_API_TOKEN = key;
370
+ return await pickScraperTargets({
371
+ providerLabel: "BrightData",
372
+ providerId: "brightdata",
373
+ allModels: BRIGHTDATA_MODELS,
374
+ targets,
375
+ });
376
+ }
377
+ async function collectOlostep(env, targets) {
378
+ const enable = await p.confirm({
379
+ message: `Configure ${pc.bold("Olostep")}? (recommended scraper — ~$2.25/mo per prompt)`,
380
+ initialValue: false,
381
+ });
382
+ assertNotCancelled(enable);
383
+ if (!enable)
384
+ return false;
385
+ p.log.info(`Grab an API key: ${link(pc.cyan(OLOSTEP_AFFILIATE), OLOSTEP_AFFILIATE)}`);
386
+ const key = await p.password({
387
+ message: "Olostep API key",
388
+ validate: (v) => (!v ? "Required" : undefined),
389
+ });
390
+ assertNotCancelled(key);
391
+ env.OLOSTEP_API_KEY = key;
392
+ return await pickScraperTargets({
393
+ providerLabel: "Olostep",
394
+ providerId: "olostep",
395
+ allModels: OLOSTEP_MODELS,
396
+ targets,
397
+ });
398
+ }
399
+ async function pickScraperTargets(args) {
400
+ const { providerLabel, providerId, allModels, targets } = args;
401
+ const useDefault = await p.confirm({
402
+ message: `Track ChatGPT and Google AI Mode via ${providerLabel}? (recommended)`,
403
+ initialValue: true,
404
+ });
405
+ assertNotCancelled(useDefault);
406
+ if (useDefault) {
407
+ for (const model of DEFAULT_SCRAPER_MODELS) {
408
+ targets.push(`${model}:${providerId}:online`);
409
+ }
410
+ return true;
400
411
  }
401
- else {
402
- env.DATABASE_URL = LOCAL_DATABASE_URL;
412
+ const selected = (await p.multiselect({
413
+ message: `Pick which surfaces to track via ${providerLabel}`,
414
+ options: allModels.map((model) => ({
415
+ value: model,
416
+ label: model,
417
+ })),
418
+ required: true,
419
+ initialValues: [...DEFAULT_SCRAPER_MODELS],
420
+ }));
421
+ assertNotCancelled(selected);
422
+ for (const model of selected) {
423
+ targets.push(`${model}:${providerId}:online`);
403
424
  }
404
- // AI providers from env vars
405
- if (process.env.ELMO_OPENAI_API_KEY) {
406
- env.OPENAI_API_KEY = process.env.ELMO_OPENAI_API_KEY;
425
+ return false;
426
+ }
427
+ async function collectAnthropic(env, targets) {
428
+ const enable = await p.confirm({
429
+ message: `Configure ${pc.bold("Anthropic API")}? (direct Claude — ~$4–5/mo per prompt per model)`,
430
+ initialValue: false,
431
+ });
432
+ assertNotCancelled(enable);
433
+ if (!enable)
434
+ return;
435
+ const key = await p.password({
436
+ message: "Anthropic API key",
437
+ validate: (v) => (!v ? "Required" : undefined),
438
+ });
439
+ assertNotCancelled(key);
440
+ env.ANTHROPIC_API_KEY = key;
441
+ const model = await p.text({
442
+ message: "Claude model",
443
+ placeholder: DEFAULT_ANTHROPIC_MODEL,
444
+ defaultValue: DEFAULT_ANTHROPIC_MODEL,
445
+ });
446
+ assertNotCancelled(model);
447
+ const slug = model || DEFAULT_ANTHROPIC_MODEL;
448
+ const webSearch = await p.confirm({
449
+ message: "Enable Claude's web search tool? (reflects real browsing behavior)",
450
+ initialValue: true,
451
+ });
452
+ assertNotCancelled(webSearch);
453
+ targets.push(webSearch ? `claude:anthropic-api:${slug}:online` : `claude:anthropic-api:${slug}`);
454
+ }
455
+ async function collectOpenAI(env, targets) {
456
+ const enable = await p.confirm({
457
+ message: `Configure ${pc.bold("OpenAI API")}? (gpt-* with web search — not the real ChatGPT UI)`,
458
+ initialValue: false,
459
+ });
460
+ assertNotCancelled(enable);
461
+ if (!enable)
462
+ return;
463
+ const key = await p.password({
464
+ message: "OpenAI API key",
465
+ validate: (v) => (!v ? "Required" : undefined),
466
+ });
467
+ assertNotCancelled(key);
468
+ env.OPENAI_API_KEY = key;
469
+ const model = await p.text({
470
+ message: "OpenAI model",
471
+ placeholder: DEFAULT_OPENAI_MODEL,
472
+ defaultValue: DEFAULT_OPENAI_MODEL,
473
+ });
474
+ assertNotCancelled(model);
475
+ const slug = model || DEFAULT_OPENAI_MODEL;
476
+ const webSearch = await p.confirm({
477
+ message: "Enable the web_search_preview tool?",
478
+ initialValue: true,
479
+ });
480
+ assertNotCancelled(webSearch);
481
+ targets.push(webSearch ? `chatgpt:openai-api:${slug}:online` : `chatgpt:openai-api:${slug}`);
482
+ }
483
+ async function collectOpenRouter(env, targets) {
484
+ const enable = await p.confirm({
485
+ message: `Configure ${pc.bold("OpenRouter")}? (one key, many hosted models)`,
486
+ initialValue: false,
487
+ });
488
+ assertNotCancelled(enable);
489
+ if (!enable)
490
+ return;
491
+ const key = await p.password({
492
+ message: "OpenRouter API key",
493
+ validate: (v) => (!v ? "Required" : undefined),
494
+ });
495
+ assertNotCancelled(key);
496
+ env.OPENROUTER_API_KEY = key;
497
+ const model = await p.text({
498
+ message: "OpenRouter model slug",
499
+ placeholder: DEFAULT_OPENROUTER_MODEL,
500
+ defaultValue: DEFAULT_OPENROUTER_MODEL,
501
+ });
502
+ assertNotCancelled(model);
503
+ const slug = model || DEFAULT_OPENROUTER_MODEL;
504
+ const webSearch = await p.confirm({
505
+ message: "Append :online for web search?",
506
+ initialValue: true,
507
+ });
508
+ assertNotCancelled(webSearch);
509
+ targets.push(webSearch ? `claude:openrouter:${slug}:online` : `claude:openrouter:${slug}`);
510
+ }
511
+ async function collectDataForSEO(env, targets) {
512
+ const enable = await p.confirm({
513
+ message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode scraping + keyword/persona suggestions in the web wizard)`,
514
+ initialValue: false,
515
+ });
516
+ assertNotCancelled(enable);
517
+ if (!enable)
518
+ return;
519
+ const login = await p.text({
520
+ message: "DataForSEO login",
521
+ validate: (v) => (!v ? "Required" : undefined),
522
+ });
523
+ assertNotCancelled(login);
524
+ env.DATAFORSEO_LOGIN = login;
525
+ const pwd = await p.password({
526
+ message: "DataForSEO password",
527
+ validate: (v) => (!v ? "Required" : undefined),
528
+ });
529
+ assertNotCancelled(pwd);
530
+ env.DATAFORSEO_PASSWORD = pwd;
531
+ const addTarget = await p.confirm({
532
+ message: "Also scrape Google AI Mode via DataForSEO? (google-ai-mode:dataforseo:online)",
533
+ initialValue: false,
534
+ });
535
+ assertNotCancelled(addTarget);
536
+ if (addTarget) {
537
+ targets.push("google-ai-mode:dataforseo:online");
407
538
  }
408
- if (process.env.ELMO_ANTHROPIC_API_KEY) {
409
- env.ANTHROPIC_API_KEY = process.env.ELMO_ANTHROPIC_API_KEY;
539
+ }
540
+ async function finalizeScrapeTargets(env, targets, options = {}) {
541
+ const deduped = dedupeTargets(targets);
542
+ if (!deduped) {
543
+ p.log.warn("No SCRAPE_TARGETS configured. Elmo will not run scheduled checks until you set them.");
544
+ p.log.info(`Reference: ${link(pc.cyan(PROVIDERS_DOC_URL), PROVIDERS_DOC_URL)}`);
545
+ const addManual = await p.confirm({
546
+ message: "Enter SCRAPE_TARGETS manually now?",
547
+ initialValue: false,
548
+ });
549
+ assertNotCancelled(addManual);
550
+ if (addManual) {
551
+ const manual = await p.text({
552
+ message: "SCRAPE_TARGETS (model:provider[:version][:online], comma-separated)",
553
+ placeholder: "chatgpt:brightdata:online,google-ai-mode:brightdata:online",
554
+ validate: (v) => (!v ? "Required" : undefined),
555
+ });
556
+ assertNotCancelled(manual);
557
+ env.SCRAPE_TARGETS = manual;
558
+ }
559
+ return;
410
560
  }
411
- if (process.env.ELMO_DATAFORSEO_LOGIN) {
412
- env.DATAFORSEO_LOGIN = process.env.ELMO_DATAFORSEO_LOGIN;
413
- env.DATAFORSEO_PASSWORD =
414
- process.env.ELMO_DATAFORSEO_PASSWORD ?? "";
561
+ if (options.skipEdit) {
562
+ env.SCRAPE_TARGETS = deduped;
563
+ return;
415
564
  }
416
- if (process.env.ELMO_SCRAPE_TARGETS) {
417
- env.SCRAPE_TARGETS = process.env.ELMO_SCRAPE_TARGETS;
565
+ const customize = await p.confirm({
566
+ message: "Edit SCRAPE_TARGETS before saving?",
567
+ initialValue: false,
568
+ });
569
+ assertNotCancelled(customize);
570
+ if (customize) {
571
+ p.log.info(`Reference: ${link(pc.cyan(PROVIDERS_DOC_URL), PROVIDERS_DOC_URL)}`);
572
+ const manual = await p.text({
573
+ message: "SCRAPE_TARGETS",
574
+ initialValue: deduped,
575
+ validate: (v) => (!v ? "Required" : undefined),
576
+ });
577
+ assertNotCancelled(manual);
578
+ env.SCRAPE_TARGETS = manual;
579
+ p.log.step(`SCRAPE_TARGETS:\n ${pc.cyan(manual)}`);
418
580
  }
419
581
  else {
420
- // Default: use the direct API providers matching the old hardcoded behavior
421
- env.SCRAPE_TARGETS = "chatgpt:openai-api:gpt-5-mini:online,claude:anthropic-api:claude-sonnet-4,google-ai-mode:dataforseo:online";
582
+ env.SCRAPE_TARGETS = deduped;
422
583
  }
423
- const composeYaml = buildComposeYaml({
424
- dev: Boolean(options.dev),
425
- postgresMode,
426
- repoRoot,
427
- dockerDir,
428
- });
429
- await ensureDir(configDir);
430
- await writeConfigFiles(configDir, {
431
- env,
432
- composeYaml,
433
- postgresMode,
434
- dev: Boolean(options.dev),
435
- });
436
- await writeGlobalConfig({
437
- configDir,
438
- dockerDir,
439
- dev: Boolean(options.dev),
440
- postgresMode,
441
- repoRoot,
442
- });
443
- console.log(`Config written to ${configDir}`);
444
- await trackCliEvent("cli_init", {
445
- version,
446
- os: process.platform,
447
- arch: process.arch,
448
- node_version: process.version,
449
- postgres_mode: postgresMode,
450
- dev_mode: Boolean(options.dev),
451
- });
584
+ }
585
+ function dedupeTargets(targets) {
586
+ const seen = new Set();
587
+ const out = [];
588
+ for (const t of targets) {
589
+ if (seen.has(t))
590
+ continue;
591
+ seen.add(t);
592
+ out.push(t);
593
+ }
594
+ return out.join(",");
452
595
  }
453
596
  // ── Command: update ──────────────────────────────────────────────────────────
454
597
  async function runRegen(options) {
@@ -499,27 +642,15 @@ async function doStart(configDir) {
499
642
  assertDockerRunning();
500
643
  log.step("Starting Docker Compose stack...");
501
644
  await runDockerCompose(configDir, ["up", "-d"]);
502
- if (!isCI()) {
503
- const s = p.spinner();
504
- s.start("Waiting for services to become healthy...");
505
- const ok = await waitForHealthy(configDir, 180_000);
506
- if (ok) {
507
- s.stop("All services healthy!");
508
- }
509
- else {
510
- s.stop("Health check timed out.");
511
- p.log.warn("Some services did not report healthy status.");
512
- }
645
+ const s = p.spinner();
646
+ s.start("Waiting for services to become healthy...");
647
+ const ok = await waitForHealthy(configDir, 180_000);
648
+ if (ok) {
649
+ s.stop("All services healthy!");
513
650
  }
514
651
  else {
515
- console.log("Waiting for services to become healthy...");
516
- const ok = await waitForHealthy(configDir, 180_000);
517
- if (ok) {
518
- console.log("All services healthy.");
519
- }
520
- else {
521
- console.warn("Some services did not report healthy status.");
522
- }
652
+ s.stop("Health check timed out.");
653
+ p.log.warn("Some services did not report healthy status.");
523
654
  }
524
655
  log.info("Examples:");
525
656
  console.log(` ${pc.bold("elmo logs -f")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elmohq/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "bin": {
5
5
  "elmo": "dist/index.js"
6
6
  },