@elmohq/cli 0.2.9 → 0.2.11
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 +9 -11
- package/dist/index.js +113 -350
- package/dist/telemetry.js +23 -56
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Requires [Docker](https://docs.docker.com/get-docker/) and Docker Compose.
|
|
|
44
44
|
elmo init
|
|
45
45
|
|
|
46
46
|
# 2. Start the stack
|
|
47
|
-
elmo
|
|
47
|
+
elmo compose up -d
|
|
48
48
|
|
|
49
49
|
# 3. Open the app at http://localhost:1515
|
|
50
50
|
```
|
|
@@ -58,29 +58,27 @@ For the full self-hosting walkthrough, see the [Elmo docs](https://www.elmohq.co
|
|
|
58
58
|
| Command | Description |
|
|
59
59
|
| --- | --- |
|
|
60
60
|
| `elmo init` | Interactive wizard to set up a local Elmo instance |
|
|
61
|
-
| `elmo
|
|
62
|
-
| `elmo
|
|
63
|
-
| `elmo status` | Check the health of running services |
|
|
64
|
-
| `elmo logs [service]` | Tail container logs (pass `-f` to follow) |
|
|
65
|
-
| `elmo regen` | Regenerate `elmo.yaml` / `.env` from your saved config |
|
|
66
|
-
| `elmo compose <args...>` | Run any `docker compose` command against your Elmo project |
|
|
67
|
-
| `elmo build` | Build Docker images locally (for `--dev` installs) |
|
|
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
|
+
| `elmo edit <env\|compose>` | Change API keys, scrape targets, or the Docker Compose YAML |
|
|
68
63
|
|
|
69
64
|
Run `elmo --help` or `elmo <command> --help` for the full list of flags.
|
|
70
65
|
|
|
71
66
|
### Useful flags
|
|
72
67
|
|
|
73
|
-
- `--dir <path>` — point any command at a specific config directory (defaults to `~/.
|
|
68
|
+
- `--dir <path>` — point any command at a specific config directory (defaults to `~/.elmo`).
|
|
74
69
|
- `elmo init --dev` — build images from a local checkout of the repo instead of pulling from the registry.
|
|
75
70
|
|
|
76
71
|
## Telemetry
|
|
77
72
|
|
|
78
|
-
The CLI sends anonymous install and command events so we can understand which flows people use and where setup breaks. To opt out,
|
|
73
|
+
The CLI sends anonymous install and command events so we can understand which flows people use and where setup breaks. To opt out, either export `DISABLE_TELEMETRY=1` in your shell, or add it to your `.env`:
|
|
79
74
|
|
|
80
75
|
```bash
|
|
81
|
-
|
|
76
|
+
elmo edit env # add DISABLE_TELEMETRY=1
|
|
77
|
+
elmo compose up -d # restart so the deployment picks it up
|
|
82
78
|
```
|
|
83
79
|
|
|
80
|
+
See [the telemetry docs](https://www.elmohq.com/docs/developer-guide/telemetry) for details on what is collected.
|
|
81
|
+
|
|
84
82
|
## Star, contribute, and chat
|
|
85
83
|
|
|
86
84
|
Elmo is built in the open and we'd love your help.
|
package/dist/index.js
CHANGED
|
@@ -7,15 +7,15 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import * as p from "@clack/prompts";
|
|
9
9
|
import { Command } from "commander";
|
|
10
|
+
import { parse as parseDotenv } from "dotenv";
|
|
10
11
|
import pc from "picocolors";
|
|
11
12
|
import semver from "semver";
|
|
12
|
-
import {
|
|
13
|
+
import { submitNewsletterSignup, trackCliEvent } from "./telemetry.js";
|
|
13
14
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
14
|
-
const CONFIG_HOME = path.join(os.homedir(), ".
|
|
15
|
-
const CONFIG_FILE = path.join(CONFIG_HOME, "config.json");
|
|
15
|
+
const CONFIG_HOME = path.join(os.homedir(), ".elmo");
|
|
16
16
|
const DEFAULT_APP_NAME = "Elmo";
|
|
17
17
|
const DEFAULT_APP_ICON = "/icons/elmo-icon.svg";
|
|
18
|
-
const
|
|
18
|
+
const DEFAULT_APP_PORT = 1515;
|
|
19
19
|
const LOCAL_DATABASE_URL = "postgres://postgres:postgres@postgres:5432/elmo";
|
|
20
20
|
const TELEMETRY_DOC_URL = "https://elmohq.com/docs/developer-guide/telemetry";
|
|
21
21
|
// ── Banner ───────────────────────────────────────────────────────────────────
|
|
@@ -64,6 +64,8 @@ async function main() {
|
|
|
64
64
|
program
|
|
65
65
|
.name("elmo")
|
|
66
66
|
.version(version)
|
|
67
|
+
.option("--dir <path>", "Config directory")
|
|
68
|
+
.configureHelp({ showGlobalOptions: true })
|
|
67
69
|
.action(() => {
|
|
68
70
|
printBanner();
|
|
69
71
|
program.outputHelp();
|
|
@@ -72,115 +74,27 @@ async function main() {
|
|
|
72
74
|
.command("init")
|
|
73
75
|
.description("set up local Elmo instance")
|
|
74
76
|
.option("--dev", "Use local build context (repo only)")
|
|
75
|
-
.option("--dir <path>", "Directory to store config files")
|
|
76
77
|
.option("--docker-dir <path>", "Path to Docker build context (dev mode)")
|
|
77
|
-
.action(async (
|
|
78
|
-
await withVersionCheck(version, () => runInit(
|
|
79
|
-
});
|
|
80
|
-
program
|
|
81
|
-
.command("regen")
|
|
82
|
-
.description("regenerate configuration files from current settings")
|
|
83
|
-
.option("--dir <path>", "Config directory")
|
|
84
|
-
.action(async (options) => {
|
|
85
|
-
await withVersionCheck(version, () => runRegen(options));
|
|
86
|
-
});
|
|
87
|
-
program
|
|
88
|
-
.command("start")
|
|
89
|
-
.description("start Elmo instance")
|
|
90
|
-
.option("--dir <path>", "Config directory")
|
|
91
|
-
.action(async (options) => {
|
|
92
|
-
await withVersionCheck(version, () => runStart(options));
|
|
93
|
-
});
|
|
94
|
-
program
|
|
95
|
-
.command("stop")
|
|
96
|
-
.description("stop Elmo instance")
|
|
97
|
-
.option("--dir <path>", "Config directory")
|
|
98
|
-
.action(async (options) => {
|
|
99
|
-
await withVersionCheck(version, () => runStop(options));
|
|
100
|
-
});
|
|
101
|
-
program
|
|
102
|
-
.command("status")
|
|
103
|
-
.description("check Elmo instance health")
|
|
104
|
-
.option("--dir <path>", "Config directory")
|
|
105
|
-
.action(async (options) => {
|
|
106
|
-
await withVersionCheck(version, () => runStatus(options));
|
|
78
|
+
.action(async (_opts, cmd) => {
|
|
79
|
+
await withVersionCheck(version, () => runInit(cmd.optsWithGlobals(), version));
|
|
107
80
|
});
|
|
108
81
|
program
|
|
109
82
|
.command("compose")
|
|
110
83
|
.description("run Docker Compose commands using your Elmo config")
|
|
111
84
|
.allowUnknownOption(true)
|
|
112
|
-
.option("--dir <path>", "Config directory")
|
|
113
85
|
.argument("[args...]", "Arguments passed to Docker Compose")
|
|
114
|
-
.action(async (args,
|
|
115
|
-
await withVersionCheck(version, () => runCompose(args,
|
|
86
|
+
.action(async (args, _opts, cmd) => {
|
|
87
|
+
await withVersionCheck(version, () => runCompose(args, cmd.optsWithGlobals()));
|
|
116
88
|
});
|
|
117
89
|
program
|
|
118
|
-
.command("
|
|
119
|
-
.description("
|
|
120
|
-
.
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
.action(async (args, options) => {
|
|
124
|
-
await withVersionCheck(version, () => runCompose(["logs", ...args], options));
|
|
125
|
-
});
|
|
126
|
-
program
|
|
127
|
-
.command("build")
|
|
128
|
-
.description("build Docker images locally for development")
|
|
129
|
-
.option("--dir <path>", "Config directory")
|
|
130
|
-
.option("--web-only", "Only build the web image")
|
|
131
|
-
.option("--worker-only", "Only build the worker image")
|
|
132
|
-
.option("--no-cache", "Build without Docker cache")
|
|
133
|
-
.action(async (options) => {
|
|
134
|
-
await withVersionCheck(version, () => runBuild(options));
|
|
135
|
-
});
|
|
136
|
-
const telemetry = program.command("telemetry").description("manage CLI + local-deployment telemetry");
|
|
137
|
-
telemetry
|
|
138
|
-
.command("status")
|
|
139
|
-
.description("show whether telemetry is enabled and what is collected")
|
|
140
|
-
.action(async () => {
|
|
141
|
-
await runTelemetryStatus();
|
|
142
|
-
});
|
|
143
|
-
telemetry
|
|
144
|
-
.command("enable")
|
|
145
|
-
.description("enable telemetry")
|
|
146
|
-
.action(async () => {
|
|
147
|
-
await setTelemetryEnabled(true);
|
|
148
|
-
const updated = await updateDeploymentEnvTelemetry(true);
|
|
149
|
-
log.success("Telemetry enabled.");
|
|
150
|
-
if (updated) {
|
|
151
|
-
log.info(`Updated ${updated}. Restart the stack with \`elmo start\` to apply.`);
|
|
152
|
-
}
|
|
153
|
-
console.log(` Details: ${link(pc.cyan(TELEMETRY_DOC_URL), TELEMETRY_DOC_URL)}`);
|
|
154
|
-
});
|
|
155
|
-
telemetry
|
|
156
|
-
.command("disable")
|
|
157
|
-
.description("disable telemetry")
|
|
158
|
-
.action(async () => {
|
|
159
|
-
await setTelemetryEnabled(false);
|
|
160
|
-
const updated = await updateDeploymentEnvTelemetry(false);
|
|
161
|
-
log.success("Telemetry disabled. No further events will be sent.");
|
|
162
|
-
if (updated) {
|
|
163
|
-
log.info(`Updated ${updated}. Restart the stack with \`elmo start\` to apply.`);
|
|
164
|
-
}
|
|
90
|
+
.command("edit")
|
|
91
|
+
.description("change API keys, scrape targets, or the Docker Compose YAML")
|
|
92
|
+
.argument("<env|compose>", "which config file to edit")
|
|
93
|
+
.action(async (target, _opts, cmd) => {
|
|
94
|
+
await runEdit(target, cmd.optsWithGlobals());
|
|
165
95
|
});
|
|
166
96
|
await program.parseAsync(process.argv);
|
|
167
97
|
}
|
|
168
|
-
async function runTelemetryStatus() {
|
|
169
|
-
const status = await getTelemetryStatus();
|
|
170
|
-
const state = status.enabled ? pc.green("enabled") : pc.yellow("disabled");
|
|
171
|
-
console.log(`Telemetry: ${state}`);
|
|
172
|
-
if (status.source === "env") {
|
|
173
|
-
console.log(" Source: DISABLE_TELEMETRY environment variable");
|
|
174
|
-
}
|
|
175
|
-
else if (status.source === "config") {
|
|
176
|
-
console.log(` Source: ${CONFIG_FILE}`);
|
|
177
|
-
}
|
|
178
|
-
if (status.distinctId) {
|
|
179
|
-
console.log(` Install ID: ${status.distinctId}`);
|
|
180
|
-
}
|
|
181
|
-
console.log(` What we collect: ${link(pc.cyan(TELEMETRY_DOC_URL), TELEMETRY_DOC_URL)}`);
|
|
182
|
-
console.log(" Toggle: `elmo telemetry enable` / `elmo telemetry disable`");
|
|
183
|
-
}
|
|
184
98
|
async function withVersionCheck(version, fn) {
|
|
185
99
|
const notifyPromise = maybeNotifyNewVersion(version);
|
|
186
100
|
await fn();
|
|
@@ -192,24 +106,13 @@ async function runInit(options, version) {
|
|
|
192
106
|
p.intro(pc.bold("Setting up Elmo"));
|
|
193
107
|
const cwd = process.cwd();
|
|
194
108
|
// ── Resolve config directory ─────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
if (options.dir) {
|
|
197
|
-
configDir = path.resolve(cwd, options.dir);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
const dir = await p.text({
|
|
201
|
-
message: "Where should the config be stored?",
|
|
202
|
-
placeholder: "./elmo",
|
|
203
|
-
defaultValue: "./elmo",
|
|
204
|
-
});
|
|
205
|
-
assertNotCancelled(dir);
|
|
206
|
-
configDir = path.resolve(cwd, dir);
|
|
207
|
-
}
|
|
109
|
+
const configDir = options.dir ? path.resolve(cwd, options.dir) : CONFIG_HOME;
|
|
208
110
|
// ── .env safety check ────────────────────────────────────────────────
|
|
209
111
|
const existingEnvPath = path.join(configDir, ".env");
|
|
112
|
+
let preservedDeploymentId;
|
|
210
113
|
if (await fileExists(existingEnvPath)) {
|
|
211
114
|
const contents = await fs.readFile(existingEnvPath, "utf8");
|
|
212
|
-
const isElmoEnv = contents.startsWith("# Generated by elmo");
|
|
115
|
+
const isElmoEnv = contents.startsWith("# Rendered by elmo") || contents.startsWith("# Generated by elmo");
|
|
213
116
|
if (!isElmoEnv) {
|
|
214
117
|
p.log.warn(`A .env file already exists in ${configDir} and was NOT created by Elmo.`);
|
|
215
118
|
const overwrite = await p.confirm({
|
|
@@ -223,7 +126,17 @@ async function runInit(options, version) {
|
|
|
223
126
|
}
|
|
224
127
|
}
|
|
225
128
|
else {
|
|
226
|
-
p.log.
|
|
129
|
+
p.log.warn(`An existing Elmo config was found at ${configDir}.`);
|
|
130
|
+
const overwrite = await p.confirm({
|
|
131
|
+
message: "Overwrite it with new values? Existing secrets (DATABASE_URL, API keys, etc.) will be replaced.",
|
|
132
|
+
initialValue: false,
|
|
133
|
+
});
|
|
134
|
+
assertNotCancelled(overwrite);
|
|
135
|
+
if (!overwrite) {
|
|
136
|
+
p.cancel("Setup cancelled. Use `elmo edit env` to change individual values.");
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
preservedDeploymentId = parseDotenv(contents).DEPLOYMENT_ID;
|
|
227
140
|
}
|
|
228
141
|
}
|
|
229
142
|
// ── Dev mode: resolve docker directory ───────────────────────────────
|
|
@@ -264,14 +177,12 @@ async function runInit(options, version) {
|
|
|
264
177
|
const env = {};
|
|
265
178
|
env.DEPLOYMENT_MODE = "local";
|
|
266
179
|
env.VITE_DEPLOYMENT_MODE = "local";
|
|
267
|
-
env.DEPLOYMENT_ID = crypto.randomUUID();
|
|
180
|
+
env.DEPLOYMENT_ID = preservedDeploymentId ?? crypto.randomUUID();
|
|
268
181
|
env.BETTER_AUTH_SECRET = generateSecret();
|
|
269
182
|
env.APP_NAME = DEFAULT_APP_NAME;
|
|
270
183
|
env.APP_ICON = DEFAULT_APP_ICON;
|
|
271
|
-
env.APP_URL = DEFAULT_APP_URL;
|
|
272
184
|
env.VITE_APP_NAME = DEFAULT_APP_NAME;
|
|
273
185
|
env.VITE_APP_ICON = DEFAULT_APP_ICON;
|
|
274
|
-
env.VITE_APP_URL = DEFAULT_APP_URL;
|
|
275
186
|
if (postgresMode === "external") {
|
|
276
187
|
p.note("Must be an IPv4-compatible direct connection or database pooler.", "DATABASE_URL");
|
|
277
188
|
const url = await p.password({
|
|
@@ -296,7 +207,7 @@ async function runInit(options, version) {
|
|
|
296
207
|
"on what to fix or build next.",
|
|
297
208
|
"",
|
|
298
209
|
pc.bold("What we send:"),
|
|
299
|
-
" •
|
|
210
|
+
" • deployment ID (random UUID stored as DEPLOYMENT_ID in your .env)",
|
|
300
211
|
" • CLI/app version, OS, arch, Node version, deployment mode",
|
|
301
212
|
" • command/event names + non-secret options (e.g. postgres mode)",
|
|
302
213
|
" • feature counts (prompts edited, brands created — never the names or text)",
|
|
@@ -306,14 +217,13 @@ async function runInit(options, version) {
|
|
|
306
217
|
" API keys, .env contents, brand names, prompt text, and scraped responses.",
|
|
307
218
|
"",
|
|
308
219
|
`Full breakdown: ${link(pc.cyan(TELEMETRY_DOC_URL), TELEMETRY_DOC_URL)}`,
|
|
309
|
-
"Toggle later
|
|
220
|
+
"Toggle later by editing DISABLE_TELEMETRY in .env (`elmo edit env`).",
|
|
310
221
|
].join("\n"), "Telemetry");
|
|
311
222
|
const telemetryEnabled = await p.confirm({
|
|
312
223
|
message: "Share telemetry?",
|
|
313
224
|
initialValue: true,
|
|
314
225
|
});
|
|
315
226
|
assertNotCancelled(telemetryEnabled);
|
|
316
|
-
await setTelemetryEnabled(telemetryEnabled);
|
|
317
227
|
if (!telemetryEnabled) {
|
|
318
228
|
env.DISABLE_TELEMETRY = "1";
|
|
319
229
|
}
|
|
@@ -323,12 +233,33 @@ async function runInit(options, version) {
|
|
|
323
233
|
placeholder: "you@example.com",
|
|
324
234
|
});
|
|
325
235
|
const email = p.isCancel(updatesEmail) ? undefined : updatesEmail || undefined;
|
|
236
|
+
// ── Web app port ────────────────────────────────────────────────────
|
|
237
|
+
const portInput = await p.text({
|
|
238
|
+
message: "Web app port",
|
|
239
|
+
placeholder: String(DEFAULT_APP_PORT),
|
|
240
|
+
defaultValue: String(DEFAULT_APP_PORT),
|
|
241
|
+
validate: (v) => {
|
|
242
|
+
if (!v)
|
|
243
|
+
return undefined;
|
|
244
|
+
const n = Number(v);
|
|
245
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
246
|
+
return "Must be an integer between 1 and 65535";
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
assertNotCancelled(portInput);
|
|
252
|
+
const port = Number(portInput);
|
|
253
|
+
env.APP_URL = `http://localhost:${port}`;
|
|
254
|
+
env.VITE_APP_URL = env.APP_URL;
|
|
326
255
|
// ── Write config ─────────────────────────────────────────────────────
|
|
327
256
|
const composeYaml = buildComposeYaml({
|
|
328
257
|
dev: Boolean(options.dev),
|
|
329
258
|
postgresMode,
|
|
330
259
|
repoRoot,
|
|
331
260
|
dockerDir,
|
|
261
|
+
port,
|
|
262
|
+
version,
|
|
332
263
|
});
|
|
333
264
|
await ensureDir(configDir);
|
|
334
265
|
await writeConfigFiles(configDir, {
|
|
@@ -336,13 +267,7 @@ async function runInit(options, version) {
|
|
|
336
267
|
composeYaml,
|
|
337
268
|
postgresMode,
|
|
338
269
|
dev: Boolean(options.dev),
|
|
339
|
-
|
|
340
|
-
await writeGlobalConfig({
|
|
341
|
-
configDir,
|
|
342
|
-
dockerDir,
|
|
343
|
-
dev: Boolean(options.dev),
|
|
344
|
-
postgresMode,
|
|
345
|
-
repoRoot,
|
|
270
|
+
version,
|
|
346
271
|
});
|
|
347
272
|
p.log.success(`Config written to ${configDir}`);
|
|
348
273
|
p.log.warn("Your generated .env file contains secrets — do not commit it to version control.");
|
|
@@ -358,10 +283,10 @@ async function runInit(options, version) {
|
|
|
358
283
|
await doStart(configDir);
|
|
359
284
|
}
|
|
360
285
|
else {
|
|
361
|
-
p.log.info("You can start later with `elmo
|
|
286
|
+
p.log.info("You can start later with `elmo compose up -d`.");
|
|
362
287
|
}
|
|
363
288
|
// CLI telemetry — silently dropped if the user opted out above.
|
|
364
|
-
await trackCliEvent("cli_init", {
|
|
289
|
+
await trackCliEvent(configDir, "cli_init", {
|
|
365
290
|
version,
|
|
366
291
|
os: process.platform,
|
|
367
292
|
arch: process.arch,
|
|
@@ -375,7 +300,7 @@ async function runInit(options, version) {
|
|
|
375
300
|
// Newsletter signup is a separate, explicit opt-in and runs even when
|
|
376
301
|
// telemetry is disabled.
|
|
377
302
|
if (email) {
|
|
378
|
-
await submitNewsletterSignup(email);
|
|
303
|
+
await submitNewsletterSignup(configDir, email);
|
|
379
304
|
}
|
|
380
305
|
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")}`);
|
|
381
306
|
p.outro(pc.green("Setup complete!"));
|
|
@@ -460,7 +385,7 @@ async function configureProvidersRecommended(env) {
|
|
|
460
385
|
initialValue: "openrouter",
|
|
461
386
|
});
|
|
462
387
|
assertNotCancelled(direct);
|
|
463
|
-
await collectDirectApiQuick(direct, env
|
|
388
|
+
await collectDirectApiQuick(direct, env);
|
|
464
389
|
await finalizeScrapeTargets(env, targets, { skipEdit: true });
|
|
465
390
|
}
|
|
466
391
|
async function configureProvidersCustom(env) {
|
|
@@ -506,7 +431,7 @@ async function collectScraperKey(scraper, env) {
|
|
|
506
431
|
env.OLOSTEP_API_KEY = key;
|
|
507
432
|
}
|
|
508
433
|
}
|
|
509
|
-
async function collectDirectApiQuick(kind, env
|
|
434
|
+
async function collectDirectApiQuick(kind, env) {
|
|
510
435
|
if (kind === "openrouter") {
|
|
511
436
|
const key = await p.password({
|
|
512
437
|
message: "OpenRouter API key",
|
|
@@ -514,7 +439,6 @@ async function collectDirectApiQuick(kind, env, targets) {
|
|
|
514
439
|
});
|
|
515
440
|
assertNotCancelled(key);
|
|
516
441
|
env.OPENROUTER_API_KEY = key;
|
|
517
|
-
targets.push(`claude:openrouter:${DEFAULT_OPENROUTER_MODEL}:online`);
|
|
518
442
|
}
|
|
519
443
|
else if (kind === "anthropic") {
|
|
520
444
|
const key = await p.password({
|
|
@@ -523,7 +447,6 @@ async function collectDirectApiQuick(kind, env, targets) {
|
|
|
523
447
|
});
|
|
524
448
|
assertNotCancelled(key);
|
|
525
449
|
env.ANTHROPIC_API_KEY = key;
|
|
526
|
-
targets.push(`claude:anthropic-api:${DEFAULT_ANTHROPIC_MODEL}:online`);
|
|
527
450
|
}
|
|
528
451
|
else if (kind === "openai") {
|
|
529
452
|
const key = await p.password({
|
|
@@ -532,7 +455,6 @@ async function collectDirectApiQuick(kind, env, targets) {
|
|
|
532
455
|
});
|
|
533
456
|
assertNotCancelled(key);
|
|
534
457
|
env.OPENAI_API_KEY = key;
|
|
535
|
-
targets.push(`chatgpt:openai-api:${DEFAULT_OPENAI_MODEL}:online`);
|
|
536
458
|
}
|
|
537
459
|
else {
|
|
538
460
|
const key = await p.password({
|
|
@@ -541,7 +463,6 @@ async function collectDirectApiQuick(kind, env, targets) {
|
|
|
541
463
|
});
|
|
542
464
|
assertNotCancelled(key);
|
|
543
465
|
env.MISTRAL_API_KEY = key;
|
|
544
|
-
targets.push(`mistral:mistral-api:${DEFAULT_MISTRAL_MODEL}:online`);
|
|
545
466
|
}
|
|
546
467
|
}
|
|
547
468
|
async function collectBrightData(env, targets) {
|
|
@@ -797,51 +718,7 @@ function dedupeTargets(targets) {
|
|
|
797
718
|
}
|
|
798
719
|
return out.join(",");
|
|
799
720
|
}
|
|
800
|
-
// ──
|
|
801
|
-
async function runRegen(options) {
|
|
802
|
-
const configDir = await resolveConfigDir(options.dir);
|
|
803
|
-
const globalConfig = await readGlobalConfig();
|
|
804
|
-
// Determine settings: prefer global config, fall back to detecting from files
|
|
805
|
-
let dev;
|
|
806
|
-
let postgresMode;
|
|
807
|
-
let repoRoot;
|
|
808
|
-
let dockerDir;
|
|
809
|
-
if (globalConfig?.postgresMode) {
|
|
810
|
-
dev = globalConfig.dev ?? false;
|
|
811
|
-
postgresMode = globalConfig.postgresMode;
|
|
812
|
-
repoRoot = globalConfig.repoRoot ?? process.cwd();
|
|
813
|
-
dockerDir = globalConfig.dockerDir;
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
const detected = await detectSettingsFromConfig(configDir);
|
|
817
|
-
dev = detected.dev;
|
|
818
|
-
postgresMode = detected.postgresMode;
|
|
819
|
-
repoRoot = detected.repoRoot;
|
|
820
|
-
dockerDir = detected.dockerDir;
|
|
821
|
-
}
|
|
822
|
-
const composeYaml = buildComposeYaml({
|
|
823
|
-
dev,
|
|
824
|
-
postgresMode,
|
|
825
|
-
repoRoot,
|
|
826
|
-
dockerDir,
|
|
827
|
-
});
|
|
828
|
-
const composePath = path.join(configDir, "elmo.yaml");
|
|
829
|
-
await fs.writeFile(composePath, composeYaml, "utf8");
|
|
830
|
-
await writeGlobalConfig({
|
|
831
|
-
configDir,
|
|
832
|
-
dockerDir,
|
|
833
|
-
dev,
|
|
834
|
-
postgresMode,
|
|
835
|
-
repoRoot,
|
|
836
|
-
});
|
|
837
|
-
log.success(`Regenerated ${composePath}`);
|
|
838
|
-
log.info("The .env file was not modified.");
|
|
839
|
-
}
|
|
840
|
-
// ── Command: start ───────────────────────────────────────────────────────────
|
|
841
|
-
async function runStart(options) {
|
|
842
|
-
const configDir = await resolveConfigDir(options.dir);
|
|
843
|
-
await doStart(configDir);
|
|
844
|
-
}
|
|
721
|
+
// ── Start helper (used by init) ──────────────────────────────────────────────
|
|
845
722
|
async function doStart(configDir) {
|
|
846
723
|
assertDockerRunning();
|
|
847
724
|
log.step("Starting Docker Compose stack...");
|
|
@@ -857,71 +734,48 @@ async function doStart(configDir) {
|
|
|
857
734
|
p.log.warn("Some services did not report healthy status.");
|
|
858
735
|
}
|
|
859
736
|
log.info("Examples:");
|
|
860
|
-
console.log(` ${pc.bold("elmo logs -f")}`);
|
|
737
|
+
console.log(` ${pc.bold("elmo compose logs -f")}`);
|
|
861
738
|
console.log(` ${pc.bold("elmo compose logs -f web")}`);
|
|
862
739
|
console.log(` ${pc.bold("elmo compose ps")}`);
|
|
740
|
+
console.log(` ${pc.bold("elmo compose down")}`);
|
|
863
741
|
}
|
|
864
|
-
// ── Command:
|
|
865
|
-
async function
|
|
742
|
+
// ── Command: compose ─────────────────────────────────────────────────────────
|
|
743
|
+
async function runCompose(args, options) {
|
|
866
744
|
const configDir = await resolveConfigDir(options.dir);
|
|
867
745
|
assertDockerRunning();
|
|
868
|
-
|
|
869
|
-
await runDockerCompose(configDir, ["down"]);
|
|
870
|
-
log.success("Stack stopped.");
|
|
746
|
+
await runDockerCompose(configDir, args);
|
|
871
747
|
}
|
|
872
|
-
// ── Command:
|
|
873
|
-
async function
|
|
748
|
+
// ── Command: edit ────────────────────────────────────────────────────────────
|
|
749
|
+
async function runEdit(target, options) {
|
|
874
750
|
const configDir = await resolveConfigDir(options.dir);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
log.warn("No running services found.");
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
let allHealthy = true;
|
|
882
|
-
for (const service of services) {
|
|
883
|
-
const status = formatServiceStatus(service);
|
|
884
|
-
console.log(status);
|
|
885
|
-
if (!isServiceReady(service)) {
|
|
886
|
-
allHealthy = false;
|
|
887
|
-
}
|
|
751
|
+
let filePath;
|
|
752
|
+
if (target === "env") {
|
|
753
|
+
filePath = path.join(configDir, ".env");
|
|
888
754
|
}
|
|
889
|
-
if (
|
|
890
|
-
|
|
755
|
+
else if (target === "compose") {
|
|
756
|
+
filePath = path.join(configDir, "elmo.yaml");
|
|
891
757
|
}
|
|
892
758
|
else {
|
|
893
|
-
|
|
759
|
+
throw new Error(`Unknown edit target: ${target}. Use \`env\` or \`compose\`.`);
|
|
894
760
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
async function runCompose(args, options) {
|
|
898
|
-
const configDir = await resolveConfigDir(options.dir);
|
|
899
|
-
assertDockerRunning();
|
|
900
|
-
await runDockerCompose(configDir, args);
|
|
901
|
-
}
|
|
902
|
-
// ── Command: build ───────────────────────────────────────────────────────────
|
|
903
|
-
async function runBuild(options) {
|
|
904
|
-
const configDir = await resolveConfigDir(options.dir);
|
|
905
|
-
assertDockerRunning();
|
|
906
|
-
const buildWeb = !options.workerOnly;
|
|
907
|
-
const buildWorker = !options.webOnly;
|
|
908
|
-
const noCache = options.cache === false;
|
|
909
|
-
const targets = [];
|
|
910
|
-
if (buildWeb)
|
|
911
|
-
targets.push("web");
|
|
912
|
-
if (buildWorker)
|
|
913
|
-
targets.push("worker");
|
|
914
|
-
log.step(`Building Docker images: ${targets.join(", ")}${noCache ? " (no cache)" : ""}...`);
|
|
915
|
-
const buildArgs = ["build"];
|
|
916
|
-
if (noCache)
|
|
917
|
-
buildArgs.push("--no-cache");
|
|
918
|
-
for (const target of targets) {
|
|
919
|
-
log.step(`Building ${target}...`);
|
|
920
|
-
await runDockerCompose(configDir, [...buildArgs, target]);
|
|
921
|
-
log.success(`${target} image built successfully.`);
|
|
761
|
+
if (!(await fileExists(filePath))) {
|
|
762
|
+
throw new Error(`File not found: ${filePath}`);
|
|
922
763
|
}
|
|
923
|
-
|
|
924
|
-
|
|
764
|
+
const editorEnv = process.env.VISUAL || process.env.EDITOR || "nano";
|
|
765
|
+
const parts = editorEnv.split(/\s+/).filter(Boolean);
|
|
766
|
+
const cmd = parts[0] ?? "nano";
|
|
767
|
+
const args = [...parts.slice(1), filePath];
|
|
768
|
+
await new Promise((resolve, reject) => {
|
|
769
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
770
|
+
child.on("close", (code) => {
|
|
771
|
+
if (code === 0)
|
|
772
|
+
resolve();
|
|
773
|
+
else
|
|
774
|
+
reject(new Error(`${cmd} exited with code ${code}`));
|
|
775
|
+
});
|
|
776
|
+
child.on("error", (err) => reject(err));
|
|
777
|
+
});
|
|
778
|
+
log.info("Restart the stack with `elmo compose up -d` to apply changes.");
|
|
925
779
|
}
|
|
926
780
|
// ── Compose YAML Builder ─────────────────────────────────────────────────────
|
|
927
781
|
function buildComposeYaml(options) {
|
|
@@ -942,6 +796,7 @@ function buildComposeYaml(options) {
|
|
|
942
796
|
dev: options.dev,
|
|
943
797
|
dockerfilePath,
|
|
944
798
|
repoRoot: options.repoRoot,
|
|
799
|
+
version: options.version,
|
|
945
800
|
}));
|
|
946
801
|
dependsOnWeb.push("db-migrate");
|
|
947
802
|
dependsOnWorker.push("db-migrate");
|
|
@@ -953,6 +808,8 @@ function buildComposeYaml(options) {
|
|
|
953
808
|
dependencyConditions,
|
|
954
809
|
repoRoot: options.repoRoot,
|
|
955
810
|
dockerfilePath,
|
|
811
|
+
port: options.port,
|
|
812
|
+
version: options.version,
|
|
956
813
|
}));
|
|
957
814
|
services.push(buildWorkerService({
|
|
958
815
|
dev: options.dev,
|
|
@@ -960,8 +817,9 @@ function buildComposeYaml(options) {
|
|
|
960
817
|
dependencyConditions,
|
|
961
818
|
repoRoot: options.repoRoot,
|
|
962
819
|
dockerfilePath,
|
|
820
|
+
version: options.version,
|
|
963
821
|
}));
|
|
964
|
-
const lines = ["name: elmo", "", "services:"];
|
|
822
|
+
const lines = [renderedByHeader(options.version), "", "name: elmo", "", "services:"];
|
|
965
823
|
lines.push(...services.map((service) => indentBlock(service, 2)));
|
|
966
824
|
if (volumes.size > 0) {
|
|
967
825
|
lines.push("", "volumes:");
|
|
@@ -997,7 +855,7 @@ function buildDbMigrateService(options) {
|
|
|
997
855
|
lines.push(" build:", ` context: ${options.repoRoot}`, ` dockerfile: ${options.dockerfilePath}`, " target: migrate");
|
|
998
856
|
}
|
|
999
857
|
else {
|
|
1000
|
-
lines.push(
|
|
858
|
+
lines.push(` image: elmohq/elmo-db-migrate:${options.version}`);
|
|
1001
859
|
}
|
|
1002
860
|
lines.push(" environment:", " - DATABASE_URL=postgres://postgres:postgres@postgres:5432/elmo", " depends_on:", " postgres:", " condition: service_healthy");
|
|
1003
861
|
return lines.join("\n");
|
|
@@ -1008,9 +866,9 @@ function buildWebService(options) {
|
|
|
1008
866
|
lines.push(" build:", ` context: ${options.repoRoot}`, ` dockerfile: ${options.dockerfilePath}`, " target: web", " args:", " DEPLOYMENT_MODE: local");
|
|
1009
867
|
}
|
|
1010
868
|
else {
|
|
1011
|
-
lines.push(
|
|
869
|
+
lines.push(` image: elmohq/elmo-web:${options.version}`);
|
|
1012
870
|
}
|
|
1013
|
-
lines.push(" env_file:", " - path: .env", " required: true", " ports:",
|
|
871
|
+
lines.push(" env_file:", " - path: .env", " required: true", " ports:", ` - "${options.port}:3000"`);
|
|
1014
872
|
if (options.dependsOn.length > 0) {
|
|
1015
873
|
lines.push(" depends_on:");
|
|
1016
874
|
for (const service of options.dependsOn) {
|
|
@@ -1026,7 +884,7 @@ function buildWorkerService(options) {
|
|
|
1026
884
|
lines.push(" build:", ` context: ${options.repoRoot}`, ` dockerfile: ${options.dockerfilePath}`, " target: worker", " args:", " DEPLOYMENT_MODE: local");
|
|
1027
885
|
}
|
|
1028
886
|
else {
|
|
1029
|
-
lines.push(
|
|
887
|
+
lines.push(` image: elmohq/elmo-worker:${options.version}`);
|
|
1030
888
|
}
|
|
1031
889
|
lines.push(" env_file:", " - path: .env", " required: true");
|
|
1032
890
|
if (options.dependsOn.length > 0) {
|
|
@@ -1076,19 +934,6 @@ async function getComposeServices(configDir) {
|
|
|
1076
934
|
}
|
|
1077
935
|
}
|
|
1078
936
|
}
|
|
1079
|
-
function formatServiceStatus(service) {
|
|
1080
|
-
const health = service.Health ?? "unknown";
|
|
1081
|
-
const state = service.State ?? "unknown";
|
|
1082
|
-
const label = `${service.Service}`.padEnd(16, " ");
|
|
1083
|
-
let color = pc.red;
|
|
1084
|
-
if (isServiceReady(service)) {
|
|
1085
|
-
color = pc.green;
|
|
1086
|
-
}
|
|
1087
|
-
else if (health === "starting" || state.includes("starting")) {
|
|
1088
|
-
color = pc.yellow;
|
|
1089
|
-
}
|
|
1090
|
-
return color(`${label} ${state} ${service.Health ? `(${health})` : ""}`.trim());
|
|
1091
|
-
}
|
|
1092
937
|
function isServiceReady(service) {
|
|
1093
938
|
if (service.Health) {
|
|
1094
939
|
return service.Health === "healthy";
|
|
@@ -1196,52 +1041,32 @@ async function resolveDockerDirAuto(cwd, explicitDir) {
|
|
|
1196
1041
|
}
|
|
1197
1042
|
// ── Config Dir Resolution ────────────────────────────────────────────────────
|
|
1198
1043
|
async function resolveConfigDir(explicitDir) {
|
|
1199
|
-
|
|
1200
|
-
const resolved = path.resolve(process.cwd(), explicitDir);
|
|
1201
|
-
const composePath = path.join(resolved, "elmo.yaml");
|
|
1202
|
-
if (!(await fileExists(composePath))) {
|
|
1203
|
-
throw new Error(`Config directory does not contain elmo.yaml: ${resolved}\nRun \`elmo init\` to create your local setup.`);
|
|
1204
|
-
}
|
|
1205
|
-
return resolved;
|
|
1206
|
-
}
|
|
1207
|
-
const config = await readGlobalConfig();
|
|
1208
|
-
if (!config?.configDir) {
|
|
1209
|
-
throw new Error("No config found. Run `elmo init` or specify --dir.");
|
|
1210
|
-
}
|
|
1211
|
-
const resolved = path.resolve(config.configDir);
|
|
1044
|
+
const resolved = explicitDir ? path.resolve(process.cwd(), explicitDir) : CONFIG_HOME;
|
|
1212
1045
|
const composePath = path.join(resolved, "elmo.yaml");
|
|
1213
1046
|
if (!(await fileExists(composePath))) {
|
|
1214
|
-
|
|
1047
|
+
if (explicitDir) {
|
|
1048
|
+
throw new Error(`Config directory does not contain elmo.yaml: ${resolved}\nRun \`elmo init --dir ${explicitDir}\` to create it.`);
|
|
1049
|
+
}
|
|
1050
|
+
throw new Error(`No config found at ${resolved}. Run \`elmo init\` to create one, or specify --dir.`);
|
|
1215
1051
|
}
|
|
1216
1052
|
return resolved;
|
|
1217
1053
|
}
|
|
1218
1054
|
// ── File & Config Helpers ────────────────────────────────────────────────────
|
|
1219
|
-
async function readGlobalConfig() {
|
|
1220
|
-
try {
|
|
1221
|
-
const contents = await fs.readFile(CONFIG_FILE, "utf8");
|
|
1222
|
-
return JSON.parse(contents);
|
|
1223
|
-
}
|
|
1224
|
-
catch {
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
async function writeGlobalConfig(config) {
|
|
1229
|
-
const globalConfig = {
|
|
1230
|
-
...config,
|
|
1231
|
-
updatedAt: new Date().toISOString(),
|
|
1232
|
-
};
|
|
1233
|
-
await ensureDir(CONFIG_HOME);
|
|
1234
|
-
await fs.writeFile(CONFIG_FILE, JSON.stringify(globalConfig, null, 2), "utf8");
|
|
1235
|
-
}
|
|
1236
1055
|
async function writeConfigFiles(configDir, initConfig) {
|
|
1237
1056
|
const envPath = path.join(configDir, ".env");
|
|
1238
1057
|
const composePath = path.join(configDir, "elmo.yaml");
|
|
1239
1058
|
await ensureDir(configDir);
|
|
1240
|
-
await fs.writeFile(envPath, buildEnvFile(initConfig.env), "utf8");
|
|
1059
|
+
await fs.writeFile(envPath, buildEnvFile(initConfig.env, initConfig.version), "utf8");
|
|
1241
1060
|
await fs.writeFile(composePath, initConfig.composeYaml, "utf8");
|
|
1242
1061
|
}
|
|
1243
|
-
function
|
|
1244
|
-
|
|
1062
|
+
function renderedByHeader(version) {
|
|
1063
|
+
return [
|
|
1064
|
+
`# Rendered by elmo ${version} on ${new Date().toISOString()}`,
|
|
1065
|
+
"# Re-run `elmo init` after upgrading the CLI to refresh this file.",
|
|
1066
|
+
].join("\n");
|
|
1067
|
+
}
|
|
1068
|
+
function buildEnvFile(env, version) {
|
|
1069
|
+
const lines = [renderedByHeader(version), "# WARNING: contains secrets. Do not commit.", ""];
|
|
1245
1070
|
for (const [key, rawValue] of Object.entries(env)) {
|
|
1246
1071
|
if (rawValue === undefined) {
|
|
1247
1072
|
continue;
|
|
@@ -1260,68 +1085,6 @@ function formatEnvValue(value) {
|
|
|
1260
1085
|
}
|
|
1261
1086
|
return value;
|
|
1262
1087
|
}
|
|
1263
|
-
async function detectSettingsFromConfig(configDir) {
|
|
1264
|
-
const envPath = path.join(configDir, ".env");
|
|
1265
|
-
const env = await readEnvFile(envPath);
|
|
1266
|
-
const postgresMode = env.DATABASE_URL?.includes("postgres:5432") ? "docker" : "external";
|
|
1267
|
-
const yamlPath = path.join(configDir, "elmo.yaml");
|
|
1268
|
-
let dev = false;
|
|
1269
|
-
let repoRoot = process.cwd();
|
|
1270
|
-
try {
|
|
1271
|
-
const yamlContent = await fs.readFile(yamlPath, "utf8");
|
|
1272
|
-
dev = yamlContent.includes("build:");
|
|
1273
|
-
const contextMatch = yamlContent.match(/context:\s+(.+)/);
|
|
1274
|
-
if (contextMatch) {
|
|
1275
|
-
repoRoot = contextMatch[1].trim();
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
catch {
|
|
1279
|
-
// Use defaults
|
|
1280
|
-
}
|
|
1281
|
-
return { dev, postgresMode, repoRoot };
|
|
1282
|
-
}
|
|
1283
|
-
async function updateDeploymentEnvTelemetry(enabled) {
|
|
1284
|
-
const config = await readGlobalConfig();
|
|
1285
|
-
if (!config?.configDir)
|
|
1286
|
-
return null;
|
|
1287
|
-
const envPath = path.join(config.configDir, ".env");
|
|
1288
|
-
if (!(await fileExists(envPath)))
|
|
1289
|
-
return null;
|
|
1290
|
-
const contents = await fs.readFile(envPath, "utf8");
|
|
1291
|
-
const lines = contents.split("\n");
|
|
1292
|
-
const filtered = lines.filter((line) => !/^\s*DISABLE_TELEMETRY\s*=/.test(line));
|
|
1293
|
-
if (!enabled) {
|
|
1294
|
-
const insertIdx = filtered.length > 0 && filtered[filtered.length - 1] === "" ? filtered.length - 1 : filtered.length;
|
|
1295
|
-
filtered.splice(insertIdx, 0, "DISABLE_TELEMETRY=1");
|
|
1296
|
-
}
|
|
1297
|
-
await fs.writeFile(envPath, filtered.join("\n"), "utf8");
|
|
1298
|
-
return envPath;
|
|
1299
|
-
}
|
|
1300
|
-
async function readEnvFile(envPath) {
|
|
1301
|
-
try {
|
|
1302
|
-
const contents = await fs.readFile(envPath, "utf8");
|
|
1303
|
-
const env = {};
|
|
1304
|
-
for (const line of contents.split("\n")) {
|
|
1305
|
-
const trimmed = line.trim();
|
|
1306
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
1307
|
-
continue;
|
|
1308
|
-
const eqIndex = trimmed.indexOf("=");
|
|
1309
|
-
if (eqIndex < 0)
|
|
1310
|
-
continue;
|
|
1311
|
-
const key = trimmed.substring(0, eqIndex);
|
|
1312
|
-
let value = trimmed.substring(eqIndex + 1);
|
|
1313
|
-
// Remove surrounding quotes
|
|
1314
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1315
|
-
value = value.slice(1, -1);
|
|
1316
|
-
}
|
|
1317
|
-
env[key] = value;
|
|
1318
|
-
}
|
|
1319
|
-
return env;
|
|
1320
|
-
}
|
|
1321
|
-
catch {
|
|
1322
|
-
return {};
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
1088
|
async function fileExists(target) {
|
|
1326
1089
|
try {
|
|
1327
1090
|
await fs.access(target);
|
package/dist/telemetry.js
CHANGED
|
@@ -1,67 +1,33 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
2
|
import path from "node:path";
|
|
3
|
+
import { parse } from "dotenv";
|
|
5
4
|
import { PostHog } from "posthog-node";
|
|
6
5
|
const POSTHOG_PUBLIC_KEY = "phc_Jhx9LnI9cTDFHpQmpOzJSDTW127qD9pFU65KRnYym6z";
|
|
7
6
|
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
function envDisabled() {
|
|
8
|
+
return Boolean(process.env.DISABLE_TELEMETRY);
|
|
9
|
+
}
|
|
10
|
+
async function readEnvKey(configDir, key) {
|
|
11
11
|
try {
|
|
12
|
-
const contents = await fs.readFile(
|
|
13
|
-
return
|
|
12
|
+
const contents = await fs.readFile(path.join(configDir, ".env"), "utf8");
|
|
13
|
+
return parse(contents)[key];
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
|
-
return
|
|
16
|
+
return undefined;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
async function
|
|
20
|
-
await fs.mkdir(CONFIG_HOME, { recursive: true });
|
|
21
|
-
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
22
|
-
}
|
|
23
|
-
function envDisabled() {
|
|
24
|
-
return Boolean(process.env.DISABLE_TELEMETRY);
|
|
25
|
-
}
|
|
26
|
-
export async function isTelemetryDisabled() {
|
|
19
|
+
async function isTelemetryDisabled(configDir) {
|
|
27
20
|
if (envDisabled())
|
|
28
21
|
return true;
|
|
29
|
-
|
|
30
|
-
return config.telemetryDisabled === true;
|
|
22
|
+
return Boolean(await readEnvKey(configDir, "DISABLE_TELEMETRY"));
|
|
31
23
|
}
|
|
32
|
-
export async function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else {
|
|
38
|
-
config.telemetryDisabled = true;
|
|
39
|
-
}
|
|
40
|
-
await writeConfig(config);
|
|
41
|
-
}
|
|
42
|
-
export async function getTelemetryStatus() {
|
|
43
|
-
if (envDisabled()) {
|
|
44
|
-
return { enabled: false, source: "env" };
|
|
45
|
-
}
|
|
46
|
-
const config = await readConfig();
|
|
47
|
-
if (config.telemetryDisabled === true) {
|
|
48
|
-
return { enabled: false, source: "config", distinctId: config.telemetryId };
|
|
49
|
-
}
|
|
50
|
-
return { enabled: true, source: "default", distinctId: config.telemetryId };
|
|
51
|
-
}
|
|
52
|
-
async function getOrCreateDistinctId() {
|
|
53
|
-
const config = await readConfig();
|
|
54
|
-
if (config.telemetryId)
|
|
55
|
-
return config.telemetryId;
|
|
56
|
-
const id = crypto.randomUUID();
|
|
57
|
-
await writeConfig({ ...config, telemetryId: id });
|
|
58
|
-
return id;
|
|
59
|
-
}
|
|
60
|
-
export async function trackCliEvent(eventName, properties, personProperties) {
|
|
61
|
-
if (await isTelemetryDisabled())
|
|
24
|
+
export async function trackCliEvent(configDir, eventName, properties, personProperties) {
|
|
25
|
+
if (await isTelemetryDisabled(configDir))
|
|
26
|
+
return;
|
|
27
|
+
const distinctId = await readEnvKey(configDir, "DEPLOYMENT_ID");
|
|
28
|
+
if (!distinctId)
|
|
62
29
|
return;
|
|
63
30
|
try {
|
|
64
|
-
const distinctId = await getOrCreateDistinctId();
|
|
65
31
|
const client = new PostHog(POSTHOG_PUBLIC_KEY, { host: POSTHOG_HOST });
|
|
66
32
|
client.capture({
|
|
67
33
|
distinctId,
|
|
@@ -77,14 +43,15 @@ export async function trackCliEvent(eventName, properties, personProperties) {
|
|
|
77
43
|
}
|
|
78
44
|
// Newsletter signup is an explicit user action with clear intent, so it
|
|
79
45
|
// fires even when anonymous telemetry is disabled. When telemetry is on the
|
|
80
|
-
// event is keyed off the
|
|
81
|
-
// property — same identity as the rest of the CLI events. When
|
|
82
|
-
// off the event is keyed off the email itself, so it is never
|
|
83
|
-
// to the anonymous
|
|
84
|
-
export async function submitNewsletterSignup(email) {
|
|
46
|
+
// event is keyed off the deployment UUID and the email is attached as a
|
|
47
|
+
// person property — same identity as the rest of the CLI events. When
|
|
48
|
+
// telemetry is off the event is keyed off the email itself, so it is never
|
|
49
|
+
// linked back to the anonymous deployment UUID.
|
|
50
|
+
export async function submitNewsletterSignup(configDir, email) {
|
|
85
51
|
try {
|
|
86
|
-
const disabled = await isTelemetryDisabled();
|
|
87
|
-
const
|
|
52
|
+
const disabled = await isTelemetryDisabled(configDir);
|
|
53
|
+
const deploymentId = disabled ? null : await readEnvKey(configDir, "DEPLOYMENT_ID");
|
|
54
|
+
const distinctId = deploymentId ?? email;
|
|
88
55
|
const client = new PostHog(POSTHOG_PUBLIC_KEY, { host: POSTHOG_HOST });
|
|
89
56
|
client.capture({
|
|
90
57
|
distinctId,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elmohq/cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.2.11",
|
|
4
|
+
"license": "MIT",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"marketing",
|
|
7
7
|
"typescript",
|
|
@@ -28,14 +28,15 @@
|
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@clack/prompts": "^1.
|
|
32
|
-
"commander": "^
|
|
31
|
+
"@clack/prompts": "^1.5.0",
|
|
32
|
+
"commander": "^15.0.0",
|
|
33
|
+
"dotenv": "17.4.2",
|
|
33
34
|
"picocolors": "^1.1.1",
|
|
34
|
-
"posthog-node": "^5.
|
|
35
|
-
"semver": "^7.
|
|
35
|
+
"posthog-node": "^5.35.6",
|
|
36
|
+
"semver": "^7.8.1"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^25.
|
|
39
|
+
"@types/node": "^25.9.1",
|
|
39
40
|
"@types/semver": "^7.7.1",
|
|
40
41
|
"typescript": "^6.0.3"
|
|
41
42
|
},
|