@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.
- package/README.md +1 -0
- package/dist/index.js +306 -25
- 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-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
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
|
|
708
|
-
message: "
|
|
709
|
-
|
|
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(
|
|
712
|
-
|
|
713
|
-
model
|
|
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
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
"semver": "^7.8.
|
|
35
|
+
"posthog-node": "^5.39.2",
|
|
36
|
+
"semver": "^7.8.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^
|
|
39
|
+
"@types/node": "^26.1.0",
|
|
40
40
|
"@types/semver": "^7.7.1",
|
|
41
|
-
"rolldown": "^1.1.
|
|
41
|
+
"rolldown": "^1.1.4",
|
|
42
42
|
"typescript": "^6.0.3",
|
|
43
|
-
"
|
|
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
|
}
|