@elnora-ai/cli 1.3.4 → 1.4.0
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 +44 -0
- package/dist/cli.js +341 -161
- package/package.json +3 -1
- package/scripts/postinstall.mjs +33 -0
package/README.md
CHANGED
|
@@ -39,6 +39,50 @@ npm install -g @elnora-ai/cli
|
|
|
39
39
|
brew install elnora-ai/cli/elnora
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Skills & Setup
|
|
43
|
+
|
|
44
|
+
After installing the CLI, configure your AI coding tools:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
elnora setup
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This auto-detects which tools you have installed and configures them:
|
|
51
|
+
|
|
52
|
+
- **Claude Code** — registers the `elnora-plugins` marketplace and enables 9 skills (`elnora-tasks`, `elnora-projects`, `elnora-files`, etc.) so you can use natural language.
|
|
53
|
+
- **Cursor / VS Code / Codex** — writes an MCP server config pointing at `https://mcp.elnora.ai/mcp` with your API key.
|
|
54
|
+
|
|
55
|
+
Target a specific tool: `elnora setup claude` · `elnora setup cursor` · `elnora setup vscode` · `elnora setup codex`.
|
|
56
|
+
|
|
57
|
+
### What You Get in Claude Code
|
|
58
|
+
|
|
59
|
+
Once configured, restart Claude Code and try any natural-language request:
|
|
60
|
+
|
|
61
|
+
> "Use Elnora to generate a PCR protocol for BRCA1 exon 11"
|
|
62
|
+
> "Use Elnora to list my projects"
|
|
63
|
+
> "Use Elnora to optimize the protocol in task abc-123"
|
|
64
|
+
|
|
65
|
+
Claude will invoke the `elnora` CLI under the hood using the bundled skills. No need to memorize command syntax.
|
|
66
|
+
|
|
67
|
+
### Where Everything Lives
|
|
68
|
+
|
|
69
|
+
| What | Where |
|
|
70
|
+
|------|-------|
|
|
71
|
+
| CLI binary (macOS/Linux) | `~/.local/bin/elnora` |
|
|
72
|
+
| CLI binary (Windows) | `%USERPROFILE%\.elnora\bin\elnora.exe` |
|
|
73
|
+
| Auth / profiles | `~/.elnora/profiles.toml` (mode 0600) |
|
|
74
|
+
| Claude Code skills | `~/.claude/plugins/marketplaces/elnora-plugins/elnora/skills/` (auto-updated) |
|
|
75
|
+
| Claude Code settings | `~/.claude/settings.json` — look for `elnora@elnora-plugins: true` |
|
|
76
|
+
|
|
77
|
+
### Manual Plugin Setup
|
|
78
|
+
|
|
79
|
+
If `elnora setup claude` doesn't work for some reason, you can install the plugin manually:
|
|
80
|
+
|
|
81
|
+
1. In Claude Code, run `/plugin`
|
|
82
|
+
2. Choose "Add marketplace"
|
|
83
|
+
3. Enter: `Elnora-AI/elnora-plugins`
|
|
84
|
+
4. Run `/plugin` again → Plugins → Enable `elnora`
|
|
85
|
+
|
|
42
86
|
## Quick Start
|
|
43
87
|
|
|
44
88
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ var VERSION, PRODUCTION_BASE_URL, BASE_URL, AI_SERVER_URL, ENDPOINTS, DEFAULT_HE
|
|
|
25
25
|
var init_config = __esm({
|
|
26
26
|
"src/lib/config.ts"() {
|
|
27
27
|
"use strict";
|
|
28
|
-
VERSION = true ? "1.
|
|
28
|
+
VERSION = true ? "1.4.0" : process.env.npm_package_version ?? "0.0.0-dev";
|
|
29
29
|
PRODUCTION_BASE_URL = "https://platform.elnora.ai/api/v1";
|
|
30
30
|
BASE_URL = process.env.ELNORA_API_URL ?? PRODUCTION_BASE_URL;
|
|
31
31
|
AI_SERVER_URL = process.env.ELNORA_AI_SERVER_URL ?? "https://platform.elnora.ai/ai-server";
|
|
@@ -2005,18 +2005,70 @@ function addCompletionCommand(program2) {
|
|
|
2005
2005
|
init_client();
|
|
2006
2006
|
init_config();
|
|
2007
2007
|
init_profiles();
|
|
2008
|
-
import { existsSync as
|
|
2009
|
-
import
|
|
2008
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2009
|
+
import { homedir as homedir4 } from "node:os";
|
|
2010
|
+
import { delimiter, join as join4 } from "node:path";
|
|
2011
|
+
import pc3 from "picocolors";
|
|
2010
2012
|
|
|
2011
|
-
// src/lib/
|
|
2012
|
-
init_config();
|
|
2013
|
+
// src/lib/setup/common.ts
|
|
2013
2014
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2014
2015
|
import { homedir as homedir2 } from "node:os";
|
|
2015
2016
|
import { join as join2 } from "node:path";
|
|
2016
2017
|
import pc from "picocolors";
|
|
2018
|
+
var HOME = homedir2();
|
|
2019
|
+
var ELNORA_DIR = join2(HOME, ".elnora");
|
|
2020
|
+
var CLAUDE_DIR = join2(HOME, ".claude");
|
|
2021
|
+
var CLAUDE_MARKETPLACES_FILE = join2(CLAUDE_DIR, "plugins", "known_marketplaces.json");
|
|
2022
|
+
var CLAUDE_SETTINGS_FILE = join2(CLAUDE_DIR, "settings.json");
|
|
2023
|
+
var CURSOR_DIR = join2(HOME, ".cursor");
|
|
2024
|
+
var CURSOR_MCP_GLOBAL = join2(HOME, ".cursor", "mcp.json");
|
|
2025
|
+
var VSCODE_DIR = join2(HOME, ".vscode");
|
|
2026
|
+
var CODEX_DIR = join2(HOME, ".codex");
|
|
2027
|
+
var MARKETPLACE_NAME = "elnora-plugins";
|
|
2028
|
+
var MARKETPLACE_REPO = "Elnora-AI/elnora-plugins";
|
|
2029
|
+
var PLUGIN_ID = `elnora@${MARKETPLACE_NAME}`;
|
|
2030
|
+
var LEGACY_PLUGIN_IDS = ["elnora@elnora-local", "elnora@elnora-ai"];
|
|
2031
|
+
var LEGACY_MARKETPLACE_NAMES = ["elnora-local", "elnora-ai"];
|
|
2032
|
+
function readJsonFile(path) {
|
|
2033
|
+
try {
|
|
2034
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
2035
|
+
} catch {
|
|
2036
|
+
return {};
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function writeJsonFile(path, data) {
|
|
2040
|
+
mkdirSync2(join2(path, ".."), { recursive: true });
|
|
2041
|
+
writeFileSync2(path, `${JSON.stringify(data, null, 2)}
|
|
2042
|
+
`, "utf-8");
|
|
2043
|
+
}
|
|
2044
|
+
var color = isColorEnabled();
|
|
2045
|
+
function ok(msg) {
|
|
2046
|
+
return color ? ` ${pc.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
|
|
2047
|
+
}
|
|
2048
|
+
function fail(msg) {
|
|
2049
|
+
return color ? ` ${pc.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
|
|
2050
|
+
}
|
|
2051
|
+
function info(msg) {
|
|
2052
|
+
return color ? ` ${pc.dim(msg)}` : ` ${msg}`;
|
|
2053
|
+
}
|
|
2054
|
+
function detectPlatforms() {
|
|
2055
|
+
return [
|
|
2056
|
+
{ name: "claude", label: "Claude Code", installed: existsSync2(CLAUDE_DIR) },
|
|
2057
|
+
{ name: "cursor", label: "Cursor", installed: existsSync2(CURSOR_DIR) },
|
|
2058
|
+
{ name: "vscode", label: "VS Code", installed: existsSync2(VSCODE_DIR) },
|
|
2059
|
+
{ name: "codex", label: "OpenAI Codex", installed: existsSync2(CODEX_DIR) }
|
|
2060
|
+
];
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/lib/update-check.ts
|
|
2064
|
+
init_config();
|
|
2065
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2066
|
+
import { homedir as homedir3 } from "node:os";
|
|
2067
|
+
import { join as join3 } from "node:path";
|
|
2068
|
+
import pc2 from "picocolors";
|
|
2017
2069
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@elnora-ai/cli/latest";
|
|
2018
|
-
var CACHE_DIR =
|
|
2019
|
-
var CACHE_FILE =
|
|
2070
|
+
var CACHE_DIR = join3(homedir3(), ".elnora");
|
|
2071
|
+
var CACHE_FILE = join3(CACHE_DIR, ".update-check");
|
|
2020
2072
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2021
2073
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
2022
2074
|
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
|
|
@@ -2027,8 +2079,8 @@ function shouldSkipEntirely() {
|
|
|
2027
2079
|
}
|
|
2028
2080
|
function readCache() {
|
|
2029
2081
|
try {
|
|
2030
|
-
if (!
|
|
2031
|
-
const raw =
|
|
2082
|
+
if (!existsSync3(CACHE_FILE)) return null;
|
|
2083
|
+
const raw = readFileSync3(CACHE_FILE, "utf-8");
|
|
2032
2084
|
return JSON.parse(raw);
|
|
2033
2085
|
} catch {
|
|
2034
2086
|
return null;
|
|
@@ -2036,8 +2088,8 @@ function readCache() {
|
|
|
2036
2088
|
}
|
|
2037
2089
|
function writeCache(entry) {
|
|
2038
2090
|
try {
|
|
2039
|
-
|
|
2040
|
-
|
|
2091
|
+
mkdirSync3(CACHE_DIR, { recursive: true });
|
|
2092
|
+
writeFileSync3(CACHE_FILE, JSON.stringify(entry), "utf-8");
|
|
2041
2093
|
} catch {
|
|
2042
2094
|
}
|
|
2043
2095
|
}
|
|
@@ -2045,7 +2097,7 @@ function showUpdateNotice(latest) {
|
|
|
2045
2097
|
if (!process.stderr.isTTY) return;
|
|
2046
2098
|
const color2 = isColorEnabled();
|
|
2047
2099
|
const msg = color2 ? `
|
|
2048
|
-
${
|
|
2100
|
+
${pc2.yellow(`Update available: v${VERSION} \u2192 v${latest}`)}
|
|
2049
2101
|
Run: elnora update
|
|
2050
2102
|
` : `
|
|
2051
2103
|
Update available: v${VERSION} \u2192 v${latest}
|
|
@@ -2101,99 +2153,275 @@ function registerUpdateCheck() {
|
|
|
2101
2153
|
}
|
|
2102
2154
|
|
|
2103
2155
|
// src/commands/doctor.ts
|
|
2156
|
+
var EXPECTED_SKILLS = [
|
|
2157
|
+
"elnora-admin",
|
|
2158
|
+
"elnora-agent",
|
|
2159
|
+
"elnora-files",
|
|
2160
|
+
"elnora-folders",
|
|
2161
|
+
"elnora-orgs",
|
|
2162
|
+
"elnora-platform",
|
|
2163
|
+
"elnora-projects",
|
|
2164
|
+
"elnora-search",
|
|
2165
|
+
"elnora-tasks"
|
|
2166
|
+
];
|
|
2167
|
+
async function runCheck(fn) {
|
|
2168
|
+
try {
|
|
2169
|
+
return await fn();
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
return { status: "fail", msg: `${e instanceof Error ? e.message : String(e)}` };
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
async function checkApiReachable() {
|
|
2175
|
+
const start = Date.now();
|
|
2176
|
+
const res = await fetch("https://platform.elnora.ai/health", { signal: AbortSignal.timeout(5e3) });
|
|
2177
|
+
const ms = Date.now() - start;
|
|
2178
|
+
if (res.ok) return { status: "pass", msg: `API reachable platform.elnora.ai (${res.status} OK, ${ms}ms)` };
|
|
2179
|
+
return { status: "fail", msg: `API unreachable platform.elnora.ai (HTTP ${res.status})` };
|
|
2180
|
+
}
|
|
2181
|
+
async function checkAuthenticated() {
|
|
2182
|
+
const key = resolveApiKey();
|
|
2183
|
+
const client = new ElnoraApiClient(key);
|
|
2184
|
+
const result = await client.get("projects", {
|
|
2185
|
+
queryParams: { page: 1, pageSize: 1 }
|
|
2186
|
+
});
|
|
2187
|
+
const count = result?.totalCount ?? result?.items?.length ?? 0;
|
|
2188
|
+
return { status: "pass", msg: `Authenticated profile: default (${count} projects accessible)` };
|
|
2189
|
+
}
|
|
2190
|
+
async function checkVersionCurrent() {
|
|
2191
|
+
try {
|
|
2192
|
+
const res = await fetch("https://registry.npmjs.org/@elnora-ai/cli/latest", {
|
|
2193
|
+
signal: AbortSignal.timeout(3e3)
|
|
2194
|
+
});
|
|
2195
|
+
if (!res.ok) return { status: "pass", msg: `Version v${VERSION} (registry check failed)` };
|
|
2196
|
+
const data = await res.json();
|
|
2197
|
+
const latest = data.version ?? "unknown";
|
|
2198
|
+
if (latest === VERSION || latest === "unknown" || !isNewerVersion(latest, VERSION)) {
|
|
2199
|
+
return { status: "pass", msg: `Version current v${VERSION} (latest: v${latest})` };
|
|
2200
|
+
}
|
|
2201
|
+
return { status: "warn", msg: `Update available v${VERSION} \u2192 v${latest}` };
|
|
2202
|
+
} catch {
|
|
2203
|
+
return { status: "pass", msg: `Version v${VERSION} (registry unreachable)` };
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function checkConfigPermissions() {
|
|
2207
|
+
if (process.platform === "win32") {
|
|
2208
|
+
return { status: "pass", msg: "Config permissions (skipped on Windows)" };
|
|
2209
|
+
}
|
|
2210
|
+
if (!existsSync4(PROFILES_FILE)) {
|
|
2211
|
+
return { status: "warn", msg: "Config permissions ~/.elnora/profiles.toml not found" };
|
|
2212
|
+
}
|
|
2213
|
+
const mode = statSync(PROFILES_FILE).mode & 511;
|
|
2214
|
+
if (mode === 384) {
|
|
2215
|
+
return { status: "pass", msg: `Config permissions ${PROFILES_FILE} (0600)` };
|
|
2216
|
+
}
|
|
2217
|
+
return { status: "warn", msg: `Config permissions ${PROFILES_FILE} (0${mode.toString(8)}) \u2014 should be 0600` };
|
|
2218
|
+
}
|
|
2219
|
+
async function checkAiServerReachable() {
|
|
2220
|
+
const start = Date.now();
|
|
2221
|
+
const res = await fetch(`${AI_SERVER_URL}/health`, { signal: AbortSignal.timeout(5e3) });
|
|
2222
|
+
const ms = Date.now() - start;
|
|
2223
|
+
const host = new URL(AI_SERVER_URL).hostname;
|
|
2224
|
+
if (res.ok) return { status: "pass", msg: `AI Server reachable ${host} (${res.status} OK, ${ms}ms)` };
|
|
2225
|
+
return { status: "fail", msg: `AI Server unreachable ${host} (HTTP ${res.status})` };
|
|
2226
|
+
}
|
|
2227
|
+
function checkPathConfigured() {
|
|
2228
|
+
const installDirs = [join4(homedir4(), ".local", "bin"), join4(homedir4(), ".elnora", "bin")];
|
|
2229
|
+
const pathVar = process.env.PATH ?? "";
|
|
2230
|
+
const entries = pathVar.split(delimiter).filter(Boolean);
|
|
2231
|
+
const matched = installDirs.find((d) => entries.includes(d));
|
|
2232
|
+
if (matched) {
|
|
2233
|
+
return { status: "pass", msg: `PATH configured ${matched}` };
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
status: "warn",
|
|
2237
|
+
msg: "PATH configured standard install dirs not in PATH (OK if installed via npm/brew)"
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
function checkClaudePlugin() {
|
|
2241
|
+
if (!existsSync4(CLAUDE_DIR)) {
|
|
2242
|
+
return { status: "skip", msg: "Plugin enabled (Claude Code not installed)" };
|
|
2243
|
+
}
|
|
2244
|
+
if (!existsSync4(CLAUDE_SETTINGS_FILE)) {
|
|
2245
|
+
return { status: "fail", msg: "Plugin enabled settings.json not found \u2014 run 'elnora setup claude'" };
|
|
2246
|
+
}
|
|
2247
|
+
let settings;
|
|
2248
|
+
try {
|
|
2249
|
+
settings = JSON.parse(readFileSync4(CLAUDE_SETTINGS_FILE, "utf-8"));
|
|
2250
|
+
} catch (e) {
|
|
2251
|
+
return {
|
|
2252
|
+
status: "fail",
|
|
2253
|
+
msg: `Plugin enabled settings.json unreadable (${e instanceof Error ? e.message : "parse error"})`
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
const raw = settings.enabledPlugins;
|
|
2257
|
+
const enabledPlugins = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
2258
|
+
const isEnabled = enabledPlugins[PLUGIN_ID] === true;
|
|
2259
|
+
const legacyKeys = Object.keys(enabledPlugins).filter((k) => k.startsWith("elnora@") && k !== PLUGIN_ID);
|
|
2260
|
+
if (isEnabled && legacyKeys.length === 0) {
|
|
2261
|
+
return { status: "pass", msg: `Plugin enabled ${PLUGIN_ID}` };
|
|
2262
|
+
}
|
|
2263
|
+
if (isEnabled && legacyKeys.length > 0) {
|
|
2264
|
+
return {
|
|
2265
|
+
status: "warn",
|
|
2266
|
+
msg: `Plugin enabled ${PLUGIN_ID} \u2014 but stale legacy entries also present: ${legacyKeys.join(", ")}`
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (legacyKeys.length > 0) {
|
|
2270
|
+
return {
|
|
2271
|
+
status: "fail",
|
|
2272
|
+
msg: `Plugin enabled found legacy entries ${legacyKeys.join(", ")} but not ${PLUGIN_ID} \u2014 run 'elnora setup claude'`
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
return { status: "fail", msg: "Plugin enabled not found \u2014 run 'elnora setup claude'" };
|
|
2276
|
+
}
|
|
2277
|
+
function checkSkillsInstalled() {
|
|
2278
|
+
if (!existsSync4(CLAUDE_DIR)) {
|
|
2279
|
+
return { status: "skip", msg: "Skills installed (Claude Code not installed)" };
|
|
2280
|
+
}
|
|
2281
|
+
const skillsDir = join4(CLAUDE_DIR, "plugins", "marketplaces", MARKETPLACE_NAME, "elnora", "skills");
|
|
2282
|
+
if (!existsSync4(skillsDir)) {
|
|
2283
|
+
return {
|
|
2284
|
+
status: "warn",
|
|
2285
|
+
msg: "Skills installed marketplace not cloned yet \u2014 restart Claude Code to trigger clone"
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
const found = EXPECTED_SKILLS.filter((s) => existsSync4(join4(skillsDir, s, "SKILL.md")));
|
|
2289
|
+
if (found.length === EXPECTED_SKILLS.length) {
|
|
2290
|
+
return { status: "pass", msg: `Skills installed ${found.length} skills` };
|
|
2291
|
+
}
|
|
2292
|
+
const missing = EXPECTED_SKILLS.filter((s) => !found.includes(s));
|
|
2293
|
+
return {
|
|
2294
|
+
status: "fail",
|
|
2295
|
+
msg: `Skills installed ${found.length}/${EXPECTED_SKILLS.length} \u2014 missing: ${missing.join(", ")}`
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function checkPluginVersion() {
|
|
2299
|
+
if (!existsSync4(CLAUDE_DIR)) {
|
|
2300
|
+
return { status: "skip", msg: "Plugin version (Claude Code not installed)" };
|
|
2301
|
+
}
|
|
2302
|
+
const pluginJsonPath = join4(
|
|
2303
|
+
CLAUDE_DIR,
|
|
2304
|
+
"plugins",
|
|
2305
|
+
"marketplaces",
|
|
2306
|
+
MARKETPLACE_NAME,
|
|
2307
|
+
"elnora",
|
|
2308
|
+
".claude-plugin",
|
|
2309
|
+
"plugin.json"
|
|
2310
|
+
);
|
|
2311
|
+
if (!existsSync4(pluginJsonPath)) {
|
|
2312
|
+
return { status: "skip", msg: "Plugin version (plugin not cloned yet)" };
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
const pj = JSON.parse(readFileSync4(pluginJsonPath, "utf-8"));
|
|
2316
|
+
const pluginVersion = pj.version ?? "unknown";
|
|
2317
|
+
if (pluginVersion === VERSION) {
|
|
2318
|
+
return { status: "pass", msg: `Plugin version v${pluginVersion} (matches CLI)` };
|
|
2319
|
+
}
|
|
2320
|
+
return {
|
|
2321
|
+
status: "warn",
|
|
2322
|
+
msg: `Plugin version v${pluginVersion} (CLI is v${VERSION}) \u2014 restart Claude Code to refresh`
|
|
2323
|
+
};
|
|
2324
|
+
} catch (e) {
|
|
2325
|
+
return {
|
|
2326
|
+
status: "fail",
|
|
2327
|
+
msg: `Plugin version plugin.json unreadable (${e instanceof Error ? e.message : "parse error"})`
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
async function checkMcpReachable() {
|
|
2332
|
+
const start = Date.now();
|
|
2333
|
+
try {
|
|
2334
|
+
const res = await fetch("https://mcp.elnora.ai/mcp", {
|
|
2335
|
+
method: "HEAD",
|
|
2336
|
+
signal: AbortSignal.timeout(5e3)
|
|
2337
|
+
});
|
|
2338
|
+
const ms = Date.now() - start;
|
|
2339
|
+
if (res.status < 500) {
|
|
2340
|
+
return { status: "pass", msg: `Server reachable mcp.elnora.ai (${res.status}, ${ms}ms)` };
|
|
2341
|
+
}
|
|
2342
|
+
return { status: "fail", msg: `Server unreachable mcp.elnora.ai (HTTP ${res.status})` };
|
|
2343
|
+
} catch (e) {
|
|
2344
|
+
return {
|
|
2345
|
+
status: "fail",
|
|
2346
|
+
msg: `Server unreachable mcp.elnora.ai (${e instanceof Error ? e.message : "network error"})`
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2104
2350
|
function addDoctorCommand(program2) {
|
|
2105
2351
|
program2.command("doctor").description("Check Elnora CLI configuration and connectivity").action(async () => {
|
|
2106
2352
|
const color2 = isColorEnabled();
|
|
2107
|
-
const ok2 = (msg) => color2 ? ` ${
|
|
2108
|
-
const fail2 = (msg) => color2 ? ` ${
|
|
2109
|
-
const warn = (msg) => color2 ? ` ${
|
|
2353
|
+
const ok2 = (msg) => color2 ? ` ${pc3.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
|
|
2354
|
+
const fail2 = (msg) => color2 ? ` ${pc3.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
|
|
2355
|
+
const warn = (msg) => color2 ? ` ${pc3.yellow("!")} ${msg}` : ` ! ${msg}`;
|
|
2356
|
+
const skip = (msg) => color2 ? ` ${pc3.dim("\u25CB")} ${msg}` : ` \u25CB ${msg}`;
|
|
2357
|
+
const section = (name) => color2 ? pc3.bold(` ${name}`) : ` ${name}`;
|
|
2358
|
+
const render = (r) => {
|
|
2359
|
+
switch (r.status) {
|
|
2360
|
+
case "pass":
|
|
2361
|
+
return ok2(r.msg);
|
|
2362
|
+
case "fail":
|
|
2363
|
+
return fail2(r.msg);
|
|
2364
|
+
case "warn":
|
|
2365
|
+
return warn(r.msg);
|
|
2366
|
+
case "skip":
|
|
2367
|
+
return skip(r.msg);
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2110
2370
|
console.error(`
|
|
2111
2371
|
Elnora CLI v${VERSION}
|
|
2112
2372
|
`);
|
|
2113
2373
|
let passed = 0;
|
|
2374
|
+
let failed = 0;
|
|
2375
|
+
let warnings = 0;
|
|
2114
2376
|
let total = 0;
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
const ms = Date.now() - start;
|
|
2120
|
-
if (res.ok) {
|
|
2121
|
-
console.error(ok2(`API reachable platform.elnora.ai (${res.status} OK, ${ms}ms)`));
|
|
2377
|
+
const tally = (r) => {
|
|
2378
|
+
total++;
|
|
2379
|
+
if (r.status === "pass" || r.status === "skip") passed++;
|
|
2380
|
+
else if (r.status === "warn") {
|
|
2122
2381
|
passed++;
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
passed++;
|
|
2139
|
-
} catch (e) {
|
|
2140
|
-
console.error(fail2(`Authentication failed ${e instanceof Error ? e.message : "no API key"}`));
|
|
2141
|
-
}
|
|
2142
|
-
total++;
|
|
2143
|
-
try {
|
|
2144
|
-
const res = await fetch("https://registry.npmjs.org/@elnora-ai/cli/latest", {
|
|
2145
|
-
signal: AbortSignal.timeout(3e3)
|
|
2146
|
-
});
|
|
2147
|
-
if (res.ok) {
|
|
2148
|
-
const data = await res.json();
|
|
2149
|
-
const latest = data.version ?? "unknown";
|
|
2150
|
-
if (latest === VERSION || latest === "unknown" || !isNewerVersion(latest, VERSION)) {
|
|
2151
|
-
console.error(ok2(`Version current v${VERSION} (latest: v${latest})`));
|
|
2152
|
-
} else {
|
|
2153
|
-
console.error(warn(`Update available v${VERSION} \u2192 v${latest}`));
|
|
2154
|
-
}
|
|
2155
|
-
passed++;
|
|
2156
|
-
} else {
|
|
2157
|
-
console.error(ok2(`Version v${VERSION} (registry check failed)`));
|
|
2158
|
-
passed++;
|
|
2159
|
-
}
|
|
2160
|
-
} catch {
|
|
2161
|
-
console.error(ok2(`Version v${VERSION} (registry unreachable)`));
|
|
2162
|
-
passed++;
|
|
2382
|
+
warnings++;
|
|
2383
|
+
} else failed++;
|
|
2384
|
+
};
|
|
2385
|
+
console.error(section("CLI"));
|
|
2386
|
+
const cliChecks = [
|
|
2387
|
+
await runCheck(() => checkApiReachable()),
|
|
2388
|
+
await runCheck(() => checkAuthenticated()),
|
|
2389
|
+
await runCheck(() => checkVersionCurrent()),
|
|
2390
|
+
await runCheck(() => checkConfigPermissions()),
|
|
2391
|
+
await runCheck(() => checkAiServerReachable()),
|
|
2392
|
+
await runCheck(() => checkPathConfigured())
|
|
2393
|
+
];
|
|
2394
|
+
for (const r of cliChecks) {
|
|
2395
|
+
console.error(render(r));
|
|
2396
|
+
tally(r);
|
|
2163
2397
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
passed++;
|
|
2175
|
-
} else {
|
|
2176
|
-
console.error(warn(`Config permissions ${PROFILES_FILE} (0${mode.toString(8)}) \u2014 should be 0600`));
|
|
2177
|
-
}
|
|
2398
|
+
console.error("");
|
|
2399
|
+
console.error(section("Claude Code"));
|
|
2400
|
+
const claudeChecks = [
|
|
2401
|
+
await runCheck(() => checkClaudePlugin()),
|
|
2402
|
+
await runCheck(() => checkSkillsInstalled()),
|
|
2403
|
+
await runCheck(() => checkPluginVersion())
|
|
2404
|
+
];
|
|
2405
|
+
for (const r of claudeChecks) {
|
|
2406
|
+
console.error(render(r));
|
|
2407
|
+
tally(r);
|
|
2178
2408
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
console.error(ok2(`AI Server reachable ${new URL(AI_SERVER_URL).hostname} (${res.status} OK, ${ms}ms)`));
|
|
2186
|
-
passed++;
|
|
2187
|
-
} else console.error(fail2(`AI Server unreachable ${new URL(AI_SERVER_URL).hostname} (HTTP ${res.status})`));
|
|
2188
|
-
} catch {
|
|
2189
|
-
console.error(fail2(`AI Server unreachable ${AI_SERVER_URL}`));
|
|
2409
|
+
console.error("");
|
|
2410
|
+
console.error(section("MCP"));
|
|
2411
|
+
const mcpChecks = [await runCheck(() => checkMcpReachable())];
|
|
2412
|
+
for (const r of mcpChecks) {
|
|
2413
|
+
console.error(render(r));
|
|
2414
|
+
tally(r);
|
|
2190
2415
|
}
|
|
2191
2416
|
console.error("");
|
|
2192
|
-
if (
|
|
2417
|
+
if (failed === 0 && warnings === 0) {
|
|
2193
2418
|
console.error("All checks passed.");
|
|
2419
|
+
} else if (failed === 0) {
|
|
2420
|
+
console.error(`${passed}/${total} checks passed (${warnings} warning${warnings === 1 ? "" : "s"}).`);
|
|
2194
2421
|
} else {
|
|
2195
|
-
|
|
2196
|
-
|
|
2422
|
+
const warnSuffix = warnings > 0 ? ` (${warnings} warning${warnings === 1 ? "" : "s"})` : "";
|
|
2423
|
+
console.error(`${passed}/${total} checks passed \u2014 ${failed} failed${warnSuffix}.`);
|
|
2424
|
+
if (!existsSync4(PROFILES_FILE)) console.error("\nRun: elnora auth login");
|
|
2197
2425
|
}
|
|
2198
2426
|
});
|
|
2199
2427
|
}
|
|
@@ -2586,7 +2814,7 @@ var filesUpdate = {
|
|
|
2586
2814
|
|
|
2587
2815
|
// src/commands/files/upload.ts
|
|
2588
2816
|
init_client();
|
|
2589
|
-
import { readFileSync as
|
|
2817
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
2590
2818
|
import { basename } from "node:path";
|
|
2591
2819
|
import { z as z37 } from "zod";
|
|
2592
2820
|
var inputSchema36 = z37.object({
|
|
@@ -2602,7 +2830,7 @@ var filesUpload = {
|
|
|
2602
2830
|
inputSchema: inputSchema36,
|
|
2603
2831
|
outputSchema: z37.any(),
|
|
2604
2832
|
async execute(input, ctx) {
|
|
2605
|
-
const fileBuffer =
|
|
2833
|
+
const fileBuffer = readFileSync5(input.filePath);
|
|
2606
2834
|
const fileName = input.fileName ?? basename(input.filePath);
|
|
2607
2835
|
const contentType = input.contentType ?? "application/octet-stream";
|
|
2608
2836
|
const fileSize = statSync2(input.filePath).size;
|
|
@@ -2633,7 +2861,7 @@ var filesUpload = {
|
|
|
2633
2861
|
// src/commands/files/upload-batch.ts
|
|
2634
2862
|
init_client();
|
|
2635
2863
|
init_errors();
|
|
2636
|
-
import { readFileSync as
|
|
2864
|
+
import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
|
|
2637
2865
|
import { basename as basename2 } from "node:path";
|
|
2638
2866
|
import { z as z38 } from "zod";
|
|
2639
2867
|
var inputSchema37 = z38.object({
|
|
@@ -2662,7 +2890,7 @@ var filesUploadBatch = {
|
|
|
2662
2890
|
for (const filePath of paths) {
|
|
2663
2891
|
const fileName = basename2(filePath);
|
|
2664
2892
|
try {
|
|
2665
|
-
const fileBuffer =
|
|
2893
|
+
const fileBuffer = readFileSync6(filePath);
|
|
2666
2894
|
const contentType = "application/octet-stream";
|
|
2667
2895
|
const fileSize = statSync3(filePath).size;
|
|
2668
2896
|
const uploadInfo = await ctx.client.post("file_upload", {
|
|
@@ -4132,58 +4360,6 @@ init_profiles();
|
|
|
4132
4360
|
|
|
4133
4361
|
// src/lib/setup/claude.ts
|
|
4134
4362
|
import { existsSync as existsSync5 } from "node:fs";
|
|
4135
|
-
|
|
4136
|
-
// src/lib/setup/common.ts
|
|
4137
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4138
|
-
import { homedir as homedir3 } from "node:os";
|
|
4139
|
-
import { join as join3 } from "node:path";
|
|
4140
|
-
import pc3 from "picocolors";
|
|
4141
|
-
var HOME = homedir3();
|
|
4142
|
-
var ELNORA_DIR = join3(HOME, ".elnora");
|
|
4143
|
-
var CLAUDE_DIR = join3(HOME, ".claude");
|
|
4144
|
-
var CLAUDE_MARKETPLACES_FILE = join3(CLAUDE_DIR, "plugins", "known_marketplaces.json");
|
|
4145
|
-
var CLAUDE_SETTINGS_FILE = join3(CLAUDE_DIR, "settings.json");
|
|
4146
|
-
var CURSOR_DIR = join3(HOME, ".cursor");
|
|
4147
|
-
var CURSOR_MCP_GLOBAL = join3(HOME, ".cursor", "mcp.json");
|
|
4148
|
-
var VSCODE_DIR = join3(HOME, ".vscode");
|
|
4149
|
-
var CODEX_DIR = join3(HOME, ".codex");
|
|
4150
|
-
var MARKETPLACE_NAME = "elnora-ai";
|
|
4151
|
-
var MARKETPLACE_REPO = "Elnora-AI/elnora-plugins";
|
|
4152
|
-
var PLUGIN_ID = `elnora@${MARKETPLACE_NAME}`;
|
|
4153
|
-
var LEGACY_PLUGIN_IDS = ["elnora@elnora-local"];
|
|
4154
|
-
var LEGACY_MARKETPLACE_NAMES = ["elnora-local"];
|
|
4155
|
-
function readJsonFile(path) {
|
|
4156
|
-
try {
|
|
4157
|
-
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
4158
|
-
} catch {
|
|
4159
|
-
return {};
|
|
4160
|
-
}
|
|
4161
|
-
}
|
|
4162
|
-
function writeJsonFile(path, data) {
|
|
4163
|
-
mkdirSync3(join3(path, ".."), { recursive: true });
|
|
4164
|
-
writeFileSync3(path, `${JSON.stringify(data, null, 2)}
|
|
4165
|
-
`, "utf-8");
|
|
4166
|
-
}
|
|
4167
|
-
var color = isColorEnabled();
|
|
4168
|
-
function ok(msg) {
|
|
4169
|
-
return color ? ` ${pc3.green("\u2713")} ${msg}` : ` \u2713 ${msg}`;
|
|
4170
|
-
}
|
|
4171
|
-
function fail(msg) {
|
|
4172
|
-
return color ? ` ${pc3.red("\u2717")} ${msg}` : ` \u2717 ${msg}`;
|
|
4173
|
-
}
|
|
4174
|
-
function info(msg) {
|
|
4175
|
-
return color ? ` ${pc3.dim(msg)}` : ` ${msg}`;
|
|
4176
|
-
}
|
|
4177
|
-
function detectPlatforms() {
|
|
4178
|
-
return [
|
|
4179
|
-
{ name: "claude", label: "Claude Code", installed: existsSync4(CLAUDE_DIR) },
|
|
4180
|
-
{ name: "cursor", label: "Cursor", installed: existsSync4(CURSOR_DIR) },
|
|
4181
|
-
{ name: "vscode", label: "VS Code", installed: existsSync4(VSCODE_DIR) },
|
|
4182
|
-
{ name: "codex", label: "OpenAI Codex", installed: existsSync4(CODEX_DIR) }
|
|
4183
|
-
];
|
|
4184
|
-
}
|
|
4185
|
-
|
|
4186
|
-
// src/lib/setup/claude.ts
|
|
4187
4363
|
function setupClaude() {
|
|
4188
4364
|
if (!existsSync5(CLAUDE_DIR)) {
|
|
4189
4365
|
console.error(fail("Claude Code not found (~/.claude/ does not exist)"));
|
|
@@ -4223,8 +4399,8 @@ function setupClaude() {
|
|
|
4223
4399
|
|
|
4224
4400
|
// src/lib/setup/codex.ts
|
|
4225
4401
|
import { existsSync as existsSync6 } from "node:fs";
|
|
4226
|
-
import { join as
|
|
4227
|
-
var CODEX_MCP_FILE =
|
|
4402
|
+
import { join as join5 } from "node:path";
|
|
4403
|
+
var CODEX_MCP_FILE = join5(CODEX_DIR, "mcp.json");
|
|
4228
4404
|
function setupCodex(apiKey) {
|
|
4229
4405
|
if (!existsSync6(CODEX_DIR)) {
|
|
4230
4406
|
console.error(fail("Codex CLI not found (~/.codex/ does not exist)"));
|
|
@@ -4280,8 +4456,8 @@ function setupCursor(apiKey) {
|
|
|
4280
4456
|
|
|
4281
4457
|
// src/lib/setup/vscode.ts
|
|
4282
4458
|
import { existsSync as existsSync8 } from "node:fs";
|
|
4283
|
-
import { join as
|
|
4284
|
-
var VSCODE_MCP_GLOBAL =
|
|
4459
|
+
import { join as join6 } from "node:path";
|
|
4460
|
+
var VSCODE_MCP_GLOBAL = join6(HOME, ".vscode", "mcp.json");
|
|
4285
4461
|
function setupVscode(apiKey) {
|
|
4286
4462
|
if (!existsSync8(VSCODE_DIR)) {
|
|
4287
4463
|
console.error(fail("VS Code not found (~/.vscode/ does not exist)"));
|
|
@@ -4384,6 +4560,10 @@ function addSetupCommand(program2) {
|
|
|
4384
4560
|
const platforms = installed.map((p) => p.name);
|
|
4385
4561
|
runSetup(platforms, opts.profile);
|
|
4386
4562
|
});
|
|
4563
|
+
program2.command("setup-claude").description("Set up Claude Code (alias for 'setup claude')").option("--profile <name>", "API key profile to use", "default").action((opts) => {
|
|
4564
|
+
console.error("");
|
|
4565
|
+
runSetup(["claude"], opts.profile);
|
|
4566
|
+
});
|
|
4387
4567
|
}
|
|
4388
4568
|
|
|
4389
4569
|
// src/commands/tasks/archive.ts
|
|
@@ -4871,9 +5051,9 @@ function registerTaskCommands() {
|
|
|
4871
5051
|
init_config();
|
|
4872
5052
|
import { execFileSync } from "node:child_process";
|
|
4873
5053
|
import { createHash } from "node:crypto";
|
|
4874
|
-
import { chmodSync as chmodSync2, copyFileSync, createWriteStream, mkdirSync as mkdirSync4, readFileSync as
|
|
5054
|
+
import { chmodSync as chmodSync2, copyFileSync, createWriteStream, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync } from "node:fs";
|
|
4875
5055
|
import { tmpdir } from "node:os";
|
|
4876
|
-
import { join as
|
|
5056
|
+
import { join as join7 } from "node:path";
|
|
4877
5057
|
import { Readable } from "node:stream";
|
|
4878
5058
|
import { pipeline } from "node:stream/promises";
|
|
4879
5059
|
import pc5 from "picocolors";
|
|
@@ -4940,9 +5120,9 @@ async function installBinaryUpdate(latest) {
|
|
|
4940
5120
|
const { target, ext } = platformInfo;
|
|
4941
5121
|
const binaryUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.${ext}`;
|
|
4942
5122
|
const checksumUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${latest}/${target}.sha256`;
|
|
4943
|
-
const tmp =
|
|
5123
|
+
const tmp = join7(tmpdir(), `elnora-update-${Date.now()}`);
|
|
4944
5124
|
mkdirSync4(tmp, { recursive: true });
|
|
4945
|
-
const archivePath =
|
|
5125
|
+
const archivePath = join7(tmp, `${target}.${ext}`);
|
|
4946
5126
|
try {
|
|
4947
5127
|
console.error("Downloading...");
|
|
4948
5128
|
await downloadToFile(binaryUrl, archivePath);
|
|
@@ -4953,7 +5133,7 @@ async function installBinaryUpdate(latest) {
|
|
|
4953
5133
|
const checksumText = await checksumRes.text();
|
|
4954
5134
|
const expected = checksumText.split(/\s/)[0].trim();
|
|
4955
5135
|
if (expected.length === 64) {
|
|
4956
|
-
const actual = createHash("sha256").update(
|
|
5136
|
+
const actual = createHash("sha256").update(readFileSync7(archivePath)).digest("hex");
|
|
4957
5137
|
if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
4958
5138
|
console.error("Checksum verification failed. The download may be corrupted.");
|
|
4959
5139
|
process.exitCode = 1;
|
|
@@ -4968,21 +5148,21 @@ async function installBinaryUpdate(latest) {
|
|
|
4968
5148
|
console.error("Warning: Could not verify download checksum. Proceeding anyway.");
|
|
4969
5149
|
}
|
|
4970
5150
|
if (process.platform === "win32") {
|
|
4971
|
-
const extractDir =
|
|
5151
|
+
const extractDir = join7(tmp, "extracted");
|
|
4972
5152
|
execFileSync("powershell", [
|
|
4973
5153
|
"-NoProfile",
|
|
4974
5154
|
"-Command",
|
|
4975
5155
|
`Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`
|
|
4976
5156
|
]);
|
|
4977
|
-
const installDir =
|
|
5157
|
+
const installDir = join7(process.env.USERPROFILE ?? process.env.HOME ?? "", ".elnora", "bin");
|
|
4978
5158
|
mkdirSync4(installDir, { recursive: true });
|
|
4979
|
-
copyFileSync(
|
|
5159
|
+
copyFileSync(join7(extractDir, target), join7(installDir, "elnora.exe"));
|
|
4980
5160
|
} else {
|
|
4981
5161
|
execFileSync("tar", ["-xzf", archivePath, "-C", tmp]);
|
|
4982
|
-
const installDir = process.env.ELNORA_INSTALL_DIR ??
|
|
5162
|
+
const installDir = process.env.ELNORA_INSTALL_DIR ?? join7(process.env.HOME ?? "", ".local", "bin");
|
|
4983
5163
|
mkdirSync4(installDir, { recursive: true });
|
|
4984
|
-
const dest =
|
|
4985
|
-
copyFileSync(
|
|
5164
|
+
const dest = join7(installDir, "elnora");
|
|
5165
|
+
copyFileSync(join7(tmp, target), dest);
|
|
4986
5166
|
chmodSync2(dest, 493);
|
|
4987
5167
|
}
|
|
4988
5168
|
const done = color2 ? `${pc5.green("\u2713")} Updated to v${latest}` : `\u2713 Updated to v${latest}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elnora-ai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Elnora AI Platform CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"main": "dist/cli.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
+
"scripts/postinstall.mjs",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"vitest": "^4.1.2"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
57
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
56
58
|
"build": "node scripts/build-npm.mjs",
|
|
57
59
|
"build:binary": "node scripts/build.mjs --pkg",
|
|
58
60
|
"dev": "tsx src/cli.ts",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* npm postinstall — show setup instructions after `npm install -g @elnora-ai/cli`.
|
|
5
|
+
*
|
|
6
|
+
* Intentionally non-interactive: npm postinstall runs in contexts where stdin
|
|
7
|
+
* may not be available (CI, Docker, scripts). Interactive prompts belong in
|
|
8
|
+
* `elnora auth login` which the user runs manually after install.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Skip in CI or when output is suppressed
|
|
12
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS || !process.stderr.isTTY) {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const msg = `
|
|
17
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ │
|
|
19
|
+
│ Elnora CLI installed successfully! │
|
|
20
|
+
│ │
|
|
21
|
+
│ To get started, run: │
|
|
22
|
+
│ elnora auth login │
|
|
23
|
+
│ │
|
|
24
|
+
│ Set up your AI coding tools: │
|
|
25
|
+
│ elnora setup │
|
|
26
|
+
│ │
|
|
27
|
+
│ Documentation: │
|
|
28
|
+
│ https://github.com/Elnora-AI/elnora-cli │
|
|
29
|
+
│ │
|
|
30
|
+
└──────────────────────────────────────────────────────────────┘
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
console.error(msg);
|