@elmohq/cli 0.2.0 → 0.2.2

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 +303 -171
  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 ─────────────────────────────────────────
@@ -239,7 +229,8 @@ async function runInitInteractive(options, version) {
239
229
  env.VITE_APP_ICON = DEFAULT_APP_ICON;
240
230
  env.VITE_APP_URL = DEFAULT_APP_URL;
241
231
  if (postgresMode === "external") {
242
- const url = await p.text({
232
+ p.note("Must be an IPv4-compatible direct connection or database pooler.", "DATABASE_URL");
233
+ const url = await p.password({
243
234
  message: "DATABASE_URL",
244
235
  validate: (v) => (!v ? "Required" : undefined),
245
236
  });
@@ -250,69 +241,10 @@ async function runInitInteractive(options, version) {
250
241
  env.DATABASE_URL = LOCAL_DATABASE_URL;
251
242
  }
252
243
  // ── 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;
244
+ await configureProvidersInteractive(env);
313
245
  // ── Product updates ─────────────────────────────────────────────────
314
246
  const updatesEmail = await p.text({
315
- message: "Enter your email to receive product updates (optional)",
247
+ message: "Enter your work email to receive product updates (optional)",
316
248
  placeholder: "you@example.com",
317
249
  });
318
250
  const email = p.isCancel(updatesEmail) ? undefined : updatesEmail || undefined;
@@ -338,7 +270,7 @@ async function runInitInteractive(options, version) {
338
270
  repoRoot,
339
271
  });
340
272
  p.log.success(`Config written to ${configDir}`);
341
- p.log.warn("Your .env contains secrets — do not commit it to version control.");
273
+ p.log.warn("Your generated .env file contains secrets — do not commit it to version control.");
342
274
  if (options.dev) {
343
275
  p.log.info("Dev mode enabled. Run `elmo compose build` before starting.");
344
276
  }
@@ -365,90 +297,302 @@ async function runInitInteractive(options, version) {
365
297
  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
298
  p.outro(pc.green("Setup complete!"));
367
299
  }
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, "..");
300
+ // ── Provider Configuration ───────────────────────────────────────────────────
301
+ const BRIGHTDATA_AFFILIATE = "https://get.brightdata.com/67h1b7h0shcn";
302
+ const OLOSTEP_AFFILIATE = "https://olostep.com/?ref=elmo";
303
+ const PROVIDERS_DOC_URL = "https://docs.elmohq.com/docs/deployment/providers";
304
+ // Surfaces each scraper can track — the first two are the "recommended starter" set.
305
+ const BRIGHTDATA_MODELS = [
306
+ "chatgpt",
307
+ "google-ai-mode",
308
+ "perplexity",
309
+ "copilot",
310
+ "gemini",
311
+ "grok",
312
+ ];
313
+ const OLOSTEP_MODELS = [
314
+ "chatgpt",
315
+ "google-ai-mode",
316
+ "google-ai-overview",
317
+ "perplexity",
318
+ "copilot",
319
+ "gemini",
320
+ "grok",
321
+ ];
322
+ const DEFAULT_SCRAPER_MODELS = ["chatgpt", "google-ai-mode"];
323
+ const DEFAULT_OPENAI_MODEL = "gpt-5-mini";
324
+ const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514";
325
+ const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6";
326
+ async function configureProvidersInteractive(env) {
327
+ p.note([
328
+ "Elmo tracks which AI answer engines mention your brand. Most of",
329
+ "that traffic comes from ChatGPT and Google AI Mode — neither has",
330
+ "a public API, so tracking them requires a scraper.",
331
+ "",
332
+ pc.bold("Recommended scrapers:"),
333
+ ` • ${pc.cyan("BrightData")} — cheap solid option, ~$0.45/mo per prompt`,
334
+ ` • ${pc.cyan("Olostep")} — premium option, powers Peec/AirOps, ~$2.25/mo per prompt`,
335
+ "",
336
+ "Pricing assumes Elmo's default cadence (5 runs/day × 2 surfaces).",
337
+ "Configure any combination below — every target is opt-in.",
338
+ ].join("\n"), "AI visibility providers");
339
+ const targets = [];
340
+ const brightDataTookDefaults = await collectBrightData(env, targets);
341
+ if (brightDataTookDefaults) {
342
+ await finalizeScrapeTargets(env, targets, { skipEdit: true });
343
+ return;
380
344
  }
381
- else {
382
- repoRoot = cwd;
345
+ const olostepTookDefaults = await collectOlostep(env, targets);
346
+ if (olostepTookDefaults) {
347
+ await finalizeScrapeTargets(env, targets, { skipEdit: true });
348
+ return;
383
349
  }
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 ?? "";
350
+ await collectAnthropic(env, targets);
351
+ await collectOpenAI(env, targets);
352
+ await collectOpenRouter(env, targets);
353
+ await collectDataForSEO(env, targets);
354
+ await finalizeScrapeTargets(env, targets);
355
+ }
356
+ async function collectBrightData(env, targets) {
357
+ const enable = await p.confirm({
358
+ message: `Configure ${pc.bold("BrightData")}? (recommended scraper — ~$0.45/mo per prompt)`,
359
+ initialValue: true,
360
+ });
361
+ assertNotCancelled(enable);
362
+ if (!enable)
363
+ return false;
364
+ p.log.info(`Sign up and generate an API token: ${link(pc.cyan(BRIGHTDATA_AFFILIATE), BRIGHTDATA_AFFILIATE)}`);
365
+ const key = await p.password({
366
+ message: "BrightData API token",
367
+ validate: (v) => (!v ? "Required" : undefined),
368
+ });
369
+ assertNotCancelled(key);
370
+ env.BRIGHTDATA_API_TOKEN = key;
371
+ return await pickScraperTargets({
372
+ providerLabel: "BrightData",
373
+ providerId: "brightdata",
374
+ allModels: BRIGHTDATA_MODELS,
375
+ targets,
376
+ });
377
+ }
378
+ async function collectOlostep(env, targets) {
379
+ const enable = await p.confirm({
380
+ message: `Configure ${pc.bold("Olostep")}? (recommended scraper — ~$2.25/mo per prompt)`,
381
+ initialValue: false,
382
+ });
383
+ assertNotCancelled(enable);
384
+ if (!enable)
385
+ return false;
386
+ p.log.info(`Grab an API key: ${link(pc.cyan(OLOSTEP_AFFILIATE), OLOSTEP_AFFILIATE)}`);
387
+ const key = await p.password({
388
+ message: "Olostep API key",
389
+ validate: (v) => (!v ? "Required" : undefined),
390
+ });
391
+ assertNotCancelled(key);
392
+ env.OLOSTEP_API_KEY = key;
393
+ return await pickScraperTargets({
394
+ providerLabel: "Olostep",
395
+ providerId: "olostep",
396
+ allModels: OLOSTEP_MODELS,
397
+ targets,
398
+ });
399
+ }
400
+ async function pickScraperTargets(args) {
401
+ const { providerLabel, providerId, allModels, targets } = args;
402
+ const useDefault = await p.confirm({
403
+ message: `Track ChatGPT and Google AI Mode via ${providerLabel}? (recommended)`,
404
+ initialValue: true,
405
+ });
406
+ assertNotCancelled(useDefault);
407
+ if (useDefault) {
408
+ for (const model of DEFAULT_SCRAPER_MODELS) {
409
+ targets.push(`${model}:${providerId}:online`);
410
+ }
411
+ return true;
400
412
  }
401
- else {
402
- env.DATABASE_URL = LOCAL_DATABASE_URL;
413
+ const selected = (await p.multiselect({
414
+ message: `Pick which surfaces to track via ${providerLabel}`,
415
+ options: allModels.map((model) => ({
416
+ value: model,
417
+ label: model,
418
+ })),
419
+ required: true,
420
+ initialValues: [...DEFAULT_SCRAPER_MODELS],
421
+ }));
422
+ assertNotCancelled(selected);
423
+ for (const model of selected) {
424
+ targets.push(`${model}:${providerId}:online`);
403
425
  }
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;
426
+ return false;
427
+ }
428
+ async function collectAnthropic(env, targets) {
429
+ const enable = await p.confirm({
430
+ message: `Configure ${pc.bold("Anthropic API")}? (direct Claude — ~$4–5/mo per prompt per model)`,
431
+ initialValue: false,
432
+ });
433
+ assertNotCancelled(enable);
434
+ if (!enable)
435
+ return;
436
+ const key = await p.password({
437
+ message: "Anthropic API key",
438
+ validate: (v) => (!v ? "Required" : undefined),
439
+ });
440
+ assertNotCancelled(key);
441
+ env.ANTHROPIC_API_KEY = key;
442
+ const model = await p.text({
443
+ message: "Claude model",
444
+ placeholder: DEFAULT_ANTHROPIC_MODEL,
445
+ defaultValue: DEFAULT_ANTHROPIC_MODEL,
446
+ });
447
+ assertNotCancelled(model);
448
+ const slug = model || DEFAULT_ANTHROPIC_MODEL;
449
+ const webSearch = await p.confirm({
450
+ message: "Enable Claude's web search tool? (reflects real browsing behavior)",
451
+ initialValue: true,
452
+ });
453
+ assertNotCancelled(webSearch);
454
+ targets.push(webSearch ? `claude:anthropic-api:${slug}:online` : `claude:anthropic-api:${slug}`);
455
+ }
456
+ async function collectOpenAI(env, targets) {
457
+ const enable = await p.confirm({
458
+ message: `Configure ${pc.bold("OpenAI API")}? (gpt-* with web search — not the real ChatGPT UI)`,
459
+ initialValue: false,
460
+ });
461
+ assertNotCancelled(enable);
462
+ if (!enable)
463
+ return;
464
+ const key = await p.password({
465
+ message: "OpenAI API key",
466
+ validate: (v) => (!v ? "Required" : undefined),
467
+ });
468
+ assertNotCancelled(key);
469
+ env.OPENAI_API_KEY = key;
470
+ const model = await p.text({
471
+ message: "OpenAI model",
472
+ placeholder: DEFAULT_OPENAI_MODEL,
473
+ defaultValue: DEFAULT_OPENAI_MODEL,
474
+ });
475
+ assertNotCancelled(model);
476
+ const slug = model || DEFAULT_OPENAI_MODEL;
477
+ const webSearch = await p.confirm({
478
+ message: "Enable the web_search_preview tool?",
479
+ initialValue: true,
480
+ });
481
+ assertNotCancelled(webSearch);
482
+ targets.push(webSearch ? `chatgpt:openai-api:${slug}:online` : `chatgpt:openai-api:${slug}`);
483
+ }
484
+ async function collectOpenRouter(env, targets) {
485
+ const enable = await p.confirm({
486
+ message: `Configure ${pc.bold("OpenRouter")}? (one key, many hosted models)`,
487
+ initialValue: false,
488
+ });
489
+ assertNotCancelled(enable);
490
+ if (!enable)
491
+ return;
492
+ const key = await p.password({
493
+ message: "OpenRouter API key",
494
+ validate: (v) => (!v ? "Required" : undefined),
495
+ });
496
+ assertNotCancelled(key);
497
+ env.OPENROUTER_API_KEY = key;
498
+ const model = await p.text({
499
+ message: "OpenRouter model slug",
500
+ placeholder: DEFAULT_OPENROUTER_MODEL,
501
+ defaultValue: DEFAULT_OPENROUTER_MODEL,
502
+ });
503
+ assertNotCancelled(model);
504
+ const slug = model || DEFAULT_OPENROUTER_MODEL;
505
+ const webSearch = await p.confirm({
506
+ message: "Append :online for web search?",
507
+ initialValue: true,
508
+ });
509
+ assertNotCancelled(webSearch);
510
+ targets.push(webSearch ? `claude:openrouter:${slug}:online` : `claude:openrouter:${slug}`);
511
+ }
512
+ async function collectDataForSEO(env, targets) {
513
+ const enable = await p.confirm({
514
+ message: `Configure ${pc.bold("DataForSEO")}? (Google AI Mode scraping + keyword/persona suggestions in the web wizard)`,
515
+ initialValue: false,
516
+ });
517
+ assertNotCancelled(enable);
518
+ if (!enable)
519
+ return;
520
+ const login = await p.text({
521
+ message: "DataForSEO login",
522
+ validate: (v) => (!v ? "Required" : undefined),
523
+ });
524
+ assertNotCancelled(login);
525
+ env.DATAFORSEO_LOGIN = login;
526
+ const pwd = await p.password({
527
+ message: "DataForSEO password",
528
+ validate: (v) => (!v ? "Required" : undefined),
529
+ });
530
+ assertNotCancelled(pwd);
531
+ env.DATAFORSEO_PASSWORD = pwd;
532
+ const addTarget = await p.confirm({
533
+ message: "Also scrape Google AI Mode via DataForSEO? (google-ai-mode:dataforseo:online)",
534
+ initialValue: false,
535
+ });
536
+ assertNotCancelled(addTarget);
537
+ if (addTarget) {
538
+ targets.push("google-ai-mode:dataforseo:online");
407
539
  }
408
- if (process.env.ELMO_ANTHROPIC_API_KEY) {
409
- env.ANTHROPIC_API_KEY = process.env.ELMO_ANTHROPIC_API_KEY;
540
+ }
541
+ async function finalizeScrapeTargets(env, targets, options = {}) {
542
+ const deduped = dedupeTargets(targets);
543
+ if (!deduped) {
544
+ p.log.warn("No SCRAPE_TARGETS configured. Elmo will not run scheduled checks until you set them.");
545
+ p.log.info(`Reference: ${link(pc.cyan(PROVIDERS_DOC_URL), PROVIDERS_DOC_URL)}`);
546
+ const addManual = await p.confirm({
547
+ message: "Enter SCRAPE_TARGETS manually now?",
548
+ initialValue: false,
549
+ });
550
+ assertNotCancelled(addManual);
551
+ if (addManual) {
552
+ const manual = await p.text({
553
+ message: "SCRAPE_TARGETS (model:provider[:version][:online], comma-separated)",
554
+ placeholder: "chatgpt:brightdata:online,google-ai-mode:brightdata:online",
555
+ validate: (v) => (!v ? "Required" : undefined),
556
+ });
557
+ assertNotCancelled(manual);
558
+ env.SCRAPE_TARGETS = manual;
559
+ }
560
+ return;
410
561
  }
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 ?? "";
562
+ if (options.skipEdit) {
563
+ env.SCRAPE_TARGETS = deduped;
564
+ return;
415
565
  }
416
- if (process.env.ELMO_SCRAPE_TARGETS) {
417
- env.SCRAPE_TARGETS = process.env.ELMO_SCRAPE_TARGETS;
566
+ const customize = await p.confirm({
567
+ message: "Edit SCRAPE_TARGETS before saving?",
568
+ initialValue: false,
569
+ });
570
+ assertNotCancelled(customize);
571
+ if (customize) {
572
+ p.log.info(`Reference: ${link(pc.cyan(PROVIDERS_DOC_URL), PROVIDERS_DOC_URL)}`);
573
+ const manual = await p.text({
574
+ message: "SCRAPE_TARGETS",
575
+ initialValue: deduped,
576
+ validate: (v) => (!v ? "Required" : undefined),
577
+ });
578
+ assertNotCancelled(manual);
579
+ env.SCRAPE_TARGETS = manual;
580
+ p.log.step(`SCRAPE_TARGETS:\n ${pc.cyan(manual)}`);
418
581
  }
419
582
  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";
583
+ env.SCRAPE_TARGETS = deduped;
422
584
  }
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
- });
585
+ }
586
+ function dedupeTargets(targets) {
587
+ const seen = new Set();
588
+ const out = [];
589
+ for (const t of targets) {
590
+ if (seen.has(t))
591
+ continue;
592
+ seen.add(t);
593
+ out.push(t);
594
+ }
595
+ return out.join(",");
452
596
  }
453
597
  // ── Command: update ──────────────────────────────────────────────────────────
454
598
  async function runRegen(options) {
@@ -499,27 +643,15 @@ async function doStart(configDir) {
499
643
  assertDockerRunning();
500
644
  log.step("Starting Docker Compose stack...");
501
645
  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
- }
646
+ const s = p.spinner();
647
+ s.start("Waiting for services to become healthy...");
648
+ const ok = await waitForHealthy(configDir, 180_000);
649
+ if (ok) {
650
+ s.stop("All services healthy!");
513
651
  }
514
652
  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
- }
653
+ s.stop("Health check timed out.");
654
+ p.log.warn("Some services did not report healthy status.");
523
655
  }
524
656
  log.info("Examples:");
525
657
  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.2",
4
4
  "bin": {
5
5
  "elmo": "dist/index.js"
6
6
  },